文章目录
- 2.2.1 UnSafe接口(重点)
- 2.2.2 NioUnSafe接口-略
- 2.2.3 AbstractUnsafe类-略
- 2.2.4 AbstractNioUnsafe类-略
- 2.2.5 NioSocketChannelUnsafe类和NioByteUnsafe类-重点
- 2.3.1 NioByteUnsafe中的读:委托到外部类NioSocketChannel
- 2.3.2 NioByteUnsafe中的写:委托到外部类NioSocketChannel
- 2.3.3 NioMessageUnsafe中的读:委托到外部类NioSocketChannel
- 2.3.4 NioMessageUnsafe 的写,在tcp协议层面我们基本不会涉及,暂时忽略,udp协议的读者可以自己去研究一番~
- 3.1 从NioEventLoop类的processSelectedKey()方法(处理轮询到的事件)到NioByteUnsafe类中的read()方法
- 3.2 附:HeadContext类源码解析(重点:channelRead(msg)方法,且看上面调用的invokeChannelRead(head, msg)是怎样的?)
- 3.3 channelReadComplete()中的自动读取
- 5.1 源码解析:TailContext类(继承AbstractChannelHandlerContext类,实现ChannelInboundHandler接口),TailContext类第一个作用:中止事件传播
- 5.2源码解析:TailContext类的两个方法:exceptionCaught()方法和channelRead()方法,TailContext类第二个作用:对一些重要的事件做一些日志提醒
- 6.1 pipeline中的outBound事件传播(outBound事件:writeAndFlush操作为例)
- 6.2 源码解析:AbstractChannelHandlerContext类的write()方法,pipeline中的outBound事件传播(outBound事件:writeAndFlush操作为例,AbstractChannelHandlerContext类的write() 这个才是真正干事的方法)
- 6.3 pipeline中的outBound事件传播(outBound事件:encoder节点为例)
- 7.1 异常处理器需要加载自定义节点的最末尾
- 7.2 inBound异常的处理:解释了为什么异常处理器要加在pipeline的最后?(以AbstractChannelHandlerContext.invokeChannelRead()发生的异常为例)
- 7.3 outBound异常的处理(以writeAndFlush()发生的异常为例)
- 8.2.1 从NioEventLoop类的processSelectedKey()方法(处理轮询到的事件)到NioByteUnsafe类中的read()方法
- 8.2.2 源码解析:HeadContext类
- 8.2.3 channelReadComplete()中的自动读取
- 8.3 pipeline中的inBound事件传播
- 8.4 TailContext类的源码解析(pipeline中tail节点的两个作用)
- 8.5 pipeline中的outBound事件传播(outBound事件:writeAndFlush操作和encoder节点为例)
- 8.5.1 pipeline中的outBound事件传播(outBound事件:writeAndFlush操作为例)
- 8.5.2 pipeline中的outBound事件传播(outBound事件:encoder节点为例)
- 8.6.1 异常处理器需要加载自定义节点的最末尾
- 8.6.2 inBound异常的处理:解释了为什么异常处理器要加在pipeline的最后?(以AbstractChannelHandlerContext.invokeChannelRead()发生的异常为例)
- 8.6.3 outBound异常的处理(以writeAndFlush()发生的异常为例)
- 九、小结
一、前言
我们已经了解了pipeline在netty中所处的角色,像是一条流水线,控制着字节流的读写,本文,我们在这个基础上继续深挖pipeline在事件传播,异常传播等方面的细节
主要内容
(2)netty中的Unsafe到底是干什么的
(3)pipeline中的head
(4)pipeline中的inBound事件传播
(5)pipeline中的tail
(6)pipeline中的outBound事件传播
(7)pipeline 中异常的传播
Pipeline第一篇:核心是pipeline非循环双链表的插入和删除;
Pipeline第二篇:UnSafe接口+HeadContext类+inBound事件传播+TailContext类+outBound事件传播+异常Exception传播。
二、Unsafe到底是干什么的
之所以Unsafe放到pipeline中讲,是因为unsafe和pipeline密切相关,pipeline中的有关io的操作最终都是落地到unsafe,所以,有必要先讲讲unsafe
2.1 初识Unsafe
金手指:顾名思义,unsafe是不安全的意思,就是告诉你不要在应用程序里面直接使用Unsafe以及他的衍生类对象。
netty官方的解释如下
Unsafe operations that should never be called from user-code. These methods are only provided to implement the actual transport, and must be invoked from an I/O thread
金手指:Unsafe 在Channel定义,属于Channel的内部类,表明Unsafe和Channel密切相关,所以,我们不要在应用程序里面直接使用Unsafe以及他的衍生类对象
2.2 Unsafe整个继承结构(7个类)
UnSafe整个继承体系中,一共包括七个,UnSafe接口、NioUnSafe接口、AbstractUnsafe类、
AbstractNioSafe类、NioByteUnsafe类、NioMessageUnSafe类、NioSocketChannelUnsafe类
2.2.1 UnSafe接口(重点)
interface Unsafe {
RecvByteBufAllocator.Handle recvBufAllocHandle(); // 分配内存
SocketAddress localAddress(); // 返回本地接口,对于服务端来说,就是自己的的接口
SocketAddress remoteAddress(); // 返回远程接口,对于服务端来说就是客户端接口
void register(EventLoop eventLoop, ChannelPromise promise); // 注册事件循环
void bind(SocketAddress localAddress, ChannelPromise promise); // 绑定网卡端口
void connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise); // Socket的连接和关闭
void disconnect(ChannelPromise promise); // Socket的连接和关闭
void close(ChannelPromise promise); // Socket的连接和关闭
void closeForcibly(); // Socket的连接和关闭
void beginRead(); // 读
void write(Object msg, ChannelPromise promise); // 写
void flush(); // 刷新
ChannelPromise voidPromise();
ChannelOutboundBuffer outboundBuffer();
}
按功能可以分为分配内存,Socket四元组信息(服务端ip:port 客户端ip:port),注册事件循环,绑定网卡端口,Socket的连接和关闭,Socket的读写,看的出来,这些操作都是和jdk底层相关
2.2.2 NioUnSafe接口-略
NioUnsafe 在 Unsafe基础上增加了以下几个接口
public interface NioUnsafe extends Unsafe {
SelectableChannel ch(); // 访问底层javase的SelectableChannel的功能
void finishConnect(); // 连接
void read(); // 读
void forceFlush(); // 写
}
从增加的接口以及类名上来看,NioUnsafe 增加了可以访问底层javase的SelectableChannel的功能,定义了从SelectableChannel读取数据的read方法
2.2.3 AbstractUnsafe类-略
AbstractUnsafe 实现了大部分Unsafe的功能
2.2.4 AbstractNioUnsafe类-略
AbstractNioUnsafe 主要是通过代理到其外部类AbstractNioChannel拿到了与jdk nio相关的一些信息,比如SelectableChannel,SelectionKey等等
2.2.5 NioSocketChannelUnsafe类和NioByteUnsafe类-重点
NioSocketChannelUnsafe和NioByteUnsafe放到一起讲,其实现了IO的基本操作,读,和写,这些操作都与jdk底层相关
NioMessageUnsafe和 NioByteUnsafe 是处在同一层次的抽象,netty将一个新连接的建立也当作一个io操作来处理,这里的Message的含义我们可以当作是一个SelectableChannel,
读的意思就是accept一个SelectableChannel,
写的意思是针对一些无连接的协议,比如UDP来操作的,我们先不用关注
2.3 Unsafe的分类
从以上继承结构来看,我们可以总结出两种类型的Unsafe分类,一个是与连接的字节数据读写相关的NioByteUnsafe,一个是与新连接建立操作相关的NioMessageUnsafe
2.3.1 NioByteUnsafe中的读:委托到外部类NioSocketChannel
NioByteUnsafe类中的doReadBytes()方法
protected int doReadBytes(ByteBuf byteBuf) throws Exception {
final RecvByteBufAllocator.Handle allocHandle = unsafe().recvBufAllocHandle(); // 分配内存,使用allocHandle记录
allocHandle.attemptedBytesRead(byteBuf.writableBytes()); // 写出到allocHandle记录下来
return byteBuf.writeBytes(javaChannel(), allocHandle.attemptedBytesRead());
}
NioByteUnsafe中的读:委托到外部类NioSocketChannel?
解释:NioByteUnsafe类中的doReadBytes()方法,新建内存,然后内存读入实参bytebuf写出的数据,
最后一行:已经与jdk底层(金手指:javaChannel())以及netty中的ByteBuf相关(金手指:byteBuf.writeBytes),将jdk的 SelectableChannel的字节数据读取到netty的ByteBuf中
2.3.2 NioByteUnsafe中的写:委托到外部类NioSocketChannel
NioByteUnsafe类中的doWriteBytes()方法
方法概要:实参bytebuf读入
@Override
protected int doWriteBytes(ByteBuf buf) throws Exception {
final int expectedWrittenBytes = buf.readableBytes();
return buf.readBytes(javaChannel(), expectedWrittenBytes);
}
最后一行已经与jdk底层以及netty中的ByteBuf相关,将netty的ByteBuf中的字节数据写到jdk的 SelectableChannel中
2.3.3 NioMessageUnsafe中的读:委托到外部类NioSocketChannel
protected int doReadMessages(List<Object> buf) throws Exception {
SocketChannel ch = javaChannel().accept(); // javase channel不用阻塞
if (ch != null) {
buf.add(new NioSocketChannel(this, ch));
return 1;
}
return 0;
}
NioMessageUnsafe 的读操作很简单,就是调用jdk的accept()方法,新建立一条连接
2.3.4 NioMessageUnsafe 的写,在tcp协议层面我们基本不会涉及,暂时忽略,udp协议的读者可以自己去研究一番~
关于Unsafe我们就先了解这么多
三、核心:pipeline中的head
3.1 从NioEventLoop类的processSelectedKey()方法(处理轮询到的事件)到NioByteUnsafe类中的read()方法
金手指:从NioEventLoop类的processSelectedKey()方法(处理轮询到的事件)到NioByteUnsafe类中的read()方法?整个流程?
NioByteUnsafe 要做的事情可以简单地分为以下几个步骤
(1)拿到Channel的config之后,拿到ByteBuf分配器;
(2)用分配器来分配一个ByteBuf,ByteBuf是netty里面的字节数据载体,后面读取的数据都读到这个对象里面将Channel中的数据读取到ByteBuf,
(3)数据读完之后,调用 pipeline.fireChannelRead(byteBuf); 从head节点开始传播至整个pipeline
【netty源码分析之pipeline(一)】中,我们了解到head节点在pipeline中第一个处理IO事件,新连接接入和读事件【【Netty源码解析002】Netty中的Reactor(Netty中Reactor线程启动+Reactor线程三步骤执行)第二步骤】被检测到
NioEventLoop类
private void processSelectedKey(SelectionKey k, AbstractNioChannel ch) {
final AbstractNioChannel.NioUnsafe unsafe = ch.unsafe(); // 从channel变量得到unsafe变量
//新连接的已准备接入或者已存在的连接有数据可读
if ((readyOps & (SelectionKey.OP_READ | SelectionKey.OP_ACCEPT)) != 0 || readyOps == 0) {
unsafe.read(); // 调用unsafe变量完成read()方法
}
}
读操作直接依赖到unsafe来进行(就是本文第二部分讲到的unsafe),新连接的接入【【Netty源码分析003】新连接接入全解析】中已详细阐述,这里不再描述,下面将重点放到连接字节数据流的读写,进入NioByteUnsafe类的read()方法。
NioByteUnsafe
@Override
public final void read() {
final ChannelConfig config = config(); // 得到config
final ChannelPipeline pipeline = pipeline(); // 得到pipeline
// 创建ByteBuf分配器
final ByteBufAllocator allocator = config.getAllocator(); // 拿到ByteBuf类型的分配器allocator
final RecvByteBufAllocator.Handle allocHandle = recvBufAllocHandle();
allocHandle.reset(config);
ByteBuf byteBuf = null; // 声明一个局部变量ByteBuf,dowhile循环中用到
do { // do..while循环不断读取
// 分配一个ByteBuf
byteBuf = allocHandle.allocate(allocator); // 用这个ByteBuf类型的分配器allocator来分配一个ByteBuf
// 将数据读取到分配的ByteBuf中去
allocHandle.lastBytesRead(doReadBytes(byteBuf));
if (allocHandle.lastBytesRead() <= 0) {
byteBuf.release();
byteBuf = null; // GC回收
close = allocHandle.lastBytesRead() < 0;
break;
}
// 触发事件,将会引发pipeline的读事件传播
pipeline.fireChannelRead(byteBuf); // 从head节点开始传播至整个pipeline
byteBuf = null; // GC回收
} while (allocHandle.continueReading());
pipeline.fireChannelReadComplete();
}
同样,我抽出了核心代码,细枝末节先剪去,NioByteUnsafe 要做的事情可以简单地分为以下几个步骤
拿到Channel的config之后,拿到ByteBuf分配器,用分配器来分配一个ByteBuf,ByteBuf是netty里面的字节数据载体,后面读取的数据都读到这个对象里面
将Channel中的数据读取到ByteBuf
数据读完之后,调用 pipeline.fireChannelRead(byteBuf); 从head节点开始传播至整个pipeline
这里,我们的重点其实就是 pipeline.fireChannelRead(byteBuf);
DefaultChannelPipeline类
final AbstractChannelHandlerContext head; // DefaultChannelPipeline类中提供head变量
//...
head = new HeadContext(this); // DefaultChannelPipeline构造函数中head节点
public final ChannelPipeline fireChannelRead(Object msg) {
AbstractChannelHandlerContext.invokeChannelRead(head, msg); //1、调用静态方法,实参为head msg,表示从head节点开始读事件的传播,表示调用HeadContext类中的chanelRead()方法
return this; // 2、返回这个DefaultChannelPipeline类对象
}
金手指:DefaultChannelPipeline类的fireChannelRead()方法完成两件事
1、调用静态方法,实参为head msg,表示从head节点开始读事件的传播,表示调用HeadContext类中的chanelRead()方法
2、返回这个DefaultChannelPipeline类对象
pipeline如下图:
可以看到,数据从head节点开始流入,在进行下一步之前,我们先把head节点的功能过一遍
3.2 附:HeadContext类源码解析(重点:channelRead(msg)方法,且看上面调用的invokeChannelRead(head, msg)是怎样的?)
HeadContext类,让我们认识一下这个HeadContext类
final class HeadContext extends AbstractChannelHandlerContext
implements ChannelOutboundHandler, ChannelInboundHandler {
private final Unsafe unsafe; // Unsafe引用
HeadContext(DefaultChannelPipeline pipeline) { // HeadContext构造函数
super(pipeline, null, HEAD_NAME, false, true); // 调用父类
unsafe = pipeline.channel().unsafe(); // 初始化unsafe引用
setAddComplete(); // 设置添加成功标志,上一篇文章pipeline添加节点最后一步的时候遇到过,这里是因为在pipeline中添加head节点,所以这个方法,设置添加成功标志
}
@Override
public ChannelHandler handler() {
return this; // 需要调用handler()方法,就是直接返回this,表示HeadContext类对象
}
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception {
// NOOP // 添加删除没什么好说的-略
}
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception {
// NOOP // 添加删除没什么好说的-略
}
// 这些方法都是交给unsafe去实现:bind() connect() disconnect() close() deregister() read() write() flush()
@Override
public void bind(
ChannelHandlerContext ctx, SocketAddress localAddress, ChannelPromise promise)
throws Exception {
unsafe.bind(localAddress, promise); // HeadContext类中注入unsafe引用,bind()方法是通过调用unsafe的bind()方法实现的
// bind()与connect()的不同,bind()是localAddress
}
@Override
public void connect(
ChannelHandlerContext ctx,
SocketAddress remoteAddress, SocketAddress localAddress,
ChannelPromise promise) throws Exception {
unsafe.connect(remoteAddress, localAddress, promise); // HeadContext类中注入unsafe引用,connect()方法是通过调用unsafe的connect()方法实现的
// bind()与connect()的不同,connect()是remoteAddress和localAddress
}
@Override
public void disconnect(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
unsafe.disconnect(promise); // HeadContext类中注入unsafe引用,disconnect()方法是通过调用unsafe的disconnect()方法实现的
}
@Override
public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
unsafe.close(promise); // HeadContext类中注入unsafe引用,close()方法是通过调用unsafe的close()方法实现的
}
@Override
public void deregister(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
unsafe.deregister(promise); // HeadContext类中注入unsafe引用,deregister()方法是通过调用unsafe的deregister()方法实现的
}
@Override
public void read(ChannelHandlerContext ctx) {
unsafe.beginRead(); // HeadContext类中注入unsafe引用,read()方法是通过调用unsafe的read()方法实现的
}
@Override
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
unsafe.write(msg, promise); // HeadContext类中注入unsafe引用,write()方法是通过调用unsafe的write()方法实现的
}
// 下面就是调用ctx的方法了
@Override
public void flush(ChannelHandlerContext ctx) throws Exception {
unsafe.flush(); // HeadContext类中注入unsafe引用,flush()方法是通过调用unsafe的flush()方法实现的
}
}
小结:
001
DefaultChannelPipeline类调用HeadContext类的方法
public final ChannelPipeline fireChannelInactive() {
AbstractChannelHandlerContext.invokeChannelInactive(this.head);
return this;
}
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelInactive();
}
002
DefaultChannelPipeline类调用HeadContext类的方法
public final ChannelPipeline fireChannelWritabilityChanged() {
AbstractChannelHandlerContext.invokeChannelWritabilityChanged(this.head);
return this;
}
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelWritabilityChanged();
}
003
DefaultChannelPipeline类调用HeadContext类的方法
public final ChannelPipeline fireUserEventTriggered(Object event) {
AbstractChannelHandlerContext.invokeUserEventTriggered(this.head, event);
return this;
}
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
ctx.fireUserEventTriggered(evt);
}
004
DefaultChannelPipeline类调用HeadContext类的方法
public final ChannelPipeline fireChannelReadComplete() {
AbstractChannelHandlerContext.invokeChannelReadComplete(this.head);
return this;
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelReadComplete();
readIfIsAutoRead(); // HeadContext调用自己的的readIfIsAutoRead()
}
private void readIfIsAutoRead() {
if (channel.config().isAutoRead()) {
channel.read(); // 调用channel的read()方法,channel.read()底层会调用pipeline.read()方法
}
}
005
DefaultChannelPipeline类调用HeadContext类的方法
public final ChannelPipeline fireChannelRead(Object msg) {
AbstractChannelHandlerContext.invokeChannelRead(this.head, msg);
return this;
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg); // DefaultChannelPipeline类中的fireChannelRead(msg)方法(即ctx.fireChannelRead(msg); ),调用AbstractChannelHandlerContext.invokeChannelRead(head,msg),底层调用HctxeadContext类的channelRead(ctx,msg)方法
}
006
DefaultChannelPipeline类调用HeadContext类的方法
public final ChannelPipeline fireChannelInactive() {
AbstractChannelHandlerContext.invokeChannelInactive(this.head);
return this;
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelInactive(); DefaultChannelPipeline类中的fireChannelInactive(msg)方法(即ctx.fireChannelRead(msg); ),调用AbstractChannelHandlerContext.invokeChannelRead(head,msg),底层调用HctxeadContext类的channelRead(ctx,msg)方法
}
007
DefaultChannelPipeline类调用HeadContext类的方法
public final ChannelPipeline fireChannelActive() {
AbstractChannelHandlerContext.invokeChannelActive(this.head);
return this;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelActive();
readIfIsAutoRead(); // 激活方法中,HeadContext调用自己的的readIfIsAutoRead()
}
private void readIfIsAutoRead() {
if (channel.config().isAutoRead()) {
channel.read(); // 调用channel的read()方法,channel.read()底层会调用pipeline.read()方法
}
}
008
DefaultChannelPipeline类调用HeadContext类的方法
public final ChannelPipeline fireChannelUnregistered() {
AbstractChannelHandlerContext.invokeChannelUnregistered(this.head);
return this;
}
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelUnregistered();
// Remove all handlers sequentially if channel is closed and unregistered.
if (!channel.isOpen()) {
destroy();
}
}
009
DefaultChannelPipeline类调用HeadContext类的方法
public final ChannelPipeline fireChannelRegistered() {
AbstractChannelHandlerContext.invokeChannelRegistered(this.head);
return this;
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
invokeHandlerAddedIfNeeded();
ctx.fireChannelRegistered();
}
010
DefaultChannelPipeline类调用HeadContext类的方法
public final ChannelPipeline fireChannelRegistered() {
AbstractChannelHandlerContext.invokeChannelRegistered(this.head);
return this;
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
ctx.fireExceptionCaught(cause); // 异常捕获
}
DefaultChannelPipeline类的10个fireXxx方法,就是调用HeadContext中10个方法、
所以就到了HeadContext类中的channelRead()方法,这里就又到了
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
ctx.fireChannelRead(msg); // 又到了fireXxx()就循环了
}
相关问题1:为什么HeadContext构造函数里面调用setAddComplete()方法?
回答1:设置添加成功标志,上一篇文章pipeline添加节点最后一步的时候遇到过,这里是因为在pipeline中添加head节点,所以这个方法,设置添加成功标志。
final class HeadContext extends AbstractChannelHandlerContext
implements ChannelOutboundHandler, ChannelInboundHandler {
从head节点继承的两个接口看,它既是一个ChannelHandlerContext,同时又是一个inBoundHandler和outBoundHandler
在传播读写事件的时候,head的功能只是简单地将事件传播下去,如ctx.fireChannelRead(msg);
真正执行:在真正执行读写操作的时候,例如在调用writeAndFlush()等方法的时候,最终都会委托到unsafe执行。
3.3 channelReadComplete()中的自动读取
而当一次数据读完,channelReadComplete方法首先被调用,TA要做的事情除了将事件继续传播下去之外,还得继续向reactor线程注册读事件,即调用readIfIsAutoRead(), 我们进入这个readIfIsAutoRead()方法看一下
HeadContext
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelReadComplete(); //1、事件继续传播下去
this.readIfIsAutoRead(); // 2、继续向reactor线程注册读事件 如果是自动读取继续读
}
private void readIfIsAutoRead() {
if (channel.config().isAutoRead()) { // 如果是自动读取,就调用read()
channel.read(); // 调用read
}
}
AbstractChannel
@Override
public Channel read() {
pipeline.read(); // 1、向reactor线程注册读事件,具体是怎样向reactor线程注册read事件的,进入看
return this; // 2、返回Channel及其子类对象
}
DefaultChannelPipeline类
public final ChannelPipeline read() {
this.tail.read();
return this;
}
TailContext类
public ChannelHandlerContext read() { // 这个才是真正干事的,具体是怎样向reactor线程注册read事件的
final AbstractChannelHandlerContext next = this.findContextOutbound();
EventExecutor executor = next.executor();
if(executor.inEventLoop()) { // reator线程
next.invokeRead(); //所谓的向reactor线程中注册事件,就是找到next,然后调用它的invokeRead(),就可以继续读下一个了
} else { // 业务线程
Runnable task = next.invokeReadTask;
if(task == null) {
next.invokeReadTask = task = new Runnable() {
public void run() {
next.invokeRead();
}
};
}
executor.execute(task);
}
return this;
}
HeadContext的自动读取模式:默认情况下,Channel都是默认开启自动读取模式的,即只要Channel是active的(金手指:自动读取的判断标志就是 channel.config().isAutoRead()),读完一波数据之后就继续向selector注册读事件,这样就可以连续不断得读取数据,最终,通过pipeline,还是传递到head节点。
所以,我们再看head节点,HeadContext类
HeadContext
@Override
public void read(ChannelHandlerContext ctx) {
unsafe.beginRead(); // 没有任何逻辑,直接委托给了 NioByteUnsafe 类的beginRead()方法
}
NioByteUnsafe
@Override
public final void beginRead() {
doBeginRead(); // 没有任何逻辑,直接委托给了 AbstractNioChannel 类的doBeginRead()方法
}
AbstractNioChannel
@Override
protected void doBeginRead() throws Exception { // pipeline.fireChannelActive();最终会调用到AbstractNioChannel.doBeginRead()
DefaultChannelPipeline类的fireChannelActive(); 调用 HeadContext类的
// Channel.read() or ChannelHandlerContext.read() was called,都会调用到这里来
final SelectionKey selectionKey = this.selectionKey;
if (!selectionKey.isValid()) {
return;
}
readPending = true;
final int interestOps = selectionKey.interestOps();
if ((interestOps & readInterestOp) == 0) {
selectionKey.interestOps(interestOps | readInterestOp);
}
}
doBeginRead() 做的事情很简单,拿到处理过的selectionKey,然后如果发现该selectionKey若在某个地方被移除了readInterestOp操作,这里给他加上,事实上,标准的netty程序是不会走到这一行的,只有在三次握手成功之后,如下方法被调用
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelActive();
readIfIsAutoRead();
}
才会将readInterestOp注册到SelectionKey上,可结合【Netty处理新连接】来看
总结一点,head节点的作用就是作为pipeline的头节点开始传递读写事件,调用unsafe进行实际的读写操作,下面,进入pipeline中非常重要的一环,inbound事件的传播
四、pipeline中的inBound事件传播
4.1 为什么pipeline.fireChannelActive();最终会调用到AbstractNioChannel.doBeginRead()?且看事件传播机制
在【Netty源码解析003 新连接接入】一文中,我们没有详细描述为啥pipeline.fireChannelActive();最终会调用到AbstractNioChannel.doBeginRead(),了解pipeline中的事件传播机制,你会发现相当简单
第一步,了解整个事件传播机制,从DefaultChannelPipeline类中的fireChannelActive()开始
DefaultChannelPipeline
public final ChannelPipeline fireChannelActive() {
AbstractChannelHandlerContext.invokeChannelActive(head);
return this;
}
三次握手成功之后,TCP连接成功建立,pipeline.fireChannelActive();被调用,
然后以head节点为参数,直接一个静态方法调用,且看这个静态方法
AbstractChannelHandlerContext.invokeChannelActive(head);
AbstractChannelHandlerContext
static void invokeChannelActive(final AbstractChannelHandlerContext next) {
EventExecutor executor = next.executor(); //得到head.executor()方法
if (executor.inEventLoop()) { // 在事件循环中,就是reactor线程的执行器executor
next.invokeChannelActive();
} else { // 不在事件循环中,就是普通线程执行器
executor.execute(new Runnable() {
@Override
public void run() {
next.invokeChannelActive();
}
});
}
}
第二步,事件传播,从HeadContext类的channelActive()方法开始
首先,netty为了确保线程的安全性(为了确保线程的安全性,这是重点),将确保该操作在reactor线程中被执行,不在reactor线程中也会封装到reactor线程中,这里直接调用 到HeadContext.fireChannelActive()方法,表示从head节点开始
HeadContext
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.fireChannelActive();
readIfIsAutoRead();
}
我们先看 ctx.fireChannelActive();,跟进去之前我们先看下当前pipeline的情况,这里到了父类AbstractChannelHandlerContext类的fireChannelActive()方法实现
AbstractChannelHandlerContext
public ChannelHandlerContext fireChannelActive() {
final AbstractChannelHandlerContext next = findContextInbound(); // 得到一个next变量,是AbstractChannelHandlerContext类型,表示下一个inBound类型的节点
invokeChannelActive(next); // next作为实参,执行AbstractChannelHandlerContext类中的invokeChannelActive()方法
return this;
}
第三步,调用 findContextInbound() 找到下一个inbound节点,由于当前pipeline的双向链表结构中既有inbound节点,又有outbound节点,让我们看看netty是怎么找到下一个inBound节点的(key)
AbstractChannelHandlerContext
private AbstractChannelHandlerContext findContextInbound() {
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.next; // 不断找
} while (!ctx.inbound); //当ctx不是inbound就继续找,是inbound就跳出,
这样将inbound作为循环判断条件,可以保证找到下一个inbound节点
return ctx;
}
小结:netty寻找下一个inBound节点的过程是一个线性搜索的过程,他会遍历双向链表的下一个节点,直到下一个节点为inBound(关于inBound和outBound,【netty源码分析004 pipeline(一)】已有说明,这里不再详细分析)
第四步,找到下一个inbound节点之后,执行 invokeChannelActive(next);,一个递归调用,直到最后一个inBound节点——tail节点
TailContext
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception { }
Tail节点的该方法为空,结束调用(TailContext节点的第一个作用,中止事件传播),同理,可以分析所有的inBound事件的传播,正常情况下,即用户如果不覆盖每个节点的事件传播操作doBeginRead()方法,几乎所有的事件最后都落到tail节点,所以,我们有必要研究一下tail节点所具有的功能
五、TailContext类的源码解析(pipeline中tail节点的两个作用)
5.1 源码解析:TailContext类(继承AbstractChannelHandlerContext类,实现ChannelInboundHandler接口),TailContext类第一个作用:中止事件传播
final class TailContext extends AbstractChannelHandlerContext implements ChannelInboundHandler {
TailContext(DefaultChannelPipeline pipeline) {
super(pipeline, null, TAIL_NAME, true, false);
setAddComplete();
}
@Override
public ChannelHandler handler() {
return this; //hander直接返回这个,因为HeadContext类和TailContext类,都是实现ChannelInboundHandler,都是已经是是一个Handler了
}
@Override
public void channelRegistered(ChannelHandlerContext ctx) throws Exception { }
@Override
public void channelUnregistered(ChannelHandlerContext ctx) throws Exception { }
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception { }
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception { }
@Override
public void channelWritabilityChanged(ChannelHandlerContext ctx) throws Exception { }
@Override
public void handlerAdded(ChannelHandlerContext ctx) throws Exception { }
@Override
public void handlerRemoved(ChannelHandlerContext ctx) throws Exception { }
@Override
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
// This may not be a configuration error and so don't log anything.
// The event may be superfluous for the current pipeline configuration.
ReferenceCountUtil.release(evt);
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
onUnhandledInboundException(cause);
}
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
onUnhandledInboundMessage(msg);
}
@Override
public void channelReadComplete(ChannelHandlerContext ctx) throws Exception { }
}
相关问题1:为什么HeadContext类和TailContext类的构造函数最后一句调用setAddComplete()方法?
回答: //博客【Netty pipeline(一)中】,讲到DefaultChannelPipeline构造函数,this.tail = new DefaultChannelPipeline.TailContext(this); 将DefaultChannelPipeline对象作为实参,调用TailContext构造函数,就是在DefaultChannelPipeline构造函数中,插入tail节点,并设置tail prev指针指向head节点,所以这里有一句 setAddComplete(); 设置添加完成的标志位
相关问题2:关于对于HeadContext类和TailContext类的handler()方法返回this的解释?
回答:TailContext类实现ChannelInBoundHandler接口,ChannelInBoundHandler接口又向上实现ChannelHandler接口,所以它的handler()方法,就是直接返回this;
HeadContext类实现ChannelInBoundHandler接口和ChannelOutBoundHandler接口,ChannelInBoundHandler接口和ChannelOutBoundHandler接口又向上实现ChannelHandler接口,所以它的handler()方法,就是直接返回this;
正如我们前面所提到的,tail节点的大部分作用即终止事件的传播(方法体为空)(就只有handler()方法和构造函数有点用),除此之外,有两个重要的方法我们必须提一下,exceptionCaught()和channelRead()
5.2源码解析:TailContext类的两个方法:exceptionCaught()方法和channelRead()方法,TailContext类第二个作用:对一些重要的事件做一些日志提醒
TailContext类的exceptionCaught()方法
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
DefaultChannelPipeline.this.onUnhandledInboundException(cause);
}
DefaultChannelPipeline类的onUnhandledInboundException()方法
protected void onUnhandledInboundException(Throwable cause) {
try {
// 如果用户自定义节点没有处理的话,会落到tail节点,tail节点可不会简单地吞下这个异常,而是向你发出警告
logger.warn(
"An exceptionCaught() event was fired, and it reached at the tail of the pipeline. " +
"It usually means the last handler in the pipeline did not handle the exception.",
cause);
} finally {
ReferenceCountUtil.release(cause);
}
}
异常传播的机制和inBound事件传播的机制一样,最终如果用户自定义节点没有处理的话,会落到tail节点,tail节点可不会简单地吞下这个异常,而是向你发出警告,相信使用netty的同学对这段警告不陌生吧?
TailContext类的channelRead()方法
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
DefaultChannelPipeline.this.onUnhandledInboundMessage(msg);
}
DefaultChannelPipeline类的onUnhandledInboundMessage()方法
protected void onUnhandledInboundMessage(Object msg) {
try {
logger.debug(
"Discarded inbound message {} that reached at the tail of the pipeline. " +
"Please check your pipeline configuration.", msg);
} finally {
ReferenceCountUtil.release(msg);
}
}
另外,tail节点在发现字节数据(ByteBuf)或者decoder之后的业务对象在pipeline流转过程中没有被消费,落到tail节点,tail节点就会给你发出一个警告,告诉你,我已经将你未处理的数据给丢掉了
总结一下,tail节点的作用就是结束事件传播,并且对一些重要的事件做一些善意提醒
六、pipeline中的outBound事件传播(outBound事件:writeAndFlush操作和encoder节点为例)
6.1 pipeline中的outBound事件传播(outBound事件:writeAndFlush操作为例)
上一节中,我们在阐述tail节点的功能时,忽略了其父类 AbstractChannelHandlerContext 所具有的功能,这里我们来看看这个TailContext类的父类AbstractChannelHandlerContext到底干什么?
金手指:
class TailContext extends AbstractChannelHandlerContext
TailContext 是 AbstractChannelHandlerContext 的子类
这一节中,我们以最常见的writeAndFlush操作为例来看下pipeline中的outBound事件是如何向外传播的
典型的消息推送系统中,会有类似下面的一段代码
Channel channel = getChannel(userInfo); // 根据用户信息拿到对应的Channel
channel.writeAndFlush(pushInfo); // 给用户推送消息
这段代码的含义就是根据用户信息拿到对应的Channel,然后给用户推送消息,跟进 channel.writeAndFlush
NioSocketChannel类
public ChannelFuture writeAndFlush(Object msg) {
return pipeline.writeAndFlush(msg); // NioSocketChannel类的writeAndFlush()方法什么都不做,直接调用pipeline的writeAndFlush(msg)方法
}
从pipeline开始往外传播
DefaultChannelPipeline类
public final ChannelFuture writeAndFlush(Object msg) {
return tail.writeAndFlush(msg);
}
Channel 中大部分outBound事件都是从tail开始往外传播, writeAndFlush()方法是tail继承而来的方法,我们跟进去,跟到TailContext继承writeAndFlush()方法的这个类,AbstractChannelHandlerContext类,看writeAndFlush()方法的源码实现
AbstractChannelHandlerContext类(TailContext类继承于AbstractChannelHandlerContext类,其writeAndFlush()的实现在其父类的AbstractChannelHandlerContext类里面,所以,ctrl+左键,点击进来就到了AbstractChannelHandlerContext类里面)
public ChannelFuture writeAndFlush(Object msg) {
return writeAndFlush(msg, newPromise()); // 调用自己类的两个参数的writeAndFlush()方法
}
public ChannelFuture writeAndFlush(Object msg, ChannelPromise promise) {
write(msg, true, promise); // 1、调用自己类的write()方法去完成这个写操作
return promise; // 2、在返回值方面,返回一个ChannelPromise类型给调用方,这个ChannelPromise本质上是一个ChannelFuture,调用方拿到这个future可以在适当的时机拿到操作的结果,或者注册回调
}
public interface ChannelPromise extends ChannelFuture, Promise {
这里提前说一点,netty中很多io操作都是异步操作,返回一个ChannelFuture给调用方,调用方拿到这个future可以在适当的时机拿到操作的结果,或者注册回调,后面的源码系列会深挖,这里就带过了,我们继续
6.2 源码解析:AbstractChannelHandlerContext类的write()方法,pipeline中的outBound事件传播(outBound事件:writeAndFlush操作为例,AbstractChannelHandlerContext类的write() 这个才是真正干事的方法)
AbstractChannelHandlerContext类的write() 这个才是真正干事的方法
private void write(Object msg, boolean flush, ChannelPromise promise) {
AbstractChannelHandlerContext next = findContextOutbound(); // 找到下一个outBound类型的AbstractChannelHandlerContext
final Object m = pipeline.touch(msg, next); //pipeline.touch() 传入msg 和next
EventExecutor executor = next.executor(); // 得到next的执行器executor
if (executor.inEventLoop()) { // 是reactor线程
if (flush) { // 实参flush作为判断条件
next.invokeWriteAndFlush(m, promise); // 这个方法是后面的核心,next是调用者
} else {
next.invokeWrite(m, promise);
}
} else { // 普通线程 如果业务线程调用Channel的读写方法
AbstractWriteTask task;
if (flush) { // 实参flush作为判断条件
task = WriteAndFlushTask.newInstance(next, m, promise); // netty会将该操作封装成一个task
} else {
task = WriteTask.newInstance(next, m, promise); // netty会将该操作封装成一个task
}
safeExecute(executor, task, promise, m); // 随后在reactor线程中执行
}
}
netty为了保证程序的高效执行,保证所有的核心的操作都在reactor线程中处理(无论是reactor线程调用channel的读写方法还是业务线程调用channel的读写方法),如果业务线程调用Channel的读写方法,netty会将该操作封装成一个task,随后在reactor线程中执行,参考【netty源码分析之揭开reactor线程的面纱(三)】,异步task的执行
这里我们为了不跑偏,假设是在reactor线程中(上面的这段例子其实是在业务线程中),
第一步,先调用findContextOutbound()方法从后往前找到下一个outBound()节点
AbstractChannelHandlerContext类
private AbstractChannelHandlerContext findContextOutbound() {
AbstractChannelHandlerContext ctx = this;
do {
ctx = ctx.prev;
} while (!ctx.outbound);
return ctx;
}
找outBound节点的过程和找inBound节点类似,反方向遍历pipeline中的双向链表,直到第一个outBound节点next,
第二步,从tail节点向前传递,调用next.invokeWriteAndFlush(m, promise)
AbstractChannelHandlerContext类
private void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
if (invokeHandler()) {
invokeWrite0(msg, promise); // 参数msg和ChannelPromise(ChannelFuture子类)
invokeFlush0();
} else {
writeAndFlush(msg, promise);
}
}
调用该节点的ChannelHandler的write方法,flush方法我们暂且忽略,后面会专门讲writeAndFlush的完整流程
AbstractChannelHandlerContext类
private void invokeWrite0(Object msg, ChannelPromise promise) {
try {
((ChannelOutboundHandler) handler()).write(this, msg, promise); // 后面这个ctx实参就是从this传递过来的,不一定是AbstractChannelHandlerContext类对象,是实际对象,是AbstractChannelHandlerContext类子对象,另外两个参数是msg和ChannelPromise(ChannelFuture子类)
} catch (Throwable t) {
notifyOutboundHandlerException(t, promise); // 异常后面讲
}
}
第三步,实际调用:父类ChannelOutboundHandlerAdapter类的write()方法
我们在使用outBound类型的ChannelHandler中,一般会继承 ChannelOutboundHandlerAdapter,所以,我们需要看看他的 write方法是怎么处理outBound事件传播的(就是到父类ChannelOutboundHandlerAdapter类去找write()方法的具体实现)
ChannelOutboundHandlerAdapter类
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ctx.write(msg, promise); // 调用这个ctx实参的write()方法,那么这个ctx实参是哪里来的,将msg和ChannelPromise(ChannelFuture子类)传递过来
}
很简单,他除了递归调用 ctx.write(msg, promise);之外,啥事也没干,在【netty源码分析之pipeline(一)】我们已经知道,pipeline的双向链表结构中,最后一个outBound节点是head节点,因此数据最终会落地到TA的write方法
第四步,不断传递,直到HeadContext类这个AbstractChannelHandlerContext子类,才调用unsafe.write(),完成实际操作,参数还是msg和ChannelPromise(ChannelFuture子类)
HeadContext类
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
unsafe.write(msg, promise); // 不断传递,直到HeadContext类这个AbstractChannelHandlerContext子类,才调用unsafe.write(),完成实际操作,参数还是msg和ChannelPromise(ChannelFuture子类)
}
这里,小结:加深了我们对head节点的理解,即所有的数据写出都会到最后的head节点(outBound就是数据写出),我们在下一节会深挖,这里暂且到此为止
6.3 pipeline中的outBound事件传播(outBound事件:encoder节点为例)
实际情况下,outBound类的节点中会有一种特殊类型的节点叫encoder,它的作用是根据自定义编码规则将业务对象转换成ByteBuf,而这类encoder 一般继承自 MessageToByteEncoder
下面是一段,继承于MessageToByteEncoder类,就是一个encoder节点了
public abstract class DataPacketEncoder extends MessageToByteEncoder<DatePacket> {
@Override
protected void encode(ChannelHandlerContext ctx, DatePacket msg, ByteBuf out) throws Exception {
// 这里拿到业务对象msg的数据,然后调用 out.writeXXX()系列方法编码
}
}
**为什么业务代码只需要覆盖这里的encode方法,就可以将业务对象转换成字节流写出去呢?**通过前面的调用链条,我们需要查看一下其父类MessageToByteEncoder的write方法是怎么处理业务对象的
MessageToByteEncoder类
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ByteBuf buf = null;
try {
// 需要判断当前编码器能否处理这类对象
if (acceptOutboundMessage(msg)) { // 先调用 acceptOutboundMessage 方法判断,该encoder是否可以处理msg对应的类的对象(暂不展开)
//可以处理,进入
I cast = (I) msg; // 通过之后,就强制转换,这里的泛型I对应的是DataPacket
// 分配内存
buf = allocateBuffer(ctx, cast, preferDirect); // 强制类型转换之后,先开辟一段内存,
try {
encode(ctx, cast, buf); // 调用encode(),即回到DataPacketEncoder中,将buf装满数据,
} finally {
ReferenceCountUtil.release(cast);
}
// buf到这里已经装载着数据,于是把该buf往前丢,知道head节点
if (buf.isReadable()) { //最后,如果buf中被写了数据(buf.isReadable()),
ctx.write(buf, promise); //就将该buf往前丢write,一直传递到head节点,被head节点的unsafe消费掉
} else {
buf.release(); // 释放这个buf
ctx.write(Unpooled.EMPTY_BUFFER, promise); // // 如果当前encoder不能处理当前业务对象,就简单地将该业务对象向前传播write,直到head节点
}
buf = null; // GC操作
} else {
// 如果不能处理,就将outBound事件继续往前面传播
ctx.write(msg, promise);
}
} catch (EncoderException e) {
throw e;
} catch (Throwable e) {
throw new EncoderException(e);
} finally {
if (buf != null) {
buf.release(); // 最后,都处理完之后,释放buf,避免堆外内存泄漏。
}
}
先调用 acceptOutboundMessage 方法判断,该encoder是否可以处理msg对应的类的对象(暂不展开),通过之后,就强制转换,这里的泛型I对应的是DataPacket,转换之后,先开辟一段内存,调用encode(),即回到DataPacketEncoder中,将buf装满数据,最后,如果buf中被写了数据(buf.isReadable()),就将该buf往前丢,一直传递到head节点,被head节点的unsafe消费掉
当然,如果当前encoder不能处理当前业务对象,就简单地将该业务对象向前传播,直到head节点(金手指:outBound事件最后都是到head节点),最后,都处理完之后,释放buf,避免堆外内存泄漏。
七、pipeline 中异常Exception的传播
7.1 异常处理器需要加载自定义节点的最末尾
我们通常在业务代码中,会加入一个异常处理器,统一处理pipeline过程中的所有的异常,并且,一般该异常处理器需要加载自定义节点的最末尾,即
pipeline中异常的传播
此类ExceptionHandler一般继承自 ChannelDuplexHandler,表示异常节点既是一个inBound节点又是一个outBound节点,我们分别分析一下inBound事件和outBound事件过程中,ExceptionHandler是如何才处理这些异常的
7.2 inBound异常的处理:解释了为什么异常处理器要加在pipeline的最后?(以AbstractChannelHandlerContext.invokeChannelRead()发生的异常为例)
我们以数据的读取为例,看下netty是如何传播在这个过程中发生的异常
我们前面已经知道,对于每一个节点的数据读取都会调用AbstractChannelHandlerContext.invokeChannelRead()方法,所以我们先定位到这个方法
AbstractChannelHandlerContext
private void invokeChannelRead(Object msg) {
try {
((ChannelInboundHandler) handler()).channelRead(this, msg);
} catch (Throwable t) {
notifyHandlerException(t);
}
}
可以看到该节点最终委托到其内部的ChannelHandler处理channelRead,而在最外层catch整个Throwable,因此,我们在如下用户代码中的异常会被捕获
public class BusinessHandler extends ChannelInboundHandlerAdapter {
@Override
protected void channelRead(ChannelHandlerContext ctx, Object data) throws Exception {
//...
throw new BusinessException(...);
//...
}
}
上面这段业务代码中的 BusinessException 会被 BusinessHandler所在的节点捕获,进入到 notifyHandlerException(t);往下传播,我们看下它是如何传播的
AbstractChannelHandlerContext
private void notifyHandlerException(Throwable cause) {
// 略去了非关键代码,读者可自行分析
invokeExceptionCaught(cause);
}
private void invokeExceptionCaught(final Throwable cause) {
handler().exceptionCaught(this, cause);
}
可以看到,此Hander中异常优先由此Handelr中的exceptionCaught方法来处理,默认情况下,如果不覆写此Handler中的exceptionCaught方法,调用
ChannelInboundHandlerAdapter
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
ctx.fireExceptionCaught(cause); // 默认调用的ChannelInboundHandlerAdapter类中的exceptionCaught()方法,仅仅委托给ChannelHandlerContext类对象去处理
}
AbstractChannelHandlerContext
public ChannelHandlerContext fireExceptionCaught(final Throwable cause) {
invokeExceptionCaught(next, cause); // 但是其父类AbstractChannelHandlerContext,仅仅是去调用下一个ChannelHandlerContext的exceptionCaught()方法,这里的的next参数是ChannelHandlerContext类型,cause参数是Throwable,就是这个异常本身
return this;
}
到了这里,已经很清楚了,如果我们在自定义Handler中没有处理异常,那么默认情况下该异常将一直传递下去,遍历每一个节点,直到最后一个自定义异常处理器ExceptionHandler来终结,收编异常
Exceptionhandler
public Exceptionhandler extends ChannelDuplexHandler {
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause)
throws Exception {
// 处理该异常,并终止异常的传播
}
}
到了这里,你应该知道为什么异常处理器要加在pipeline的最后了吧?
7.3 outBound异常的处理(以writeAndFlush()发生的异常为例)
然而对于outBound事件传播过程中所发生的异常,该Exceptionhandler照样能完美处理,为什么?
我们以前面提到的writeAndFlush方法为例,来看看outBound事件传播过程中的异常最后是如何落到Exceptionhandler中去的
前面我们知道,channel.writeAndFlush()方法最终也会调用到节点的 invokeFlush0()方法(write机制比较复杂,我们留到后面的文章中将)
AbstractChannelHandlerContext
private void invokeWriteAndFlush(Object msg, ChannelPromise promise) {
if (invokeHandler()) {
invokeWrite0(msg, promise);
invokeFlush0();
} else {
writeAndFlush(msg, promise);
}
}
private void invokeFlush0() {
try {
((ChannelOutboundHandler) handler()).flush(this);
} catch (Throwable t) {
notifyHandlerException(t);
}
}
而invokeFlush0()会委托其内部的ChannelHandler的flush方法,我们一般实现的即是ChannelHandler的flush方法
好,假设在当前节点在flush的过程中发生了异常,都会被 notifyHandlerException(t);捕获,该方法会和inBound事件传播过程中的异常传播方法一样,也是轮流找下一个异常处理器,而如果异常处理器在pipeline最后面的话,一定会被执行到,这就是为什么该异常处理器也能处理outBound异常的原因
小结:金手指:关于为啥 ExceptionHandler 既能处理inBound,又能处理outBound类型的异常的原因,总结一点就是,在任何节点中发生的异常都会往下一个节点传递,最后终究会传递到异常处理器
八、面试金手指
8.0 小结
- 一个Channel对应一个Unsafe,Unsafe处理底层操作,NioServerSocketChannel对应NioMessageUnsafe, NioSocketChannel对应NioByteUnsafe
- inBound事件从head节点传播到tail节点,outBound事件从tail节点传播到head节点
- 异常传播只会往后传播,而且不分inbound还是outbound节点,不像outBound事件一样会往前传播
8.1 Unsafe接口源码解析
1、UnSafe只能在Channel中使用,不能单独使用
顾名思义,unsafe是不安全的意思,就是告诉你不要在应用程序里面直接使用Unsafe以及他的衍生类对象。
Unsafe 在Channel定义,属于Channel的内部类,表明Unsafe和Channel密切相关,所以,我们不要在应用程序里面直接使用Unsafe以及他的衍生类对象
2、UnSafe API接口
按功能可以分为分配内存,Socket四元组信息(服务端ip:port 客户端ip:port),注册事件循环,绑定网卡端口,Socket的连接和关闭,Socket的读写,看的出来,这些操作都是和jdk底层相关
3、两种类型的UnSafe分类
从以上继承结构来看,我们可以总结出两种类型的Unsafe分类,一个是与连接的字节数据读写相关的NioByteUnsafe,一个是与新连接建立操作相关的NioMessageUnsafe
4、NioByteUnsafe中的读:委托到外部类NioSocketChannel?
解释:NioByteUnsafe类中的doReadBytes()方法,新建内存,然后内存读入实参bytebuf写出的数据,
最后一行:已经与jdk底层(金手指:javaChannel())以及netty中的ByteBuf相关(金手指:byteBuf.writeBytes),将jdk的 SelectableChannel的字节数据读取到netty的ByteBuf中
5、NioByteUnsafe中的写:委托到外部类NioSocketChannel, 将netty的ByteBuf中的字节数据写到jdk的 SelectableChannel中
8.2 pipeline中的head
8.2.1 从NioEventLoop类的processSelectedKey()方法(处理轮询到的事件)到NioByteUnsafe类中的read()方法
金手指:从NioEventLoop类的processSelectedKey()方法(处理轮询到的事件)到NioByteUnsafe类中的read()方法?整个流程?
NioByteUnsafe 要做的事情可以简单地分为以下几个步骤
(1)拿到Channel的config之后,拿到ByteBuf分配器;
(2)用分配器来分配一个ByteBuf,ByteBuf是netty里面的字节数据载体,后面读取的数据都读到这个对象里面将Channel中的数据读取到ByteBuf,
(3)数据读完之后,调用 pipeline.fireChannelRead(byteBuf); 从head节点开始传播至整个pipeline
8.2.2 源码解析:HeadContext类
金手指:对于HeadContext类的理解
1、注入一个Unsafe对象,这些方法都是交给unsafe去实现:bind() connect() disconnect() close() deregister() read() write() flush()
2、构造函数中setAddComplete()设置添加完成标志,handler()返回this,因为HeadContext就是一个Handler子类对象。
3、DefaultChannelPipeline类中的10个fireXxx都是沿着pipeline调用,从head开始,head对应的方法在HeadContext里面。
4、pipeline链传递包括:fireChannelRead() invokeChannelRead() channelRead() 其中包括选择next
8.2.3 channelReadComplete()中的自动读取
金手指:channelReadComplete()中的自动读取 源码解析
1、当一次数据读完,channelReadComplete方法首先被调用,TA要做的事情除了将事件继续传播下去之外,还得继续向reactor线程注册读事件,即调用readIfIsAutoRead();
2、HeadContext的自动读取模式:默认情况下,Channel都是默认开启自动读取模式的,即只要Channel是active的(金手指:自动读取的判断标志就是 channel.config().isAutoRead()),读完一波数据之后就继续向selector注册读事件,这样就可以连续不断得读取数据,最终,通过pipeline,还是传递到head节点。
小结:head节点的作用就是作为pipeline的头节点开始传递读写事件,调用unsafe进行实际的读写操作
8.3 pipeline中的inBound事件传播
回答刚开始的问题:为什么pipeline.fireChannelActive();最终会调用到AbstractNioChannel.doBeginRead()?且看事件传播机制?
第一步,了解整个事件传播机制,从DefaultChannelPipeline类中的fireChannelActive()开始;
第二步,事件传播,从HeadContext类的channelActive()方法开始;
第三步,调用 findContextInbound() 找到下一个inbound节点;
第四步,找到下一个inbound节点之后,执行 invokeChannelActive(next);,一个递归调用,直到最后一个inBound节点——tail节点;
第五步,Tail节点的该方法为空,结束调用(TailContext节点的第一个作用,中止事件传播),同理,可以分析所有的inBound事件的传播,正常情况下,即用户如果不覆盖每个节点的事件传播操作doBeginRead()方法,几乎所有的事件最后都落到tail节点。
8.4 TailContext类的源码解析(pipeline中tail节点的两个作用)
金手指:TailContext类的源码解析(pipeline中tail节点的两个作用)
1、tail节点第一个作用:中止事件传播,所以很多方法体为空
tail节点的大部分作用即终止事件的传播(方法体为空)(就只有handler()方法和构造函数有点用)
2、tail节点第二个作用:对一些重要的事件做一些日志提醒,如
TailContext类的方法:exceptionCaught()方法;异常传播的机制和inBound事件传播的机制一样,最终如果用户自定义节点没有处理的话,会落到tail节点,tail节点可不会简单地吞下这个异常,而是向你发出警告
TailContext类的方法:channelRead()方法;tail节点在发现字节数据(ByteBuf)或者decoder之后的业务对象在pipeline流转过程中没有被消费,落到tail节点,tail节点就会给你发出一个警告,告诉你,我已经将你未处理的数据给丢掉了
8.5 pipeline中的outBound事件传播(outBound事件:writeAndFlush操作和encoder节点为例)
8.5.1 pipeline中的outBound事件传播(outBound事件:writeAndFlush操作为例)
金手指:
1、pipeline中的outBound事件传播(outBound事件:writeAndFlush操作为例)
1.1 我们在阐述tail节点的功能时,忽略了其父类 AbstractChannelHandlerContext 所具有的功能,这里我们来看看这个TailContext类的父类AbstractChannelHandlerContext到底干什么? 我们以最常见的writeAndFlush操作为例来看下pipeline中的outBound事件是如何向外传播的。
1.2 典型的消息推送源码中, 根据用户信息拿到对应的Channel,然后给用户推送消息,就是调用NioSocketChannel类的writeAndFlush()方法
1.3 从pipeline的tail节点开始往外传播:
NioSocketChannel类的writeAndFlush()方法什么都不做,直接调用pipeline的writeAndFlush(msg)方法
DefaultChannelPipeline类的writeAndFlush()方法,调用tail节点的writeAndFlush()方法,表示从pipeline中的tail节点开始向外传播
Channel 中大部分outBound事件都是从tail开始往外传播, writeAndFlush()方法是tail继承而来的方法,我们跟进去,跟到TailContext继承writeAndFlush()方法的这个类,AbstractChannelHandlerContext类,看writeAndFlush()方法的源码实现
1.4 AbstractChannelHandlerContext类(TailContext类继承于AbstractChannelHandlerContext类,其writeAndFlush()的实现在其父类的AbstractChannelHandlerContext类里面,所以,ctrl+左键,点击进来就到了AbstractChannelHandlerContext类里面)
// AbstractChannelHandlerContext类调用自己类的两个参数的writeAndFlush()方法
//(1)AbstractChannelHandlerContext类调用自己类的write()方法去完成这个写操作
//(2)这个write()方法,在返回值方面,返回一个ChannelPromise类型给调用方,这个ChannelPromise本质上是一个ChannelFuture,调用方拿到这个future可以在适当的时机拿到操作的结果,或者注册回调
2、源码解析:AbstractChannelHandlerContext类的write()方法,pipeline中的outBound事件传播(outBound事件:writeAndFlush操作为例,AbstractChannelHandlerContext类的write() 这个才是真正干事的方法)
2.1 第一步,先调用findContextOutbound()方法从后往前找到下一个outBound()节点;
2.2 第二步,netty为了保证程序的高效执行,保证所有的核心的操作都在reactor线程中处理(无论是reactor线程调用channel的读写方法还是业务线程调用channel的读写方法),如果业务线程调用Channel的读写方法,netty会将该操作封装成一个task,随后在reactor线程中执行;
2.3 第三步,从tail节点向前传递,调用next.invokeWriteAndFlush(m, promise)
2.4 第四步,实际调用:父类ChannelOutboundHandlerAdapter类的write()方法
2.5 第五步,不断传递,直到HeadContext类这个AbstractChannelHandlerContext子类,才调用unsafe.write(),完成实际操作,参数还是msg和ChannelPromise(ChannelFuture子类)
小结:加深了我们对head节点的理解,即所有的数据写出都会到最后的head节点(outBound就是数据写出)
8.5.2 pipeline中的outBound事件传播(outBound事件:encoder节点为例)
金手指:MessageToByteEncoder类的write()方法
1、先调用 acceptOutboundMessage 方法判断,该encoder是否可以处理msg对应的类的对象(暂不展开),通过之后,就强制转换,这里的泛型I对应的是DataPacket,转换之后,先开辟一段内存,调用encode(),即回到DataPacketEncoder中,将buf装满数据,最后,如果buf中被写了数据(buf.isReadable()),就将该buf往前丢,一直传递到head节点,被head节点的unsafe消费掉
2、当然,如果当前encoder不能处理当前业务对象,就简单地将该业务对象向前传播,直到head节点(金手指:outBound事件最后都是到head节点),最后,都处理完之后,释放buf,避免堆外内存泄漏。
8.6 异常的传播
8.6.1 异常处理器需要加载自定义节点的最末尾
1、异常处理器需要加载自定义节点的最末尾
2、此类ExceptionHandler一般继承自 ChannelDuplexHandler,表示异常节点既是一个inBound节点又是一个outBound节点
8.6.2 inBound异常的处理:解释了为什么异常处理器要加在pipeline的最后?(以AbstractChannelHandlerContext.invokeChannelRead()发生的异常为例)
相关问题:为什么异常处理器要加在pipeline的最后?以AbstractChannelHandlerContext.invokeChannelRead()发生的异常为例
1、每一个节点的数据读取都会调用AbstractChannelHandlerContext.invokeChannelRead()方法,所以我们先定位到这个方法;
2、invokeChannelRead()方法中,该节点最终委托到其内部的ChannelHandler处理channelRead,而在最外层catch整个Throwable,因此,我们在如下用户代码中的异常会被捕获
3、channelRead()方法中, throw new BusinessException(…); 抛出的BusinessException 会被 BusinessHandler所在的节点捕获,进入到 notifyHandlerException(t);往下传播
4、notifyHandlerException()方法中,调用invokeExceptionCaught(cause);,该方法中,再次调用handler().exceptionCaught(this, cause);方法,
5、向后传递的业务逻辑:所以,此Hander中异常优先由此Handelr中的exceptionCaught方法来处理,默认情况下,如果不覆写此Handler中的exceptionCaught方法,调用ChannelInboundHandlerAdapter类的exceptionCaught()方法,这个方法中,再次调用AbstractChannelHandlerContext类中的fireExceptionCaught()方法,这个方法中,再次调用invokeExceptionCaught(next, cause);
ChannelInboundHandlerAdapter类的exceptionCaught()方法,默认调用的ChannelInboundHandlerAdapter类中的exceptionCaught()方法,仅仅委托给ChannelHandlerContext类对象去处理;然后,但是其父类AbstractChannelHandlerContext,仅仅是去调用下一个ChannelHandlerContext的exceptionCaught()方法,这里的的next参数是ChannelHandlerContext类型,cause参数是Throwable,就是这个异常本身
6、Exceptionhandler类最终处理异常的业务逻辑:如果我们在自定义Handler中没有处理异常,那么默认情况下该异常将一直传递下去,遍历每一个节点,直到最后一个自定义异常处理器ExceptionHandler来终结,收编异常,到最后的Exceptionhandler的exceptionCaught()方法中处理异常
小结:启动的handler类中的exceptionCaught()方法仅仅是传递异常,不处理
8.6.3 outBound异常的处理(以writeAndFlush()发生的异常为例)
小结:金手指:关于为啥 ExceptionHandler 既能处理inBound,又能处理outBound类型的异常的原因,总结一点就是,在任何节点中发生的异常都会往下一个节点传递,最后终究会传递到异常处理器
九、小结
【Netty源码解析004】Netty中的pipeline(二),完成了。
天天打码,天天进步!!!