当前位置: 首页 > news >正文

Netty从0到1系列之NIO

文章目录

  • 一、核心思想: 为什么需要NIO?
  • 二、NIO三大核心组件
    • 2.1 Channel【通道】
    • 2.2 Buffer【缓冲区】
    • 2.3 Selector【选择器】
    • 2.4 工作原理流程图
  • 三、代码示例【NIO echo 服务器】
    • 3.1 示例代码
    • 3.2 客户端测试
  • 四、NIO优缺点与实践经验
    • 4.1 优点
    • 4.2 缺点
    • 4.3 实践经验总结

一、核心思想: 为什么需要NIO?

传统的 Java I/O(java.io,又称 BIO - Blocking I/O)是基于阻塞的。

  • :单向的,要么是输入流(InputStream),要么是输出流(OutputStream),不能同时读写。
  • 阻塞:当一个线程调用 read()write() 时,该线程会被阻塞,直到数据被读取到或写入完成。在此期间,线程什么也做不了。

BIO 模型在处理大量连接时非常低效。通常采用“一个连接,一个线程”的方案。当连接数暴涨时,线程数量也随之暴涨,导致巨大的上下文切换开销,最终耗尽系统资源。

NIO (New I/O / Non-blocking I/O) 的出现就是为了解决这些问题:

  1. 非阻塞 I/O:线程可以从通道请求读取数据,但如果尚无数据可用,线程可以立即去做别的事情,而不是被阻塞。
  2. 面向缓冲区:数据被读入或写出一个缓冲区,你可以根据需要前后移动缓冲区,这提供了更大的灵活性。
  3. 多路复用器:使用单个(或少量)线程来管理多个通道(连接),这是高性能的关键。

🎯 目标:用少量线程处理成千上万个连接。

NIO vs BIO

场景BIO(阻塞IO)NIO(非阻塞IO)
10个连接10个线程1个线程即可
1000个连接1000个线程 → 崩溃1~4个线程可支撑
CPU开销高(上下文切换)
编程复杂度简单较复杂(状态管理)
NIO模型
BIO模型
Selector
单线程
连接1: 非阻塞
连接2: 非阻塞
连接3: 非阻塞
连接1: 阻塞读写
线程1
连接2: 阻塞读写
线程2
连接3: 阻塞读写
线程3

二、NIO三大核心组件

Channel、Buffer、Selector

组件说明
Buffer(缓冲区)数据的容器,所有数据都通过 Buffer 读写
Channel(通道)类似流,但可双向读写,支持非阻塞模式
Selector(选择器)实现 I/O 多路复用,监听多个 Channel 的事件

2.1 Channel【通道】

public interface Channel extends Closeable {// channel是否已经打开public boolean isOpen();// 关闭Channelpublic void close() throws IOException;
}

类似于传统的“流”,但是双向的,既可以读,也可以写。它是连接缓冲区和数据源(如文件、套接字)的桥梁。

  • 主要实现
    • FileChannel:用于文件 IO。
    • SocketChannel & ServerSocketChannel:用于 TCP 网络 IO。
    • DatagramChannel:用于 UDP 网络 IO。

在这里插入图片描述

channel vs stream

对比项StreamChannel
方向单向双向
阻塞总是阻塞可设为非阻塞
传输方式逐字节与 Buffer 配合批量传输

2.2 Buffer【缓冲区】

public abstract class Buffer {// Invariants: mark <= position <= limit <= capacityprivate int mark = -1;private int position = 0;private int limit;private int capacity;// ...
}

在这里插入图片描述

一个容器对象,所有数据的读写都直接通过缓冲区进行。它本质上是一个内存块数组,并提供了一组方法以便更轻松地使用该内存。

  • 核心属性

    • capacity:容量,缓冲区最大大小,创建后不可变。
    • position:位置,下一个要读取或写入的元素的索引。
    • limit:界限,缓冲区中第一个不应读取或写入的元素的索引。
    • mark:标记,一个备忘位置,通过 mark() 标记,可通过 reset() 恢复到 mark 的位置。
    • 关系:0 <= mark <= position <= limit <= capacity
  • 主要实现ByteBuffer, CharBuffer, IntBuffer 等,最常用的是 ByteBuffer

  • 核心操作

    • allocate(int capacity):分配一块新的缓冲区。
    • put() / get():写入/读取数据。
    • flip():将缓冲区从写模式切换到读模式。limit = position; position = 0;
    • clear():清空缓冲区,准备再次写入。position = 0; limit = capacity;
    • rewind():重读缓冲区。position = 0;

2.3 Selector【选择器】

NIO 的灵魂。它是一个多路复用器,可以监控多个通道的 IO 状态(例如:连接就绪、读就绪、写就绪)。一个单线程的 Selector 可以管理成千上万的通道。

  • SelectionKey:表示一个通道在选择器上的注册 token。它包含:
    • 兴趣集合 (Interest Set):你关心通道的什么事件
      • OP_ACCEPT
      • OP_CONNECT
      • OP_READ
      • OP_WRITE
    • 就绪集合 (Ready Set):通道已就绪的操作集合。
    • 通道 (Channel)选择器 (Selector) 的引用。
    • 附件 (Attachment):可附加一个对象(如一个 Buffer 或会话对象),是强大的扩展机制。

工作流程图
在这里插入图片描述

2.4 工作原理流程图

Selector ThreadSelectorSelectionKey (ACCEPT)SelectionKey (READ)Client ChannelSocketChannelBuffer.select() (阻塞或非阻塞等待)监控所有注册的通道返回就绪的Key数量遍历SelectionKeySet检查key是否有效接受连接,创建SocketChannel配置非阻塞,注册到Selector(OP_READ)alt[isAcceptable()]获取附加的Buffer.read(ByteBuffer)处理Buffer中的数据.clear()/.flip()alt[isReadable()]从集合中移除已处理的Keyloop[处理SelectedKeys]Selector ThreadSelectorSelectionKey (ACCEPT)SelectionKey (READ)Client ChannelSocketChannelBuffer

三、代码示例【NIO echo 服务器】

3.1 示例代码

下面是一个完整的 NIO Echo 服务器实现,它接收客户端的消息并原样返回。

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;public class NioEchoServer {public static void main(String[] args) throws IOException {// 1. 创建选择器 (The Multiplexer)Selector selector = Selector.open();// 2. 创建服务器套接字通道并设置为非阻塞模式ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();serverSocketChannel.configureBlocking(false); // 必须设置为非阻塞serverSocketChannel.bind(new InetSocketAddress(9090)); // 绑定端口// 3. 将通道注册到选择器,并指定感兴趣的事件为 ACCEPT(接受连接)serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);System.out.println("NIO Echo Server started on port 9090...");// 主事件循环while (true) {// 4. 阻塞等待就绪的通道。参数可设置超时时间。selector.select();// 5. 获取就绪的 SelectionKey 集合Set<SelectionKey> selectedKeys = selector.selectedKeys();Iterator<SelectionKey> keyIterator = selectedKeys.iterator();while (keyIterator.hasNext()) {SelectionKey key = keyIterator.next();// 必须先移除,防止下次重复处理keyIterator.remove();try {if (key.isAcceptable()) {// 6. 处理 ACCEPT 事件:新的客户端连接handleAccept(key, selector);} else if (key.isReadable()) {// 7. 处理 READ 事件:客户端发送了数据handleRead(key);}// 可以处理 isWritable(),但通常只在需要写入大量数据时才注册} catch (IOException e) {// 客户端断开连接等异常System.err.println("Error handling client: " + e.getMessage());key.cancel(); // 取消这个键的注册if (key.channel() != null) {key.channel().close(); // 关闭通道}}}}}private static void handleAccept(SelectionKey key, Selector selector) throws IOException {// 获取注册的 ServerSocketChannelServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();// 接受连接,获取客户端的 SocketChannelSocketChannel clientChannel = serverChannel.accept();clientChannel.configureBlocking(false); // 设置为非阻塞模式// 欢迎信息String welcomeMsg = "Welcome to NIO Echo Server. Type 'exit' to quit.\n";ByteBuffer buffer = ByteBuffer.wrap(welcomeMsg.getBytes());clientChannel.write(buffer); // 发送欢迎信息// 将新客户端通道注册到选择器,对 READ 事件感兴趣// 并为每个通道附加一个专属的 BufferclientChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(256));System.out.println("Client connected: " + clientChannel.getRemoteAddress());}private static void handleRead(SelectionKey key) throws IOException {// 获取客户端通道和附加的 BufferSocketChannel clientChannel = (SocketChannel) key.channel();ByteBuffer buffer = (ByteBuffer) key.attachment();// 清空Buffer,准备读取数据buffer.clear();int bytesRead = clientChannel.read(buffer);if (bytesRead == -1) {// 客户端关闭了连接System.out.println("Client disconnected: " + clientChannel.getRemoteAddress());key.cancel();clientChannel.close();return;}// 切换Buffer为读模式buffer.flip();// 将接收到的数据转换为字符串String received = new String(buffer.array(), 0, buffer.limit()).trim();System.out.println("Received from " + clientChannel.getRemoteAddress() + ": " + received);if ("exit".equalsIgnoreCase(received)) {// 如果客户端发送 "exit",则关闭连接clientChannel.write(ByteBuffer.wrap("Goodbye!\n".getBytes()));System.out.println("Client exited: " + clientChannel.getRemoteAddress());key.cancel();clientChannel.close();} else {// Echo 功能:将收到的数据原样发回// 注意:write()可能不会一次性写完,需要注册OP_WRITE事件来持续写,// 但对于Echo这种小数据量场景,通常一次write就能完成,这里做了简化。buffer.rewind(); // 将position重置为0,重新读一遍BufferclientChannel.write(buffer);}}
}

3.2 客户端测试

客户端测试
你可以使用 telnetnetcat 命令来测试这个服务器:

telnet localhost 9090
# 或
nc localhost 9090

服务器端

在这里插入图片描述

客户端
在这里插入图片描述

四、NIO优缺点与实践经验

4.1 优点

  1. 高性能和高伸缩性:单线程即可管理大量连接,避免了线程创建和上下文切换的巨大开销。这是 NIO 最大的优势。
  2. 更少的资源消耗:与为每个连接创建一个线程相比,线程资源消耗要少得多。

4.2 缺点

  1. API 复杂:相比于 BIO,NIO 的 API 调用复杂得多,开发和调试难度大。
  2. 可靠性编程困难:需要处理许多边缘情况,如 write 不一定能一次写完(需要注册 OP_WRITE 并循环写),TCP 粘包/拆包问题(需要在应用层设计协议,如定长消息、分隔符、长度字段等)。
  3. Bug 与陷阱:如 SelectionKey 必须手动从集合中移除,否则会重复处理;对 ByteBuffer 的状态(position, limit)管理不当会导致错误。

4.3 实践经验总结

  1. 不要阻塞 Selector 线程Selector.select() 所在的线程是核心,其中的所有操作(如 handleRead)都必须快速非阻塞。任何耗时的操作(如数据库查询、复杂计算)都应该交给专门的业务线程池去处理,以免影响其他连接的响应。
  2. 管理好 Buffer
    • 考虑使用 Buffer 池来避免频繁的创建和销毁。
    • 深刻理解 flip(), clear(), rewind() 的含义。
  3. 处理写操作channel.write(buffer) 的返回值表示写入的字节数,它可能无法一次性写完缓冲区中的所有数据。如果需要写入大量数据,应在未写完时注册 OP_WRITE 事件,在 isWritable() 时继续写,写完后再取消注册,以避免 Selector 被不必要的写事件占满。
  4. 使用 Netty 等框架99% 的场景下,你不应该直接使用原生 NIO API 来编写网络应用。 框架如 NettyMina 完美地封装了 NIO 的复杂性,解决了上述所有缺点和陷阱(如粘包拆包、API复杂、线程模型),提供了强大、易用且高性能的抽象。学习 NIO 是为了更好地理解这些框架的原理。
http://www.xdnf.cn/news/1437391.html

相关文章:

  • 进程优先级(Process Priority)
  • 猫猫狐狐的“你今天有点怪怪的”侦察日记
  • CentOS7安装Nginx服务——为你的网站配置https协议和自定义服务端口
  • Java注解深度解析:从@ResponseStatus看注解奥秘
  • 大模型RAG项目实战:Pinecone向量数据库代码实践
  • 二叉树经典题目详解(下)
  • 【数据分享】31 省、342 个地级市、2532 个区县农业机械总动力面板数据(2000 - 2020)
  • MySQL数据库——概述及最基本的使用
  • Python实现浅拷贝的常用策略
  • Vite 插件 @vitejs/plugin-legacy 深度解析:旧浏览器兼容指南
  • 【Linux】信号量
  • 09.01总结
  • LeetCode算法日记 - Day 30: K 个一组翻转链表、两数之和
  • 基于Springboot和Vue的前后端分离项目
  • playwright+python UI自动化测试中实现图片颜色和像素对比
  • milvus使用
  • Hard Disk Sentinel:全面监控硬盘和SSD的健康与性能
  • Python学习-day4
  • 2026届长亭科技秋招正式开始
  • 算法 --- 模拟
  • NLP学习系列 | Transformer代码简单实现
  • Zephyr如何注册设备实例
  • [Java]PTA:jmu-Java-01入门-取数字浮点数
  • 自学嵌入式第三十三天:网络编程-UDP
  • Day19(前端:JavaScript基础阶段)
  • 分布式中防止重复消费
  • Spring Security的@PreAuthorize注解为什么会知道用户角色?
  • 开悟篇Docker从零到实战一篇文章搞定
  • 基于Python毕业设计推荐:基于Django的全国降水分析可视化系统
  • 战略咨询——解读81页中小企业企业战略规划方案【附全文阅读】