Netty 4.2.x 源码深度解析 (十二):NIO传输——NioSocketChannel与NioServerSocketChannel的IO读写实现

在 Netty 的传输层架构中,NIO 传输是最基础也是跨平台兼容性最强的实现。它基于 JDK 的 Selector 多路复用模型,通过 java.nio.channels.SelectableChannel 完成事件的注册与就绪通知。在 Netty 4.2 版本中,NIO 传输经历了一次重要的架构重构:旧的 NioEventLoop 被标记为 @Deprecated,取而代之的是 NioIoHandler,它与 IoHandler/IoHandle/IoRegistration 三层抽象无缝集成,将事件循环逻辑与 SingleThreadIoEventLoop 解耦。AbstractNioUnsafe 实现了 NioIoHandle 接口,成为连接 NioIoHandler 事件分发与 Channel 读写操作的桥梁。与此同时,NioIoOpsSelectionKey 的兴趣操作集封装为位掩码常量,通过 addAndSubmit()/removeAndSubmit() 方法以幂等方式提交到 IoRegistration,取代了旧版直接操作 SelectionKey.interestOps() 的方式。在写入侧,NioSocketChannel 覆写了父类的 doWrite() 方法,利用 JDK SocketChannel.write(ByteBuffer[]) 的 gathering write 能力,将多个 ByteBuf 聚合为 ByteBuffer 数组一次性写入,并通过 maxBytesPerGatheringWrite 动态调整策略优化吞吐量。

围绕 NIO 传输的 IO 读写实现,以下问题值得深入探究:

  • NioIoHandlerrun() 方法如何通过 SelectStrategy 策略判定走 selectNow() 非阻塞轮询还是 select() 阻塞等待,wakenUpAtomicBoolean 原子标志如何避免 Selector.wakeup() 的冗余调用,SELECTOR_AUTO_REBUILD_THRESHOLD 如何检测并修复 JDK 的空轮询 Bug?
  • SelectedSelectionKeySet 如何通过反射替换 Selector 内部的 selectedKeyspublicSelectedKeys 字段为数组实现,将就绪键的添加从 Set.add() 的哈希操作优化为 keys[size++] = o 的数组追加,processSelectedKeysOptimized() 又如何利用这一优化实现无 Iterator 的索引遍历?
  • AbstractNioUnsafehandle(IoRegistration, IoEvent) 方法作为唯一的 IO 事件入口,如何按 CONNECTWRITEREAD_AND_ACCEPT 的固定顺序分发事件,flush0() 如何通过 isFlushPending() 检测 OP_WRITE 避免重复刷新,finishConnect() 又如何在 handle() 中被触发而非独立调用?
  • NioByteUnsaferead() 方法如何通过 allocHandle.allocate() 分配缓冲区、doReadBytes()SocketChannel 读取数据、continueReading() 控制循环、fireChannelRead() 传播事件的四步循环实现数据读取,closeOnRead() 如何处理半关闭场景?
  • NioSocketChannel 覆写的 doWrite() 如何通过 ChannelOutboundBuffer.nioBuffers() 将多个 ByteBuf 提取为 ByteBuffer 数组,利用 SocketChannel.write(ByteBuffer[], offset, length) 的 gathering write 一次写入多个缓冲区,adjustMaxBytesPerGatheringWrite() 如何根据写入结果动态调整下次 gathering write 的最大字节数?
  • NioServerSocketChanneldoReadMessages() 如何通过 SocketUtils.accept() 接受新连接并包装为 NioSocketChannelNioMessageUnsaferead() 如何使用 List<Object> readBuf 批量接受连接后逐个触发 fireChannelRead()

本文将沿着"架构定位 → 事件循环 → 就绪键优化 → 事件抽象 → Channel 抽象 → 字节流读写 → SocketChannel 实现 → 消息型读写 → ServerSocketChannel 实现 → Socket 选项"的递进逻辑,从 NioIoHandler 的事件循环出发,逐一分析 SelectedSelectionKeySet 的数组优化、NioIoOps 的事件封装、AbstractNioUnsafe 的事件分发、NioByteUnsafe 的读循环、NioSocketChannel 的 gathering write 与 NioServerSocketChannel 的连接接受,最后串联从 SelectorChannel 的完整 IO 处理闭环。

一、NIO 传输在 Netty 4.2 传输架构中的定位

在深入源码之前,我们需要先理清 NIO 传输在 Netty 4.2 整体架构中的定位。所谓 NIO 传输,是指 Netty 基于 JDK java.nio.channels.Selector 多路复用模型实现的传输层方案。它在所有支持 Java 的平台上均可运行,是 Netty 传输层的默认实现。

与 Epoll、KQueue、io_uring 等原生传输不同,NIO 传输通过 JDK Selector 间接调用底层系统调用。在 Linux 平台上,JDK Selector 底层使用 epoll;在 macOS 上则使用 kqueue。原生传输通过 JNI 直接调用系统调用,绕过 JDK 抽象层,减少了额外的开销。但 NIO 传输的跨平台优势使其成为需要部署在多种操作系统上的应用的首选。

Netty 4.2 引入了三层 IO 抽象架构:IoHandler(事件循环策略)→ IoHandle(IO 句柄,持有 SelectableChannel)→ IoRegistration(注册信息,封装 SelectionKey)。NIO 传输通过 NioIoHandler/NioIoHandle/DefaultNioRegistration 三组实现适配此架构。在 4.2 版本中,旧的 NioEventLoopNioEventLoopGroup 均被标记为 @Deprecated,推荐使用 MultiThreadIoEventLoopGroup + NioIoHandler.newFactory() 的组合。

从类继承体系来看,NIO 传输的核心继承链为:AbstractNioChannel(抽象基类)分出两条分支,一条是 AbstractNioByteChannel(字节流读写),另一条是 AbstractNioMessageChannel(消息型读写)。NioSocketChannel 继承前者,负责客户端连接和数据读写;NioServerSocketChannel 继承后者,负责服务端连接接受。

在这个体系中,AbstractNioUnsafe 扮演着至关重要的角色。它既继承 AbstractUnsafe 承担 ChannelUnsafe 职责,又实现 NioIoHandle 接口承担 IO 事件处理职责。这种双重身份使其成为连接 IO 抽象层与 Channel 层的关键枢纽。

理解了整体架构后,我们进入核心实现的分析。

二、NioIoHandler 与 Selector:事件循环的核心实现

NioIoHandler 是 NIO 传输事件循环的核心实现类,它的类定义为 final class NioIoHandler implements IoHandler。这个类持有多个关键字段来管理 Selector 的生命周期和事件循环逻辑。

2.1 类结构与工厂创建

NioIoHandler 持有的核心字段包括:Selector selector(包装后的选择器)、Selector unwrappedSelector(原始选择器)、SelectedSelectionKeySet selectedKeys(优化后的就绪键集)、AtomicBoolean wakenUp(唤醒标志)、SelectStrategy selectStrategy(选择策略)、ThreadAwareExecutor executor(线程执行器)、int cancelledKeys(已取消键计数)和 boolean needsToSelectAgain(重新选择标志)。

NioIoHandler 通过 newFactory() 工厂方法创建,该方法返回一个 IoHandlerFactory,在 newHandler(ThreadAwareExecutor) 中创建 NioIoHandler 实例。值得注意的是,isChangingThreadSupported() 返回 true,表示支持线程切换。

2.2 openSelector():Selector 创建与反射优化

openSelector()NioIoHandler 构造时调用的核心方法,它不仅创建 JDK Selector,还尝试通过反射优化就绪键集。其核心逻辑分为两步:首先通过 provider.openSelector() 创建 JDK Selector,然后尝试通过反射将 Selector 内部的 selectedKeyspublicSelectedKeys 字段替换为 SelectedSelectionKeySet 数组实现。

反射优化的路径如下:通过 AccessController.doPrivileged 加载 sun.nio.ch.SelectorImpl 类,获取其 selectedKeyspublicSelectedKeys 字段。在 Java 9+ 中使用 PlatformDependent.objectFieldOffset() + putObject() 通过 Unsafe 直接替换字段值;在 Java 8 中则使用 ReflectionUtil.trySetAccessible() 通过反射替换。

如果 Selector 实现类不是 SelectorImpl 的子类,或反射替换失败,则返回未包装的原始 SelectorselectedKeys 字段为 null。后续事件处理将走 processSelectedKeysPlain() 普通路径而非优化路径。

2.3 run():事件循环主逻辑

run(IoHandlerContext)NioIoHandler 的核心方法,它实现了完整的事件循环逻辑。

java 复制代码
// io.netty.channel.nio.NioIoHandler#run

@Override
public int run(IoHandlerContext context) {
    int handled = 0;
    try {
        try {
            // 根据选择策略判定走哪条分支
            switch (selectStrategy.calculateStrategy(selectNowSupplier, !context.canBlock())) {
            case SelectStrategy.CONTINUE:
                // 无 IO 事件且可继续,直接返回
                if (context.shouldReportActiveIoTime()) {
                    context.reportActiveIoTime(0);
                }
                return 0;

            case SelectStrategy.BUSY_WAIT:
                // NIO 不支持 busy-wait,落入 SELECT 分支

            case SelectStrategy.SELECT:
                // 先原子地获取并重置唤醒标志,再阻塞等待
                select(context, wakenUp.getAndSet(false));

                // 修复竞态:若 select 期间 wakenUp 被设为 true,需要再次唤醒
                if (wakenUp.get()) {
                    selector.wakeup();
                }
                // fall through
            default:
            }
        } catch (IOException e) {
            // Selector 出现 IO 异常,重建后重试
            rebuildSelector0();
            handleLoopException(e);
            return 0;
        }

        cancelledKeys = 0;
        needsToSelectAgain = false;

        // 处理就绪键并统计处理数量
        if (context.shouldReportActiveIoTime()) {
            long activeIoStartTimeNanos = System.nanoTime();
            handled = processSelectedKeys();
            long activeIoEndTimeNanos = System.nanoTime();
            context.reportActiveIoTime(activeIoEndTimeNanos - activeIoStartTimeNanos);
        } else {
            handled = processSelectedKeys();
        }
    } catch (Error e) {
        throw e;
    } catch (Throwable t) {
        // 捕获所有异常防止事件循环退出
        handleLoopException(t);
    }
    return handled;
}

上面的代码展示了 run() 的双层 try-catch 结构。内层 try 包裹 select() 调用,捕获 IOException 时调用 rebuildSelector0() 重建 Selector 并返回 0;外层 try 捕获 Throwable,调用 handleLoopException() 记录日志并 Thread.sleep(1000) 防止 CPU 空转。

SelectStrategy 有三种策略分支:CONTINUE 直接返回 0;BUSY_WAIT 在 NIO 中不支持,落入 SELECT 分支;SELECTselect() 阻塞等待。这里有一个关键的竞态修复设计:run() 中先通过 wakenUp.getAndSet(false) 原子地获取并重置唤醒标志,再调用 select()select() 返回后检查 if (wakenUp.get()) { selector.wakeup(); },这是为了修复 wakenUp 设置为 true 过早导致 select() 立即返回的竞态条件。

2.4 select():阻塞选择与空轮询检测

select(IoHandlerContext, boolean oldWakenUp) 是阻塞选择的核心方法,它需要计算超时时间、检测空轮询,并在必要时重建 Selector

java 复制代码
// io.netty.channel.nio.NioIoHandler#select

private void select(IoHandlerContext runner, boolean oldWakenUp) throws IOException {
    Selector selector = this.selector;
    try {
        int selectCnt = 0;
        long currentTimeNanos = System.nanoTime();
        // 计算最近定时任务的到期时间,用于确定 select 超时
        final long delayNanos = runner.delayNanos(currentTimeNanos);
        long selectDeadLineNanos = Long.MAX_VALUE;
        if (delayNanos != Long.MAX_VALUE) {
            selectDeadLineNanos = currentTimeNanos + runner.delayNanos(currentTimeNanos);
        }
        for (;;) {
            final long timeoutMillis;
            if (delayNanos != Long.MAX_VALUE) {
                // 计算距离截止时间的剩余毫秒数
                long millisBeforeDeadline = millisBeforeDeadline(selectDeadLineNanos, currentTimeNanos);
                if (millisBeforeDeadline <= 0) {
                    if (selectCnt == 0) {
                        selector.selectNow();
                        selectCnt = 1;
                    }
                    break;
                }
                timeoutMillis = millisBeforeDeadline;
            } else {
                // 无定时任务,无限期阻塞
                timeoutMillis = 0;
            }
            // 若有任务需要执行,先非阻塞探测一次
            if (!runner.canBlock() && wakenUp.compareAndSet(false, true)) {
                selector.selectNow();
                selectCnt = 1;
                break;
            }

            int selectedKeys = selector.select(timeoutMillis);
            selectCnt ++;

            // 满足以下任一条件则退出循环
            if (selectedKeys != 0 || oldWakenUp || wakenUp.get() || !runner.canBlock()) {
                break;
            }
            if (Thread.interrupted()) {
                // 线程被中断,重置计数防止空转
                selectCnt = 1;
                break;
            }

            long time = System.nanoTime();
            if (time - TimeUnit.MILLISECONDS.toNanos(timeoutMillis) >= currentTimeNanos) {
                // 超时退出,正常情况
                selectCnt = 1;
            } else if (SELECTOR_AUTO_REBUILD_THRESHOLD > 0 &&
                       selectCnt >= SELECTOR_AUTO_REBUILD_THRESHOLD) {
                // 检测到空轮询 Bug,重建 Selector
                selector = selectRebuildSelector(selectCnt);
                selectCnt = 1;
                break;
            }

            currentTimeNanos = time;
        }
    } catch (CancelledKeyException e) {
        // harmless exception
    }
}

上面的代码中,SELECTOR_AUTO_REBUILD_THRESHOLD 默认值为 512,通过 SystemPropertyUtil.getInt("io.netty.selectorAutoRebuildThreshold", 512) 设置。当 selectCnt 达到此阈值时,说明 Selector.select() 在没有事件就绪的情况下反复返回,这是 JDK 的空轮询 Bug,此时调用 selectRebuildSelector() 重建 Selector

2.5 wakeup():原子唤醒机制

java 复制代码
// io.netty.channel.nio.NioIoHandler#wakeup

@Override
public void wakeup() {
    // 非 EventLoop 线程才需要唤醒,同线程不需要
    if (!executor.isExecutorThread(Thread.currentThread()) && wakenUp.compareAndSet(false, true)) {
        selector.wakeup();
    }
}

wakeup() 的设计体现了精细的性能意识。首先检查 !executor.isExecutorThread(Thread.currentThread()),如果是 EventLoop 自身线程调用则无需唤醒。然后通过 wakenUp.compareAndSet(false, true) 原子操作检测是否需要唤醒,仅当 CAS 成功时才调用 selector.wakeup()。这样可以避免对已经处于唤醒状态的 Selector 执行冗余的 wakeup() 调用。

2.6 rebuildSelector0():Selector 重建

java 复制代码
// io.netty.channel.nio.NioIoHandler#rebuildSelector0

void rebuildSelector0() {
    final Selector oldSelector = selector;
    final SelectorTuple newSelectorTuple;

    if (oldSelector == null) {
        return;
    }

    try {
        // 创建新 Selector,同样会尝试反射优化
        newSelectorTuple = openSelector();
    } catch (Exception e) {
        logger.warn("Failed to create a new Selector.", e);
        return;
    }

    // 将所有 Channel 重新注册到新 Selector
    int nChannels = 0;
    for (SelectionKey key : oldSelector.keys()) {
        DefaultNioRegistration handle = (DefaultNioRegistration) key.attachment();
        try {
            if (!key.isValid() || key.channel().keyFor(newSelectorTuple.unwrappedSelector) != null) {
                continue;
            }
            // 将 Channel 重新注册到新 Selector
            handle.register(newSelectorTuple.unwrappedSelector);
            nChannels++;
        } catch (Exception e) {
            logger.warn("Failed to re-register a NioHandle to the new Selector.", e);
            handle.cancel();
        }
    }

    selector = newSelectorTuple.selector;
    unwrappedSelector = newSelectorTuple.unwrappedSelector;

    // 关闭旧 Selector
    try {
        oldSelector.close();
    } catch (Throwable t) {
        if (logger.isWarnEnabled()) {
            logger.warn("Failed to close the old Selector.", t);
        }
    }
}

上面的代码展示了 rebuildSelector0() 的重建逻辑:创建新 Selector → 遍历旧 Selector 的所有 SelectionKey → 通过 DefaultNioRegistration.register(newSelector) 重新注册 → 替换 selectorunwrappedSelector → 关闭旧 Selector。整个过程保证所有 Channel 的事件监听不中断。

2.7 run() 完整事件循环时序图

上面的时序图清晰地展示了 NioIoHandler.run() 的完整执行链路。SelectStrategy 三种策略分支、wakenUp 原子标志的竞态修复、select() 的空轮询检测与 rebuildSelector0() 重建、processSelectedKeys() 的优化/普通两条路径,构成了 NIO 事件循环的四道核心关卡。后续内容将按图的顺序一步步深入。

三、SelectedSelectionKeySet:就绪键集的数组优化

NioIoHandler 的事件循环中,processSelectedKeys() 的性能至关重要。Netty 通过 SelectedSelectionKeySetSelector 内部的 HashSet 就绪键集替换为数组实现,实现了显著的性能提升。

3.1 数据结构设计

SelectedSelectionKeySet 继承 AbstractSet<SelectionKey>,但其内部并非基于哈希表,而是使用数组实现。核心数据结构非常简单:SelectionKey[] keys(初始容量 1024)和 int size(当前元素数量)。

java 复制代码
// io.netty.channel.nio.SelectedSelectionKeySet#add

@Override
public boolean add(SelectionKey o) {
    if (o == null) {
        return false;
    }

    // 容量不足时动态扩容
    if (size == keys.length) {
        increaseCapacity();
    }

    // 数组追加,O(1) 确定性时间复杂度
    keys[size++] = o;
    return true;
}

上面的代码中,add() 方法直接将元素追加到数组末尾,时间复杂度为 O(1) 确定性操作。相比之下,HashSet.add() 需要计算哈希值、探测哈希桶,虽然均摊也是 O(1),但存在哈希冲突的不确定性。

remove(Object o) 固定返回 false,不支持删除操作。就绪键的移除通过外部遍历时置为 null 来实现。

3.2 reset():重置与 GC 友好性

java 复制代码
// io.netty.channel.nio.SelectedSelectionKeySet#reset

void reset(int start) {
    // 将已处理的键置为 null,允许 GC 回收
    Arrays.fill(keys, start, size, null);
    size = 0;
}

reset() 方法在每次 select() 调用前由 SelectedSelectionKeySetSelector 执行。Arrays.fill(keys, start, size, null) 将已处理的键置为 null,使 SelectionKey 对象可以被及时 GC 回收,避免内存泄漏。

3.3 数组结构与优化对比

为了直观理解 SelectedSelectionKeySet 相对于 JDK 原始 HashSet 的性能优势,下图从数据结构、添加操作和遍历方式三个维度进行对比:

上图清晰地展示了从 HashSet 到数组结构的三个维度的优化。数据结构层面HashSet 底层是 HashMap 桶数组 + 链表/红黑树,元素位置由 hashCode() 决定,内存布局不连续;而 SelectedSelectionKeySet 使用紧凑的 SelectionKey[] 数组,元素按追加顺序连续排列,对 CPU 缓存更加友好。添加操作层面HashSet.add() 需要计算哈希值并处理可能的哈希冲突,虽然均摊时间复杂度也是 O(1),但存在不确定性;而 keys[size++] = o 是纯粹的数组赋值 + 索引自增,确定性 O(1),无哈希计算开销。遍历方式层面HashSet 必须通过 Iterator 对象逐个访问,每次调用 next() 都有方法调用和状态检查开销;而数组遍历使用 for 循环索引访问,无需创建 Iterator 对象,且可以在遍历过程中通过 keys[i] = null 及时释放引用,帮助 GC 回收。

3.4 processSelectedKeysOptimized():优化遍历

java 复制代码
// io.netty.channel.nio.NioIoHandler#processSelectedKeysOptimized

private int processSelectedKeysOptimized() {
    int handled = 0;
    for (int i = 0; i < selectedKeys.size; ++i) {
        final SelectionKey k = selectedKeys.keys[i];
        // 置为 null 以允许 GC 回收
        selectedKeys.keys[i] = null;

        processSelectedKey(k);
        ++handled;

        if (needsToSelectAgain) {
            // 重置剩余部分并重新选择
            selectedKeys.reset(i + 1);
            selectAgain();
            i = -1;
        }
    }
    return handled;
}

上面的代码展示了优化遍历的核心逻辑。for (int i = 0; i < selectedKeys.size; ++i) 使用索引遍历,消除了 Iterator 对象的创建开销。selectedKeys.keys[i] = null 在处理完每个键后立即置为 null,允许 GC 及时回收。当 needsToSelectAgaintrue 时(即取消键数量达到 CLEANUP_INTERVAL=256),调用 selectedKeys.reset(i + 1) 重置剩余部分并重新选择。

与优化路径对应的是 processSelectedKeysPlain() 普通路径,通过 Iterator<SelectionKey> 遍历 selector.selectedKeys(),每次 i.remove() 移除已处理键。当 needsToSelectAgain 时调用 selectAgain() 并重建迭代器。这条路径在反射替换失败时作为降级使用。

无论是优化路径还是普通路径,最终都调用 processSelectedKey(SelectionKey k) 处理单个就绪键。这个方法是连接 processSelectedKeysDefaultNioRegistration.handle() 的桥梁。

java 复制代码
// io.netty.channel.nio.NioIoHandler#processSelectedKey

private void processSelectedKey(SelectionKey k) {
    // 从 SelectionKey 的 attachment 中获取 DefaultNioRegistration
    final DefaultNioRegistration registration = (DefaultNioRegistration) k.attachment();
    if (!registration.isValid()) {
        try {
            registration.handle.close();
        } catch (Exception e) {
            logger.debug("Exception during closing " + registration.handle, e);
        }
        return;
    }
    // 将就绪操作集传递给 registration.handle() 分发
    registration.handle(k.readyOps());
}

上面的代码展示了 processSelectedKey() 的桥接逻辑。首先从 SelectionKeyattachment() 获取 DefaultNioRegistration 实例------这个 attachment 在 DefaultNioRegistration 构造器中通过 SelectableChannel.register(selector, initialOps.value, this) 的第三个参数 this 设置。如果 registration.isValid() 返回 false(键已取消或通道已关闭),直接调用 handle.close() 关闭底层资源并返回。否则调用 registration.handle(k.readyOps()),将 JDK SelectionKey 的就绪操作集传递给 DefaultNioRegistration.handle(),后者再通过 NioIoOps.eventOf(ready) 转换为 NioIoEvent 并调用 handle.handle(this, nioIoEvent) 最终分发到 AbstractNioUnsafe.handle()

3.5 needsToSelectAgain 的触发条件

needsToSelectAgain 的触发条件位于 DefaultNioRegistration.cancel() 中。每次取消一个 SelectionKey 时,cancelledKeys 递增。当 cancelledKeys >= CLEANUP_INTERVAL(256)时,重置计数器并设置 needsToSelectAgain = true。这会在遍历就绪键的过程中触发一次 selector.selectNow(),清理已取消但仍在就绪键集中的键,防止这些无效键被重复处理。

至此,我们已经分析了 NioIoHandler 的事件循环和 SelectedSelectionKeySet 的数组优化。接下来将深入 NIO 传输的事件抽象层和 Channel 层。

四、NioIoOps、NioIoEvent 与 DefaultNioRegistration:NIO 事件抽象与注册

在 Netty 4.2 的三层 IO 抽象中,IoOpsIoEventIoRegistration 是三个核心接口。NIO 传输通过 NioIoOpsNioIoEventDefaultNioRegistration 三组实现适配这些抽象。

4.1 NioIoOps:兴趣操作集的位掩码封装

NioIoOps 实现了 IoOps 接口,将 JDK SelectionKey 的兴趣操作常量封装为统一的类型安全对象。所谓兴趣操作集,是指 SelectionKey 中用于告诉 Selector "我对哪些 IO 事件感兴趣"的位掩码。

NioIoOps 预定义了以下常量与 SelectionKey 的映射关系:NONE(0) 表示无事件,ACCEPT(OP_ACCEPT=16) 表示接受连接,CONNECT(OP_CONNECT=8) 表示完成连接,WRITE(OP_WRITE=4) 表示可写,READ(OP_READ=1) 表示可读,READ_AND_ACCEPT(OP_READ|OP_ACCEPT=17) 表示读或接受,READ_AND_WRITE(OP_READ|OP_WRITE=5) 表示读或写。

java 复制代码
// io.netty.channel.nio.NioIoOps#with

// 合并当前 ops 与给定 ops,返回新的 NioIoOps
public NioIoOps with(NioIoOps ops) {
    if (contains(ops)) {
        return this;
    }
    // 位或操作合并兴趣集
    return valueOf(value | ops.value());
}
java 复制代码
// io.netty.channel.nio.NioIoOps#without

// 从当前 ops 中移除给定 ops,返回新的 NioIoOps
public NioIoOps without(NioIoOps ops) {
    if (!contains(ops)) {
        return this;
    }
    // 位与非操作移除兴趣集
    return valueOf(value & ~ops.value());
}

上面的代码展示了 with()without() 的位运算实现。with() 通过位或操作合并兴趣集,without() 通过位与非操作移除兴趣集。contains(NioIoOps ops) 内部委托 isIncludedIn(ops.value),通过 (ops.value & value) != 0 判断当前操作的位掩码是否包含在指定 ops 的位掩码中。

NioIoOps 还维护了一个 EVENTS[] 缓存数组,在静态初始化块中将常用 ops 值预存入 NioIoEvent[] 数组。eventOf(int value) 方法通过数组索引实现 O(1) 查找,未命中则创建 DefaultNioIoEvent 新实例。

4.2 NioIoEvent 与 NioIoHandle

NioIoEvent 接口继承 IoEvent,仅增加 NioIoOps ops() 方法返回触发事件的 ops。DefaultNioIoEvent 作为内部实现类封装 NioIoOps。当 Selector 返回就绪键时,DefaultNioRegistration.handle(int ready) 会调用 NioIoOps.eventOf(ready)SelectionKeyreadyOps 转换为 NioIoEvent,再分发给 NioIoHandle

NioIoHandle 接口继承 IoHandle,增加 SelectableChannel selectableChannel() 方法返回底层 SelectableChannel。这个方法供 NioIoHandler 在注册时获取 JDK Channel,从而调用 SelectableChannel.register(selector, ops, attachment)

4.3 DefaultNioRegistration:SelectionKey 的封装

DefaultNioRegistrationNioIoHandler 的内部类,实现 IoRegistration 接口,封装了 SelectionKeyNioIoHandle

java 复制代码
// io.netty.channel.nio.NioIoHandler.DefaultNioRegistration

final class DefaultNioRegistration implements IoRegistration {
    private final AtomicBoolean canceled = new AtomicBoolean();
    private final NioIoHandle handle;
    private volatile SelectionKey key;

    // 将 SelectableChannel 注册到 Selector,this 作为 attachment
    DefaultNioRegistration(ThreadAwareExecutor executor, NioIoHandle handle,
                           NioIoOps initialOps, Selector selector) throws IOException {
        this.handle = handle;
        key = handle.selectableChannel().register(selector, initialOps.value, this);
    }

上面的代码展示了构造器的核心逻辑:handle.selectableChannel().register(selector, initialOps.value, this)SelectableChannel 注册到 Selector,初始 ops 为 NioIoOps.NONE(0),thisDefaultNioRegistration 自身)作为 SelectionKey 的 attachment。这样当 Selector 返回就绪键时,可以通过 key.attachment() 获取到 DefaultNioRegistration 实例。

4.4 submit():兴趣操作集修改

java 复制代码
// io.netty.channel.nio.NioIoHandler.DefaultNioRegistration#submit

@Override
public long submit(IoOps ops) {
    if (!isValid()) {
        return -1;
    }
    // 将 IoOps 强转为 NioIoOps,直接修改 SelectionKey 的兴趣操作集
    int v = cast(ops).value;
    key.interestOps(v);
    return v;
}

上面的代码中,submit() 方法将 IoOps 强转为 NioIoOps,提取其 value,直接调用 key.interestOps(v) 修改 SelectionKey 的兴趣操作集。这是 NIO 传输中兴趣操作管理的最终执行点,所有 addAndSubmit()/removeAndSubmit() 调用最终都会走到这里。

4.5 cancel():幂等注销

java 复制代码
// io.netty.channel.nio.NioIoHandler.DefaultNioRegistration#cancel

@Override
public boolean cancel() {
    // CAS 保证幂等,只能取消一次
    if (!canceled.compareAndSet(false, true)) {
        return false;
    }
    key.cancel();
    cancelledKeys++;
    // 取消键达到阈值时触发重新选择
    if (cancelledKeys >= CLEANUP_INTERVAL) {
        cancelledKeys = 0;
        needsToSelectAgain = true;
    }
    handle.unregistered();
    return true;
}

上面的代码展示了 cancel() 的幂等设计。AtomicBoolean.compareAndSet(false, true) 保证只能取消一次。key.cancel() 取消 SelectionKeycancelledKeys++ 累加计数器,达到 CLEANUP_INTERVAL(256) 时设置 needsToSelectAgain = true,触发在遍历过程中重新执行 selector.selectNow() 清理已取消的键。最后调用 handle.unregistered() 通知 IoHandle 已注销。

4.6 handle():就绪事件分发

java 复制代码
// io.netty.channel.nio.NioIoHandler.DefaultNioRegistration#handle

void handle(int ready) {
    if (!isValid()) {
        return;
    }
    // 将 readyOps 转换为 NioIoEvent 后分发给 NioIoHandle
    handle.handle(this, NioIoOps.eventOf(ready));
}

上面的代码是就绪事件分发的入口。handle(int ready) 接收 SelectionKeyreadyOps,通过 NioIoOps.eventOf(ready) 转换为 NioIoEvent,然后调用 handle.handle(this, event) 分发给 NioIoHandle(即 AbstractNioUnsafe)。

4.7 register() 与 submit() 时序图

上面的时序图清晰地展示了注册和兴趣操作修改两个流程。注册流程从 AbstractNioChannel.doRegister() 开始,通过 IoEventLoop.register() 委托给 NioIoHandler.register(),最终创建 DefaultNioRegistration 并将 SelectableChannel 注册到 Selector。兴趣操作修改流程从 AbstractNioChannel.doBeginRead() 开始,通过 addAndSubmit(readOps) 委托给 DefaultNioRegistration.submit(),最终调用 key.interestOps() 修改 SelectionKey 的兴趣操作集。

五、AbstractNioChannel 与 AbstractNioUnsafe:Channel 抽象基类与事件分发

AbstractNioChannel 是所有 NIO 传输 Channel 的抽象基类,它管理 SelectableChannel 的生命周期和兴趣操作集。其内部类 AbstractNioUnsafe 则是 IO 事件与 Channel 操作的交汇点。

5.1 核心字段与构造器

AbstractNioChannel 的核心字段包括:SelectableChannel ch(底层 JDK Channel)、int readInterestOp(读兴趣操作,如 OP_READOP_ACCEPT)、NioIoOps readOps(读操作的 NioIoOps 封装)、IoRegistration registration(注册信息)、boolean readPending(读待处理标志)、ChannelPromise connectPromise(连接 Promise)和 Future<?> connectTimeoutFuture(连接超时 Future)。

java 复制代码
// io.netty.channel.nio.AbstractNioChannel#AbstractNioChannel

protected AbstractNioChannel(Channel parent, SelectableChannel ch, NioIoOps readOps) {
    super(parent);
    this.ch = ch;
    this.readInterestOp = ObjectUtil.checkNotNull(readOps, "readOps").value;
    this.readOps = readOps;
    try {
        // 必须设置为非阻塞模式,这是 NIO 多路复用的前提
        ch.configureBlocking(false);
    } catch (IOException e) {
        try {
            ch.close();
        } catch (IOException e2) {
            logger.warn("Failed to close a partially initialized socket.", e2);
        }
        throw new ChannelException("Failed to enter non-blocking mode.", e);
    }
}

上面的代码中,构造器的关键操作是 ch.configureBlocking(false),将 SelectableChannel 设置为非阻塞模式。这是 NIO 多路复用的前提条件,因为 Selector 只能管理非阻塞的 Channel。如果设置失败,会尝试关闭 Channel 并抛出 ChannelException

5.2 addAndSubmit() 与 removeAndSubmit():幂等兴趣操作管理

Netty 4.2 中,兴趣操作管理通过 IoRegistration.submit() 而非直接操作 SelectionKey.interestOps(),这是一个重要的设计变化。旧的 selectionKey() 方法已被标记为 @Deprecated

java 复制代码
// io.netty.channel.nio.AbstractNioChannel#addAndSubmit

protected void addAndSubmit(NioIoOps addOps) {
    // 获取当前兴趣操作集
    int interestOps = selectionKey().interestOps();
    // 幂等检查:仅在未包含时才提交
    if (!addOps.isIncludedIn(interestOps)) {
        try {
            // 合并后通过 IoRegistration.submit() 提交
            registration().submit(NioIoOps.valueOf(interestOps).with(addOps));
        } catch (Exception e) {
            throw new ChannelException(e);
        }
    }
}
java 复制代码
// io.netty.channel.nio.AbstractNioChannel#removeAndSubmit

protected void removeAndSubmit(NioIoOps removeOps) {
    int interestOps = selectionKey().interestOps();
    // 幂等检查:仅在已包含时才提交
    if (removeOps.isIncludedIn(interestOps)) {
        try {
            // 移除后通过 IoRegistration.submit() 提交
            registration().submit(NioIoOps.valueOf(interestOps).without(removeOps));
        } catch (Exception e) {
            throw new ChannelException(e);
        }
    }
}

上面的代码展示了幂等提交的设计。addAndSubmit() 先检查 addOps 是否已包含在当前兴趣操作集中,未包含时通过 registration().submit() 提交合并后的新 ops。removeAndSubmit() 的逻辑类似,先检查 removeOps 是否包含在当前兴趣操作集中,包含时才提交移除后的新 ops。这种幂等设计避免了重复的 interestOps() 调用,减少了对 Selector 的无效操作。

5.3 doRegister() 与 doDeregister():注册与注销

java 复制代码
// io.netty.channel.nio.AbstractNioChannel#doRegister

@SuppressWarnings("unchecked")
@Override
protected void doRegister(ChannelPromise promise) {
    assert registration == null;
    // 将 AbstractNioUnsafe(作为 NioIoHandle)注册到 IoEventLoop
    ((IoEventLoop) eventLoop()).register((AbstractNioUnsafe) unsafe()).addListener(f -> {
        if (f.isSuccess()) {
            // 注册成功后保存 IoRegistration
            registration = (IoRegistration) f.getNow();
            promise.setSuccess();
        } else {
            promise.setFailure(f.cause());
        }
    });
}

上面的代码中,doRegister()AbstractNioUnsafe(它同时是 NioIoHandle)注册到 IoEventLoop。注册是异步的,通过 addListener 回调在成功时保存 IoRegistration 并标记 promise 成功。

doDeregister() 的逻辑更简单:调用 registration.cancel() 取消 SelectionKey,将 registration 置为 null

5.4 doBeginRead():读兴趣注册

java 复制代码
// io.netty.channel.nio.AbstractNioChannel#doBeginRead

@Override
protected void doBeginRead() throws Exception {
    IoRegistration registration = this.registration;
    // 检查 registration 有效性
    if (registration == null || !registration.isValid()) {
        return;
    }

    readPending = true;
    // 提交读兴趣操作到 SelectionKey
    addAndSubmit(readOps);
}

上面的代码中,doBeginRead() 先检查 registration 有效性,然后设置 readPending = true,最后调用 addAndSubmit(readOps) 将读兴趣操作提交到 SelectionKey。对于 NioSocketChannelreadOpsOP_READ;对于 NioServerSocketChannelreadOpsOP_ACCEPT

5.5 AbstractNioUnsafe:事件分发枢纽

AbstractNioUnsafe 的类定义为 extends AbstractUnsafe implements NioUnsafe, NioIoHandle。它同时实现 Unsafe 接口和 NioIoHandle 接口,是 IO 事件与 Channel 操作的交汇点。

AbstractNioUnsafe.handle(IoRegistration, IoEvent) 是唯一的 IO 事件入口,按 CONNECTWRITEREAD_AND_ACCEPT 的固定顺序分发事件。

java 复制代码
// io.netty.channel.nio.AbstractNioChannel.AbstractNioUnsafe#handle

@Override
public void handle(IoRegistration registration, IoEvent event) {
    try {
        NioIoEvent nioEvent = (NioIoEvent) event;
        NioIoOps nioReadyOps = nioEvent.ops();

        // 1. CONNECT 优先处理:未完成连接时读写会抛 NotYetConnectedException
        if (nioReadyOps.contains(NioIoOps.CONNECT)) {
            // 移除 OP_CONNECT,否则 Selector.select() 会空轮询
            removeAndSubmit(NioIoOps.CONNECT);
            unsafe().finishConnect();
        }

        // 2. WRITE 次优先:尽快释放写缓冲区内存
        if (nioReadyOps.contains(NioIoOps.WRITE)) {
            // forceFlush 会自动清除 OP_WRITE
            forceFlush();
        }

        // 3. READ_AND_ACCEPT 最后处理:NONE 也调用 read() 作为 JDK Bug 兜底
        if (nioReadyOps.contains(NioIoOps.READ_AND_ACCEPT) || nioReadyOps.equals(NioIoOps.NONE)) {
            read();
        }
    } catch (CancelledKeyException ignored) {
        close(voidPromise());
    }
}

上面的代码展示了三分支事件分发的完整逻辑。CONNECT 分支优先处理,因为未完成连接时 SocketChannelread()/write() 会抛出 NotYetConnectedException。处理前先 removeAndSubmit(NioIoOps.CONNECT) 移除连接兴趣,防止 Selector.select() 空轮询。WRITE 分支调用 forceFlush() 强制刷新写缓冲区。READ_AND_ACCEPT 分支调用 read() 执行读取操作。值得注意的是 NONE 分支:当 nioReadyOps.equals(NioIoOps.NONE) 时也调用 read(),这是作为 JDK Bug 的兜底处理,防止就绪操作为 0 时的空轮询。

5.6 flush0() 与 forceFlush():协作刷新

java 复制代码
// io.netty.channel.nio.AbstractNioChannel.AbstractNioUnsafe#flush0

@Override
protected final void flush0() {
    // 仅在没有待处理刷新时才立即执行
    if (!isFlushPending()) {
        super.flush0();
    }
}

@Override
public final void forceFlush() {
    // 绕过 isFlushPending 检查,直接执行刷新
    super.flush0();
}

private boolean isFlushPending() {
    IoRegistration registration = registration();
    // 检查 OP_WRITE 是否已在兴趣操作集中
    return registration.isValid() && NioIoOps.WRITE.isIncludedIn(
            ((SelectionKey) registration.attachment()).interestOps());
}

上面的代码展示了 flush0()forceFlush() 的协作设计。flush0() 先通过 isFlushPending() 检查 OP_WRITE 是否已设置。若已设置,说明有未完成的写操作(正在等待 Selector 通知可写),跳过当前刷新避免重复操作。forceFlush() 绕过检查直接调用 super.flush0() 执行实际写操作,它在 handle()WRITE 分支中被调用,此时 Selector 已经通知 Channel 可写。

5.7 finishConnect():连接完成

java 复制代码
// io.netty.channel.nio.AbstractNioChannel.AbstractNioUnsafe#finishConnect

@Override
public final void finishConnect() {
    assert eventLoop().inEventLoop();

    try {
        boolean wasActive = isActive();
        // 调用子类实现完成 JDK 层面的连接
        doFinishConnect();
        fulfillConnectPromise(connectPromise, wasActive);
    } catch (Throwable t) {
        fulfillConnectPromise(connectPromise, annotateConnectException(t, requestedRemoteAddress));
    } finally {
        // 取消超时 Future 并清理 connectPromise
        if (connectTimeoutFuture != null) {
            connectTimeoutFuture.cancel(false);
        }
        connectPromise = null;
    }
}

上面的代码中,finishConnect()handle()CONNECT 分支中被触发,而非独立调用。它调用 doFinishConnect() 完成 JDK 层面的三次握手,fulfillConnectPromise() 触发 fireChannelActive() 事件,最后取消超时 Future 并清理 connectPromise

5.8 handle() 事件分发时序图

上面的时序图清晰地展示了 AbstractNioUnsafe.handle() 的三级分发顺序。CONNECT 优先处理的原因在于未完成连接时 SocketChannel 的读写操作会抛出异常。WRITE 次优先处理以便尽快释放写缓冲区内存。READ_AND_ACCEPT 最后处理,NONE 作为 JDK Bug 兜底也走 read() 路径。后续章节将分别分析 NioByteUnsafeNioMessageUnsaferead() 的具体实现。

六、AbstractNioByteChannel 与 NioByteUnsafe:字节流读写的基石

AbstractNioByteChannel 是所有基于字节流的 NIO Channel 的抽象基类。它继承 AbstractNioChannel,为 ByteBufFileRegion 类型的消息提供读写基础实现。其内部类 NioByteUnsafe 覆写了 read() 方法,实现了 Netty 标志性的读循环。

6.1 类结构与配置

AbstractNioByteChannel 的构造器调用 super(parent, ch, SelectionKey.OP_READ) 设置读兴趣操作为 OP_READ。它定义了 ChannelMetadata METADATA = new ChannelMetadata(false, 16),其中 hasDisconnect = false 表示不支持 disconnect()(通过 close() 替代),defaultConnectAttemptCount = 16 为默认连接重试次数。

AbstractNioByteChannel.newUnsafe() 返回 new NioByteUnsafe(),而 NioByteUnsafe 继承 AbstractNioUnsafe,覆写了 read() 方法实现字节流读取。

6.2 NioByteUnsafe.read():四步读循环

NioByteUnsafe.read() 是 Netty NIO 读取的核心方法,通过分配缓冲区、读取数据、传播事件、判断继续的四步循环实现高效读取。

java 复制代码
// io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#read

@Override
public final void read() {
    final ChannelConfig config = config();
    // 检查是否应中断读就绪处理(半关闭场景)
    if (shouldBreakReadReady(config)) {
        clearReadPending();
        return;
    }
    final ChannelPipeline pipeline = pipeline();
    final ByteBufAllocator allocator = config.getAllocator();
    final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
    allocHandle.reset(config);

    ByteBuf byteBuf = null;
    boolean close = false;
    try {
        do {
            // 步骤1:分配缓冲区
            byteBuf = allocHandle.allocate(allocator);
            // 步骤2:从 SocketChannel 读取数据并记录读取量
            allocHandle.lastBytesRead(doReadBytes(byteBuf));
            if (allocHandle.lastBytesRead() <= 0) {
                // 无数据可读,释放缓冲区
                byteBuf.release();
                byteBuf = null;
                // bytesRead < 0 表示 EOF,需要关闭
                close = allocHandle.lastBytesRead() < 0;
                if (close) {
                    readPending = false;
                }
                break;
            }

            // 步骤3:递增计数并传播读事件
            allocHandle.incMessagesRead(1);
            readPending = false;
            pipeline.fireChannelRead(byteBuf);
            byteBuf = null;
        // 步骤4:判断是否继续读取
        } while (allocHandle.continueReading());

        allocHandle.readComplete();
        pipeline.fireChannelReadComplete();

        if (close) {
            closeOnRead(pipeline);
        }
    } catch (Throwable t) {
        handleReadException(pipeline, byteBuf, t, close, allocHandle);
    } finally {
        // 处理用户在回调中手动调用 read() 的场景
        if (!readPending && !config.isAutoRead()) {
            removeReadOp();
        }
    }
}

上面的代码展示了四步读循环的完整实现。步骤1 allocHandle.allocate(allocator) 根据自适应分配器的猜测值分配适当大小的 ByteBuf步骤2 doReadBytes(byteBuf)SocketChannel 读取数据到 ByteBufallocHandle.lastBytesRead() 记录实际读取量。当 lastBytesRead <= 0 时退出循环,lastBytesRead < 0 表示对端关闭(EOF)。步骤3 fireChannelRead(byteBuf) 将读取到的数据沿 ChannelPipeline 传播。步骤4 continueReading() 根据分配器的策略判断是否继续读取。

finally 块中的逻辑处理了一个边界场景:当用户在 channelReadchannelReadComplete 回调中手动调用 read() 时,readPending 会被重新设置为 true。如果此时 autoRead 关闭且 readPendingfalse,则调用 removeReadOp() 移除读兴趣操作,避免不必要的事件唤醒。

6.3 closeOnRead():半关闭处理

java 复制代码
// io.netty.channel.nio.AbstractNioByteChannel.NioByteUnsafe#closeOnRead

private void closeOnRead(ChannelPipeline pipeline) {
    if (!isInputShutdown0()) {
        // 输入未关闭
        if (isAllowHalfClosure(config())) {
            // 允许半关闭:仅关闭输入端
            shutdownInput();
            pipeline.fireUserEventTriggered(ChannelInputShutdownEvent.INSTANCE);
        } else {
            // 不允许半关闭:直接关闭整个 Channel
            close(voidPromise());
        }
    } else if (!inputClosedSeenErrorOnRead) {
        // 输入已关闭且未见过错误:触发读完成事件
        inputClosedSeenErrorOnRead = true;
        pipeline.fireUserEventTriggered(ChannelInputShutdownReadComplete.INSTANCE);
    }
}

上面的代码展示了半关闭的处理逻辑。当 isInputShutdown0()false(输入未关闭)时,如果配置了半关闭(isAllowHalfClosure()),则调用 shutdownInput() 仅关闭输入端并触发 ChannelInputShutdownEvent;否则直接 close() 关闭整个 Channel。当 isInputShutdown0()true(输入已关闭)且未见过错误时,触发 ChannelInputShutdownReadComplete 通知 Pipeline 输入已彻底关闭。

6.4 doWrite():写循环

AbstractNioByteChannel.doWrite() 是字节流写入的基础实现,通过 writeSpinCount 控制单次写循环的最大尝试次数。

java 复制代码
// io.netty.channel.nio.AbstractNioByteChannel#doWrite

@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
    int writeSpinCount = config().getWriteSpinCount();
    do {
        Object msg = in.current();
        if (msg == null) {
            // 全部写入完成,清除 OP_WRITE
            clearOpWrite();
            return;
        }
        // 尝试写入当前消息
        writeSpinCount -= doWriteInternal(in, msg);
    } while (writeSpinCount > 0);

    // 写入未完成,根据情况选择策略
    incompleteWrite(writeSpinCount < 0);
}

上面的代码中,doWrite() 使用 writeSpinCount 循环调用 doWriteInternal() 尝试写入。当所有消息写入完成时 clearOpWrite() 清除写兴趣并返回。当 writeSpinCount 耗尽时调用 incompleteWrite() 处理未完成写入。

6.5 doWriteInternal():消息写入

java 复制代码
// io.netty.channel.nio.AbstractNioByteChannel#doWriteInternal

private int doWriteInternal(ChannelOutboundBuffer in, Object msg) throws Exception {
    if (msg instanceof ByteBuf) {
        ByteBuf buf = (ByteBuf) msg;
        if (!buf.isReadable()) {
            // 空消息,直接移除
            in.remove();
            return 0;
        }
        // 调用子类 doWriteBytes() 写入
        final int localFlushedAmount = doWriteBytes(buf);
        if (localFlushedAmount > 0) {
            in.progress(localFlushedAmount);
            if (!buf.isReadable()) {
                // 完全写入,移除
                in.remove();
            }
            return 1;
        }
    } else if (msg instanceof FileRegion) {
        FileRegion region = (FileRegion) msg;
        // 已完成的 FileRegion,直接移除
        if (region.transferred() >= region.count()) {
            in.remove();
            return 0;
        }
        // 零拷贝写入
        long localFlushedAmount = doWriteFileRegion(region);
        if (localFlushedAmount > 0) {
            in.progress(localFlushedAmount);
            if (region.transferred() >= region.count()) {
                in.remove();
            }
            return 1;
        }
    } else {
        throw new Error("Unexpected message type: " + className(msg));
    }
    // 写入返回 0,发送缓冲区已满
    return WRITE_STATUS_SNDBUF_FULL;
}

上面的代码展示了 doWriteInternal() 对两种消息类型的处理。对于 ByteBuf,调用 doWriteBytes() 写入并记录进度。对于 FileRegion,调用 doWriteFileRegion() 实现零拷贝写入。当写入返回 0 时返回 WRITE_STATUS_SNDBUF_FULL,表示发送缓冲区已满。返回值用于递减 writeSpinCount:返回 1 消耗一次自旋,返回 0 不消耗(空消息),返回 WRITE_STATUS_SNDBUF_FULLInteger.MAX_VALUE)会使 writeSpinCount 减为负数,从而提前结束循环。

6.6 incompleteWrite():两策略分支

java 复制代码
// io.netty.channel.nio.AbstractNioByteChannel#incompleteWrite

protected final void incompleteWrite(boolean setOpWrite) {
    if (setOpWrite) {
        // 策略1:注册 OP_WRITE,等待 Selector 通知可写
        setOpWrite();
    } else {
        // 策略2:清除 OP_WRITE,重新提交写任务
        clearOpWrite();
        // 当前 socket 仍然可写,无需等待事件通知
        eventLoop().execute(flushTask);
    }
}

上面的代码展示了 incompleteWrite() 的两种策略。当 setOpWrite == true(发送缓冲区已满)时,调用 setOpWrite() 注册 OP_WRITE 等待 Selector 通知可写。当 setOpWrite == false(自旋次数耗尽但 socket 仍可写)时,clearOpWrite() 清除 OP_WRITE,通过 eventLoop().execute(flushTask) 重新提交写任务。这保证了在其他任务有机会执行的同时,写操作不会被饿死。

6.7 NioByteUnsafe.read() 读循环时序图

上面的时序图清晰地展示了 NioByteUnsafe.read() 的四步读循环:分配缓冲区、读取数据、传播事件、判断继续。特别标注了 lastBytesRead <= 0 时的退出逻辑和 lastBytesRead < 0 时的 EOF 关闭路径,以及 finally 块中根据 readPendingautoRead 决定是否移除读兴趣操作的逻辑。

七、NioSocketChannel:客户端 Channel 的读写实现

NioSocketChannel 是 NIO 传输中客户端 Channel 的具体实现,它继承 AbstractNioByteChannel 并实现 SocketChannel 接口。与父类的基础写入不同,NioSocketChannel 覆写了 doWrite() 方法,引入了 gathering write 批量写入优化,这是 NIO 传输写入侧的核心性能设计。

7.1 类结构与构造器链

NioSocketChannel 的类定义为 extends AbstractNioByteChannel implements io.netty.channel.socket.SocketChannel。它持有一个 NioSocketChannelConfig config 字段,用于管理 Socket 相关配置。

构造器链从 newChannel(SelectorProvider, SocketProtocolFamily) 开始,通过 SelectorProviderUtil.newChannel()provider.openSocketChannel() 创建 JDK SocketChannel,最终调用 super(parent, socket) 传入 AbstractNioByteChannel 构造器(设置 OP_READ 为读兴趣操作),同时创建 NioSocketChannelConfig

java 复制代码
// io.netty.channel.socket.nio.NioSocketChannel#NioSocketChannel

public NioSocketChannel(Channel parent, SocketChannel socket) {
    super(parent, socket);
    config = new NioSocketChannelConfig(this, socket.socket());
}

上面的代码中,构造器调用 super(parent, socket) 进入 AbstractNioByteChannel 的构造器,后者会调用 AbstractNioChannel 的构造器完成 configureBlocking(false) 的非阻塞设置。NioSocketChannelConfig 在构造时立即调用 calculateMaxBytesPerGatheringWrite() 初始化 gathering write 的最大字节数。

7.2 isActive() 与读写基础方法

java 复制代码
// io.netty.channel.socket.nio.NioSocketChannel#isActive

@Override
public boolean isActive() {
    SocketChannel ch = javaChannel();
    return ch.isOpen() && ch.isConnected();
}

上面的代码中,isActive() 要求 JDK SocketChannel 同时满足 isOpen()isConnected() 两个条件。这与 NioServerSocketChannelisActive() 判断逻辑不同,后者只需要 isOpen() && isBound()

java 复制代码
// io.netty.channel.socket.nio.NioSocketChannel#doReadBytes

@Override
protected int doReadBytes(ByteBuf byteBuf) throws Exception {
    final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
    // 记录尝试读取的字节数
    allocHandle.attemptedBytesRead(byteBuf.writableBytes());
    // 通过 ByteBuf.writeBytes(SocketChannel, int) 直接读取
    return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
}

上面的代码中,doReadBytes() 先通过 allocHandle.attemptedBytesRead(byteBuf.writableBytes()) 记录本次尝试读取的字节数(即 ByteBuf 的可写容量),然后调用 byteBuf.writeBytes(javaChannel(), attemptedBytesRead) 将数据从 JDK SocketChannel 直接读入 ByteBuf 内部。这个方法由 NioByteUnsafe.read() 的步骤2调用。

java 复制代码
// io.netty.channel.socket.nio.NioSocketChannel#doWriteBytes

@Override
protected int doWriteBytes(ByteBuf buf) throws Exception {
    final int expectedWrittenBytes = buf.readableBytes();
    // 通过 ByteBuf.readBytes(SocketChannel, int) 写入
    return buf.readBytes(javaChannel(), expectedWrittenBytes);
}

上面的代码中,doWriteBytes() 是单次写入的实现,通过 buf.readBytes(javaChannel(), expectedWrittenBytes)ByteBuf 数据写入 JDK SocketChannel,返回实际写入字节数。这个方法在父类 AbstractNioByteChannel.doWriteInternal() 中被调用,但在 NioSocketChannel 覆写的 doWrite()不会被调用------后者使用 gathering write 路径。

7.3 doWrite():gathering write 批量写入优化

NioSocketChannel.doWrite() 覆写了父类 AbstractNioByteChannel.doWrite(),引入了 ByteBuffer[] 批量写入能力,这是 NIO 传输写入侧的核心优化。

java 复制代码
// io.netty.channel.socket.nio.NioSocketChannel#doWrite

@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
    SocketChannel ch = javaChannel();
    int writeSpinCount = config().getWriteSpinCount();
    do {
        // 缓冲区为空,全部写入完成
        if (in.isEmpty()) {
            clearOpWrite();
            return;
        }

        // 获取单次 gathering write 最大字节数
        int maxBytesPerGatheringWrite = ((NioSocketChannelConfig) config).getMaxBytesPerGatheringWrite();
        // 将 ChannelOutboundBuffer 中的 ByteBuf 转换为 ByteBuffer 数组
        ByteBuffer[] nioBuffers = in.nioBuffers(1024, maxBytesPerGatheringWrite);
        int nioBufferCnt = in.nioBufferCount();

        switch (nioBufferCnt) {
        case 0:
            // 非 ByteBuf 消息,回退到父类写入逻辑
            writeSpinCount -= doWrite0(in);
            break;
        case 1: {
            // 单个 ByteBuffer,非聚合写入
            ByteBuffer buffer = nioBuffers[0];
            int attemptedBytes = buffer.remaining();
            final int localWrittenBytes = ch.write(buffer);
            if (localWrittenBytes <= 0) {
                incompleteWrite(true);
                return;
            }
            adjustMaxBytesPerGatheringWrite(attemptedBytes, localWrittenBytes, maxBytesPerGatheringWrite);
            in.removeBytes(localWrittenBytes);
            --writeSpinCount;
            break;
        }
        default: {
            // 多个 ByteBuffer,gathering write 批量写入
            long attemptedBytes = in.nioBufferSize();
            final long localWrittenBytes = ch.write(nioBuffers, 0, nioBufferCnt);
            if (localWrittenBytes <= 0) {
                incompleteWrite(true);
                return;
            }
            adjustMaxBytesPerGatheringWrite((int) attemptedBytes, (int) localWrittenBytes,
                    maxBytesPerGatheringWrite);
            in.removeBytes(localWrittenBytes);
            --writeSpinCount;
            break;
        }
        }
    } while (writeSpinCount > 0);

    incompleteWrite(writeSpinCount < 0);
}

上面的代码展示了 gathering write 的完整实现。核心逻辑分为以下几步:

第一步,通过 in.nioBuffers(1024, maxBytesPerGatheringWrite)ChannelOutboundBuffer 中的多个 ByteBuf 提取为 ByteBuffer 数组,最多 1024 个缓冲区,总字节数不超过 maxBytesPerGatheringWrite

第二步,switch(nioBufferCnt) 根据缓冲区数量选择写入策略:case 0 回退到 doWrite0(in) 父类逻辑(处理 FileRegion 等非 ByteBuf 消息);case 1 调用 ch.write(buffer) 单次写入;default 调用 ch.write(nioBuffers, 0, nioBufferCnt) 一次性将多个 ByteBuffer 写入 SocketChannel,这就是 gathering write。

第三步,localWrittenBytes <= 0 表示发送缓冲区已满,调用 incompleteWrite(true) 注册 OP_WRITE 等待可写通知后返回。

第四步,adjustMaxBytesPerGatheringWrite() 根据写入结果动态调整下次 gathering write 的最大字节数。

7.4 adjustMaxBytesPerGatheringWrite():自适应调整

java 复制代码
// io.netty.channel.socket.nio.NioSocketChannel#adjustMaxBytesPerGatheringWrite

private void adjustMaxBytesPerGatheringWrite(int attempted, int written, int oldMaxBytesPerGatheringWrite) {
    if (attempted == written) {
        // 全部写入成功:尝试翻倍增大限制
        if (attempted << 1 > oldMaxBytesPerGatheringWrite) {
            ((NioSocketChannelConfig) config).setMaxBytesPerGatheringWrite(attempted << 1);
        }
    } else if (attempted > MAX_BYTES_PER_GATHERING_WRITE_ATTEMPTED_LOW_THRESHOLD && written < attempted >>> 1) {
        // 写入不足一半且尝试量超过阈值:减半降低限制
        ((NioSocketChannelConfig) config).setMaxBytesPerGatheringWrite(attempted >>> 1);
    }
}

上面的代码展示了自适应调整策略。attempted == written(全部写入成功)时,如果 attempted << 1(翻倍后)大于当前最大值,则将限制翻倍,允许下次尝试更大的 gathering write。written < attempted >>> 1(写入不足一半)且 attempted > 4096MAX_BYTES_PER_GATHERING_WRITE_ATTEMPTED_LOW_THRESHOLD)时,将限制减半,避免过大的 gathering write 浪费 CPU。这种双向调整策略使 gathering write 的批次大小能够自适应网络条件和操作系统缓冲区状态。

7.5 calculateMaxBytesPerGatheringWrite():初始化策略

java 复制代码
// io.netty.channel.socket.nio.NioSocketChannel.NioSocketChannelConfig#calculateMaxBytesPerGatheringWrite

private void calculateMaxBytesPerGatheringWrite() {
    // 将 SO_SNDBUF 的两倍作为初始值,给操作系统额外处理空间
    int newSendBufferSize = getSendBufferSize() << 1;
    if (newSendBufferSize > 0) {
        setMaxBytesPerGatheringWrite(newSendBufferSize);
    }
}

上面的代码中,calculateMaxBytesPerGatheringWrite()SO_SNDBUF(发送缓冲区大小)的两倍作为 gathering write 的初始最大字节数。翻倍的目的是"给操作系统额外的处理空间"------操作系统的 TCP 栈可能会使用比 SO_SNDBUF 更大的缓冲区。当用户通过 setSendBufferSize() 修改发送缓冲区大小时,会重新调用此方法更新限制。

7.6 doConnect() 与 doFinishConnect():非阻塞连接

java 复制代码
// io.netty.channel.socket.nio.NioSocketChannel#doConnect

@Override
protected boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
    if (localAddress != null) {
        doBind0(localAddress);
    }

    boolean success = false;
    try {
        // 发起非阻塞连接
        boolean connected = SocketUtils.connect(javaChannel(), remoteAddress);
        if (!connected) {
            // 连接未完成,注册 OP_CONNECT 等待连接就绪通知
            addAndSubmit(NioIoOps.CONNECT);
        }
        success = true;
        return connected;
    } finally {
        if (!success) {
            doClose();
        }
    }
}

上面的代码展示了非阻塞连接的实现。SocketUtils.connect(javaChannel(), remoteAddress) 调用 JDK SocketChannel.connect(),非阻塞模式下立即返回:true 表示连接已建立,false 表示连接进行中。当返回 false 时,通过 addAndSubmit(NioIoOps.CONNECT) 注册 OP_CONNECT 兴趣操作,等待 Selector 通知连接就绪后在 handle()CONNECT 分支中调用 finishConnect() 完成连接。

java 复制代码
// io.netty.channel.socket.nio.NioSocketChannel#doFinishConnect

@Override
protected void doFinishConnect() throws Exception {
    if (!javaChannel().finishConnect()) {
        throw new UnsupportedOperationException("finishConnect is not supported for " + getClass().getName());
    }
}

上面的代码中,doFinishConnect() 调用 JDK SocketChannel.finishConnect() 完成三次握手。如果 finishConnect() 返回 false(连接仍未完成),抛出 UnsupportedOperationException,这种情况理论上不应出现,因为 OP_CONNECT 就绪意味着连接已完成或失败。

7.7 prepareToClose() 与 SO_LINGER 处理

java 复制代码
// io.netty.channel.socket.nio.NioSocketChannel.NioSocketChannelUnsafe#prepareToClose

private final class NioSocketChannelUnsafe extends NioByteUnsafe {
    @Override
    protected Executor prepareToClose() {
        try {
            if (javaChannel().isOpen() && config().getSoLinger() > 0) {
                // 提前注销 SelectionKey,避免 EventLoop 空轮询
                doDeregister();
                return GlobalEventExecutor.INSTANCE;
            }
        } catch (Throwable ignore) {
            // 忽略异常:底层 Channel 可能已关闭
        }
        return null;
    }
}

上面的代码展示了 prepareToClose() 的 SO_LINGER 处理。当 SO_LINGER 大于 0 时,close() 操作会阻塞等待数据发送完成,这会阻塞 EventLoop 线程。为此,prepareToClose() 先调用 doDeregister() 提前注销 SelectionKey(避免 EventLoop 空轮询),然后返回 GlobalEventExecutor.INSTANCE------这意味着后续的 close() 操作将在 GlobalEventExecutor 的独立线程中执行,不会阻塞 EventLoop。

7.8 doWrite() gathering write 时序图

上面的时序图清晰地展示了 NioSocketChannel.doWrite() 的 gathering write 完整链路。nioBuffers() 提取 ByteBuffer 数组后,switch(nioBufferCnt) 三分支选择写入策略,adjustMaxBytesPerGatheringWrite() 动态调整下次最大字节数,incompleteWrite() 在写入未完成时选择注册 OP_WRITE 或重新提交 flushTask

八、AbstractNioMessageChannel、NioMessageUnsafe 与 NioServerSocketChannel:消息型读写与连接接受

AbstractNioMessageChannel 是所有基于消息(非字节流)的 NIO Channel 的抽象基类。与 AbstractNioByteChannel 处理 ByteBuf/FileRegion 不同,它处理的是离散的消息对象------最典型的场景就是 NioServerSocketChannel 接受新连接,每条消息是一个 NioSocketChannel

8.1 NioMessageUnsafe.read():消息读循环

NioMessageUnsaferead() 方法与 NioByteUnsafe.read() 有一个关键差异:NioByteUnsafe 每次读取一个 ByteBuf 并立即 fireChannelRead,而 NioMessageUnsafe 批量读取到 List<Object> readBuf 后统一 fireChannelRead

java 复制代码
// io.netty.channel.nio.AbstractNioMessageChannel.NioMessageUnsafe#read

private final class NioMessageUnsafe extends AbstractNioUnsafe {

    private final List<Object> readBuf = new ArrayList<Object>();

    @Override
    public void read() {
        assert eventLoop().inEventLoop();
        final ChannelConfig config = config();
        final ChannelPipeline pipeline = pipeline();
        final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
        allocHandle.reset(config);

        boolean closed = false;
        Throwable exception = null;
        try {
            try {
                do {
                    // 调用子类实现读取消息到 readBuf
                    int localRead = doReadMessages(readBuf);
                    if (localRead == 0) {
                        // 无消息可读,退出循环
                        break;
                    }
                    if (localRead < 0) {
                        // 通道已关闭
                        closed = true;
                        break;
                    }
                    // 累加消息计数
                    allocHandle.incMessagesRead(localRead);
                } while (continueReading(allocHandle));
            } catch (Throwable t) {
                exception = t;
            }

            // 统一传播所有读取到的消息
            int size = readBuf.size();
            for (int i = 0; i < size; i ++) {
                readPending = false;
                pipeline.fireChannelRead(readBuf.get(i));
            }
            // 清空缓冲列表
            readBuf.clear();
            allocHandle.readComplete();
            pipeline.fireChannelReadComplete();

            if (exception != null) {
                closed = closeOnReadError(exception);
                pipeline.fireExceptionCaught(exception);
            }

            if (closed) {
                inputShutdown = true;
                if (isOpen()) {
                    close(voidPromise());
                }
            }
        } finally {
            if (!readPending && !config.isAutoRead()) {
                removeReadOp();
            }
        }
    }
}

上面的代码展示了消息读循环的完整实现。doReadMessages(readBuf) 调用子类实现将消息读取到 readBuf 列表中,返回值 localRead 表示读取的消息数量:0 表示无消息可读退出循环,< 0 表示通道已关闭。循环结束后,遍历 readBuf 逐个 fireChannelRead(readBuf.get(i)) 传播消息,然后 readBuf.clear() 清空列表。

NioByteUnsafe.read() 对比,两者的核心差异在于传播时机:NioByteUnsafe 在循环内部每读一条就 fireChannelRead,而 NioMessageUnsafe 在循环外部批量读取完成后统一传播。这种设计差异源于消息语义不同------字节流数据可以分片处理,而连接接受等消息需要以完整对象为单位传播。

8.2 AbstractNioMessageChannel.doWrite():消息写入循环

java 复制代码
// io.netty.channel.nio.AbstractNioMessageChannel#doWrite

@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
    int maxMessagesPerWrite = maxMessagesPerWrite();
    while (maxMessagesPerWrite > 0) {
        Object msg = in.current();
        if (msg == null) {
            break;
        }
        try {
            boolean done = false;
            // 写自旋重试
            for (int i = config().getWriteSpinCount() - 1; i >= 0; i--) {
                if (doWriteMessage(msg, in)) {
                    done = true;
                    break;
                }
            }

            if (done) {
                maxMessagesPerWrite--;
                in.remove();
            } else {
                break;
            }
        } catch (Exception e) {
            if (continueOnWriteError()) {
                maxMessagesPerWrite--;
                in.remove(e);
            } else {
                throw e;
            }
        }
    }
    if (in.isEmpty()) {
        // 全部写入完成,清除写兴趣
        removeAndSubmit(NioIoOps.WRITE);
    } else {
        // 写入未完成,注册写兴趣
        addAndSubmit(NioIoOps.WRITE);
    }
}

上面的代码展示了消息写入循环的实现。maxMessagesPerWrite 控制单次写入的最大消息数,内部通过 writeSpinCount 重试 doWriteMessage()。写入全部完成时 removeAndSubmit(NioIoOps.WRITE) 清除写兴趣;写入未完成时 addAndSubmit(NioIoOps.WRITE) 注册写兴趣等待 Selector 通知。注意 NioServerSocketChannel 覆写了 doWriteMessage() 抛出 UnsupportedOperationException,因为服务端 Channel 不参与数据写入。

8.3 closeOnReadError():ServerChannel 容错策略

java 复制代码
// io.netty.channel.nio.AbstractNioMessageChannel#closeOnReadError

protected boolean closeOnReadError(Throwable cause) {
    if (!isActive()) {
        // 通道已失活,需要关闭
        return true;
    }
    if (cause instanceof PortUnreachableException) {
        // 端口不可达是临时错误,不需要关闭
        return false;
    }
    if (cause instanceof IOException) {
        // ServerChannel 在 IO 异常时不应关闭,可以继续接受新连接
        return !(this instanceof ServerChannel);
    }
    return true;
}

上面的代码展示了 closeOnReadError() 的容错策略。对于 ServerChannel 实例,IOException 时返回 false------这意味着服务端 Channel 在遇到 "too many open files" 等 IO 异常时不会关闭,而是继续运行接受新连接。这是一个重要的容错设计:服务端 Channel 的可用性比单个连接更重要,临时性 IO 异常不应导致整个服务端关闭。

8.4 NioServerSocketChannel:服务端 Channel 实现

NioServerSocketChannel 的类定义为 extends AbstractNioMessageChannel implements io.netty.channel.socket.ServerSocketChannel。它的核心职责是绑定端口、接受新连接。

java 复制代码
// io.netty.channel.socket.nio.NioServerSocketChannel#NioServerSocketChannel

public NioServerSocketChannel(ServerSocketChannel channel) {
    super(null, channel, SelectionKey.OP_ACCEPT);
    config = new NioServerSocketChannelConfig(this, javaChannel().socket());
}

上面的代码中,构造器调用 super(null, channel, SelectionKey.OP_ACCEPT)OP_ACCEPT 设置为读兴趣操作。这意味着 doBeginRead() 时注册的是 OP_ACCEPT 而非 OP_READ,当有新连接到达时 Selector 会通知 OP_ACCEPT 就绪。

java 复制代码
// io.netty.channel.socket.nio.NioServerSocketChannel#isActive

@Override
public boolean isActive() {
    return isOpen() && javaChannel().socket().isBound();
}

上面的代码中,isActive() 只需要 isOpen() && isBound(),不要求 isConnected()------服务端 Socket 不需要建立连接,只需要绑定端口即可处于活跃状态。

8.5 doBind() 与 doReadMessages():绑定与连接接受

java 复制代码
// io.netty.channel.socket.nio.NioServerSocketChannel#doBind

@Override
protected void doBind(SocketAddress localAddress) throws Exception {
    javaChannel().bind(localAddress, config.getBacklog());
}

上面的代码中,doBind() 调用 JDK ServerSocketChannel.bind(localAddress, backlog) 绑定地址并设置连接队列 backlog。backlog 参数来自 ServerSocketChannelConfig,表示操作系统内核维护的未完成连接队列(SYN 队列)和已完成连接队列(ACCEPT 队列)的最大长度。

java 复制代码
// io.netty.channel.socket.nio.NioServerSocketChannel#doReadMessages

@Override
protected int doReadMessages(List<Object> buf) throws Exception {
    SocketChannel ch = SocketUtils.accept(javaChannel());

    try {
        if (ch != null) {
            // 将接受的 SocketChannel 包装为 Netty 的 NioSocketChannel
            buf.add(new NioSocketChannel(this, ch));
            return 1;
        }
    } catch (Throwable t) {
        logger.warn("Failed to create a new channel from an accepted socket.", t);

        try {
            ch.close();
        } catch (Throwable t2) {
            logger.warn("Failed to close a socket.", t2);
        }
    }

    return 0;
}

上面的代码展示了连接接受的核心实现。SocketUtils.accept(javaChannel()) 调用 JDK ServerSocketChannel.accept() 接受新连接,返回 SocketChannel。如果 ch != null,将其包装为 new NioSocketChannel(this, ch) 添加到消息列表并返回 1。如果 accept() 抛出异常,记录 WARN 日志并尝试关闭接受的 SocketChannel 防止描述符泄漏,返回 0 继续事件循环。注意 this 作为 parent 传入,使 NioSocketChannelparent() 方法可以返回所属的 ServerSocketChannel

8.6 服务端 Channel 的操作禁用

NioServerSocketChannel 覆写了多个方法直接抛出 UnsupportedOperationException

java 复制代码
// io.netty.channel.socket.nio.NioServerSocketChannel

// Unnecessary stuff
@Override
protected boolean doConnect(
        SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
    throw new UnsupportedOperationException();
}

@Override
protected void doFinishConnect() throws Exception {
    throw new UnsupportedOperationException();
}

@Override
protected void doDisconnect() throws Exception {
    throw new UnsupportedOperationException();
}

@Override
protected boolean doWriteMessage(Object msg, ChannelOutboundBuffer in) throws Exception {
    throw new UnsupportedOperationException();
}

@Override
protected final Object filterOutboundMessage(Object msg) throws Exception {
    throw new UnsupportedOperationException();
}

源码中这些方法集中在一个区域,上方注释 // Unnecessary stuff(不必要的东西)直白地表明了设计意图。五个方法的禁用逻辑如下:

  • doConnect():服务端不主动发起连接,只接受连接
  • doFinishConnect():服务端不经历连接完成阶段
  • doDisconnect():服务端通过 close() 关闭,不走断连逻辑
  • doWriteMessage():服务端不发送数据,只接受新连接
  • filterOutboundMessage():服务端无出站消息,无需过滤

值得注意的是,这五个方法在父类 AbstractNioMessageChannelAbstractNioChannel 中均为抽象方法或默认实现。如果 NioServerSocketChannel 不覆写,调用时会执行父类逻辑,可能导致难以排查的行为异常。通过覆写并抛异常而非留空实现,可以在调用路径错误时快速发现问题。

8.7 NioServerSocketChannel 连接接受时序图

上面的时序图清晰地展示了从 doBind()accept()new NioSocketChannel()fireChannelRead() 的完整链路。NioMessageUnsafe.read() 的批量读取 + 统一传播与 NioByteUnsafe.read() 的逐条传播形成鲜明对比,这源于消息型 Channel(连接接受)与字节流型 Channel(数据读取)的语义差异。

九、NioChannelOption:JDK 标准 Socket 选项的桥接

NioChannelOption 是 Netty ChannelOption 与 JDK java.net.SocketOption 之间的桥接器。它使 Netty 的 Channel 配置体系能够透明地代理 JDK 原生 Socket 选项,无需为每个 SocketOption 单独定义 ChannelOption 常量。

9.1 类结构与工厂方法

NioChannelOption<T> 的类定义为 extends ChannelOption<T>,内部持有 SocketOption<T> option 字段。

java 复制代码
// io.netty.channel.socket.nio.NioChannelOption#NioChannelOption

private NioChannelOption(SocketOption<T> option) {
    super(option.name());
    this.option = option;
}

public static <T> ChannelOption<T> of(SocketOption<T> option) {
    return new NioChannelOption<T>(option);
}

上面的代码中,构造器调用 super(option.name())SocketOption.name() 作为 ChannelOption 的名称。of() 工厂方法是唯一的创建入口,将 JDK SocketOption 包装为 NioChannelOption

9.2 setOption() 与 getOption():选项设置与获取

java 复制代码
// io.netty.channel.socket.nio.NioChannelOption#setOption

static <T> boolean setOption(Channel jdkChannel, NioChannelOption<T> option, T value) {
    NetworkChannel channel = (NetworkChannel) jdkChannel;
    // 检查是否支持该选项
    if (!channel.supportedOptions().contains(option.option)) {
        return false;
    }
    // ServerSocketChannel 跳过 IP_TOS(JDK Bug workaround)
    if (channel instanceof ServerSocketChannel && option.option == StandardSocketOptions.IP_TOS) {
        return false;
    }
    try {
        channel.setOption(option.option, value);
        return true;
    } catch (IOException e) {
        throw new ChannelException(e);
    }
}

上面的代码展示了 setOption() 的实现。先将 jdkChannel 强转为 NetworkChannel(JDK 中所有支持 Socket 选项的 Channel 都实现此接口),检查 supportedOptions().contains(option.option) 确认支持该选项。然后检查 ServerSocketChannel + IP_TOS 的组合------这是一个 JDK Bug 的 workaround,ServerSocketChannel 不支持 IP_TOS 选项但 supportedOptions() 可能错误地包含它。最后调用 channel.setOption(option.option, value) 设置值。

getOption() 的逻辑类似,同样包含 ServerSocketChannel 跳过 IP_TOS 的 workaround。

9.3 getOptions():批量获取支持选项

java 复制代码
// io.netty.channel.socket.nio.NioChannelOption#getOptions

static ChannelOption<?>[] getOptions(Channel jdkChannel) {
    NetworkChannel channel = (NetworkChannel) jdkChannel;
    Set<SocketOption<?>> supportedOpts = channel.supportedOptions();

    if (channel instanceof ServerSocketChannel) {
        List<ChannelOption<?>> extraOpts = new ArrayList<ChannelOption<?>>(supportedOpts.size());
        for (SocketOption<?> opt : supportedOpts) {
            if (opt == StandardSocketOptions.IP_TOS) {
                // ServerSocketChannel 跳过 IP_TOS
                continue;
            }
            extraOpts.add(new NioChannelOption(opt));
        }
        return extraOpts.toArray(new ChannelOption[0]);
    } else {
        ChannelOption<?>[] extraOpts = new ChannelOption[supportedOpts.size()];
        int i = 0;
        for (SocketOption<?> opt : supportedOpts) {
            extraOpts[i++] = new NioChannelOption(opt);
        }
        return extraOpts;
    }
}

上面的代码展示了 getOptions() 的批量获取逻辑。遍历 channel.supportedOptions() 获取所有支持的 SocketOption,逐个包装为 NioChannelOption 返回数组。对于 ServerSocketChannel,额外跳过 IP_TOS 选项。

9.4 在 ChannelConfig 中的集成

NioSocketChannelConfigNioServerSocketChannelConfig 都覆写了 setOption()/getOption()/getOptions() 方法来集成 NioChannelOption

java 复制代码
// io.netty.channel.socket.nio.NioSocketChannel.NioSocketChannelConfig#setOption

@Override
public <T> boolean setOption(ChannelOption<T> option, T value) {
    if (option instanceof NioChannelOption) {
        return NioChannelOption.setOption(jdkChannel(), (NioChannelOption<T>) option, value);
    }
    return super.setOption(option, value);
}

上面的代码展示了集成逻辑。setOption() 先检查 option instanceof NioChannelOption,是则委托 NioChannelOption.setOption(jdkChannel(), ...) 处理 JDK 原生选项;否则委托父类 DefaultSocketChannelConfig.setOption() 处理 Netty 自定义选项(如 TCP_NODELAYSO_KEEPALIVE 等)。这种双路径设计使 Netty 既能管理自定义 ChannelOption,又能透明代理 JDK 原生 SocketOption

十、整体链路串联:从 Selector 到 Channel 的完整闭环

至此,我们已经逐一分析了 NIO 传输的各个组件。现在将这些组件串联起来,形成从 SelectorChannel 的完整 IO 处理闭环。

10.1 完整调用链路

NioIoHandler.newFactory() 创建工厂开始,到 NioByteUnsafe.read() / forceFlush() 执行读写操作,整个链路可以概括为以下流程:

这个链路体现了 Netty 4.2 NIO 传输的**"JDK Selector 委托 + 数组优化 + 幂等提交"**设计思想。NioIoHandler 委托 JDK Selector 完成事件多路复用,SelectedSelectionKeySet 通过反射替换将就绪键集从 HashSet 优化为数组,addAndSubmit()/removeAndSubmit() 通过 IoRegistration.submit() 实现幂等兴趣操作管理。三项设计在 JDK NIO 框架约束下将性能发挥到极致。

10.2 核心设计思想

总结 NIO 传输的三大核心设计思想:

"JDK 委托" ------NioIoHandler 不直接操作系统调用而是委托 JDK SelectorDefaultNioRegistration 不直接管理事件而是封装 SelectionKeyNioChannelOption 不定义新选项而是代理 JDK SocketOption。这种委托策略使 Netty 的 NIO 传输完全运行在 JDK NIO 框架之上,获得了跨平台能力。

"数组优化" ------SelectedSelectionKeySet 通过反射替换 Selector 内部的 HashSet 为数组实现,将就绪键添加从 Set.add() 的哈希操作优化为 keys[size++] = o 的数组追加,遍历从 Iterator 优化为索引访问。这是在 JDK 框架约束下的极致性能优化。

"幂等提交" ------addAndSubmit()/removeAndSubmit() 先检查操作是否已在兴趣集中,仅在需要变更时通过 IoRegistration.submit() 提交。这种幂等设计避免了对 SelectionKey.interestOps() 的无效调用,减少了对 Selector 内部状态的扰动。

十一、全文小结

本文聚焦 Netty 4.2 中 NIO 传输的完整实现,从 JDK Selector 事件循环和 Channel 读写两个维度,深入分析了 NioIoHandler 事件循环、SelectedSelectionKeySet 数组优化、NioIoOps 事件抽象、AbstractNioUnsafe 事件分发、NioByteUnsafe 读循环、NioSocketChannel gathering write 以及 NioServerSocketChannel 连接接受的核心机制。

在事件循环层面,NioIoHandler.run() 通过 SelectStrategy 策略判定、wakenUp 原子标志防重复唤醒、select() 的空轮询检测与 rebuildSelector0() 重建,实现了基于 JDK Selector 的稳定事件循环。在就绪键优化层面,SelectedSelectionKeySet 通过反射替换 Selector 内部的 HashSet 为数组实现,将就绪键添加从哈希操作优化为数组追加,processSelectedKeysOptimized() 利用索引遍历消除 Iterator 开销。在事件抽象层面,NioIoOpsSelectionKey 的兴趣操作封装为位掩码常量,通过 with()/without() 的位运算组合操作,EVENTS[] 缓存数组实现 eventOf() 的 O(1) 查找,DefaultNioRegistration 封装 SelectionKey 并通过 submit() 直接调用 key.interestOps() 修改兴趣集。在 Channel 抽象层面,AbstractNioUnsafe.handle() 作为唯一的 IO 事件入口,按 CONNECTWRITEREAD_AND_ACCEPT 固定顺序分发事件,flush0() 通过 isFlushPending() 检测 OP_WRITE 避免重复刷新,finishConnect()handle() 中被触发而非独立调用。在字节流读写层面,NioByteUnsafe.read() 通过分配缓冲区、读取数据、传播事件、判断继续的四步循环实现读取,doWrite() 通过 writeSpinCount 控制写入次数,incompleteWrite() 在写入未完成时选择注册 OP_WRITE 或重新提交 flushTask。在 SocketChannel 实现层面,NioSocketChannel.doWrite() 覆写引入 ByteBuffer[] gathering write 优化,adjustMaxBytesPerGatheringWrite() 根据写入结果动态调整最大字节数,doConnect() 通过非阻塞连接 + OP_CONNECT 注册实现异步连接。在消息型读写层面,NioMessageUnsafe.read() 使用 List<Object> readBuf 批量接受连接后统一传播,NioServerSocketChannel.doReadMessages() 通过 SocketUtils.accept() 接受新连接并包装为 NioSocketChannelcloseOnReadError()ServerChannel 容错策略保证服务端在 IO 异常时持续运行。


原创不易,如果本文对您有帮助,带来了些许灵感或启发,烦请动动小手点赞、关注、转发、收藏。这是作者持续更新的动力源泉,衷心感谢您的支持。我会尽量在工作之余,为大家带来更高品质的内容,努力保持周更。

相关推荐
Lyra_Infra3 小时前
云效主机部署场景下 Python 服务生命周期问题复盘
后端·python
程序员cxuan3 小时前
GPT images 2.5 一手实测,这也太颠了。。。
后端·程序员
山岚的运维笔记3 小时前
mysql 专业笔记 -- 第 17 章:连接:连接三个具有相同名称 ID 的表
运维·数据库·笔记·后端·学习·mysql·dba
遨翔在知识的海洋里3 小时前
nest(3)- filter和Interceptor
后端
挽安6214 小时前
若依微服务 Nacos 2.x 控制台配置列表为空?一个 tenant_id 字段引发的血案
后端
周杰伦fans4 小时前
ASP.NET Core Identity 从入门到实战:常见问题与解决方案
后端·asp.net
n8n4 小时前
Spring AI 三层架构详解:ChatModel → ChatClient → Controller,同步调用 vs 流式调用(SSE)
后端
小傅哥4 小时前
Java + DDD,1:1 复刻 Deepseek Harness 项目
前端·后端·ai编程
Lambert2814 小时前
AgentScope Java 从零(03):Agent 的记性默认全开,我在第 50 轮翻了车
后端·aigc
积硅步致千里4 小时前
Word 里的 .wmf 其实是 EMF:一次 metafile 转 PNG 排障
前端·后端