Loading

[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 @ 2022-06-26 21:30  tree6x7  阅读(184)  评论(0编辑  收藏  举报