[13] writeAndFlush 解析
在前面的章节中,我们已经详述了事件和异常传播在 Netty 中的实现,其中有一类事件我们在实际编码中用的最后,那就是 write 或者 writeAndFlush。
本章分以下几个部分阐述一个 Java 对象最后是如何转变成字节流,又写到 Socket 缓冲区的。
1. Pipeline 中的标准链表结构#
一个标准的 Pipeline 链式结构如下图所示(我们省去了异常处理 Handler)。
数据从 head 节点流入,先拆包,然后解码成业务对象,最后经过业务 Handler 处理,调用 write,将结果对象写出去。而写的过程先通过 tail 节点,然后通过 Encoder 节点将对象编码成 ByteBuf,最后将该 ByteBuf 对象传递到 head 节点,调用底层的 Unsafe 写到 JDK 底层管道。
2. Java 对象编码过程#
为什么我们在 Pipeline 中添加 Encoder 节点,Java 对象就转换成 Netty 可以处理的 ByteBuf,写到管道里?
我们先看下调用 write 的代码。
BusinessHandler
@Override
protected void channelRead0(ChannelHandlerContext ctx, Request request) throws Exception {
Response response = doBusiness(request);
if (response != null) {
ctx.channel().write(response);
}
}
业务处理器接收请求之后,先做一些业务处理,返回一个 Response;然后 Response 在 Pipeline 中传递,落到 Encoder 节点。下面是 Encoder 节点的处理流程。
Encoder
public class Encoder extends MessageToByteEncoder<Response> {
@Override
protected void encode(ChannelHandlerContext ctx, Response response, ByteBuf out) throws Exception {
out.writeByte(response.getVersion());
out.writeInt(4 + response.getData().length);
out.writeBytes(response.getData());
}
}
Encoder 的处理流程很简单,按照简单的自定义协议,将 Java 对象 Response 写到传入的参数 out 中,这个 out 到底是什么?
为了回答这个问题,我们需要了解 Response 对象,从 BusinessHandler 传入 MessageToByteEncoder 的时候,首先是传入到 write 方法。
MessageToByteEncoder
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ByteBuf buf = null;
try {
// 判断当前 Handler 是否能处理写入的消息
if (acceptOutboundMessage(msg)) {
@SuppressWarnings("unchecked")
// 强制转换
I cast = (I) msg;
// 分配一段 ByteBuf
buf = allocateBuffer(ctx, cast, preferDirect);
try {
// 调用 encode,这里就调回到 Encoder 这个 Handler 中
encode(ctx, cast, buf);
} finally {
// 既然自定义 Java 对象转换成 ByteBuf 了,那么这个对象就已经无用了,释放掉
// (当传入的 msg 类型是 ByteBuf 的时候,就不需要自己手动释放了)
ReferenceCountUtil.release(cast);
}
// 如果 buf 中写入了数据,就把 buf 传到下一个节点
if (buf.isReadable()) {
ctx.write(buf, promise);
} else {
// 否则,释放 buf,将空数据传到下一个节点
buf.release();
ctx.write(Unpooled.EMPTY_BUFFER, promise);
}
buf = null;
} else {
// 如果当前节点不能处理传入的对象,直接传递给下一个节点处理
ctx.write(msg, promise);
}
} catch (EncoderException e) {
throw e;
} catch (Throwable e) {
throw new EncoderException(e);
} finally {
// 当 buf 在 Pipeline 中处理完之后,释放
if (buf != null) {
buf.release();
}
}
}
其实,这一小节的内容,在前面的章节中,已经提到过,这里我们详细阐述一下 Encoder 是如何处理传入的 Java 对象的。
- 判断当前 Handler 是否能处理写入的消息,如果能处理,则进入下面的流程,否则,直接传递给下一个节点处理;
- 将对象强制转换成 Encoder 可以处理的 Response 对象;
- 分配一个 ByteBuf;
- 调用 Encoder,即进入到 Encoder 的 encode 方法,该方法是用户代码,用户将数据写入 ByteBuf;
- 既然自定义 Java 对象转换成 ByteBuf 了,那么这个对象就已经无用了,释放掉(当传入的 msg 类型是 ByteBuf 的时候,就不需要自己手动释放了);
- 如果 buf 中写入了数据,就把 buf 传到下一个节点,否则,释放 buf,将空数据传到下一个节点;
- 最后,当 buf 在 Pipeline 中处理完之后,释放节点.
总结一点就是,Encoder 节点分配一个 ByteBuf,调用 encode 方法,将 Java 对象根据自定义协议写入到 ByteBuf,然后把 ByteBuf 传入到下一个节点,在我们的例子中,最终会传入到 head 节点。
HeadContext
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
unsafe.write(msg, promise);
}
这里的 msg 就是前面在 Encoder 节点中,载有 Java 对象数据的自定义 ByteBuf 对象。
3. write:写队列#
AbstractChannel$AbstractUnsafe
(1)首先,调用 assertEventLoop 确保该方法的调用是在 Reactor 线程中;
@Override
public final void write(Object msg, ChannelPromise promise) {
// => 1
assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) {
safeSetFailure(promise, WRITE_CLOSED_CHANNEL_EXCEPTION);
// release message now to prevent resource-leak
ReferenceCountUtil.release(msg);
return;
}
int size;
try {
// => 2
msg = filterOutboundMessage(msg);
// => 3
size = pipeline.estimatorHandle().size(msg);
if (size < 0) {
size = 0;
}
} catch (Throwable t) {
safeSetFailure(promise, t);
ReferenceCountUtil.release(msg);
return;
}
// => 4
outboundBuffer.addMessage(msg, size, promise);
}
(2)然后,调用 filterOutboundMessage() 方法,将待写入的对象过滤,把 !ByteBuf 对象和 FileRegion 过滤,把所有的非直接内存转换成直接内存 DirectBuffer;
AbstractNioByteChannel
@Override
protected final Object filterOutboundMessage(Object msg) {
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
if (buf.isDirect()) {
return msg;
}
return newDirectBuffer(buf);
}
if (msg instanceof FileRegion) {
return msg;
}
throw new UnsupportedOperationException(
"unsupported message type: " + StringUtil.simpleClassName(msg) + EXPECTED_TYPES);
}
(3)估算出需要写入的 ByteBuf 的 size;
(4)调用 ChannelOutboundBuffer 的 addMessage(msg, size, promise) 方法。
ChannelOutboundBuffer
public void addMessage(Object msg, int size, ChannelPromise promise) {
Entry entry = Entry.newInstance(msg, size, total(msg), promise);
if (tailEntry == null) {
flushedEntry = null;
tailEntry = entry;
} else {
Entry tail = tailEntry;
tail.next = entry;
tailEntry = entry;
}
if (unflushedEntry == null) {
unflushedEntry = entry;
}
// increment pending bytes after adding message to the unflushed arrays.
incrementPendingOutboundBytes(entry.pendingSize, false);
}
想要理解上面这段代码,必须得掌握写缓存中的几个消息指针,如下图所示。
ChannelOutboundBuffer 里面的数据结构是一个单链表结构,每个节点是一个 Entry,Entry 里面包含了待写出 ByteBuf 以及消息回调 promise,下面分别是三个指针的作用:
- flushedEntry 指针表示第一个被写到操作系统 Socket 缓冲区中的节点;
- unFlushedEntry 指针表示第一个未被写入到操作系统Socket缓冲区中的节点;
- tailEntry 指针表示 ChannelOutboundBuffer 缓冲区的最后一个节点。
初次调用 addMessage 之后,各个指针的情况为:
fushedEntry 指向空,unFushedEntry 和 tailEntry 都指向新加入的节点。
第二次调用 addMessage 之后,各个指针的情况为:
第 n 次调用 addMessage 之后,各个指针的情况为:
可以看到,调用 n 次 addMessage,flushedEntry 指针一直指向 NULL,表示现在还未有节点需要写出 Socket 缓冲区,而 unFushedEntry 之后有 n 个节点,表示当前还有 n 个节点尚未写出 Socket 缓冲区。
4. flush:刷新写队列#
不管调用 channel.flush() 还是 ctx.flush(),最终都会落地到 Pipeline 中的 head 节点。
HeadContext
@Override
public void flush(ChannelHandlerContext ctx) throws Exception {
unsafe.flush();
}
之后进入 AbstractUnsafe。
@Override
public final void flush() {
assertEventLoop();
ChannelOutboundBuffer outboundBuffer = this.outboundBuffer;
if (outboundBuffer == null) {
return;
}
outboundBuffer.addFlush();
flush0();
}
在 flush 方法中,先调用 addFlush() 方法。
public void addFlush() {
// There is no need to process all entries if there was already
// a flush before and no new messages where added in the meantime.
Entry entry = unflushedEntry;
if (entry != null) {
if (flushedEntry == null) {
// there is no flushedEntry yet, so start with the entry
flushedEntry = entry;
}
do {
flushed ++;
if (!entry.promise.setUncancellable()) {
// Was cancelled so make sure we free up memory and notify about the freed bytes
int pending = entry.cancel();
decrementPendingOutboundBytes(pending, false, true);
}
entry = entry.next;
} while (entry != null);
// All flushed so reset unflushedEntry
unflushedEntry = null;
}
}
结合前面的图来看,首先拿到 unflushedEntry 指针,然后将 flushedEntry 指向unflushedEntry 所指向的节点,调用完毕之后,三个指针的情况如下所示:
接下来,调用 flush0()。
AbstractUnsafe
protected void flush0() {
// ...
doWrite(outboundBuffer);
// ...
}
这里的核心就只有一个 doWrite,继续跟进。
AbstractNioByteChannel
@Override
protected void doWrite(ChannelOutboundBuffer in) throws Exception {
int writeSpinCount = config().getWriteSpinCount();
do {
// 获得第一个需要 flush 的节点的数据
Object msg = in.current();
if (msg == null) {
// Wrote all messages.
clearOpWrite();
// Directly return here so incompleteWrite(...) is not called.
return;
}
writeSpinCount -= doWriteInternal(in, msg);
} while (writeSpinCount > 0);
incompleteWrite(writeSpinCount < 0);
}
private int doWriteInternal(ChannelOutboundBuffer in, Object msg) throws Exception {
// 强转为 ByteBuf,若发现没有数据可读,直接删除该节点
if (msg instanceof ByteBuf) {
ByteBuf buf = (ByteBuf) msg;
if (!buf.isReadable()) {
in.remove();
return 0;
}
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;
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 {
// Should not reach here.
throw new Error();
}
return WRITE_STATUS_SNDBUF_FULL;
}
- 获得第一个需要 flush 的节点的数据
- 获取自旋锁的迭代次数
- 采用自旋方式将 ByteBuf 写出 JDK NIO 的 Channel
- 删除该节点
5. writeAndFlush:写出队列并刷新#
理解了 write 和 flush 这两个过程,writeAndFlush 也就不难理解了。writeAndFlush 在某个 Handler 中被调用之后,最终会落到 TailContext 节点。
AbstractChannelHandlerContext
@Override
public final ChannelFuture writeAndFlush(Object msg) {
return tail.writeAndFlush(msg);
}
@Override
public ChannelFuture writeAndFlush(Object msg) {
return writeAndFlush(msg, newPromise());
}
@Override
public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
// ...
write(msg, true, promise);
return promise;
}
private void write(Object msg, boolean flush, ChannelPromise promise) {
AbstractChannelHandlerContext next = findContextOutbound();
final Object m = pipeline.touch(msg, next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
if (flush) {
next.invokeWriteAndFlush(m, promise);
} else {
next.invokeWrite(m, promise);
}
} else {
AbstractWriteTask task;
if (flush) {
task = WriteAndFlushTask.newInstance(next, m, promise);
} else {
task = WriteTask.newInstance(next, m, promise);
}
safeExecute(executor, task, promise, m);
}
}
可以看到,最终通过一个 boolean 变量,表示是调用 invokeWriteAndFlush 还是 invokeWrite,invokeWrite 便是我们上文中的 write 过程。
private void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
if (invokeHandler()) {
invokeWrite0(msg, promise);
invokeFlush0();
} else {
writeAndFlush(msg, promise);
}
}
可以看到,最终调用的底层方法和单独调用 write 和 flush 是一样的。
private void invokeWrite0(Object msg, ChannelPromise promise) {
try {
((ChannelOutboundHandler) handler()).write(this, msg, promise);
} catch (Throwable t) {
notifyOutboundHandlerException(t, promise);
}
}
private void invokeFlush0() {
try {
((ChannelOutboundHandler) handler()).flush(this);
} catch (Throwable t) {
notifyHandlerException(t);
}
}
由此看来,invokeWriteAndFlush 基本等价于 write 方法之后再来一次 flush。
6. 总结#
- Pipeline 中的编码器原理是创建一个 ByteBuf,将 Java 对象转换为 ByteBuf,然后再把 ByteBuf 继续向前传递;
- 调用 write 方法并没有将数据写到 Socket 缓冲区中,而是写到了一个单向链表的数据结构中,flush 才是真正的写出;
- writeAndFlush 等价于先将数据写到 Netty 的缓冲区,再将 Netty 缓冲区中的数据写到 Socket 缓冲区中,写的过程与并发编程类似,用自旋锁保证写成功;
- Netty 缓冲区中的 ByteBuf 为 DirectByteBuf。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?