互联网服务应用协议设计-借鉴备忘

0 我们为什么需要自己设计协议:

在互联网后台开发中,稍微复杂一些的业务,服务是必要的,进而协议也是必要的。那么我们是否可以复用已有的协议呢?主要是因为现在已有的协议都没有能完全match互联网后台开发的需求,存在这样或那样的问题。

1 协议设计的原则:

解析效率:互联网业务具有高并发的特点,解析效率决定了使用协议的CPU成本;

编码长度:信息编码出来的长度,编码长度决定了使用协议的网络带宽及存储成本;

易于实现:互联网业务需要一个轻量级的协议,而不是大而全的,CORBA这种重量级的协议就不太适合,易于实现决定了使用协议的开发成本和学习成本;

可读性:编码后的数据的可读性决定了使用协议的调试及维护成本;

兼容性:互联网的需求具有灵活多变的特点,协议会经常升级,使用协议的双方是否可以独立升级协议、增减协议中的字段是非常重要的。兼容性决定了持续开发时的开发成本;

2 协议设计需要解决的问题:

1) 序列化/反序列化

2) 判断包的完整性

 2.1 序列化/反序列化:

序列化我们常称之为编码,或者打包,反序列化常称之为解码,或者解包。常用的序列化/反序列化方式主要有以下几种:

1)  TLV编码及其变体(后面统称为TLV编码):Protobuf/thrift/ASN BER都属于这种。

TLV编码基本原理是每个字段打一个二进制包,每个包包含tag、length、value 3个部分:

tag: 一般占用1个字节,表示数据类型,有的编码方式(Protobuf/thrift)中tag包含字段的id,有的编码方式(ASN BER)不包含字段的id。包含字段id的序列化方式,id是字段的标志,协议可以灵活的增删字段,只要保证字段id唯一,就能兼容解析,非常适合互联网开发。

length:一个整数,表示后面数据块的长度,Protobuf/thrift的序列化不包含length字段,因为大部分数据类型的长度都可以根据tag中的类型信息可以得到。

value:真正的数据内容。

举个tag包含id的序列化方式打包解包的例子(只是举个例子说明原理,实际上Protobuf等协议都做了比较巧妙的实现,比如varint、ZigZag编码来尽量减少编码长度):

协议包括2个字段, name字段的id为0,类型为1(string);age字段的id为1,类型为2(unsigned int     )

字段id

字段类型

字段名

0

string

name

1

unsigned int

age

需要传输的数据:

name = "xxx"

age = 18

序列化之后大约是

字段类型(tag的一部分)

字段id(tag的一部分)

字段值(value)

0x01

0x00

xxx

0x02

0x01

0x12

反序列化的时候,逐步解析字节流,先解析字段类型和字段id,再根据字段类型解析出后面的数据内容,得到了一个id和值的映射关系

0 : "xxx"

1 : 18

根据协议,id=0的字段表示name,id=1的字段表示age,反序列化之后,就知道传过来的数据是

name = "xxx",age = 18了

如果协议做了升级,增加了1个字段“gender”,删除一个已经没有意义的字段age,协议变成

0 string name

2 string gender

需要传输的数据:

name = "xxx"

gender = "male"

发送方升级了协议,序列化之后大约是

字段类型

字段id

字段值

0x01

0x00

xxx

0x01

0x02

male

反序列化之后,得到了一个id和值的映射关系

0 : "xxx"

2 : "male"

反序列化的一方由于没有升级协议,不知道id=2的字段什么意思,直接忽略,没找到id=1的age字段,那么使用默认值,这样单方的升级,完全不影响协议的解析,协议是具有兼容性的。

举个tag不包含id的序列化方式打包解包的例子

如果tag中没有字段id,那么字段所在的位置决定字段的含义

协议包括2个字段, 第1个字段name,类型为1(string);第2个字段age类型为2(unsigned int )

字段类型

字段名

string

name

unsigned int

age

需要传输的数据:

name = "xxx"

age = 18

序列化之后大约是

字段类型

字段值

0x01

xxx

0x02

0x12

反序列化程序解析出第1个字段是字符串xxx,第二个字段是整数18,根据协议,第1个字段是name,第2个字段是age,这时反序列化程序就知道了name是xxx,age是18

但是相比上面有id的序列化方式,这种方式有个明显的缺陷:一方升级了协议时,另一方很可能需要升级协议才行,协议不具有兼容性。比如协议做了升级,增加了一个字段gender,删除一个已经没有意义的字段age,协议变成

string name

string gender

需要传输的数据:

name = "xxx"

gender = "male"

发送方升级了协议,序列化之后大约是

字段类型

字段值

0x01

xxx

0x01

male

这时接收方如果不升级协议就完全无法理解协议的含义

可以看出tag包含ID的序列化方式(Protobuf/thrift)兼容性和灵活性方面优于不包含ID的方式(asn-ber)

TLV编码的特点是:

解析效率高:主要是因为不需要转义字符

编码长度低:主要是因为元数据占用的空间很少

不易于实现:但是有很多开源的工具,根据IDL自动生成代码,提高开发效率

兼容性高:协议双方可以独立升级

可读性差:二进制协议,肉眼很难识别

2) 文本流编码:xml/json都属于这种。

基本原理是把每个字段打一个字符串形式的包,通过键值对(key-value)的方式存储数据,key是字段的名字,用于区分不同的字段(对比上面TLV编码采用id的方式标志一个字段),特殊字符特别是非文本字符需要做适当转义,转义为xml/json的合法字符。xml的解析效率低于json,而编码长度高于json,json作为序列化的方式一般是优于xml的。

同样是上面的协议:

序列化的结果大概是

<p><name>xxx</name><age>18</age></p>

或者

{name:xxx,age:18}

文本流编码的特点是:解析效率低,编码长度高,易于实现,可扩展性高,可读性好

 

3) 固定结构编码:

基本原理是,协议约定了传输字段类型和字段含义,和TLV的方式类似,但是没有了tag和len,只有value

同样是上面的协议:

序列化的结果大概是

xxx 0x00 0x12

反序列化的时候,根据协议中约定的字段位置、字段类型和字段含义,逐个解出相应的字段

固定结构编码如果协议升级了又需要保证兼容性,那么可以在协议中增加一个“版本号”字段,然后根据版本号决定如何序列化和反序列化,这样可以保证协议的兼容性。但是这样会导致代码非常混乱和让人费解

固定结构编码解析效率、编码长度、易于实现、可读性方面略微优于TLV方式,但是灵活性和兼容性非常差,如果不使用版本号判断就不能单方增删字段,不能单方修改字段数据类型,甚至,把协议中的short int字段改成int,反序列化就可能会出错,因此除了业务逻辑非常固定的场景外不推荐使用。

4) 内存dump:

基本原理是,把内存中的数据直接输出,不做任何序列化操作。反序列化的时候,直接还原内存。

一般我们声明c++的结构如下即可

       #pragma pack (1)struct{char name[64];unsigned int age;};#pragma pack ()      

这种方式适合c/c++语言,单机进程间交换数据。这是一种简单高效的协议,特别适合通过共享内存交换数据的场景。但是不具有通用性,不适合跨越语言和机器,本文不再讨论这种编码方式

如果没有特别的必要,自己发明一种序列化方式一般是费力不讨好的,有重复造轮子的嫌疑,所以我们在成熟序列化方式中选择一种即可。

综上,我们可以看出,如果我们想设计一个具有通用性,可以用于分布式环境,适合互联网后台开发,能传递复杂数据,具有很好的灵活性和兼容性的协议,常用的序列化方式是TLV编码和字符流编码2种。那么根据不重复造轮子的原则,可选的编码方式就只有Protobuf、thrift 和 json 3种了。我们对比一下这3种编码方式。

序列化方式对比

Protobuf/thrift VS json

根据google的测评结果,Protobuf/thrift 效率高于 json, 而可读性弱于json。解析效率大概比json高1倍。这个具体的倍数关系我没测试过,存疑,而且不同的程序使用的json库不一样,还是应该以实测结果为准。

参考http://code.google.com/p/thrift-protobuf-compare/wiki/Benchmarking

Protobuf VS thrift

Protobuf 效率和编码长度略有优势,文档比thrift丰富

thrift 内建的数据类型更多(有map和set)

thrift官方比Protobuf支持更多的编程语言,并有RPC框架,但是Protobuf有很多第三方的支持,同样提供了多种语言的支持和RPC框架的实现

参考http://code.google.com/p/protobuf/wiki/ThirdPartyAddOns

参考http://blog.mirthlab.com/2009/06/01/thrift-vs-protocol-bufffers-vs-json/

个人比较倾向于Protobuf,主要是考虑到文档和第三方支持多,目前使用的更广泛。

至此,我们就选定了2种序列化方式Protobuf和json,如果并发度非常高,数据量非常大,使用Protobuf,否则使用json.

  2.2 判断包的完整性:

一般有两种方法:

1) 在序列化后的buffer前面增加一个采用固定结构编码的头部,头部长度和结构固定,其中有个字段存储包总长度。收包时,先接收固定字节数的头部,解出这个包完整长度,按此长度接收包体。

2) 在序列化后的buffer前面增加一个字符流的头部,其中有个字段存储包总长度,根据特殊字符(比如根据\n 或者\0)判断头部的完整性。这样通常比1要麻烦一些,http、memcached和radis采用的是这种方式。收包的时候,先判断已收到的数据中是否包含结束符,收到结束符后解析包头,解出这个包完整长度,按此长度接收包体。

至此,我们已经得到了一个协议框架,采用这个协议框架,再根据业务需要约定字段含义,就可以得到一个具体的协议,可以用于把一个机器上的消息,发送到另一个机器,并让对方完全理解消息的含义。但是如果这就是这个协议框架的全部,那这个协议就太弱了,因为如果一个程序只知道协议框架而不知道协议的字段内容,那它除了可以收包和发包外,做不了任何事情,而在客户端和服务之间搭建一个代理层,来做容灾、监控、统计、路由、认证等等事情是一种常见的架构模式,这样这些公共的处理逻辑就不用每个服务都做一次了,服务可以专注于业务,而把这些逻辑交由代理层来做。换句话说,我们需要为协议框架增加一个头部,并约定一些所有业务都可以使用的公共字段。

3 协议头部:

那么头部中可以增加哪些字段呢?这个取决于你希望代理帮你做哪些事情。通常以下字段是可以考虑的:

seq                     //消息序列号,可以用于排查问题,也可以用于某些IO模型中包的解析

protocol version       //协议版本号,可以用于协议的兼容

request useragent    //请求者机器环境,包括操作系统、客户端版本等等信息

request user ip        //请求者ip

request user id        //请求者id

client ip               //客户端ip

client id               //客户端业务id

server ip                     //服务ip

server id                     //服务id

server server cmd     //服务命令字

retcode                     //返回码

有了这些字段,代理层就能完全监控到服务的访问情况,并生成报表

4  自己设计协议:

有了上面的理论,我们就可以真正的设计协议了。下面设计的这个协议可以应用于互联网后台服务的绝大部分场景,协议中把一个包分为3个部分:

包头的第1部分:固定8字节:协议标志(2字节) 包头长度(2字节)  包体长度(4字节)

包头的第2部分:这部分主要是前面第4点提到的公共头部,包括seq等字段,采用Protobuf序列化,包头的字段是可以增删的,即使没有任何字段,也不影响数据传递,但是可能影响你的代理做的工作;

包体:采用Protobuf序列化,具体内容取决于业务。

5  一些常用的协议:

http协议:http协议是我们最常见的协议,我们是否可以采用http协议作为互联网后台的协议呢?这个一般是不适当的,主要是考虑到以下2个原因:

1) http协议只是一个框架,没有指定包体的序列化方式,所以还需要配合其他序列化的方式使用才能传递业务逻辑数据。

2) http协议解析效率低,而且比较复杂(不知道有没有人觉得http协议简单,其实不是http协议简单,而是http大家比较熟悉而已)

有些情况下是可以使用http协议的:

1) 对公网用户api,http协议的穿透性最好,所以最适合;

2) 效率要求没那么高的场景;

3) 希望提供更多人熟悉的接口,比如新浪微、腾讯博提供的开放接口,就是http的;

memcache的协议

基本原理是:先发送字符流,以\r\n作为结束标志,字符流中不允许存在特殊字符。

再发送一个数据包,可以包含任何字符,数据包的长度已经在前面的字符流中指定。

memcache的协议并没有包含业务数据序列化和反序列化的部分,只有包头和一个buffer,是一种适合于业务逻辑简单场景下的协议。参考:http://www.ccvita.com/306.html

redis协议:

基本原理是:先发送一个字符串表示参数个数,然后再逐个发送参数,每个参数发送的时候,先发送一个字符串表示参数的数据长度,再发送参数的内容。

redis的协议和memcache类似,但是memcached只能带一个二进制字段,redis可以带多个

参考:http://www.redisdoc.com/en/latest/topic/protocol.html

转载于:https://www.cnblogs.com/claresun/p/4844138.html

互联网服务应用协议设计相关推荐

  1. 2017-2018-1 20155327 实验五 通讯协议设计

    2017-2018-1 20155327 实验五 通讯协议设计 实验一: 实验要求: 在Ubuntu中完成 http://www.cnblogs.com/rocedu/p/5087623.html 中 ...

  2. 2018-2019-1 20165212 实验五 通讯协议设计

    2018-2019-1 20165212 实验五 通讯协议设计 OpenSSL简介 OpenSSL是为网络通信提供安全及数据完整性的一种安全协议,囊括了主要的密码算法.常用的密钥和证书封装管理功能以及 ...

  3. 【Paper】2015_异构无人机群鲁棒一致性协议设计_孙长银

    原文地址:[1]孙长银,余瑶,张兰.异构无人机群鲁棒一致性协议设计[J].中国科学:技术科学,2015,45(06):573-582. 2015_异构无人机群鲁棒一致性协议设计_孙长银 4 分布式鲁棒 ...

  4. 【MORE协议】基于MORE的改进协议设计的MATLAB仿真

    0.完整源码获得方式 方式1:微信或者QQ联系博主 方式2:订阅MATLAB/FPGA教程,免费获得教程案例以及任意2份完整源码 1.软件版本 MATLAB2021a 2.本算法理论知识 随着无线通信 ...

  5. 一次彻底搞透协议设计(没做过通讯底层也没有关系)!

    系统设计,协议先行. 大部分人不了解协议的设计细节,更多使用已有协议进行应用层设计,例如: (1)使用HTTP,设计get/post/cookie参数,以及json包格式: (2)使用dubbo,而不 ...

  6. 2017-2018-1 201553334 实验五 通讯协议设计

    2017-2018-1 201553334 实验五 通讯协议设计 1.在Ubuntu中完成 http://www.cnblogs.com/rocedu/p/5087623.html 中的作业 提交运行 ...

  7. 2017-2018-1 20155222 201552228 实验五 通讯协议设计

    2017-2018-1 20155222 201552228 实验五 通讯协议设计 实验内容和要求 通讯协议设计-1 在Ubuntu中完成 http://www.cnblogs.com/rocedu/ ...

  8. 浅析低延迟直播协议设计:RTP/RTCP

    作者:王宇航,红点直播联合创始人&CTO.毕业于中国科学技术大学,风云直播创始团队成员,曾参与逆向Adobe 来源:UPYUN Open Talk 声明:本文已获得授权. Flash非公开加密 ...

  9. 2018-2019-1 20165201 实验五 通讯协议设计

    2018-2019-1 20165201 实验五 通讯协议设计 实验五 通讯协议设计-1 任务详情 在Ubuntu中完成 http://www.cnblogs.com/rocedu/p/5087623 ...

最新文章

  1. HTTP之访问控制「CORS」
  2. mysql1215_MySQL全面瓦解15:视图
  3. 牛客题霸 [求平方根] C++题解/答案
  4. win7 / mysql-8.0.11-winx64 安装的测坑步骤
  5. 计算机一级b考试理论知识,计算机等级考试一级B基础知识精选考点串讲
  6. HTML背景图片设置
  7. 中山大学计算机学院转专业,广东大一新生想转专业原来有窍门,满足这些成绩和技能很重要!...
  8. flashgot免费下载音乐
  9. GCC编译过程及使用
  10. 开源公告|更可信的人脸识别,腾讯优图TFace正式开源!
  11. 获取复选框的被选中的值
  12. 如何两个电脑共享文件实现多人编辑_excel怎么实现多人共同编辑一个文档
  13. Android O 收音机学习研究(基于Car)
  14. 在CMD上运行javac前应该这样做
  15. c语言中余数取整,C 逻辑运算, 移位运算 , 取整 , 取模(取余)
  16. 《流浪地球》票房:预测10亿却飚50亿 ,大数据预测为什么这么难...
  17. 【leetcode】唯一摩尔斯密码词 c++ python
  18. 1. 将数据导入到前置数据库中(MySQL)
  19. HP OEM XP的BIOS破解方法
  20. 【Java知识点总结】Java语句简介及顺序结构

热门文章

  1. matlab混叠现象与频率分辨率,连续时间信号频谱分析研究及MATLAB实现
  2. SpringBoot的@Conditional和自动配置类生效
  3. istio多集群链路追踪,附实操视频
  4. ElasticSearch的安装过程
  5. 如何设计一个权限系统
  6. springboot 实现接口灰度发布
  7. mysql存储过程 --游标的使用 取每行记录 (多字段)
  8. 技术分享连载(六十八)
  9. oracle 12c grid db 安装的的checklist
  10. phpexcel如何读和写大于26列的excel