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 压力过大,其他空闲

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

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

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

相关推荐
小王不爱笑1322 小时前
Java 异常全解析:从原理到实战,搞定异常处理
java·开发语言
人工智能AI技术2 小时前
Spring Boot 3.5正式普及!Java虚拟线程+GraalVM原生镜像,启动仅0.3秒
java
没有bug.的程序员2 小时前
撕裂微服务网关的认证风暴:Spring Security 6.1 与 JWT 物理级免登架构大重构
java·spring·微服务·架构·security·jwt
小王不爱笑1322 小时前
Java Set 集合全家桶:HashSet、LinkedHashSet、TreeSet 详解与实战
java·开发语言
杨过姑父2 小时前
java 面试,jvm笔记
java·jvm·面试
mldlds2 小时前
Spring Boot应用关闭分析
java·spring boot·后端
woniu_buhui_fei2 小时前
Java 服务最常见的线上性能故障
java·jvm·算法
96772 小时前
Java 类映射数据库表的核心规则
java·数据库·oracle
阳光下的米雪2 小时前
存储过程的使用以及介绍
java·服务器·数据库·pgsql