Netty[ NIO 核心速成 ] ---- NIO三大组件(Channel & Buffer&selector)

前言

我又做了一个新项目: 里面涉及到关于聊天啊, 通信那种内容吧, 于是抽出几天利用ai来速通一下Netty( 写的可能不好, 但我尽可能写全 )

这里写目录标题

首先来看看什么是NIO呢? 为啥学Netty就要学NIO呢?

一、先对比:平时写的 IO 代码 vs NIO 文件操作

IO操作 NIO 文件操作
操作对象 FileOutputStream、BufferedWriter 等流 FileChannel + ByteBuffer 通道 + 缓冲区
读写方式 「流模式」:一字节 / 一行地读,单向流动 「块模式」:整段数据读进缓冲区,可双向操作切换模式读写
性能 频繁上下文切换,大文件 / 高并发下慢 减少拷贝次数,大文件传输效率高
代码风格 流式调用,逐行写(比如 bw.write()) 先写缓冲区,再刷到通道(buffer.flip()+channel.write())

简单来说:

  • 你写的 BIO 是「小文件 / 低并发」场景的常规写法,简单易上手,但性能上限低
  • NIO 的 FileChannel 是「大文件 / 高并发」场景的优化方案,核心是缓冲区 + 通道,减少数据拷贝

这里再补充一个概念( 后面有用到 ):

阻塞 BIO vs 非阻塞 NIO
BIO(阻塞 IO):

  • 一个线程只能管一个连接
  • 调用 accept()、read() 会卡住线程,死等
  • 连接一多,线程爆炸,服务器扛不住

NIO(非阻塞 IO):

  • 一个线程可以管理成千上万个连接
  • accept()、read() 不会卡住线程
  • 没有事件就去干别的,有事件再处理
  • 这就是高并发通信的基础(聊天、推送、网游都用它)

一句话:非阻塞 = 不傻等,一个线程管理一堆连接。

二、NIO 和 Netty 的关系:Netty 是 NIO 的豪华升级版🫣

为什么所有 Netty 教程都先学 NIO?因为 Netty 本质是对 JDK NIO 的「封装 + 优化 + 增强」,没有 NIO 基础,学 Netty 就是「空中楼阁」。

NIO基础

NIO三大组件:

NIO的三大组件分为: Selector, ByteBuffer 和 Channel , 因为NIO主要是由Selector控制着channel将数据读入写入buffer. 所以这仨分开来看:

Channel & Buffer

channel 有一点类似于stream, 它类似一个数据通道( 既可以读数据也可以写数据 ) 将数据弄到buffer( 这个有点像缓存 )里

常见的Channel:

  • SocketChannel
  • FileChannel
  • ServerSocketChannel
  • DatagramChannel

这里需要特别注意一个关键点:

并不是所有 Channel 都支持非阻塞模式。

  • FileChannel 只能运行在阻塞模式,不能切换非阻塞;
  • 而 SocketChannel、ServerSocketChannel 这类网络通信 Channel,支持非阻塞模式

这也是为什么网络 NIO 必须使用 ServerSocketChannel + SocketChannel ------
只有支持非阻塞的 Channel,才能注册到 Selector 中,实现一个线程管理成千上万个连接。

换句话说:
Selector 只认非阻塞的 Channel,阻塞 Channel 无法注册到 Selector!

所以后面我们写 NIO 网络代码时,一定会加上这行关键代码:

channel.configureBlocking(false);

这是使用 Selector 的前提,也是实现高并发通信的基础。

2.selector 设计:

selector的作用就是配合一个线程来管理多个channel, 获取这些channel上发生的事件( channel必须工作在非阻塞模式下 ), 适合连接数多但流量低的场景.

三的组件的使用

ByteBuffer(集装箱:读写数据的缓冲区)


ByteBuffer 正确使用姿势

  1. 向 buffer 写入数据,例如调用 channel.read(buffer)
  2. 调用 flip() 切换至读模式
  3. 从 buffer 读取数据,例如调用 buffer.get()
  4. 调用 clear() 或 compact() 切换至写模式
java 复制代码
public class TestByteBuffer {
    public static void main(String[] args) {
        // FileChannel
        // 1. 输入输出流,2. RandomAccessFile
        try (FileChannel channel = new FileInputStream("data.txt").getChannel()) {
            // 准备缓冲区
            ByteBuffer buffer = ByteBuffer.allocate(10);
            while(true) {
                // 从 channel 读取数据,向 buffer 写入
                int len = channel.read(buffer);
                log.debug("读取到的字节数 {}", len);
                if(len == -1) { // 没有内容了
                    break;
                }
                // 打印 buffer 的内容
                buffer.flip(); // 切换至读模式
                while(buffer.hasRemaining()) { // 是否还有剩余未读数据
                    byte b = buffer.get();
                    log.debug("实际字节 {} ", (char) b);
                }
                buffer.clear(); // 切换为写模式
            }
        } catch (IOException e) {
        }
    }
}

🙌🙌🙌ByteBuffer 的工作原理可简单总结为:

三个核心属性:

  1. capacity(容量):缓冲区总大小,固定不变。
  2. position(位置):当前读写指针,写入时指向下一个可写位置,读取时指向下一个可读位置。
  3. limit(限制):当前操作的上限,写模式下等于 capacity,读模式下等于已写入数据的末尾。

✅ 简单说:"写满翻转读,读完清空再写" ------ 通过 position 和 limit 控制读写边界,flip/clear 实现模式切换,高效复用缓冲区。

两种模式切换:

写模式 → 读模式:调用 flip(),将 position 重置为 0,limit 设为之前 position 的值(即有效数据长度)。

读模式 → 写模式:调用 clear() 或 compact(),恢复 position=0、limit=capacity,准备重新写入。

工作流程:

写入数据 → flip() 切换读模式 → 读取数据 → clear()/compact() 切换回写模式 → 循环重复。

Channel(通道:数据传输的高速公路)


🚀 Channel 正确使用姿势(非阻塞模式)

  1. 创建并配置通道为非阻塞
  • 调用 ServerSocketChannel.open() 创建服务通道。
  • 立即调用 configureBlocking(false) 设置为非阻塞模式。
  1. 绑定监听端口
  • 调用 bind(new InetSocketAddress(port)) 启动监听。
    此时通道开始接受连接请求,但不会阻塞等待。
  1. 非阻塞接受连接 & 管理客户端通道
  • 调用 ssc.accept() 获取新连接:
    • 有连接 → 返回 SocketChannel
    • 无连接 → 返回 null,线程不阻塞,继续执行
  • 对新获得的 SocketChannel 也调用 configureBlocking(false)
    将其加入集合(如 List)统一管理。
  1. 非阻塞读写数据 + 条件判断
  • 遍历所有已注册的 SocketChannel
  • 调用 channel.read(buffer):
    • 有数据 → 返回字节数 > 0 → 处理数据(flip → read → clear)
    • 无数据 → 返回 0 → 跳过,不阻塞
    • 客户端断开 → 返回 -1 → 应移除该 channel(本例未处理,实际需补充)
  • 写操作同理:channel.write(buffer) 也可能只写部分,需循环直到写完或返回 0。
java 复制代码
   // 创建 ByteBuffer
        ByteBuffer buffer = ByteBuffer.allocate(16);

        // 1. 创建服务器通道
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false); // 设置为非阻塞模式

        // 2. 绑定监听端口
        ssc.bind(new InetSocketAddress(8080));

        // 3. 连接集合
        List<SocketChannel> channels = new ArrayList<>();

        while (true) {
            // 4. accept 建立与客户端连接(非阻塞)
            log.debug("connecting...");
            SocketChannel sc = ssc.accept(); // 非阻塞:无连接时返回 null,线程继续运行
            if (sc != null) {
                log.debug("connected... {}", sc);
                sc.configureBlocking(false); // 客户端通道也设为非阻塞
                channels.add(sc);
            }

            // 5. 遍历所有已连接的客户端通道,读取数据(非阻塞)
            for (SocketChannel channel : channels) {
                log.debug("before read... {}", channel);
                int read = channel.read(buffer); // 非阻塞:无数据时返回 0,不卡住
                if (read > 0) { // 只有读到数据才处理
                    buffer.flip();        // 切换为读模式
                    debugRead(buffer);    // 打印内容
                    buffer.clear();       // 清空,准备下次写入
                    log.debug("after read...{}", channel);
                }
            }
        }
Selector(管家:管理所有 Channel)


SelectionKey(凭证:Channel 和 Selector 的绑定关系)

🚀 Selector + Channel 正确使用姿势(多路复用非阻塞模式)

  1. 创建 Selector 并注册通道
  • 调用 Selector.open() 创建选择器。
  • 创建 ServerSocketChannel → 设非阻塞 → 绑定端口。
  • 调用 channel.register(selector, OP_ACCEPT) 将服务通道注册到 Selector,监听"接受连接"事件。
  1. 阻塞等待事件发生
  • 调用 selector.select() ------ 线程在此阻塞,直到有客户端连接或有数据可读。
  • 一旦有事件,方法返回,线程恢复执行。
  1. 遍历并处理就绪事件
  • 调用 selector.selectedKeys() 获取所有就绪的 SelectionKey。
  • 遍历每个 SelectionKey:
    • 判断事件类型:isAcceptable() / isReadable() / isWritable()
    • 根据类型执行对应操作:
      • accept() → 获取新 SocketChannel → 设非阻塞 → 注册 OP_READ
      • readable() → 从 channel.read(buffer) 读取数据
  1. 清理已处理事件 & 循环继续
  • 必须调用 iter.remove() 移除当前处理过的 SelectionKey,否则下次循环会重复处理!
  • (可选)调用 key.cancel() 标记取消,但不如 remove() 直接有效。
  • 回到步骤 2,继续 select() 等待下一个事件。
java 复制代码
 // 1. 创建 Selector
        Selector selector = Selector.open();

        // 2. 创建 ServerSocketChannel 并注册到 Selector
        ServerSocketChannel ssc = ServerSocketChannel.open();
        ssc.configureBlocking(false); // 必须设为非阻塞
        ssc.bind(new InetSocketAddress(8080));

        // 注册 accept 事件到 selector
        SelectionKey ssKey = ssc.register(selector, SelectionKey.OP_ACCEPT);
        log.debug("register key: {}", ssKey);

        while (true) {
            // 3. select() 阻塞等待事件发生(有连接或数据才唤醒)
            selector.select();

            // 4. 获取所有已就绪的 SelectionKey
            Set<SelectionKey> selectedKeys = selector.selectedKeys();
            Iterator<SelectionKey> iter = selectedKeys.iterator();

            while (iter.hasNext()) {
                SelectionKey key = iter.next();
                log.debug("key: {}", key);

                // 5. 根据事件类型处理
                if (key.isAcceptable()) { // 如果是 accept 事件
                    ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                    SocketChannel sc = channel.accept(); // 接受新连接
                    sc.configureBlocking(false);         // 新通道也必须非阻塞
                    // 注册 read 事件到 selector
                    SelectionKey scKey = sc.register(selector, SelectionKey.OP_READ);
                    log.debug("{} registered for READ", sc);
                } else if (key.isReadable()) { // 如果是 read 事件
                    SelectableChannel channel = key.channel(); // 拿到触发事件的 channel
                    // TODO: 这里应该调用 channel.read(buffer) 处理数据
                    // 实际项目中需配合 ByteBuffer 使用
                }

                // ⚠️ 重要:手动移除已处理的 key,避免重复处理
                iter.remove();
                // 或者:key.cancel(); (但推荐用 iter.remove())
            }
        }

NIO 主从多线程 Reactor 模型

咱们先把这个模型的每个角色搞懂, 再来看看为什么要用这个模型:

Boss(主 Reactor):

  • 只有 1 个线程 + 1 个 Selector
  • 只负责接收新连接
  • 不处理读写,只把新连接分配给 Worker

Worker(从 Reactor):

  • 图里有 worker0、worker1 两个,每个都是 1 个线程 + 1 个 Selector
  • 负责已建立连接的读写事件
  • 多个 Worker 可以并行处理,充分利用多核 CPU

🔍 为啥要这么设计?

1. 职责分离:

  • Boss 只干 "接通客户端"(accept),不处理读写,避免新连接等待
  • Worker 只干 "服务"(read/write),多个 Worker 并行处理,把多核 CPU 用满

2. 避免单 Selector 瓶颈:

  • 如果只用 1 个 Selector 管所有连接,高并发下 Selector 遍历事件会成为瓶颈
  • 拆成多个 Selector,每个管一部分连接,并行处理,吞吐量更高

3. 负载均衡:

  • 新连接来了,Boss 会把它均匀分配到不同 Worker 上(比如图里用 AtomicInteger 做轮询)
  • 避免某个 Worker 压力过大,其他空闲

小白啊!!!写的不好轻喷啊🤯如果觉得写的不好,点个赞吧🤪(批评是我写作的动力)

...。。。。。。。。。。。...

...。。。。。。。。。。。...

相关推荐
腻害兔5 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
zzqssliu5 小时前
煤炉自动代拍系统的队列设计与超时控制机制
git·github
码智社6 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
萧瑟余晖7 小时前
JDK 26 新特性详解
java·开发语言
马优晨7 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
fthux8 小时前
“装闭”,让装修套路“装”不下去
人工智能·ai·开源·github·open source
维天说10 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json
Zane199411 小时前
并发 vs 并行:别再傻傻分不清了,一文讲透 Java 并发编程的第一课
java·后端
用户849137175471611 小时前
想做护眼工具却脑子一片空白?我用 OpenSpec 把模糊想法聊成了 v0.1
github·vibecoding
wangruofeng11 小时前
git-filter-repo 把 .git 从 112MB 砍到 1.4MB,但漏推 tag 让 clone 又胖回来
github·devops