一、通信协议概述
通信协议是两个节点之间为了协同工作、实现信息交换,而协商的规则和约定,例如规定字节序,各个字段类型,使用什么压缩算法或加密算法等。
1、原始数据
假设A和B通信,获取或设置用户基本资料,一般开发人员第一步就是定义一个协议结构:
struct userbase
{
unsigned short cmd; //1-get, 2-set
unsigned char gender; //1 – man , 2-woman
char name[8];
};
在这种方式下,A基本不用编码,直接从内存copy出来,再做一下网络字节序变换,发送给B,B也能解析。
这种编码方式,除了数据本身外,没有一点额外冗余信息,可以看成是Raw Data。
2、版本号控制
有一天,A在基本资料里面加一个生日字段,然后告诉B:
struct userbase
{
unsigned short cmd;
unsigned char gender;
unsigned int birthday;
char name[8];
};
可是当B收到A的数据包后,并不知道第3个字段到底是旧协议中的name字段,还是新协议中的birthday。
于是他们意识到,一个好的协议应该具有兼容性和可扩展性。
他们决定制定一个以后每个版本兼容的新协议。方法很简单,就是加一个version字段:
struct userbase
{
unsigned short version;
unsigned short cmd;
unsigned char gender;
unsigned int birthday;
char name[8];
};
这样子以后就可以很方便的扩展,通过版本号来做不同的解析处理。
3、使用tag
过了一段时间,A和B发现又有新的问题:每增加一个字段就要改变一下版本号,这样代码维护起来相当麻烦,每个版本一个case分支,到了最后,代码里面几十个case分支,看起来丑陋而且维护成本相当高。
于是他们决定为每个字段增加一个额外信息来作为一个字段的唯一标识——tag,虽然增加内存和带宽,但是可以容许这些冗余,换取易用性。
struct userbase
{
1 unsigned short version;
2 unsigned short cmd;
3 unsigned char gender;
4 unsigned int birthday;
5 char name[8];
};
有了tag之后,每个字段都有唯一标识,双方通过tag即可知道第几个字段代表的是什么。于是我们就可以自由地增加字段了,只要保证tag不修改即可。注意一般不删除字段,因为删除字段后tag可能会不小心被复用了,从而类型不匹配导致解码失败。
4、强扩展性的TLV
后来他们发现,name使用8个字节不够用,最大长度可能会达到100个字节。如果每次都按照100个字节这种固定长度打包,太浪费流量了。
于是他们决定使用**<Tag,Length,Value>**三元组编码,简称TLV编码。其中Value字段是可以嵌套的。
TLV具备了很好可扩展性,但是由于其增加了2个额外的冗余信息 Tag 和 Length,特别是如果协议大部分是基本数据类型int ,short, byte,会浪费几倍存储空间。另外Value具体是什么含义,需要通信双方事先得到描述文档,即TLV不具备结构化和自解释特性。
5、自解释性的TTLV
TT[L]V是 <Tag,Type,Length,Value> 四元组编码,其中,当type是定长的基本数据类型如int, short, long, byte时,因为其长度是已知的,所以L不需要。
于是我们可以定义一些type值如下:
类型 | Type值 | 类型描述 |
---|