简介:本篇本跟上篇《Protobuf从入门到精通,一篇就够!》类似,都适合作为Protobuf的入门文章,
本文由vivo技术团队Li Guanyun分享,为了提升阅读体验,进行了较多修订和重新排版。
Protobuf 作为一种跨平台、语言无关、可扩展的序列化结构数据通讯协议,已广泛应用于网络数据交换的场景中(比如IM通信、分布式RPC调用等)。
随着互联网的发展,分布式系统的异构性会愈发突出,跨语言的需求会愈加明显,同时 gRPC 也大有取代Restful之势,而 Protobuf 作为gRPC 跨语言、高性能的法宝,我们技术人有必要深入理解 Protobuf 原理,为以后的技术更新和选型打下基础。
借此机会,我将个人的Protobuf学习过程以及实践经验,总结成文,与大家一起探讨学习。本篇主要从Protobuf的基础概念开始,包括技术背景、技术原理、使用方法和优缺点。
PS:本篇本跟上篇《Protobuf从入门到精通,一篇就够!》类似,都适合作为Protobuf的入门文章,但本篇力求简洁,尽量不涉及Protobuf的具体技术细节,目的是降低阅读的门槛、提升阅读效果,希望对你有用。

学习交流:
移动端IM开发入门文章:《新手入门一篇就够:从零开发移动端IM》
开源IM框架源码:https://github.com/JackJiang2011/MobileIMSDK(备用地址点此)
(本文已同步发布于:http://www.52im.net/thread-4081-1-1.html)
本文是系列文章中的第 2 篇,本系列总目录如下:

Protobuf(全称是Protocol Buffers)是一种跨平台、语言无关、可扩展的序列化结构数据的方法,可用于网络通信数据交换及存储。
在序列化结构化数据的机制中,Protobuf是灵活、高效、自动化的,相对常见的XML、JSON,描述同样的信息,Protobuf序列化后数据量更小、序列化/反序列化速度更快、更简单。
一旦定义了要处理的数据的数据结构之后,就可以利用Protobuf的代码生成工具生成相关的代码。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言(proto3支持C++, Java, Python, Go, Ruby, Objective-C, C#)或从各种不同流中对你的结构化数据轻松读写。
PS:类似的介绍,在上篇《Protobuf从入门到精通,一篇就够!》中也有涉及,有兴趣可以一并阅读之。
大家可能会觉得 Google 发明 Protobuf 是为了解决序列化速度的,其实真实的原因并不是这样的。
Protobuf最先开始是 Google用来解决索引服务器 request/response 协议的。
在没有Protobuf之前,Google 已经存在了一种 request/response 格式,用于手动处理 request/response 的编解码。
这种sstk式也能支持多版本协议,不过代码不够优雅:
if(protocolVersion=1) {
doSomething();
} elseif(protocolVersion=2) {
doOtherThing();
} …
如果是非常明确的格式化协议,会使新协议变得非常复杂。因为开发人员必须确保请求发起者与处理请求的实际服务器之间的所有服务器都能理解新协议,然后才能切换开关以开始使用新协议。
这也就是每个服务器开发人员都遇到过的低版本兼容、新旧协议兼容相关的问题。
为了解决这些问题,于是Protobuf就诞生了。
Protobuf 最初被寄予以下 2 个期望:
随着Protobuf的发展、演进,它具有了更多的特性:
Why the name “Protocol Buffers”?
The name originates from the early days of the format, >before we had the protocol buffer compiler to generate >classes for us. At the time, there was a class called >ProtocolBuffer which actually acted as a buffer for an >individual method. Users would add tag/value pairs to this >buffer individually by calling methods like AddValue(tag, >value). The raw bytes were stored in a buffer which could >then be written out once the message had been constructed.
Why the name “Protocol Buffers”?
The name originates from the early days of the format, >before we had the protocol buffer compiler to generate >classes for us. At the time, there was a class called >ProtocolBuffer which actually acted as a buffer for an >individual method. Users would add tag/value pairs to this >buffer individually by calling methods like AddValue(tag, >value). The raw bytes were stored in a buffer which could >then be written out once the message had been constructed.
Since that time, the “buffers” part of the name has lost >its meaning, but it is still the name we use. Today, >people usually use the term “protocol message” to refer to >a message in an abstract sense, “protocol buffer” to refer >to a serialized copy of a message, and “protocol message >object” to refer to an in-memory object representing the >parsed message.Since that time, the “buffers” part of the >name has lost its meaning, but it is still the name we >use. Today, people usually use the term “protocol message” >to refer to a message in an abstract sense, “protocol >buffer” to refer to a serialized copy of a message, and >”protocol message object” to refer to an in-memory object >representing the parsed message.
Protobuf 现在是 Google 用于数据交换和存储的通用语言。
谷歌代码树中定义了 48162 种不同的消息类型,包括 12183 个 .proto 文件。它们既用于 RPC 系统,也用于在各种存储系统中持久存储数据。
Protobuf 诞生之初是为了解决服务器端新旧协议(高低版本)兼容性问题,名字也很体贴——“协议缓冲区”,只不过后期慢慢发展成用于传输数据。
如下图所示:可以看到,对于序列化协议来说,使用方只需要关注业务对象本身,即 idl 定义,序列化和反序列化的代码只需要通过工具生成即可。

Protobuf 的消息是在idl文件(.proto)中描述的。
下面是本次样例中使用到的消息描述符 customer.proto:
syntax = “proto3”;
package domain;
option java_package = “com.Protobuf.generated.domain”;
option java_outer_classname = “CustomerProtos”;
message Customers {
repeated Customer customer = 1;
}
message Customer {
int32 id= 1;
string firstName = 2;
string lastName = 3;
enum EmailType {
PRIVATE = 0;
PROFESSIONAL = 1;
}
message EmailAddress {
string email = 1;
EmailType type= 2;
}
repeated EmailAddress email = 5;
}
上面的消息比较简单,Customers包含多个Customer(Customer包含一个id字段、一个firstName字段、一个lastName字段以及一个email的集合)。
除了上述定义外,文件顶部还有三行可帮助代码生成器的申明:
首先安装 Protobuf 编译器 protoc(点这里有详细的安装教程)。
安装完成后,可以使用以下命令生成 Java 源代码:
1protoc —>java_out=./src/main/java./src/main/idl/customer.proto
上述命令的意图是:从项目的根路径执行该命令,并添加了两个参数 java_out(即定义 ./src/main/java/ 为Java代码的输出目录;而 ./src/main/idl/customer.proto 是.proto文件所在目录)。
生成的代码非常复杂,但幸运的是它的用法却非常简单:
CustomerProtos.Customer.EmailAddress email = CustomerProtos.Customer.EmailAddress.newBuilder()
.setType(CustomerProtos.Customer.EmailType.PROFESSIONAL)
.setEmail(“crichardson@email.com”).build();
CustomerProtos.Customer customer = CustomerProtos.Customer.newBuilder()
.setId(1)
.setFirstName("Lee")
.setLastName("Richardson")
.addEmail(email)
.build();
// 序列化
byte[] binaryInfo = customer.toByteArray();
System.out.println(bytes_String16(binaryInfo));
System.out.println(customer.toByteArray().length);
// 反序列化
CustomerProtos.Customer anotherCustomer = CustomerProtos.Customer.parseFrom(binaryInfo);
System.out.println(anotherCustomer.toString());
我们简单地以上述Customers为模型,分别构造、选取小对象、普通对象、大对象进行性能对比。
序列化耗时以及序列化后数据大小对比:

反序列化耗时:

更多性能数据可以参考官方的测试Benchmark。
从序列化后的数据体积角度,与XML、JSON这类文本协议相比,Protobuf通过 T-(L)-V(TAG-LENGTH-VALUE)方式编码,不需要”, {, }, :等分隔符来结构化信息。同时在编码层面使用varint压缩。
所以描述同样的信息,Protobuf序列化后的体积要小很多,在网络中传输消耗的网络流量更少,进而对于网络资源紧张、性能要求非常高的场景。比如在移动网络下的IM即时通讯应用中,Protobuf协议就是非常不错的选择(PS:这也是我为什么着手分享Protobuf系列文章的原因啦)。
我们来简单做个对比。
要描述如下JSON数据:
1{“id”:1,”firstName”:”Chris”,”lastName”:”Richardson”,”email”:[{“type”:”PROFESSIONAL”,”email”:”crichardson@email.com”}]}
使用JSON序列化后的数据大小为118byte:
7b226964223a312c2266697273744e616d65223a224368726973222c226c6173744e616d65223a2252696368617264736f6e222c22656d61696c223a5b7b2274797065223a2250524f46455353494f4e414c222c22656d61696c223a226372696368617264736f6e40656d61696c2e636f6d227d5d7d
而使用Protobuf序列化后的数据大小为48byte:
0801120543687269731a0a52696368617264736f6e2a190a156372696368617264736f6e40656d61696c2e636f6d1001
从序列化/反序列化速度角度,与XML、JSON相比,Protobuf序列化/反序列化的速度更快,比XML要快20-100倍。
Protobuf是平台无关的,无论是Android、iOS、PC,还是C#与Java,都可以利用Protobuf进行无障碍通讯。
proto3支持C++、Java、Python、Go、Ruby、Objective-C、C#(详见《Protobuf从入门到精通,一篇就够》)。
Protobuf具有向后兼容的特性:更新数据结构以后,老版本依旧可以兼容,这也是Protobuf诞生之初被寄予解决的问题,因为编译器对不识别的新增字段会跳过不处理。
Protobuf 提供了一套编译工具,可以自动生成序列化、反序列化的样板代码,这样开发者只要关注业务数据idl,简化了编码解码工作以及多语言交互的复杂度。
Protobuf的优点很突出,但缺点也很明显。
Protobuf的缺点主要是:
然而:由于没有idl文件无法解析二进制数据流,ProtoBuf在一定程度上可以保护数据,提升核心数据被破解的门槛,降低核心数据被盗爬的风险(也算是缺点变优点的典型范例)。
11、参考资料
[1] Protobuf官方网站
[2] Protobuf从入门到精通,一篇就够!
[3] 如何选择即时通讯应用的数据传输格式
[4] 强列建议将Protobuf作为你的即时通讯应用数据传输格式
[5] APP与后台通信数据格式的演进:从文本协议到二进制协议
[6] 面试必考,史上最通俗大小端字节序详解
[7] 移动端IM开发需要面对的技术问题(含通信协议选择)
[8] 简述移动端IM开发的那些坑:架构设计、通信协议和客户端
[9] 理论联系实际:一套典型的IM通信协议设计详解
[10] 58到家实时消息系统的协议设计等技术实践分享