揭秘通信协议设计的奥妙,作为面试官我都看蒙了

简介: 所谓的通信协议就是通信双方共同遵循的一种“约定”,用于通信发送方将内容按照“通信协议”所规定的格式组装成**“二进制流”**,通信接收方按照“通信协议”所规定的格式正确的从二进制流中解码出一个个原始请求。

本节将重点探讨网络编程中一种非常通用的协议设计方法论:协议头 + 消息体。

所谓的通信协议就是通信双方共同遵循的一种“约定”,用于通信发送方将内容按照“通信协议”所规定的格式组装成**“二进制流”**,通信接收方按照“通信协议”所规定的格式正确的从二进制流中解码出一个个原始请求。

那通信协议如何设计呢?

1、通用的协议设计方法论

在网络编程中,流行着一种经典的协议设计方法论:协议头 + 消息体。

网络异常,图片无法展示
|

其设计的关键点如下:

  • 协议头的长度是固定的,通常为识别一个业务的最小长度
  • 协议头中会包含一个长度字段,用来标识一个完整包的长度,用来表示长度字段的字节位数直接决定了一个包的最大长度,长度字段通常被设计为4个字节。
  • 消息体中存储业务数据,例如如果是一个Dubbo协议,那消息体中可能会包含请求参数、调用的服务名等,而且字符串类的存储通常会采取字段长度、字段内容的组织方式。

为了有一个更直观的展示,我以一个简单的RPC通信场景为例,实现类似Dubbo服务的远程服务调用,其通信协议可以简单设置成下图所示:

网络异常,图片无法展示
|

基于 Header + Boby 的通信协议设计模式后,通信接收方就能很好的从二进制流中非常容易地解码出一条一条原始的请求数据包,解码的基本套路如下(在面试中面试官非常喜欢问的“粘包”问题的破解之道)

  • 首先判断累积缓存区中是否存在一个完整的Head头部,例如上述示例中,一个包的Header的长度为6个字节,那首先判断累积缓存中可读字节数是否大于等于6,如果不足6个字节,跳过本次处理,等待更多数据到达累积缓存区
  • 尝试将头部6个字节读取,并且提取长度字段中存储的数值,即包长度,然后判断累积缓存区中可读字节数大于等于整个包的长度,如果累积缓存区不包含一个完整的数据包,则跳过本次处理,等待更多数据到达累积缓存区。
  • 如果包含一个完整的包,则按照通信协议的格式按序读取相关的内容。

正是因为这种设计理念非常通用,Netty 对上述协议设计进行了统一封装:
LengthFieldBasedFrameDecoder 闪亮登场了,
接下来我们来看看Netty是如何进行封装的,揭晓更多的实现细节,让大家做到理论与实践相结合。

2、LengthFieldBasedFrameDecoder 详解

2.1 概述

网络异常,图片无法展示
|

接下来对其核心属性进行一个详细的解读:

  • ByteOrder byteOrder
    字节序列,Netty默认使用大端序列(主要是针对int、long等数值类型),所谓的大端序列,通常可以这样理解,接收端收到的字节流的顺序是从数值类型的高字节。
  • int maxFrameLength
    一条消息最大的长度。
  • int lengthFieldOffset
    代表长度字段的开始偏移量。
  • int lengthFieldLength
    代表长度字段占用的字节长度。
  • int lengthFieldEndOffset
    代表长度字段的结束偏移量,等于lengthFieldOffset + lengthFieldLength。
  • int lengthAdjustment
    长度适配适配值。该值表示协议中长度字段与消息体字段直接的距离。
  • int initialBytesToStrip
    跳过一个包中前面多少个字节不处理,通常是将协议头部跳过,只将消息体中内容传输到下游时使用
  • boolean failFast
    是否快速失败。
  • boolean discardingTooLongFrame
    是否吞没(跳过)大帧包。
  • long tooLongFrameLength
    当前在处理吞没大包的实际大小。
  • long bytesToDiscard
    下一次解码之前,需要先忽略的字节数,当遇到超过maxFrameLength的包时使用。

上面的属性如果不太好理解,没关系,因为本节的最后会有两张图勾画出协议的全貌(用图示的方式勾画出各个属性的位置与含义)

2.2 decode 方法详解

接下来我们来看一下其decode方法,通过阅读源码的方法来理解其内部的工作原理。

LengthFieldBasedFrameDecoder#decode

网络异常,图片无法展示
|

Step1:跳过无效数据包的处理逻辑。如果discardingTooLongFrame为true,表示正在处理大于****maxFrameLength的包,需要跳过这个超长的包,不对其解码,由于数据是陆续到达累积缓存区,并不能一次跳过整个无效包,故需引入 bytesToDiscard 变量,用于记录本次能跳过的字节,当 bytesToDiscard 为 0后表示一个无效包已全部跳过,需要处理正常数据包,此时discardingTooLongFrame 会重置为 false。

网络异常,图片无法展示
|

Step2:如果累积缓冲区的可读字节大小小于length字段的结束偏移量,返回null,结束解码,说明该累积缓存区中的数据还不完整。

网络异常,图片无法展示
|

Step3:尝试从累积缓存区中获取包的长度。其中表示 lengthFiedlOffset 表示长度字段的其实偏移量,在结合长度字段的长度 lengthFieldLength ,再结合字节序列**(大端序列、小端序列)**。

网络异常,图片无法展示
|

Step4:这里是包长度超过协议允许的最大包长度时的处理逻辑,在这里大家先姑且跳过 lengthAdjustment 属性的含义。

  • 如果当前累积缓存区中的可读字节大于 frameLength,大于当前包的长度,可以通过调用 skipBytes 方法跳过这包。
  • 如果当前累积缓存区的可读自己小于 frmaeLength,需要分多次跳过,故先将累积区中的数据全部跳过,然后通过 bytesToDiscard 记录还需要跳过的字节数。

网络异常,图片无法展示
|

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 来准确表示数据帧(业务数据的长度),这里是一种
逆向思维

3、协议设计子类的最佳实践

最佳实践:
LengthFieldBasedFrameDecoder 的 decode 方法的职责是从二进制流中解码出一个完整的数据包,其返回类型还是 ByteBuf,故自定义的编码解码器的 decode 方法就是先调用父类的 decode 方法 得到 ByteBuf ,然后对 ByteBuf 中的数据解码出对象。


LengthFieldBasedFrameDecoder 并不负责将 ByteBuf 转换为协议对象,而是从二进制流中解码出一个数据帧,而将ByteBuf 转换为协议对象的职责由其子类实现,通常的编码风格如下:

网络异常,图片无法展示
|

相关文章
|
5月前
|
传感器 芯片
嵌入式通信协议全解析:SPI、I²C、UART详解(附带面试题)
通信是指人与人或人与自然之间通过某种行为或媒介进行的信息交流与传递。从广义上来说,通信是指需要信息的双方或多方在不违背各自意愿的情况下采用任意方法、任意媒质,将信息从某方准确安全地传送到另方。在出现电波传递通信后,通信被单一解释为信息的传递,是指由一地向另一地进行信息的传输与交换,其目的是传输消息。通信方式包括利用“电”来传递消息的电信,这种通信具有迅速、准确、可靠等特点,且几乎不受时间、地点、空间、距离的限制,因而得到了飞速发展和广泛应用。
1213 0
|
存储 设计模式 缓存
一文揭晓通信协议设计的奥妙,直接"秒杀"面试官
一文揭晓通信协议设计的奥妙,直接"秒杀"面试官
一文揭晓通信协议设计的奥妙,直接"秒杀"面试官
|
4月前
|
存储 Java
【IO面试题 四】、介绍一下Java的序列化与反序列化
Java的序列化与反序列化允许对象通过实现Serializable接口转换成字节序列并存储或传输,之后可以通过ObjectInputStream和ObjectOutputStream的方法将这些字节序列恢复成对象。
|
1月前
|
存储 缓存 算法
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
本文介绍了多线程环境下的几个关键概念,包括时间片、超线程、上下文切换及其影响因素,以及线程调度的两种方式——抢占式调度和协同式调度。文章还讨论了减少上下文切换次数以提高多线程程序效率的方法,如无锁并发编程、使用CAS算法等,并提出了合理的线程数量配置策略,以平衡CPU利用率和线程切换开销。
面试官:单核 CPU 支持 Java 多线程吗?为什么?被问懵了!
|
1月前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
1月前
|
存储 缓存 Java
大厂面试必看!Java基本数据类型和包装类的那些坑
本文介绍了Java中的基本数据类型和包装类,包括整数类型、浮点数类型、字符类型和布尔类型。详细讲解了每种类型的特性和应用场景,并探讨了包装类的引入原因、装箱与拆箱机制以及缓存机制。最后总结了面试中常见的相关考点,帮助读者更好地理解和应对面试中的问题。
62 4
|
2月前
|
算法 Java 数据中心
探讨面试常见问题雪花算法、时钟回拨问题,java中优雅的实现方式
【10月更文挑战第2天】在大数据量系统中,分布式ID生成是一个关键问题。为了保证在分布式环境下生成的ID唯一、有序且高效,业界提出了多种解决方案,其中雪花算法(Snowflake Algorithm)是一种广泛应用的分布式ID生成算法。本文将详细介绍雪花算法的原理、实现及其处理时钟回拨问题的方法,并提供Java代码示例。
93 2
|
2月前
|
JSON 安全 前端开发
第二次面试总结 - 宏汉科技 - Java后端开发
本文是作者对宏汉科技Java后端开发岗位的第二次面试总结,面试结果不理想,主要原因是Java基础知识掌握不牢固,文章详细列出了面试中被问到的技术问题及答案,包括字符串相关函数、抽象类与接口的区别、Java创建线程池的方式、回调函数、函数式接口、反射以及Java中的集合等。
37 0
|
4月前
|
XML 存储 JSON
【IO面试题 六】、 除了Java自带的序列化之外,你还了解哪些序列化工具?
除了Java自带的序列化,常见的序列化工具还包括JSON(如jackson、gson、fastjson)、Protobuf、Thrift和Avro,各具特点,适用于不同的应用场景和性能需求。
|
4月前
|
Java
【Java基础面试三十七】、说一说Java的异常机制
这篇文章介绍了Java异常机制的三个主要方面:异常处理(使用try、catch、finally语句)、抛出异常(使用throw和throws关键字)、以及异常跟踪栈(异常传播和程序终止时的栈信息输出)。

热门文章

最新文章