序列化之 TLV

通信协议可以理解两个节点之间为了协同工作实现信息交换,协商一定的规则和约定,例如规定字节序,各个字段类型,使用什么压缩算法或加密算法等。常见的有 tcp,udo,http,sip 等常见协议。协议有流程规范和编码规范。流程如呼叫流程等信令流程,编码规范规定所有信令和数据如何打包/解包。

编码规范就是我们通常所说的编解码,序列化。不光是用在通信工作上,在存储工作上我们也经常用到。如我们经常想把内存中对象存放到磁盘上,就需要对对象进行数据序列化工作。

本文采用先循序渐进,先举一个例子,然后不断提出问题-解决完善,这样一个迭代进化的方式,介绍一个协议逐步进化和完善,最后总结。看完之后,大家以后在工作就很容易制定和选择自己的编码协议。

1. 紧凑模式

本文例子是 A 和 B 通信,获取或设置基本资料,一般开发人员第一步就是定义一个协议结构:

struct userbase {
unsigned short cmd; // 1-get, 2-set, 定义一个short,为了扩展更多命令(理想那么丰满)
unsigned char gender; // 1 – man , 2-woman, 3 - ??
char name[8]; // 当然这里可以定义为 string name;或len + value 组合,为了方便叙述使用简单定长
}

在这种方式下,A 基本不用编码,直接从内存 copy 出来,再把 cmd 做一下网络字节序变换,发送给 B。B 也能解析,一切都很和谐愉快。

这时候编码结果可以用图表示为(1格一个字节)

这种编码方式,称之为紧凑模式,意思是除了数据本身外,没有一点额外冗余信息,可以看成是 Raw Data。在 dos 年代,这种使用方式非常普遍,那时候可是内存和网络都是按 K 计算,cpu 还没有到 1G。如果添加额外信息,不光耗费捉襟见肘的 cpu,连内存和带宽都伤不起。

2. 可扩展性

有一天,A 在基本资料里面加一个生日字段,然后告诉 B

struct userbase {
unsigned short cmd;
unsigned char gender;
unsigned int birthday;
char name[8];
}

这是 B 就犯愁了,收到 A 的数据包,不知道第 3 个字段到底是旧协议中的name 字段,还是新协议中 birthday。这是后 A,和 B 终于从教训中认识到一个协议重要特性——兼容性和可扩展性。

于是乎,A 和 B 决定废掉旧的协议,从新开始,制定一个以后每个版本兼容的协议。方法很简单,就是加一个 version 字段。

struct userbase {
unsigned short version;
unsigned short cmd;
unsigned char gender;
unsigned int birthday;
char name[8];
}

这样,A 和 B 就松一口气,以后就可以很方便的扩展。增加字段也很方便。这种方法即使在现在,应该还有不少人使用。

3. 更好的可扩展性

过了一段较长时间,A 和 B 发现又有新的问题,就是没增加一个字段就改变一下版本号,这还不是重点,重点是这样代码维护起来相当麻烦,每个版本一个 case 分支,到了最好,代码里面 case 几十个分支,看起来丑陋而且维护起来成本高。

A 和 B 仔细思考了一下,觉得光靠一个 version 维护整个协议,不够细,于是觉得为每个字段增加一个额外信息 - tag,虽然增加内存和带宽,但是现在已经不像当年那样,可以容许这些冗余,换取易用性。

struct userbase {
unsigned short version;
unsigned short cmd;
unsigned char gender;
unsigned int birthday;
char name[8];
}

制定完这些协议后,A 和 B 很得意,觉得这个协议不错,可以自由的增加和减少字段。随便扩展。

现实总是很残酷的,不久就有新的需求,name 使用 8 个字节不够,最大长度可能会达到 100 个字节,A 和 B 就愁怀了,总不能即使叫 “steven” 的人,每次都按照 100 个字节打包,虽然不差钱,也不能这样浪费。

于是 A 和 B 寻找各方资料,找到了 ANS.1 编码规范,好东西啊.. ASN.1 是一种 ISO/ITU-T 标准。其中一种编码 BER(Basic Encoding Rules)简单好用,它使用三元组编码,简称 TLV 编码

每个字段编码后内存组织如下

Tag | Length | Value

字段可以是结构,即可以嵌套

A 和 B 使用 TLV 打包协议后,数据内存组织大概如下:

TLV 具备了很好可扩展性,很简单易学。同时也具备了缺点,因为其增加了 2 个额外的冗余信息,tag 和 len,特别是如果协议大部分是基本数据类型 int ,short, byte. 会浪费几倍存储空间。另外 Value 具体是什么含义,需要通信双方事先得到描述文档,即 TLV 不具备结构化和自解释特性。

4. 自解释性

当 A 和 B 采用 TLV 协议后,似乎问题都解决了。但是还是觉得不是很完美,决定增加自解释特性,这样抓包就能知道各个字段类型,不用看协议描述文档。这种改进的类型就是 TT[L]V(tag,type,length,value),其中 L 在 type 是定长的基本数据类型如 int,short, long, byte 时候,因为其长度是已知的,所以 L 不需要。

于是定义了一些 type 值如下

类型 Type值 类型描述
bool 1 布尔值
int8 2 带符号的一个字符
uint8 3 带符号的一个字符
int16 4 16位有符号整型
uint16 5 16位无符号整型
int32 6 32位有符号整型
uint32 7 32位无符号整型
...
string 2 字符串或二进制序列
struct 13 自定义的结构,嵌套使用
list 14 有序列表
map 15 无序列表

按照 ttlv 序列化后,内存组织如下

改完后,A 和 B 发现,的确带来很多好处,不光可以随心所以的增删字段,还可以修改数据类型,例如把 cmd 改成 int cmd;可以无缝兼容。真是太给力了。

5. 跨语言特性

有一天来了一个新的同事 C,他写一个新的服务,需要和 A 通信,但是 C 是用 java 或 PHP 的语言,没有无符号类型,导致负数解析失败。为了解决这个问题,A 重新规划一下协议类型,做了有些剥离语言特性,定义一些共性,对使用类型做了强制性约束。虽然带来了约束,但是带来通用型和简洁性,和跨语言性,大家表示都很赞同,于是有了一个类型(type)规范。

类型 Type值 类型描述
bool 1 布尔值
int8 2 带符号的一个字符
int16 3 16位有符号整型
int32 4 32位有符号整型
...
string 2 字符串或二进制序列
struct 13 自定义的结构,嵌套使用
list 14 有序列表
map 15 无序列表

6. 代码自动化:IDL语言

但是 A 和 B 发现了新的烦恼,就是每搞一套新的协议,都要从头编解码,调试,虽然 TLV 很简单,但是写编解码是一个毫无技术含量的枯燥体力活,一个非常明显的问题是,由于大量 copy/past,不管是对新手还是老手,非常容易犯错,一犯错,定位排错非常耗时。于是 A 想到使用工具自动生成代码。

IDL(Interface Description Language),它是一种描述语言,也是一个中间语言,IDL 一个使命就是规范和约束,就像前面提到,规范使用类型,提供跨语言特性。通过工具分析 idl 文件,生成各种语言代码。

Gencpp.exe sample.idl 输出 sample.cpp sample.h
Genphp.exe sample.idl 输出 sample.php
Genjava.exe sample.idl 输出 sample.java

7. 总结

大家看到这里,是不是觉得很面熟。是的,协议讲到最后,其实就是和 facebook 的 thrift 和 google protocol buffer 协议大同小异了。包括公司无线使用的 jce 协议。咋一看这些协议的 idl 文件,发现几乎是一样的。只是有些细小差异化。

这些协议在一些细节上增加了一些特性:

  1. 压缩,这里压缩不是指 gzip 之类通用压缩,是指针对整数压缩,如 int 类型,很多情况下值是小于 127(值为 0 的情况特别多),就不需要占用 4 个字节,所以这些协议做了一些细化处理,把 int 类型按照情况,只使用 1/2/3/4 字节,实际上还是一种 ttlv 协议。

  2. reuire/option 特性: 这个特性有两个作用,一是压缩,有时候一个协议很多字段,有些字段可以带上也可以不带上,不赋值的时候不是也要带一个缺省值打包,这样很浪费,如果字段是 option 特性,没有赋值的话,就不用打包。二是约束功能,规定哪些字段必须有,加强校验。

序列化是通信协议的基础,不管是信令通道还是数据通道,还是 rpc,都需要使用到。在设计协议早期就考虑到扩展性和跨语言特性。会为以后省去不少麻烦。

Ps

本篇主要介绍二进制通信协议序列化,没有讲文本协议。从某种意义来讲,文本协议天生具有兼容和可扩展性。不像二进制需要考虑那么多问题。文本协议易于调试(如抓包就是可见字符,telnet 即可调试,数据包可以手工生成不借助特殊工具),简单易学是其最强大的优势。二进制协议优势就是性能和安全性。但是调试麻烦。

两者各有千秋,按需选择

参考:

本文转发至 https://www.jianshu.com/p/fb183509f14d

https://www.jianshu.com/p/73c9ed3a4877

https://www.jianshu.com/p/72108f0aefca


每天用心记录一点点。内容也许不重要,但习惯很重要!

最新文章

  1. [BZOJ1251]序列终结者
  2. @interface NSFileManager : NSObject
  3. java对xml文件做增删改查------摘录
  4. 记一次https访问握手失败(handshake failure)
  5. CCF 201312-2 ISBN号码 (水题)
  6. 排序算法_HeapSort
  7. R语言——包的添加和使用
  8. hadoop集群中的日志文件
  9. 微信分享jssdk config:invalid signature 签名错误
  10. Controller 中Action 返回值类型 及其 页面跳转的用法
  11. 前端项目经验总结之js防缓存(避免缓存的影响)
  12. composer安装出现proc_open没有开启问题的解决方案
  13. 如何对n个大小都小于100的整数进行排序,要求时间复杂度O(n),空间复杂度O(1)。
  14. Win2012 R2安装 sqlserver2017 Express
  15. React 中 Link 和 NavLink 组件 activeClassName、activeStyle 属性不生效的问题
  16. 依赖注入框架Ninject
  17. Kotlin入门教程——目录索引
  18. Http如何利用304让浏览器去更新数据
  19. 杭电acm2059-龟兔赛跑 java
  20. [转]Python多线程与多线程中join()的用法

热门文章

  1. POJ 1797 Heavy Transportation (Dijkstra)
  2. redis zset 介绍
  3. python操作DB2数据库
  4. 12-vim-撤销和删除命令-02-删除文本
  5. loj6626 幼儿园唱歌题
  6. 晒订单赢图灵图书,《第一行代码——Android》福利活动劲爆来袭!
  7. CSS2 从入门到精通
  8. 【记录】Http 状态码整理
  9. redis 发布订阅(pub/sub )
  10. Docker部署web环境之Lanmt