本文由码农的荒岛求生陆小风分享,为了晋升浏览体验,进行了较多订正和排版。
1、引言
搞即时通讯IM方面开发的程序员,在谈到通信层实现时,必然会提到网络编程。那么计算机网络编程中的一个十分根本的问题:到底该怎么组织Client与server之间交互的数据呢?本篇文章咱们不探讨IM零碎中的那些高端技术话题,咱们回归到通信的实质——也就是数据在网络中交互时的编解码原理,并由浅入深从底层了解Protobuf的编解码技术实现。
学习交换:
-- 挪动端IM开发入门文章:《新手入门一篇就够:从零开发挪动端IM》
--开源IM框架源码:https://github.com/JackJiang2...(备用地址点此)
(本文已同步公布于:http://www.52im.net/thread-40...)
2、系列文章
本文是系列文章中的第 3 篇,本系列总目录如下:
《IM通信协定专题学习(一):Protobuf从入门到精通,一篇就够!》
《IM通信协定专题学习(二):疾速了解Protobuf的背景、原理、应用、优缺点》
《IM通信协定专题学习(三):由浅入深,从根上了解Protobuf的编解码原理》(* 本文)
《IM通信协定专题学习(四):从Base64到Protobuf,详解Protobuf的数据编码原理》(稍后公布..)
《IM通信协定专题学习(五):Protobuf到底比JSON快几倍?请看全方位实测!》(稍后公布..)《IM通信协定专题学习(六):手把手教你如何在Android上从零应用Protobuf》(稍后公布..)《IM通信协定专题学习(七):手把手教你如何在NodeJS中从零应用Protobuf》(稍后公布..)《IM通信协定专题学习(八):金蝶顺手记团队的Protobuf利用实际(原理篇) 》(稍后公布..)《IM通信协定专题学习(九):金蝶顺手记团队的Protobuf利用实际(实战篇) 》(稍后公布..)
3、共识与协定
针对引言中引出的“到底该怎么组织Client与Server之间交互的数据呢?”。这个问题可不像看上去那样简略,因为Client过程和Server过程运行在不同的机器上,这些机器可能运行在不同的处理器平台、可能运行在不同的操作系统、可能是由不同的编程语言编写的,Server要怎样才能辨认出Client发送的是什么数据呢?就像这样:
如上图所示,Client给Server发送了一段数据:0101000100100001Server怎么能晓得该怎么“解读”这段数据呢?显然:Client和Server在发送数据之前必须首先达成某种对于怎么解读数据的共识,这就是所谓的协定。这里的协定能够是这样的:“将每8个比特为一个单位解释为无符号数字”。如果协定是下面这样定义的:那么Server接管到这串二进制后就会将其解析为 81(01010001) 与 33(00100001)。当然,这里的协定也能够是这样的:“将每8个比特为一个单位解释为ASCII字符”,那么Server接管到这串二进制后就将其解析为“Q!”。可见:同样一串二进制在不同的“上下文/协定”下有齐全不一样的解读,这也是为什么计算机明明只认知0和1然而却能解决非常复杂工作的根本原因,因为所有都能够编码为0和1,同样的咱们也能够从0和1中解析出咱们想要的信息,这就是所谓的编解码技术。实际上不止0和1,咱们也能够将信息编码为摩斯明码(Morse code)等,只不过计算机善于解决0和1而已。
扯远了,回到本文的主题。
4、一个例子:
近程过程调用(RPC)作为程序员咱们晓得,Client以及Server之间不会简略传递一串数字以及字符这样简略,尤其在互联网大厂后端服务这种场景下。当咱们在电商App里搜寻商品、打车App里呼叫出租车以及刷短视频时,每一次申请的背地在后端都波及大量服务之间的交互。就像这样:
实现一次客户端申请gateway这个服务要“调用”N多个上游服务,所谓“调用”是说A服务向B服务发送一段数据(申请),B服务接管到这段数据后执行相应的函数,并将后果返回给A服务。只不过对于服务A来说并不想关怀网络传输这样的底层细节,如果能像调用本地函数一样调用近程服务就好了,这就是所谓的RPC。经典的实现形式是这样的:
RPC对下层提供和一般函数一样的接口,只不过在实现上封装了底层简单的网络通信(当然也包含协定的定义,协定的解解码等)。RPC框架是以后互联网后端的基石之一,很多所谓互联网后端的职位无非就是在此基础之上堆业务逻辑。本文咱们不关怀其中的细节,咱们只关怀在网络层Client是怎么对申请参数进行编码、Server怎么对申请参数进行解码的,也就是本文结尾提出的问题。
5、信息的编解码
5.1纯文本的编解码对人类很敌对
在思考怎么进行编解码之前,咱们必须意识到:1)Client和Server可能是用不同语言编写的(你的编解码计划必须通用且不能和语言绑定);2)编解码办法的性能问题必须要思考(尤其是对工夫要求刻薄的服务)。首先,咱们最应该能想到的就是以纯文本的模式来示意。纯文本素来都是一种十分有敌对的信息载体。为什么?很简略,因为人类(咱们)能够间接看懂。就像这段:{ "widget": { "window": { "title": "Sample Konfabulator Widget", "name": "main_window", "width": 500, "height": 500 }, "image": { "src": "Images/Sun.png", "name": "sun1", "hOffset": 250, "vOffset": 250, }, }}是不是高深莫测:只有咱们实现约定好文本的构造(也就是语法),那么Client和Server就能利用这种文本进行信息的编码以及解码,不论Client和Server是运行在x86还是ARM、是32位的还是64位的、运行在Linux上还是Windows上、是大端还是小端,都能够无障碍交换。因而:在这里,文本的语法就是一种协定(如下图所示)。
顺便说一句:你都规定好了文本的语法,实际上就相当于创造了一种语言。这里用来举例用的语言就是所谓的JSON,只不过JSON这种语言不是用来示意逻辑(代码)而是用来存储数据的。
JSON就是这个老头提出来的:
除了JSON,另一种利用文本存储数据的示意办法是XML。来一段XML感触下:<note><to>Tove</to><from>Jani</from><heading>Reminder</heading><body>Don't forget me this weekend!</body></note>绝对JSON来说是不是就没那么容易看懂了,自从JSON呈现后在Web畛域就逐步取代了XML。当两段数据量很少的时候——就像浏览器和服务端的交互,JSON能够工作的十分好(如下图所示)。这个场景就是这样:
在这里是JSON的天下。
5.2纯文本对计算机来说不够敌对
在上大节中咱们晓得,JSON这类纯文本的编解码形式对于人类十分敌对。但对于后端服务之间的交互(或者具体如IM里Client和Server之间的交互)来说就不一样了,后端服务之间的RPC调用可能会传输大量数据,如果全副用纯文本的模式来示意数据那么不论是网络带宽还是性能可能都会差强人意。
在这种场景下,JSON并不是最好的选项,次要起因之一就在于性能以及数据的体积。咱们晓得:文本示意对人类是最敌对的,对机器来说则不是这样,对机器来说最好的还是01二进制。那么有没有二进制的编码方法吗?答案是必定的,这就是以后互联网后端中风行的Protobuf,Google公司开源我的项目。那么Protobuf有什么神奇之处吗?假如Client端想给Server端传输这样一段信息:“我有一个id,其值为43”。那么在XML下是这样示意的:<id>43</id>数一数这这段数据占据了多少字节,很显然是11字节。而如果用JSON来示意呢?{"id":43}数一数这段数据占据了多少字节,显然是9字节。而如果用Protobuf来示意呢? 是这样的://音讯定义message Msg { optional int32 id= 1;}//实例化Msg msg;msg.set_id(43);其中Msg的定义看上去比JSON和XML更加简单了,但这些只是给人看的,这些还会被protbuf进一步解决。最终被Protobuf编码为:1082b也就是0x08与0x2b,这占据了多少字节呢?答案是2字节。从JSON的9字节到Protobuf的2字节,数据大小缩小了4倍多。数据量的缩小意味着:1)更少的网络带宽;2)更快的解析速度。那么,Protobuf是怎么做到这一点的呢?
6、Protobuf是怎么实现编解码的?
首先,咱们来思考最简略的状况,失常状况下,咱们该怎么示意数字。你可能会想这还不简略,对立用固定长度,比方用64个比特(8字节)。这种办法可行,但问题是不管一个数字有多小,比如2,那么用这种办法示意2也须要占据64个比特(8字节),如下所示。
明明只有一个字节就能示意而咱们却用了8个,后面的全都是0,这也太侈靡太节约了吧。显然,在这里咱们不能应用固定长度来示意数字,而须要应用变长办法来示意。什么叫变长?意思是说如果数字自身比拟大,那么其应用的比特位能够较多,但如果数字很小那么就应该应用较少的比特位来示意,这就叫变长,随机应变,不死板。那怎么变长呢?咱们规定:对于每一个字节来说,第一个比特位如果是1那么示意接下来的一个比特仍然要用来解释为一个数字,如果第一个比特为0,那么阐明接下来的一个字节不是用来示意该数字的。也就是说对于每个8个比特(1字节)来说,它的有效载荷是7个比特,第一个比特仅仅用来标记是否还应该把接下来的一个字节解析为数字。依据这个规定,假如来了这样一串01二进制:1010110000000010依据规定,咱们首先取出第一个字节,也就是:10101100此时咱们发现第一个比特位是1,因而咱们晓得接下来的一个字节也属于该数字。将以后字节的1去掉就是:0101100而后咱们看下一个字节:00000010咱们发现第一个bit为0,因而咱们晓得下一个字节不属于该数字了。接下来咱们将解析到的0101100(第一个字节去掉第一个比特位)以及第二个字节0000010(第二个字节去掉第一个比特位)翻转之后拼接到一起(这里之所以翻转是因为咱们规定数字的高位在后)。这个过程就是: 1010110000000010 -> 10101100 | 00000010 //解析失去两个字节 _ _-> 0101100 | 0000010 //各自去掉最高位-> 0000010 | 0101100 //两个字节翻转程序 0000010 + 0101100-> 100101100 //拼接最初咱们失去了100101100,这一串二进制示意数字300。这种数字的变长示意办法在Protobuf中被称之为varint。因而在这种示意办法下,如果数字较大,那么应用的比特就多,如果数字较小那么应用比特就少,聪慧吧。有的同学看到这里可能会问题,方才解说的办法只能示意无符号数字,那么有符号数字该怎么示意呢?比方-2该怎么示意?
7、Protobuf的有符号数示意
依照方才变长编码的思维,-2147483646应用的比特位应该比-2要少。然而咱们晓得在计算机世界中正数应用补码示意的,也就是说最高位(最左侧的比特位)肯定是1,假如咱们应用64位来示意数字,那么如果咱们仍然用补码来示意数字的话那么无论这个正数有多大还是多小都须要占据10个字节的空间。为什么是10个字节呢?不要忘了varint每个字节的无效负荷是7个比特,那么对于须要64位示意的数字来说就须要64/7向上取整也就是10个字节来示意。这显然不能满足咱们对数字变长存储的要求。该怎么解决这个问题呢?既然无符号数字能够不便的进行变长编码,那么咱们将有符号数字映射称为无符号数字不就能够了,这就是所谓的ZigZag编码,是不是很聪慧。ZigZag编码就像这样:原始信息 编码后0 0-1 11 2-2 32 4-3 53 6... ...2147483647 4294967294-2147483648 4294967295这样咱们就能够将有符号数字转为无符号数字,接管方接管到该数据后再复原出有符号数字。当初数字的问题彻底解决了,但这仅仅是万里长征第一步。
8、Protobuf的字段名称与字段类型
对于任何一个有用的信息都蕴含这样几局部:1)字段名称;2)字段类型;3)字段值。就像C/C++中定义变量时:int i = 100;在这里,字段名称就是i,字段类型是int,字段值是100。方才咱们用varint以及ZigZag编码解决了字段值示意的问题,那么该怎么示意字段名称和字段类型呢?首先,对于字段类型还比较简单,因为字段类型就那么多。Protobuf中定义了6种字段类型:
对于6种字段类型咱们应用3个比特位来示意就足够了。接下来比拟乏味的是字段名称该怎么示意呢?假如咱们须要传递这样一个字段:int long_long_name = 100;那么咱们真的须要把“long_long_name”这么多字符通过网络传递给对端吗?既然通信单方须要协定,那么“long_long_name”这字段其实是Client和Server都晓得的,它们惟一不晓得的就是“哪些值属于哪些字段”。为解决这个问题,咱们给每个字段都进行编号,比方通信单方都晓得“long_long_name”这个字段的编号是2。那么对于“int long_long_name = 100; ”咱们该怎么示意呢。这个信息咱们只须要传递:1)字段名称:2 (2对应字段“long_long_name”);2)字段类型:0 (0示意varint类型,参见上图);3)字段值:100。所以咱们能够看到,无论你用如许简单的字段名称也不会影响编码后占据的空间,字段名称基本就不会呈现在编码后的信息中,so clever。
9、从宏观上看Protobuf的编码原理
咱们曾经在Protobuf中看到了数字以及字段名称以及字段类型是怎么示意了,当初是时候从宏观角度来看看多个字段该怎么编码了。从实质上讲,Protobuf被编码后造成一系列的key-value,每个key-value对应一个proto中的字段。也就是键值对:
其中value比较简单,也就是字段值;而字段名称和字段类型会被拼接成key。Protobuf中共有6种类型,因而只须要3个比特位即可。字段名称只须要存储对应的编号。这样就能够这样编码:(字段编号 << 3) | 字段类型假如Server接管到了一个key为0x08,其二进制的示意为:0000 1000因为key也是利用varint编码的,因而须要将第一个比特位去掉。这样我的失去:000 1000依据key的编码方式,其后三个比特位示意字段类型,即:000也就是0,这样咱们晓得该key的类型是Varint(第0号类型),而字段编号为抹掉后3个比特位的值,即:0001这样,咱们就晓得了该key对应的字段编号为1,失去编号咱们就能依据编号找到对应的编号名称。
10、Protobuf的嵌套数据
与JSON和XML相似,Protobuf中也反对嵌套音讯.就像这样:message SubMsg { optional int32 id= 1;}message Msg { optional SubMsg msg = 1;}其实现也比较简单,这仍然遵循被编码后造成一系列的key-value,只不过对于嵌套类型的key来说,其value是由子音讯的key-value组成,如下图所示。
11、Protobuf与编译语言
与JSON一样,Protobuf也是一门语言,兼具了文本的可读性以及二进制的高效。Protobuf之所以能做到这一点,就好比C语言与机器指令。C语言是给程序员看的,可读性好。而机器指令是给硬件应用的,性能好。编译器会将C语言程序转为机器可执行的机器指令。而Protobuf也一样,Protobuf也是一门语言,会将可读性较好的音讯编码为二进制从而能够在网络中进行流传,而对端也能够将其解码回来。在这里Protobuf中定义的音讯就好比C语言,编码后的二进制音讯就好比机器指令。而Protobuf作为事实上语言必然有本人的语法。其语法就是这样:
怎么样,还感觉编译原理没什么用吗?不了解编译原理是不可能创造Protobuf这种技术的。
12、本文小结
我在写这篇文章时一直感叹,Google的这项技术节俭了多少程序员的工夫,同时咱们也能看到这种基石般的技术依赖的底层原理却十分古老。
比方上面这些:
1)信息的编解码;
2)编译原理。
怎么样,这些是不是远远没有IT界各种风行的技术听下来时尚乏味,而正是这种奢侈的技术撑持起了工业界,当初你也应该能明确底层技术的重要性了吧。
13、参考资料
[1]Protobuf官方网站
[2]Protobuf从入门到精通,一篇就够!
[3]如何抉择即时通讯利用的数据传输格局
[4]强列倡议将Protobuf作为你的即时通讯利用数据传输格局
[5]APP与后盾通信数据格式的演进:从文本协定到二进制协定
[6]面试必考,史上最艰深大小端字节序详解
[7]挪动端IM开发须要面对的技术问题(含通信协议抉择)
[8]简述挪动端IM开发的那些坑:架构设计、通信协议和客户端
[9]实践联系实际:一套典型的IM通信协议设计详解
[10]58到家实时音讯零碎的协定设计等技术实际分享
(本文已同步公布于:http://www.52im.net/thread-40...)