Netty 原理解析与实战开发(二)
Netty 原理解析与开发实战
八、ChannelHandler
8.1 ChannelHandler介绍
我们对数据的处理都是在ChannelHandler中完成的,Netty提供了众多ChannelHandler的实现类来帮助我们实现一些网络编程中通用功能,比如最常用的心跳检测、数据编解码等。
Netty中的ChannelHandler分为两类,一类处理入站数据,都实现了ChannelInboundHandler接口,一类处理出站数据,都实现了ChannelOutboundHandler接口。前面章节所说的编解码器是一类比较特殊的Handler,解码器负责将入站byte数据解码为对象,编码器负责将出站对象数据编码为byte数据。
ChannelHandler在进行数据处理的时候有匹配机制,意思就说如果当前Handler所注册的类型不能够处理,则直接交给下一个处理器。具体的匹配方法大致是:在运行时获取到类的模版参数列表,获得运行时模版类的Class对象,然后当要处理数据时,调用class.isInstance(msg)来判断当前数据是否是该模版类中模版的对象,如果是,则处理,如果不是,则交给下一个处理器。
上面的匹配方式也给了我一些启发,即如何模版类如何在运行时获取到模版所代表的类型,模版类即public class ClassA<T,I>
(其中T和I被称为模版,这在写框架的时候应该很有用。示例代码如下:
ChannelHandler为什么会一直传下去呢?
下图展示了ChannelHandler从调用Pipeline.fireChannelRead()函数后ChannelHandler是如何传播OP_READ事件的。
- 当调用事件循环器检测到有OP_READ事件后,调用unsafe.read()来读取从
java.nio.channels.SocketChannel
中读取数据; - 然后调用
pipeline.fireChannelRead
方法将数据传入处理器链中,(pipeline中包含了一个双向链表,链表头为head,链表尾为tail) - 然后pipeline调用
head.invokeChannelRead
方法,该方法会主动调用该ChannelHandlerContext
下的handler的channelRead
方法(channelRead方法是我们经常来处理数据的地方),在该方法内,用户可自己选择是否将事件传播到下一个处理器中 - 当用户可以通过主动调用
ChannelHandlerContext
对象的fireChannelRead
方法时,该方法会自动找到下一个入站事件处理器,然后调用该事件处理器的invokeChannelRead
方法,从而将事件传播下去。
OP_READ事件处理流程 :
- 通过Selector.selectNow()检测到读事件
- 检查Channel.config是否读取,如果可读取,则调用channel.unsafe.read()方法
- unsafe通过
RecvByteBufAllocator
分配接收缓存,然后将javaChannel中的数据写入道该缓存中 - unsafe将读取到的数据放入当前Channel的pipeline中。
OP_WRITE事件处理流程 :
- 用户调用ctx.write()方法,最终会调用当前channel的unsafe的write方法将数据写入到
ChannelOutboundBuffer
中,ChannelOutboundBuffer
是Netty的出站数据缓存。 - 用户调用ctx.flush()方法,最终会调用当前channel的unsafe的flush方法,将
ChannelOutboundBuffer
中的数据写入到javaChannel中。
8.2 超时处理、心跳检测
首先来说心跳检测,Netty提供了IdleStateHandler
类来帮助我们完成心跳检测功能,该类实现了ChannelDuplexHandler
类,说明该类是一个双向处理器,可以处理入站和出站事件,主要的构造方法如下:
-
observeOutput:如果设置该值为true,Netty将考虑是否是接受端接收数据缓慢的问题(通过检查出站缓存区)导致写超时。如果出站缓慢,则Netty不认为是这是空闲。
-
readerIdleTime:表示读空闲,多长时间未接收到数据后将触发
READER_IDLE_STATE_EVENT
事件 -
writerIdleTime:表示写空闲,多长时间未写入数据后将触发
WRITER_IDLE_STATE_EVENT
事件 -
allIdleTime:表示读写都空闲,多长时间未读入或者写入后将触发
ALL_IDLE_STATE_EVENT
事件 -
unit:表示以上参数的时间单位
一般需要将IdleStateHandler
处理器加入到处理链的最前面,并且要注意的是该处理器仅仅按以上逻辑来触发一个用户事件,对事件的处理还需要用户自定义处理器来实现,并且该自定义处理器需要实现userEventTriggered
方法。
示例代码如下:
读超时:读超时的实现有ReadTimeoutHandler
,当发生读超时,会触发ReadTimeoutException
异常并关闭通道。
写超时:写超时的实现有WriteTimeoutHandler
,当写入数据时,如果任务过了设定的时间后还没开始做,则会触发WriteTimeoutExeception
。底层通过定时任务+异步编程实现,提交一个定时任务,当时间到达后,如果给定的Promise的isDone()返回false,则表明发生写超时。
8.3 Netty日志框架
Netty内部有自己的日志框架,但是可配置性不高,因此我们在这里引入log4j日志框架。
首先,引入pom依赖包:
然后配置Log4j,resources/log4j.properties,完毕,Netty会自动探测log4j。
log4j配置详见log4j配置详解
8.4 IP地址过滤
Netty提供了IP地址过滤的Handler,只需配置一些规则,即可使用。主要的Handler如下:
RuleBasedIpFilter
:基于规则的IP过滤器
- IpSubnetFilter:Ip子网过滤器,也需要配置规则
- UniqueIpFilter:用来确保一个IP只建立一个连接的过滤器
8.5 大数据流的处理
Netty中提供了对大数据流的处理,当我们想向通道中写大数据流的时候可以使用netty提供的方法。几个主要的类如下:
-
ChunkedInput
:是对输入数据的抽象,提供了ChunkedFile
、ChunkedNioFile
、ChunkedStream
等实现。 -
ChunkedWriteHandler
:Netty提供的大数据写入的处理器,该处理器接收ChunkedInput
对象,并对该对象进行处理,将数据以块的方式写入到Channel中。
示例代码如下:
8.6 数据安全,使用SSL/TLS协议
SSL/TLS 中文全称为安全套接字层(Secure Sockets Layer),首先我们需要生成密钥仓库,生成方式详见:使用 keytool 生成密钥对 + keytool 命令详解。
一般认证方式有两种,双向认证和单向认证,双向认证需要双方都持有对方的证书,单向认证中,一般是客户端需持有服务器的证书(1.先导出服务器的cer证书 2.将服务器的cer证书导入到客户端的秘钥仓库中)。
在Netty中使用SSL/TLS协议非常简单,有以下几步(该方式较为第二步较为繁琐,可看下面的方法):
- 通过keytool生成密钥仓库,具体生成命令可以见使用 keytool 生成密钥对 + keytool 命令详解。
- 编写工具类,该工具类的作用是读取密钥仓库初始化
SSLContext
对象。
具体代码如下:
SslUtil.java
SslContextFactory.java
- 将SslHandler添加到Pipeline中,SslHandler的构造方法中需要SSLEngine,可以通过SSLContext获取。代码如下:
使用Netty提供的方法生成SslHandler:
以上是生成服务端Sslhandler的方法,如果是客户端,只需要提供X509Certificate
对象,然后调用SslContextBuilder.forClient()方法即可。如下:
如果需要双向认证,则客户端可以进行如下配置:
8.7 流量整形
前面有讲过使用ChannelOption配置Channel的高低水位,通过高低水位可以控制Channel的写入速度(写入前需要调用channel.isWritable()
方法来判断通道是否可写,如果直接写,可能导入写入失败)。本小节来讲流量整形,它是流量控制的一种机制。流量整形是一种主动调整流量输出速率的措施。它的思路如下:
- 写入:对通道的写入事件进行监控,来实时计算写入速率,然后每次写入前计算一个写入等待时间(该等待时间是根据写入速率求得的,如果速率过快,则等待时间越大),如果等待时间大于10ms,则会将数据缓存到一个队列中,再在流量计量算法的控制下“均匀”地发送这些被缓存的数据。当缓存队列满或者等待时间超过最大等待时间时,会设置
channel.isWritable()
方法返回false。 - 读取:对通道的读取事件进行监控,计算得到读入速率,然后计算读等待时间wait,如果等待时间大于10ms,则设置通道的autoRead为false,并暂停通道读取,然后启动一个定时任务,等待wait时间后,设置autoRead为true,开始读通道。当暂停通道读事件的时候,会导致操作系统的端口缓存被占满(如果速率过快),因为TCP的滑动窗口机制,发送方会自动调节发送速率。
流量整形可能会增加延迟。
Netty提供了3种流量整形方式:
- ChannelTrafficShapingHandler(通道流量整形):只对当前通道起作用
- GlobalChannelTrafficShapingHandler(全局通道流量整形):一般用在服务器端,需要注意的是,全部通道需公用一个该类的对象。该类会对所有的通道和全局流量速率都做计算。
- GlobalTrafficShapingHandler(全局流量整形):该类和
GlobalChannelTrafficShapingHandler
的区别是,它只对全局流量做监控,对单个通道不做更细的监控。
九、常用的协议
Netty支持的传输层协议有TCP、UDP等,本章主要介绍应用层协议,因此仅仅涉及Handler的使用。
Netty支持的应用层协议有:SSL、HTTP、WebSocket、FTP、SMTP等。
协议本身是一种人为设定的规约,就好比语言一样,两个人对话,只有使用相同的语言,才能更好的交流。
只需要在接受到数据后对数据进行解码,发送数据时对数据进行编码,而这两个操作均可在ChannelHandler中实现。
9.1 Http协议的使用
Netty中使用http协议非常方便,Netty本身提供了http的编解码器,只需要在pipeline
中添加即可,需要注意的是:此类协议需要最先添加。
HttpServerCodec
类继承了CombinedChannelDuplexHandler<HttpResponseDecoder, HttpRequestEncoder>
,可以看到它是将Http编解码器进行了组合。也可以用HttpResponseDecoder
和HttpRequestEncoder
替换HttpServerCodec
,不过一般不必。
示例代码如下:
上面的代码中,创建了一个Channel初始化器,并且在pipeline中添加了HttpServerCodec
对象、HttpObjectAggregator
对象、自定义的MyHttpChannelInboundHandler
对象。
HttpObjectAggregator
的作用是将HttpMessage和HttpContent进行聚合,组成FullHttpRequest或者FullHttpResponse对象。
服务端Channel的初始化代码如下:
可以看到,初始化Channel时,同一般的TCP服务没有区别。
9.2 Http2
Http2具有如下特点:
1.二进制分帧层
如图所示,二进制分帧层是加载SSL/TLS(如果有)之上的,Http2将所有传输的信息分隔为更小的消息和帧,并采用二进制格式来对它们进行编码。
2.数据流、消息和帧
新的二进制分帧机制改变了客户端和服务端之间数据交换的方式。为了说明这个过程,需要了解下面三个概念:
- 数据流(Stream):已建立的连接内的双向字节流,可以承载一条或者多条消息
- 消息(Message):与逻辑请求或响应消息对应的一系列帧
- 帧(Frame):Http2的最小通信单位,每个帧都包含有帧头,至少也会标识出当前帧所属的数据流
这些概念之间的关系如下:
- 所有通信都是在一条TCP连接上完成,此连接可以承载任意数量的双向数据流
- 每一个数据流都有一个唯一的标识符和优先级信息,用于承载双向消息
- 每条消息都是一条逻辑Http消息(例如请求或响应),包含一个或多个帧
- 帧是最小通信单位,承载特定类型的数据,如Http标头、消息负载等。来自不同数据流的帧可以交错发送,再根据每个帧头的数据流表示标识重新组装。
3.请求与响应复用
- 并行交错地发送多个请求,请求之间互不影响
- 并行交错地发送多个响应,响应之间互不干扰
- 使用一个连接并行发送多个请求或响应
- 消除不必要的连接和提高了现有网络容量的利用率,从而减少页面加载时间
4.头部压缩算法(HPack)
头部压缩算法需要再Http客户端和服务器端进行如下操作:
- 维护一份相同的静态表,包含常见的头部名称以及特别常见的头部名称和值的组合
- 维护一份相同的动态表,可以动态地添加内容
- 基于静态哈夫曼码表的哈夫曼编码
头部压缩算法原理就是使用静态表和动态表对头部字段进行替换(用表索引),然后对于动态表中不存在的内容,还可以使用哈夫曼编码来减小体积。
5.协商机制
虽然http2是比http1更加优秀的协议,但是目前仍有很多公司使用http1.1或http1.0,因此需要协商机制来保障不同协议之间的兼容。
通过协商机制,如果双方都支持http2,则会进行协议升级(Upgrade)。
这部分实战内容暂时空着,有点麻烦。。
9.3 WebSocket
使用WebSocket可以实现如在线聊天室、在线推送等功能。
WebSocket协议是建立在http协议之上的,在建立websocket时会进行协议升级(upgrade),WebSocket数据传输的最小单位为帧Frame,数据将被承载到一个或多个帧(如果数据过大)中进行传输。
- 因为WebSocket是建立在Http协议之上的,因此使用的编解码器仍然是
HttpServerCodec
- 如果数据过大,WebSocket会将数据分为多个帧,因此需要将他们聚合起来,因此需要使用
HttpObjectAggregator
(http也类似) - 然后使用
WebSocketServerProtocolHandler
解析WebSocket数据帧
初始化Pipeline代码如下:
WebSocket的帧类型:
帧的抽象为WebSocketFrame
接口。
BinaryWebSocketFrame
:包含二进制数据CloseWebSocketFrame
:Close帧,用于关闭连接ContinuationWebSocketFrame
:包含延续的文本或二进制数据PingWebSocketFrame
:Ping帧PongWebSocketFrame
:Pong帧TextWebSocketFrame
:包含文本数据
与websocket服务器通信的浏览器代码如下:
十、测试
10.1 使用EmbeddedChannel测试ChannelHandler
首先导入Junit测试框架依赖包
然后编写测试类,在这里我们测试了FixedLengthFrameDecoder
固定长度解码器
测试函数的输出如下:
可以看到,FixedLengthFrameDecoder
对输入的ByteBuf对象进行了正确的解码。
10.2 使用Apache JMeter来对网络程序进行压力测试
测试TCP连接:
- 首先从Apache Jimeter处下载 jimeter程序包,然后运行
- 点击保存,保存测试计划
- 右键测试计划,选择add->Threads->Threads Group,设置 Thread Properties
-
Number of Threads(users):线程的数量,开启多少个线程,即模拟多少个用户
-
Ramp-up period(seconds):加速时间,即上述每个线程启动的间隔时间
-
Loop Count:循环的次数
-
配置好Thread Group后,鼠标右键Thread Group,选择add->Sampler->TCP Sampler,设置TCP连接的Server Name or Ip 和 port、Text to Send。
-
配置监听,鼠标右键 TCP Sampler,选择add->Linsteners->Summary Report
-
启动测试任务
十一、案例分析
11.1 RocketMQ
11.2 Eclipse Vert.x
Vert.x的API很友好,可以用极少的代码量实现所需的功能,个人认为它对于小项目非常友好,因此也非常适合微服务,SpringBoot对于微服务来说可能有点庞大,或许Vert.x刚好。
Vert.x底层使用Netty实现的,支持高并发。如果项目需要快速上线,或许Vert.x是一个不错的选择。
Vert.x更像是一个工具的集合,它隐藏了复杂的实现细节,只暴露了简单的调用接口就可实现功能。
__EOF__

本文链接:https://www.cnblogs.com/zolmk/p/17570799.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?