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

发布时间:2026/7/5 9:42:58

Netty[ NIO 核心速成 ] ---- NIO三大组件(Channel  Bufferselector) 前言我又做了一个新项目: 里面涉及到关于聊天啊, 通信那种内容吧, 于是抽出几天利用ai来速通一下Netty( 写的可能不好, 但我尽可能写全 )这里写目录标题前言首先来看看什么是NIO呢? 为啥学Netty就要学NIO呢?NIO基础NIO三大组件:Channel Buffer2.selector 设计:三的组件的使用ByteBuffer集装箱读写数据的缓冲区Channel通道数据传输的高速公路Selector管家管理所有 ChannelNIO 主从多线程 Reactor 模型首先来看看什么是NIO呢? 为啥学Netty就要学NIO呢?一、先对比平时写的 IO 代码 vs NIO 文件操作IO操作NIO 文件操作操作对象FileOutputStream、BufferedWriter 等流FileChannel ByteBuffer 通道 缓冲区读写方式「流模式」一字节 / 一行地读单向流动「块模式」整段数据读进缓冲区可双向操作切换模式读写性能频繁上下文切换大文件 / 高并发下慢减少拷贝次数大文件传输效率高代码风格流式调用逐行写比如 bw.write()先写缓冲区再刷到通道buffer.flip()channel.write()简单来说:你写的 BIO 是「小文件 / 低并发」场景的常规写法简单易上手但性能上限低NIO 的 FileChannel 是「大文件 / 高并发」场景的优化方案核心是缓冲区 通道减少数据拷贝这里再补充一个概念( 后面有用到 ):阻塞 BIO vs 非阻塞 NIOBIO阻塞 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 Bufferchannel 有一点类似于stream, 它类似一个数据通道( 既可以读数据也可以写数据 ) 将数据弄到buffer( 这个有点像缓存 )里常见的Channel:SocketChannelFileChannelServerSocketChannelDatagramChannel这里需要特别注意一个关键点并不是所有 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 正确使用姿势向 buffer 写入数据例如调用 channel.read(buffer)调用 flip() 切换至读模式从 buffer 读取数据例如调用 buffer.get()调用 clear() 或 compact() 切换至写模式publicclassTestByteBuffer{publicstaticvoidmain(String[]args){// FileChannel// 1. 输入输出流2. RandomAccessFiletry(FileChannelchannelnewFileInputStream(data.txt).getChannel()){// 准备缓冲区ByteBufferbufferByteBuffer.allocate(10);while(true){// 从 channel 读取数据向 buffer 写入intlenchannel.read(buffer);log.debug(读取到的字节数 {},len);if(len-1){// 没有内容了break;}// 打印 buffer 的内容buffer.flip();// 切换至读模式while(buffer.hasRemaining()){// 是否还有剩余未读数据bytebbuffer.get();log.debug(实际字节 {} ,(char)b);}buffer.clear();// 切换为写模式}}catch(IOExceptione){}}}ByteBuffer 的工作原理可简单总结为三个核心属性capacity容量缓冲区总大小固定不变。position位置当前读写指针写入时指向下一个可写位置读取时指向下一个可读位置。limit限制当前操作的上限写模式下等于 capacity读模式下等于已写入数据的末尾。✅ 简单说“写满翻转读读完清空再写” —— 通过 position 和 limit 控制读写边界flip/clear 实现模式切换高效复用缓冲区。两种模式切换写模式 → 读模式调用 flip()将 position 重置为 0limit 设为之前 position 的值即有效数据长度。读模式 → 写模式调用 clear() 或 compact()恢复 position0、limitcapacity准备重新写入。工作流程写入数据 → flip() 切换读模式 → 读取数据 → clear()/compact() 切换回写模式 → 循环重复。Channel通道数据传输的高速公路 Channel 正确使用姿势非阻塞模式创建并配置通道为非阻塞调用 ServerSocketChannel.open() 创建服务通道。立即调用 configureBlocking(false) 设置为非阻塞模式。绑定监听端口调用 bind(new InetSocketAddress(port)) 启动监听。此时通道开始接受连接请求但不会阻塞等待。非阻塞接受连接 管理客户端通道调用 ssc.accept() 获取新连接有连接 → 返回 SocketChannel无连接 → 返回 null线程不阻塞继续执行对新获得的 SocketChannel 也调用 configureBlocking(false)将其加入集合如 List统一管理。非阻塞读写数据 条件判断遍历所有已注册的 SocketChannel调用 channel.read(buffer)有数据 → 返回字节数 0 → 处理数据flip → read → clear无数据 → 返回 0 → 跳过不阻塞客户端断开 → 返回 -1 → 应移除该 channel本例未处理实际需补充写操作同理channel.write(buffer) 也可能只写部分需循环直到写完或返回 0。// 创建 ByteBufferByteBufferbufferByteBuffer.allocate(16);// 1. 创建服务器通道ServerSocketChannelsscServerSocketChannel.open();ssc.configureBlocking(false);// 设置为非阻塞模式// 2. 绑定监听端口ssc.bind(newInetSocketAddress(8080));// 3. 连接集合ListSocketChannelchannelsnewArrayList();while(true){// 4. accept 建立与客户端连接非阻塞log.debug(connecting...);SocketChannelscssc.accept();// 非阻塞无连接时返回 null线程继续运行if(sc!null){log.debug(connected... {},sc);sc.configureBlocking(false);// 客户端通道也设为非阻塞channels.add(sc);}// 5. 遍历所有已连接的客户端通道读取数据非阻塞for(SocketChannelchannel:channels){log.debug(before read... {},channel);intreadchannel.read(buffer);// 非阻塞无数据时返回 0不卡住if(read0){// 只有读到数据才处理buffer.flip();// 切换为读模式debugRead(buffer);// 打印内容buffer.clear();// 清空准备下次写入log.debug(after read...{},channel);}}}Selector管家管理所有 ChannelSelectionKey凭证Channel 和 Selector 的绑定关系 Selector Channel 正确使用姿势多路复用非阻塞模式创建 Selector 并注册通道调用 Selector.open() 创建选择器。创建 ServerSocketChannel → 设非阻塞 → 绑定端口。调用 channel.register(selector, OP_ACCEPT) 将服务通道注册到 Selector监听“接受连接”事件。阻塞等待事件发生调用 selector.select() —— 线程在此阻塞直到有客户端连接或有数据可读。一旦有事件方法返回线程恢复执行。遍历并处理就绪事件调用 selector.selectedKeys() 获取所有就绪的 SelectionKey。遍历每个 SelectionKey判断事件类型isAcceptable() / isReadable() / isWritable()根据类型执行对应操作accept() → 获取新 SocketChannel → 设非阻塞 → 注册 OP_READreadable() → 从 channel.read(buffer) 读取数据清理已处理事件 循环继续必须调用 iter.remove() 移除当前处理过的 SelectionKey否则下次循环会重复处理可选调用 key.cancel() 标记取消但不如 remove() 直接有效。回到步骤 2继续 select() 等待下一个事件。// 1. 创建 SelectorSelectorselectorSelector.open();// 2. 创建 ServerSocketChannel 并注册到 SelectorServerSocketChannelsscServerSocketChannel.open();ssc.configureBlocking(false);// 必须设为非阻塞ssc.bind(newInetSocketAddress(8080));// 注册 accept 事件到 selectorSelectionKeyssKeyssc.register(selector,SelectionKey.OP_ACCEPT);log.debug(register key: {},ssKey);while(true){// 3. select() 阻塞等待事件发生有连接或数据才唤醒selector.select();// 4. 获取所有已就绪的 SelectionKeySetSelectionKeyselectedKeysselector.selectedKeys();IteratorSelectionKeyiterselectedKeys.iterator();while(iter.hasNext()){SelectionKeykeyiter.next();log.debug(key: {},key);// 5. 根据事件类型处理if(key.isAcceptable()){// 如果是 accept 事件ServerSocketChannelchannel(ServerSocketChannel)key.channel();SocketChannelscchannel.accept();// 接受新连接sc.configureBlocking(false);// 新通道也必须非阻塞// 注册 read 事件到 selectorSelectionKeyscKeysc.register(selector,SelectionKey.OP_READ);log.debug({} registered for READ,sc);}elseif(key.isReadable()){// 如果是 read 事件SelectableChannelchannelkey.channel();// 拿到触发事件的 channel// TODO: 这里应该调用 channel.read(buffer) 处理数据// 实际项目中需配合 ByteBuffer 使用}// ⚠️ 重要手动移除已处理的 key避免重复处理iter.remove();// 或者key.cancel(); 但推荐用 iter.remove()}}NIO 主从多线程 Reactor 模型咱们先把这个模型的每个角色搞懂, 再来看看为什么要用这个模型Boss主 Reactor只有1 个线程 1 个 Selector只负责接收新连接不处理读写只把新连接分配给 WorkerWorker从 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 压力过大其他空闲小白啊写的不好轻喷啊如果觉得写的不好点个赞吧批评是我写作的动力…。。。。。。。。。。。……。。。。。。。。。。。…

相关新闻