0 网络数据协议
协议包的格式,json, msgpack, protobuf 以及自定义格式’
项目的网路层在建设中,除了 选择长短连接TCP,UDP,HTTP方式 外
还需要选择在 传输过程中使用什么样的协议格式 。
我的理解在于.
1. 链接方式: TCP UDP HTTP Socket
2. 协议格式: 表示数据的格式.如何进行打包. 解析.
在传输层和应用层之上的逻辑层中,信息传递格式的选择与利弊
我们将深入剖析 JSON
, MessagePack
,Protobuf
的原理. 使我们对网络数据协议的理解更加透彻清晰。
- 包括它们都是由什么组成的
- 怎么序列化的
- 怎么反序列化的
- 数据格式究竟是由哪些元素构成的,
1 JSON
JSON:JavaScript 对象表示法(JavaScript Object Notation)。
它是存储和交换文本信息的语法。类似 XML,但比 XML 更小、更快,更易解析。
JSON 是轻量级的文本数据交换格式,它独立于语言,具有自我描述性,更易理解。
JSON 是属于纯文本,具有“自我描述性”(人类可读),具有层级结构(值中存在值)
JSON 文本的 MIME 类型是 “application/json” (MIME (Multipurpose Internet Mail Extensions) 是描述消息内容类型的因特网标准。MIME 消息能包含文本、图像、音频、视频以及其他应用程序专用的数据。)
JSON 解析器可以用些比较常用的,比如simpleJson,MiniJson,DataContractJsonSerializer,JArray,JObject等等,都是非常通用高效的插件。
2 自定义二进制流协议格式
大部分的网络协议都具有一定的通用性,JSON是最典型的案例,XML,MessagePack,Protobuf都具有一定的通用性,但自定义二进制流协议格式则不是,它完全不通用,因为它不需要顾及通用性。
我们在存储一串数据的时候,无论这串数据里包含了哪些数据以及哪些数据类型,
当我们拿到这串数据在解析的时候能够知道该怎么解析,这是定义协议格式的目标。
简单的来说就是,当你传给我一串数据的时候,我是用什么样的规则知道这串数据里的内容的。
JSON就制定了这么一个规则,这个规则以字符串KEY-VALUE,以及一些辅助的符号‘{’,’}’,’[’,’]'组合而成,这个规则非常通用,以至于任何人拿到任何JSON数据都能知道里面有什么数据。
自定义二进制流协议格式则不具有通用性,不是任何人拿到数据都能知道里面装的是什么的,有且只有两端在私下协定的双方才知道该如何解析收到的数据。
一个自定义二进制流协议格式,分成三部分:
数据大小|协议编号|具体数据
数据大小、协议编号、具体数据
这三者构成了一个完整的协议内容,这一整个协议内容少了谁都不成,不过有时数据大小和协议编号的前后顺序可以交换。
我们举例来描述这个协议格式,假设我们客户端有这样一个数据结构需要传输到服务端去:
struct test
{
int test1;
float test2;
bool test3;
}
服务端拿到数据时,其实完全不知道当前拿到的数据是什么,也不知道数据是否完整,有可能只拿到一半的数据,或者一部分的数据。
首先我们要确定的是,我们收到的数据包它的 完整的大小有多大
- 只有知道完整的包体大小才能确定我当前收到的数据在大小上是否完整,我们是要等待继续接受后面的数据,还是现在就可以进行解析操作了。
为了确定包的完整性,我们必须先向二进制流中读取4个比特,组合成一个无符号整数,这个整数总共32位,也就是说我们的数据包的大小最大为2的32次减1个byte,然后再用来确定接下来的完整包体大小。
例如我现在接受到了20个byte,读取前4个,组成一个整数后为24,说明我接受到的后面16个byte是一个不完整的包体,我应该继续等待后续的数据到来。
其次我们要确定的是收到的数据包是属于哪个格式的协议。
于是再读取4个byte大小的数据,组成一个无符号整数,用来确定协议号。比如这个无符号整数位为1002,就代表是编号为1002的协议。
假设我们上面这个test结构的协议号是1002,那么接下来连着这个协议号的所有数据直到包体大小的末尾,都是这个test结构的数据。
在解析这个具体数据的时候,要根据生成这个数据的顺序来解析。
假设在生成这个二进制流数据时,我们的顺序是,先推入test1,再推入test2,再推入test3。test1是4个byte的整数,test2是4个byte的浮点数,test3是1个byte的布尔值,于是就有了
xxxx|xxxx|x
这样一个形状的二进制流,每个‘x’为一个byte,这里4个byte组成一个int或float数据,1个byte组成布尔数据,‘|’只是为了解释说明用的分隔符不存在于数据内,这个数据其实就是由9个byte组成,其中前4个为test1,中间4个为test2,后面1个为test3。
那么在网络传输过程中整个test结构的数据包格式为如下:
13|1002|test1|test2|test3
13为接下来的数据大小,1002位协议编号,test1|test2|test3为具体数据。
我们在解析的时候也需要按照生成时的顺序来解析,先读取前4个byte组成一个整数赋值给test1,接着再读取4个byte组成一个浮点数赋值给test2,接着再读取1个byte赋值给test3,完成数据解析。
对于数组形式的数据则要在原来的基础上多增加一个长度标志,比如 int[]类型数据,在生成时先推入一个长度,再连续推入所有内容,在解析的时候做同样的反向操作,先读取4个byte的长度标志,再对连续读取N个具体数据,这个N为长度标志。
举例int[]为3个整数数组则二进制为如下效果:
xxxx|xxxx|xxxx|xxxx
前4个byte为长度,接着3次4个byte为数组内的整数数据。
自定义二进制流协议格式为 最不通用的格式 .
但也是 最节省流量的协议
- 因为每个数据都可以用最小的方式进行定义,比如协议号不需要4个byte,2个byte大小2的16次-1就够用了,长度有可能也不需要4个byte,只要2个甚至1个byte就够用了,有些数据不需要4个byte组成int整数,只需要2个byte数组short就够用了,甚至有些可以组合起来使用,比如协议结构中有4个bool,可以拼成一个byte来传递,
- 这些都可以完全由我们来控制包体的大小不受到任何规则的限制,这也是自定义二进制协议格式最吸引人的地方。
自定义二进制流协议格式 最大的缺点是不通用
- 当我们需要更换一个协议格式的时候,旧的协议格式就无法解析了,特别是当新的协议解析旧的协议时就会报错。
- 我们也可以做些补救这种问题的措施,为了能让旧的协议格式还能继续使用,我们在每个数据头部都加入一个2个byte的整数代表 版本号,由版本号来决定该读取哪个版本的协议,这样旧的协议也照样可以兼容新的协议,只是处理起来的时候需要注意些初始化问题,那些旧协议没有的而新协议有的数据则要尽可能的初始化成默认值以免造成逻辑报错。
3 MessagePack
MessagePack 是一个 介乎于JSON和自定义二进制流之间的协议格式 ,他的理念是 ‘It’s like JSON. but fast and small.’ 。
与JSON相同的是MessagePack也有采用Key-Value形式的Map映射类型
不同的是MessagePack用byte形式存储整数、浮点数、布尔值,
并且在Map映射类型外加入了更多单独类型(非KEY-VALUE形式)的数据类型
其中也包括了自定义二进制流的数据类型。
其中map映射类型是比较常用,也是比较通用的存储形式类型,也因为它的通用性被很多程序员所喜爱。
使用起来能和JSON用起来一样,并且数据大小比JSON小,解析速度又比JSON快,是MessagePack最大的特点。
非map类型的数据
非map类型的数据, 其实和自定义二进制流的存储方式差不多,只是把原来的 ‘数据大小|数据’ 的形式改为了 ‘类型|数据’.
比如存储一个4个byte也就是32位的整数:
+--------+--------+--------+--------+--------+
| 0xd2 |ZZZZZZZZ|ZZZZZZZZ|ZZZZZZZZ|ZZZZZZZZ|
+--------+--------+--------+--------+--------+
第一个byte的值0xd2代表32位整数类型,它表示后面4个byte组合起来是整数类型的数据。
再举个列子,32位的浮点数:
+--------+--------+--------+--------+--------+
| 0xca |XXXXXXXX|XXXXXXXX|XXXXXXXX|XXXXXXXX|
+--------+--------+--------+--------+--------+
第一个byte的值0xca代表32位浮点数类型,它表示后面4个byte组合起来是浮点数类型的数据。
以此类推,nil,bool,8位无符号整数,16位无符号整数,32位无符号整数,64位无符号整数,8位有符号整数,16位有符号整数,32位有符号整数,64位有符号整数等,以及32位浮点数,64位浮点数,都用这种类似的方式表示。
其实用MessagePack并不是冲着这些单独的数据类型去的,因为这些单独的数据类型完全可以用自定义二进制流代替,我们最关心的其实是它的map类型数据。
map类型数据
我们专门来看看,MessagePack的map类型的存储机制,为什么就比JSON快,为什么就比JSON小,它是如何存储和解析的。
在map之前我们看看数组类型的格式:
+--------+--------+--------+~~~~~~~~~~~~~~~~~+
| 0xdc |YYYYYYYY|YYYYYYYY| N objects |
+--------+--------+--------+~~~~~~~~~~~~~~~~~+
第一个byte的值0xdc代表是个总共可以存储16位长度的数组. 也就是最大为2的16次-1个元素的数组
后面2个byte组合起来成为一个无符号的整数代表后面有多少个元素,接着后面N个为相同类型的元素的数据。
假设说这N个为32位整数类型,那么就是如下格式
+--------+--------+--------+~~~~~~~~~~~~~~~~~+
| 0xdc |00000000|00000011| 0xd2|00001001|0xd2|00001101|...(3 objects)
+--------+--------+--------+~~~~~~~~~~~~~~~~~+
一个数组中指定了数组类型,以及数组元素的个数,
接下来的数据就是单个元素的数据了,每个数据都包含了 ‘类型|数据’ 格式。
其实map类型就是Array数组类型的变种,在数组类型上每个元素,多加了个KEY字符串,我们来看下map的格式:
+--------+--------+--------+~~~~~~~~~~~~~~~~~+
| 0xde |YYYYYYYY|YYYYYYYY| N*2 objects |
+--------+--------+--------+~~~~~~~~~~~~~~~~~+
第一个byte的值0xde代表是最大个数为16位的map类型数据,
接着2个byte组合起来表示有多少个元素,
再接着N乘2个元素为数据元素,其中以2个元素为一个组合,第一元素一定是字符串KEY,第二个元素为单独的任意的数据类型。
我们用官方的例子来分析下:
一个JSON类型的数据:
{"compact":true, "schema":0}
在MessagePack中的map格式为:
82|A7|'c'|'o'|'m'|'p'|'a'|'c'|'t'|C3|A6|'s'|'c'|'h'|'e'|'m'|'a'|00|
其中8位前半个byte的值代表是个15个以内的map类型数据
8后面的2是后半个byte的值,代表总共有2个元素。
接着A为前半个byte的值,代表是是个31个以内的字符串,A后面的7代表这个字符串拥有7个字符。
接着7个元素都是字符。
接着C3是KEY-VALUE的VALUE,这个VALUE是一个bool型的ture值。
接着A6,A为前半个byte代表是31个以内的字符串,A后面的6代表这个字符串有6个字符。
接着6个元素都是字符。
最后00,前面0为前半个byte,表示类型为7位以内的整数,接着的0位后半个byte,代表数据为0。
MessagePack整个map就是以这种“类型|数据”或者"类型|大小|数据"的方式存储。
由于存储的方式是顺序,所以在解析的时候不需要排序,不需要解析符号和类型,数据的类型直接可以用byte来表示,能用byte存储绝不用字符串形式存储,能减少byte使用个数的尽量减少byte的使用个数,能合并的尽量合并为一个byte。
因此MessagePack对于JSON来说,比JSON减少了大量的解析,比JSON减少了更多的数据空间,使得MessagePack能比起JSON来更快并且更小,就像它自己所说的那样 ‘It’s like JSON. but fast and small.’。
4 Protocol Buffer
待续比较重要!