上一节,我们一起学习了 Netty 接收新数据过程的源码剖析,我们又发现了一个有趣的现象,Netty 的 ByteBuf 竟然也是对 Java 原生 ByteBuffer 的包装。

经过前面的学习,我想你一定迫不及待地想知道 Netty 中写出数据的过程了吧,也有可能,你自己已经根据我们前面的调试方法自己看了,也有可能,睿智的你已经猜测 Netty 中写出数据,肯定也是调用 Java 原生 SocketChannel 的写出数据。

回顾过去。Java 原生 NIO 写出数据是从 ByeBuffer 中写出的:

private static void send(SocketChannel socketChannel, String msg) {
    try {
        ByteBuffer writeBuffer = ByteBuffer.allocate(1024);
        writeBuffer.put(msg.getBytes());
        writeBuffer.flip();
        // 调用SocketChannel的写出方法
        socketChannel.write(writeBuffer);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

那么,今天的问题是:

1 Netty 底层是不是调用的 SocketChannel 的 write () 方法呢?
2 写出数据在 ChannelPipeline 中的传递顺序是怎样的?
3 写出为什么还有一个叫做 flush 的过程?之前写出的数据又在哪里呢?

public class EchoServerHandler extends ChannelInboundHandlerAdapter {
    // 省略其它代码
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) {
        // 读取数据后写回客户端
        // key1,断点在此处
        ctx.write(msg);
    }
    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        // key2,这里也需要个断点
        ctx.flush();
    }
}

不过,一般来说,服务端发送数据不会在每次 write () 的时候就发送出去,而是先缓存起来,等到一定量之后或者显式地说明要发送的时候再真正地发送出去,这样能在一定程度上提高效率。
就像快递公司一般都会在各个地方设置网点一样,快递小哥并不是收取完快递就给你发出去了,而是,先集成存放在网点,等达到一定量之后,或者到晚上八九点钟之后,再装车发送出去。快递小哥往网点投放快递就类似于write (msg) 的过程,而装车拉走就类似于 flush () 的过程。

TODO 有一个很有意思的类比。类比发快递。

// 1. io.netty.channel.AbstractChannelHandlerContext#write
@Override
public ChannelFuture write(Object msg) {
    return write(msg, newPromise());
}
// 2. io.netty.channel.AbstractChannelHandlerContext#write
@Override
public ChannelFuture write(final Object msg, final ChannelPromise promise) {
    // 第二个参数为flush,这里传入的是false
    // 也就是默认不进行真正地发送
    write(msg, false, promise);

    return promise;
}
// 3. io.netty.channel.AbstractChannelHandlerContext#write
private void write(Object msg, boolean flush, ChannelPromise promise) {
    // 检查参数,可以跳过
    ObjectUtil.checkNotNull(msg, "msg");
    try {
        if (isNotValidPromise(promise, true)) {
            ReferenceCountUtil.release(msg);
            // cancelled
            return;
        }
    } catch (RuntimeException e) {
        ReferenceCountUtil.release(msg);
        throw e;
    }
    // key1,寻找下一个可用的outbound类型的ChannelHandlerContext
    // 在ChannelPipeline中也就是prev指针标记的那个
    // 这里找到的就是LoggingHandler对应的那个ChannelHandlerContext
    final AbstractChannelHandlerContext next = findContextOutbound(flush ?
                                                                   (MASK_WRITE | MASK_FLUSH) : MASK_WRITE);
    // touch()可以先不管,可以把这里返回的对象看作与msg是一个对象
    final Object m = pipeline.touch(msg, next);
    EventExecutor executor = next.executor();
    if (executor.inEventLoop()) {
        if (flush) {
            next.invokeWriteAndFlush(m, promise);
        } else {
            // key2,flush为false,所以走到了这里
            // 调用context的invokeWrite()方法
            next.invokeWrite(m, promise);
        }
    } else {
        final WriteTask task = WriteTask.newInstance(next, m, promise, flush);
        if (!safeExecute(executor, task, promise, m, !flush)) {
            task.cancel();
        }
    }
}
// 4. io.netty.channel.AbstractChannelHandlerContext#invokeWrite
void invokeWrite(Object msg, ChannelPromise promise) {
    if (invokeHandler()) {
        invokeWrite0(msg, promise);
    } else {
        write(msg, promise);
    }
}
// 5. io.netty.channel.AbstractChannelHandlerContext#invokeWrite0
private void invokeWrite0(Object msg, ChannelPromise promise) {
    try {
        // 调用Handler的write()方法
        ((ChannelOutboundHandler) handler()).write(this, msg, promise);
    } catch (Throwable t) {
        notifyOutboundHandlerException(t, promise);
    }
}
// 6. io.netty.handler.logging.LoggingHandler#write
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
    if (logger.isEnabled(internalLevel)) {
        logger.log(internalLevel, format(ctx, "WRITE", msg));
    }
    // 又调回了第2步中的方法,继续寻找下一个可用的outbound类型的ChannelHandlerContext
    ctx.write(msg, promise);
}

netty rpc 通信怎么保证数据的一一对应 netty 发送数据_.net


为了便于理解,我特意把箭头区分成两种颜色,并标上数字:

红色表示接收数据的过程,蓝色表示写出数据的过程;

1 表示调用 ctx.fireChannelRead (msg) 方法,触发下一个 ChannelHandlerContext 的调用;

2 表示调用 next.invokeChannelRead (m) 方法,调用到下一个 ChannelHandlerContext ;

3 表示调用 ((ChannelInboundHandler) handler ()).channelRead (this, msg) 方法,调用 ChannelHandler 的channelRead () 方法;

4 表示调用 ctx.write (msg) 或者 ctx.write (msg, promise) 方法,触发下一个 ChannelHandlerContext 的调用;

5 表示调用 next.invokeWrite (m, promise) 方法,调用到下一个 ChannelHandlerContext ;

6 表示调用 ((ChannelOutboundHandler) handler ()).write (this, msg, promise) 方法,调用 ChannelHandler 的write () 方法;

所以,
对于接收数据,如果需要数据在 ChannelPipeline 中传递,就调用 ctx.fireChannelRead(msg) 方法;
对于写出数据,如果需要数据在 ChannelPipeline 中传递,就调用 ctx.write(msg) 或者 ctx.write(msg, promise) 方法;

至此,写出数据过程的源码剖析就讲完了,让我们再来总结一下:

  1. 调用 ctx.write () 方法时,只是把数据添加到 ChannelOutboundBuffer 缓存中;
  2. 调用 ctx.flush () 方法时,才把数据从 ChannelOutboundBuffer 取出来;
  3. 调用 Java 原生的 SocketChannel 把数据发送出去

本节,我们一起学习了 Netty 中服务写出数据过程的源码剖析,通过阅读源码,我们知道, Netty 中写出数据实际上分成了两步: ctx.write () 和 ctx.flush () , write () 的时候只是把数据添加到缓存中, flush () 才真正把数据发送出去,之所以要分成两步,也是基于效率来考虑的,大家可以类比快递网点的生活案例进行对比,如果没有网点,则需要接收到快递就装车拉走,将要耗费巨大的人力物力财力。

好鬼枯燥,有没有,一点都不够有趣。 TODO 需要重新写