最初是在 “序列化” 问题时接触到这个库的(作为XML的替代方案), 之后慢慢熟悉, 发现其在数据存储和RPC数据交换领域有大作为, 好比说客户端Java, 服务器端Cpp; 服务器端通信直接一字节对齐, char来char去, 但是Java就复杂很多(ByteBuffer中根据预定的报文格式, 然后逐字节的解析字段), 并且容易出现拼接错误; 而使用SOAP协议(WebService)作为消息报文的格式载体, 由该方式生成的报文是基于文本格式的, 同时还存在大量的XML描述信息,因此将会大大增加网络IO的负担, 解析速度也不快.
于是, 来了protocol buffer, 简称protobuffer或者pb.
本文详细地介绍一下我的一些实践和心得.
引子
快速说一下它的某个简单使用, 引入话题.
比如有个电子商务的系统(假设用C++实现),其中的模块A需要发送大量的订单信息给模块B,通讯的方式使用socket.
假设订单包括如下属性:
1 | 时间:time(用整数表示) |
如果使用protobuf实现,首先要写一个proto文件(不妨叫Order.proto),在该文件中添加一个名为”Order”的message结构,用来描述通讯协议中的结构化数据。该文件的内容大致如下:
(解释一下: .proto文件确定数据协议,数据结构中存在哪些数据,数据类型是怎么样; 该文件就是一个接口规范)
1 | message Order |
关于modifiers的说明:
- required 不可以增加或删除的字段,必须初始化
- optional 可选字段,可删除,可以不初始化
- repeated 可重复字段, 对应到java文件里,生成的是List
(在protoBuf 3中,optional和required都不需要的了,如果配了两个,protoBuf 编译器还会报错,但是repeated的还是保留的,用来说明该字段是一个list)
然后,使用protobuf内置的编译器编译 该proto。由于本例子的模块是C++,你可以通过protobuf编译器的命令行参数,让它生成C++语言的“订单包装类”。(一般来说,一个message结构会生成一个包装类)然后你使用类似下面的代码来序列化/解析该订单包装类:
1 | // 发送方 |
注: 生成的类中还有其他的序列化API, 比如 ParseFromIstream(&input)
以及 SerializeToOstream(&output)
等.
有了这种代码生成机制,开发人员再也不用吭哧吭哧地编写那些协议解析的代码了.
万一将来需求发生变更,要求给订单再增加一个“状态”的属性,那只需要在Order.proto文件中增加一行代码。对于发送方(模块A),只要增加一行设置状态的代码;对于接收方(模块B)只要增加一行读取状态的代码。哇塞,简直太轻松了!
不说了, 太激动, 请看详情.
正文
优缺点
优点
- 性能好, 效率高(XML解析速度慢, XML冗余信息多空间占用多)
- 代码生成机制, 数据解析类自动生成(这个最赞, 你不需要自己去写解析代码了)
- 支持向后兼容和向前兼容(得益于约定好的协议, 即使增加字段, 先前的代码也不必改变)
- 多语言支持(主流语言Cpp,Java,Python; 前后端语言不一样也没有关系, 协议的处理一致)
- 平台无关
- 不必学习DOM模型
性能上和 JSON 拼也是不怕的 参考
和 XML,JSON,Thrift相比,Protobuf 有什么不同呢?简单说来 Protobuf 的主要优点就是:简单,快。
你可以定义自己的数据结构,然后使用代码生成器生成的代码来读写这个数据结构。只需使用 Protobuf 对数据结构进行一次描述,即可利用各种不同语言或从各种不同数据流中对你的结构化数据轻松读写。
缺点
- 应用不够广
- 为了提高性能, 导致可读性差(二进制格式)
正是由于可读性, Protocol Buffer还取代不了XML.
安装
Linux平台 和 Window(VS2015)
源码安装:
Linux平台
Ubuntu 14, 64bit1
git clone https://github.com/google/protobuf.git
当然你也可以去release网页下找到相关的包, 在进行编译.
先确保你及其上有相关的工具集合:
- autoconf
- automake
- libtool
- curl
- make
- g++
- unzip
然后确保有configure
文件, 没有话 ./autogen.sh
生成相关脚本(并下载gmock).
然后编译安装(可以自己制定位置, 我就默认了)1
2
3
4
5$ ./configure
$ make
$ make check
$ sudo make install
$ sudo ldconfig # refresh shared library cache.
此时 /usr/local/include/google/protobuf
以及 /usr/local/lib
下就有了相应的头文件和库文件, 并且增加了/usr/local/bin/protoc
编译器, 此时安装成功.
windows平台
这里采用的是VS2015, 操作系统win10 64位.
在这个网站找到 protoc-$VERSION-win32.zip
It contains the protoc binary as well as public proto files of protobuf library.
使用案例
主要的使用步骤就是:
- 定义你自己的数据结构格式(.pro)源文件
- 利用 ProtoBuf 提供的编译器编译源文件
- 使用 ProtoBuf C++的 API 读写消息
简单Demo
以linux平台为例, 先实践一个简单的:
定义一个person.pro1
2
3
4
5
6
7
8
9syntax = "proto2";
package lm;
message helloworld
{
required int32 id = 1; // ID
required string str = 2; // str
optional int32 opt = 3; //optional field
}
编译:(/usr/local/bin/protoc)1
protoc -I. --cpp_out=. lm.helloworld.proto
生成了一个头文件和一个源文件:
- lm.helloworld.pb.cc
- lm.helloworld.pb.h
相当于POJO类或者domain类已经有(并且序列化API也已经有了), 类名 lm::helloworld
, 现在可以进行序列化了.
简单的操作代码如下:
(Writer和Reader来对消息进行操作, 序列化和反序列化)
writer.cpp1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
using namespace std;
int main(void)
{
//主要逻辑如下
lm::helloworld msg1;
msg1.set_id(101);
msg1.set_str("hello");
// Write the new address book back to disk.
fstream output("./log", ios::out | ios::trunc | ios::binary);
if (!msg1.SerializeToOstream(&output)) {
cerr << "Failed to write msg." << endl;
return -1;
}
return 0;
}
reader.cpp1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using namespace std;
void ListMsg(const lm::helloworld & msg) {
cout << msg.id() << endl;
cout << msg.str() << endl;
}
int main(int argc, char* argv[]) {
lm::helloworld msg1;
{
fstream input("./log", ios::in | ios::binary);
if (!msg1.ParseFromIstream(&input)) {
cerr << "Failed to parse address book." << endl;
return -1;
}
}
ListMsg(msg1);
return 0;
}
(上面的代码主要用到了 SerializeToOstream
和 ParseFromIstream
两个成员方法)
然后编译一下1
2
3
4
5
6
7
8
9
10
11
12main:write read
write:writer.cpp lm.helloworld.pb.cc
g++ -g -Wall -O0 writer.cpp lm.helloworld.pb.cc \
-o write -I. -L/usr/local/lib -lprotobuf
read:reader.cpp lm.helloworld.pb.cc
g++ -g -Wall -O0 reader.cpp lm.helloworld.pb.cc \
-o read -I. -L/usr/local/lib -lprotobuf
clean:
rm -f read write
如果你不知道库的具体位置, 那么可以使用pkg-config --cflags --libs protobuf
代替后面的 -I
等(该工具通过PKG_CONFIG_PATH环境变量指定的地址去找.pc文件, 该文件记录了ProtoBuf安装时头文件和库文件所在的目录, 但是注意, 把PKG_CONFIG_PATH
环境变量设置正确, 例如$export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig/
)
运行结果:1
2
3
4merlin@remote~/test/pb_test$ ./write
merlin@remote~/test/pb_test$ ./read
101
hello
总结下来, 其实protocol buffer这个库, 就是解决了你序列化, 或者消息传递时, 需要去写实体类, 以及序列化API的工作, 并且由于消息是语言无关的, 所以又可以跨语言了(当然跨语言时数据类型对接, 还是要在这些API中得到解决的, 这也是该库工作的一部分).
如果你还没有意识到这个库的重要性, 那么下面涉及到RPC, 序列化的部分, 看看吧:
老实说, 初级部分没有太多讲的, 下面看看生成的代码(具体的工程实现细节就不展开了, 只看生成代码的结构, 下面的代码我已经添加了注释):
1 | //根据option optimize_for优化类型不同, 可能继承自Message或者MessageLite |
上面的代码Message没有嵌套, 还是蛮简单的, 主要使用的是和字段相关的方法(其他序列化方法, 如 SerializeToArray 等, 不在此类中实现). 上目前的 ByteSizeLong()
主要用于序列化到buffer或者array时, 例如:1
2
3
4
5
6
7
8
9
10
11helloworld message;
message.set_str("xxx");
message.set_id(1);
message.set_opt(2);
size_t length = message.ByteSizeLong();
char *buf = new char[length];
message.SerializeToArray(buf, length);
//读的时候 ParseFromArray
delete []buf;
buf = NULL;
复杂案例
复杂的案例, 也就是带有嵌套message. 和简单message不同的是, 不能简单的使用set方法设置被嵌套在message内部的内容了, 而是用另外的方法. 下面举一个案例, 如下.
.proto文件如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20enum UserStatus {
OFFLINE = 0;
ONLINE = 1;
}
enum LoginResult {
LOGON_RESULT_SUCCESS = 0;
LOGON_RESULT_NOTEXIST = 1;
LOGON_RESULT_ERROR_PASSWD = 2;
LOGON_RESULT_ALREADY_LOGON = 3;
LOGON_RESULT_SERVER_ERROR = 4;
}
message UserInfo {
required int64 acctID = 1;
required string name = 2;
required UserStatus status = 3;
}
message LogonRespMessage {
required LoginResult logonResult = 1;
required UserInfo userInfo = 2; //这里嵌套了UserInfo消息
}
生成的关键代码如下:1
2
3
4
5
6
7
8
9
10// required .UserInfo userInfo = 2;
//下面的成员函数都是因message中定义的UserInfo字段而生成。
//这里只是列出和非消息类型字段差异的部分。
static const int kUserInfoFieldNumber = 2;
inline bool has_userinfo() const;
inline void clear_userinfo();
inline const ::UserInfo& userinfo() const; //重要
inline ::UserInfo* mutable_userinfo(); //重要
inline ::UserInfo* release_userinfo(); //重要
可以看到该类并没有生成用于设置和修改userInfo字段set_userinfo函数,而是将该工作交给了下面的mutable_userinfo函数. 因此每当调用函数之后, Protocol Buffer都会认为该字段的值已经被设置了, 同时 has_userinfo 函数亦将返回true. 在实际编码中, 我们可以通过该函数返回 userInfo 字段的内部指针, 并基于该指针完成userInfo成员变量的初始化工作.
而对该字段的操作, 大致如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26LogonRespMessage logonResp;
logonResp.set_logonresult(LOGON_RESULT_SUCCESS);//设置枚举
//通过mutable_userinfo方法拿到指针
UserInfo* userInfo = logonResp.mutable_userinfo();
userInfo->set_acctid(200);
userInfo->set_name("Tester");
userInfo->set_status(OFFLINE);
//序列化
int length = logonResp.ByteSize();
char* buf = new char[length];
logonResp.SerializeToArray(buf,length);
//反序列化
LogonRespMessage logonResp2;
logonResp2.ParseFromArray(buf,length);
printf("LogonResult = %d, UserInfo->acctID = %I64d,
UserInfo->name = %s, UserInfo->status = %d\n",
logonResp2.logonresult(),
logonResp2.userinfo().acctid(),
logonResp2.userinfo().name().c_str(),
logonResp2.userinfo().status());
delete [] buf;
如果嵌套中涉及到了 repeated
, 例如下面这个案例:
1 | message BuddyInfo { |
那么它生成代码中, 关于 RetrieveBuddiesResp
部分可能是这样的:
1 | class RetrieveBuddiesResp : public ::google::protobuf::MessageLite { |
对齐操作, 大致上是这样的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
RetrieveBuddiesResp retrieveResp;
retrieveResp.set_buddiescnt(2);
//拿到新元素指针, 然后初始化
BuddyInfo* buddyInfo = retrieveResp.add_buddiesinfo();
buddyInfo->set_groupid(20);
UserInfo* userInfo = buddyInfo->mutable_userinfo();
userInfo->set_acctid(200);
userInfo->set_name("user1");
userInfo->set_status(OFFLINE);
//下面重复利用了 UserInfo* userInfo
buddyInfo = retrieveResp.add_buddiesinfo();
buddyInfo->set_groupid(21);
userInfo = buddyInfo->mutable_userinfo();
userInfo->set_acctid(201);
userInfo->set_name("user2");
userInfo->set_status(ONLINE);
//序列化
int length = retrieveResp.ByteSize();
char* buf = new char[length];
retrieveResp.SerializeToArray(buf,length);
//反序列化
RetrieveBuddiesResp retrieveResp2;
retrieveResp2.ParseFromArray(buf,length);
printf("BuddiesCount = %d\n",retrieveResp2.buddiescnt());//int32
printf("Repeated Size = %d\n",retrieveResp2.buddiesinfo_size());
//通过容器迭代器的方式遍历数组元素的测试代码
//也可以通过( buddiesinfo_size和 buddiesinfo(index) 方式)
//RepeatedPtrField< ::BuddyInfo > &buddiesinfo() const;
RepeatedPtrField<BuddyInfo> *buddiesInfo = retrieveResp2.mutable_buddiesinfo();
RepeatedPtrField<BuddyInfo>::iterator it = buddiesInfo->begin();
for ( ;it != buddiesInfo->end(); ++it) {
printf("BuddyInfo->groupID = %d\n", it->groupid());
printf("UserInfo->acctID = %I64d, UserInfo->name = %s, UserInfo->status = %d\n",
it->userinfo().acctid(),
it->userinfo().name().c_str(),
it->userinfo().status());
}
delete [] buf;
Protocol Buffer仍然提供了很多其它非常有用的功能,特别是针对序列化的目的地,比如文件流和网络流等。与此同时,也提供了完整的官方文档和规范的命名规则,在很多情况下,可以直接通过函数的名字便可获悉函数所完成的工作。
语言规范
.proto文件
要通信,必须有协议,否则双方无法理解对方的码流。在protobuf中,协议是由一系列的消息组成的。因此最重要的就是定义通信时使用到的消息格式。
消息格式
字段格式
字段格式:
限定修饰符 | 数据类型 | 字段名称 | = | 字段编码值| [字段默认值]
- 限定符
required\optional\repeated
特别说明: repeated表示的字段可以包含0个或多个数据(这一点有别于C++/Java中的数组, 因为后两者中的数组必须包含至少一个元素).升级建议
: 项目投入运营以后涉及到版本升级时的新增消息字段全部使用optional或者repeated, 尽量不实用required. 如果使用了required,需要全网统一升级,如果使用optional或者repeated可以平滑升级. 不要修改已经存在字段的编码号. 在原有的消息中, 不能移除已经存在的required字段, optional和repeated类型的字段可以被移除, 但是他们之前使用的编码号必须被保留, 不能被新的字段重用. 数据类型
protobuf定义了一套基本数据类型, 这些数据类型, 可以映射到C++\Java等语言的基础数据类型.
完整的映射表格如下:
几乎所有的整型都是, 变长编码; 表示存储的字节数根据数据的大小或者长度而定.
例如int32,如果数值比较小,在0~127时,使用一个字节打包, 而并不总是4字节; 甚至大的数字可能占用5个字节.
当然message本身也是一种类型(消息可以嵌套消息), 在C++里就表示 object of class.
关于fixed32和int32的区别: fixed32的打包效率比int32的效率高, 但是使用的空间一般比int32多. 因此一个属于时间效率高, 一个属于空间效率高. 根据项目的实际情况, 一般选择fixed32, 如果遇到对传输数据量要求比较苛刻的环境, 可以选择int32.
枚举值必须大于等于0的整数, 并且使用分号(;)分隔枚举变量而不是C++语言中的逗号(,), 例如:1
2
3
4
5
6
7enum voip_protocol
{
H323 = 1;
SIP = 2;
MGCP = 3;
H248 = 4;
}字段名称
protobuf建议字段的命名采用以下划线分割的驼峰式, 例如 first_name. (基本和C语言一样)字段编码值
字段的编码值, 表示不同的字段在序列化后的二进制数据中的布局位置(数字越大越靠后); 根据字段的类型, 一般有对应关系:
消息中的字段的编码值无需连续, 只要是合法的, 并且不能在同一个消息中有字段包含相同的编码值.
根据其内部实现来看, 一般使用在封包和解包时, 你可以把它简单理解成, 编码位置. 不同的编码值, 传输效率是不一样的. 例如编码值的取值范围为 1~2^32, 其中1~15的编码时间和空间效率都是最高的.编码值越大,其编码的时间和空间效率就越低(相对于1-15), protobuf 还建议把经常要传递的值把其字段编码设置为1-15之间的值.默认值
默认值. 当在传递数据时,对于required数据类型,如果用户没有设置值,则使用默认值传递到对端。当接受数据时,对于optional字段,如果没有接收到optional字段,则设置为默认值。(一般是反序列化的时候用的, 默认值一般系统指定; 当然你也可以指定, 见上面的编码值对应表)
关于import
protobuf 接口文件可以像C语言的h文件一个, 分离为多个, 在需要的时候通过 import导入需要对文件. 其行为和C语言的#include或者java的import的行为大致相同, 个人觉得更像Java.
关于package
避免名称冲突.可以给每个文件指定一个package名称. 对于java解析为java中的包, 对于C++则解析为名称空间. 例如:1
package ourproject.lyphone;
Option选项
在.proto文件中, 还有一些常用选项(示Protocol Buffer编译器帮助我们生成更为匹配的目标语言代码). Protocol Buffer内置的选项被分为以下三个级别:(作用范围不一样)
- 文件级别: 这样的选项将影响当前文件中定义的所有消息和枚举
- 消息级别: 这样的选项仅影响某个消息及其包含的所有字段
- 字段级别: 这样的选项仅仅响应与其相关的字段
常用的Protocol Buffer选项如下:
1 | 1. option java_package = "com.companyname.projectname"; |
命令行编译工具
一般语法是:1
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR path/to/file.proto
简单解释一下:
- protoc为Protocol Buffer提供的命令行编译工具
/usr/local/bin/protoc
- –proto_path等同于-I选项,主要用于指定待编译的.proto消息定义文件所在的目录, 或者包含目录该选项可以被同时指定多个
- –cpp_out选项表示生成C++代码,–java_out表示生成Java代码, –python_out则表示生成Python代码, 其后的目录为生成后的代码所存放的目录
- path/to/file.proto表示待编译的消息定义文件路径
对于C++而言,通过Protocol Buffer编译工具,可以将每个.proto文件生成出一对.h和.cc的C++代码文件。生成后的文件可以直接加载到应用程序所在的工程项目中。如:MyMessage.proto生成的文件为MyMessage.pb.h和MyMessage.pb.cc。
动态编译
运行时编译, 动态解析(该部分对应一些特殊的需求)
.proto文件如下:1
2
3
4
5
6
7package lm;
message helloworld
{
required int32 id = 1;
required string str = 2;
}
利用compiler 内部的对象:
- MultiFileErrorCollector
- DiskSourceTree
- Importer
即可完成运行时编译, 详细的代码:1
2
3
4
5
6
7google::protobuf::compiler::MultiFileErrorCollector errorCollector;
google::protobuf::compiler::DiskSourceTree sourceTree;
google::protobuf::compiler::Importer importer(&sourceTree, &errorCollector);
sourceTree.MapPath("", protosrc);
importer.import(“lm.helloworld.proto”);
首先构造一个 importer 对象.构造函数需要两个入口参数,一个是 source Tree 对象,该对象指定了存放 .proto 文件的源目录。第二个参数是一个 error collector 对象,该对象有一个 AddError 方法,用来处理解析 .proto 文件时遇到的语法错误. 之后,需要动态编译一个 .proto 文件时,只需调用 importer 对象的 import 方法。
描述消息: (Package google::protobuf::compiler )
- 类 FileDescriptor 表示一个编译后的 .proto 文件;
- 类 Descriptor 对应该文件中的一个 Message;
- 类 FieldDescriptor 描述一个 Message 中的一个具体 Field
三者的组合关系是: FileDescriptor > Descriptor > FieldDescriptor
例如:1
2
3
4const protobuf::Descriptor *desc =
importer_.pool()->FindMessageTypeByName(“lm.helloworld”);
const protobuf::FieldDescriptor* field =
desc->pool()->FindFileByName (“id”);
内部实现
该部分主要了解其两个特征:
- 序列化后的信息内容紧凑(这得益于 Protobuf 采用的非常巧妙的 Encoding 方法)
- 封包和解包的过程
编码
关于编码(encode)这一块儿, 怎么样压缩字节存储才最省事儿, 也最容易区分字段field的key-value, 一直是可以改进的领域; 貌似属于科学家研究的问题了. 我这里不展开, 如果展开估计也有一大堆人, 看不懂, 例如:
1 | 31 30 31 3C 2F 69 64 3E 3C 6E 61 6D 65 3E 68 65 |
其中还涉及到了big, little-endian
大端模式,是指数据的高字节保存在内存的低地址中,而数据的低字节保存在内存的高地址中,这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放;这和我们的阅读习惯一致。(从左到右, 地址从低到高)
小端模式,是指数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中,这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低。
由于encode和decode涉及到了, 太多的工程细节, 对于实际开发帮助不大, 下面给出一个 Go语言的分析参考.
但是我不得不说, 即使你看懂了, 看明白了, 真正让你去设计, 还是非常难的, 请量力而行(作为兴趣了解一下还是可以的, 但是作为解决方案参考意义不大).
封包和解包
对于XML和JSON的封包和解包.
首先一个是二进制存储, 其他的JSON, XML都是文本存储, 原理都不一样; 其次, 我们来了解一下 XML 的封解包过程:
XML 需要从文件中读取出字符串,再转换为 XML 文档对象结构模型。之后,再从 XML 文档对象结构模型中读取指定节点的字符串,最后再将这个字符串转换成指定类型的变量。这个过程非常复杂,其中将 XML 文件转换为文档对象结构模型的过程通常需要完成词法文法分析等大量消耗 CPU 的复杂计算。
好了, XML已经输了.
protobuf呢?
简单的位运算, 速度非常快; 之后存储到c++或者其他语言对应的数据类型中即可. 它只需要简单地将一个二进制序列, 按照指定的格式读取到 C++ 对应的结构类型中就可以了.
整个解析过程需要 Protobuf 本身的框架代码和由 Protobuf 编译器生成的代码共同完成:
- pb框架:CodedInputStream 类,WireFormatLite 类等提供了对二进制数据的 decode 功能
- pb编译器生成相关语言的类结构代码, 例如lm::Helloworld类的结构 (生成相关类, 一定是编译器在做这件事儿)
(之后两者配合, 把读取出来的数据, 赋值给Helloworld类的相应数据成员)
编写新的Proto编译器
使用 Google Protocol Buffer 的 Compiler 包,您可以开发出支持其他语言的新的编译器(原生最开始支持 C++, Java, Python)
类 CommandLineInterface 封装了 protoc 编译器的前端,包括命令行参数的解析,proto 文件的编译等功能。您所需要做的是实现类 CodeGenerator 的派生类,实现诸如代码生成等后端工作.
(该部分属于个高级内容)
对比Thrift
大名鼎鼎的 Thrift, 老早就在听外包公司的大佬在说这个脸书
的框架, 它和pb对抗起来如何呢?
protocol buffer 和 thrift对比, 简单来说, 没有可比性
.
thrift是全套解决方案, 而protobuffer则仅仅是rpc或者序列化问题的消息解决方案, 两者不在一个级别上. 从框架的复杂性上来说, thrift相对来说比较复杂, 但是功能丰富(全套RPC解决方案,包括序列化机制、传输层、并发处理框架等). pb更加实用, 而thrift更加系统.
我的另外一篇博文有详细的说 thrift .
尾巴
懂了原理以及该库的作用之后, 复杂的案例最多不过帮助你熟悉相关的API; 实际上日常的开发中, 只有在定制化封包, 编码等高级需求中, 才会要求你去了解的实现原理. 好在文本也涉及到了.
花了好大的力气, 不过总结告一段落了.进一步的学习, 只有源码解析部分了, 有时间了再说.
(关于pb2, pb3各自相近的语法, 本文没有说明, 请参考官方文档)
参考资料
官方文档
pb2的语法:
https://developers.google.com/protocol-buffers/docs/proto
pb3的语法
https://developers.google.com/protocol-buffers/docs/proto3
Protocol Buffer Basics(C++) :
https://developers.google.com/protocol-buffers/docs/cpptutorial
其他参考
- http://mikewang.blog.51cto.com/3826268/1432136
- http://blog.csdn.net/caisini_vc/article/details/5599468
- http://cxshun.iteye.com/blog/1974498
- https://www.ibm.com/developerworks/cn/linux/l-cn-gpb/
- http://blog.csdn.net/sheismylife/article/category/1227779
- http://blog.csdn.net/hailong0715/article/details/52057873
- https://developers.google.com/protocol-buffers/docs/cpptutorial
- http://blog.csdn.net/hguisu/article/details/20721109
- http://shanshanpt.github.io/2016/09/10/protobuf-encode.html
- http://www.cnblogs.com/stephen-liu74/category/442364.html