针对Channel上发生的各种网络操作,例如链路创建、链路关闭、消息读写、链路注册和去注册等,Netty将这些消息封装成事件,触发ChannelPipeline调用ChannelHandler链,由系统或者用户实现的ChannelHandler对网络事件做处理。
由于网络事件种类比较多,触发和执行机制也存在一些差异,如果掌握不到位,很有可能遇到一些莫名其妙的问题。而且有些问题只有在高并发时或者在生产环境中才会出现,在测试环境中不容易复现,因此这类问题定位难度很大。
【channelReadComplete方法被调用多次问题】
上述测试结果表明,
对于channelRead方法,如果前面添加了对应协议的解码器处理粘包和拆包,则只有在消息被解码成功后才会调用channelRead方法。而channelReadComplete方法的调用机制则不同,只要底层的SocketChannel读到了ByteBuf,就会触发一次调用,对于一个完整的业务消息,可能会多次触发调用。
业务端用netty写了一个http服务器,发现channelReadComplete方法被调用多次问题,通过对客户端请求消息和Netty框架进行源码分析,找到了问题的根本原因:TCP底层并不了解上层业务数据的具体含义,
它会根据TCР缓冲区的实际情况进行包的拆分,所以在业务上认为一个完整的 HTTP报文可能会被TCP 拆分成多个包发送也有可能把多个小的包封装成一个大的数据包发送。
导致数据包拆分和重组的原因如下。
(1)应用程序写入的字节大小大于套接口发送缓冲区大小。
(2)进行MSS大小的TCP分段。
(3)以太网帧的有效载荷(payload)大于MTU的IP分片。
(4)开启了TCP Nagle算法。
由于底层的TCP无法理解上层的业务数据,所以在底层无法保证数据包不被拆分和重组,这个问题只能通过上层的应用协议栈设计来解决,根据业界主流协议的解决方案,归纳如下。
(1)
消息定长,例如每个报文的大小固定为200字节,如果不够,空位补空格。
(2)
在包尾增加换行符(或者其他分隔符)进行分隔,例如FTP。
(3)
将消息分为消息头和消息体,消息头包含表示消息总长度(或者消息体长度)的字段,通常消息头的第一个字段使用int32表示消息的总长度。
对于HTTP请求消息,当业务并发量比较大时,无法保证一个完整的HTTP消息会被一次全部读取到服务端。当采用chunked方式进行编码时,HTTP报文也是分段发送的,此时服务端读取的也不是完整的HTTP报文。为了解决这个问题,Netty 提供了HttpObjectAggregator,保证后端业务ChannelHandler接收的是一个完整的HTTP报文,相关示例代码如下:
HttpObjectAggregator可以保证Netty读到完整的HTTP请求报文后才调用一次业务ChannelHandler的channelRead方法,无论这条报文底层经过了几次SocketChannel的read调用。但是channelReadComplete方法并不是在业务语义上的读取消息完成后被触发的,而是在每次从SocketChannel成功读到消息后,由系统触发,也就是说如果一个HTTP消息被TCP协议栈发送了N次,则服务端的channelReadComplete方法就会被调用N次。在灰度测试环境中,
由于客户端并没有采用chunked 的编码方式,并发压力也不是很大,所以一直没有发现该问题,到了生产环境中有些客户端采用了chunked方式发送HTTP请求消息,客户端并发量也比较大,所以触发了服务端的问题。
【channelReadComplete方法调用】
对于channelReadComplete方法的调用,我们很容易误认为前面已经增加」对应L以的编解码器,所以只有消息解码成功才会调用channelReadComplete方法。实际上它的调用与用户是否添加协议解码器无关,只要对应的SocketChannel成功读到了ByteBuf,它就会被触发,相关代码如下(NioByteUnsafe类):
对于大部分协议解码器,例如Netty内置的ByteToMessageDecoder,它会调用具体的协议解码器对ByteBuf解码,只有解码成功,才会调用后续ChannelHandler的channelRead方法,代码如下(ByteToMessageDecoder类):
【ChannelHandler职责链调用】
ChannelPipeline以链表的方式管理某个Channel对应的所有ChannelHandler,需要说明的是,下一个ChannelHandler的触发需要在当前ChannelHandler中显式调用,而不是自动触发式调用,相关代码如下( SslHandler类):
如果遗忘了调用ctx.fireChannelActive方法,则SslHandler后续的ChannelHandler 的channelActive方法将不会被执行,职责链执行到SslHandler就会中断。当执行完业务的最后一个ChannelHandler时,要判断是否需要调用系统的TailContext,如果需要,则通过ctx.firexxx方法调用。