一文揭晓通信协议设计的奥妙,直接"秒杀"面试官

简介: 一文揭晓通信协议设计的奥妙,直接"秒杀"面试官

上一篇详细介绍了Netty的编解码的基本实现原理,本节将重点探讨网络编程中一种非常通用的协议设计方法论:协议头 + 消息体


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


那通信协议如何设计呢?


温馨提示:本文遵循的目录结构:先提炼通信协议设计的通用方法论,然后源码分析Netty提供的解决方案,最后给出最佳实践,大家被错过最后的实践部分哦。。。

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


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

29e719ff65f5c7f4870d5314d659150a.png


其设计的关键点如下:


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


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


69e6fb24e5bf6d599063e7797c7c95bd.png

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


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


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


2、LengthFieldBasedFrameDecoder 详解


2.1 概述


012b6594ccf63fc41ed9c284bd6eeb93.png

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


  • 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


623dc08c7d6bf774f6233e817d7e9c46.png

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

a8278c68ac0b3020e5febb98ee9a4ac6.png


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

271e2394c83358f38d7b57fc97430561.png

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

60974ae778b5ced1db8fdf05a72b4f97.png

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


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

a3ccca816803e5d45e573f766db8d5fa.png

Step5:如果累积缓存区中的数据不包含一个完整的包,返回null,结束本次解码,等待更多的数据包到到来。

67d81e863729eba4083bbf42789936b1.png

Step6:通过 ByteBuf 的 slince 方法,提取一个完整的包长度,解码出完整的数据包,完成一个数据包解码。


2.3 图解 LengthFieldBasedFrame 协议


在Netty 的 LengthFieldBasedFrameDecoder 中有一个 lengthAdjustment 属性,其使用的代码片段如下:

frameLength += lengthAdjustment + lengthFieldEndOffset

lengthAdjustment 长度调整字段,可以为正数,也可以为负数,主要的作用是进行包长度适配的,详情请看如下分析。


1、lengthAdjustment > 0

da839264c780d7a191b70a350b8cf6b4.png

2、lengthAdjustment < 0 在大多数情况下,length字段表示消息正文的长度,但是有些协议,其长度表示的是整个消息的长度,故Netty为了适配这种情况,可以通过 lengthAdjustment 设置为负数,来调节数据帧的大小。

7f3d106ce731eafb2093051048eeb965.png

总结:lengthAdjustment 的出现是Netty为了适配现有的协议而设计出来的字段,即 Netty LengthFieldBasedFrameDecoder 是为了i给 header + body ,并且基于长度字段的协议一种通用的解决方案,可以通过 lengthAdjustment 来准确表示数据帧(业务数据的长度),这里是一种逆向思维


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


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


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

5af8464b15ffcf91fb44e048947d4067.png

相关文章
|
5月前
|
消息中间件 缓存 安全
清华架构大牛剖析高并发与多线程的关系、区别,带你击穿面试难题
当提起这两个词的时候,是不是很多人都认为高并发=多线程? 当面试官问到高并发系统可以采用哪些手段来解决,是不是一脸懵逼?
|
7月前
|
Java 程序员
终于不慌内卷了,多亏阿里内部的并发图册+JDK源码速成笔记
并发编程 Java并发在近几年的面试里面可以说是面试热点,每个面试官面试的时候都会跟你扯一下并发,甚至是高并发。面试前你不仅得需要弄清楚的是什么是并发,还得搞清什么是高并发! 在这里很多小白朋友就会很疑惑:我工作又不用,为啥面试总是问?真就内卷卷我呗!(手动狗头)互联网内卷已经是现在的行业趋势,而且是不可逆的,这个大家也知道;但LZ要说的是,虽然简单地增删改查并不需要并发的知识,但是业务稍微复杂一点,你的技术水平稍微提升一点的话你就会知道,并发是我们Java程序员绕不开的一道坎。
35 0
|
8月前
|
前端开发 JavaScript 小程序
预备金九银十,这套前端面试小册阁下请收好
预备金九银十,这套前端面试小册阁下请收好
57 0
|
8月前
|
消息中间件 缓存 Java
牛掰!阿里人用7部分讲明白百亿级高并发系统(全彩版小册开源)
高并发 提到“高并发”相信你们应该都不会感到陌生!此时你脑中应该会浮现好多有关高并发的:业务急剧增长、电商购物、电商秒杀、12306抢票、淘宝天猫各种活动等;都是需要用到高并发的,那么如何去设计一个高并发系统抵挡这些冲击呢? 其实这也是一道很常见的面试题,但是大多数应聘者都不知如何回答,从何答起。对于一个Java程序员来讲,,更关注的是不是系统架构层面的呢?从原本的定时秒杀,到现在各种活动的预热、拼团、定金膨胀、百亿补贴、跨店满减以及更复杂的组合优惠,让用户摸不到头脑,虽然这些都扰乱了用户购买的节奏,但是也一直保持着持续升温的状态。
|
10月前
|
SQL 算法 Java
直击灵魂!美团大牛手撸并发原理笔记,由浅入深剖析JDK源码
并发编程这四个字想必大家最近都在网上看到过有很多的帖子在讨论。我们都知道并发编程可选择的方式有多进程、多线程和多协程。在Java中,并发就是多线程模式。而多线程编程也一直是一个被广泛而深入讨论的领域。如果遇到复杂的多线程编程场景,大多数情况下我们就需要站在巨人的肩膀上利用并发编程框架——JDK Concurrent包来解决相关线程问题。
|
10月前
|
缓存 Java 程序员
肝到头秃!百度强推并发编程笔记我爱了,原来这才叫并发
随着Java程序员的大幅增长,人们对Java程序员的要求也是越来越严苛。从现在Java岗的招聘需求来看,并发编程已经是我们Java程序员避不开的坎了! 编写正确的程序并不容易,而编写正确的并发程序就更难了。与顺序执行的程序相比,并发程序中显然更容易出现错误。而且并发性错误通常并不会以某种确定的方式显现出来。
|
11月前
|
算法
谈一谈|浅谈单纯形法其中奥妙
谈一谈|浅谈单纯形法其中奥妙
121 0
|
缓存 Oracle NoSQL
【面试1v1实景模拟】Spring事务经典面试场景,全方位解读面试官心理,助你面试入坑~
【面试1v1实景模拟】Spring事务经典面试场景,全方位解读面试官心理,助你面试入坑~
130 0
面试官:小伙子我们先来唠唠并发编程的几大核心知识点
并发编程算是Java的一个难点,经常做业务相关的程序员基本上用不到juc的包,但是这些知识点十分重要,所以不管在哪里,时刻保持学习真的很重要。
|
前端开发
记一道前端高难度面试题
记一道前端高难度面试题
记一道前端高难度面试题