深入理解 BIO,NIO,AIO 三者的用途和区别?Select,poll,epoll 操作系统函数简介
目录一. BIO(Blocking I/O 同步阻塞) 详解1.1 BIO 基本介绍工作机制1.2 Java 实现 BIO1.3 总结概括二. NIO(Non-blocking I/O 同步非阻塞) 详解2.1 老调重弹2.2 逐步推演NIO的工作机制2.2.1 有缺陷的方案一2.2.2 有缺陷的方案二2.2.3 NIO 基本工作过机制解析最终实现方案2.2.4 深入探究Selectpollepoll 操作系统函数简介2.2.5 NIO 客户端实现模拟真实访问状况三. AIO 详解3.1 AIO 基本介绍3.2 Java 实现 AIO四. BIONIOAIO 总结对比五. 市场现有的成熟解决方案介绍——Netty、MINA、Grizzly一. BIO(Blocking I/O同步阻塞)详解1.1 BIO 基本介绍工作机制同步阻塞BIO就是传统的 Java IO 编程其相关的类和接口再 java.io 包下BIO模型是一个连接一个线程。工作模式为当一个连接建立后服务器端会分配一个独立的线程来为该连接服务所有操作读取、写入都是阻塞的。并且会一直阻塞直到数据完成传输通常适用于低并发、连接数少的场景使用频率相对较低。其优点就是比较稳定编程模型简单易上手但设计模型也决定了缺点可扩展性较差受限于线程数量还会导致资源利用率较低。我们那其中一个客户端服务端的交互为例客户端与服务端的大致交流过程如下图所示1.2 Java 实现 BIO为了便于同学们好理解我们先以一个单线程 BIO 为例模拟一下 BIO 的数据传递过程a.首先创建一个 Server 服务端public class BIOServer { public static void main(String[] args) { byte[] buffer new byte[1024]; try { // 创建一个ServerSocket绑定到8080端口监听客户端连接 ServerSocket serverSocket new ServerSocket(8080); System.out.println(服务器已监听8080端口); while (true){ System.out.println(服务器正在等待连接); // 阻塞1阻塞等待连接请求 Socket socket serverSocket.accept(); System.out.println(服务器已接收到连接请求); System.out.println(服务器正在等待数据源); // 阻塞2连接成功后仍然阻塞等待数据传输 socket.getInputStream().read(buffer); String content new String(buffer); System.out.println(服务器已接收到数据 content); } }catch (IOException e){ e.printStackTrace(); } } }b.再创建一个客户端public class BIOClient { public static void main(String[] args) { try { Socket socket new Socket(localhost, 8080); String message null; // 创建键盘录入 Scanner scanner new Scanner(System.in); // 获取键盘录入 message scanner.next(); socket.getOutputStream().write(message.getBytes()); socket.getOutputStream().flush(); socket.close(); scanner.close(); } catch (Exception e) { e.printStackTrace(); } } }c.启动客户端控制台打印如下服务端阻塞等待客户端连接请求d.然后启动客户端可以看到此时可以键盘录入我们暂时先不录入再回到客户端线程e.如下图此时客户端已经监听到了连接请求并连接成功等待客户端发送数据f.回到客户端随便输入数据回车发送数据传输成功客户端线程中断g.切换回服务端线程可以看到客户端传输的数据66666已经被接收处理处理完毕重新进入 while 循环等待新的连接请求1.3 总结概括通过上面的单线程 BIO 示例代码可以得出如下结论(1) 客户端再启动运行后首先会经历第一次阻塞(等待客户端请求连接)如果没有客户端请求连接就会一直处理阻塞(2) 如果接收到了请求连接并连接成功则会进入第二次阻塞(等待客户端发送数据)如果客户端一直不发送数据则会一直处于阻塞状态(3) 这是其中一个线程的处理总体程序逻辑那么在实际BIO中while 循环通常是来一个请求就创建一个新的线程去处理我这里为了便于大家理解并没有展示实际逻辑如下所示来一个请求就 new Thread 一个新的线程去处理实际业务逻辑定义在了 handleClient 方法中总之大家只要明白BIO 在处理请求时会阻塞两次并且为每个请求都创建一个线程这是BIO最重要的两个特点public static void testBio() throws IOException { // 创建一个ServerSocket绑定到8080端口监听客户端连接 ServerSocket serverSocket new ServerSocket(8080); System.out.println(BIO Server started); // 无限循环持续监听接收客户端连接 while (true) { // accept()方法会阻塞当前线程直到有客户端连接当客户端连接时返回该链接的Socket对象 Socket clientSocket serverSocket.accept(); System.out.println(BIO Client connected); // 为每个客户端连接创建并启动新线程 new Thread(() - // 在新线程中处理客户端连接这里我们单独定义为一个方法 handleClient(clientSocket) ).start(); } }(4) 那么我们进一步简单分析(3) 中的逻辑不难发现我们为每一个到来请求连接都创建了一个线程去处理又已知如果请求不发送数据则线程会一直阻塞等待如果请求访问数量较少时尚可接收但倘若有成千上万的线程都是连接成功但是却不向服务器发送数据造成大量的线程干等的服务器资源被极大的浪费这便是BIO的最大弊端——容易造成服务器资源的浪费二. NIO(Non-blocking I/O同步非阻塞)详解2.1 分析总结通过上面对BIO的学习了解我们知道了BIO的最大痛点就是连接阻塞和等待数据阻塞如果单线程 BIO虽然不浪费服务器资源但是由于等待阻塞每次只能处理一个线程程序的运行处理效率极低但如果使用多线程虽然可以同时并行处理大量请求访问但是大量的线程创建或阻塞又被导致服务器资源被极大的浪费两次阻塞极大地降低了 BIO 的效率这似乎是一个无解的问题怎样解决解决这一问题此时不难发现我们的问题焦点就转移到了如何让单线程或少量线程在的服务器在等待客户数据到来或客户请求连接时不处于阻塞状态可以去处理其它用户数据或用户请求所以下面我们要讲到的 NIO 说白了就是尝试在解决这一问题。2.2 逐步推演NIO的工作机制2.2.1 有缺陷的方案一上面我们说到了如果让服务器线程在处理客户请求数据或连接时不处于阻塞状态就可以提高效率下面就是一个简单的示例代码我们将它简单运行一下/** * 基础NIO版本单线程轮询 * 问题会错过数据因为只能处理当前连接 */ public static void main(String[] args) { ByteBuffer buffer ByteBuffer.allocate(1024); try { // 使用非阻塞的类型的ServerSocketChannel ServerSocketChannel serverSocketChannel ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(8080)); // 设置为非阻塞 serverSocketChannel.configureBlocking(false); while (true) { SocketChannel socketChannel serverSocketChannel.accept(); // 为空表示无人连接 if (socketChannel null) { System.out.println(正在等待客户端请求...); Thread.sleep(5000); } else { System.out.println(当前接受客户端请求连接...); } if (socketChannel ! null){ socketChannel.configureBlocking(false); buffer.flip(); // 切换模式写——读 int effect socketChannel.read(buffer); if (effect 0){ String message Charset.forName(UTF-8).decode(buffer).toString(); System.out.println(接收到数据 message); } else { System.out.println(没有数据可读); } } } } catch (Exception e) { throw new RuntimeException(e); } }如下图所示可以看到控制台每隔五秒打印一次日志标识等待客户端请求连接。现在我们现在思考一个问题如果不设置时间那就是什么时候有连接什么时候出处理。此时又会衍生出另外一个问题假设我用户A正在向服务器传输数据服务器正在正常接收并处理同一时刻用户B也向服务器发送了请求连接那么服务器就会去处理用户B的请求连接我用户A请求访问导致服务器并没有接收到用户A传输的数据处理数据失败。所以显而易见下面这种做法明显是不妥当的因为会导致在处理请求时会导致其它连接传递的数据未能接收。换言之这种方法的解决思路总体是没错的但是可能会导致数据丢失所以说如果我们能在此基础上找到一种不让数据丢失的解决办法不就可以了吗此时聪明的你大概已经想到了我可以将每个请求连接发送的数据存储或缓存起来每个请求连接要做的请求操作或数据处理操作都可以视为一个单独的任务我们将所有的任务统一放一起或者放集合中总之只要可以让服务器线程可以去遍历处理每个任务就可以了这样一来我的服务器线程及没有空闲下来造成阻塞也没有导致请求访问或数据传输的丢失一举两得2.2.2 有缺陷的方案二上面我们分析出可以使用集合将所有请求访问进行存储然后遍历所有请求连接判断是否传输了数据如果有数据则读取数据下面我们就针对上面的代码做进一步的演化如下所示/** * NIO服务端 V2.0集合版本 * 改进用集合存储所有连接不会错过数据 * 问题需要遍历所有连接效率较低 */ public static void main(String[] args) { try { ByteBuffer byteBuffer ByteBuffer.allocate(1024); // 创建一个集合用于存储 SocketChannel 任务列表 List socketList new ArrayList(); // 使用非阻塞的类型的ServerSocketChannel ServerSocketChannel serverSocketChannel ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(8080)); // 设置为非阻塞 serverSocketChannel.configureBlocking(false); while (true) { SocketChannel socketChannel serverSocketChannel.accept(); if (socketChannel null) { // 表示无人连接 Thread.sleep(5000); } else { // 当前收到客户端请求连接添加到任务列表... socketList.add(socketChannel); } for (SocketChannel socket : socketList){ socket.configureBlocking(false); int effect socket.read(byteBuffer); if (effect 0){ byteBuffer.flip(); // 切换模式写——读 String message Charset.forName(UTF-8).decode(byteBuffer).toString(); System.out.println(接收到数据 message); byteBuffer.clear(); } else { System.out.println(没有数据可读); } } } } catch (Exception e) { throw new RuntimeException(e); } }分析上面的代码我们现在已经将所有的请求连接作为一个待处理任务添加到了集合中现在我们只需要遍历集合中所有请求连接处理数据即可啦。OK到了这里其实我们已经基本实现了NIO的工作原理了。现在我们认真分析上面的方案二代码是否还可以进一步优化答案是显而易见的可以进行优化重点优化就在任务集合 List 。同学们试想一下现在将所有的请求连接全部放入集合然后让服务器线程遍历集合即可虽然不会再次造成数据丢失但有一个比较严重的问题我们或许没有注意到就是数量什么数量呢当然是集合中的任务数量。现在我们编写的示例代码假设只有三个请求过来集合中就只有三个任务循环效率较快我们没有什么实际感知但是在实际生产项目或网络环境下会有数千数万数十万的用户请求访问连接或许只有其中的几十个或数百个有进行数据传输但是我们让然遍历所有整个 List 任务集合很显然做了很多的无用功。所以如果我们能够在此基础上再进一步将所有可被处理的请求连接放在任务集合中这样一来遍历到了每个请求都是可被处理的任务进一步提高了程序的处理效率。由此我们便正式推断出了 NIO 网络编程的基本工作机制。2.2.3 NIO 基本工作过机制解析最终实现方案NIO 的工作机制流程大致如下图所示我们简单介绍一下图中的对象其实大部分都是我们上面已经用过或者见到过的事物了。ServerSocketChannel整个服务器只有一个负责接收连接如图中所示的 ServerSocketChannel Channel通道类似于流但可以同时进行读写主要类型有 SocketChannelTCP客户端ServerSocketChannelTCP服务端和 DatagramChannelUDP如图中所示的SockerChannel1、SocketChannel2、SocketChannel3Buffer缓冲区一个容器所有数据的读写都通过 Buffer。核心属性有 capacitypositionlimitSelector选择器NIO的灵魂一个 Selector选择器 可以轮询多个 Channel通道当一个 Channel 上有事件如新连接到来数据可读数据可写发生时Selector 会通知应用程序然后应用程序再处理这些事件Reactor 线程一个主线程处理所有的网络IO可选工作线程池处理业务逻辑可以类似理解为服务器线程下面就是一个比较完整的NIO 服务器实现方案public class NIOServer { // main 方法测试启动 public static void main(String[] args) { try { new NIOServer().start(8085); } catch (IOException e) { e.printStackTrace(); } } // 服务端部分 private Selector selector; public void start(int port) throws IOException { System.out.println( NIO服务端启动 ); // 1. 创建Selector事件监听器 selector Selector.open(); // 2. 创建ServerSocketChannel ServerSocketChannel serverChannel ServerSocketChannel.open(); serverChannel.bind(new InetSocketAddress(port)); serverChannel.configureBlocking(false); // 3. 注册ACCEPT事件 serverChannel.register(selector, SelectionKey.OP_ACCEPT); System.out.println(服务器监听端口: port); System.out.println(等待客户端连接...\n); // 4. 事件循环 while (true) { // 阻塞等待事件 int readyChannels selector.select(); if (readyChannels 0) continue; // 处理就绪的事件 SetSelectionKey readyKeys selector.selectedKeys(); IteratorSelectionKey keyIterator readyKeys.iterator(); while (keyIterator.hasNext()) { SelectionKey key keyIterator.next(); keyIterator.remove(); try { if (!key.isValid()) continue; if (key.isAcceptable()) { // 处理新的连接 handleAccept(key); } else if (key.isReadable()) { // 处理读取事件 handleRead(key); } else if (key.isWritable()) { // 处理写入事件 handleWrite(key); } } catch (Exception e) { key.cancel(); try { key.channel().close(); } catch (IOException ex) { e.printStackTrace(); } } } } } // 处理连接方法 private void handleAccept(SelectionKey key) throws IOException { ServerSocketChannel server (ServerSocketChannel) key.channel(); SocketChannel client server.accept(); client.configureBlocking(false); System.out.println(✅ 新客户端连接: client.getRemoteAddress()); // 注册READ事件并附加缓冲区 ByteBuffer buffer ByteBuffer.allocate(1024); client.register(selector, SelectionKey.OP_READ, buffer); } // 处理读取事件方法 private void handleRead(SelectionKey key) throws IOException { SocketChannel client (SocketChannel) key.channel(); ByteBuffer buffer (ByteBuffer) key.attachment(); buffer.clear(); int bytesRead client.read(buffer); if (bytesRead 0) { buffer.flip(); String message StandardCharsets.UTF_8.decode(buffer).toString().trim(); System.out.println( 收到来自 client.getRemoteAddress() 的消息: message); // 准备回复 String response Server reply: message; ByteBuffer responseBuffer ByteBuffer.wrap(response.getBytes(StandardCharsets.UTF_8)); // 改为关注WRITE事件 key.interestOps(SelectionKey.OP_WRITE); key.attach(responseBuffer); } else if (bytesRead -1) { System.out.println(❌ 客户端断开: client.getRemoteAddress()); client.close(); key.cancel(); } } // 处理写入事件方法 private void handleWrite(SelectionKey key) throws IOException { SocketChannel client (SocketChannel) key.channel(); ByteBuffer buffer (ByteBuffer) key.attachment(); if (buffer.hasRemaining()) { client.write(buffer); } if (!buffer.hasRemaining()) { // 发送完成重新关注READ事件 ByteBuffer newBuffer ByteBuffer.allocate(1024); key.interestOps(SelectionKey.OP_READ); key.attach(newBuffer); System.out.println( 回复已发送到 client.getRemoteAddress()); } } }运行上述代码如下图所示模拟 NIO 服务器启动成功2.2.4 深入探究Selectpollepoll 操作系统函数简介通过上面的解释我们不难发现在NIO中资源关系为 1:1:1:N 1个ServerSocketChannel:1个Selector:1个Rector线程:N个SocketChannel它的工作模式也常被称为 Reactor 模式。实现了用少数线程管理大量的连接只有当某个连接真正有读/写事件的时候才会分配资源进行处理这也是NIO支持高并发的核心原因这个时候小伙伴们可以能会有一些疑问1怎样判断某个连接真正有读/写事件。答这一步操作并不是在 Java 代码层面实现的而是将这个轮询的步骤交给了操作系统来进行底层将轮询的那部分代码该做系统级别的系统调用Select 函数在 Linux 系统中为 epoll 通过操作系统的函数调用主动感知有数据的 Socket 然后放入 channel 通道中再由 Java 代码进行循环遍历2既然应用层也是去进行循环遍历感知为什么我们不能在代码应用层实现呢答最主要的原因是我们所写的 Java 程序每次在轮询 Socket 时也需要去调用系统函数每次轮询每次都调用会造成不必要的性能开销而如果我们使用系统函数库 Select它会直接全量复制所有请求到系统内核空间在内核空间判断每个请求是否已经准备好这样一来还规避了频繁的上下文切换所以使用系统函数轮询是要比在Java代码层面实现轮询效率要高的原本应该直接说明 NIO 的实现模式但这里我们插上一小节专门来讲解一下操作系统底层的相关 IO 函数这三个函数都是 Linux 系统下的 IO 多路复用机制其发展顺序为 select——poll——epoll但在其它系统也有类似的功能如 macOSFreeBSD中也有类似的实现下面简述它们的区别同学们可以混个眼熟大概了解一下Select 函数是一个阻塞函数如果一个或多个请求已经准备好了数据那么 Select 会先将有数据的文件描述符置位改为待处理返回整个含有待处理任务的集合数据之后通过遍历查看哪些个请求有待处理数据但是任何事物总归都是有两面性的我们不能只看好处不看缺点同样 Select 也是存在缺点的。底层存储依赖 bitmap处理是有内存上限的为 1024即最大连接数为1024文件描述是会置位的如果当被置位文件描述符需要重新使用时则需要重新赋空值fd文件描述符从用户态拷贝到内核态仍然有一笔开销select 返回后还要再进行遍历来获悉是哪个请求有数据事件复杂度O(n)poll 函数由于其底层设计为链表所以相比 Select 函数他没有文件描述符数量的限制并使用了 pollfd 结构体可以更精细的监控事件(如ROLLRDHUP)所以 poll 解决了上述 Select 函数的缺点1和缺点2但是它仍然存在和 Select 函数一样的缺陷即 每次调用仍然是复制全部 fd 到内核返回后仍然需要遍历所有 fd 检查就绪状态时间复杂度O(n)epoll 函数因为 epoll 它的 fd 是共享在用户态和内核态之间的所以不需要像 select,poll 那样将数据进行拷贝节省了系统资源此外当 epoll 发现某个请求数据已经准备好时首先会进行一个重新排序操作将所有有 fd 的数据放在最前面然后返回值返回值是存在的数据请求个数 N也就是说epoll 只会返回存在数据的请求(或者说请求个数)那么我们的上层应用就不需要像 selectpoll 那样对所有请求都进行轮询而是直接遍历 epoll 返回的前 N 个请求这些都是有效的有数据的请求。所以它相较于前两者做出了较大的改进无需每次复制 fd而是通过 epoll_ctrl 注册一次后续直接复用只返回就绪的 fdepoll_wait 直接返回就绪状态的事件列表无需再向前两者一样遍历所有 fd时间复杂度由 O(n)直接降低为O(1)支持边缘触发(ET)和水平出发(LT)两种模式底层使用红黑树管理 fd高效支持大规模连接性能对比特性selectpollepoll最大连接数FD_SETSIZE1024无限制系统限制无限制系统限制时间复杂度O(n) 需要遍历数组O(n) 需要遍历链表O(1)就绪 fd 数内存拷贝每次复制全部 fd每次复制全部 fd注册一次无需重复触发模式水平触发水平触发边缘触发(ET)/水平触发(LT) 可选内核实现线性遍历线性遍历红黑树就绪链表底层数据结构数组链表红黑树适用场景select/poll适用于fd 数量少跨平台需求或者兼容旧系统的场景epoll适用于 Linux 高并发的场景(如万级连接)尤其是长连接服务(Web服务器即时通讯)2.2.5 NIO 客户端实现模拟真实访问状况经过上面的层层推演我们现在已经了解了 NIO 服务器的基本原理了现在再编写一个客户端的方法逻辑这个就比较简单了我直接展示代码public class NIOClient { public static void main(String[] args) { try { new NIOClient().start(localhost, 8085); } catch (IOException e) { e.printStackTrace(); } } public void start(String host, int port) throws IOException { System.out.println( NIO客户端启动 ); // 1. 创建SocketChannel SocketChannel client SocketChannel.open(); client.configureBlocking(false); // 2. 连接服务器 System.out.println(正在连接服务器 host : port ...); client.connect(new InetSocketAddress(host, port)); // 等待连接完成 while (!client.finishConnect()) { System.out.println(连接中...); try { Thread.sleep(500); } catch (InterruptedException e) { Thread.currentThread().interrupt(); return; } } System.out.println(✅ 连接服务器成功!); System.out.println(请输入消息输入 exit 退出:\n); Scanner scanner new Scanner(System.in); ByteBuffer buffer ByteBuffer.allocate(1024); while (true) { System.out.print( ); String input scanner.nextLine(); if (exit.equalsIgnoreCase(input.trim())) { break; } // 发送消息 buffer.clear(); buffer.put(input.getBytes(StandardCharsets.UTF_8)); buffer.flip(); while (buffer.hasRemaining()) { client.write(buffer); } System.out.println(消息已发送: input); // 接收回复 buffer.clear(); int bytesRead client.read(buffer); if (bytesRead 0) { buffer.flip(); String response StandardCharsets.UTF_8.decode(buffer).toString(); System.out.println(服务器回复: response); } try { Thread.sleep(100); // 避免过快发送 } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } } System.out.println(客户端退出); client.close(); scanner.close(); } }运行代码如下所示输入任意数据回车发送可以看到数据已经发送成功此时我们再回到服务端窗口可以看到刚才发送的111已经接收到我们再给客户端返回数据三. AIO 详解3.1 AIO 基本介绍由于 Linux 系统对 AIO 支持不完善(底层仍然采用 epoll 模拟)且几乎所有项目都是部署在 Linux 操作系统所以就目前来说并不要求开发人员对 AIO 有多么深入地掌握运用这里小编不再做详尽的描述主要说一说基本实现思路原理等简单理解熟悉即可。异步非阻塞也常称之为NIO2.0版本IO操作都是异步的这也是AIO的核心原理。当应用程序发起一个IO操作如读、写时会立即返回不会阻塞当前线程。操作系统会完成整个IO操作包括将数据从内核空间拷贝到用户空间完成之后会主动回调应用程序提供的回调函数。AIO的工作模式被称为 Proactor 模式。它与 Reactor 的关键区别在于Reactor通知你何时可以开始一个IO操作例如数据可读了但实际的IO操作读数据还是要自己来做Proactor通知你IO操作已经完成了(例如数据已经读到你提供的 Buffer 里了)AIO用途AIO一般适用于连接数较多并且连接较长(重操作)的架构比如相册服务器充分调用OS参与并发操作编程比较复杂。3.2 Java 实现 AIO以下是使用 Java 代码实现 AIO 编程示例public static void main(String[] args) { try { testAio(); } catch (IOException e) { throw new RuntimeException(e); } catch (InterruptedException e) { throw new RuntimeException(e); } } /** * AIO服务器核心实现 * throws IOException 可能抛出网络I/O异常 * throws InterruptedException 可能抛出线程中断异常 */ public static void testAio() throws IOException, InterruptedException { // 创建异步服务器套接字通道 // open()打开异步服务器通道 // bind()绑定到8080端口 AsynchronousServerSocketChannel server AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(8080)); System.out.println(AIO Server started); // 定义连接完成回调 server.accept(null, new CompletionHandlerAsynchronousSocketChannel, Void() { Override public void completed(AsynchronousSocketChannel client, Void attachment) { // 继续接收新连接递归调用 server.accept(null, this); // 处理新连接的客户端 handleClient(client); } Override public void failed(Throwable exc, Void attachment) { // 处理连接失败异常 exc.printStackTrace(); } }); // 防止主线程退出AIO是异步的需要保持主线程运行 Thread.currentThread().join(); } /** * 处理客户端连接 * param client 客户端异步通道 */ private static void handleClient(AsynchronousSocketChannel client) { // 创建1024字节容量的缓冲区 ByteBuffer buffer ByteBuffer.allocate(1024); // 定义读操作完成回调处理器 client.read(buffer, null, new CompletionHandlerInteger, Void() { Override public void completed(Integer bytesRead, Void attachment) { // 检查客户端是否断开连接 if (bytesRead -1) { try { // 关闭通道 client.close(); System.out.println(Client disconnected); } catch (Exception e) { e.printStackTrace(); } return; } // 切换为读模式 buffer.flip(); // 创建字节数组存储数据 byte[] data new byte[buffer.remaining()]; // 从缓冲区复制数据 buffer.get(data); // 将字节数据转换为字符串 // 注意实际应用应指定字符集如StandardCharsets.UTF_8 String request new String(data); System.out.println(Received: request); // 准备响应数据 ByteBuffer response ByteBuffer.wrap((Echo: request).getBytes()); // 发起写操作发送响应给客户端 client.write(response, null, new CompletionHandlerInteger, Void() { Override public void completed(Integer bytesWritten, Void attachment) { // 清空缓冲区准备下一次读取 buffer.clear(); // 继续发起读操作形成读取循环 client.read(buffer, null, this); } Override public void failed(Throwable exc, Void attachment) { // 处理写操作失败 exc.printStackTrace(); } }); } Override public void failed(Throwable exc, Void attachment) { exc.printStackTrace(); } }); }四. BIONIOAIO 总结对比特性BIONIOAIO模型同步阻塞同步非阻塞多路复用异步非阻塞线程要求1连接 1线程多连接共享少量线程回调驱动线程利用率最高编程复杂度简单复杂需管理Selector中等回调嵌套吞吐量低线程限制高极高底层机制传统Socketepoll/kqueue系统调用内核级异步支持适用场景低并发1000连接高并发如即时通讯超大规模并发如云存储资源利用率低比较高最高核心实现思想线性流程状态机 事件驱动回调驱动五. 市场现有的成熟解决方案介绍——Netty、MINA、Grizzly由于原生 API 复杂且容易出错所以在实际开发过程中我们通常不会直接进行编写而是公司内部通常会进行封装调用亦或者直接采用市场现有的主流封装框架。大家或许听说过Netty它的底层框架模型采用的就是 NIO。此外还有 MINA 和 Grizzly。三者的简要信息如下表格框架底层模型适用场景NettyNIO高并发、自定义协议如游戏MINANIO轻量级TCP/UDP应用GrizzlyNIO/AIO混合嵌入式服务器如REST服务在现有常见的 Spring Boot 项目中几乎大多都选择使用 Netty 框架Netty 的 EventLoopGroup 可以减少线程切换还有零拷贝技术(ByteBuf 池化)带来的内存上的优势从而使得 Netty 具有比后两者更高的性能。大家熟知的 RocketMQ、Dubbo 底层均使用了 Netty 框架。dependency groupIdio.netty/groupId artifactIdnetty-all/artifactId version4.1.86.Final/version /dependency