写点什么

IM 通讯协议专题学习 (二):快速理解 Protobuf 的背景、原理、使用、优缺点

作者:JackJiang
  • 2022-11-17
    江苏
  • 本文字数:5189 字

    阅读完需:约 17 分钟

IM通讯协议专题学习(二):快速理解Protobuf的背景、原理、使用、优缺点

本文由 vivo 技术团队 Li Guanyun 分享,为了提升阅读体验,即时通讯网进行了较多修订和重新排版。

1、引言

Protobuf 作为一种跨平台、语言无关、可扩展的序列化结构数据通讯协议,已广泛应用于网络数据交换的场景中(比如 IM 通信、分布式 RPC 调用等)。

随着互联网的发展,分布式系统的异构性会愈发突出,跨语言的需求会愈加明显,同时 gRPC 也大有取代Restful之势,而 Protobuf 作为 gRPC 跨语言、高性能的法宝,我们技术人有必要深入理解 Protobuf 原理,为以后的技术更新和选型打下基础。

借此机会,我将个人的 Protobuf 学习过程以及实践经验,总结成文,与大家一起探讨学习。本篇主要从 Protobuf 的基础概念开始,包括技术背景、技术原理、使用方法和优缺点

PS:本篇本跟上篇《Protobuf从入门到精通,一篇就够!》类似,都适合作为 Protobuf 的入门文章,但本篇力求简洁,尽量不涉及 Protobuf 的具体技术细节,目的是降低阅读的门槛、提升阅读效果,希望对你有用。

(本文已同步发布于:http://www.52im.net/thread-4081-1-1.html

2、系列文章

本文是系列文章中的第 2 篇,本系列总目录如下:

  • IM通讯协议专题学习(一):Protobuf从入门到精通,一篇就够!

  • IM通讯协议专题学习(二):快速理解Protobuf的背景、原理、使用、优缺点》(* 本文)

  • 《IM 通讯协议专题学习(三):由浅入深,从通信编解码原理上理解 Protobuf》(稍后发布..)

  • 《IM 通讯协议专题学习(四):从 Base64 到 Protobuf,详解 Protobuf 的数据编码原理》(稍后发布..)

  • 《IM 通讯协议专题学习(五):Protobuf 到底比 JSON 快几倍?请看全方位实测!》(稍后发布..)

  • 《IM 通讯协议专题学习(六):手把手教你如何在 Android 上从零使用 Protobuf》(稍后发布..)

  • 《IM 通讯协议专题学习(七):手把手教你如何在 NodeJS 中从零使用 Protobuf》(稍后发布..)

  • 《IM 通讯协议专题学习(八):金蝶随手记团队的 Protobuf 应用实践(原理篇)  》(稍后发布..)

  • 《IM 通讯协议专题学习(九):金蝶随手记团队的 Protobuf 应用实践(实战篇) 》(稍后发布..)

3、什么是 Protobuf?

Protobuf(全称是 Protocol Buffers)是一种跨平台、语言无关、可扩展的序列化结构数据的方法,可用于网络通信数据交换及存储。

在序列化结构化数据的机制中,Protobuf 是灵活、高效、自动化的,相对常见的 XML、JSON,描述同样的信息,Protobuf 序列化后数据量更小、序列化/反序列化速度更快、更简单。

一旦定义了要处理的数据的数据结构之后,就可以利用 Protobuf 的代码生成工具生成相关的代码。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言(proto3 支持 C++, Java, Python, Go, Ruby, Objective-C, C#)或从各种不同流中对你的结构化数据轻松读写。

PS:类似的介绍,在上篇《Protobuf从入门到精通,一篇就够!》中也有涉及,有兴趣可以一并阅读之。

4、为什么是 Protobuf?

4.1 技术背景

大家可能会觉得 Google 发明 Protobuf 是为了解决序列化速度的,其实真实的原因并不是这样的。

Protobuf 最先开始是 Google 用来解决索引服务器 request/response 协议的。

在没有 Protobuf 之前,Google 已经存在了一种 request/response 格式,用于手动处理 request/response 的编解码。

这种 sstk 式也能支持多版本协议,不过代码不够优雅:

if(protocolVersion=1) {

    doSomething();

} elseif(protocolVersion=2) {

    doOtherThing();

} ...

如果是非常明确的格式化协议,会使新协议变得非常复杂。因为开发人员必须确保请求发起者与处理请求的实际服务器之间的所有服务器都能理解新协议,然后才能切换开关以开始使用新协议。

这也就是每个服务器开发人员都遇到过的低版本兼容、新旧协议兼容相关的问题。

为了解决这些问题,于是 Protobuf 就诞生了。

4.2 Protobuf 诞生了

Protobuf 最初被寄予以下 2 个期望:

  • 1)更容易引入新的字段,并且不需要检查数据的中间服务器可以简单地解析并传递数据(而无需了解所有字段);

  • 2)数据格式更加具有自我描述性,可以用各种语言来处理(比如 C++, Java 等各种语言)。

但这个版本的 Protobuf 仍需要自己手写解析的代码。

随着 Protobuf 的发展、演进,它具有了更多的特性:

  • 1)自动生成的序列化和反序列化代码(避免了手动解析的需要。官方提供自动生成代码工具,各个语言平台的基本都有);

  • 2)除了用于数据交换之外,Protobuf 也被用作某些持久化数据的便捷自描述格式。

Protocol Buffers 命名的由来:

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.

4.3 Protobuf 在谷歌业务中的地位

Protobuf 现在是 Google 用于数据交换和存储的通用语言。

谷歌代码树中定义了 48162 种不同的消息类型,包括 12183 个 .proto 文件。它们既用于 RPC 系统,也用于在各种存储系统中持久存储数据。

Protobuf 诞生之初是为了解决服务器端新旧协议(高低版本)兼容性问题,名字也很体贴——“协议缓冲区”,只不过后期慢慢发展成用于传输数据。

5、Protobuf 协议的工作原理

如下图所示:可以看到,对于序列化协议来说,使用方只需要关注业务对象本身,即 idl 定义,序列化和反序列化的代码只需要通过工具生成即可。

6、Protobuf 协议的消息定义

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 的集合)。

除了上述定义外,文件顶部还有三行可帮助代码生成器的申明:

  • 1)syntax = "proto3":用于 idl 语法版本,目前有两个版本 proto2 和 proto3,两个版本语法不兼容,如果不指定,默认语法是 proto2(由于 proto3 比 proto2 支持的语言更多,语法更简洁,本文使用的是 proto3);

  • 2)package domain:此配置用于嵌套生成的类/对象;

  • 3)option java_package:生成器还使用此配置来嵌套生成的源(此处的区别在于这仅适用于 Java,在使用 Java 创建代码和使用 JavaScript 创建代码时,使用了两种配置来使生成器的行为有所不同。也就是说,Java 类是在包 com.Protobuf.generated.domain 下创建的,而 JavaScript 对象是在包 domain 下创建的)。

Protobuf 提供了更多选项和数据类型,本文不做详细介绍,感兴趣可以参考官方文档

7、Protobuf 的代码生成

首先安装 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());

8、Protobuf 的性能数据

我们简单地以上述 Customers 为模型,分别构造、选取小对象、普通对象、大对象进行性能对比。

序列化耗时以及序列化后数据大小对比:

反序列化耗时:

更多性能数据可以参考官方的测试Benchmark

9、Protobuf 的优点

9.1 效率高

从序列化后的数据体积角度,与 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 倍。

9.2 支持跨平台、多语言

Protobuf 是平台无关的,无论是 Android、iOS、PC,还是 C#与 Java,都可以利用 Protobuf 进行无障碍通讯。

proto3 支持 C++、Java、Python、Go、Ruby、Objective-C、C#(详见《Protobuf从入门到精通,一篇就够》)。

9.3 扩展性、兼容性好

Protobuf 具有向后兼容的特性:更新数据结构以后,老版本依旧可以兼容,这也是 Protobuf 诞生之初被寄予解决的问题,因为编译器对不识别的新增字段会跳过不处理。

9.4 使用简单

Protobuf 提供了一套编译工具,可以自动生成序列化、反序列化的样板代码,这样开发者只要关注业务数据 idl,简化了编码解码工作以及多语言交互的复杂度。

10、Protobuf 的缺点

Protobuf 的优点很突出,但缺点也很明显。

Protobuf 的缺点主要是:

1)不具备自描述能力:跟 XML、JSON 相比,这两者是自描述的,而 ProtoBuf 则不是;

2)数据可读性非常差:ProtoBuf 是二进制协议,如果没有 idl 文件,就无法理解二进制数据流,对调试非常不友好。

不过:Charles已经支持 Protobuf 协议,导入数据的描述文件即可,详情可参考 Charles Protocol Buffers

然而:由于没有 idl 文件无法解析二进制数据流,ProtoBuf 在一定程度上可以保护数据,提升核心数据被破解的门槛,降低核心数据被盗爬的风险(也算是缺点变优点的典型范例)。

11、参考资料

[1] Protobuf官方网站

[2] Protobuf从入门到精通,一篇就够!

[3] 如何选择即时通讯应用的数据传输格式

[4] 强列建议将Protobuf作为你的即时通讯应用数据传输格式

[5] APP与后台通信数据格式的演进:从文本协议到二进制协议

[6] 面试必考,史上最通俗大小端字节序详解

[7] 移动端IM开发需要面对的技术问题(含通信协议选择)

[8] 简述移动端IM开发的那些坑:架构设计、通信协议和客户端

[9] 理论联系实际:一套典型的IM通信协议设计详解

[10] 58到家实时消息系统的协议设计等技术实践分享

(本文已同步发布于:http://www.52im.net/thread-4081-1-1.html

用户头像

JackJiang

关注

还未添加个人签名 2019-08-26 加入

开源IM框架MobileIMSDK、BeautyEye的作者。

评论

发布
暂无评论
IM通讯协议专题学习(二):快速理解Protobuf的背景、原理、使用、优缺点_JackJiang_InfoQ写作社区