浅谈protobuf机制

1. protobuf是什么?

1.1 protobuf概述

protobuf是Google公司提出的一种轻便高效的结构化数据存储格式,常用于结构化数据的序列化,具有语言无关、平台无关、可扩展性特性,常用于通讯协议、服务端数据交换场景,github地址:https://github.com/protocolbuffers/protobuf

protobuf的核心内容包括:

  • 定义消息:消息的结构体,以message标识。
  • 定义接口:接口路径和参数,以service标识。

通过protobuf提供的机制,服务端与服务端之间只需要关注接口方法名(service)和参数(message)即可通信,而不需关注繁琐的链路协议和字段解析,极大降低了服务端的设计开发成本。

1.2 protobuf使用案例

假设有个服务端程序SSR,期望对外提供一个注册接口,调用方传入手机号姓名邮箱三个信息,其中邮箱是选填的,那通过protobuf如何来实现接口定义呢?

  • step1: 定义服务名SSR和方法名register
1
2
3
service SSR {
rpc register(RegisterRequest) returns RegisterResponse;
};
  • step2: 定义接口参数和约束
1
2
3
4
5
6
7
8
9
10
message RegisterRequest {
required string name = 1; // 姓名
required string tel = 2; // 手机号
optional string email = 3;
};

message RegisterResponse {
required int code = 1; // 错误码
optional string err_msg = 2; // 错误信息
};
  • step3: 使用protoc生成语言相关的协议代码
1
2
3
4
5
// for golang
protoc -I . -I /usr/local/include -I $(GOPATH)/src --go_out=. ssr.proto

// for cpp
protoc -I . -I /usr/local/include --cpp_out=. ssr.proto

利用官方提供的protoc生成语言无关的协议代码,比如golang、C++、python、PHP等等

  • step4:实现protobuf定义的接口
1
2
3
4
5
6
class SsrImpl : public proto::SSR {
void register(::google::protobuf::RpcController* controller,
const proto::RegisterRequest* request,
proto::RegisterResponse* response,
::google::protobuf::Closure* done);
}
  • step5:通过rpc框架注册Service & 实现业务逻辑
1
2
3
// 比如baidu-rpc框架为例
auto ssr = new baidu::rpc::Server();
ssr->AddService(new SsrImpl(), baidu::rpc::SERVER_DOESNT_OWN_SERVICE);

通过protobuf我们可以低成本的定义服务对外接口,并且无需关注繁琐的序列化/反序列过程,大大降低了服务端的开发设计成本。

1.3 protobuf延伸知识

为了支持复杂数据结构和实现对数据的种种约束,protobuf内置了自己的一套规则和语法,

在一些特殊场景需求时,可以查阅protobuf开发者文档或者源码介绍。

截止2022年3月份,protobuf版本已经更新至3.2版本。

2. 其他数据传输协议(JSON、FlatBuffers)

数据交换除了使用protobuf外,还可以使用JSON、FlatBuffers等,这里简单介绍一下各自的优缺点和适用场景。

2.1 JSON

JSON(JavaScript Object Notation) 是一种轻量级的数据交换格式。 易于人阅读和编写。同时也易于机器解析和生成。JSON建构于两种结构:
“名称/值”对的集合(A collection of name/value pairs)。不同的语言中,它被理解为对象(object),纪录(record),结构(struct),字典(dictionary),哈希表(hash table),有键列表(keyed list),或者关联数组 (associative array)。
值的有序列表(An ordered list of values)。在大部分语言中,它被理解为数组(array)。
JSON格式示例:

1
2
3
4
{
"城市": "上海",
"一周温度": [10, 12, 15, 12, 11, 15, 20]
}

这些都是常见的数据结构。事实上大部分现代计算机语言都以某种形式支持它们。这使得一种数据格式在同样基于这些结构的编程语言之间交换成为可能。

2.2 FlatBuffers

FlatBuffers是一种高效的跨平台的序列化库,目前已经支持C++, C#, C, Go, Java, Kotlin, JavaScript, PHP, Python等语言。它最初是Google公司为游戏开发等一些高性能应用创造出来的一种数据序列化协议。具有以下好处:

  • 无须解析、拆包即可访问序列化的数据。
  • 可扩展:和protobuf类似,提供了Optional字段支持扩展。
  • 强类型:编译时进行类型检查 & 校验,避免一些低级错误。
  • 跨平台。

2.3 Protobuf VS JSON VS FlatBuffers

protobuf

  • 优势:
    • 类型安全
    • 易用性好
    • 序列化/反序列性能好
    • 兼容性好
    • 不仅可以定义结构体,还可以定义rpc服务接口
  • 劣势:
    • 可读性较差:没有schema的情况下,难以阅读和编辑。
    • 灵活性较差:无法动态修改schema。

json: https://www.json.org/json-zh.html

  • 优势:

    • 可读性好:方便理解和编辑
    • 易用性好:使用简单
    • 灵活性好:支持动态修改schema
  • 劣势:

    • 序列化/反序列化性能差
    • 编码问题导致解析失败之类的

flatbuf: https://google.github.io/flatbuffers/

  • 优势:

    • 灵活性好
    • 性能好:序列化/反序列化几乎无耗时、高效的内存使用和读取速度。
    • 占用内存小
  • 劣势:

    • 可读性差
    • 易用性差:使用成本高,接口较为复杂

序列化/反序列化性能对比图:

各自的应用场景,可以参看图:

简单总结:

  1. rpc场景定义服务接口使用protobuf。
  2. 高频序列化/反序列化场景 & 期望灵活schema则使用flatbuf。
  3. 关注可读性&开发成本则使用JSON。

3. protobuf坑和小技巧

  1. 字符串如果包含非英文字符,建议使用bytes字段:https://developers.google.com/protocol-buffers/docs/proto#scalar
  2. 已有字段的field number不要修改:https://developers.google.com/protocol-buffers/docs/proto#assigning_field_numbers
  3. 慎用required字段,容易造成不兼容的隐患(既不能删,又不能加):https://stackoverflow.com/questions/31801257/why-required-and-optional-is-removed-in-protocol-buffers-3