这篇长文章主要是用来帮助我们深切了解netty中常用的一些组件,以及这些组件之间的存在关系
- EventLoop 事件循环 ----控制流,多线程处理,并发;
- Channel 通道 ----Socket套接字
- ChannelPipeline 管道
- ChannelHandler 通道适配器
- ChannelHandlerContext 通道适配器上下文
- Bootstrap (处理客户端)和 ServerBootstrap(处理服务端)
- ByteBuf 字节容器
一、EventLoop
EventLoop定义了Netty的核心对象,用于处理IO事件,多线程模型、并发,EventLoop, channel, Thread 以及 EventLoopGroup 之间的关系如下图:
这几个组件之间的关系总结下就是:
1、一个EventLoopGroup包含一个或者多个EventLoop;
2、一个EventLoop在它的生命周期内只和一个Thread绑定;
3、所有有EventLoop处理的I/O事件都将在它专有的Thread上被处理;
4、一个Channel在它的生命周期内只注册于一个EventLoop;
5、一个EventLoop可能会被分配给一个或多个Channel;
其实我们可以简单的把EventLoop及其相关的实现NioEventLoop、NioEventLoopGroup等理解为netty针对我们网络编程时创建的多线程进行了封装和优化,构建了自己的线程模型。
EventLoop 的线程管理
如果当前调用的线程正是支撑 EventLoop 的线程,那么所提交的代码将会直接执行(这里可以调用 inEventLoop 或者是inEventLoop(Thread thread ) 方法),否则,EventLoop 将调度该任务以便稍后执行,并将它放入到内部队列中去。当 EventLoop 下次处理他的事件时,它将会执行队列中的那些任务/事件。这也解释了任何的 Thread 是如何与 Channel 直接交互而无需在 ChannelHandler 中进行额外同步的
注:每个 EventLoop 都有它自己的任务队列,独立于任何其他的 EventLoop
异步传输的实现:异步传输使用了少量的 EventLoop ,而且在当前的线程模型中,它可能被多个 channel 所共享。这样就尽可能少的Thread 来支撑大量的 Channel
EventLoopGroup 为每个新创建的Channel 分配一个EventLoop。相同的 EventLoop 可能会被分配给多个Channel 。一旦一个Channel 分配给一个 EventLoop, 它将在你整个生命周期中都使用这个 EventLoop (以及其关联的 Thread ),这样就可以解释为什么 ChannelHandler 是线程安全的。
注:对于BIO 这样的传输方式可能和 NIO 有所不同,每个 Channel 都将分配给一个 EventLoop (以及对应的 Thread )。其模型是:一个 EventLoopGroup 对应多一个 EventLoop ,一个 EventLoop 对应一个 Channel 。
注, 因为 EventLoop 既需要执行 IO 操作, 又需要执行 task, 因此我们在调用 EventLoop.execute 方法提交任务时, 不要提交耗时任务, 更不能提交一些会造成阻塞的任务, 不然会导致我们的 IO 线程得不到调度, 影响整个程序的并发量.
二、Channel
在netty里,Channel
是通讯的载体,而ChannelHandler
负责Channel中的逻辑处理。
用户可以有以下四类操作
1.查询channel状态
2.配置channel参数
3.进行channel支持的I/O操作(read,write,connect,bind)
4.获取channel对应的ChannelPipeline, 从而自定义处理I/O事件和其他请求
Channel的生命周期
ChannelUnregistered: Channel已经被创建,但是还未注册到EventLoop;
ChannelRegistered :Channel 已经被注册到EventLoop;
ChannelActive :Channel 处于活跃状态(已经连接到远程节点),可以进行接收和发送数据
ChannelInactive: Channel 没有连接到远程节点
Channel中两个重要的api
1.EventLoop eventLoop();
2.ChannelPipeline pipeline();
大多数情况下channel 都有一个与之关联的eventLoop ,如果没有说明还没有注册到eventLoop 上,
NIO 中通过注册channel 通过注册到 selector 上而与一个 NioEventLoop 关联,
当调用 eventLoop() 方法时,与之关联的 NioEventLoop 会被返回。
pipeline() 返回了当前 channel 所对应 ChannelPipeline() 对象,
这个可以看到 channel,channelPipeline, channelHandler 三者之间的关系,
一个 channel 对应一个channelPipeline ,一个 channelPipeline 对应多个channelHandler
Channel关于状态查询的API
boolean isOpen(); // 是否开放 true 表示可用,false表示已经关闭,不可用
boolean isRegistered(); // 是否注册到一个EventLoopboolean
isActive(); // 是否激活 serverSocketChannel 表示已经绑定到端口
boolean isWritable(); // 是否可写
正常的一个channel的状态流转
1、REGISTERED->CONNECT/BIND->ACTIVE->CLOSE->INACTIVE->UNREGISTERED
2、REGISTERED->ACTIVE->CLOSE->INACTIVE->UNREGISTERED
第一种是服务端用于绑定的channel,或者客户端发起绑定的channel,第二种是服务端接受的SocketChannel
注:channel之间是有等级的,如果一个channel是由另外一个channel创建的,那么他们之间存在父子关系
比如,ServerSocketChannel 的accept() 方法返回的SocketChannel ,其父channel 就是ServerSocketChannel
如果一个channel不再使用的时候,需要调用其close()方法或者close(参数) 方法关闭,进行资源释放
三、ChannelPipeline和ChannelHandler和ChannelHandlerContext
Netty中Channel、ChannelPipeline、ChannelHandler、ChannelHandlerContext之间的关系?
1.每一个Channel被创建,就会生成对应的一个ChannelPipeline和它绑定。
2.ChannelPipeline为ChannelHandler链提供了容器,包含了一个处理该Channel消息的ChannelHandler链。
3.当ChannelHandler被注册到ChannelPipeline中就会生成一个对应的ChannelHandlerContext,和该ChannelHandler进行绑定
4.一个ChannelHandler可以注册到多个ChannelPipeline。所以,一个ChannelHandler可以绑定多个ChannelHandlerContext。
不过,这样的ChannelHandler必须标注@Sharable注解,保证它的线程安全性,否则试图将它注册到多个ChannelHandlerPipeline中时将会抛出异常。
ChannelHandler负责Channel中的逻辑处理,Netty中可以注册多个handler,以链式的方式进行处理,根据继承接口的不同,实现的顺序也不同。
1、ChannelInboundHandler:处理接收的信息。一般用来执行解码/读取客户端数据/业务处理等。如ByteToMessageDecoder;
2、ChannelOutboundHandler:对发送的信息进行处理,一般用来进行编码、发送报文到客户端。如MessageToByteEncoder;
ChannelHandler 在ChannlePipeline 的生命周期
1.一个 ChannelInitializer 的实现被注册到 ServerBootstrop 当中
2.当 ChannelInitializer.initChannel() 方法被调用时,ChannelInitializer将在 ChannelPipeline 中安装一组自定义ChannelHandle
3.ChannelInitializer 将他自己从 ChannelPipeline 中移除;
ChannelHandlerContext 主要用来管理它关联的ChannelHandler和在同一ChannlePipeline中其他的ChannelHandler之间的交互。
注:Netty 中发送消息有两种方式,可以直接写入到 Channel 中,也可以写入到和 ChannelHandler 绑定的ChannelHandlerContext 中。前者会使消息从 ChannelPipeline 当中尾部开始移动,后者会导致消息从ChannelPipeline 中的下一个ChannelHandler 中移动。
在Netty中,ChannelEvent
是数据或者状态的载体,例如传输的数据对应MessageEvent
,状态的改变对应ChannelStateEvent
。当对Channel进行操作时,会产生一个ChannelEvent,并发送到ChannelPipeline
。ChannelPipeline会选择一个ChannelHandler进行处理。这个ChannelHandler处理之后,可能会产生新的ChannelEvent,并流转到下一个ChannelHandler。
例如,一个数据最开始是一个MessageEvent
,它附带了一个未解码的原始二进制消息ChannelBuffer
,然后某个Handler将其解码成了一个数据对象,并生成了一个新的MessageEvent
,并传递给下一步进行处理。到了这里,可以看到,其实Channel的核心流程位于ChannelPipeline
中
四:Bootstrap 和 ServerBootstrap(引导类)
Bootstrap 和 ServerBootstrap 这两个引导类分别是用来处理客户端和服务端的信息,服务器端的引导一个父 Channel 用来接收客户端的连接,一个子 Channel 用来处理客户端和服务器端之间的通信,客户端则只需要一个单独的、没有父 Channel 的 Channel 来去处理所有的网络交互(或者是无连接的传输协议,如 UDP)
Bootstrap:这个类主要是为客户端和无连接协议的应用程序创建 Channel, 创建步骤如下:
Bootstrap 在 bind() 方法被调用之后创建一个新的 Channel
Bootstrap 的 connect() 方法被调用后,也会创建一个新的 Channel
ServerBootstrap:
对于引导服务器 bind() 方法调用时,将会创建一个 ServerChannel
当连接被接受时,ServerChannel 将会创建一个新的子 Channel
ServerChannel 和子 Channel 之间是一对多的关系
五:ByteBuf
ByteBuf 的四个逻辑部分
ByteBuf 是一个字节容器,内部是一个字节数组,从逻辑上来分,字节容器内部可分为四个部分:
第一个部分是已经丢弃的字节,这部分数据是无效的;
第二部分是可读字节,这部分数据是 ByteBuf 的主体数据, 从 ByteBuf 里面读取的数据都来自这一部分;
第三部分的数据是可写字节,所有写到 ByteBuf 的数据都会写到这一段。
第四部分的字节,表示的是该 ByteBuf 最多还能扩容的大小。
ByteBuf 的三个指针
ByteBuf 通过三个整型的指针(index),有效地区分可读数据和可写数据,使得读写之间相互没有冲突。
这个三个指针分别是:
1.readerIndex(读指针)
2.writerIndex(写指针)
3.maxCapacity(最大容量)
ByteBuf 的三组方法
从三个维度三大系列,介绍ByteBuf 的常用 API 方法。
第一组:容量系列
方法 一:capacity()
表示 ByteBuf 的容量,包括丢弃的字节数、可读字节数、可写字节数。
方法二:maxCapacity()
表示 ByteBuf 底层最大能够占用的最大字节数。当向ByteBuf 中写数据时,若容量不足则进行扩容,直到maxCapacity。
第二组:写入系列
方法一:isWritable()
表示 ByteBuf 是否可写。如果 capacity() 容量大于 writerIndex 指针的位置 ,则表示可写。否则为不可写。
isWritable()的源码,也是很简单的。具体如下:
public boolean isWritable() { return this.capacity() > this.writerIndex; }
注意:如果 isWritable() 返回 false,并不代表不能往 ByteBuf 中写数据。 如果Netty发现往 ByteBuf 中写数据写不进去,则会自动扩容 ByteBuf。
方法二:writableBytes()
返回表示 ByteBuf 当前可写入的字节数,它的值等于 capacity()- writerIndex。
方法三:maxWritableBytes()
返回可写的最大字节数,它的值等于 maxCapacity-writerIndex 。
方法四:writeBytes(byte[] src)
把字节数组 src 里面的数据全部写到 ByteBuf。这个是最为常用的一个方法。
第三组:读取系列
方法一:isReadable()
表示 ByteBuf 是否可读。如果 writerIndex 指针的值大于 readerIndex 指针的值 ,则表示可读。否则为不可写。
isReadable()的源码,也是很简单的。具体如下
public boolean isReadable() { return this.writerIndex > this.readerIndex; }
方法二:readableBytes()
返回表示 ByteBuf 当前可读取的字节数,它的值等于 writerIndex - readerIndex 。
网络数据的操作归根到底是字节的操作,Netty提供的ByteBuf作为一个强大高效的字节容器,提供了更加丰富的API用于字节的操作,同时保持了卓越的功能性和灵活性;
最后奉上实际的通信流程图: