编程中模块间经常需要通信,通信的基础就是收发方之间需要共同遵循的协议,本文记录相关内容。
在计算机体系中,存在着很多的网络通信协议;通信协议的实际上就是一段数据,通信双方按照提前约定的规则去进行编码解码,达到传输数据的目的;例如,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 数字,而反序列化就是按照这种规则再去把字节流转换成对应的类型数据。这些都是基于某种规则的基础上,使用技术的手段去封装的结果。
首先,我们先来了解协议在整个通讯当中的扮演的角色,如下图所示:
客户端和服务端之间的通讯流程:
客户端发送数据
服务端接受数据
由此可见,协议主要是管理字节流格式的一种规则,如果把协议环节去掉,那么服务端就无法知道字节流的结束位置。
协议介绍:
这算是比较简单,并且常用的设计思路,主要和 Netty 内置的基于长度域拆包器类似,基本上都是有一个字段是用来存储真实的数据长度,这样才能准确的读取数据的完整内容。当然,还可以在该设计基础上加上更多的字段,比如:使用的序列号技术、协议版本号等等。
一个最基本的网络协议必须包含
了解 TCP
协议的同学一定听说过粘包、拆包
这两个术语。因为TCP
协议是数据流协议,它的底层根据二进制缓冲区的实际情况进行包的划分。所以,不可避免的会出现粘包,拆包
现象 。为了解决它们,我们的网络协议往往会使用一个 4 字节的 int
类型来表示数据的大小。比如,Netty
就为我们提供了 LengthFieldBasedFrameDecoder
解码器,它可以有效的使用自定义长度帧来解决上述问题。
同时一个好的网络协议,还会将动作和业务数据分离。试想一下, HTTP
协议的分为请求头,请求体——
Http Method
、HTTP
版本这就是一种分离关注点的思想。所以自定义的网络协议也可以包含:
code
来分门别类的代表不同的业务逻辑JAVA
对象和二进制之间转换的形式,提供多种序列化/反序列化方式。比如 json
、protobuf
等等,甚至是自定义算法。比如:rocketmq
等等。同时,协议的开头可以定义一个约定的魔数
。这个固定值(4字节),一般用来判断当前的数据包是否合法。比如,当我们使用 telnet
发送错误的数据包时,很显然,它不合法,会导致解码失败。所以,为了减轻服务器的压力,我们可以取出数据包的前4
个字节与固定的魔数
对比,如果是非法的格式,直接关闭连接,不继续解码。
此类协议用于模块间数据传输,协议中会有信令和数据块。例如,VoIP通话中的数据转发场景,协议中的信令字段描述数据块的来源、目的地。数据块是经过编码的音频帧,几十毫秒就能编出一个数据块,对转发模块性能挑战较高,协议设计侧重于高性能。协议格式如下。