Netty源码分析——pipeline事件传递

1,993 阅读7分钟

前言

上一篇我们讲了pipeline如何增减节点。这篇文章主要看一下pipeline如何处理事件传播。伴随着事件传播,还会讲一些零零散散的细节。比如HeadContextTailContext

HeadContext

这是我们pipeline的起点。跟TailContext一样,HeadContext也是一种特殊的Context

final class HeadContext extends AbstractChannelHandlerContext
implements ChannelOutboundHandler, ChannelInboundHandler {

private final Unsafe unsafe;

HeadContext(DefaultChannelPipeline pipeline) {
//注意这里,第四个参数是是否是inbound,HeadContext这里直接给了false
super(pipeline, null, HEAD_NAME, false, true);
unsafe = pipeline.channel().unsafe();
setAddComplete();
}
}

相比tail更特殊的是,这个HeadContext同时实现了ChannelOutboundHandlerChannelInboundHandler,但是更有意思的是,HeadContext继承了ChannelInboundHandler但是却在构造函数里,把自己设置为了不是inbound的context。这就好比,我虽然继承了接口A,但是我说我不是A。

这个原因,我们在后面的分析中也会讲讲(原谅我功力尚浅只能猜测一下作者的意思了~)。

我们回忆一下,服务端在收到请求后,会启动一个NioEventLoop,这是一个Reacotr线程,同样会执行我们在Reacotr中提到的三个工作。其中一个工作就是处理read事件,具体的流程我就不重复分析了,直接看代码,来自processSelectedKey

if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read();
}

这里会传递给unsafe。注意这里的Channel我们说的是服务端的WorkerBoss主要做接收),Worker对应的ChannelNioSocketChannel,对应的unsafe是NioByteUnsafe,具体来说是NioSocketChannelUnsafe。这个初始化我们可以在NioSocketChannel的构造方法中,跟随其中的super构造函数一路追进去就看到了,不展开说了。

我们看下这个NioByteUnsaferead方法:

final ChannelConfig config = config();
if (shouldBreakReadReady(config)) {
clearReadPending();
return;
}
final ChannelPipeline pipeline = pipeline();
// ByteBuf分配器
final ByteBufAllocator allocator = config.getAllocator();
final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
allocHandle.reset(config);

ByteBuf byteBuf = null;
boolean close = false;
try {
do {
// 分配一个ByteBuf
byteBuf = allocHandle.allocate(allocator);
// 将数据读取到分配的ByteBuf中去
allocHandle.lastBytesRead(doReadBytes(byteBuf));
if (allocHandle.lastBytesRead() <= 0) {
//没有可读的数据,释放byteBuf
byteBuf.release();
byteBuf = null;
close = allocHandle.lastBytesRead() < 0;
if (close) {
readPending = false;
}
break;
}

allocHandle.incMessagesRead(1);
readPending = false;
//触发读
pipeline.fireChannelRead(byteBuf);
byteBuf = null;
} while (allocHandle.continueReading());

allocHandle.readComplete();
//触发读完毕
pipeline.fireChannelReadComplete();

if (close) {
closeOnRead(pipeline);
}
} catch (Throwable t) {
handleReadException(pipeline, byteBuf, t, close, allocHandle);
} finally {
if (!readPending && !config.isAutoRead()) {
removeReadOp();
}
}

以上的过程,基本上是:

  1. 拿到ByteBuf分配器,用分配器来分配一个ByteBuf
  2. Channel中的数据读入到ByteBuf中。(doReadBytes方法)
  3. 调用fireChannelRead触发pipeline的读事件,将读取到的ByteBuf在管道中传播。
  4. 数据读完之后,调用pipeline.fireChannelReadCompletehead节点开始传播至整个pipeline。

关于读的部分,我们先忽略。主要看pipeline。这里读取的时候会执行pipeline.fireChannelReadAbstractChannelHandlerContext.invokeChannelRead(head, msg);,简单的一句话。

继续追进去:

static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
//这里的next 就是 HeadContext
next.invokeChannelRead(m);
} else {
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelRead(m);
}
});
}
}

private void invokeChannelRead(Object msg) {
if (invokeHandler()) {
try {
// 这里的handler()方法会返回HeadContext自己
// 其实就是调用了HeadContext.channelRead方法
((ChannelInboundHandler) handler()).channelRead(this, msg);
} catch (Throwable t) {
notifyHandlerException(t);
}
} else {
fireChannelRead(msg);
}
}

综合上面的代码合注释,这里会传递到HeadContextchannelRead方法里。注意channelRead方法是实现了inBoundHandler接口。

这里我们回顾前面提出的问题,为什么HeadContext同时实现了inBoundHandleroutBoundHandler,却只认为自己是一个outBoundContext。这里我直接反过来说,如果HeadContext不继承inBoundHandler会怎样:invokeChannelRead(ChannelInboundHandler) handler()这一句强转会直接报错。那么实现就比较麻烦了,可能代码应该这么写:

if ( handler() instanceof HeadContext) {
(HeandContext)handler() . invokeRead
} else {
((ChannelInboundHandler) handler()).channelRead(this, msg);
}

这样做是显然不优雅的。怎么解决呢?很简单,让HeadContext继承一下inboundHandler接口即可,这样我们可以对所有的handler统一处理。

继续回来看,现在已经把方法传递给HeadContext了,HeadContextchannelRead很简单:ctx.fireChannelRead(msg);。对Netty有基本了解的同学应该知道,这里其实就是从ctx节点开始往后传递read事件,我们看下AbstractChannelHandlerContext#fireChannelRead和涉及到的几个方法:

public ChannelHandlerContext fireChannelRead(final Object msg) {
invokeChannelRead(findContextInbound(), msg);
return this;
}

//找到下一个inbound节点
private AbstractChannelHandlerContext findContextInbound() {
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.next;
} while (!ctx.inbound);
return ctx;
}

//执行这个节点的invokeChannelRead
static void invokeChannelRead(final AbstractChannelHandlerContext next, Object msg) {
final Object m = next.pipeline.touch(ObjectUtil.checkNotNull(msg, "msg"), next);
EventExecutor executor = next.executor();
if (executor.inEventLoop()) {
//这里next是一个context,invokeChannelRead会委托给这个context对应的handler
next.invokeChannelRead(m);
} else {
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelRead(m);
}
});
}
}

上面的步骤:

  1. 先找到下一个inbound节点,这里的节点是AbstractChannelHandlerContext
  2. 执行这个节点的invokeChannelRead。这个方法会把数据传递给这个节点(context)包含的handler
  3. handler对数据处理之后可以考虑写回(write)或者继续向后传递。

读完成之后,会调用pipeline.fireChannelReadComplete,最终还是会跑到headContext里:

public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelReadComplete();
readIfIsAutoRead();
}

这里多了一步readIfIsAutoRead

private void readIfIsAutoRead() {
// 默认情况下,Channel都是默认开启自动读取模式的
// channel关闭是自动读
if (channel.config().isAutoRead()) {
channel.read();
}
}

public Channel read() {
pipeline.read();
return this;
}

//tail read会从tail开始往前执行read
public final ChannelPipeline read() {
tail.read();
return this;
}

channel.config().isAutoRead()是Channel自动读,即只要Channel是active的,读完一波数据之后就继续向selector注册读事件,这样就可以连续不断得读取数据,最终,通过pipeline,还是传递到head节点,执行unsafe.beginRead();,最终会走到:

protected void doBeginRead() throws Exception {
final SelectionKey selectionKey = this.selectionKey;
if (!selectionKey.isValid()) {
return;
}

readPending = true;

final int interestOps = selectionKey.interestOps();
if ((interestOps & readInterestOp) == 0) {
selectionKey.interestOps(interestOps | readInterestOp);
}
}

selectionKey若在某个地方被移除了readInterestOp操作,则加上。

一般Channel关闭自动读,是当前Channel的缓冲区(OutboundBuffer)大小超过了WRITE_BUFFER_HIGH_WATER_MARK时。这个我们暂且不说。

总结一下,Head节点就是整个读的开始。

TailContext

tail的代码就不贴了,tail作为很多事件的截止位置(很多方法是空实现),这里主要看一下tail的两个特殊的方法:exceptionCaught()channelRead()

两个方法的实现类似,我们拿出一个看,看下channelRead

try {
logger.debug(
"Discarded inbound message {} that reached at the tail of the pipeline. " +
"Please check your pipeline configuration.", msg);
} finally {
ReferenceCountUtil.release(msg);
}

记录一个日志,说明消息没有被处理,并且被传递到了pipeline的尾部。然后释放消息。

outBound事件传递

我们解析读的代码,实际上我们也说过了,这是一种inBound的事件,我们看下outBound事件是如何传递的。outBound举个例子,就是writeAndFlush。这个操作和write类似,但是比write稍微复杂一点点,多了一步flush

例子就是调用channel.writeAndFlush(pushInfo);,第一次跳转到AbstractChannelwriteAndFlush,委托给pipelinewriteAndFlush,然后调用tail.writeAndFlush(msg);

这里注意一点就是,无论我们在pipeline中的哪个位置调用channel.writeAndFlush(pushInfo);,都会从整个管道的最尾端开始向前传递(pipelinewriteAndFlush),如果我们调用AbstractChannelHandlerContext,则是从当前节点开始向前传递。这里注意一下。

我们看下AbstractChannelHandlerContextwriteAndFlushwrite(msg, true, promise);,这个true就表示写过之后flush缓存,直接写回远端。

看下这个方法:

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);
}

这里会把writeflush封装成一个task,然后由Reactor线程去执行,这里我们在之前的文章Reacotr的task那篇中已经讲过了。

writeAndFlushTask实际上会调用super.write(ctx, msg, promise);ctx.invokeFlush();,父类的write实际上调用的是ctx.invokeWrite(msg, promise);,其实我们就可以看到,都是同一个套路,都是调用AbstractChannelHandlerContext的对应方法。这个我们之前也说了,实际上是从当前context节点开始,向前传递outBound事件,我们之前同样也说过,最外层的outBoundHandler就是我们的HeadContext,我们看下HeadContextwrite方法:unsafe.write(msg, promise);,这里委托给unsafe,写入缓存中,而HeadContextflush,同样把flush操作委托给了unsafe

这里同样总结一下,HeadContext同样也是所有写出的出口。

那么HeadContextTailContext的作用就已经很清楚了。

至此,我们已经把pipelin中的事件传播讲完了。其实除了出入两种事件,pipeline中还包扩一种异常事件传播,这个我们后面会单独拿出来说一下。敬请期待吧~