互联网后台服务的协议设计html
1. 基本概念c++
服务(server):“服务”能够分软件和硬件两个类别,本文提到的“服务”都是指软件,是一种程序。称之为“服务”的程序通常具有2个特色:redis
1) 程序启动后常驻内存,成为守护进程。编程
2) 能与其余进程通讯,接收请求,处理请求并作出回应。json
本文中的服务特指基于TCP/IP 协议经过socket进行通讯的服务。api
为何互联网业务须要“服务”这种类型的程序呢?主要有2个缘由网络
1) 有些功能能够经过一个独立的程序来完成,不用每一个程序都写一套代码来实现这个功能,这样有利于程序的解耦和复用架构
2) 有些功能单机、单进程没法完成,须要经过多台机器、多个程序的协做完成。并发
好比memcache服务,它常驻内存并监听TCP端口,接收来自socket的数据包,使客户端能够以key-value的方式存取数据,是互联网后台中一种经常使用的cache服务。框架
客户端(client):本文中的客户端指的是主动发包给服务的程序,或者说是发起请求的程序,相对的,服务就是接收请求并处理的程序。
协议:协议是一种约定,经过约定,不一样的进程能够对一段数据产生相同的理解,从而能够相互协做,存在进程间通讯的程序就必定须要协议。
咱们为何须要本身设计协议:
经过上面讲的咱们能够看出,在互联网后台开发中,稍微复杂一些的业务,服务是必要的,进而协议也是必要的。那么咱们是否能够复用已有的协议呢?主要是由于如今已有的协议都没有能彻底match互联网后台开发的需求,存在这样或那样的问题。
协议设计的目标:
解析效率:互联网业务具备高并发的特色,解析效率决定了使用协议的CPU成本;
编码长度:信息编码出来的长度,编码长度决定了使用协议的网络带宽及存储成本;
易于实现:互联网业务须要一个轻量级的协议,而不是大而全的,CORBA这种重量级的协议就不太适合,易于实现决定了使用协议的开发成本和学习成本;
可读性:编码后的数据的可读性决定了使用协议的调试及维护成本
兼容性: 互联网的需求具备灵活多变的特色,协议会常常升级,使用协议的双方是否能够独立升级协议、增减协议中的字段是很是重要的。兼容性决定了持续开发时的开发成本,我的以为这点是互联网协议中最重要的一个指标。
协议设计须要解决的问题:
1) 序列化/反序列化
2) 判断包的完整性
只要解决了这2个问题,2个不一样机器的进程就能完成通讯。
2. 序列化/反序列化:
序列化咱们常称之为编码,或者打包,反序列化常称之为解码,或者解包。经常使用的序列化/反序列化方式主要有如下几种:
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.
3. 判断包的完整性:
通常有两种方法:
1) 在序列化后的buffer前面增长一个采用固定结构编码的头部,头部长度和结构固定,其中有个字段存储包总长度。收包时,先接收固定字节数的头部,解出这个包完整长度,按此长度接收包体。
2) 在序列化后的buffer前面增长一个字符流的头部,其中有个字段存储包总长度,根据特殊字符(好比根据\n 或者\0)判断头部的完整性。这样一般比1要麻烦一些,http、memcached和radis采用的是这种方式。收包的时候,先判断已收到的数据中是否包含结束符,收到结束符后解析包头,解出这个包完整长度,按此长度接收包体。
至此,咱们已经获得了一个协议框架,采用这个协议框架,再根据业务须要约定字段含义,就能够获得一个具体的协议,能够用于把一个机器上的消息,发送到另外一个机器,并让对方彻底理解消息的含义。可是若是这就是这个协议框架的所有,那这个协议就太弱了,由于若是一个程序只知道协议框架而不知道协议的字段内容,那它除了能够收包和发包外,作不了任何事情,而在客户端和服务之间搭建一个代理层,来作容灾、监控、统计、路由、认证等等事情是一种常见的架构模式,这样这些公共的处理逻辑就不用每一个服务都作一次了,服务能够专一于业务,而把这些逻辑交由代理层来作。换句话说,咱们须要为协议框架增长一个头部,并约定一些全部业务均可以使用的公共字段。
4. 协议头部:
那么头部中能够增长哪些字段呢?这个取决于你但愿代理帮你作哪些事情。一般如下字段是能够考虑的:
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 //返回码
有了这些字段,代理层就能彻底监控到服务的访问状况,并生成报表
5. 我设计的协议:
有了上面的理论,咱们就能够真正的设计协议了。我设计的这个协议能够应用于互联网后台服务的绝大部分场景,协议中把一个包分为3个部分:
包头的第1部分:固定8字节:协议标志(2字节) 包头长度(2字节) 包体长度(4字节)
包头的第2部分:这部分主要是前面第4点提到的公共头部,包括seq等字段,采用Protobuf序列化,包头的字段是能够增删的,即便没有任何字段,也不影响数据传递,可是可能影响你的代理作的工做;
包体:采用Protobuf序列化,具体内容取决于业务。
6. 一些经常使用的协议:
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