浅谈Netty相关概念
Posted 默辨
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅谈Netty相关概念相关的知识,希望对你有一定的参考价值。
文章目录
一、Java的NIO
Java的NIO机制,使用的时多路复用IO模型。后文在提及NIO时,默认指是Java的NIO,而非网络模型的NIO
1、NIO整体架构
NIO 有三大核心组件: Channel(通道), Buffer(缓冲区),Selector(多路复用器)
1、channel 类似于流,每个 channel 对应一个 buffer缓冲区,buffer 底层就是个数组
2、channel 会注册到 selector 上,由 selector 根据 channel 读写事件的发生将其交由某个空闲的线程处理
3、NIO 的 Buffer 和 channel 都是既可以读也可以写
2、select、poll、epoll比较
NIO底层在JDK1.4版本是用linux的内核函数select()或poll()来实现,跟上面的Nioserver代码类似,selector每次都会轮询所有的sockchannel看下哪个channel有读写事件,有的话就处理,没有就继续遍历,JDK1.5开始引入了epoll基于事件响应机制来优化NIO。
I/O多路复用底层主要用的Linux 内核函数(select,poll,epoll)来实现,windows不支持epoll实现,windows底层是基于winsock2的select函数实现的(不开源)
select | poll | epoll(jdk 1.5及以上) | |
---|---|---|---|
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | 哈希表 |
IO效率 | 每次调用都进行线性遍历,时间复杂度为O(n) | 每次调用都进行线性遍历,时间复杂度为O(n) | 事件通知方式,每当有IO事件就绪,系统注册的回调函数就会被调用,时间复杂度O(1) |
最大连接 | 有上限 | 无上限 | 无上限 |
3、NIO核心方法讲解
NIO代码里这几个方法非常重要:
Selector.open(); //创建多路复用器
socketChannel.register(selector, SelectionKey.OP_READ); //将channel注册到多路复用器上
selector.select() //阻塞等待需要处理的事件发生
NIO整个调用流程就是Java调用了操作系统的内核函数来创建Socket,获取到Socket的文件描述符,再创建一个Selector对象,对应操作系统的Epoll描述符,将获取到的Socket连接的文件描述符的事件绑定到Selector对应的Epoll文件描述符上,进行事件的异步通知,这样就实现了使用一条线程,并且不需要太多的无效的遍历,将事件处理交给了操作系统内核(操作系统中断程序实现),大大提高了效率。
二、Netty分析
NIO 的类库和 API 繁杂, 使用麻烦: 需要熟练掌握Selector、 ServerSocketChannel、 SocketChannel、 ByteBuffer等。
开发工作量和难度都非常大: 例如客户端面临断线重连、 网络闪断、心跳处理、半包读写、 网络拥塞和异常流的处理等等。
Netty 对 JDK 自带的 NIO 的 API 进行了良好的封装,解决了上述问题。且Netty拥有高性能、 吞吐量更高,延迟更低,减少资源消耗,最小化不必要的内存复制等优点。
Netty 现在都在用的是4.x,5.x版本已经废弃,Netty 4.x 需要JDK 6以上版本支持
1、Netty整体架构
模型解释:
1、Netty 抽象出两组线程池BossGroup和WorkerGroup,BossGroup专门负责接收客户端的连接, WorkerGroup专门负责网络的读写
2、BossGroup和WorkerGroup类型都是NioEventLoopGroup
3、NioEventLoopGroup 相当于一个事件循环线程组, 这个组中含有多个事件循环线程 , 每一个事件循环线程是NioEventLoop
4、每个NioEventLoop都有一个selector , 用于监听注册在其上的socketChannel的网络通讯
5、每个Boss NioEventLoop线程内部循环执行的步骤为:
- 处理accept事件 , 与client 建立连接 , 生成 NioSocketChannel
- 将NioSocketChannel注册到某个worker NIOEventLoop上的selector
- 处理任务队列的任务 , 即runAllTasks
6、每个worker NIOEventLoop线程循环执行的步骤:
- 轮询注册到自己selector上的所有NioSocketChannel 的read, write事件
- 处理 I/O 事件, 即read , write 事件, 在对应NioSocketChannel 处理业务
- runAllTasks处理任务队列TaskQueue的任务 ,一些耗时的业务处理一般可以放入TaskQueue中慢慢处理,这样不影响数据在 pipeline 中的流动处理
7、每个worker NIOEventLoop处理NioSocketChannel业务时,会使用 pipeline (管道),管道中维护了很多 handler 处理器用来处理 channel 中的数据
看不懂的小伙伴,可以先混个眼熟,由于这在NIO的代码中都有对应的代码逻辑,所以想搞清楚这些概念的小伙伴可以参考我之前的文章:浅谈Netty中ServerBootstrap服务端源码(含bind全流程)。在对Netty源码有了了解之后,上面的这些文字,对你来说就是轻轻松松。
2、Netty相关模块介绍
1、Bootstrap、ServerBootstrap
Bootstrap意思是引导,一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Nett 程序,串联各个组件,Netty中 Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类。
2、Future,ChannelFuture
正如前面介绍,在 Netty 中所有的 IO 操作都是异步的,不能立刻得知消息是否被正确处理。
但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过Future和ChannelFutures,可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。
3、Channel
Netty 网络通信的组件,能够用于执行网络 I/O 操作。Channel 为用户提供:
- 当前网络连接的通道的状态(例如是否打开?是否已连接?)
- 网络连接的配置参数 (例如接收缓冲区大小)
- 提供异步的网络 I/O 操作(如建立连接,读写,绑定端口),异步调用意味着任何 I/O 调用都将立即返回,并且不保证在调用结束时所请求的 I/O 操作已完成。
- 调用立即返回一个 ChannelFuture 实例,通过注册监听器到 ChannelFuture 上,可以 I/O 操作成功、失败或取消时回调通知调用方。
- 支持关联 I/O 操作与对应的处理程序。
不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应。
下面是一些常用的 Channel 类型,这些通道涵盖了UDP和TCP网络IO以及文件 IO:
-
NioSocketChannel,异步的客户端 TCP Socket 连接。
-
NioServerSocketChannel,异步的服务器端 TCP Socket 连接。
-
NioDatagramChannel,异步的 UDP 连接。
-
NioSctpChannel,异步的客户端 Sctp 连接。
-
NioSctpServerChannel,异步的 Sctp 服务器端连接。
4、Selector
Netty 基于 Selector 对象实现 I/O 多路复用,通过 Selector 一个线程可以监听多个连接的 Channel 事件。
当向一个 Selector 中注册 Channel 后,Selector 内部的机制就可以自动不断地查询(Select) 这些注册的 Channel 是否有已就绪的 I/O 事件(例如可读,可写,网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel 。
5、NioEventLoop
NioEventLoop 中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用 NioEventLoop 的 run 方法,执行 I/O 任务和非 I/O 任务:
I/O 任务,即 selectionKey 中 ready 的事件,如 accept、connect、read、write等,由 processSelectedKeys 方法触发。
非IO任务,添加到 taskQueue 中的任务,如register0、bind0等任务,由runAllTasks方法触发。
6、NioEventLoopGroup
NioEventLoopGroup,主要管理 eventLoop 的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个 Channel上的事件,而一个 Channel只对应于一个线程。
7、ChannelHandler
ChannelHandler 是一个接口,处理 I/O 事件或拦截 I/O 操作,并将其转发到其 ChannelPipeline(业务处理链)中的下一个处理程序。
ChannelHandler 本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类:ChannelInboundHandler 用于处理入站 I/O 事件, ChannelOutboundHandler 用于处理出站 I/O 操作。或者使用以下适配器类:ChannelInboundHandlerAdapter 用于处理入站 I/O 事件。 ChannelOutboundHandlerAdapter 用于处理出站 I/O 操作。
例如,实现ChannelInboundHandler接口(或ChannelInboundHandlerAdapter),你就可以接收入站事件和数据,这些数据随后会被你的应用程序的业务逻辑处理。当你要给连接的客户端发送响应时,也可以从ChannelInboundHandler冲刷数据。你的业务逻辑通常写在一个或者多个ChannelInboundHandler中。ChannelOutboundHandler原理一样,只不过它是用来处理出站数据的。
8、ChannelHandlerContext
保存 Channel 相关的所有上下文信息,同时关联一个 ChannelHandler 对象。
9、ChannelPipline
保存 ChannelHandler 的 List,用于处理或拦截 Channel 的入站事件和出站操作。
ChannelPipeline 实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及 Channel 中各个的 ChannelHandler 如何相互交互。
在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应,它们的组成关系如下:
一个 Channel 包含了一个 ChannelPipeline,而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表,并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。
read事件(入站事件)和write事件(出站事件)在一个双向链表中,入站事件会从链表 head 往后传递到最后一个入站的 handler,出站事件会从链表 tail 往前传递到最前一个出站的 handler,两种类型的 handler 互不干扰。
ChannelPipeline提供了ChannelHandler链的容器。以客户端应用程序为例,如果事件的运动方向是从客户端到服务端的,那么我们称这些事件为出站的,即客户端发送给服务端的数据会通过pipeline中的一系列ChannelOutboundHandler(ChannelOutboundHandler调用是从tail到head方向逐个调用每个handler的逻辑),并被这些Handler处理,反之则称为入站的,入站只调用pipeline里的ChannelInboundHandler逻辑(ChannelInboundHandler调用是从head到tail方向逐个调用每个handler的逻辑)。
3、Netty编码解码器
本人在搭建demo的时候,就因为忘记配置编解码器,浪费了很多时间。客户端和服务端之间只有传递的消息格式统一了,才能够进行正常的消息收发
当你通过Netty发送或者接受一个消息的时候,就将会发生一次数据转换。入站消息会被解码:从字节转换为另一种格式(比如java对象);如果是出站消息,它会被编码成字节。
Netty提供了一系列实用的编码解码器,他们都实现了ChannelInboundHadnler或者ChannelOutboundHandler接口。在这些类中,channelRead方法已经被重写了。以入站为例,对于每个从入站Channel读取的消息,这个方法会被调用。随后,它将调用由已知解码器所提供的decode()方法进行解码,并将已经解码的字节转发给ChannelPipeline中的下一个ChannelInboundHandler。
Netty提供了很多编解码器,比如编解码字符串的StringEncoder和StringDecoder,编解码对象的ObjectEncoder和ObjectDecoder等。
如果要实现高效的编解码可以用protobuf,但是protobuf需要维护大量的proto文件比较麻烦,现在一般可以使用protostuff。
4、Netty粘包拆包
其实这一个点不仅在Netty中有,但凡是涉及到网络传输的都会有这个问题
比如浏览器中的Content-Length、Chunked都和它有关简单测试Content-Length和Chunked两种不同的数据传输方式
TCP是一个流协议,就是没有界限的一长串二进制数据。TCP作为传输层协议并不不了解上层业务数据的具体含义,它会根据TCP缓冲区的实际情况进行数据包的划分,所以在业务上认为是一个完整的包,可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。面向流的通信是无消息保护边界的。
解决方案:
1)消息定长度,传输的数据大小固定长度,例如每段的长度固定为100字节,如果不够空位补空格
2)在数据包尾部添加特殊分隔符,比如下划线,中划线等,这种方法简单易行,但选择分隔符的时候一定要注意每条数据的内部一定不能出现分隔符。
3)发送长度:发送每条数据的时候,将数据的长度一并发送,比如可以选择每条数据的前4位是数据的长度,应用层处理时可以根据长度来判断每条数据的开始和结束。
Netty提供了多个解码器,可以进行分包的操作,如下:
- LineBasedFrameDecoder (回车换行分包)
- DelimiterBasedFrameDecoder(特殊分隔符分包)
- FixedLengthFrameDecoder(固定长度报文来分包)
5、Netty心跳检测机制
Netty中实现心跳检测机制,直接使用IdleStateHandler即可完成(还需要配合对应的处理后续发生超时后的handler)
1、initialize
在连接建立的时候,会触发channelActive方法,然后就会调用到initialize方法
2、schedule
当设置的读超时时间大于0,就会创建对应的读心跳检测延时任务用来检测心跳
最终会调用到AbstractScheduledEventExecutor类的schedule方法,添加到一个PriorityQueue优先级队列中,用于后期完成任务的调度。
3、ReaderIdleTimeoutTask
任务添加到了队列中,后期会执行对应的任务。该任务的主要作用为判断设置的超时时间和接收任务的时间之间的关系,使用当前时间减去收到消息的时间。
- 如果没有超时,再次调用自己:再次向任务中添加任务,已达到不断循环监听任务的效果(这种写法在Redisson中也有体现,使用递归的方式处理定时任务,能够更加灵活的处理延迟时间)
- 如果结果小于0,说明超时:会通过channelIdle方法触发对应的超时处理逻辑(需要自己新增handler去实现)
4、invokeUserEventTriggered
会调用我们服务类中自己添加的handler中的userEventTriggered方法,我们便可以在该方法中完成对应的超时处理逻辑。
即实现ChannelInboundHandler接口,重写对应的userEventTriggered方法,并且在ch.pipeline().addLast()方法中完成对应handler的添加,一定要放在
以上是关于浅谈Netty相关概念的主要内容,如果未能解决你的问题,请参考以下文章