上一篇详细介绍了Netty的编解码的基本实现原理,本节将重点探讨网络编程中一种非常通用的协议设计方法论:协议头 + 消息体。
所谓的通信协议就是通信双方共同遵循的一种“约定”,用于通信发送方将内容按照“通信协议”所规定的格式组装成“二进制流”,通信接收方按照“通信协议”所规定的格式正确的从二进制流中解码出一个个原始请求。
那通信协议如何设计呢?
在网络编程中,流行着一种经典的协议设计方法论:协议头 + 消息体。
揭秘通信协议设计的奥妙,作为面试官我都看蒙了
其设计的关键点如下:
为了有一个更直观的展示,我以一个简单的RPC通信场景为例,实现类似Dubbo服务的远程服务调用,其通信协议可以简单设置成下图所示:
揭秘通信协议设计的奥妙,作为面试官我都看蒙了
基于 Header + Boby 的通信协议设计模式后,通信接收方就能很好的从二进制流中非常容易地解码出一条一条原始的请求数据包,解码的基本套路如下(在面试中面试官非常喜欢问的“粘包”问题的破解之道)
正是因为这种设计理念非常通用,Netty 对上述协议设计进行了统一封装: LengthFieldBasedFrameDecoder 闪亮登场了,接下来我们来看看Netty是如何进行封装的,揭晓更多的实现细节,让大家做到理论与实践相结合。
2.1 概述
image.png
接下来对其核心属性进行一个详细的解读:
上面的属性如果不太好理解,没关系,因为本节的最后会有两张图勾画出协议的全貌(用图示的方式勾画出各个属性的位置与含义) 。
2.2 decode 方法详解
接下来我们来看一下其decode方法,通过阅读源码的方法来理解其内部的工作原理。
LengthFieldBasedFrameDecoder#decode
image.png
Step1:跳过无效数据包的处理逻辑。如果discardingTooLongFrame为true,表示正在处理大于****maxFrameLength的包,需要跳过这个超长的包,不对其解码,由于数据是陆续到达累积缓存区,并不能一次跳过整个无效包,故需引入 bytesToDiscard 变量,用于记录本次能跳过的字节,当 bytesToDiscard 为 0后表示一个无效包已全部跳过,需要处理正常数据包,此时discardingTooLongFrame 会重置为 false。
揭秘通信协议设计的奥妙,作为面试官我都看蒙了
Step2:如果累积缓冲区的可读字节大小小于length字段的结束偏移量,返回null,结束解码,说明该累积缓存区中的数据还不完整。
揭秘通信协议设计的奥妙,作为面试官我都看蒙了
Step3:尝试从累积缓存区中获取包的长度。其中表示 lengthFiedlOffset 表示长度字段的其实偏移量,在结合长度字段的长度 lengthFieldLength ,再结合字节序列(大端序列、小端序列)。
揭秘通信协议设计的奥妙,作为面试官我都看蒙了
Step4:这里是包长度超过协议允许的最大包长度时的处理逻辑,在这里大家先姑且跳过 lengthAdjustment 属性的含义。
揭秘通信协议设计的奥妙,作为面试官我都看蒙了
Step5:如果累积缓存区中的数据不包含一个完整的包,返回null,结束本次解码,等待更多的数据包的到来。
揭秘通信协议设计的奥妙,作为面试官我都看蒙了
Step6:通过 ByteBuf 的 slince 方法,提取一个完整的包长度,解码出完整的数据包,完成一个数据包解码。
2.3 图解 LengthFieldBasedFrame 协议
在Netty 的 LengthFieldBasedFrameDecoder 中有一个 lengthAdjustment 属性,可以是正数,也可以是负数,其使用的代码片段如下:
frameLength += lengthAdjustment + lengthFieldEndOffset
lengthAdjustment 长度调整字段,可以为正数,也可以为负数,主要的作用是 长度字段中的值是否包含 Header 长度本身,严格意义上来说应该是包含 长度字段之前的字节序列。
1、lengthAdjustment > 0
揭秘通信协议设计的奥妙,作为面试官我都看蒙了
2、lengthAdjustment < 0 在大多数情况下,length字段表示消息正文的长度,但是有些协议,其长度表示的是整个消息的长度,故Netty为了适配这种情况,可以通过 lengthAdjustment 设置为负数,来调节数据帧的大小。
揭秘通信协议设计的奥妙,作为面试官我都看蒙了
总结:lengthAdjustment 的出现是Netty为了适配现有的协议而设计出来的字段,即 Netty LengthFieldBasedFrameDecoder 是为了i给 header + body ,并且基于长度字段的协议一种通用的解决方案,可以通过 lengthAdjustment 来准确表示数据帧(业务数据的长度),这里是一种逆向思维。
最佳实践: LengthFieldBasedFrameDecoder 的 decode 方法的职责是从二进制流中解码出一个完整的数据包,其返回类型还是 ByteBuf,故自定义的编码解码器的 decode 方法就是先调用父类的 decode 方法 得到 ByteBuf ,然后对 ByteBuf 中的数据解码出对象。
即 LengthFieldBasedFrameDecoder 并不负责将 ByteBuf 转换为协议对象,而是从二进制流中解码出一个数据帧,而将ByteBuf 转换为协议对象的职责由其子类实现,通常的编码风格如下:
image.png