Selector能单线程管理多个连接,但需正确实现注册、轮询、分发三步:Channel须非阻塞,注册时指定兴趣集,select后遍历并remove keys;read返回-1需关闭连接,返回0非错误;避免业务阻塞事件循环,缓冲区操作须严谨。

Selector 是什么,它真能单线程管多个连接?
能,但前提是用对了——Selector 本身不处理 I/O,它只告诉你「哪个 Channel 就绪了」。Java NIO 的 Selector 是基于操作系统 epoll(Linux)或 kqueue(macOS)的封装,本质是 I/O 多路复用,不是并发模型。你得自己写循环轮询 select()、自己处理 OP_READ/OP_WRITE 事件、自己管理缓冲区和连接状态。
关键三步:注册、轮询、分发
漏掉任一环,连接就会卡住或丢失事件:
-
Channel必须设为非阻塞模式(configureBlocking(false)),否则注册会抛IllegalBlockingModeException - 注册时要显式指定兴趣集,比如
channel.register(selector, SelectionKey.OP_READ);刚连上的SocketChannel如果立即注册OP_READ,可能还没收到数据,但后续有数据到达时select()才会唤醒 - 每次
select()返回后,必须遍历selectedKeys()并手动remove()当前 key,否则下次select()可能重复触发(尤其在未处理完数据就返回循环时)
read() 返回 0 或 -1 怎么处理?
这是最容易被忽略的断连信号:
-
read()返回-1:对端已关闭连接(FIN),应调用key.cancel()+channel.close() -
read()返回0:TCP 窗口满或对方发了零长包,不是错误,但需注意是否已读完应用层完整消息(比如按 \n 或长度头解析时) - 不要在
OP_READ处理中直接write();如果要回包,应把响应数据写入ByteBuffer缓存,并改注册OP_WRITE,等下一次select()通知可写再发——否则可能因 socket 发送缓冲区满而阻塞整个线程
为什么写了 Selector 还是卡在某个连接上?
问题往往出在业务逻辑阻塞了事件循环线程:
- 在
OP_READ回调里做 JSON 解析、DB 查询、HTTP 调用等耗时操作,会拖慢整个 selector 轮询节奏,其他连接饿死 - 正确做法:把重活扔给线程池(如
ForkJoinPool.commonPool()),用CompletableFuture或回调衔接;Selector线程只负责收发字节和状态机跳转 - 缓冲区没清空(
buffer.clear()或flip()错误)会导致反复读到旧数据,看起来像“卡住”
真正难的不是写通 Selector,而是把协议解析、心跳、重连、背压控制这些都塞进单线程事件循环里还不崩——每个环节少一个 clear(),多一次阻塞调用,就离超时和连接泄漏不远了。

















