Netty 是由 JBOSS 提供的一个Java开源框架。Netty提供异步的、基于事件驱动的网络应用程序框架,用以快速开发高性能、高可靠性的网络 IO 程序,是目前最流行的NIO框架,Netty在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,知名的Elasticsearch 、Dubbo框架内部都采用了 Netty。
JavaNIO 的缺陷
NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、 ByteBuffer ServerSocketChannel、SocketChannel 等。
需要具备其它的额外技能做铺垫,例如熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的NIO程序。
可靠性能力补齐,开发工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大。
JDK NIO的BUG,例如epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,但是直到JDK1.7版本该问题仍旧存在,只不过该bug发生概率降低了一些而已,它并没有被根本解决。
Netty的优点从以下几个方面描述
使用简单:封装了NIO的很多细节,使用更简单;
功能强大:预置了多种编解码功能,支持多种主流协议;
定制能力强:可以通过ChannelHandler对通信框架进行灵活的扩展;
性能高:通过与其他业界主流的NIO框架对比,Netty的综合性能最优;
稳定:Netty修复了已经发现的NIO的bug,让开发人员可以专注于业务本身;
社区活跃:Netty是活跃的开源项目,版本迭代周期短,bug修复速度快。
Netty 的线程模型
Netty采用的线程模型是Reactor线程模型。
Reactor模型,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程。
Reactor模式也叫Dispatcher模式,即I/O多了复用统一监听事件,收到事件后分发(Dispatch)给某进程,是编写高性能网络服务器的必备技术之一。
Reactor模型中有2个关键组件
Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人。
Handlers处理程序执行I/O事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor通过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操作。
Netty的Reactor的具体实现:
Netty通过Reactor模型基于多路复用器接收并处理用户请求,内部实现了两个线程池,boss线程池和work线程池,其中boss线程池的线程负责处理请求的accept事件,当接收到accept事件的请求时,把对应的socket封装到一个NioSocketChannel 中,并交给work线程池,其中work线程池负责请求的read和write事件,由对应Handler处理。
其中MainReactor负责客户端的连接请求,并将请求转交给SubReactor。SubReactor负责相应通道的IO读写请求。非IO请求(具体逻辑处理)的任务则会直接写入队列,等待worker threads进行处理。
Bootstrap、ServerBootstrap:Bootstrap意思是引导,一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类。
Future、ChannelFuture:在Netty中所有的IO操作都是异步的,不能立刻得知消息是否被正确处理,但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过Future和ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。
Selector :Netty基于Selector对象实现I/O多路复用,通过 Selector,一个线程可以监听多个连接的Channel事件,当向一个Selector中注册Channel 后,Selector 内部的机制就可以自动不断地查询(select) 这些注册的Channel是否有已就绪的I/O事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel 。
ChannelHandler:是一个接口,处理I/O事件或拦截I/O操作,并将其转发到其ChannelPipeline中的下一个处理程序。ChannelHandler本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间可以继承它的子类,如下:
ChannelInboundHandler用于处理入站I/O事件;
ChannelOutboundHandler用于处理出站I/O操作。
或者使用以下适配器类:
ChannelInboundHandlerAdapter用于处理入站I/O事件;
ChannelOutboundHandlerAdapter用于处理出站I/O操作。
ChannelHandlerContext:保存Channel相关的所有上下文信息,同时关联一个ChannelHandler对象;
ChannelPipline:保存ChannelHandler的List,用于处理或拦截Channel的入站事件和出站操作。ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及Channel中各个的ChannelHandler如何相互交互。
read操作从head往tail,write操作从tail往head
JavaNIO提供了缓存容器(ByteBuffer),但是使用复杂。因此Netty引入缓存ButeBuf,一串字节数组构成。
ByteBuf的特性:
池化 - 可以重用池中ByteBuf实例,更节约内存,减少内存溢出的可能;
读写指针分离,不需要像 ByteBuffer 一样切换读写模式;
可以自动扩容;
支持链式调用,使用更流畅;
很多地方体现零拷贝,例如 slice、duplicate、CompositeByteBuf。
ByteBuf提供了两个指针变量来支持顺序读取和写入操作----读取操作的readerIndex和写入操作的writerIndex。
下图显示了如何通过两个指针将缓冲区分为三个区域:
| 可丢弃字节 | 可读字节 | 可写入字节 | (内容) |
0 <= readerIndex <= writerIndex <= 容量可读字节 (实际内容)
通过调用discardReadBytes()来丢弃读取的字节以回收未使用的区域。可以使用mark和reset重置readIndex索引和writerIndex索引,来重复读取目的。
扩容规则:如果写入后数据大小未超过 512,则选择下一个 16 的整数倍,例如写入后大小为 12 ,则扩容后 capacity 是 16;如果写入后数据大小超过 512,则选择下一个 2^n-1之前的容量,扩容不能超过max capacity会报错。
ByteBuf根据内存类型分类:基于直接内存的ByteBuf(默认)和基于堆内存的ByteBuf。
ByteBuf buffer = ByteBufAllocator.DEFAULT.heapBuffer(10);//基于堆内存
ByteBuf buffer = ByteBufAllocator.DEFAULT.directBuffer(10);//基于堆外内存
ByteBuf实现池化:没有池化,每次都创建新的 ByteBuf 实例,这个操作对直接内存代价昂贵,就算是堆内存,也会增加 GC 压力。
通过重用 ByteBuf 实例,并且采用了与 jemalloc 类似的内存分配算法提升分配效率,高并发时,池化功能更节约内存,减少内存溢出的可能。池化功能是否开启,可以通过下面的系统环境变量来设置-Dio.netty.allocator.type={unpooled|pooled} //4.1以后对于非安卓平台默认池化,安卓平台非池化。
对于堆外内存的回收
Netty 这里采用了引用计数法来控制回收内存,每个 ByteBuf 都实现了 ReferenceCounted 接口,每个ByteBuf对象的初始计数为 1:
调用release方法计数减 1,如果计数为 0,ByteBuf内存被回收;
调用retain方法计数加 1,表示调用者没用完之前,其它handler即使调用了release也不会造成回收;
当计数为 0 时,底层内存会被回收,这时即使 ByteBuf 对象还在,其各个方法均无法正常使用。
本期分享就到这里,关注我们定期更新更多精彩内容~
﹀
﹀
﹀