JAVA BIO至NIO演进
主要阐述点:
1、同步/异步 or 阻塞/非阻塞
2、网络模型演进
3、NIO代码示例
一、同步/异步 or 阻塞/非阻塞 |
同步/异步:核心点在于是否等待结果返回。同步即调用者必须等到结果才返回,而异步则可立即返回无需等待结果,通过后期异步回调、状态检查等方式得到结果。
阻塞/非阻塞:核心点在于执行线程是否会阻塞。阻塞,例如在读操作中如果内核数据未准备好则会当阻塞读线程;而非阻塞,在内核数据未准备好前读线程无需等待,可以忙里偷闲干别的事情,
但是需要定期检查。
二、网络模型演进 |
1、原始版BIO: 单线程监听链接,每次只处理一个链接请求且读写阻塞。缺点:每次只能处理一个请求,读写阻塞。
2、线程版BIO: 单线程监听链接,可同时处理多个请求,每次分配一个线程处理一个链接请求,线程之间读写非阻塞。缺点:线程可能开设过多导致机器瓶颈,线程过多导致cpu上下文切换消耗大,
线程销毁浪费资源,单线程内部读写依然阻塞。
3、线程池版BIO: 单线程监听链接,可同时处理多个请求,每次将链接请求加入线程池工作队列,减少【线程版BIO】线程创建过多问题,线程之间读写非阻塞。缺点:存在客户链接数量限制,
单线程内部读写依然阻塞。
4、JDK4版本NIO:单线程监听链接,可同时处理多个请求,基于事件驱动形式。Selector封装操作系统调用(如linux epoll),每个客户端链接用通道Channel表示,当通道Channel数据准备完毕将触发相应事件。
- 4.1、NIO解决传统BIO痛点问题:
(1)读写阻塞:传统BIO只有当线程读写完成才会返回,否则线程将阻塞等待。而NIO在通道Channel准备完毕之后会由Selector触发事件,线程基于事件完成相应操作。在内核数据未准备好之前,线程
可以忙里偷闲处理其他逻辑,解决BIO阻塞等待内核数据准备的问题。
(2)客户端链接数量限制:BIO利用开设线程解决客户端之间读写非阻塞问题,但单机开设线程数量存在限制(即使开设线程池处理也有上限),而在像QQ聊天室这样需要建立大量长链接但数据量小的场景中难以满足需求。
在NIO中每个客户端链接对应一个SocketChannel,所有通道Channel注册到Selector统一管理。通道Channel相对线程Thread更轻量级,单机即可同时处理大量链接。
- 4.2、网络模型
上图体现了JAVA NIO 中有3个核心概念:
- Channel:与传统BIO的InputStream/OutputStream类似,区别在于Channel为双向通道支持同时读写。
- Buffer:独立数据缓冲区,所有关于Channel的读写操作都需要经过Buffer。
- Selector:将Channel注册到Selector并监听通道事件,是NIO模型中的核心类。
详细概念参考 JAVA NIO Tutorial
三、NIO代码示例 |
服务端Server端代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public class MyNioServer { private Selector selector; private final static int port = 8686; private final static int BUF_SIZE = 10240; private static ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE); private void initServer() throws IOException { //创建通道管理器对象selector this.selector = Selector.open(); //创建一个通道对象channel ServerSocketChannel channel = ServerSocketChannel.open(); channel.configureBlocking(false); channel.socket().bind(new InetSocketAddress(port)); channel.register(selector, SelectionKey.OP_ACCEPT); while (true){ // 这是一个阻塞方法,一直等待直到有数据可读,返回值是key的数量(可以有多个) selector.select(); // 如果channel有数据了,将生成的key访入keys集合中 Set<SelectionKey> keys = selector.selectedKeys(); // 得到这个keys集合的迭代器 Iterator<SelectionKey> iterator = keys.iterator(); while (iterator.hasNext()){ SelectionKey key = iterator.next(); iterator.remove(); if (key.isAcceptable()){ doAccept(key); }else if (key.isReadable()){ doRead(key); }else if (key.isWritable()){ doWrite(key); }else if (key.isConnectable()){ System.out.println("连接成功!"); } } selector.selectedKeys().clear(); } } public void doAccept(SelectionKey key) throws IOException { ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel(); System.out.println("ServerSocketChannel正在循环监听"); SocketChannel clientChannel = serverChannel.accept(); clientChannel.configureBlocking(false); clientChannel.register(key.selector(), SelectionKey.OP_READ); } public void doRead(SelectionKey key) throws IOException { SocketChannel clientChannel = (SocketChannel) key.channel(); byteBuffer.clear() ; int size = clientChannel.read(byteBuffer); byteBuffer.flip() ; byte[] data = byteBuffer.array(); String msg = new String(data, 0, size).trim(); System.out.println("从客户端发送过来的消息是:"+msg); clientChannel.register(selector, SelectionKey.OP_WRITE); } public void doWrite(SelectionKey key) throws IOException { SocketChannel clientChannel = (SocketChannel) key.channel(); byteBuffer.clear(); byteBuffer.put("收到你的请求 给客户端回复消息".getBytes()) ; byteBuffer.flip() ; while (byteBuffer.hasRemaining()){ clientChannel.write(byteBuffer); } clientChannel.register(selector, SelectionKey.OP_READ); } public static void main(String[] args) throws IOException { MyNioServer myNioServer = new MyNioServer(); myNioServer.initServer(); } }
客户端Client代码:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
public class MyNioClient { private Selector selector; //创建一个选择器 private final static int port_server = 8686; private final static int BUF_SIZE = 10240; private static ByteBuffer byteBuffer = ByteBuffer.allocate(BUF_SIZE); private void initClient() throws IOException { this.selector = Selector.open(); SocketChannel clientChannel = SocketChannel.open(); clientChannel.configureBlocking(false); clientChannel.connect(new InetSocketAddress(port_server)); clientChannel.register(selector, SelectionKey.OP_CONNECT); Scanner scanner = new Scanner(System.in); while (true){ selector.select(); Iterator<SelectionKey> iterator = selector.selectedKeys().iterator(); while (iterator.hasNext()){ SelectionKey key = iterator.next(); iterator.remove(); if (key.isConnectable()){ doConnect(key); }else if (key.isWritable()){ doWrite(key, scanner); }else if (key.isReadable()){ doRead(key); } } selector.selectedKeys().clear(); } } public void doConnect(SelectionKey key) throws IOException { SocketChannel clientChannel = (SocketChannel) key.channel(); if (clientChannel.isConnectionPending()){ clientChannel.finishConnect(); } System.out.println("已经与服务端建立链接"); clientChannel.register(selector, SelectionKey.OP_WRITE); } public void doWrite(SelectionKey key, Scanner scanner) throws IOException { SocketChannel clientChannel = (SocketChannel) key.channel(); System.out.print("please input message:"); String message = scanner.nextLine(); byteBuffer.clear(); byteBuffer.put(message.getBytes("UTF-8")); byteBuffer.flip(); while (byteBuffer.hasRemaining()){ clientChannel.write(byteBuffer); } clientChannel.register(selector, SelectionKey.OP_READ); } public void doRead(SelectionKey key) throws IOException { SocketChannel clientChannel = (SocketChannel) key.channel(); byteBuffer.clear() ; int size = clientChannel.read(byteBuffer); byteBuffer.flip() ; byte[] data = byteBuffer.array(); String msg = new String(data, 0 , size).trim(); System.out.println("服务端发送消息:"+msg); clientChannel.register(selector, SelectionKey.OP_WRITE); } public static void main(String[] args) throws IOException { MyNioClient myNioClient = new MyNioClient(); myNioClient.initClient(); } }
交互信息:
==> 客户端 已经与服务端建立链接 please input message:hello 服务端发送消息:收到你的请求 给客户端回复消息 please input message:world 服务端发送消息:收到你的请求 给客户端回复消息 please input message:kitty 服务端发送消息:收到你的请求 给客户端回复消息 please input message: ==> 服务端 ServerSocketChannel正在循环监听 从客户端发送过来的消息是:hello 从客户端发送过来的消息是:world 从客户端发送过来的消息是:kitty
参看链接:深入分析 Java I/O 的工作机制