Netty 4.2.x 源码深度解析 (十七):Channel 底层读写 —— Unsafe 的 I/O 操作内核

Channel 既是 Netty 对网络连接套接字的抽象,也是所有 IO 数据进出 Netty 的唯一通道。但 Channel 对外暴露的方法绝大多数只是异步编排入口,真正把数据写进操作系统、从操作系统读出数据的,是收口在 Unsafe 内部接口里的底层 IO 操作。一次 bind 如何让端口真正监听,一次 connect 如何在非阻塞模式下完成连接握手,一次 read 如何把字节读进 ByteBuf 并送入 Pipeline,一次 write/flush 又如何把 ChannelOutboundBuffer 里的数据批量写进 Socket,这些正是本文要回答的问题。

在 Netty 4.2 的 IoHandler/IoHandle 抽象下,这些操作统一收口到 handle(IoRegistration, IoEvent) 一个入口完成就绪事件分发,兴趣事件的增删由 NioIoOps 位图配合 IoRegistration.submit() 提交内核。围绕 Unsafe 读写操作从 Netty 到操作系统的完整流转路径,以下几个问题将贯穿本文的源码分析主线:

  • 在 Netty 4.2 的 IoHandler/IoHandle 抽象下,AbstractNioUnsafe 如何通过 handle(IoRegistration, IoEvent) 这一个统一入口分发连接、写入、读取三类 IO 事件,NioIoOps 兴趣位图又如何与 JDK 的 SelectionKey 兴趣集一一对应?
  • AbstractUnsafe.connect() 如何通过 doConnect() 提交 JDK SocketChannel.connect(),连接未就绪时如何用 addAndSubmit(NioIoOps.CONNECT) 注册连接关注,finishConnect() 又如何在握手完成后触发 fireChannelActive()?
  • read() 作为读操作统一入口,如何按 Channel 类型分流,服务端 NioServerSocketChannel.doReadMessages() 的 accept 与客户端 NioSocketChannel.doReadBytes() 的字节读取各自如何把数据送入 Pipeline?
  • flush() 如何借助 ChannelOutboundBuffer 触发 doWrite() 循环写入,字节型 Channel 的 gather write 与消息型 Channel 的逐条发送有何差异,写不动时又是如何通过 addAndSubmit(NioIoOps.WRITE) 让 EventLoop 稍后接管续写的?
  • doBeginRead() 如何通过 readPending 标志与 addAndSubmit(readOps) 建立读关注,addAndSubmit/removeAndSubmit 又在 registration().submit() 这一步如何完成从兴趣集变更到内核生效的提交?

本文将沿着"总览 → 绑定 → 连接 → 读取 → 写入 → 关注管理 → 链路串联"的顺序,先剖析 I/O 操作内核的三层模型与 handle() 统一分发入口,再依次分析 bind、connect/finishConnect、read、write/flush 四种操作的落地实现,最后收口 doBeginRead 与 doClose 的兴趣管理与资源释放,串联起数据从 Netty 到操作系统的完整闭环。

一、I/O 操作内核总览:三层模型、NioIoOps 兴趣位图与 handle 统一分发

1.1 三层协作模型:Unsafe → JDK Channel → OS

理解 Netty 底层 IO 操作,首先要建立一张"三层协作"的全局认知图。所谓三层协作模型,是指一次 IO 操作从发起到真正生效,要依次穿越 Unsafe 策略层、JDK Channel 层与操作系统 Socket 三层:

最上层是 Unsafe,它是所有危险 IO 操作的收口点,Channel 对外暴露的安全 API 最终都收敛到这里;中间层是 JDK 的 SelectableChannel 及其子类(SocketChannel、ServerSocketChannel),Netty 并不直接操作内核,而是调用这些 JDK 封装的 NIO 通道;最底层才是操作系统 Socket 与内核协议栈,真正完成数据收发。三层之间各司其职,Unsafe 只关心"做什么",JDK Channel 只关心"怎么调用",内核只关心"数据往哪去"。

1.2 NioIoOps 兴趣位图:兴趣事件的位掩码表达

在分析具体操作之前,需要先认识一个贯穿全文的数据结构:NioIoOps。它位于 io.netty.channel.nio 包,是 Netty 4.2 为 NIO transport 引入的兴趣事件位掩码,与 JDK SelectionKey 的兴趣集常量一一对应:

java 复制代码
io.netty.channel.nio.NioIoOps

public final class NioIoOps implements IoOps {
    // 不关注任何 IO 事件
    public static final NioIoOps NONE   = new NioIoOps(0);
    // 关注新连接接入(服务端)
    public static final NioIoOps ACCEPT = new NioIoOps(SelectionKey.OP_ACCEPT);
    // 关注连接建立完成
    public static final NioIoOps CONNECT = new NioIoOps(SelectionKey.OP_CONNECT);
    // 关注通道可写
    public static final NioIoOps WRITE  = new NioIoOps(SelectionKey.OP_WRITE);
    // 关注通道可读
    public static final NioIoOps READ   = new NioIoOps(SelectionKey.OP_READ);
    // 同时关注可读 + 新连接
    public static final NioIoOps READ_AND_ACCEPT = new NioIoOps(SelectionKey.OP_READ | SelectionKey.OP_ACCEPT);
    // 同时关注可读 + 可写
    public static final NioIoOps READ_AND_WRITE  = new NioIoOps(SelectionKey.OP_READ | SelectionKey.OP_WRITE);

    final int value;

    private NioIoOps(int value) {
        this.value = value;
    }

    // 判断当前位图是否已包含给定操作
    public boolean contains(NioIoOps ops) {
        return isIncludedIn(ops.value);
    }

    public boolean isIncludedIn(int ops) {
        return (ops & value) != 0;
    }

    // 返回底层 int 位掩码值
    public int value() {
        return value;
    }

    // 在当前位置图上追加一个操作(位或)
    public NioIoOps with(NioIoOps ops) {
        if (contains(ops)) {
            return this;
        }
        return valueOf(value | ops.value());
    }

    // 从当前位置图上移除一个操作(位清除)
    public NioIoOps without(NioIoOps ops) {
        if (!contains(ops)) {
            return this;
        }
        return valueOf(value & ~ops.value());
    }
}

NioIoOps 的本质是一个携带 int value 的不可变位图,value 直接复用 JDK 的 SelectionKey.OP_ACCEPT/OP_READ/OP_WRITE/OP_CONNECT 常量。它提供了三个关键运算:contains() 判断某个操作是否已存在于位图,with() 用位或追加一个操作,without() 用位清除移除一个操作。这三个运算构成了后续 addAndSubmit/removeAndSubmit 兴趣集变更的原子基础。

每个 Channel 在构造时都会确定自己的读关注类型,保存在 AbstractNioChannel 的 readOps(位图)与 readInterestOp(int)两个字段中:

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

public abstract class AbstractNioChannel extends AbstractChannel {
    private final SelectableChannel ch;
    protected final int readInterestOp;
    protected final NioIoOps readOps;
    volatile IoRegistration registration;
    boolean readPending;

    protected AbstractNioChannel(Channel parent, SelectableChannel ch, NioIoOps readOps) {
        super(parent);
        this.ch = ch;
        // 同时保存 int 形兴趣值(供 SelectionKey 场景使用)与位图形 readOps(供位运算使用)
        this.readInterestOp = ObjectUtil.checkNotNull(readOps, "readOps").value;
        this.readOps = readOps;
        try {
            // 统一设置为非阻塞模式,这是 Reactor 模式的前提
            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);
        }
    }
}

readOps 的取值由子类决定:字节型通道 AbstractNioByteChannel 构造函数传入 SelectionKey.OP_READ(即 READ),服务端 NioServerSocketChannel 传入 SelectionKey.OP_ACCEPT(即 ACCEPT)。这意味着"读关注"在客户端是读数据,在服务端是接新连接,二者共用 readOps 这一抽象。

1.3 handle 统一分发:连接、写入、读取的单一入口

在 IoHandler/IoHandle 抽象下,就绪事件被封装成 NioIoEvent,统一交给 AbstractNioUnsafe.handle() 一个入口分发:

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();
        // 必须先处理 OP_CONNECT,否则 JDK 通道会抛 NotYetConnectedException
        if (nioReadyOps.contains(NioIoOps.CONNECT)) {
            // 移除 OP_CONNECT,否则 Selector.select() 会一直立即返回而无法阻塞
            removeAndSubmit(NioIoOps.CONNECT);
            unsafe().finishConnect();
        }

        // 优先处理 OP_WRITE,尽可能先把积压的缓冲区写出去,从而释放内存
        if (nioReadyOps.contains(NioIoOps.WRITE)) {
            // forceFlush 内部会负责在所有数据写完后清除 OP_WRITE
            forceFlush();
        }

        // 最后处理读就绪;readOps 为 0 也被视为读就绪,用于规避 JDK 可能的空转 bug
        if (nioReadyOps.contains(NioIoOps.READ_AND_ACCEPT) || nioReadyOps.equals(NioIoOps.NONE)) {
            read();
        }
    } catch (CancelledKeyException ignored) {
        // SelectionKey 已被取消,直接关闭通道
        close(voidPromise());
    }
}

handle() 的分发逻辑有严格的顺序考量:先连接、再写入、后读取 。之所以先处理 CONNECT,是因为连接未建立完成前,JDK 通道还不允许读或写,若跳过连接处理直接读写会抛 NotYetConnectedException;之所以在处理 CONNECT 时立即 removeAndSubmit(CONNECT),是因为连接关注一旦建立且未移除,Selector.select() 会一直返回就绪而不阻塞,导致空转。写入优先于读取,则是因为把积压数据尽早发出去可以尽快释放写缓冲区占用的内存。

配合 handle() 的是兴趣事件的增删方法 addAndSubmit/removeAndSubmit,它们完成了从位图变更到内核生效的提交:

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

protected void addAndSubmit(NioIoOps addOps) {
    int interestOps = selectionKey().interestOps();
    // 幂等保护:已包含该操作则无需重复提交
    if (!addOps.isIncludedIn(interestOps)) {
        try {
            // 位或合并新掩码后,交给 IoRegistration 提交到内核
            registration().submit(NioIoOps.valueOf(interestOps).with(addOps));
        } catch (Exception e) {
            throw new ChannelException(e);
        }
    }
}

protected void removeAndSubmit(NioIoOps removeOps) {
    int interestOps = selectionKey().interestOps();
    if (removeOps.isIncludedIn(interestOps)) {
        try {
            // 位清除移出该操作后提交
            registration().submit(NioIoOps.valueOf(interestOps).without(removeOps));
        } catch (Exception e) {
            throw new ChannelException(e);
        }
    }
}

这两个方法先是读取当前 selectionKey().interestOps() 得到现有兴趣集,再通过 NioIoOps.valueOf(interestOps).with(addOps)(位或)或 .without(removeOps)(位清除)计算出新的掩码,最后统一交给 registration().submit(...) 提交。值得注意的细节是:提交前会先做一次"是否已包含/未包含"的幂等判断,避免对内核发起无谓的重复提交,这是对 NIO 与 epoll 两类 transport 都适用的统一优化。这条"计算新掩码 → 提交内核"的链路,也是后文 doConnect、doBeginRead、doWrite 等所有兴趣变更操作的公共底座。

二、端口绑定:bind 的全链路落地

2.1 AbstractUnsafe.bind:入口校验与激活传播

bind 用于把本地端口与底层 Socket 绑定,是服务端"先监听、后接入"的第一步。它的入口同样收口在 AbstractUnsafe 中:

java 复制代码
io.netty.channel.AbstractChannel.AbstractUnsafe#bind

@Override
public final void bind(final SocketAddress localAddress, final ChannelPromise promise) {
    assertEventLoop();
    // 断言在 EventLoop 线程内,且 promise 未被取消、通道仍打开
    if (!promise.setUncancellable() || !ensureOpen(promise)) {
        return;
    }

    // 记录绑定前的激活状态,用于后续判断是否需要触发 channelActive
    boolean wasActive = isActive();
    try {
        doBind(localAddress);
    } catch (Throwable t) {
        // 绑定失败则使 promise 失败,并视情况关闭通道
        safeSetFailure(promise, t);
        closeIfClosed();
        return;
    }

    // 由未激活变为激活时,异步传播 channelActive 事件
    if (!wasActive && isActive()) {
        invokeLater(new Runnable() {
            @Override
            public void run() {
                pipeline.fireChannelActive();
            }
        });
    }

    safeSetSuccess(promise);
}

这段逻辑清晰地体现了 Netty 出站操作的两个共性:一是所有操作都要求运行在 EventLoop 线程内(assertEventLoop()),保证线程安全;二是通过 wasActive/isActive() 的前后对比,判断本次绑定是否让通道由"未激活"变为"激活",若是则异步触发 fireChannelActive() 传播激活事件。之所以用 invokeLater 延迟触发,是为了避免在 bind 调用栈内同步传播事件,给用户 handler 一个干净的调用栈。

bind 真正落地依赖的是 doBind(localAddress),这是定义在 AbstractChannel 上的 abstract 模板方法,由 transport 层子类实现。

2.2 服务端与客户端的 doBind 落地

服务端的 NioServerSocketChannel 与客户端的 NioSocketChannel 各自实现了 doBind,底层调用截然不同:

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

@Override
protected void doBind(SocketAddress localAddress) throws Exception {
    // 服务端绑定并设置 backlog,决定内核全连接队列长度
    javaChannel().bind(localAddress, config.getBacklog());
}
java 复制代码
io.netty.channel.socket.nio.NioSocketChannel#doBind / #doBind0

@Override
protected void doBind(SocketAddress localAddress) throws Exception {
    doBind0(localAddress);
}

private void doBind0(SocketAddress localAddress) throws Exception {
    // 客户端借助 SocketUtils 进行绑定(通常仅在显式指定本地地址时才走此路径)
    SocketUtils.bind(javaChannel(), localAddress);
}

服务端的 doBind 直接调用 JDK ServerSocketChannel.bind(localAddress, backlog),多传了一个 backlog 参数,它来自 ServerSocketChannelConfig,决定内核维护的全连接队列最大长度;客户端则委托 SocketUtils.bind,一般情况下客户端发起连接时内核会自动分配本地端口,只有显式指定了 localAddress 时才需要真正绑定本地地址。

整个 bind 流程可以用下面的时序图概括:

上面的时序图清晰地展示了两层结构:AbstractUnsafe.bind 负责校验、状态对比与事件传播,doBind 负责真正调用 JDK 绑定。绑定完成后,服务端就可以继续注册 ACCEPT 关注去接新连接了。

三、连接建立:connect 与非阻塞握手 finishConnect

3.1 connect 入口:非阻塞提交与 OP_CONNECT 注册

客户端的连接建立是 Netty 中最能体现"非阻塞 + 事件驱动"的一段逻辑。连接的发起入口是 AbstractNioUnsafe.connect:

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

@Override
public final void connect(
        final SocketAddress remoteAddress, final SocketAddress localAddress, final ChannelPromise promise) {
    // 连接 promise 无需标记为不可取消:非阻塞连接本就允许被取消
    if (promise.isDone() || !ensureOpen(promise)) {
        return;
    }

    try {
        if (connectPromise != null) {
            // 同一时刻只允许一个进行中的连接
            throw new ConnectionPendingException();
        }

        boolean wasActive = isActive();
        if (doConnect(remoteAddress, localAddress)) {
            // 立即连接成功(本地回环等场景)
            fulfillConnectPromise(promise, wasActive);
        } else {
            // 连接未就绪,挂起 promise,等待 OP_CONNECT 就绪后由 finishConnect 完成
            connectPromise = promise;
            requestedRemoteAddress = remoteAddress;

            final int connectTimeoutMillis = config().getConnectTimeoutMillis();
            if (connectTimeoutMillis > 0) {
                // 注册连接超时任务,超时未完成则失败 promise 并关闭通道
                connectTimeoutFuture = eventLoop().schedule(new Runnable() {
                    @Override
                    public void run() {
                        ChannelPromise connectPromise = AbstractNioChannel.this.connectPromise;
                        if (connectPromise != null && !connectPromise.isDone()
                                && connectPromise.tryFailure(new ConnectTimeoutException(
                                        "connection timed out after " + connectTimeoutMillis + " ms: " +
                                                remoteAddress))) {
                            close(voidPromise());
                        }
                    }
                }, connectTimeoutMillis, TimeUnit.MILLISECONDS);
            }

            // 若用户取消连接 future,同步取消超时任务并关闭底层 socket
            promise.addListener(new ChannelFutureListener() {
                @Override
                public void operationComplete(ChannelFuture future) {
                    if (future.isCancelled()) {
                        if (connectTimeoutFuture != null) {
                            connectTimeoutFuture.cancel(false);
                        }
                        connectPromise = null;
                        close(voidPromise());
                    }
                }
            });
        }
    } catch (Throwable t) {
        promise.tryFailure(annotateConnectException(t, remoteAddress));
        closeIfClosed();
    }
}

connect 的核心是通过 connectPromise 字段保证"同一时刻只有一个进行中的连接",若已有连接在途,后续连接请求直接抛 ConnectionPendingException。doConnect 的返回结果决定了后续走向:立即成功则 fulfillConnectPromise 直接完成;未就绪则保存 connectPromise、requestedRemoteAddress 并注册超时任务,等待事件驱动后续完成。

真正提交 JDK 连接的是 NioSocketChannel.doConnect:

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 {
        // 非阻塞 connect:JDK 立即返回,未建立连接时返回 false
        boolean connected = SocketUtils.connect(javaChannel(), remoteAddress);
        if (!connected) {
            // 连接未就绪,注册 OP_CONNECT 关注,等就绪后再 finishConnect
            addAndSubmit(NioIoOps.CONNECT);
        }
        success = true;
        return connected;
    } finally {
        if (!success) {
            // 连接提交过程抛异常,关闭底层通道兜底
            doClose();
        }
    }
}

这里的 SocketUtils.connect(javaChannel(), remoteAddress) 是非阻塞调用,它要么立即完成连接返回 true(典型如本地回环地址),要么返回 false 表示连接"已发出但尚未完成"。后一种情况下,doConnect 通过 addAndSubmit(NioIoOps.CONNECT) 注册 OP_CONNECT 关注,告诉 EventLoop"连接完成时叫我"。这正是非阻塞连接的精髓:发起连接只是提交,真正完成要交给事件循环异步推进。

3.2 finishConnect:握手完成与 fireChannelActive

当 TCP 三次握手完成、OP_CONNECT 就绪后,EventLoop 会回到第一节的 handle() 入口,先执行 removeAndSubmit(CONNECT) 移除连接关注,再调用 finishConnect() 完成连接:

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

@Override
public final void finishConnect() {
    // 仅由 EventLoop 在连接未取消、未超时时调用
    assert eventLoop().inEventLoop();

    try {
        boolean wasActive = isActive();
        // 完成 JDK 层的连接
        doFinishConnect();
        fulfillConnectPromise(connectPromise, wasActive);
    } catch (Throwable t) {
        fulfillConnectPromise(connectPromise, annotateConnectException(t, requestedRemoteAddress));
    } finally {
        // 无论成败都要清理连接状态
        if (connectTimeoutFuture != null) {
            connectTimeoutFuture.cancel(false);
        }
        connectPromise = null;
    }
}

finishConnect 记录 wasActive 后调用 doFinishConnect() 完成握手,再走 fulfillConnectPromise 完成 promise 并传播激活事件。doFinishConnect 的落地实现很简单:

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

@Override
protected void doFinishConnect() throws Exception {
    // JDK 层完成连接;返回 false 表示连接未建立或失败
    if (!javaChannel().finishConnect()) {
        throw new UnsupportedOperationException("finishConnect is not supported for " + getClass().getName());
    }
}

而连接完成后激活事件的传播,由 fulfillConnectPromise 统一负责:

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

private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {
    if (promise == null) {
        // 已因取消而关闭,promise 已被通知,无需处理
        return;
    }

    // 先取激活状态:trySuccess 可能触发 listener 关闭通道
    boolean active = isActive();
    boolean promiseSet = promise.trySuccess();

    // 无论连接是否被用户取消,channelActive 事件都应触发(事实已经发生)
    if (!wasActive && active) {
        pipeline().fireChannelActive();
    }

    // 若用户取消了连接尝试,则关闭通道(伴随后续的 channelInactive)
    if (!promiseSet) {
        close(voidPromise());
    }
}

fulfillConnectPromise 有一个巧妙的设计:它先取 promise.trySuccess() 的结果再决定是否 close。这是因为 trySuccess() 可能触发注册在 promise 上的 listener,而 listener 里用户可能已经关闭了通道或取消了连接,所以必须在 trySuccess() 之后重新评估状态,避免激活事件传播错误。无论连接最终是否被用户取消,只要确实由未激活变为激活,fireChannelActive() 都应触发。

连接链路的完整时序如下:

上面的时序图清晰地展示了非阻塞连接的两段式结构:第一段由用户主动发起,doConnect 提交 JDK 连接并注册 OP_CONNECT 关注后立即返回;第二段由事件驱动推进,OP_CONNECT 就绪后 handle 移除关注并 finishConnect 收尾。两段之间靠 connectPromise 字段串起状态。

四、读取内核:read 统一入口与 accept/readBytes 分流

4.1 服务端 NioMessageUnsafe:accept 构建子 Channel

read() 同样由 handle() 统一触发,但具体实现按 Channel 类型分叉:服务端走 NioMessageUnsafe,客户端走 NioByteUnsafe。先看服务端的 NioMessageUnsafe.read:

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

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 {
                // 一次 accept 一个连接,返回 0 表示本轮已无连接可接
                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;
        }

        // 把本轮 accept 到的所有子 Channel 逐个上抛给 Pipeline
        int size = readBuf.size();
        for (int i = 0; i < size; i ++) {
            readPending = false;
            pipeline.fireChannelRead(readBuf.get(i));
        }
        readBuf.clear();
        allocHandle.readComplete();
        pipeline.fireChannelReadComplete();

        // 读取过程抛出异常时,据异常类型判断是否关闭通道并上抛给 Pipeline
        if (exception != null) {
            closed = closeOnReadError(exception);
            pipeline.fireExceptionCaught(exception);
        }

        // 需要关闭(如 accept 出错)时,置 inputShutdown 并关闭通道
        if (closed) {
            inputShutdown = true;
            if (isOpen()) {
                close(voidPromise());
            }
        }
    } finally {
        // 未挂起读且关闭了 autoRead 时,回收读关注
        if (!readPending && !config.isAutoRead()) {
            removeReadOp();
        }
    }
}

服务端读取的核心是一个 do...while 循环:反复调用 doReadMessages(readBuf),把 accept 到的子 Channel 收集进 readBuf,直到 continueReading(allocHandle) 判定应该停止(防止单个服务端 Channel 通过大量 accept 饿死同一 EventLoop 上的其他 Channel)。循环结束后,再统一遍历 readBuf,把每个新接入的 NioSocketChannel 通过 fireChannelRead 交给 Pipeline。

doReadMessages 真正调用了 JDK 的 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) {
            // 为接入的 JDK 通道包装出 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()) 返回的 SocketChannel 是 JDK 层的连接通道,doReadMessages 用 new NioSocketChannel(this, ch) 把它包装成 Netty 的子 Channel,其中 this 是父 Channel(服务端),ch 是接入的 JDK 通道。返回值 1 表示接入了一个连接,0 表示本轮无连接可接。

4.2 客户端 NioByteUnsafe:RecvByteBufAllocator 自适应读取

客户端的读是字节流读取,由 NioByteUnsafe.read 实现,并引入了 RecvByteBufAllocator.Handle 做自适应读:

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

@Override
public final void read() {
    final ChannelConfig config = config();
    if (shouldBreakReadReady(config)) {
        // autoRead 已关闭时不再继续读
        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 {
            // 自适应分配器分配本次读入缓冲
            byteBuf = allocHandle.allocate(allocator);
            // 从 JDK 通道读字节进 ByteBuf,返回读入字节数
            allocHandle.lastBytesRead(doReadBytes(byteBuf));
            if (allocHandle.lastBytesRead() <= 0) {
                // 未读到数据则释放缓冲;小于 0 表示对端已发送 EOF
                byteBuf.release();
                byteBuf = null;
                close = allocHandle.lastBytesRead() < 0;
                if (close) {
                    // 收到 EOF,无剩余可读,置读挂起为 false
                    readPending = false;
                }
                break;
            }

            allocHandle.incMessagesRead(1);
            readPending = false;
            // 每次读满一个 ByteBuf 即上抛一次 channelRead
            pipeline.fireChannelRead(byteBuf);
            byteBuf = null;
        } while (allocHandle.continueReading());

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

        if (close) {
            closeOnRead(pipeline);
        }
    } catch (Throwable t) {
        handleReadException(pipeline, byteBuf, t, close, allocHandle);
    } finally {
        // 未挂起读且关闭了 autoRead 时回收读关注,避免读事件反复触发
        if (!readPending && !config.isAutoRead()) {
            removeReadOp();
        }
    }
}

客户端读取同样是 do...while 循环,但每轮先由 allocHandle.allocate(allocator) 分配一块 ByteBuf,再 doReadBytes(byteBuf) 把字节读进去,读满即 fireChannelRead 上抛一次。所谓自适应读 ,是指 RecvByteBufAllocator.Handle 会根据历史读到的字节数动态调整下一次分配的缓冲区大小,读得多就加大缓冲,读得少就减小缓冲,从而在吞吐与内存占用之间取得平衡。

字节真正从 JDK 通道进入 ByteBuf 的动作在 doReadBytes:

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

@Override
protected int doReadBytes(ByteBuf byteBuf) throws Exception {
    final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
    // 记录本次尝试读取的字节数,供 allocator 自适应调整
    allocHandle.attemptedBytesRead(byteBuf.writableBytes());
    // 直接把 JDK 通道读进 ByteBuf,返回实际读入字节数
    return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
}

byteBuf.writeBytes(javaChannel(), attemptedBytesRead) 一步完成"从 JDK 通道读字节并写入 ByteBuf",它复用 JDK 的 ScatteringByteChannel.read 语义,避免了一次用户态的临时拷贝。读取到 ByteBuf 后,数据便沿着 fireChannelRead 进入用户业务 handler。

读操作的两路分流可以用下面的时序图概括:

上面的时序图清晰地展示了读操作按 Channel 类型分叉的两条路径:服务端把"读"实现在 accept 新连接上,客户端把"读"实现在字节流的接收上,两条路径最终都汇聚到 fireChannelRead/fireChannelReadComplete 这两个 Pipeline 事件上,保持了入站传播语义的一致性。

五、写入内核:flush 与 doWrite 循环写入

5.1 write 与 flush 的两阶段语义

Netty 的写出被有意拆成了 write 与 flush 两个阶段,这是理解写入内核的钥匙。write 只把消息放进写缓冲区,并不真正写 Socket:

java 复制代码
io.netty.channel.AbstractChannel.AbstractUnsafe#write

@Override
public final void write(Object msg, ChannelPromise promise) {
    assertEventLoop();

    ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
    if (outboundBuffer == null) {
        // 通道已关闭(outboundBuffer 被置空),释放消息并失败 promise
        try {
            ReferenceCountUtil.release(msg);
        } finally {
            safeSetFailure(promise,
                    newClosedChannelException(initialCloseCause, "write(Object, ChannelPromise)"));
        }
        return;
    }

    int size;
    try {
        // 过滤与校验消息(如堆内 ByteBuf 转堆外)
        msg = filterOutboundMessage(msg);
        size = pipeline.estimatorHandle().size(msg);
        if (size < 0) {
            size = 0;
        }
    } catch (Throwable t) {
        try {
            ReferenceCountUtil.release(msg);
        } finally {
            safeSetFailure(promise, t);
        }
        return;
    }

    // 仅追加到写缓冲区,不真正写 Socket
    outboundBuffer.addMessage(msg, size, promise);
}

write 做了三件事:先 filterOutboundMessage 过滤消息(字节型通道会把堆内 ByteBuf 转成堆外、校验消息类型),再 estimatorHandle().size(msg) 估算消息大小,最后 outboundBuffer.addMessage(msg, size, promise) 把消息挂到发送队列尾部。真正的写出要到 flush 才发生:

java 复制代码
io.netty.channel.AbstractChannel.AbstractUnsafe#flush / #flush0

@Override
public final void flush() {
    assertEventLoop();

    ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
    if (outboundBuffer == null) {
        return;
    }

    // 把所有未刷新消息标记为已刷新,准备写出
    outboundBuffer.addFlush();
    flush0();
}

protected void flush0() {
    if (inFlush0) {
        // 防重入:避免 flush0 里再触发 flush 造成递归
        return;
    }

    final ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
    if (outboundBuffer == null || outboundBuffer.isEmpty()) {
        return;
    }

    inFlush0 = true;

    // 通道未激活时,先把待发消息标记为失败(如尚未连接就 write)
    if (!isActive()) {
        try {
            if (!outboundBuffer.isEmpty()) {
                if (isOpen()) {
                    // 通道已打开但未连接,用 NotYetConnectedException 失败
                    outboundBuffer.failFlushed(new NotYetConnectedException(), true);
                } else {
                    // 通道已关闭,用 ClosedChannelException 失败(不触发可写性变化事件)
                    outboundBuffer.failFlushed(newClosedChannelException(initialCloseCause, "flush0()"), false);
                }
            }
        } finally {
            inFlush0 = false;
        }
        return;
    }

    try {
        // 真正写 Socket
        doWrite(outboundBuffer);
    } catch (Throwable t) {
        // 写入异常统一交给 handleWriteError 处理
        handleWriteError(t);
    } finally {
        inFlush0 = false;
    }
}

flush() 先 addFlush() 把 write 阶段累积的未刷新消息推进到"已刷新"状态,再 flush0() 触发真正写出。flush0() 里做了一系列防御:inFlush0 防重入、空缓冲区短路、未激活通道直接失败消息。这些判断通过之后,才进入 doWrite(outboundBuffer) 核心写出逻辑。

在此之上,AbstractNioUnsafe 覆写了 flush0,并新增了 forceFlush 与 isFlushPending:

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

@Override
protected final void flush0() {
    // 仅当当前没有挂起的 flush 时才立即写出
    if (!isFlushPending()) {
        super.flush0();
    }
}

@Override
public final void forceFlush() {
    // 无条件立即写出(供 EventLoop 在 OP_WRITE 就绪时调用来续写)
    super.flush0();
}

private boolean isFlushPending() {
    IoRegistration registration = registration();
    return registration.isValid() && NioIoOps.WRITE.isIncludedIn(
            ((SelectionKey) registration.attachment()).interestOps());
}

这是写入续写的关键设计。当上次 doWrite 没写完时,OP_WRITE 已被注册到兴趣集,此时 isFlushPending() 返回 true,用户的 flush() 便只做 addFlush() 而不再立即写,真正的写出延迟到 OP_WRITE 就绪后由 EventLoop 调 handle() → forceFlush() 完成。也就是说,写不出去时把写出权交给事件循环,写得出时立即写 ,两者用 OP_WRITE 的注册状态作为切换开关。

5.2 字节型 Channel 的 gather write 与 OP_WRITE 管理

字节型通道(NioSocketChannel)的 doWrite 实现了 gather write,一次系统调用可以写出多个 ByteBuffer:

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()) {
            // 全部写完,清除 OP_WRITE 关注
            clearOpWrite();
            // 直接返回,不再走 incompleteWrite
            return;
        }

        // 把已刷新的 Entry 转成 JDK ByteBuffer 数组(最多 maxBytesPerGatheringWrite 字节)
        int maxBytesPerGatheringWrite = ((NioSocketChannelConfig) config).getMaxBytesPerGatheringWrite();
        ByteBuffer[] nioBuffers = in.nioBuffers(1024, maxBytesPerGatheringWrite);
        int nioBufferCnt = in.nioBufferCount();

        switch (nioBufferCnt) {
            case 0:
                // 存在非 ByteBuf 消息(如 FileRegion),走单条兜底写出
                writeSpinCount -= doWrite0(in);
                break;
            case 1: {
                // 单缓冲:直接单 buffer 写出
                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: {
                // 多缓冲:gather write 一次写多个 ByteBuffer
                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);

    // 写完了规定次数但仍有剩余,处理 OP_WRITE 关注
    incompleteWrite(writeSpinCount < 0);
}

doWrite 以 writeSpinCount 为"写配额",在循环内反复:in.nioBuffers(...) 把已刷新的消息转成 JDK ByteBuffer[],再按缓冲数量分三路写出。其中单缓冲用 ch.write(buffer),多缓冲用 ch.write(nioBuffers, 0, cnt) 做 gather write,一次系统调用写入多个缓冲区,这正是 Netty 写出高性能的来源。每写出一部分就 in.removeBytes(localWrittenBytes) 更新发送进度。此外,NioSocketChannel 还会在每次写出后调用 adjustMaxBytesPerGatheringWrite(...),根据本次实际写入量与尝试写入量的比例动态放大或缩小 maxBytesPerGatheringWrite,让 gather write 的聚集上限随 OS 发送缓冲区的实际表现自适应调整。

当内核对缓冲区写满、write 返回 <= 0 时,incompleteWrite(true) 会注册 OP_WRITE 关注暂停写出:

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

protected final void incompleteWrite(boolean setOpWrite) {
    if (setOpWrite) {
        // 内核写不动了,注册 OP_WRITE,等可写就绪后再续写
        setOpWrite();
    } else {
        // 写完了本次配额,先清除 OP_WRITE,再安排稍后重试,让出事件循环
        clearOpWrite();
        eventLoop().execute(flushTask);
    }
}

protected final void setOpWrite() {
    final IoRegistration registration = registration();
    if (!registration.isValid()) {
        return;
    }
    addAndSubmit(NioIoOps.WRITE);
}

protected final void clearOpWrite() {
    final IoRegistration registration = registration();
    if (!registration.isValid()) {
        return;
    }
    removeAndSubmit(NioIoOps.WRITE);
}

setOpWrite/clearOpWrite 分别对应 addAndSubmit(NioIoOps.WRITE)/removeAndSubmit(NioIoOps.WRITE),把"可写"兴趣的增删又归结回第一节的公共提交链路。这套"写不动就注册 OP_WRITE、写得完就清除 OP_WRITE"的机制,配合 forceFlush 的续写入口,构成了完整的写流量控制闭环。

5.3 消息型 Channel 的逐条发送

消息型通道(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;
            // 对单条消息最多重试 writeSpinCount 次
            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()) {
        // 全部写完,清除 OP_WRITE 关注
        removeAndSubmit(NioIoOps.WRITE);
    } else {
        // 没写完,注册 OP_WRITE 等待续写
        addAndSubmit(NioIoOps.WRITE);
    }
}

消息型 doWrite 以 maxMessagesPerWrite 为消息数配额(防止一次发太多消息饿死其他 Channel),循环内对每条消息用 doWriteMessage(msg, in) 尝试发送,配合 writeSpinCount 做单条重试。消息全部发完则 removeAndSubmit(WRITE) 清除可写关注,否则 addAndSubmit(WRITE) 注册关注等待续写。这里的 doWriteMessage 是 abstract 方法,由 UDP 等消息型 transport 实现,本文面向 NIO 的 TCP NioSocketChannel 走的是字节型路径,消息型路径的 OP_WRITE 管理逻辑同样收敛回 addAndSubmit/removeAndSubmit。

写操作的整体时序如下:

上面的时序图清晰地展示了写入的完整闭环:write 只入队、flush 才真正写,doWrite 循环里 gather write 批量写出,写不动时用 addAndSubmit(WRITE) 挂起并由 OP_WRITE 就绪后经 forceFlush 续写,写完后 removeAndSubmit(WRITE) 收尾。

六、兴趣事件管理与资源释放:doBeginRead 与 doClose

6.1 doBeginRead 与 readPending:读关注的建立与回收

读操作不是凭空发生的,它需要先建立"读关注",即告诉 EventLoop"这个 Channel 的读事件就绪时叫我"。建立读关注的入口是 doBeginRead:

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

@Override
protected void doBeginRead() throws Exception {
    // Channel.read() 或 ChannelHandlerContext.read() 被调用
    IoRegistration registration = this.registration;
    if (registration == null || !registration.isValid()) {
        return;
    }

    // 置读挂起标记,表示有读需求待处理
    readPending = true;

    // 注册 readOps(客户端 READ,服务端 ACCEPT)关注
    addAndSubmit(readOps);
}

doBeginRead 做了两件事:置 readPending = true 标记"有读需求",并 addAndSubmit(readOps) 把 READ(客户端)或 ACCEPT(服务端)关注注册进兴趣集。readPending 是读流程的关键协调标志,doBeginRead 置 true,read() 读满一轮后置 false。

读关注的回收由 removeReadOp 与 clearReadPending 共同完成:

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

protected final void removeReadOp() {
    IoRegistration registration = registration();
    if (!registration.isValid()) {
        return;
    }
    // 取消读关注,避免读事件反复触发
    removeAndSubmit(readOps);
}

protected final void clearReadPending() {
    if (isRegistered()) {
        EventLoop eventLoop = eventLoop();
        if (eventLoop.inEventLoop()) {
            clearReadPending0();
        } else {
            // 不在 EventLoop 线程则投递任务异步清理
            eventLoop.execute(clearReadPendingRunnable);
        }
    } else {
        // 尚未注册,仅置标志位,SelectionKey 尚不存在
        readPending = false;
    }
}

读关注的建与收,本质上是同一套 addAndSubmit/removeAndSubmit 机制在两个方向上的应用。用户或系统调用 read() 时建立关注,读到数据后 fireChannelRead;当用户关闭了 autoRead 且没有挂起的读需求时,read() 的 finally 块会调 removeReadOp() 回收关注,把读事件"关掉",直到下一次 read() 再次建立关注。

6.2 doClose:JDK Channel 关闭与连接状态清理

写入与读取内核之外,通道资源的释放收口在 doClose。在 Netty 4.2 中,doClose 被拆成多层职责,AbstractNioChannel.doClose 只负责连接状态的清理:

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

@Override
protected void doClose() throws Exception {
    ChannelPromise promise = connectPromise;
    if (promise != null) {
        // 关闭未完成的连接尝试:失败连接 promise
        promise.tryFailure(new ClosedChannelException());
        connectPromise = null;
    }

    Future<?> future = connectTimeoutFuture;
    if (future != null) {
        // 取消连接超时任务
        future.cancel(false);
        connectTimeoutFuture = null;
    }
}

而真正关闭 JDK 通道的动作在子类里:

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

@Override
protected void doClose() throws Exception {
    super.doClose();
    // 关闭底层 JDK SocketChannel
    javaChannel().close();
}
java 复制代码
io.netty.channel.socket.nio.NioServerSocketChannel#doClose

@Override
protected void doClose() throws Exception {
    // 关闭底层 JDK ServerSocketChannel
    javaChannel().close();
}

这里有一个需要澄清的易错点:doClose 只负责"关闭 JDK 通道 + 清理连接状态",它不取消 SelectionKey 。取消注册(IoRegistration.cancel())发生在 doDeregister 中,属于注销流程。二者职责分离:doClose 释放底层 Socket 资源,doDeregister 释放注册关系。把这两件事分开,Netty 才能在"半关闭""异常关闭"等复杂场景下分别精确控制。

至于兴趣集变更如何提交到内核,前文第一节介绍的 addAndSubmit / removeAndSubmit → registration().submit() 链路同样适用,此处不再赘述。

七、整体链路串联

把前面六节串联起来,一条完整的数据通路是这样的:服务端 bind 绑定端口后,doBeginRead 注册 ACCEPT 关注;handle 分发读就绪时走 accept 接入新连接、构建子 NioSocketChannel;客户端 connect 通过非阻塞 doConnect 发起连接,OP_CONNECT 就绪后 finishConnect 完成握手并触发 fireChannelActive;连接建立后双方 doBeginRead 注册 READ 关注,进入读写循环;入站方向由 read 统一分流(服务端接新连接、客户端读字节)后经 fireChannelRead 上抛,出站方向由 write 入队、flush/doWrite 批量写出。

四种操作与兴趣事件的对应关系可以总结如下:

操作 入站/出站 JDK 落地方法 兴趣事件 结果去向
bind 出站 ServerSocketChannel.bind / SocketUtils.bind 无(主动调用) fireChannelActive
connect 出站 SocketUtils.connect + finishConnect CONNECT fireChannelActive
read 入站 accept / channel.read 到 ByteBuf READ(服务端 ACCEPT) fireChannelRead
write/flush 出站 SocketChannel.write(gather write) WRITE ChannelOutboundBuffer → Socket

回顾整条链路,Netty 底层读写内核的设计思想可以凝练为"统一分发 + 关注驱动 + 位图提交":handle() 一个入口把连接、写入、读取三类就绪事件统一分发,NioIoOps 位图与 readOps 字段表达兴趣事件,addAndSubmit/removeAndSubmit 通过 IoRegistration.submit() 完成位图到内核的幂等提交。这套机制把 EventLoop 的调度、兴趣事件的管理、JDK 通道的调用三者干净地剥离开,既保留了对 JDK NIO 的完整控制力,又为 epoll 等更底层的 transport 预留了统一的扩展点。

全文小结

本文聚焦 Netty Channel 的底层 I/O 操作内核,从出站写入与入站读取两个维度,深入分析了 Unsafe → JDK Channel → OS 的三层合作模型、handle() 统一分发机制,以及 bind/connect/read/write 四种操作从发起到底层落地的完整流转路径。

在架构总览层面,AbstractNioUnsafe 同时实现 NioUnsafe 与 NioIoHandle,在 Netty 4.2 下由 EventLoop 通过 handle(IoRegistration, IoEvent) 统一分发连接、写入、读取三类就绪事件,NioIoOps 位图与 JDK SelectionKey 兴趣集一一对应。在兴趣管理层面,addAndSubmit/removeAndSubmit 以 selectionKey().interestOps() 为基准做位运算,仅在结果变化时经 registration().submit() 幂等提交内核。在绑定与连接层面,服务端 doBind 直接 bind(localAddress, backlog),客户端 doConnect 以非阻塞方式提交 JDK connect() 并在未就绪时注册 CONNECT 关注,finishConnect() 完成握手后触发 fireChannelActive()。在读取层面,read() 统一入口按 Channel 类型分流,服务端 doReadMessages 经 accept 构建子 Channel,客户端 doReadBytes 经 byteBuf.writeBytes(javaChannel(), ...) 把字节读入 ByteBuf,二者最终都通过 fireChannelRead 上抛数据。在写入层面,write 只把消息放入 ChannelOutboundBuffer,flush 经 flush0 触发 doWrite 循环,字节型 Channel 走 gather write、消息型 Channel 逐条发送,写不动时通过 OP_WRITE 关注让 EventLoop 稍后经 forceFlush 续写。在资源释放层面,doClose 只处理 JDK Channel 关闭与连接状态清理,与 doDeregister 的 IoRegistration.cancel() 职责分离。


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

相关推荐
DS小龙哥1 小时前
STM32+华为云IoT+Qt:手把手教你打造跨平台智能家居控制系统
后端
量化分析码农1 小时前
【Python量化策略评估体系 #02】5 个因子,谁真贡献 Alpha?单因子归因 + MPA 多因子分解实战
后端
SimonKing1 小时前
Qoder中Qwen3.8-Flash 限时免费使用
java·后端·程序员
骑着蜗牛撵大象3271 小时前
SpringBoot+Vue3 企业智能体侧挂架构:独立服务、独立数据库与主线零侵入落地
数据库·spring boot·架构·vue·springboot·事件驱动·服务拆分
云和数据.ChenGuang1 小时前
langchain4j InMemoryEmbeddingStore常用的方法
java·人工智能·windows·java-ee·fastapi·springai
爱勇宝1 小时前
裁员裁掉了那个干了14年的人:我这才看清职场的5条潜规则
前端·后端·程序员
花间相见1 小时前
【计算基础|网络01】网络模型与一次完整请求全景
后端·http
2501_933923251 小时前
SpringCloud:通过订单服务认识什么是微服务
spring boot·后端·spring cloud
小番茄程序猿1 小时前
RAG 的 precision 只有 0.55,我以为检索烂了,MRR 一测才发现是我用错了尺子
java·后端