客户端发送,服务端接收
本文以 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”。它会根据事件方向和事件类型,用findContextOutbound或findContextInbound找下一个真正支持该事件的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 之后,出站事件继续向前传播,最终到达 HeadContext。HeadContext.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 来说,通常只接受 ByteBuf 和 FileRegion 这类可以写到底层通道的数据。业务对象必须先被 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
writeAndFlush 的 flush = 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 中包含多条完整消息,且没有开启
singleDecode,callDecode(...)会在同一轮循环中尽量多解几条。
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 向前找,所以如果 ObjectEchoServerHandler 在 ObjectDecoder 后面,回写时会先向前跳过不处理出站的 ObjectDecoder,再进入 ObjectEncoder。
7. 结论
ObjectEncoder是出站编码器,负责把对象编码为带长度字段的ByteBuf。ObjectDecoder是入站解码器,负责按长度字段拆帧并把字节反序列化成对象。write不等于发送到网络,它只是把消息追加到ChannelOutboundBuffer。flush会先addFlush(),再通过doWrite(...)尽量写入 JDKSocketChannel。SocketChannel.write(...)写入的是操作系统 socket 发送缓冲区。- 入站事件从
HeadContext向TailContext方向传播;出站事件从当前上下文向HeadContext方向传播。 - Pipeline 查找会按事件类型跳过不支持该事件的处理器,所以不能只按
addLast的链表顺序判断实际调用顺序。 cumulation是ByteToMessageDecoder为解决 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(...)