当前位置: 首页 > 科技观察

Netty的常用招式——ChannelHandler和Codec

时间:2023-03-14 20:54:51 科技观察

在上一篇文章中,我们深入研究了Netty逻辑架构中的ChannelHandler和ChannelPipeline这两个核心组件,并介绍了其在日常开发和使用中的最佳实践。文中也提到,ChannelHandler主要用于数据输入输出过程中的处理,如编解码、异常处理等,今天我们就选择日常开发中最常用的一种ChannelHandler用法来学习——编解码器。如果说ChannelHandler的学习是Netty的基本招数,那么codec就是由“基本招数”衍生出来的“普通招式”。我们经常使用ChannelHandler来实现编解码器逻辑。无论是实际的网络编程还是面试套路,都离不开编解码器的知识。本文预计阅读时间15分钟左右,将重点关注以下问题:学习编解码器,从贴/解包开始如何实现自定义编解码器Netty中有哪些开箱即用的编解码器一、学习编解码器,从贴/解包开始1.1为什么会出现贴/解包问题贴/解包问题,相信大家都有所耳闻,造成这个问题的原因主要有以下三个:1)MTU和MSS限制MTU(MaxitumTransmissionUnit)是限制OSI五层网络模型中数据链路层对一次可以发送的最大数据量的限制。一般来说,大小为1500字节。MSS(MaximumSegmentSize)是指TCP报文中数据部分的最大长度,是传输层一次发送的最大数据量的大小限制。MSS和MTU的关系如下:MSS长度=MTU长度-IPHeader-TCPHeader所以当MSS长度+IPHeader+TCPHeader>MTU长度时,需要拆分多个包发送,会造成“拆包”现象.2)TCP滑动窗口TCP的流量控制方式是“滑动窗口”。当A向B发送数据时,作为接收端的B会告知发送端A可以接受的窗口值,从而控制A发送流量的大小,从而达到流量控制的目的。假设接收方B通知发送方A窗口大小为256,也就是说发送方最多可以发送256字节,由于发送方的数据大小为518字节,所以只能发送前256字节,等到receiver收到partyacks后,剩下的字节就可以发送了。会造成“脱壳”现象。3)Nagle算法在TCP/IP协议中,无论发送多少数据,都必须在数据(DATA)前面加上一个协议头(TCPHeader+IPHeader)。如果每次需要发送的数据只有1个字节,加上20个字节的IPHeader和20个字节的TCPHeader,则每次发送的数据包大小为41个字节,但真正有效的信息只有1个字节,造成非常大的浪费。因此在TCP/IP中使用Nagle算法来提高效率。Nagle算法的核心思想是“化零为整数”。它在数据确认前写入缓冲区,等待数据确认或缓冲区累积到一定大小后才发送数据包。多个小数据包合并在一起发送,产生粘包。1.2如何处理粘包/拆包对于TCP,其实我们都知道它的一个特点就是它是一种“面向字节流”的传输协议,本身没有数据包的边界。所以不管是什么原因导致“粘包/解包”,TCP协议本身的数据传输都是可靠和正确的。首先,我们需要明确一点:“粘包/解包”带来的问题,本质上是应用层的数据分析问题。因此,解决拆包/粘包问题的核心方法:定义应用层的通信协议。核心是定义正确的数据边界。常见的协议解决方案包括三种:1)固定长度每个数据包约定一个固定长度。当接收方累计读取到一条定长消息后,就认为得到了一条完整的消息。比如我们要发送一条ABCDEFGHIJKLM消息,约定固定消息长度为4,那么接收方就可以按照4的长度进行解析。如下。当发送方的数据小于固定长度时,比如最后一个数据包,只有两个字符MN,则需要补空。这个方案很简单,但是它的缺点也很明显,很不灵活。如果定长定义太长,会浪费数据传输空间。如果定义太短,会影响数据的正确传输。这种方法一般不用。2)除了特定定界符的固定长度外,我们很容易想到的区分“数据边界”的方法就是使用“特定定界符”。当接收方读取到特定的定界符时,它认为它有一个完整的消息。比如我们用换行符\n来区分。AB\nCDEFG\nHIJK\nLMN\n这种方式比较灵活,适应不同长度的消息。但必须注意,“特殊分隔符”不能与消息内容重复,否则会解析失败。因此,在实践中,我们可以考虑对消息进行编码(如base64),然后将编码字符集之外的符号作为“特定分隔符”。该方案一般用于协议比较简单的场景。3)消息长度+内容在一般的项目开发中,最常见的方式是使用消息长度+内容的方式进行处理。例如,定义这样一个消息格式:当以这样的格式存储时,消息的接收者在解析时首先读取4个字节的信息作为“消息长度”,这里是3,表示消息长度为3个字节。然后只读3字节的消息内容作为一个完整的消息。例如:2AB5CDEFG4HIJK3LMN消息长度+内容的方式非常灵活,可以适用于各种场景。注意,在消息头中,除了定义消息长度外,还可以自定义其他扩展字段,比如消息版本、算法类型等。2.Netty中如何实现自定义编解码器上面我们已经了解了原因“粘性/解压”和常见的解决方案。让我们看看如何在Netty中实现自定义编解码器。Netty作为一个优秀的网络通信框架,提供了非常丰富的处理编码和解码的抽象类。我们只需要自定义编解码算法扩展即可。2.1自定义编码器我们先来看看自定义编码器。因为编码器比较简单,所以不需要关注“粘/解包问题”。常用的编码抽象类有MessageToByteEncoder和MessageToMessageEncoder,继承自ChannelOutboundHandlerAdapter,对Outbound相关数据进行操作。1)MessageToByteEncoder该编码器用于将消息对象编码成字节流。它提供了一种编码的抽象方法。我们只需要实现encode方法即可进行自定义编码。encoder的实现很简单,不需要关注解包/粘贴的问题。我们举个栗子,将一个String类型的消息转成字节流:2)编码器MessageToMessageEncoder,用于将一个消息对象编码为另一个消息对象。这里的第二个Message可以理解为任意对象。如果使用ByteBuf对象,则和上面的MessageToByteEncoder是一样的。找个Netty自带的栗子,StringEncoder:2.2CustomDecoderDecoder比encoder要复杂一些,因为需要考虑“拆包/粘连”的问题。由于接收方可能没有收到完整的消息,解码框架需要缓冲传入的数据,直到获得完整的消息。常用的解码器抽象类有ByteToMessageDecoder和MessageToMessageDecoder,继承自ChannelInboundHandlerAdapter,对Inbbound相关数据进行操作。一般的做法是使用ByteToMessageDecoder解析TCP协议来解决解包/卡顿问题。解析得到有效的ByteBuf数据,然后传递给后续的MessageToMessageDecoder进行数据对象转换。1)ByteToMessageDecoderByteToMessageDecoder解码器用于将字节流解码为消息对象。拿上面的“定长法”解决“sticky/unpacked”举个栗子,Netty自带的FixedLengthFrameDecoder。消息按固定长度的frameLength解析。在生产实践中,可能会使用更复杂的协议来实现自定义编解码器,例如protobuf。2)MessageToMessageDecoderMessageToMessageDecoder解码器用于将一个消息对象解码为另一个消息对象。如果需要对解析出的字节数据进行对象模型转换,就需要使用这个解码器。3.Netty有哪些开箱即用的解码器?作为一个优秀的网络编程框架,Netty不仅支持扩展的自定义编解码器,还提供了非常丰富的开箱即用的编解码器。尤其是我们上面1.2节提到的“粘包/解包问题”的三种解决方案,都有开箱即用的实现。3.1定长解码器FixedLengthFrameDecoder上面已经提到,对应1.2节的“定长解码”,这里稍微展开一下。通过构造函数配置定长的frameLength,解码时根据frameLength进行解码。当读取到长度为frameLength的消息时,解码器认为已经获得了一条完整的消息。当消息长度小于frameLength时,FixedLengthFrameDecoder解码器会等待后续数据包的到来,直到得到完整的消息。3.2SpecialDelimiterDelimiterDelimiterBasedFrameDecoder该解码器对应1.2节中的“SpecialDelimiterDecoding”,也是继承自ByteToMessageDecoder的解码器。该解码器将使用1个或多个符号分隔符解码传入消息(ByteBuf)。让我们看一下构造函数以了解几个重要参数。maxFranmeLengthmaxFranmeLength是待处理消息的最大长度限制。如果未检测到指定的分隔符超过maxFranmeLength,则会抛出TooLongFrameException。stripDelimiterstripDelimiter是一个boolean类型,用于判断解码后的消息是否去掉分隔符。如果stripDelimiter=false,那么解码后的消息内容会保留分隔符信息。failFastfailFast是布尔类型。如果为true,则在消息超过maxFranmeLength后将立即抛出TooLongFrameException。如果为false,则在解码完整消息之前不会抛出TooLongFrameException。delimiters的类型delimiters是一个ByteBuf数组,在构造函数中可以同时传入多个定界符,但是在解析的时候会选择长度最短的定界符进行消息拆分。例如接收到的数据是:ABCD\nEFG\r\n如果指定的分隔符是\n和\r\n,那么会解码两条消息。如果ABCDEFG只指定\r\n作为特定的分隔符,则只会解码一条消息:ABCD\nEFG3.3LengthFieldDecoderLengthFieldBasedFrameDecoder这种解码器在生产实践中被广泛使用(如RocketMQ)。它复杂,但极其灵活,基本可以涵盖各种基于长度的解包方案,比如1.2节提到的“消息长度+内容”方案。使用此解码器时,了解4个参数很重要。掌握参数设置后,可以快速实现不同的基于长度的解包解码方案。1)解码方案1:基于消息长度+消息内容,解码结果不截断消息头。消息中只包含消息长度Length和消息内容Content字段,其中Length用16进制表示,一共占用2个字节,Length的值为0x000C表示Content占用12个字节。解码示例:2)解码方案2:根据消息长度+消息内容,对解码结果进行截断。与方案1不同的是解码结果会截断消息头(跳过2个字节)。解码示例:3)解码方案3:基于Messageheader+messagelength+messagecontent在消息的起始位置添加一个特殊的消息头,并将消息长度的Length字段后移。解码示例:4)解码方案4:根据消息长度+消息头+消息内容,消息起始位置为消息长度Length字段,后面不直接添加消息内容,而是先添加消息头,然后添加消息内容。解码示例:由于在Length之后并没有立即添加内容,所以需要添加lengthAdjustment(2字节)得到Header+Content(14字节)的内容。4.让我们简单回顾一下总结。本文主要介绍ChannelHandler的一个典型应用场景——codec。编解码器的核心重点是“粘贴/解包”的处理。我们介绍了“卡壳/脱壳”的原因和常见的解决方法。然后展示了如何使用Netty框架实现自定义编解码器。最后,介绍几个在Netty中非常有用的开箱即用的编解码器。