传统网络编程
首先看一段最传统的Java网络编程。
服务端代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| public class IOServer {
public static void main(String[] args) throws IOException { ServerSocket serverSocket = new ServerSocket(8000); new Thread(() -> { while (true) { try { Socket socket = serverSocket.accept();
new Thread(() -> { int len; byte[] data = new byte[1024]; try { InputStream inputStream = socket.getInputStream(); while ((len = inputStream.read(data)) != -1) { System.out.println(new String(data, 0, len)); } } catch (IOException e) { e.printStackTrace(); } }).start();
} catch (IOException e) { e.printStackTrace(); } } }).start(); }
}
|
客户端代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class IOClient {
public static void main(String[] args) { new Thread(() -> { try { Socket socket = new Socket("127.0.0.1", 8000); while (true) { socket.getOutputStream().write((new Date() + " hello world").getBytes()); Thread.sleep(3000); } } catch (IOException | InterruptedException e) { e.printStackTrace(); } }).start(); }
}
|
可以看到,服务端启动时创建了一个线程,建立了ServerSocket
,循环的等待客户端的连接。客户端每次连接,服务端都会创建一个新的线程去处理请求,然后读取数据。
这种模型在客户端数量比较小的时候是没问题的,但是如果客户端数量较多,服务端就需要创建很多的线程,线程的创建比较消耗成本,而且线程数过多时,CPU调度导致的线程切换也会影响性能。
主要有以下三个问题:
- 创建太多线程,而线程是很宝贵的资源,同一时刻大量线程处于阻塞会浪费资源。
- 线程切换效率低下。
- IO编程中,数据传输都是以字节为单位。
为了解决上述问题,引入了NIO,即同步非阻塞IO。
NIO
在NIO中,只会创建一个线程去进行while循环,该线程监控所有的客户端连接,而不会像传统的有多少个客户端连接就创建多少个线程去进行while循环。
传统的模型中,在每一时刻,只有少数线程会有数据读写需求,而没有数据需要读写的就白白浪费了CPU资源。NIO中,通过将所有的客户端连接都注册到selector上,通过检测selector上是否有数据数据需要读写,就可以达到批量读取数据的目的。
这样,就解决了创建太多线程浪费资源和CPU频繁切换线程导致的性能问题。而传统IO是面向字节流,读取数据后需要自己缓存,流中就不存在该数据。而NIO的读写是面向Buffer的,解决了数据只能用字节传输的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76
| public class NIOServer {
public static void main(String[] args) throws IOException { Selector serverSelector = Selector.open(); Selector clientSelector = Selector.open();
new Thread(() -> { try { ServerSocketChannel listenerChannel = ServerSocketChannel.open(); listenerChannel.socket().bind(new InetSocketAddress(8000)); listenerChannel.configureBlocking(false); listenerChannel.register(serverSelector, SelectionKey.OP_ACCEPT);
while (true) { if (serverSelector.select(1) > 0) { Set<SelectionKey> set = serverSelector.selectedKeys(); Iterator<SelectionKey> keyIterator = set.iterator(); while (keyIterator.hasNext()) { SelectionKey selectionKey = keyIterator.next(); if (selectionKey.isAcceptable()) { try { SocketChannel clientChannel = ((ServerSocketChannel) selectionKey.channel()).accept(); clientChannel.configureBlocking(false); clientChannel.register(clientSelector, SelectionKey.OP_READ); } finally { keyIterator.remove(); } } } } } } catch (IOException e) { e.printStackTrace(); } }).start();
new Thread(() -> { try { while (true) { if (clientSelector.select(1) > 0) { Set<SelectionKey> set = clientSelector.selectedKeys(); Iterator<SelectionKey> keyIterator = set.iterator(); while (keyIterator.hasNext()) { SelectionKey key = keyIterator.next(); if (key.isReadable()) { try { SocketChannel clientChannel = (SocketChannel) key.channel(); ByteBuffer byteBuffer = ByteBuffer.allocate(1024); clientChannel.read(byteBuffer); byteBuffer.flip(); System.out.println(Charset.defaultCharset().newDecoder().decode(byteBuffer).toString()); } finally { keyIterator.remove(); key.interestOps(SelectionKey.OP_READ); } } } } } } catch (Exception ignored) {
} }).start(); }
}
|
NIO编程的核心思路:
- NIO 模型中通常会有两个线程,每个线程绑定一个轮询器 selector ,在上面的例子中 serverSelector负责轮询是否有新的连接,clientSelector负责轮询连接是否有数据可读。
- 服务端监测到新的连接之后,不再创建一个新的线程,而是直接将新连接绑定到clientSelector上, 这样就不用 IO 模型中 每个连接一个while循环。
- clientSelector被一个 while 死循环包裹着,如果在某一时刻有多条连接有数据可读,那么通过 clientSelector.select(1)方法可以轮询出来,进而批量处理。
- 数据读写面向Buffer。
但是不难看出,整个编码变得极其复杂。所有,就有了Netty。