本文最后更新于:2024年7月29日 上午

编程中模块间经常需要通信,通信的基础就是收发方之间需要共同遵循的协议,本文记录相关内容。

背景

在计算机体系中,存在着很多的网络通信协议;通信协议的实际上就是一段数据,通信双方按照提前约定的规则去进行编码解码,达到传输数据的目的;例如,TCP/IP是目前计算机设备最常用的通信协议;TCP/IP实际上是一个协议族,包含一组协议,其中靠近应用层且最常用的协议是TCP和UDP。

TCP是流式协议,即协议的内容是流水一样的字节流,内容与内容之间没有明确的分解标志,需要人为的给这些内容划分边界;例如,A与B进行TCP通讯,A发送两个数据包给B,大小分别为100个字节,200个字节,对于A来说,作为发送方,是知道如何划分这两个数据包的界限的,但是对于B来说,可能一次,或者多次受到A发送的数据包,例如先收到50字节,后受到250字节,或者先收到200字节,后收到100字节,因此B是无法知道应该将受到的多少个字节的数据包作为一个有效的数据包;而规定每次将多少个字节作为有效的数据包,就是协议格式需要定义的内容。

协议

首先,我们大概了解什么是协议,协议可以把它认为是一种规则而不是技术,约束客户端和服务端之间通讯,数据组装和拆分的一种规范。客户端安装某种规范去组装数据,把数据传输给服务端,服务端再安装这种规范拆解数据,那么这就是一种协议,可以根据实际业务区指定符合自身的协议,其实基于 Netty 去制定的私有协议,我个人接触过的是传输车辆 GPS 数据的 809 协议,在和 GPS 服务器通讯时,必须按照该协议去进行封装和解析数据,否则通讯异常。

其实,类似的规则还有很多,从开发的角度来说,都是各种规则和约束,比如说:前面提到的序列化技术,序列化其实就是把数据按照某种规则去转换成 byte 数字,而反序列化就是按照这种规则再去把字节流转换成对应的类型数据。这些都是基于某种规则的基础上,使用技术的手段去封装的结果。

通讯协议

首先,我们先来了解协议在整个通讯当中的扮演的角色,如下图所示:

客户端和服务端之间的通讯流程:

客户端发送数据

  1. 客户端先把一个对象序列化成字节流;
  2. 然后把字节流根据协议把字节流组装好;
  3. 最后转换成二进制传输到网络。

服务端接受数据

  1. 从网络中读取二进制数据到本地的缓冲区;
  2. 根据协议的规则读取指定数据,并且识别是否是完整的数据包;
  3. 如果是完整的数据包,则转换成实体对象。

由此可见,协议主要是管理字节流格式的一种规则,如果把协议环节去掉,那么服务端就无法知道字节流的结束位置。

协议设计

协议介绍:

  1. 协议标识符,以一个固定数作为标识符,占用 4 个字节,主要目的是用来识别协议的开头,只要是以该标识开头的协议则进行处理,否则不处理。主要目的是提高处理性能问题,如果随便一个请求都需要进行处理,但是最终处理起来发现协议格式不对,抛异常,肯定会影响系统性能;
  2. 数据长度,占用 4 个字节,标识数据的真实长度,获取到该值后,往后读取指定长度的数据即可。主要目的是防止粘包和拆包安全性问题;
  3. 指令,协议是某个应用所有的业务公用的一种规则,那么应该如何区分是哪种业务呢?这里主要通过指令来进行区分;
  4. 数据,这部分存储的是真实的数据。

这算是比较简单,并且常用的设计思路,主要和 Netty 内置的基于长度域拆包器类似,基本上都是有一个字段是用来存储真实的数据长度,这样才能准确的读取数据的完整内容。当然,还可以在该设计基础上加上更多的字段,比如:使用的序列号技术、协议版本号等等。

网络协议的设计

概要划分

一个最基本的网络协议必须包含

  • 数据的长度
  • 数据

了解 TCP 协议的同学一定听说过粘包、拆包 这两个术语。因为TCP协议是数据流协议,它的底层根据二进制缓冲区的实际情况进行包的划分。所以,不可避免的会出现粘包,拆包 现象 。为了解决它们,我们的网络协议往往会使用一个 4 字节的 int 类型来表示数据的大小。比如,Netty 就为我们提供了 LengthFieldBasedFrameDecoder 解码器,它可以有效的使用自定义长度帧来解决上述问题。

同时一个好的网络协议,还会将动作和业务数据分离。试想一下, HTTP 协议的分为请求头,请求体——

  • 请求头:定义了接口地址、Http MethodHTTP 版本
  • 请求体:定义了需要传递的数据

这就是一种分离关注点的思想。所以自定义的网络协议也可以包含:

  • 动作指令:比如定义 code 来分门别类的代表不同的业务逻辑
  • 序列化算法:描述了 JAVA 对象和二进制之间转换的形式,提供多种序列化/反序列化方式。比如 jsonprotobuf 等等,甚至是自定义算法。比如:rocketmq 等等。

同时,协议的开头可以定义一个约定的魔数。这个固定值(4字节),一般用来判断当前的数据包是否合法。比如,当我们使用 telnet 发送错误的数据包时,很显然,它不合法,会导致解码失败。所以,为了减轻服务器的压力,我们可以取出数据包的前4个字节与固定的魔数对比,如果是非法的格式,直接关闭连接,不继续解码。

数据传输协议

此类协议用于模块间数据传输,协议中会有信令和数据块。例如,VoIP通话中的数据转发场景,协议中的信令字段描述数据块的来源、目的地。数据块是经过编码的音频帧,几十毫秒就能编出一个数据块,对转发模块性能挑战较高,协议设计侧重于高性能。协议格式如下。

  • Header:固定头部,二进制形式,通常是一字节对齐的结构体,用于描述协议类型、协议中各部分的长度、数据校验值。

  • Meta: 用于描述协议本身例如,加密方式,包类型(请求包/响应包),以及协议应用层部分的描述。例如,Body的编码格式、Attachment的校验值,也会进行序列化/反序列化。

  • Body:通常是序列化的信令结构,使用Protobuf、json、thrift 格式进行序列化、压缩。

  • Attachment:业务层传递的二进制数据,不经过序列化,直接通过网络发送,避免序列化带来额外的性能开销。

参考资料



文章链接:
https://www.zywvvd.com/notes/protocol/network-protocol/network-protocol/


“觉得不错的话,给点打赏吧 ୧(๑•̀⌄•́๑)૭”

微信二维码

微信支付

支付宝二维码

支付宝支付

协议设计
https://www.zywvvd.com/notes/protocol/network-protocol/network-protocol/
作者
Yiwei Zhang
发布于
2024年7月29日
许可协议