本文参考《Netty权威指南》
与Socket类和ServerSocketl类相对应,NIO也提供SocketChannel和ServerSocketChannel两种不同的套接字通道实现。两种新增的通道都支持阻塞和非阻塞两种模式。
基本概念
1. 缓冲区Buffer
Buffer是一个对象,包含一些要写入或者要读出的数据。缓冲区实质上是一个数组,但是一个缓冲区不仅仅是一个数组,它提供了对数据的结构化访问以及维护读写位置(Limit)等信息。
2. 通道Channel
Channel是一个通道,网络数据通过Channel读取和写入。通道与流不同在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或者OutputStream的子类),而通道可以用于读、写或者二者同时进行。
3. 多路复用器 Selector
Selector通过不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。基于epoll操作(区别select的三点)。
NIO服务端序列图
服务端代码:
1 public class NIOTimeServer { 2 public static void main(String[] args){ 3 int port = 8082; 4 if(args != null && args.length > 0){ 5 try{ 6 port = Integer.valueOf(args[0]); 7 } catch (Exception e){ 8 //采用默认值 9 } 10 } 11 MultiplexerTimeServer timeServer = new MultiplexerTimeServer(port); 12 new Thread(timeServer,"NIO-MultiplexerTimeServer-001").start(); 13 } 14 }
public class MultiplexerTimeServer implements Runnable{ private Selector selector; private ServerSocketChannel servChannel; private volatile boolean stop; private int port; /** * 初始化多路复用器,绑定监听端口 */ public MultiplexerTimeServer(int port){ this.port = port; try { //1. 打开ServerSocketChannel,用于监听客户端的连接,他是所有客户端连接的副管道 servChannel = ServerSocketChannel.open(); //2. 绑定监听端口,设置连接为非阻塞模式 servChannel.configureBlocking(false); servChannel.socket().bind(new InetSocketAddress(port),1024); //3. 创建多路复用器 selector = Selector.open(); //4. 将ServerSocketChannel注册到多路复用器Selector上,监听Accept事件 servChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("The time server is start in port: " + port); }catch (IOException e){ e.printStackTrace(); System.exit(1); } } public void setStop(){ this.stop = true; } public void run(){ //多路复用器在线程run方法的无限循环体内轮询准备就绪的key while(!stop){ try{ /** * Selects a set of keys whose corresponding channels are ready for I/O operations. * <p> This method performs a blocking <a href="#selop">selection * operation</a>. It returns only after at least one channel is selected, * */ selector.select(1000); Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> it = selectionKeys.iterator(); SelectionKey selectionKey = null; while(it.hasNext()){ selectionKey = it.next(); it.remove(); try{ //处理就绪的任务 handleInput(selectionKey); } catch (Exception e){ if (selectionKey != null){ selectionKey.cancel(); if(selectionKey.channel() != null){ selectionKey.channel().close(); } } } } }catch (Throwable t){ t.printStackTrace(); } } } private void handleInput(SelectionKey key) throws IOException{ //处理新接入的请求消息 if(key.isValid()){ if(key.isAcceptable()){ //Accept the new connect ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); //6. 多路复用器监听到有新的客户端接入,处理新的接入请求,完成TCP三次握手,建立物理链路 SocketChannel sc = ssc.accept(); //7. 设置客户端链路为非阻塞模式 sc.configureBlocking(false); //8. 将新接入的客户端连接注册到多路复用器上,监听读操作,读取客户端发送的网络消息 sc.register(selector,SelectionKey.OP_READ); } if (key.isReadable()){ //Read the data SocketChannel sc = (SocketChannel) key.channel(); ByteBuffer readBuffer = ByteBuffer.allocate(1024); //9. 异步读取客户端请求消息到缓冲区 int readBytes = sc.read(readBuffer); if(readBytes > 0){ readBuffer.flip(); byte[] bytes = new byte[readBuffer.remaining()]; readBuffer.get(bytes); String body = new String(bytes,"UTF-8"); System.out.println("The time server receive order: " + body); String currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)? new java.util.Date( System.currentTimeMillis()).toString():"Bad order"; System.out.println("currentTime: " + currentTime); doWrite(sc,currentTime); } else if(readBytes <0){ //对端链路关闭 key.cancel(); sc.close(); } else ; //读到0字节,忽略 } } } private void doWrite(SocketChannel channel, String response) throws IOException{ if(response != null && response.trim().length() > 0){ byte[] bytes = response.getBytes(); ByteBuffer writeBuffer = ByteBuffer.allocate(bytes.length); writeBuffer.put(bytes); writeBuffer.flip(); //将消息异步发送到客户端 channel.write(writeBuffer); } } }
NIO客户端序列图
客户端代码:
1 public class NIOTimeClient { 2 public static void main(String[] args){ 3 int port = 8082; 4 if(args != null && args.length >0){ 5 try{ 6 port = Integer.valueOf(args[0]); 7 } catch (NumberFormatException e){ 8 //采用默认值 9 } 10 } 11 new Thread(new NIOTimeClientHandle("192.168.1.107",port),"TimeClient - 001").start(); 12 } 13 }
1 public class NIOTimeClientHandle implements Runnable{ 2 private String host; 3 private int port; 4 private Selector selector; 5 private SocketChannel socketChannel; 6 private volatile boolean stop; 7 8 public NIOTimeClientHandle(String host,int port){ 9 this.host = host; 10 this.port = port; 11 try { 12 //初始化NIO的多路复用器和SocketChannel, 13 //1. 打开SocketChannel,绑定客户端本地地址(可选默认系统会随机分配一个可用的本地地址) 14 selector = Selector.open(); 15 socketChannel = SocketChannel.open(); 16 //需要注意:SocketChannel创建之后,需要将其设置为异步非阻塞模式 17 socketChannel.configureBlocking(false); 18 }catch (IOException e){ 19 e.printStackTrace(); 20 System.exit(1); 21 } 22 } 23 24 public void run(){ 25 //发送连接请求,因为是示例,没有做重连操作 26 try { 27 //4 5 步 28 doConnect(); 29 } catch (IOException e){ 30 e.printStackTrace(); 31 System.exit(1); 32 } 33 /** 34 * 循环体中轮询多路复用器Selector。当有就绪的channel时,执行 handleInput(key)方法 35 */ 36 while(!stop){ 37 try{ 38 selector.select(1000); 39 Set<SelectionKey> selectionKeys = selector.selectedKeys(); 40 Iterator<SelectionKey> it = selectionKeys.iterator(); 41 SelectionKey key = null; 42 while(it.hasNext()){ 43 key = it.next(); 44 it.remove(); 45 try{ 46 handleInput(key); 47 } catch (Exception e){ 48 if(key != null){ 49 key.cancel(); 50 } 51 } 52 } 53 }catch (Exception e){ 54 e.printStackTrace(); 55 System.exit(1); 56 } 57 } 58 } 59 private void handleInput(SelectionKey key) throws IOException{ 60 /** 61 public abstract boolean isValid() 62 Tells whether or not this key is valid. 63 A key is valid upon creation and remains so until it is cancelled, 64 its channel is closed, or its selector is closed. 65 */ 66 if(key.isValid()){ 67 //判断是否连接成功 68 SocketChannel sc = (SocketChannel) key.channel(); 69 /** 70 public final boolean isConnectable() 71 Tests whether this key's channel has either finished, or failed to finish, 72 its socket-connection operation. 73 */ 74 /** 75 * 首先对Selection进行判断,看它处于什么状态,如果是处于连接状态,说明服务器已经返回ACK应答消息。 76 * 这是我们需要对连接结果进行判断,调用SocketChannel的finishConnect()方法,如果返回值为true,说明 77 * 客户端连接成功,否则是失败。将SocketChannel注册到多路复用器上,注册SelectionKey.OP_READ操作位, 78 * 监听网络读操作 79 */ 80 if(key.isConnectable()){ 81 /** 82 public abstract boolean finishConnect() throws IOException 83 Finishes the process of connecting a socket channel. 84 */ 85 if(sc.finishConnect()){ 86 sc.register(selector,SelectionKey.OP_READ); 87 doWrite(sc); 88 } else{ 89 System.exit(1); //连接失败,进程退出 90 } 91 } 92 //读取消息预先分配1MB的接收缓冲区用于读取应答消息 93 if(key.isReadable()){ 94 ByteBuffer readBuffer = ByteBuffer.allocate(1024); 95 int readBytes = sc.read(readBuffer); 96 if(readBytes > 0){ 97 readBuffer.flip(); 98 byte[] bytes = new byte[readBuffer.remaining()]; 99 readBuffer.get(bytes); 100 String body = new String(bytes,"UTF-8"); 101 System.out.println("Now is: " + body); 102 this.stop = true; 103 } else if (readBytes < 0){ 104 //对端链路关闭 105 key.cancel(); 106 sc.close(); 107 } else ; 108 } 109 } 110 } 111 private void doConnect() throws IOException{ 112 //如果直接连接成功,则注册到多路复用器上,发送请求消息,读应答 113 /** 114 * 首先对SocketChannel的connect()操作进行判断。如果连接成功,则将SocketChannel注册到 115 * 多路复用器Selector上,注册SelectionKey.OP_READ;如果没有直接连接成功,则说明 116 * 服务端没有返回TCP握手应答消息,但这并不代表连接失败。而是注册SelectionKey.OP_CONNECT 117 * 当服务端返回TCP syn-ack消息后,Selector就能轮询到这个SocketChannel处于连接就绪状态 118 */ 119 //4. 判断是否连接成功,如果连接成功,则直接注册读状态位到多路复用器中 120 if(socketChannel.connect(new InetSocketAddress(host,port))){ 121 socketChannel.register(selector, SelectionKey.OP_READ); 122 doWrite(socketChannel); 123 } else { 124 //5, 向多路复用器注册OP_CONNECT状态位,监听服务端的TCP ACK应答 125 socketChannel.register(selector,SelectionKey.OP_CONNECT); 126 } 127 } 128 private void doWrite(SocketChannel sc) throws IOException{ 129 byte[] req = "QUERY TIME ORDER".getBytes(); 130 ByteBuffer writeBuffer = ByteBuffer.allocate(req.length); 131 writeBuffer.put(req); 132 writeBuffer.flip(); 133 sc.write(writeBuffer); 134 if(!writeBuffer.hasRemaining()){ 135 System.out.println("Send order 2 server succeed."); 136 } 137 } 138 }
服务端执行结果:
客户端执行结果:
NIO优点总结:
1. 客户端发起的连接操作是异步的,可以通过在多路复用器注册OP_CONNECT等待后续结果,不需要想之前的客户端那样被同步阻塞。
2. SocketChannel的读写操作都是异步的,如果没有可读写的数据他不会同步等待,直接返回,这样I/O通信线程就可以处理其他的链路,不需要同步等待这个链路可用
3. 线程模型的优化,采用epoll实现,没有连接句柄数的限制。适合做高性能、高负载的网络服务器。