4.客户端发送,服务端接收

1 阅读13分钟

客户端发送,服务端接收

本文以 Netty 4.2 的 objectecho 示例为入口,梳理一次“客户端写对象,服务端读对象”的完整链路。

示例路径:

example/src/main/java/io/netty/example/objectecho

客户端和服务端的业务 Channel 都会添加相同的编解码器,只是最后的业务处理器不同:

// 服务端 child Channel
p.addLast(
        new ObjectEncoder(),
        new ObjectDecoder(ClassResolvers.cacheDisabled(null)),
        new ObjectEchoServerHandler());

// 客户端 Channel
p.addLast(
        new ObjectEncoder(),
        new ObjectDecoder(ClassResolvers.cacheDisabled(null)),
        new ObjectEchoClientHandler());

需要先明确 Pipeline 的传播方向:

出站事件:Tail -> 业务 Handler -> ObjectEncoder -> Head -> Unsafe -> SocketChannel
入站事件:SocketChannel -> Unsafe -> Head -> ObjectDecoder -> 业务 Handler -> Tail

ObjectDecoder 是入站解码器,处理从网络读到的 ByteBuf;客户端发送对象时,真正参与出站的是 ObjectEncoder,它把 Java 对象编码成 ByteBuf 后继续向 HeadContext 传播。同理,服务端接收时虽然 ObjectEncoder 排在 ObjectDecoder 前面,但 ObjectEncoder 只处理出站事件,不处理 channelRead,所以入站查找会跳过它,直接进入 ObjectDecoder

1. 总体链路

一次客户端发送、服务端接收可以拆成四段:

客户端业务 Handler
  -> ctx.writeAndFlush(object)
  -> ObjectEncoder.write(...)
  -> AbstractUnsafe.write(...) 写入 ChannelOutboundBuffer
  -> AbstractUnsafe.flush(...) / NioSocketChannel.doWrite(...) 写到 JDK SocketChannel

服务端 NIO 读就绪
  -> NioByteUnsafe.read(...)
  -> doReadBytes(ByteBuf) 从 JDK SocketChannel 读到 ByteBuf
  -> pipeline.fireChannelRead(ByteBuf)
  -> ObjectDecoder.channelRead(...)
  -> ObjectEchoServerHandler.channelRead(object)

关键点:

  • write 只是把消息放入 Netty 的出站缓冲区 ChannelOutboundBuffer
  • flush 才会把已 flush 的出站消息尽量写入底层 SocketChannel
  • ObjectEncoder 负责对象序列化和长度字段编码,属于出站处理器。
  • ObjectDecoder 继承自 LengthFieldBasedFrameDecoder,先按长度字段拆帧,再反序列化对象,属于入站处理器。
  • ChannelPipeline 不会简单地“从头遍历所有 Handler”。它会根据事件方向和事件类型,用 findContextOutboundfindContextInbound 找下一个真正支持该事件的 ChannelHandlerContext

2. 客户端发送

客户端一般在业务处理器里调用:

// 客户端业务 Handler 发起出站写
ctx.writeAndFlush(msg);

writeAndFlush 可以理解为一次组合操作:先 write,再 flush。在 Netty 4.2 的实现里,它进入 AbstractChannelHandlerContext.write(msg, true, promise),通过 flush = true 表示本次写完后还要刷新。

public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
    // write + flush 合并执行
    write(msg, true, promise);
    return promise;
}

2.1 查找下一个出站处理器

出站事件从当前 ctx 向前找,也就是沿 prev 方向传播:

void write(Object msg, boolean flush, ChannelPromise promise) {
    if (validateWrite(msg, promise)) {
        // 出站从当前 ctx 向 prev 方向找下一个处理器
        final AbstractChannelHandlerContext next = findContextOutbound(
                flush ? MASK_WRITE | MASK_FLUSH : MASK_WRITE);
        final Object m = pipeline.touch(msg, next);
        EventExecutor executor = next.executor();

        if (executor.inEventLoop()) {
            if (next.invokeHandler()) {
                // 先触发 write
                ...
                if (flush) {
                    // write 后再触发 flush
                    ...
                }
            } else {
                next.write(msg, flush, promise);
            }
        } else {
            // 不在目标 EventLoop,提交任务切线程
            ...
        }
    }
}

findContextOutbound 的方向是 ctx.prev

private AbstractChannelHandlerContext findContextOutbound(int mask) {
    AbstractChannelHandlerContext ctx = this;
    EventExecutor currentExecutor = executor();
    do {
        // 出站方向是 prev
        ctx = ctx.prev;
    } while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_OUTBOUND));
    return ctx;
}

因此,当 ObjectEchoClientHandler 调用 ctx.writeAndFlush(msg) 时,下一个出站处理器是它前面的 ObjectEncoder,不是 ObjectDecoder

2.2 ObjectEncoder 编码对象

ObjectEncoder 本质是一个出站编码器。它接收到 Java 对象后,将对象序列化为 ByteBuf,再调用 ctx.write(buf, promise) 继续向前传播。

从父类 MessageToByteEncoder 的流程看,核心步骤是:

public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    ByteBuf buf = null;
    try {
        // 当前编码器能处理该消息才编码
        if (acceptOutboundMessage(msg)) {
            I cast = (I) msg;
            buf = allocateBuffer(ctx, cast, preferDirect);
            try {
                // 对象 -> ByteBuf
                encode(ctx, cast, buf);
            } finally {
                ReferenceCountUtil.release(cast);
            }

            if (buf.isReadable()) {
                // 编码后的 ByteBuf 继续向前传播
                ctx.write(buf, promise);
            } else {
                buf.release();
                ctx.write(Unpooled.EMPTY_BUFFER, promise);
            }
            buf = null;
        } else {
            ctx.write(msg, promise);
        }
    } finally {
        // 异常路径释放临时 ByteBuf
        if (buf != null) {
            buf.release();
        }
    }
}

这里有几个注意点:

  • acceptOutboundMessage(msg) 判断当前编码器是否能处理该消息。
  • encode(ctx, cast, buf) 才是真正的编码逻辑。
  • 编码后的 ByteBuf 会继续调用 ctx.write(...),因此事件会继续沿出站方向传播到 HeadContext
  • 原始对象在编码完成后会被 ReferenceCountUtil.release(cast) 释放;如果对象不是引用计数对象,这个调用通常没有实际影响。

ObjectEncoder 的编码结果不是裸 Java 序列化字节流,而是带长度字段的帧。这个长度字段是为了让对端 ObjectDecoder 能按帧读取完整对象,避免 TCP 粘包、半包影响对象反序列化。

2.3 HeadContext 写入出站缓冲区

ObjectEncoder 之后,出站事件继续向前传播,最终到达 HeadContextHeadContext.write(...) 会委托给 Unsafe.write(...)

AbstractUnsafe.write(...) 的重点不是写 socket,而是把消息加入 ChannelOutboundBuffer

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

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

    int size;
    try {
        // 底层 Channel 只接受可写类型,例如 ByteBuf
        msg = filterOutboundMessage(msg);
        size = pipeline.estimatorHandle().size(msg);
        if (size < 0) {
            size = 0;
        }
    } catch (Throwable t) {
        // 过滤失败要释放消息
        ReferenceCountUtil.release(msg);
        safeSetFailure(promise, t);
        return;
    }

    // 这里只进入出站缓冲区,还没有写 socket
    outboundBuffer.addMessage(msg, size, promise);
}

filterOutboundMessage(msg) 会做底层传输需要的消息检查和转换。对 NioSocketChannel 来说,通常只接受 ByteBufFileRegion 这类可以写到底层通道的数据。业务对象必须先被 ObjectEncoder 编成 ByteBuf,否则走到这里会失败。

2.4 ChannelOutboundBuffer 暂存消息

ChannelOutboundBuffer.addMessage(...) 会把消息包装成 Entry,追加到未 flush 队列:

public void addMessage(Object msg, int size, ChannelPromise promise) {
    // 每条待写消息包装成 Entry
    Entry entry = Entry.newInstance(msg, size, total(msg), promise);

    // 追加到链表尾部
    if (tailEntry == null) {
        flushedEntry = null;
    } else {
        Entry tail = tailEntry;
        tail.next = entry;
    }
    tailEntry = entry;

    if (unflushedEntry == null) {
        unflushedEntry = entry;
    }

    ReferenceCountUtil.touch(msg);
    // 增加待写字节数,可能触发不可写状态
    incrementPendingOutboundBytes(entry.pendingSize, false);
}

此时消息还没有写入网络,只是进入了 Netty 的出站缓冲区。

ChannelOutboundBuffer 里几个指针的含义:

  • unflushedEntry:已经 write 但还没有 flush 的第一条消息。
  • flushedEntry:已经被 flush 标记、等待真正写 socket 的第一条消息。
  • tailEntry:链表尾部。

incrementPendingOutboundBytes(...) 会增加待写字节数,并在超过高水位线时把 Channel 标记为不可写,从而触发背压相关的 channelWritabilityChanged

3. flush 发送到 SocketChannel

writeAndFlushflush = true 会在 write 之后继续触发 flush 传播。最终仍然到达 HeadContext.flush(...),再委托给 Unsafe.flush()

3.1 addFlush:把未 flush 消息变成待写消息

AbstractUnsafe.flush() 的第一步是:

public final void flush() {
    assertEventLoop();

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

    // unflushed -> flushed,提交为可写
    outboundBuffer.addFlush();
    // 尝试真正写 socket
    flush0();
}

这里的 addFlush() 很关键。它会把 unflushedEntry 开始的消息移动到 flushed 区域,让后续 doWrite(...) 能看到这些消息。

所以更准确的表述是:

write:进入 ChannelOutboundBuffer 的 unflushed 区域 flush:调用 addFlush,把 unflushed 消息变成 flushed 消息,然后尝试写到底层 SocketChannel

3.2 flush0:处理状态并调用 doWrite

flush0() 负责做重入保护、Channel 状态检查,然后调用具体子类的 doWrite(...)

protected void flush0() {
    // 防止 flush 重入
    if (inFlush0) {
        return;
    }

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

    inFlush0 = true;

    if (!isActive()) {
        // Channel 不活跃,失败通知 flushed 消息
        try {
            if (!outboundBuffer.isEmpty()) {
                if (isOpen()) {
                    outboundBuffer.failFlushed(new NotYetConnectedException(), true);
                } else {
                    outboundBuffer.failFlushed(newClosedChannelException(
                            initialCloseCause, "flush0()"), false);
                }
            }
        } finally {
            inFlush0 = false;
        }
        return;
    }

    try {
        // 子类执行真正的底层写
        doWrite(outboundBuffer);
    } catch (Throwable t) {
        handleWriteError(t);
    } finally {
        inFlush0 = false;
    }
}

3.3 NioSocketChannel.doWrite:真正写 JDK SocketChannel

NioSocketChannel.doWrite(...) 会从 ChannelOutboundBuffer 取出待写数据,并调用 JDK 的 SocketChannel.write(...)

核心流程:

protected void doWrite(ChannelOutboundBuffer in) throws Exception {
    SocketChannel ch = javaChannel();
    int writeSpinCount = config().getWriteSpinCount();

    do {
        if (in.isEmpty()) {
            // 没有剩余数据,不再监听 OP_WRITE
            clearOpWrite();
            return;
        }

        // 将 flushed 区域转成 NIO ByteBuffer
        int maxBytesPerGatheringWrite =
                ((NioSocketChannelConfig) config).getMaxBytesPerGatheringWrite();
        ByteBuffer[] nioBuffers = in.nioBuffers(1024, maxBytesPerGatheringWrite);
        int nioBufferCnt = in.nioBufferCount();

        switch (nioBufferCnt) {
            case 0:
                writeSpinCount -= doWrite0(in);
                break;
            case 1:
                ByteBuffer buffer = nioBuffers[0];
                int attemptedBytes = buffer.remaining();
                int writtenBytes = ch.write(buffer);
                if (writtenBytes <= 0) {
                    // socket 暂时不可写,等待 OP_WRITE
                    incompleteWrite(true);
                    return;
                }
                adjustMaxBytesPerGatheringWrite(
                        attemptedBytes, writtenBytes, maxBytesPerGatheringWrite);
                in.removeBytes(writtenBytes);
                --writeSpinCount;
                break;
            default:
                long attempted = in.nioBufferSize();
                // 聚集写,多 ByteBuffer 一次写入
                long written = ch.write(nioBuffers, 0, nioBufferCnt);
                if (written <= 0) {
                    incompleteWrite(true);
                    return;
                }
                adjustMaxBytesPerGatheringWrite(
                        (int) attempted, (int) written, maxBytesPerGatheringWrite);
                in.removeBytes(written);
                --writeSpinCount;
                break;
        }
    } while (writeSpinCount > 0);

    // 自旋次数用完仍没写完,按需注册 OP_WRITE
    incompleteWrite(writeSpinCount < 0);
}

Netty 调用 JDK SocketChannel.write(...),把用户态中的 ByteBuffer 数据尽量写入操作系统的 socket 发送缓冲区。后续何时真正经过网卡发出,由操作系统 TCP/IP 协议栈决定。

如果一次没有写完:

  • SocketChannel.write(...) 返回 0 或者写入不足,说明底层发送缓冲区暂时不可写满。
  • Netty 会调用 incompleteWrite(true) 注册或保留 OP_WRITE
  • 等下次写就绪事件到来后,EventLoop 再继续 flush 剩余数据。

4. 服务端接收

服务端接收从 NIO 读就绪事件开始。Netty 4.2 中,AbstractNioChannel.AbstractNioUnsafe.handle(...) 会根据 ready ops 判断是否需要读:

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

        // 连接完成、写事件等处理省略
        ...

        // 读就绪时开始读取
        if (nioReadyOps.contains(NioIoOps.READ_AND_ACCEPT)
                || nioReadyOps.equals(NioIoOps.NONE)) {
            read();
        }
    } catch (CancelledKeyException ignored) {
        close(voidPromise());
    }
}

NioSocketChannel 来说,真正读数据的是 NioByteUnsafe.read()

4.1 从 SocketChannel 读到 ByteBuf

read() 的核心逻辑是循环分配 ByteBuf,再调用 doReadBytes(byteBuf) 从 JDK SocketChannel 读取数据:

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 {
            // 分配接收缓冲区
            byteBuf = allocHandle.allocate(allocator);
            // 从 SocketChannel 读入 ByteBuf
            allocHandle.lastBytesRead(doReadBytes(byteBuf));

            if (allocHandle.lastBytesRead() <= 0) {
                // 没读到数据,释放本次分配的 ByteBuf
                byteBuf.release();
                byteBuf = null;
                close = allocHandle.lastBytesRead() < 0;
                if (close) {
                    readPending = false;
                }
                break;
            }

            allocHandle.incMessagesRead(1);
            readPending = false;
            // ByteBuf 交给 Pipeline,后续由入站 Handler 处理
            pipeline.fireChannelRead(byteBuf);
            byteBuf = null;
        } while (allocHandle.continueReading());

        allocHandle.readComplete();
        // 触发 readComplete 事件
        pipeline.fireChannelReadComplete();

        if (close) {
            closeOnRead(pipeline);
        }
    } catch (Throwable t) {
        handleReadException(pipeline, byteBuf, t, close, allocHandle);
    } finally {
        if (!readPending && !config.isAutoRead()) {
            removeReadOp();
        }
    }
}

doReadBytes(byteBuf)NioSocketChannel 中大致是:

protected int doReadBytes(ByteBuf byteBuf) throws Exception {
    final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle();
    allocHandle.attemptedBytesRead(byteBuf.writableBytes());
    // 底层 SocketChannel -> Netty ByteBuf
    return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
}

这里的语义是:从底层 SocketChannel 读数据,写入 Netty 分配的 ByteBuf。读到数据后,ByteBuf 的所有权交给 Pipeline,所以后面把局部变量置为 null,避免异常路径重复释放。

4.2 触发入站 channelRead

读到 ByteBuf 后,Netty 调用:

// 读取到 ByteBuf 后,触发入站传播
pipeline.fireChannelRead(byteBuf);

入站事件从 HeadContext 开始向后找,也就是沿 next 方向传播:

public ChannelHandlerContext fireChannelRead(final Object msg) {
    // 入站从当前 ctx 向 next 方向找下一个处理器
    AbstractChannelHandlerContext next = findContextInbound(MASK_CHANNEL_READ);
    if (next.executor().inEventLoop()) {
        final Object m = pipeline.touch(msg, next);
        if (next.invokeHandler()) {
            try {
                final ChannelHandler handler = next.handler();
                // 调用下一个入站 Handler 的 channelRead
                if (handler instanceof ChannelDuplexHandler) {
                    ((ChannelDuplexHandler) handler).channelRead(next, m);
                } else {
                    ((ChannelInboundHandler) handler).channelRead(next, m);
                }
            } catch (Throwable t) {
                next.invokeExceptionCaught(t);
            }
        } else {
            next.fireChannelRead(m);
        }
    } else {
        // 不在目标 EventLoop,提交任务切线程
        next.executor().execute(() -> fireChannelRead(msg));
    }
    return this;
}

findContextInbound 的方向是 ctx.next

private AbstractChannelHandlerContext findContextInbound(int mask) {
    AbstractChannelHandlerContext ctx = this;
    EventExecutor currentExecutor = executor();
    do {
        // 入站方向是 next
        ctx = ctx.next;
    } while (skipContext(ctx, currentExecutor, mask, MASK_ONLY_INBOUND));
    return ctx;
}

因此服务端 channelRead 的有效处理顺序是:

HeadContext -> ObjectDecoder -> ObjectEchoServerHandler

ObjectEncoder 虽然在链表位置上靠前,但它不处理入站 channelRead,会被查找逻辑跳过。

5. ObjectDecoder 解码

ObjectDecoder 是入站解码器,继承自 LengthFieldBasedFrameDecoder。它先基于长度字段拆出完整帧,再反序列化成 Java 对象。

父类 ByteToMessageDecoder.channelRead(...) 的核心职责是维护半包累积缓冲区 cumulation,然后循环调用子类的 decode(...)

cumulation 可以理解为 ByteToMessageDecoder 内部保存的“跨多次 read 的 ByteBuf”。TCP 是字节流,一次底层 SocketChannel.read(...) 不保证刚好读到一条完整业务消息,所以解码器必须把暂时不够解码的字节保存下来,等下一次读到新字节后再一起尝试解码。

例如客户端发送一条对象消息,编码后的结构可以简化成:

[length=100][100 字节对象内容]

服务端可能分三次读到:

第 1 次 read: [length=100][前 30 字节]
第 2 次 read: [中间 50 字节]
第 3 次 read: [最后 20 字节]

第一次和第二次都不够解出完整对象,这些字节就会留在 cumulation 中。第三次新数据进来后,cumulator.cumulate(...) 会把旧的 cumulation 和新的 input 合并,然后 ObjectDecoder 才能基于长度字段拿到完整帧并反序列化对象。

反过来,如果一次读到了:

[msg1 完整][msg2 前半段]

callDecode(...) 会先解出 msg1,而 msg2 的前半段仍然留在 cumulation,等待下次读补齐。

public void channelRead(ChannelHandlerContext ctx, Object input) throws Exception {
    // 正常状态直接解码;重入时先入队
    if (decodeState == STATE_INIT) {
        do {
            // 只处理 ByteBuf,其他消息直接传下去
            if (input instanceof ByteBuf) {
                // 保存本轮解码产物
                CodecOutputList out = CodecOutputList.newInstance();
                try {
                    // 是否没有历史半包
                    first = cumulation == null;

                    // 合并旧 cumulation 和本次 input
                    cumulation = cumulator.cumulate(
                            ctx.alloc(), first ? EMPTY_BUFFER : cumulation, (ByteBuf) input);

                    // 从累积缓冲区尝试解码
                    callDecode(ctx, cumulation, out);
                } finally {
                    // 全部消费完就释放;没消费完说明还有半包
                    if (cumulation != null && !cumulation.isReadable()) {
                        numReads = 0;
                        cumulation.release();
                        cumulation = null;
                    } else if (++numReads >= discardAfterReads) {
                        // 定期丢弃已读字节,避免 cumulation 膨胀
                        numReads = 0;
                        discardSomeReadBytes();
                    }

                    // 解出的对象继续向后传播
                    int size = out.size();
                    firedChannelRead |= out.insertSinceRecycled();
                    fireChannelRead(ctx, out, size);

                    // 回收列表容器
                    out.recycle();
                }
            } else {
                ctx.fireChannelRead(input);
            }
        } while (inputMessages != null && (input = inputMessages.poll()) != null);
    } else {
        // 重入消息延后处理
        if (inputMessages == null) {
            inputMessages = new ArrayDeque<>(2);
        }
        inputMessages.offer(input);
    }
}

这段逻辑的重点:

  • 如果收到的是 ByteBuf,先合并到 cumulation,解决 TCP 半包和粘包问题。
  • cumulation 不是每次 read 都重新创建的临时变量,而是 decoder 对象上的成员状态,用来保存上一次没解完的字节。
  • callDecode(...) 负责循环调用具体解码器,并通过 readerIndex 的变化判断本轮是否消费了输入。
  • 解码出的对象先放入 out,最后通过 fireChannelRead(ctx, out, size) 继续向后传播。
  • 如果 cumulation 被全部消费,会立即 release() 并置空;如果还有残留,说明仍有半包,需要留到下一次 read。
  • 如果收到的不是 ByteBuf,当前解码器无法处理,会直接 ctx.fireChannelRead(input) 交给后续处理器。

5.1 callDecode 的退出条件

callDecode(...) 会循环调用子类 decode(...),但不是无限循环。它依赖几个条件退出:

protected void callDecode(ChannelHandlerContext ctx, ByteBuf in, List<Object> out) {
    // in 就是 cumulation
    while (in.isReadable()) {
        // 先传播上一轮已经解出的消息
        final int outSize = out.size();
        if (outSize > 0) {
            fireChannelRead(ctx, out, outSize);
            out.clear();

            // handler 被移除就停止
            if (ctx.isRemoved()) {
                break;
            }
        }

        // 用于判断 decode 是否消费了字节
        int oldInputLength = in.readableBytes();

        // 真正调用子类 decode(...)
        decodeRemovalReentryProtection(ctx, in, out);

        if (ctx.isRemoved()) {
            break;
        }

        if (out.isEmpty()) {
            if (oldInputLength == in.readableBytes()) {
                // 半包:数据不够,等待下次 read
                break;
            } else {
                // 消费了部分字节,继续尝试
                continue;
            }
        }

        // 防止 decode 产出消息但不推进 readerIndex
        if (oldInputLength == in.readableBytes()) {
            throw new DecoderException(
                    StringUtil.simpleClassName(getClass())
                            + ".decode() did not read anything but decoded a message.");
        }

        // 单次解码模式
        if (isSingleDecode()) {
            break;
        }
    }
}

关键约束:

  • 如果没有产出对象,也没有消耗输入字节,说明数据还不够,退出等待下次读。
  • 如果产出了对象但没有消耗任何输入字节,这是解码器 bug,Netty 会抛出异常,避免死循环。
  • 如果开启 singleDecode,每次只解一个消息。
  • 如果一次 read 中包含多条完整消息,且没有开启 singleDecodecallDecode(...) 会在同一轮循环中尽量多解几条。

5.2 ObjectDecoder 的真实职责

ObjectDecoder 的调用链可以概括为:

ByteToMessageDecoder.channelRead(...)
  -> callDecode(...)
  -> decodeRemovalReentryProtection(...)
  -> LengthFieldBasedFrameDecoder.decode(...)
  -> ObjectDecoder.decode(...)
  -> ctx.fireChannelRead(decodedObject)

注意这里的顺序含义:

  • LengthFieldBasedFrameDecoder 先解决“这次有没有完整一帧”的问题。
  • ObjectDecoder 再解决“如何把这一帧字节变成 Java 对象”的问题。
  • 只有成功解出对象后,才会继续传给 ObjectEchoServerHandler.channelRead(...)

因此服务端业务处理器收到的不是原始 ByteBuf,而是已经反序列化后的 Java 对象。

6. 服务端业务处理器

经过 ObjectDecoder 后,入站事件继续向后传播到:

// 服务端业务 Handler 收到解码后的 Java 对象
ObjectEchoServerHandler.channelRead(ChannelHandlerContext ctx, Object msg)

到这里,客户端发送对象、服务端接收对象的链路闭合。

如果服务端业务处理器再次调用 ctx.write(msg)ctx.writeAndFlush(msg) 回写对象,那么方向又会切换成出站:

ObjectEchoServerHandler -> ObjectDecoder(跳过出站或不处理) -> ObjectEncoder -> HeadContext -> SocketChannel

这里也要注意:出站从当前 ctx 向前找,所以如果 ObjectEchoServerHandlerObjectDecoder 后面,回写时会先向前跳过不处理出站的 ObjectDecoder,再进入 ObjectEncoder

7. 结论

  1. ObjectEncoder 是出站编码器,负责把对象编码为带长度字段的 ByteBuf
  2. ObjectDecoder 是入站解码器,负责按长度字段拆帧并把字节反序列化成对象。
  3. write 不等于发送到网络,它只是把消息追加到 ChannelOutboundBuffer
  4. flush 会先 addFlush(),再通过 doWrite(...) 尽量写入 JDK SocketChannel
  5. SocketChannel.write(...) 写入的是操作系统 socket 发送缓冲区。
  6. 入站事件从 HeadContextTailContext 方向传播;出站事件从当前上下文向 HeadContext 方向传播。
  7. Pipeline 查找会按事件类型跳过不支持该事件的处理器,所以不能只按 addLast 的链表顺序判断实际调用顺序。
  8. cumulationByteToMessageDecoder 为解决 TCP 半包和粘包维护的跨 read 缓冲区;解完就释放,没解完就保留到下一次 read。

8. 源码定位

建议阅读时按这个顺序定位:

ObjectEchoClientHandler / ObjectEchoServerHandler
ObjectEncoder
MessageToByteEncoder.write(...)
AbstractChannelHandlerContext.write(...)
AbstractChannelHandlerContext.findContextOutbound(...)
DefaultChannelPipeline.HeadContext.write(...)
AbstractChannel.AbstractUnsafe.write(...)
ChannelOutboundBuffer.addMessage(...)
AbstractChannel.AbstractUnsafe.flush(...)
ChannelOutboundBuffer.addFlush(...)
AbstractChannel.AbstractUnsafe.flush0(...)
NioSocketChannel.doWrite(...)

AbstractNioChannel.AbstractNioUnsafe.handle(...)
AbstractNioByteChannel.NioByteUnsafe.read(...)
NioSocketChannel.doReadBytes(...)
DefaultChannelPipeline.fireChannelRead(...)
AbstractChannelHandlerContext.findContextInbound(...)
ByteToMessageDecoder.channelRead(...)
ByteToMessageDecoder.callDecode(...)
LengthFieldBasedFrameDecoder.decode(...)
ObjectDecoder.decode(...)
ObjectEchoServerHandler.channelRead(...)