Java 网络编程的核心在于理解 NIO(Non-blocking I/O) 模型,它通过三大核心组件 Channel、Buffer 和 Selector 实现了高效的高并发处理。以下是基于这三大组件及关键机制的网络编程小结:
一、 三大核心组件
1. Channel(通道
Channel 是双向的数据传输通道,类似于传统 IO 中的流,但功能更强大。
特点:支持非阻塞模式,数据必须通过 Buffer 进行读写。
常见类型:
SocketChannel:用于 TCP 客户端和服务端连接。
ServerSocketChannel:专用于服务端监听新连接。
DatagramChannel:用于 UDP 通信。
FileChannel:用于文件读写。
2. Buffer(缓冲区)
Buffer 是一块内存区域,用于暂存读写数据。所有与 Channel 的数据交互都必须经过 Buffer。
核心属性:
capacity:容量,固定不变。
position:当前读写位置。
limit:读写限制,写模式下等于 capacity,读模式下等于实际数据大小。
常用操作:
put()/get():写入/读取数据。
flip():写转读。将 limit 设为当前 position,position 重置为 0。
clear():清空准备写。position 重置为 0,limit 重置为 capacity(注意:不清除数据,只是重置指针)。
compact():压缩准备写。将未读数据复制到缓冲区头部,position 指向未读数据之后,适合半包处理。
分配方式:
ByteBuffer.allocate():堆内存。受 GC管理,分配快,但 IO 时需拷贝到直接内存,效率略低。
ByteBuffer.allocateDirect():直接内存(堆外)。不受 GC 直接影响,IO 效率高(零拷贝优势),但分配和回收成本高需手动管理或依赖 Cleaner。
3. Selector(选择器)
Selector 允许单线程管理多个 Channel,是实现 IO 多路复用的关键。
工作原理:
将 Channel 注册到 Selector,并指定感兴趣的事件(OP_ACCEPT, OP_CONNECT, OP_READ, OP_WRITE)。
调用 select() 阻塞等待,直到有通道就绪。
通过 selectedKeys() 获取就绪的 SelectionKey 集合。
遍历集合,根据事件类型处理业务逻辑。
适用场景:连接数多、但每个连接流量较低的场景(如聊天服务器、网关)。
注意事项:
Channel 必须设置为非阻塞模式 (configureBlocking(false))才能注册到 Selector。
每次处理完事件后,必须从 selectedKeys 集合中移除该 Key (iterator.remove()),否则会导致重复处理。

二、 关键机制与最佳实践
1. ByteBuffer 的正确使用流程
写数据:channel.read(buffer) 或 buffer.put()。
切换读模式:调用 buffer.flip()。
读数据:buffer.get() 或 channel.write(buffer)。
切换写模式:调用 buffer.clear()(全清空)或 buffer.compact()(保留未读数据)。
2. Selector 写事件(OP_WRITE)的处理陷阱
问题:如果一直注册 OP_WRITE,只要发送缓冲区有空闲,Selector 就会不断触发写事件,导致 CPU 100% 空转。
正确策略:
尝试直接写:业务需要发送数据时,先直接调用 channel.write()。
判断剩余:如果 buffer.hasRemaining() 为 true(没写完),说明内核缓冲区满。
注册事件:此时才注册 OP_WRITE,并将 Buffer 绑定到 Key 的 attachment。
监听可写:在 isWritable() 事件中继续写入剩余数据。
取消注册:数据一旦写完,立即取消 OP_WRITE 监听,改回只监听 OP_READ。
3. 粘包/拆包处理
由于 TCP 是流式协议,数据没有边界。
现象:多次发送的小数据包可能被合并接收(粘包),或一个大包被分多次接收(拆包)。
解决:应用层需定义协议,如"长度字段+内容"(TLV格式)。读取时先读固定长度的头部获取 body 长度,若 Buffer 中数据不足则等待下一次读事件,或使用 compact() 保留已读部分。
4. 内存管理与性能优化
堆外内存泄漏:大量使用 allocateDirect 时,需注意 -XX:MaxDirectMemorySize 参数,避免直接内存溢出。
池化技术:在高并发场景下,频繁创建/销毁 Buffer 开销大。建议使用对象池(如 Netty 的 PooledByteBufAllocator)复用 Buffer 对象。
线程模型:原生 NIO 通常采用 Reactor 模式(单线程或多线程 Reactor),将 IO 监听和业务处理分离,避免阻塞 IO 线程。
三、 总结对比
| 特性 | BIO (Blocking IO) | NIO (Non-blocking IO) |
|---|---|---|
| 模型 | 面向流,阻塞 | 面向缓冲区,非阻塞 |
| 并发处理 | 一连接一线程 | 单线程管理多连接 (Selector) |
| 资源消耗 | 线程开销大,适合少连接 | CPU 轮询开销,适合多连接低流量 |
| 核心组件 | InputStream/OutputStream | Channel, Buffer, Selector |
| 适用场景 | 连接数少且长连接,高带宽 | 连接数巨大,轻量级通信 (IM, 网关) |
掌握 NIO 的关键在于理解 Buffer 的状态切换 以及 Selector 的事件驱动机制,特别是正确处理写事件的注册与取消,是构建高性能 Java 网络服务的基础。