上一节,我们一起学习了 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);
}
为了便于理解,我特意把箭头区分成两种颜色,并标上数字:
红色表示接收数据的过程,蓝色表示写出数据的过程;
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) 方法;
至此,写出数据过程的源码剖析就讲完了,让我们再来总结一下:
- 调用 ctx.write () 方法时,只是把数据添加到 ChannelOutboundBuffer 缓存中;
- 调用 ctx.flush () 方法时,才把数据从 ChannelOutboundBuffer 取出来;
- 调用 Java 原生的 SocketChannel 把数据发送出去
本节,我们一起学习了 Netty 中服务写出数据过程的源码剖析,通过阅读源码,我们知道, Netty 中写出数据实际上分成了两步: ctx.write () 和 ctx.flush () , write () 的时候只是把数据添加到缓存中, flush () 才真正把数据发送出去,之所以要分成两步,也是基于效率来考虑的,大家可以类比快递网点的生活案例进行对比,如果没有网点,则需要接收到快递就装车拉走,将要耗费巨大的人力物力财力。
好鬼枯燥,有没有,一点都不够有趣。 TODO 需要重新写