Netty简介
- Netty是高性能、异步事件驱动的NIO框架 基于Java NIO提供的API实现
- 提供了对TCP、UDP和文件传输的支持
- Netty所有IO操作都是异步非阻塞的 通过Future-Lisener机制 用户可以方便的主动获取或者通过通知机制获得IO操作结果
- 广泛应用互联网领域、大数据分布式计算领域、游戏行业、通信行业
Netty线程模型
Netty结合Java NIO的Selector和Reactor模式设计了高效的线程模型
Reactor模式
基于事件驱动
- 有一个或多个并发输入源
- 一个Server Handler
- 多个Request Handlers
每当一个事件输入到Service Handler之后,该Service Handler会主动根据不同的Evnent类型将其分发给对应的Request Handler来处理
Reactor模式实现
Reactor单线程模型
1、一个线程可以独立处理所有的IO操作
2、既负责多路分离套接字又Accept新连接又并分发请求到处理链中
3、一些小容量应用场景,可以使用到单线程模型
但对于高负载,大并发的应用却不合适 原因是
- 当一个NIO线程同时处理成百上千的链路,性能上无法支撑,即使NIO线程的CPU负荷达到100%,也无法完全处理消息
- 当NIO线程负载过重后,处理速度会变慢,会导致大量客户端连接超时,超时之后往往会重发,更加重了NIO线程的负载。
- 可靠性低,一个线程意外死循环,会导致整个通信系统不可用
Reactor多线程模型
该模型可以解决上述问题
该模型在处理链部分采用了多线程(线程池)
绝大多数场景下,该模型都能满足性能需求
但在一些特殊的应用场景下,如服务器会对客户端的握手消息进行安全认证
这类场景下,单独的一个Acceptor线程可能会存在性能不足的问题
为了解决这些问题,产生了第三种Reactor线程模型
Reactor主从模型
a、Reactor分成两部分 mainReactor负责监听server socket、accept新连接
并将建立的socket分派给subReactor
b、subReactor负责多路分离已连接的socket
c、worker线程池完成读写网络数据,对业务处理功能
d、subReactor个数上可与CPU个数等同
弊端
- 当系统在运行过程中,频繁的进行线程上下文切换,会带来额外的性能损耗
- 要考虑多线程安全问题
Netty模型
Netty线程模型去掉了Reactor主从模型中线程池
为了解决上述问题
Netty采用了串行化设计理念
从消息的读取、编码以及后续Handler的执行
始终都由IO线程EventLoop负责
这就意外着整个流程不会进行线程上下文的切换
数据也不会面临被并发修改的风险
核心组件
Selector
Selector即为NIO中提供的SelectableChannel多路复用器
充当着demultiplexer的角色
EventLoopGroup/EventLoop
- EventLoopGroup是一组EventLoop的抽象 EventLoopGroup提供next接口
- 从一组EventLoop里面按照一定规则获取其中一个EventLoop来处理任务
- 对于EventLoopGroup这里需要了解的是在Netty中 在Netty服务器编程中需要BossEventLoopGroup和WorkerEventLoopGroup两个EventLoopGroup来进行工作
- 通常一个服务端口即一个ServerSocketChannel对应一个Selector和一个EventLoop线程,也就是说BossEventLoopGroup的线程数参数为1
- BossEventLoop负责接收客户端的连接并将SocketChannel交给WorkerEventLoopGroup来进行IO处理
ChannelPipeline
- ChannelPipeline的默认实现是DefaultChannelPipeline,
- DefaultChannelPipeline本身维护着一个用户不可见的tail和head的ChannelHandler,他们分别位于链表队列的头部和尾部。tail在更上层的部分,而head在靠近网络层的方向
- 在Netty中关于ChannelHandler有两个重要的接口,ChannelInBoundHandler和ChannelOutBoundHandler
- inbound可以理解为网络数据从外部流向系统内部而outbound可以理解为网络数据从系统内部流向系统外部
- 用户实现的ChannelHandler可以根据需要实现其中一个或多个接口,将其放入Pipeline中的链表队列中,ChannelPipeline会根据不同的IO事件类型来找到相应的Handler来处理,同时链表队列是责任链模式的一种变种,自上而下或自下而上所有满足事件关联的Handler都会对事件进行处理
- ChannelInBoundHandler对从客户端发往服务器的报文进行处理,一般用来执行半包/粘包,解码,读取数据,业务处理等
- ChannelOutBoundHandler对从服务器发往客户端的报文进行处理,一般用来进行编码,发送报文到客户端
Buffer
- ByteBuf读写指针
在ByteBuffer中,读写指针都是position
而在ByteBuf中,读写指针分别为readerIndex和writerIndex
直观看上去ByteBuffer仅用了一个指针就实现了两个指针的功能,节省了变量
但是当对于ByteBuffer的读写状态切换的时候必须要调用flip方法而当下一次写之前,必须要将Buffe中的内容读完,再调用clear方法
每次读之前调用flip,写之前调用clear,这样无疑给开发带来了繁琐的步骤
而且内容没有读完是不能写的,这样非常不灵活
相比之下我们看看ByteBuf
读的时候仅仅依赖readerIndex指针,写的时候仅仅依赖writerIndex指针
不需每次读写之前调用对应的方法
而且没有必须一次读完的限制
- 零拷贝
1、Netty的接收和发送ByteBuffer采用DIRECT BUFFERS
使用堆外直接内存进行Socket读写,不需要进行字节缓冲区的二次拷贝
如果使用传统的堆内存(HEAP BUFFERS)进行Socket读写
JVM会将堆内存Buffer拷贝一份到直接内存中,然后才写入Socket中
相比于堆外直接内存,消息在发送过程中多了一次缓冲区的内存拷贝。
2、Netty提供了组合Buffer对象,可以聚合多个ByteBuffer对象
用户可以像操作一个Buffer那样方便的对组合Buffer进行操作
避免了传统通过内存拷贝的方式将几个小Buffer合并成一个大的Buffer
Netty的文件传输采用了transferTo方法
它可以直接将文件缓冲区的数据发送到目标Channel
避免了传统通过循环write方式导致的内存拷贝问题。
- 引用计数与池化技术
在Netty中,每个被申请的Buffer对于Netty来说都可能是很宝贵的资源
因此为了获得对于内存的申请与回收更多的控制权
Netty自己根据引用计数法去实现了内存的管理
Netty对于Buffer的使用都是基于直接内存(DirectBuffer)实现的
大大提高I/O操作的效率
然而DirectBuffer和HeapBuffer相比之下除了I/O操作效率高之外还有一个天生的缺点
即对于DirectBuffer的申请相比HeapBuffer效率更低
因此Netty结合引用计数实现了PolledBuffer,即池化的用法
当引用计数等于0的时候 Netty将Buffer回收致池中
在下一次申请Buffer的没某个时刻会被复用
总结
Netty其实本质上就是Reactor模式的实现Selector作为多路复用器,EventLoop作为转发器,Pipeline作为事件处理器
但是和一般的Reactor不同的是,Netty使用串行化实现,并在Pipeline中使用了责任链模式