多线程笔记 - NIO
随着访问量越来越多, BIO 和 伪异步IO 已经不能满足需求了. 所以后面又出了个 NIO.
1. NIO 使用了一个 通道Channel 的概念, 他是一个双向通道, 可以读取和写入数据.
程序中使用的 SocketChannel 读写操作都是异步的, 没有读写的数据最直接返回.
2. NIO 使用了 多路复用器Selector, 轮询客户端的连接, 根据不同的标识, 干不同的事情.
服务端创建序列图
server:
public class Server implements Runnable{ //多路复用器, 管理所有通道 private Selector selector; private ServerSocketChannel serverChannel; private volatile boolean stop; public void stop(){ this.stop = true; } public Server(int port){ try{ this.selector = Selector.open(); //打开服务器通道 serverChannel = ServerSocketChannel.open(); //设置非阻塞模式 serverChannel.configureBlocking(false); //绑定地址 serverChannel.socket().bind(new InetSocketAddress(1234)); //把服务器通道注册到多路复用器上, 监听 SelectionKey.OP_ACCEPT操作位 serverChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println("服务器端已启动 : " + port); } catch(Exception e){ e.printStackTrace(); } } @Override public void run() { while (!stop) { try { //让多路复用器开始监听, 并设置休眠时间为1s, // 无论是否有读写事件发生, 每隔1s他都会被唤醒1次 selector.select(1000); Iterator<SelectionKey> keys = selector.selectedKeys().iterator(); //遍历多路复用器 while (keys.hasNext()) { SelectionKey selectionKey = keys.next(); keys.remove(); //判断是否有效 if (!selectionKey.isValid()) { continue; } //处理新接入的请求消息 if (selectionKey.isAcceptable()) { accept(selectionKey); } if (selectionKey.isReadable()) { read(selectionKey); } } } catch (IOException e) { e.printStackTrace(); } } if(selector != null){ try { selector.close(); } catch (IOException e) { e.printStackTrace(); } } } private void read(SelectionKey selectionKey) { try { //读取缓冲区 ByteBuffer readBuf = ByteBuffer.allocate(1024); //获取通道内注册的socket对象 SocketChannel sc = (SocketChannel) selectionKey.channel(); //读取数据 int count = sc.read(readBuf); //没有数据 if(count == -1){ selectionKey.channel().close(); selectionKey.cancel(); return; } //有数据, 则进行读取, 且读取后要进行复位 readBuf.flip(); byte[] bytes = new byte[readBuf.remaining()]; readBuf.get(bytes); String msg = new String(bytes, "UTF-8").trim(); System.out.println("from client : " + msg); if("几点了".equals(msg)){ String response = new DateTime(2020, 1, 1, 1, 1, 1).toString("yyyy-MM-dd HH:mm:ss"); byte[] resBytes = response.getBytes(); ByteBuffer outBuffer = ByteBuffer.allocate(resBytes.length); outBuffer.put(resBytes); outBuffer.flip(); sc.write(outBuffer); } } catch (IOException e) { e.printStackTrace(); } } private void accept(SelectionKey selectionKey) { try { //获取服务通道 ServerSocketChannel serverChannel = (ServerSocketChannel) selectionKey.channel(); //执行阻塞方法, 等待客户端接入, 接入成功时, 返回 SocketChannel 实例 SocketChannel client = serverChannel.accept(); //设置非阻塞 client.configureBlocking(false); //将新连接注册到多路复用器上, 且注册为 OP_READ client.register(selector, SelectionKey.OP_READ); } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) throws Exception { new Thread(new Server(1234)).start(); } }
客户端创建序列图:
Netty 权威指南里, 将客户端也加入了 Selector,进行轮询, 根据客户端与服务器连接的不同状态, 干不同的事情.
我这里偷个懒, 客户端就直接上了, 不加Selector
public class ClientA { public static void main(String[] args) { InetSocketAddress addr = new InetSocketAddress("127.0.0.1", 1234); ByteBuffer buf = ByteBuffer.allocate(1024); SocketChannel socketChannel = null; try { socketChannel = SocketChannel.open(); socketChannel.connect(addr); String msg = "几点了"; System.out.println("client : " + msg); byte[] bytes = msg.getBytes(); //将数据放入缓冲区 buf.put(bytes); //对缓冲区进行复位 buf.flip(); //写出数据 socketChannel.write(buf); //清空缓冲区数据 buf.clear(); while (true) { //读数据 ByteBuffer readBuf = ByteBuffer.allocate(1024); int readBytes = socketChannel.read(readBuf); if (readBytes == -1) { break; } readBuf.flip(); byte[] bytes1 = new byte[readBuf.remaining()]; readBuf.get(bytes1); String msg1 = new String(bytes1, "UTF-8").trim(); System.out.println("from server : " + msg1); } } catch (Exception e) { e.printStackTrace(); } finally { if (socketChannel != null) { try { socketChannel.close(); } catch (Exception e) { e.printStackTrace(); } } } } }
结果:
client:
server:
参考:
Netty权威指南