1. 阻塞与非阻塞
传统的 IO 流都是阻塞式的。也就是说,当一个线程调用 read() 或 write()时,该线程被阻塞,直到有一些数据被读取或写入,该线程在此期间不能执行其他任务。因此,在完成网络通信进行 IO 操作时,由于线程会阻塞,所以服务器端必须为每个客户端都提供一个独立的线程进行处理,当服务器端需要处理大量客户端时,性能急剧下降。
Java NIO 是非阻塞模式的。当线程从某通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。因此, NIO 可以让服务器端使用一个或有限几个线程来同时处理连接到服务器端的所有客户端。
下面演示了基于阻塞式的客户端和服务器之间的网络通信,具体代码如下所示:
客户端代码:
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
| @Test public void client() throws IOException { SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898)); FileChannel fChannel = FileChannel.open(Paths.get("1.txt"), StandardOpenOption.READ); ByteBuffer buf = ByteBuffer.allocate(1024); while (fChannel.read(buf) != -1) { buf.flip(); sChannel.write(buf); buf.clear(); } sChannel.shutdownOutput(); int len = -1; while ((len = sChannel.read(buf)) != -1) { buf.flip(); System.out.println(new String(buf.array(), 0, len)); buf.clear(); } fChannel.close(); sChannel.close(); }
|
服务器端代码:
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
| @Test public void server() throws IOException { ServerSocketChannel ssChannel = ServerSocketChannel.open(); FileChannel outChannel = FileChannel.open(Paths.get("2.txt"), StandardOpenOption.WRITE, StandardOpenOption.CREATE); ssChannel.bind(new InetSocketAddress(9898)); SocketChannel sChannel = ssChannel.accept(); ByteBuffer buf = ByteBuffer.allocate(1024); while (sChannel.read(buf) != -1) { buf.flip(); outChannel.write(buf); buf.clear(); } sChannel.shutdownInput(); buf.put("服务器成功接收到客户端发来的数据!".getBytes()); buf.flip(); sChannel.write(buf); outChannel.close(); sChannel.close(); ssChannel.close(); }
|
需要强调的是,如果注释掉客户端和服务器代码中的shutdownOutput和shutdownInput方法后,此时运行会一直处于阻塞状态。因为服务器无法确定客户端输出数据是否已经结束,也就是无法确定是否还需要执行sChannel.write(buf);该调用,那么运行就会一直处于阻塞状态。服务器端同理。
shutdownOutput 该方法的作用就是断开Socket的输出流,但是不关闭通道。调用该方法之后,我们无法再向通道中写入数据了,也就是表明客户端的输出已经结束。shutdownInput方法同理。
2. 选择器的使用
前面我们已经说了,NIO是非阻塞模式的,而非阻塞的实现的核心就是选择器。选择器(Selector) 是 SelectableChannle 对象的多路复用器, Selector 可以同时监控多个 SelectableChannel 的 IO 状况,也就是说,利用 Selector可使一个单独的线程管理多个 Channel。
SelectableChannle 的结构如下图:
下面列出来了Selector的一些常用的方法及其描述:
另外,我们可以通过Selector的静态方法open来获取选择器:
1
| Selector selector = Selector.open();
|
并可以使用通道的register方法向选择器注册通道:
1
| ssChannel.register(selector, SelectionKey.OP_ACCEPT);
|
ssChannel为SocketServetChannel的一个对象,调用register方法时不仅指定了选择器,同时还指定了SelectKey.OP_ACCEPT,即选择器对通道指定的监听事件,这里为接收事件。
SelectionKey: 表示 SelectableChannel 和 Selector 之间的注册关系。每次向选择器注册通道时就会选择一个事件(选择键)。SelectionKey可以监听的事件类型的常量表示如下:
- 读 : SelectionKey.OP_READ
- 写 : SelectionKey.OP_WRITE
- 连接 : SelectionKey.OP_CONNECT
- 接收 : SelectionKey.OP_ACCEPT
SelectKey的一些常用方法及其描述如下图示:
2.1 示例
下面的示例使用NIO实现了多人聊天室的功能。服务器通道使用了configureBlocking(false)将通道切换为非阻塞模式,并向选择器注册该通道时指定了监听服务器接收的事件,然后轮询的使用Selector的select方法来判断这些接收事件是否就绪。这些操作表明了只有服务器接收到监听事件时(select方法返回值大于0)才去处理接收事件,此时线程可以去干别的事而不用一直等待接收监听。
服务器端的代码如下所示:
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
| @Test public void server() throws IOException { ServerSocketChannel ssChannel = ServerSocketChannel.open(); ssChannel.configureBlocking(false); ssChannel.bind(new InetSocketAddress(9898)); Selector selector = Selector.open(); ssChannel.register(selector, SelectionKey.OP_ACCEPT); while (selector.select() > 0) { Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); if (key.isAcceptable()) { SocketChannel sChannel = ssChannel.accept(); sChannel.configureBlocking(false); sChannel.register(selector, SelectionKey.OP_READ); } else if (key.isReadable()) { SocketChannel sChannel = (SocketChannel) key.channel(); ByteBuffer buf = ByteBuffer.allocate(1024); int len = -1; while ((len = sChannel.read(buf)) > 0) { buf.flip(); System.out.println(new String(buf.array(), 0, len)); buf.clear(); } } it.remove(); } } }
|
客户端的代码如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| @Test public void client() throws IOException { SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9898)); sChannel.configureBlocking(false); ByteBuffer buf = ByteBuffer.allocate(1024); Scanner scan = new Scanner(System.in); while (scan.hasNext()) { String content = scan.nextLine(); if (content.equals("exit")) break; buf.put((new Date() + ":" + content).getBytes()); buf.flip(); sChannel.write(buf); buf.clear(); } scan.close(); sChannel.close(); }
|
当多个用户输出数据时,服务器端都会接收到数据并打印在控制台上:
1 2
| Mon Apr 01 11:11:12 CST 2019:你好 Mon Apr 01 11:11:20 CST 2019:hello
|
3. DatagramChannel 数据传输
Java NIO中的DatagramChannel是一个能收发UDP包的通道。下面使用该通道实现多人聊天室功能,具体代码如下所示:
接收端代码:
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
| @Test public void receive() throws IOException { DatagramChannel dc = DatagramChannel.open(); dc.configureBlocking(false); dc.bind(new InetSocketAddress(9898)); Selector selector = Selector.open(); dc.register(selector, SelectionKey.OP_READ);
while (selector.select() > 0) { Iterator<SelectionKey> it = selector.selectedKeys().iterator(); while (it.hasNext()) { SelectionKey key = it.next(); if (key.isReadable()) { ByteBuffer buf = ByteBuffer.allocate(1024); dc.receive(buf); buf.flip(); System.out.println(new String(buf.array(), 0, buf.limit())); buf.clear(); it.remove(); } } }
}
|
发送端代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| @Test public void send() throws IOException { DatagramChannel dc = DatagramChannel.open(); dc.configureBlocking(false); ByteBuffer buf = ByteBuffer.allocate(1024); Scanner scan = new Scanner(System.in);
while (scan.hasNext()) { String content = scan.next(); buf.put((new Date().toString() + "\n" + content).getBytes()); buf.flip(); dc.send(buf, new InetSocketAddress("127.0.0.1", 9898)); buf.clear(); } scan.close(); dc.close(); }
|
4. Pipe 管道数据传输
Java NIO 管道是2个线程之间的单向数据连接。Pipe有一个source通道和一个sink通道。数据会被写到sink通道,从source通道读取。
下面演示一下管道之间的数据传输示例:
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
| public class TestPipe { private Pipe pipe = null; public TestPipe() throws IOException { pipe = Pipe.open(); }
public void testSink() throws IOException { ByteBuffer buffer = ByteBuffer.allocate(1024); Pipe.SinkChannel sink = pipe.sink(); buffer.put("通过单向管道写入数据".getBytes()); buffer.flip(); sink.write(buffer); sink.close(); } public void testSource() throws IOException { ByteBuffer buffer = ByteBuffer.allocate(1024); Pipe.SourceChannel source = pipe.source(); int len = source.read(buffer); System.out.println(new String(buffer.array(), 0, len)); source.close(); } public static void main(String[] args) throws IOException { TestPipe testPipe = new TestPipe(); testPipe.testSink(); testPipe.testSource(); } }
|