前言
上一篇我们讲了pipeline如何增减节点。这篇文章主要看一下pipeline如何处理事件传播。伴随着事件传播,还会讲一些零零散散的细节。比如HeadContext和TailContext。
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同时实现了ChannelOutboundHandler和ChannelInboundHandler,但是更有意思的是,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我们说的是服务端的Worker(Boss主要做接收),Worker对应的Channel是NioSocketChannel,对应的unsafe是NioByteUnsafe,具体来说是NioSocketChannelUnsafe。这个初始化我们可以在NioSocketChannel的构造方法中,跟随其中的super构造函数一路追进去就看到了,不展开说了。
我们看下这个NioByteUnsafe的read方法:
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();
}
}
以上的过程,基本上是:
- 拿到
ByteBuf分配器,用分配器来分配一个ByteBuf。 - 把
Channel中的数据读入到ByteBuf中。(doReadBytes方法) - 调用
fireChannelRead触发pipeline的读事件,将读取到的ByteBuf在管道中传播。 - 数据读完之后,调用
pipeline.fireChannelReadComplete从head节点开始传播至整个pipeline。
关于读的部分,我们先忽略。主要看pipeline。这里读取的时候会执行pipeline.fireChannelRead:AbstractChannelHandlerContext.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);
}
}
综合上面的代码合注释,这里会传递到HeadContext的channelRead方法里。注意channelRead方法是实现了inBoundHandler接口。
这里我们回顾前面提出的问题,为什么HeadContext同时实现了inBoundHandler和outBoundHandler,却只认为自己是一个outBoundContext。这里我直接反过来说,如果HeadContext不继承inBoundHandler会怎样:invokeChannelRead中(ChannelInboundHandler) handler()这一句强转会直接报错。那么实现就比较麻烦了,可能代码应该这么写:
if ( handler() instanceof HeadContext) {
(HeandContext)handler() . invokeRead
} else {
((ChannelInboundHandler) handler()).channelRead(this, msg);
}
这样做是显然不优雅的。怎么解决呢?很简单,让HeadContext继承一下inboundHandler接口即可,这样我们可以对所有的handler统一处理。
继续回来看,现在已经把方法传递给HeadContext了,HeadContext的channelRead很简单: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);
}
});
}
}
上面的步骤:
- 先找到下一个inbound节点,这里的节点是
AbstractChannelHandlerContext。 - 执行这个节点的
invokeChannelRead。这个方法会把数据传递给这个节点(context)包含的handler - 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);,第一次跳转到AbstractChannel的writeAndFlush,委托给pipeline的writeAndFlush,然后调用tail.writeAndFlush(msg);。
这里注意一点就是,无论我们在pipeline中的哪个位置调用channel.writeAndFlush(pushInfo);,都会从整个管道的最尾端开始向前传递(pipeline的writeAndFlush),如果我们调用AbstractChannelHandlerContext,则是从当前节点开始向前传递。这里注意一下。
我们看下AbstractChannelHandlerContext的writeAndFlush:write(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);
}
这里会把write和flush封装成一个task,然后由Reactor线程去执行,这里我们在之前的文章Reacotr的task那篇中已经讲过了。
writeAndFlushTask实际上会调用super.write(ctx, msg, promise);和ctx.invokeFlush();,父类的write实际上调用的是ctx.invokeWrite(msg, promise);,其实我们就可以看到,都是同一个套路,都是调用AbstractChannelHandlerContext的对应方法。这个我们之前也说了,实际上是从当前context节点开始,向前传递outBound事件,我们之前同样也说过,最外层的outBoundHandler就是我们的HeadContext,我们看下HeadContext的write方法:unsafe.write(msg, promise);,这里委托给unsafe,写入缓存中,而HeadContext的flush,同样把flush操作委托给了unsafe。
这里同样总结一下,HeadContext同样也是所有写出的出口。
那么HeadContext和TailContext的作用就已经很清楚了。
至此,我们已经把pipelin中的事件传播讲完了。其实除了出入两种事件,pipeline中还包扩一种异常事件传播,这个我们后面会单独拿出来说一下。敬请期待吧~