一个线程如何管理成千上万条连接?关键不是“同时执行所有连接”,而是让操作系统通知哪些连接当前可以继续读写。这就是 I/O 多路复用。
Table of contents
Open Table of contents
运行 Selector 回声服务器
下载NIO 回声服务器下载,运行真实 Socket 测试:
mvn -Dtest=NetworkServerLabTest#echoesThroughARealNioSocket test
服务器把 ServerSocketChannel 注册到 Selector,关注接受连接事件;新连接再注册读取事件。select() 返回后必须遍历并移除已选择的 key。
三种机制解决同一个问题
select 每次传入文件描述符集合,集合大小受实现限制,内核和用户态需要反复复制并扫描。poll 使用可变数组,摆脱固定集合位图限制,
但仍随关注数量线性扫描。
Linux 的 epoll 在内核维护关注集合,应用等待就绪事件列表,不必每轮重新提交全部连接。它适合“连接很多、同一时刻活跃连接较少”的服务器,
但业务处理耗时仍决定最终吞吐。
Java Selector 是跨平台抽象:Linux 通常映射到 epoll,macOS 可能使用 kqueue。应用不应依赖某个平台的内部实现细节来保证正确性。
就绪不等于操作一定完成
读就绪表示读取大概率不会阻塞,但一次 read 可能只得到半条消息,也可能返回 0;写就绪表示发送缓冲区有空间,不代表整条响应一次写完。
因此每个连接都要保存尚未解析的输入和尚未发送的输出状态。
水平触发会在条件仍满足时重复通知,较容易写对;边沿触发只在状态变化时通知,处理方通常要读到返回“暂时无数据”为止,否则可能错过后续机会。
事件循环的正确边界
事件循环应该快速完成:接受连接、读取有限字节、推进协议状态、提交耗时任务、写出已完成结果。不要在循环中执行文件大读、远程调用、长时间锁等待或无界计算。
需要观测:
- 每轮循环耗时和任务队列长度;
- 连接接受速率、活跃连接数与失败数;
- 每连接待读/待写字节;
- CPU、上下文切换和系统调用;
- 事件循环延迟,而不只是业务平均耗时。
常见故障
空轮询会让 select 立即返回却没有有效事件,表现为单核 CPU 飙高。连接忘记取消注册或关闭会泄漏文件描述符。只关注读、不处理写积压会让内存持续增长。
先保存线程栈、系统调用和连接指标证据,再判断是 JDK、框架还是应用状态机问题。
下一步
阅读Netty 快速入门,用框架提供的 Channel、EventLoop 和 Pipeline 管理这些状态。