源码剖析 Java NIO 核心三组件:Selector、SelectionKey 与 Channel 的实现原理

发布时间:2026/9/13 18:25:17
源码剖析 Java NIO 核心三组件:Selector、SelectionKey 与 Channel 的实现原理 源码剖析 Java NIO 核心三组件Selector、SelectionKey 与 Channel 的实现原理【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter导读Selector、SelectionKey 和 Channel 三个组件构成了 Java NIO 包的核心也是 Reactor 模型在代码层面的直接体现。本文以 JDK 抽象类源码为主线逐一拆解 Selector 的轮询机制、SelectionKey 的事件标记模型以及 SelectableChannel/ServerSocketChannel/SocketChannel 的注册与读写接口并结合仓库中 Netty 的 EventLoop 组件 与 NIO 实战代码帮助读者彻底理解单线程如何管理成千上万条连接这一 NIO 底层原理为阅读 Netty 源码打下坚实基础。三组件如何构成 Reactor 模型Java NIO 与传统的 BIO 最大的区别在于BIO 中一个客户端连接通常需要一个独立线程处理而 NIO 允许一个线程通过 Selector 同时管理多个客户端 Channel非常适合高并发、传输数据量较小的场景。要使用 Selector首先要将对应的 Channel 及 IO 事件读、写、连接注册到 Selector。注册后会产生一个SelectionKey 对象它同时关联 Selector 和 Channel是后续所有 IO 事件处理的句柄。三者的协作关系如下图所示从架构视角看这三者正好对应了 Reactor反应器模式的核心角色Channel通道相当于被监听的事件源对应 Reactor 中的输入Selector多路复用器相当于事件分离器Event Demultiplexer负责轮询哪些 Channel 上发生了感兴趣的事件SelectionKey选择键相当于事件 事件源的绑定体事件就绪后处理器通过它拿到对应的 Channel 与就绪事件类型。正如仓库文档 把被说烂的BIO、NIO、AIO再从头到尾扯一遍 所述NIO 中有四种事件connectable、acceptable、readable、writable我们为每一种事件编写处理器并设置每个 socket 要监听哪种事件随后由 Selector 调用对应的处理器——这就是事件驱动思想在 Java 层面的落地。而对 nio 编程不熟的同学可以先用简单 demo 跑通流程下面我们直接进入源码窥探一些 nio 的奥秘。Selector 抽象类源码分析不管 Selector 还是 SelectionKey其具体实现类都依赖于底层操作系统Windows 上对应WindowsSelectorImplLinux 上对应EPollSelectorImplJDK 通过SelectorProvider完成平台相关的实例创建。这里我们只看抽象类Selector的源码public abstract class Selector implements Closeable { protected Selector() { } /** * 获取一个 Selector对象具体实现依赖于底层操作系统 */ public static Selector open() throws IOException { return SelectorProvider.provider().openSelector(); } /** * 判断该 Selector 是否已开启 */ public abstract boolean isOpen(); /** * 当前所有向Selector注册的Channel 所对应的SelectionKey的集合 */ public abstract SetSelectionKey keys(); /** * 相关事件已经被 Selector 捕获的 SelectionKey的集合 */ public abstract SetSelectionKey selectedKeys(); /** * 阻塞到至少有一个通道在你注册的事件上就绪了 */ public abstract int select() throws IOException; /** * 和select()一样除了最长会阻塞timeout毫秒 */ public abstract int select(long timeout) throws IOException; /** * 此方法执行非阻塞的选择操作如果自从上一次选择操作后 * 没有通道变成可选择的则此方法直接返回 0 */ public abstract int selectNow() throws IOException; /** * 用完Selector后调用其close()方法会关闭该Selector且使注册到该Selector上的所有SelectionKey实例无效 * 通道本身并不会关闭 */ public abstract void close() throws IOException; }几个关键点值得展开1.open()与 SelectorProviderSelector.open()并非直接 new 一个对象而是通过SelectorProvider.provider()获取当前平台默认的 Provider再调用其openSelector()创建实例。Linux 下底层对应的是epoll多路复用技术JDK 7 之后Windows 下对应 select 模型Mac 下对应 kqueue。这正是仓库文档 IO模型 中提到的Java NIO 的核心类库中多路复用器 Selector 就是基于 epoll 的多路复用技术实现。2.keys()与selectedKeys()的区别keys()返回所有注册到该 Selector 上的 Channel 对应的 SelectionKey 集合无论事件是否就绪selectedKeys()返回已就绪相关事件被 Selector 捕获的 SelectionKey 集合这才是业务代码轮询处理的目标。3.select()/select(timeout)/selectNow()的选择方法阻塞行为返回值select()阻塞直到至少一个注册通道就绪就绪通道个数select(long timeout)最长阻塞 timeout 毫秒就绪通道个数超时返回 0selectNow()非阻塞立即返回就绪通道个数没有就绪直接返回 0三者返回的都是本次轮询到的事件数量通常大于 0 时才去遍历selectedKeys()处理事件。4.close()的注意点关闭 Selector 会使注册到其上的所有 SelectionKey 实例失效但通道本身并不会被关闭。这一点在资源清理时尤其重要关闭 Selector 不等于关闭 ChannelChannel 需要单独调用close()。SelectionKeyChannel 与 Selector 的桥梁SelectionKey 表示 SelectableChannel 在 Selector 中的注册标记/句柄一个 SelectionKey 对应一个 Channel 一个 Selector 一组兴趣事件的三元组。public abstract class SelectionKey { protected SelectionKey() { } // -- Channel and selector operations -- /** * 获取该 SelectionKey 对应的ChannelChannel注册到Selector时会产生该 SelectionKey对象 */ public abstract SelectableChannel channel(); /** * 获取该 SelectionKey 对应的 Selector */ public abstract Selector selector(); /** * 该 SelectionKey 是否是有效的 */ public abstract boolean isValid(); // ------ Operation-set accessors ------ /** * 获取该 SelectionKey 的兴趣事件 (既 SelectionKey 的4个 事件静态常量) */ public abstract int interestOps(); /** * 设置该 SelectionKey 的兴趣事件 */ public abstract SelectionKey interestOps(int ops); /** * 获取该 SelectionKey 的已操作集 */ public abstract int readyOps(); // ------ Operation bits and bit-testing convenience methods ------ /** * channel中的数据是否已经可以读取 */ public static final int OP_READ 1 0; /** * channel是否可以开始写入数据 */ public static final int OP_WRITE 1 2; /** * channel是否已经建立连接 */ public static final int OP_CONNECT 1 3; /** * ServerSocketChannel 是否可以与客户端建立连接 */ public static final int OP_ACCEPT 1 4; /** * channel是否可读 */ public final boolean isReadable() { return (readyOps() OP_READ) ! 0; } /** * channel是否可写 */ public final boolean isWritable() { return (readyOps() OP_WRITE) ! 0; } /** * channel是否建立连接 */ public final boolean isConnectable() { return (readyOps() OP_CONNECT) ! 0; } /** * ServerSocketChannel是否可与客户端channel建立连接 */ public final boolean isAcceptable() { return (readyOps() OP_ACCEPT) ! 0; } }四个事件常量位掩码的设计四个事件常量采用位掩码bit mask方式定义每个值在 int 中占用不同 bit常量值适用 Channel含义OP_READ1 0 1SocketChannelchannel 中的数据已经可以读取OP_WRITE1 2 4SocketChannelchannel 可以开始写入数据OP_CONNECT1 3 8SocketChannel客户端channel 已经建立连接OP_ACCEPT1 4 16ServerSocketChannel服务端可以接受新连接位掩码的好处是可以用一个 int 同时表达多种兴趣事件如OP_READ | OP_WRITE并通过按位与运算快速判断某事件是否就绪。源码中的isReadable()等四个便捷方法本质就是(readyOps() OP_READ) ! 0这样的位测试。interestOps 与 readyOps 的区别interestOps兴趣集注册时我们希望监听的事件可以随时通过interestOps(int ops)修改例如写完数据后改回只监听读事件readyOps已操作集Selector 检测到的实际已经就绪的事件由操作系统内核更新业务代码通过它来判断接下来可以做什么。仓库文档 把被说烂的BIO、NIO、AIO再从头到尾扯一遍 中特别提醒写就绪比较特殊。写操作的就绪条件是底层缓冲区有空闲空间而写缓冲区绝大部分时间都有空闲空间所以一旦注册写事件它几乎总是就绪会让选择处理线程一直忙碌。因此只有确实有数据要写时才注册 OP_WRITE写完以后马上取消注册。Channel 组件族谱平时编码用得比较多的就是 SocketChannel 和 ServerSocketChannel而将 Channel 与 Selector 关联到一起的核心 API 则定义在它们的公共父类SelectableChannel中。整个 Channel 组件的核心类图如下所示Channel 接口所有通道的顶层接口定义close()、isOpen()等基础能力AbstractInterruptibleChannel提供可中断关闭的骨架实现SelectableChannel可被 Selector 选择的通道register()注册接口的所在地ServerSocketChannel / SocketChannel对应 TCP 服务端与客户端的具体通道。SelectableChannel注册能力的源头public abstract class SelectableChannel extends AbstractInterruptibleChannel implements Channel { protected SelectableChannel() { } /** * 当前channel是否注册到了某个selector上新创建的channel都是未注册状态 */ public abstract boolean isRegistered(); /** * 根据给定的 Selector获取本channel注册上去的 SelectionKey */ public abstract SelectionKey keyFor(Selector sel); /** * 将当前channel及关注的事件注册到Selector上返回一个 SelectionKey */ public final SelectionKey register(Selector sel, int ops) throws ClosedChannelException { return register(sel, ops, null); } public abstract SelectionKey register(Selector sel, int ops, Object att) throws ClosedChannelException; /** * 设置该channel的阻塞模式默认为 true阻塞 */ public abstract SelectableChannel configureBlocking(boolean block) throws IOException; /** * 是否为阻塞IO模式 */ public abstract boolean isBlocking(); }关键 API 说明register(Selector sel, int ops)将当前 channel 及关注的事件注册到 Selector 上返回一个 SelectionKey。注意只有非阻塞模式的 channel 才能注册到 Selector所以注册前必须先调用configureBlocking(false)register(sel, ops, Object att)注册时还可以携带一个附件对象如业务 Handler事件就绪后通过SelectionKey.attachment()取回这是 NIO 编程中传递上下文信息的常用手段keyFor(Selector sel)查询该 channel 在某个 Selector 上的注册记录未注册则返回 nullconfigureBlocking(boolean block)切换阻塞/非阻塞模式默认是 true阻塞。切换会抛IllegalBlockingModeException的约束是已注册到 Selector 的 channel 不能再切换阻塞模式必须先将 key 取消。ServerSocketChannel服务端的门卫相当于 BIO 中的 ServerSocket主要用于服务端与客户端建立连接通信的 channel。public abstract class ServerSocketChannel extends AbstractSelectableChannel implements NetworkChannel { protected ServerSocketChannel(SelectorProvider provider) { super(provider); } /** * 获取一个 ServerSocketChannel实例具体实现依赖底层操作系统 */ public static ServerSocketChannel open() throws IOException { return SelectorProvider.provider().openServerSocketChannel(); } // -- ServerSocket-specific operations -- /** * 绑定ip地址及要监听的端口 */ public final ServerSocketChannel bind(SocketAddress local) throws IOException { return bind(local, 0); } public abstract ServerSocketChannel bind(SocketAddress local, int backlog) throws IOException; /** * 与一个客户端channel建立连接返回该客户端的存根 SocketChannel */ public abstract SocketChannel accept() throws IOException; }open()同样是走SelectorProvider获取平台相关实现bind(SocketAddress local)绑定监听地址与端口backlog表示内核连接队列的最大长度默认 0使用系统默认值accept()在非阻塞模式下会立即返回有连接就返回对应的 SocketChannel没有则返回 null这是与 BIO 的ServerSocket.accept()阻塞行为最大的不同。SocketChannel通信双方的水管相当于 BIO 中的 Socket主要用于通信双方的读写操作。public abstract class SocketChannel extends AbstractSelectableChannel implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel { protected SocketChannel(SelectorProvider provider) { super(provider); } /** * 根据 SocketAddress 获取一个 SocketChannel具体实现依赖底层操作系统 */ public static SocketChannel open(SocketAddress remote) throws IOException { SocketChannel sc open(); try { sc.connect(remote); } catch (Throwable x) { try { sc.close(); } catch (Throwable suppressed) { x.addSuppressed(suppressed); } throw x; } assert sc.isConnected(); return sc; } public static SocketChannel open() throws IOException { return SelectorProvider.provider().openSocketChannel(); } // -- Socket-specific operations -- /** * 绑定要连接的远程服务的ip及端口 */ Override public abstract SocketChannel bind(SocketAddress local) throws IOException; /** * 该channel与服务端是否已连接 */ public abstract boolean isConnected(); // -- ByteChannel operations -- /** * 将 channel 中的数据读到 ByteBuffer */ public abstract int read(ByteBuffer dst) throws IOException; public final long read(ByteBuffer[] dsts) throws IOException { return read(dsts, 0, dsts.length); } public abstract long read(ByteBuffer[] dsts, int offset, int length) throws IOException; /** * 将 ByteBuffer 中的数据写到 channel */ public abstract int write(ByteBuffer src) throws IOException; public final long write(ByteBuffer[] srcs) throws IOException { return write(srcs, 0, srcs.length); } public abstract long write(ByteBuffer[] srcs, int offset, int length) throws IOException; }值得注意的细节open(SocketAddress remote)是同步连接它会先open()再阻塞式connect(remote)适用于阻塞模式或简单场景非阻塞模式请用open()后自行connect()通过isConnectionPending()/finishConnect()判断连接结果实现了ScatteringByteChannel与GatheringByteChannel支持read(ByteBuffer[] dsts)和write(ByteBuffer[] srcs)的散射读/聚集写一次调用即可读写多个缓冲区isConnected()判断连接是否建立常用于非阻塞 connect 后的状态检查。源码背后的真相从抽象到操作系统JDK 把Selector、SelectionKey、Channel设计为抽象类 平台实现类的模式Strategy/Factory 思想SelectorProvider是中间的工厂。整个调用链可以概括为Selector.open() └─ SelectorProvider.provider() // 按操作系统选择默认 Provider └─ openSelector() // Linux → EPollSelectorImplWindows → WindowsSelectorImpl ServerSocketChannel.open() └─ SelectorProvider.provider().openServerSocketChannel()从源码结构可以推断Selector 的select()最终会落到操作系统内核的多路复用系统调用上——Linux 下是epoll_waitWindows 下是select。仓库文档 IO模型 对底层做了补充说明select/poll 顺序扫描 fd 是否就绪且支持的 fd 数量有限而 epoll 采用事件驱动方式代替顺序扫描支持一个进程打开大量 socket 描述符IO 效率不会随 FD 数目增加而线性下降。这正是 NIO 能在单线程下接入成千上万客户端的原因——阻塞只发生在 select() 这一个点上而不是每个连接的读写上。实战串联一个完整的 NIO 服务端与客户端结合三组件源码我们用一个仓库文档 把被说烂的BIO、NIO、AIO再从头到尾扯一遍 中的经典示例把 Selector、SelectionKey、Channel 全部串起来。服务端核心流程public class NioServer { private int port; private Selector selector; private ExecutorService service Executors.newFixedThreadPool(5); public void init() { ServerSocketChannel ssc null; try { // 1. 打开 ServerSocketChannel ssc ServerSocketChannel.open(); // 2. 设为非阻塞模式注册到 Selector 的前提 ssc.configureBlocking(false); ssc.bind(new InetSocketAddress(port)); // 3. 打开多路复用器 Selector selector Selector.open(); // 4. 将 ServerSocketChannel 注册到 Selector监听 ACCEPT 事件 ssc.register(selector, SelectionKey.OP_ACCEPT); System.out.println(NioServer started ......); } catch (IOException e) { e.printStackTrace(); } } public void accept(SelectionKey key) { try { // 通过 key 拿到对应的 Channel ServerSocketChannel ssc (ServerSocketChannel) key.channel(); SocketChannel sc ssc.accept(); sc.configureBlocking(false); // 新连接注册到 Selector改监听读事件 sc.register(selector, SelectionKey.OP_READ); System.out.println(accept a client : sc.socket().getInetAddress().getHostName()); } catch (IOException e) { e.printStackTrace(); } } public void start() { this.init(); while (true) { try { // 5. 阻塞轮询至少一个 Channel 就绪才返回 int events selector.select(); if (events 0) { // 6. 取出就绪的 SelectionKey 集合并遍历 IteratorSelectionKey selectionKeys selector.selectedKeys().iterator(); while (selectionKeys.hasNext()) { SelectionKey key selectionKeys.next(); selectionKeys.remove(); // 必须手动移除否则下次还会处理 if (key.isAcceptable()) { accept(key); } else { service.submit(new NioServerHandler(key)); } } } } catch (IOException e) { e.printStackTrace(); } } } // NioServerHandler通过 key.channel() 拿到 SocketChannel 进行读写... }客户端核心流程public void connect(String host, int port) { try { SocketChannel sc SocketChannel.open(); sc.configureBlocking(false); this.selector Selector.open(); // 注册连接事件非阻塞 connect sc.register(selector, SelectionKey.OP_CONNECT); sc.connect(new InetSocketAddress(host, port)); } catch (IOException e) { e.printStackTrace(); } } public void listen() { while (true) { try { int events selector.select(); if (events 0) { IteratorSelectionKey selectionKeys selector.selectedKeys().iterator(); while (selectionKeys.hasNext()) { SelectionKey selectionKey selectionKeys.next(); selectionKeys.remove(); // 连接事件就绪完成连接并注册读事件 if (selectionKey.isConnectable()) { SocketChannel socketChannel (SocketChannel) selectionKey.channel(); if (socketChannel.isConnectionPending()) { socketChannel.finishConnect(); } socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); socketChannel.write(ByteBuffer.wrap(Hello.getBytes())); } else if (selectionKey.isReadable()) { SocketChannel sc (SocketChannel) selectionKey.channel(); ByteBuffer buffer ByteBuffer.allocate(1024); sc.read(buffer); buffer.flip(); // 处理读到的数据... } } } } catch (IOException e) { e.printStackTrace(); } } }这个例子完整印证了源码中的三个要点configureBlocking(false)是注册到 Selector 的前提对应SelectableChannel源码事件常量决定注册意图服务端先注册OP_ACCEPTaccept 之后新连接改注册OP_READ客户端先注册OP_CONNECTfinishConnect()成功后改注册OP_READ对应SelectionKey四个事件常量selectedKeys()遍历后必须手动remove()selectedKeys()返回的是当前集合的引用若不主动移除已经处理过的 key 会残留在集合中导致重复处理。仓库文档 四种IO编程及对比 还给出了服务端创建的 11 步标准流程打开 ServerSocketChannel → 绑定端口并设为非阻塞 → 创建 Reactor 线程与 Selector → 注册 ACCEPT 事件 → 轮询 selectedKeys → accept 完成三次握手 → 新连接注册 OP_READ → 异步读 → 半包处理 → 业务编排 → 异步写可作进一步参考。走进 Netty三组件在 Netty 中的运用Netty 本质上是 JDK NIO 的上层封装Selector 与 SelectionKey 依然是它的地基。仓库文档 EventLoop 组件 的源码摘录清晰地展示了这一点// EventLoop 聚合一个多路复用器对象 Selector private Selector selector; // 通过 provider.open() 从操作系统底层获取 Selector实例 private final SelectorProvider provider; // 处理就绪事件 if (selectedKeys ! null) { processSelectedKeysPlain(selector.selectedKeys()); } private void processSelectedKeysPlain(SetSelectionKey selectedKeys) { if (selectedKeys.isEmpty()) { return; } IteratorSelectionKey i selectedKeys.iterator(); for (;;) { final SelectionKey k i.next(); // ... 处理单个 SelectionKey processSelectedKey(k, ch); } } private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) { // 就绪事件按位判断 if ((readyOps SelectionKey.OP_CONNECT) ! 0) { // remove OP_CONNECT as otherwise Selector.select(..) will always return without blocking ops ~SelectionKey.OP_CONNECT; } if ((readyOps SelectionKey.OP_WRITE) ! 0) { // 处理写事件 } if ((readyOps (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) ! 0 || readyOps 0) { // 处理读 / accept 事件 } }从源码结构可以看出Netty 的NioEventLoop正是一个线程 一个 Selector 无限循环 select()的 Reactor 线程实现selector.selectedKeys()取出就绪 key 集合后逐个processSelectedKey()再通过SelectionKey上携带的OP_CONNECT / OP_WRITE / OP_READ / OP_ACCEPT位判断事件类型并分发到对应的 Channel 处理逻辑。理解了本文的三组件源码就相当于拿到了阅读 NettyNioEventLoop核心调度逻辑的钥匙。常见误区与注意事项selectedKeys()不会自动清理处理完必须调用Iterator.remove()或selectedKeys().clear()否则重复处理、甚至空轮询写事件不要随便注册写缓冲区大部分时间就绪常驻 OP_WRITE 会让 select() 一直有事件返回白白消耗 CPU应该需要写时注册、写完立即取消非阻塞模式下 read/write 返回 0 是正常的说明本次没有数据可读或缓冲区已满不能当作异常TCP 半包问题需要配合缓冲区 mark/reset 处理可参考仓库 TCP粘拆包问题及Netty中的解决方案accept()非阻塞返回 null服务端在非阻塞模式下必须判空避免空指针关闭顺序Selector.close()只让 SelectionKey 失效不会关闭 ChannelChannel 需单独关闭同时注意已注册的 channel 不能再次configureBlocking(true)底层实现依赖操作系统同一套 API 在 Linuxepoll、Windowsselect、Mackqueue下的具体实现类不同性能特性也有差异这是 NIO 代码跨平台表现不一致的根源之一。总结本文沿着 Selector、SelectionKey及Channel组件 的源码脉络完整剖析了 Java NIO 三大核心组件Selector负责阻塞轮询与就绪事件收集SelectionKey用位掩码模型承载兴趣事件/就绪事件并桥接 Channel 与 SelectorChannel家族SelectableChannel → ServerSocketChannel / SocketChannel提供注册、accept、read/write 等底层能力。三者共同实现了 Reactor 模型让单线程管理海量连接成为可能也是理解 NettyNioEventLoop调度机制的基础。配合仓库中的 NIO 服务端序列图 与 NIO 客户端序列图 以及完整 NIO 示例代码读者可以自行搭建实验环境验证上述每一个结论。【免费下载链接】source-code-hunter 从源码层面剖析挖掘互联网行业主流技术的底层实现原理为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全家桶Mybatis、Netty、Dubbo 框架及 Redis、Tomcat 中间件等项目地址: https://gitcode.com/GitHub_Trending/so/source-code-hunter创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询