[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 对象的。

  1. 判断当前 Handler 是否能处理写入的消息,如果能处理,则进入下面的流程,否则,直接传递给下一个节点处理;
  2. 将对象强制转换成 Encoder 可以处理的 Response 对象;
  3. 分配一个 ByteBuf;
  4. 调用 Encoder,即进入到 Encoder 的 encode 方法,该方法是用户代码,用户将数据写入 ByteBuf;
  5. 既然自定义 Java 对象转换成 ByteBuf 了,那么这个对象就已经无用了,释放掉(当传入的 msg 类型是 ByteBuf 的时候,就不需要自己手动释放了);
  6. 如果 buf 中写入了数据,就把 buf 传到下一个节点,否则,释放 buf,将空数据传到下一个节点;
  7. 最后,当 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,下面分别是三个指针的作用:

  1. flushedEntry 指针表示第一个被写到操作系统 Socket 缓冲区中的节点;
  2. unFlushedEntry 指针表示第一个未被写入到操作系统Socket缓冲区中的节点;
  3. 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;
}
  1. 获得第一个需要 flush 的节点的数据
  2. 获取自旋锁的迭代次数
  3. 采用自旋方式将 ByteBuf 写出 JDK NIO 的 Channel
  4. 删除该节点

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. 总结#

  1. Pipeline 中的编码器原理是创建一个 ByteBuf,将 Java 对象转换为 ByteBuf,然后再把 ByteBuf 继续向前传递;
  2. 调用 write 方法并没有将数据写到 Socket 缓冲区中,而是写到了一个单向链表的数据结构中,flush 才是真正的写出;
  3. writeAndFlush 等价于先将数据写到 Netty 的缓冲区,再将 Netty 缓冲区中的数据写到 Socket 缓冲区中,写的过程与并发编程类似,用自旋锁保证写成功;
  4. Netty 缓冲区中的 ByteBuf 为 DirectByteBuf。
posted @   tree6x7  阅读(248)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示
主题色彩