目录
一:初识Netty
Netty的起源
Netty项目起源于2004年,Java社区第一个基于事件驱动的网络应用开发框架。Hadoop、Cassandra、Spark、Dubbo、gRPC、RocketMQ、Zookeeper、Spring5等都在使用Netty,是开发高性能Java服务器的必学框架。
学习Netty的基础知识
- 协议知识:TCP的keeplive、粘包/半包现象、封帧等;
- 网络编程知识:三种IO、三种Reactor模式;
- Java高阶编程知识:对象池、堆外内存、锁等知识
Netty是什么
- 本质:网络应用程序框架
- 实现:异步、事件驱动
- 特性:高性能、可维护、快速开发
- 用途:开发服务器和客户端
Netty的架构
JDK NIO与Netty
- Netty支持常用应用层协议;解决传输问题:粘包、半包现象;支持流量整形;完善的锻炼、Idle等异常处理;
- 规避JDK NIO bug:经典的epoll bug:异常唤醒空转导致CPU 100%;IP_TOS参数(IP包的优先级和QoS选项)使用时抛出异常;
- API更友好更强大:JDK的NIO一些API不友好,功能薄弱,例如ByteBuffer > Netty’s ByteBuf;其他的增强:ThreadLocal > Netty’s FastThreadLocal;
- 隔离变化、屏蔽细节:JDK NIO的实现变化:NIO > NIO2(AIO) > …;屏蔽JDK NIO的实现细节;
为什么只选择Netty
- Apache Mina:Netty作者本人解答:Netty是Mina的重新打造版本,提高了扩展性并解决了一些已知的问题,Netty使用起来也比Mina简单的更多;
- Sun Grizzly:三少:用的少、文档少、更新少;
- Apple SwiftNIO或者ACE:其他语言,Java领域不会考虑;
- Cindy:生命周期不长;
- Tomcat、Jetty:与Netty不属于同一个层次,有自己的通信层实现,且专门与Servlet容器实现的,通用性不高。
Netty的前尘往事
- 4.0之前署于JBOSS,4.0之后独立;
- 2004年6月发布,第一个基于事件驱动的应用网络框架;2008年10月Netty3发布;2013年7月Netty4发布;2013年12月Netty5.Alpha1发布;2015年11月废弃Netty5(废弃原因:复杂、没有明显的性能优势、维护不过来)
Netty与Mina
- 同一作者开发,处于维护阶段
- Alex为Apache Directory开发网络框架,看到Netty2后邀请作者合作开发,结合Netty便有了Mina
Netty的现状与趋势
应用现状:截至2019年9月,30000+项目在使用,实际使用中远大于30000
使用Netty的典型项目:Cassandra数据库、Spark、Hadoop、RocketMQ、Elasticsearch、gRPC、Spring5、Dubbo等
未来趋势
更多流行协议的支持;
紧跟新JDK的步伐;
更多易用性、人性化的支持:IP地址黑白名单、流量整形;
应用越来越多。
二:Netty与三种I/O模式
三种I/O模式
- BIO(阻塞I/O):JDK1.4之前
- NIO(非阻塞I/O):JDK1.4以后
- AIO(异步I/O):JDK1.7以后
Netty对三种I/O模式的支持
BIO -> OIO (Deprecated) | NIO | AIO (Removed) | ||
COMMON | Linux | macOS/BSD | ||
ThreadPreChannelEventLoopGroup | NioEventLoopGroup | EpollEventLoopGroup | KQueueEventLoopGroup | AioEventLoopGroup |
ThreadPreChannelEventLoop | NioEventLoop | EpollEventLoop | KQueueEventLoop | AioEventLoop |
OioServerSocketChannel | NioServerSocketChannel | EpollServerSocketChannel | KQueueServerSocketChannel | AioServerSocketChannel |
OioSocketChannel | NioSocketChannel | EpollSocketChannel | KQueueSocketChannel | AioSocketChannel |
为什么Netty仅支持NIO
- 不建议BIO:连接数高的情况下,阻塞会消耗大量的资源,且效率低下
- 删除AIO:Windows实现成熟,很少用来做服务器,Linux实现不成熟且Linux下的AIO性能提升不明显
为什么Netty有多种实现的NIO
- Netty暴露了更多的可控参数,例如:JDK的NIO默认实现是水平触发,Netty是边缘触发和水平触发同时切换
- Netty实现的垃圾回收更少、性能更好
NIO与BIO
- BIO代码简单
- 当连接数少、并发度低时,BIO性能不输于NIO
Netty对于三种I/O模式的切换
// 此处可以切换至Oio,即
EventLoopGroup bossGroup = new OioEventLoopGroup();
EventLoopGroup bossGroup = new NioEventLoopGroup();
// 此处可以切换至Oio,即EventLoopGroup workGroup = new OioEventLoopGroup();
EventLoopGroup workGroup = new NioEventLoopGroup();
try {
ServerBootstrap b = new ServerBootstrap();
b.group(bossGroup, workGroup)
// 此处可以切换至Oio,即.channel(OioServerSocketChannel.class)
.channel(NioServerSocketChannel.class)
.option(ChannelOption.SO_BACKLOG, 100)
.handler(new LoggingHandler(LogLevel.INFO))
.childOption(ChannelOption.SO_KEEPALIVE, true)
.childOption(NioChannelOption.SO_KEEOALIVE, true);
}
泛型+反射+工厂模式实现I/O模式的切换
三:Netty与三种Reactor
Reactor的三种模式
- Reactor单线程
- Reactor多线程模式
- 主从Reactor多线程模式
I/O与Reactor模式
BIO | NIO | AIO |
Thead-Per-Connection | Reactor | Proactor |
Reactor核心流程
- 注册感兴趣的事件;
- 扫描是否有感兴趣的事件发生;
- 事件发生后做出相应的处理
SocketChannel与监听事件
client/Server | SocketChannel/ServerSocketChannel | OP_ACCEPT | OP_CONNECT | OP_WRITE | OP_READ |
client | SocketChannel | Y | Y | Y | |
server | ServerSocketChannel | Y | |||
server | SocketChannel | Y | Y |
Netty使用Reactor模式
Reactor单线程模式 | EventLoopGroup eventGroup = new NioEventLoopGroup(1); ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(eventGroup); |
Reactor多线程模式 | EventLoopGroup eventGroup = new NioEventLoopGroup(); ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(eventGroup); |
主从Reactor多线程模式 | EventLoopGroup boosGroup = new NioEventLoopGroup(); EventLoopGroup workerGroup = new NioEventLoopGroup(); ServerBootstrap serverBootstrap = new ServerBootstrap(); serverBootstrap.group(bossGroup, workerGroup) |
四:Netty与粘包、半包
粘包与半包
假设客户端分别发送了两个数据包D1和D2给服务端,由于服务端一次读取到的字节数是不确定的,故可能存在以下4种情况。
- 服务端分两次读取到了两个独立的数据包,分别是D1和D2,没有粘包和拆包;
- 服务端一次接收到了两个数据包,D1和D2粘合在一起,被称为TCP粘包;
- 服务端分两次读取到了两个数据包,第一次读取到了完整的D1包和D2包的部分内容,第二次读取到了D2包的剩余内容,这被称为TCP拆包;
- 服务端分两次读取到了两个数据包,第一次读取到了D1包的部分内容D1_1,第二次读取到了D1包的剩余内容D1_2和D2包的整包;
- 如果此时服务端TCP接收滑窗非常小,而数据包D1和D2比较大,很有可能会发生第五种可能,即服务端分多次才能将D1和D2包接收完全,期间发生多次拆包。
粘包发生原因
- 发送方每次写入数据 < 套接字缓冲区大小;
- 接收方读取套接字缓冲区的数据不够及时。
半包发生原因
- 发送方写入数据 > 套接字缓冲区大小
- 发送的数据大于协议的MTU(最大传输单元),必须拆包
根本原因
TCP是流式协议,消息无边界
Netty对封帧方式的支持
方式\支持 | 解码 | 编码 | |
封帧(Framing) | 固定长度 | FixedLengthFrameDecoder | 简单 |
分隔符 | DelimiterBasedFrameDecoder | ||
固定长度字段存个内容的长度信息 | LengthFieldBasedFrameDecoder | LengthFieldPrepender |
五:keepalive与idle
为什么需要keepalive
- 生活场景:假设开了一个饭店,别人来电话订餐,电话通了后,订餐的说了一堆订餐要求,说着说着,对方就不讲电话了(可能忘了/出去办事/线路故障等)。
- 这个时候不会一直握着电话,会确认一句“你还在吗”,如果对方没有回复,挂机。
- 这套机制即“keepalive”。
例:TCP keepalive为例
sysctl -a | grep tcp_keepalive
问题出现概率小 -> 没有必要频繁
net.ipv4.tcp_keepalive_time = 7200
net.ipv4.tcp_keepalive_intvl = 75
判断需“谨慎” -> 不能武断
net.ipv4.tcp_keepalive_probes = 9
当启用(默认关闭)keepalive时,TCP在连接没有数据。通过7200秒后发送keepalive消息,当探测没有确认时,按75秒的重试频率重发,一直发9个探测包都没有确认,就认定连接失败。所以总耗时一般为:2小时11分钟(7200 + 75秒 * 9次)
应用层keepalive
- 协议分层,各层关注点不同:传输层关注是否“通”,应用层关注是否可服务?类比前面的电话订餐例子,电话能通,不代表有人接;服务器连接在,但是不定可以服务(例如服务不过来)
- TCP层的keepalive默认关闭,且经过路由等中转设备keepalive包可能被丢弃
- TCP层的keepalive时间过长,默认大于2小时,但属于系统参数,改动影响所有应用
idle监测
假设开了一个饭店,,别人电话来订餐,电话通了后,订餐的说了一堆订餐要求,说着说着,对方就不讲话了。你会稍等片刻,看短时间内对方还会不会说话(idle检测),如果对方不说,认定对方存在问题(idle),于是开始发问“你还在吗”(keepalive),或者直接挂机(关闭连接)。
idle监测的功能
idle检测,只是负责诊断,诊断后,做出不同的行为,决定idle检测的最终用途。
- 发送keepalive:一般用来配合keepalive,减少keepalive消息。
- 直接关闭连接:快速释放损坏的、恶意的、很久不用的连接,让系统时刻保持最好的状态;简单粗暴,客户端可能需要重连
如何在Netty中开启TCP keepalive和idle检测
开启keepalive:
- Server端开其TCP keepalive
bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true)
bootstrap.childOption(NioChannelOption.of(StandardSocketOptions.SO_KEEPALIVE), true)
- 开启不同的idle check
ch.pipeline().addLast("idleCheckHandler", new IdleStateHandler(0, 20, 0, TimeUnit.SECONDS));
六:Netty的锁
同步问题三要素
- 原子性
- 可见性
- 有序性
锁的分类
- 对竞争的态度:乐观锁(java.util.concurrent包中的原子类)与悲观锁(synchronized)
- 等待锁的人是否公平而言:公平锁 new ReentrantLock(true)与非公平锁 new ReentrantLock()
- 是否可以共享:共享锁与独享锁:ReadWriteLock,其读锁是共享锁,其写锁是独享锁
Netty中锁的使用
synchronized method -> synchronized block
@Override
void init(Channel channel) throw Exception {
final Map<ChannelOption<?>, Object> options = options0();
synchronized (options) {
setChannelOptions(channel, options, logger);
}
final Map<AttributeKey<?>, Object> attrs = attrs0();
synchronized (attrs) {
for (Entry<AttributeKey<?>, Object> e : attrs.entrySet()) {
AttributeKey<Object> key = (AttributeKey<Object>) e.getKey();
channel.attr(key).set(e.getValue());
}
}
}
AtomicLong -> Volatile long + AtomicLongFieldUpdater
private static final AtomicLongFieldUpdater<ChannelOutboundBuffer> TOTAL_PENDING_SIZE_UPDATER = AtomicLongFieldUpdater.newUpdater(ChannelOutboundBuffer.class, "totalPendingSize");
private volatile long totalPendingSize;
private void incrementPendingOutboundBytes(long size, boolean invokerLater) {
if (size == 0) {
return;
}
long newWriteBufferSize = TOTAL_PENGING_SIZE_UPDATER.addAndGet(this, size);
if (newWriteBufferSize > channel.config().getWriteBufferHighWaterMark()) {
setUnwritable(invokeLater);
}
}
Atomic long VS long:
前者是一个对象,包含对象头(object header)以用来保存hashcode、lock等信息,32位系统占用8字节;64位系统占16字节,所以在64位系统情况下:
volatile long = 8 bytes
AtomicLong = 8 bytes(volatile long)+ 16bytes(对象头)+ 8 bytes(引用)= 32 bytes
至少节约24字节
结论:Atomic* objects -> Volatile primary type + static Atomic* FieldUpdater
记录内存分配字节数等功能用到的LongCounter
高并发时:java.util.concurrent.atomic.AtomicLong -> java.util.concurrent.atomic.LongAdder
结论:及时衡量、使用JDK最新的功能
Object.wait/notify -> CountDownLatch
七:ByteBuf
有一点我们需要知道的是,ByteBuf的jar包,是可以单独使用的。比如某个项目中有一个场景,需要处理某个自定义的协议,那么我们在解析协议时,就可以将接收到的将字节内容写入一个ByteBuf,然后从ByteBuf中慢慢的将内容读取出来。下面让我们用一个例子简单的了解下ByteBuf的使用。
ByteBuf的创建
要想使用ByteBuf,首先肯定是要创建一个ByteBuf,更确切的说法就是要申请一块内存,后续可以在这块内存中执行写入数据读取数据等等一系列的操作。
那么如何创建一个ByteBuf呢?Netty中设计了一个专门负责分配ByteBuf的接口:ByteBufAllocator。该接口有一个抽象子类和两个实现类,分别对应了用来分配池化的ByteBuf和非池化的ByteBuf。
有了Allocator之后,Netty又为我们提供了两个工具类:Pooled、Unpooled,分类用来分配池化的和未池化的ByteBuf,进一步简化了创建ByteBuf的步骤,只需要调用这两个工具类的静态方法即可。
不同的创建方法
我们以Unpooled类为例,查看Unpooled的源码可以发现,他为我们提供了许多创建ByteBuf的方法,但最终都是以下这几种,只是参数不一样而已:
// 在堆上分配一个ByteBuf,并指定初始容量和最大容量
public static ByteBuf buffer(int initialCapacity, int maxCapacity) {
return ALLOC.heapBuffer(initialCapacity, maxCapacity);
}
// 在堆外分配一个ByteBuf,并指定初始容量和最大容量
public static ByteBuf directBuffer(int initialCapacity, int maxCapacity) {
return ALLOC.directBuffer(initialCapacity, maxCapacity);
}
// 使用包装的方式,将一个byte[]包装成一个ByteBuf后返回
public static ByteBuf wrappedBuffer(byte[] array) {
if (array.length == 0) {
return EMPTY_BUFFER;
}
return new UnpooledHeapByteBuf(ALLOC, array, array.length);
}
// 返回一个组合ByteBuf,并指定组合的个数
public static CompositeByteBuf compositeBuffer(int maxNumComponents){
return new CompositeByteBuf(ALLOC, false, maxNumComponents);
}
其中包装方法除了上述这个方法之外,还有一些其他常用的包装方法,比如参数是一个ByteBuf的包装方法,比如参数是一个原生的ByteBuffer的包装方法,比如指定一个内存地址和大小的包装方法等等。
另外还有一些copy*开头的方法,实际是调用了buffer(int initialCapacity, int maxCapacity)或directBuffer(int initialCapacity, int maxCapacity)方法,然后将具体的内容write进生成的ByteBuf中返回。
以上所有的这些方法都实际通过一个叫ALLOC的静态变量进行了调用,来实现具体的ByteBuf的创建,而这个ALLOC实际是一个ByteBufAllocator:
private static final ByteBufAllocator
ALLOC = UnpooledByteBufAllocator.DEFAULT;
ByteBufAllocator是一个专门负责ByteBuf分配的接口,对应的Unpooled实现类就是UnpooledByteBufAllocator。在UnpooledByteBufAllocator类中可以看到UnpooledByteBufAllocator.DEFAULT变量是一个final类型的静态变量
/**
* Default instance which uses leak-detection for direct buffers.
* 默认的UnpooledByteBufAllocator实例,并且会对堆外内存进行泄漏检测
*/
public static final UnpooledByteBufAllocator
DEFAULT = new UnpooledByteBufAllocator(PlatformDependent.directBufferPreferred());
涉及的设计模式
ByteBuf和ByteBufAllocator之间是一种相辅相成的关系,ByteBufAllocator用来创建一个ByteBuf,而ByteBuf亦可以返回创建他的Allocator。ByteBuf和ByteBufAllocator之间是一种 抽象工厂模式
执行规则
- ByteBuf有读和写两个指针,用来标记“可读”、“可写”、“可丢弃”的字节
- 调用write*方法写入数据后,写指针将会向后移动
- 调用read*方法读取数据后,读指针将会向后移动
- 写入数据或读取数据时会检查是否有足够多的空间可以写入和是否有数据可以读取
- 写入数据之前,会进行容量检查,当剩余可写的容量小于需要写入的容量时,需要执行扩容操作
- 扩容时有一个4MB的阈值,需要扩容的容量小于阈值或大于阈值所对应的扩容逻辑不同
- clear等修改读写指针的方法,只会更改读写指针的值,并不会影响ByteBuf中已有的内容
- setZero等修改字节值的方法,只会修改对应字节的值,不会影响读写指针的值以及字节的可读写状态
八:ChannelHandler
前言
ChannelHandler是netty中的核心处理部分,我们使用netty的绝大部分代码都写在这部分,所以了解它的一些机制和特性是很有必要的。
Channel
Channel接口抽象了底层socket的一些状态属性以及调用方法
针对不同类型的socket提供不同的子类实现。
Channel生命周期
ChannelHandler
ChannelHandler用于处理Channel对应的事件ChannelHandler接口里面只定义了三个生命周期方法,我们主要实现它的子接口ChannelInboundHandler和ChannelOutboundHandler,为了便利,框架提供ChannelInboundHandlerAdapter,ChannelOutboundHandlerAdapter和ChannelDuplexHandler这三个适配类,在使用的时候只需要实现你关注的方法即可
ChannelHandler生命周期方法
ChannelHandler里面定义三个生命周期方法,分别会在当前ChannelHander加入ChannelHandlerContext中,从ChannelHandlerContext中移除,以及ChannelHandler回调方法出现异常时被回调
ChannelInboundHandler
这些回调方法被触发的时机
回调方法 | 触发时机 | client | server |
channelRegistered | 当前channel注册到EventLoop | true | true |
channelUnregistered | 当前channel当前channel从EventLoop取消注册 到EventLoop | true | true |
channelActive | 当前channel激活的时候 | true | true |
channelInactive | 当前channel不活跃的时候,也就是当前channel到了它生命周期末 | true | true |
channelRead | 当前channel从远端读取到数据 | true | true |
channelReadComplete | channel read消费完读取的数据的时候被触发 | true | true |
userEventTriggered | 用户事件触发的时候 | true | true |
channelWritabilityChanged | channel的写状态变化的时候触发 | true | true |
ChannelOutboundHandler
回调方法 | 触发时机 | client | server |
bind | bind操作执行前触发 | false | true |
connect | connect 操作执行前触发 | true | false |
disconnect | disconnect 操作执行前触发 | false | true |
close | close操作执行前触发 | false | true |
deregister | deregister操作执行前触发 | false | true |
read | read操作执行前触发 | true | true |
write | write操作执行前触发 | true | true |
flush | flush操作执行前触发 | true | true |
注意到一些回调方法有ChannelPromise这个参数,我们可以调用它的addListener注册监听,当回调方法所对应的操作完成后,会触发这个监听
下面这个代码,会在写操作完成后触发,完成操作包括成功和失败
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
ctx.write(msg,promise);
System.out.println("out write");
promise.addListener(new GenericFutureListener<Future<? super Void>>() {
@Override
public void operationComplete(Future<? super Void> future) throws Exception {
if(future.isSuccess()){
System.out.println("OK");
}
}
});
}
ChannelInboundHandler和ChannelOutboundHandler的区别
in和out的区别主要在于ChannelInboundHandler的channelRead和channelReadComplete回调和ChannelOutboundHandler的write和flush回调上,ChannelOutboundHandler的channelRead回调负责执行入栈数据的decode逻辑,ChannelOutboundHandler的write负责执行出站数据的encode工作。其他回调方法和具体触发逻辑有关,和in与out无关。
ChannelHandlerContext
每个ChannelHandler通过add方法加入到ChannelPipeline中去的时候,会创建一个对应的ChannelHandlerContext,并且绑定,ChannelPipeline实际维护的是ChannelHandlerContext 的关系
在DefaultChannelPipeline源码中可以看到会保存第一个ChannelHandlerContext以及最后一个ChannelHandlerContext的引用
final AbstractChannelHandlerContext head;
final AbstractChannelHandlerContext tail;
而在AbstractChannelHandlerContext源码中可以看到
volatile AbstractChannelHandlerContext next;
volatile AbstractChannelHandlerContext prev;
每个ChannelHandlerContext之间形成双向链表
ChannelPipeline
在Channel创建的时候,会同时创建ChannelPipeline
protected AbstractChannel(Channel parent) {
this.parent = parent;
id = newId();
unsafe = newUnsafe();
pipeline = newChannelPipeline();
}
在ChannelPipeline中也会持有Channel的引用
protected DefaultChannelPipeline newChannelPipeline() {
return new DefaultChannelPipeline(this);
}
ChannelPipeline会维护一个ChannelHandlerContext的双向链表
final AbstractChannelHandlerContext head;
final AbstractChannelHandlerContext tail;
链表的头尾有默认实现
protected DefaultChannelPipeline(Channel channel) {
this.channel = ObjectUtil.checkNotNull(channel, "channel");
succeededFuture = new SucceededChannelFuture(channel, null);
voidPromise = new VoidChannelPromise(channel, true);
tail = new TailContext(this);
head = new HeadContext(this);
head.next = tail;
tail.prev = head;
}
我们添加的自定义ChannelHandler会插入到head和tail之间,如果是ChannelInboundHandler的回调,根据插入的顺序从左向右进行链式调用,ChannelOutboundHandler则相反
具体关系如下,但是下图没有把默认的head和tail画出来,这两个ChannelHandler做的工作相当重要
上面的整条链式的调用是通过Channel接口的方法直接触发的,如果使用ChannelContextHandler的接口方法间接触发,链路会从ChannelContextHandler对应的ChannelHandler开始,而不是从头或尾开始
HeadContext
HeadContext实现了ChannelOutboundHandler,ChannelInboundHandler这两个接口
class HeadContext extends AbstractChannelHandlerContext
implements ChannelOutboundHandler, ChannelInboundHandler
因为在头部,所以说HeadContext中关于in和out的回调方法都会触发
关于ChannelInboundHandler,HeadContext的作用是进行一些前置操作,以及把事件传递到下一个ChannelHandlerContext的ChannelInboundHandler中去
看下其中channelRegistered的实现
public void channelRegistered(ChannelHandlerContext ctx) throws Exception {
invokeHandlerAddedIfNeeded();
ctx.fireChannelRegistered();
}
从语义上可以看出来在把这个事件传递给下一个ChannelHandler之前会回调ChannelHandler的handlerAdded方法而有关ChannelOutboundHandler接口的实现,会在链路的最后执行,看下write方法的实现
public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) throws Exception {
unsafe.write(msg, promise);
}
这边的unsafe接口封装了底层Channel的调用,之所以取名为unsafe,是不需要用户手动去调用这些方法。这个和阻塞原语的unsafe不是同一个。也就是说,当我们通过Channel接口执行write之后,会执行ChannelOutboundHandler链式调用,在链尾的HeadContext ,在通过unsafe回到对应Channel做相关调用
从netty Channel接口的实现就能论证这个
public ChannelFuture write(Object msg) {
return pipeline.write(msg);
}
TailContext
TailContext实现了ChannelInboundHandler接口,会在ChannelInboundHandler调用链最后执行,只要是对调用链完成处理的情况进行处理,看下channelRead实现
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
onUnhandledInboundMessage(msg);
}
如果我们自定义的最后一个ChannelInboundHandler,也把处理操作交给下一个ChannelHandler,那么就会到TailContext,在TailContext会提供一些默认处理
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);
}
}
比如channelRead中的onUnhandledInboundMessage方法,会把msg资源回收,防止内存泄露
强调一点的是,如果要执行整个链路,必须通过调用Channel方法触发,ChannelHandlerContext引用了ChannelPipeline,所以也能间接操作channel的方法,但是会从当前ChannelHandlerContext绑定的ChannelHandler作为起点开始,而不是ChannelHandlerContext的头和尾
这个特性在不需要调用整个链路的情况下可以使用,可以增加一些效率
上述组件之间的关系
- 每个Channel会绑定一个ChannelPipeline,ChannelPipeline中也会持有Channel的引用
- ChannelPipeline持有ChannelHandlerContext链路,保留ChannelHandlerContext的头尾节点指针
- 每个ChannelHandlerContext会对应一个ChannelHandler,也就相当于ChannelPipeline持有ChannelHandler链路
- ChannelHandlerContext同时也会持有ChannelPipeline引用,也就相当于持有Channel引用
- ChannelHandler链路会根据Handler的类型,分为InBound和OutBound两条链路