一.安装
二.ProtocolBuf的作用
ProtocolBuf是谷歌提供的一种序列化解决方案。序列化常用于持久化存储与网络通信,假设在进行网络通信时需要传输一个数据结构,那么我们可以使用的最简单的方式是,服务器与客户端使用相同的方式进行内存布局(字节对齐,元素顺序),但这种方式不利于扩展也容易出现错误,比如:若在服务器程序在某次版本更新或功能扩展时,在传输结构中添加了一个字段,那么将造成与现有的客户端通信故障,这将不利于程序版本的更新与兼容,造成了服务器与客户端版本的强关联。当涉及到跨平台跨语言时,这个问题就变得更加复杂了。
Protocol Buffers提供了一种灵活,高效,自动序列化结构数据的机制,可以联想XML,但是比XML更小,更快,更简单。仅需要自定义一次你所需的数据格式, 然后用户就可以使用Protocol Buffers自动生成的特定的源码,方便的读写用户自定义的格式化的数据。不限语言,不限平台。还可以在不破坏原数据格式的基础上,依据老的数据格式, 更新现有的数据格式。通过 Protocol buffers ,你可以编写一个 .proto 文件描述你想要存储的数据结构。通过它, Protocol buffers 编译器创建一个类,以一种高效的二进制格式实现自动的编码和解析 Protocol buffers数据。生成的类为构成一个 Protocol buffers数据 的字段提供了getters和setters方法,并处理读取和写入 Protocol buffers 的细节。重要地是, Protocol buffers 格式通过使代码依然能够读取用老的格式编码的数据来支持随着时间对格式的扩展,即可做到新老格式的兼容,避免了之前说的问题。
三.ProtocolBuf语法
如前所述用户需要编写一个.proto文件描述要数据结构,二编写.proto文件需要遵守一定的语法规则。
1.message关键字
每一个message定义的结构,代表着一种数据格式,可以把他想象成C的struct,比如要定义一个消息格式,其中包括:消息类型,消息长度,校验和三个字段,可如下编写.proto文件
message InfoHead{
required string infoType = 1;
optional int32 infoLen = 2 [default = 0];
required int32 checkSum = 3;
}
1)声明字段类型
上例中的string,int32为每个字段的类型,字段类型不仅可以为标准类型,还可以是复合类型:枚举类型及其它message类型。
2)为字段设置数字标签
message中定义的每一个字段都有一个唯一的数字标签,每个数字标签都可以在二进制message中唯一标识一个字段。其中1~15数字标签在编码后只占用一个字节,这一个字节包括数字标签和字段类型,而16~2047的数字标签占两个字节。因此1~15数字标签应该用于最频繁出现的数据元素,有时还可以保留一些该区间的数字标签用于后续扩展。。
3)字段修饰符
protocolbuf协议中有三种字段标识符,且每个字段都要使用其中之一进行修饰
required | 其修饰的字段必须被赋值,不可为空,否则该条message会被认为是“uninitialized”。build一个 “uninitialized” message会抛出一个RuntimeException异常,解析一条“uninitialized” message会抛出一条IOException异常。除此之外,“required”字段跟“optional”字段并无差别。 【注】:由于一些历史原因,数字类型的repeated字段性能有些不尽人意,但是,PB已经做了改进,但是需要再添加一点改动,即在声明后添加[packed=true]例如: |
optional | 字段可以赋值,也可以不赋值。假如没有赋值的话,会被赋上默认值。对于简单类型,默认值可以自己设定(在后面加上[default = x];)。如果没有自行设定,会被赋上一个系统默认值,数字类型会被赋为0,String类型会被赋为空字符 串,bool类型会被赋为false。当获取没有显式 设置值的optional字段的值时,就会返回该字段的默认值(这说明不是在发送时设置默认值的)。 【问】:若新旧xxx.proto中字段的默认值不同为如何? |
repeated | 该字段可以重复任意次数,包括0次。重复数据的顺序将会保存在protocol buffer中,将这个字段想象成一个可以自动设置size的数组就可以了。 |
【问】:若在新版的xxx.protol中将某字段的修饰符从required更改为optional会怎么样?
【答】:若字段是赋值了的,那么不会有什么问题,若没有赋值,那么读取端在读取时发现该字段为required但未赋值,则会丢弃该字段,并报错。
4)一个.proto文件中可以定义多个message
2.注释
在PB中使用与C/C++相同的注释语法,即: //注释内容。
3.标准值类型
C++类型 | PB类型 | 说明 |
int32_t | int32 | 使用可变长编码. 对于负数比较低效,如果负数较多,请使用sint32 |
sint32 | 使用可变长编码. Signed int value. 编码负数比int32更高效 | |
sfixed32 | 恒定四个字节 | |
double | double | |
float | float | |
int64_t | int64 | 使用可变长编码. 对于负数比较低效,如果负数较多,请使用sint64 |
sint64 | 使用可变长编码. Signed int value. 编码负数比int64更高效 | |
sfixed64 | 恒定八个字节 | |
uint32_t | uint32 | 使用可变长编码 |
fixed32 | 恒定四个字节。如果数值几乎总是大于2的28次方,该类型比unit32更高效。 | |
uint64 | uint64 | 使用可变长编码 |
fixed64 | 恒定四个字节。如果数值几乎总是大于2的56次方,该类型比unit64更高效。 | |
bool | bool | |
string | string | A string must always contain UTF-8 encoded or 7-bit ASCII text. |
bytes | 包含任意数量的字节 |
4.枚举类型
可以像在C++中那样定义枚举类型,只是需要为每个字段设置数字标签(不会与其它message或枚举中的数字标签冲突,即可以与它们一样)。枚举类型可以定义在message中,也可定义在之外。比如:为消息类型定义为枚举类型
message InfoHead{
enum msgTpye{
UNKNOWTYPE = 0; // 不会与外部message中的数字标签冲突
SYN = 1;
ACK = 2;
}
required string infoType = 1;
optional int32 infoLen = 2 [default = 0];
required int32 checkSum = 3;
}
还可以为枚举类型中的枚举值设置别名,只需设置allow_alias为true并将相同的数字标签给不同的名称,如下:
message InfoHead{
enum msgTpye{
option allow_alias = true; // 不可少,否则不能为枚举值设置别名
UNKNOWTYPE = 0; // 不会与外部message中的数字标签冲突
ERRTYPE = 0;
SYN = 1;
ACK = 2;
}
required string infoType = 1;
optional int32 infoLen = 2 [default = 0];
required int32 checkSum = 3;
}
5.使用message类型做为字段类型
PB允许使用message类型做为字段类型,如下所示:
message InfoHead{
enum msgTpye{
UNKNOWTYPE = 0; // 不会与外部message中的数字标签冲突
SYN = 1;
ACK = 2;
}
required string infoType = 1;
optional int32 infoLen = 2 [default = 0];
required int32 checkSum = 3;
}
message InfoMsg {
InfoHead head;
repeated string data;
}
可以通过导入其他.proto文件来使用其内的定义。为达此目的,需要在现.proto文件前增加一条import语句:
import "myproject/other_protos.proto";
6.嵌套类型
PB支持message内嵌套message,如下所示:
message InfoMsg{
enum msgTpye{
UNKNOWTYPE = 0; // 不会与外部message中的数字标签冲突
SYN = 1;
ACK = 2;
}
required string infoType = 1;
optional int32 infoLen = 2 [default = 0];
required int32 checkSum = 3;
message InfoBody {
repeated string data;
}
repeated InfoBody infoData;
}
如果想要在父Message外复用该message的话,可以用Parent.Type格式来引用。如:
message SomeOtherMessage {
optional InfoMsg.InfoBody result = 1;
}
【注】:PB支持无限深层次的message嵌套。
7.Package关键字
PB建议在.proto文件开头添加一个package说明符来避免不同message类型的名字冲突,就好像是C++中的命名空间一样。在PB中的使用如下所示:
package foo.bar;
message Open { ... }
// 接着可以使用该命名空间
message Foo {
...
required foo.bar.Open open = 1;
...
}
四.修改Message对象
如前所述,使用PB进行序列化,可以很好的应对版本的更新,允许两端以新旧格式进行通信。但是必须注意以下几点,否则会造成新旧格式的不匹配。
- 不要修改现有字段后面的数字标签。(因为前面说了在PB二进制数据中,数字标签可以唯一标识其中的一个字段,若是更改了,那么这种映射关系也就乱了)。
- 只能新增optional或者repeated字段,即不可新增required字段。(因为required字段要求必须被赋值,假设发送端使用的是旧的.procol,接收端使用的是新的.procol,那么接收端为因为收到的数据中未对新的required字段赋值而报错)。
- 可以删除非必须字段(即非required字段),但是他们的数字标签不能再被使用。最好的方法是不删除,而是修改名字,比如在前缀上加OBSOLETE_,这样就可以避免后人尽量少的出错。
- 非required字段可以转化成extension字段,反之亦然,同时保留原类型和数字标签(不太懂)
- int32, uint32, int64, uint64, 和bool是兼容的。即这些字段可以相互切换,在代码处理的时候,不会出错,但是小心范围小的数据接收范围大的数据会发生截断
- sint32, sint64是相互兼容的,但是不与其他整型类型兼容(这应该是因为编码方式不同)。
- string和bytes是兼容的,因为bytes也是合法的UTF-8
- Embedded messages are compatible with bytes if the bytes contain an encoded version of the message(不知道怎么翻译了)
- fixed32与 sfixed32兼容, fixed64 与sfixed64兼容
- optional与repeated兼容,也存在数据截断,假如讲一个repeated的序列化后的数据作为输入给客户端,客户端会截取最后一个原子类型的字节。或者,如果是一个message类型的字段的话,合并所有的元素。
- 可以修改字段默认值
五.简单使用示例
以下示例已TCP通信为例,其实现建立在之前实现的一个简单Tcp封装之上。
1.示例1
step1:建立一个.proto文件,
package InfoTypeSpace;
message InfoStruct {
required int32 iType = 1;
}
step2:输入命令
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
//【注】 第一个路径:-I=$放置你的应用程序源代码的路径;
// 第二个路径:DST_DIR:生成C++文件的路径,cpp_out说明要生成对应的C++文件
// 第三个路径:$SRC_DIR/addressbook.proto即.proto文件的路径
// 【注意问题】:在使用时发现第一个路径和第三个路径必须相同,否则会出现错误
step3:使用.proto文件生成的文件进行序列化
在linu环境下,分别使用了Qt与直接使用g++进行过编译,下面分别进行介绍。
1)在Qt下使用protocolbuffers
首先将简易TCP封装与之前用protoc命令生成的.h及.c文件加入工程,服务器端与客户端文件组成如下如下所示:
服务端主程序:
#include "InfoStruct.pb.h"
#include "sampletcpserver.h"
#include <iostream>
#include <unistd.h>
using namespace InfoTypeSpace;
// 成功建立连接后等待接收服务器发送来的消息,并使用protocolBuf进行反序列化
void newConnCallback(int fd, sockaddr_in clientAddr) {
InfoStruct info;
char buf[65536];
while(recv(fd, buf, sizeof buf, 0) != 0) {
info.ParseFromString(buf);
std::cout<<"infotype: "<<info.itype()<<std::endl;
}
::close(fd);
}
int main()
{
HDU::NET::SampleTcpServer server; // 服务器类对象
server.bind("127.0.0.1", 9090);
// 设置服务器在新连接建立时的回调函数
server.setNewConnCb(std::bind(newConnCallback, std::placeholders::_1,std::placeholders::_2));
server.start(); // 启动服务器
return 0;
}
客户端主程序:
#include "sampletcpclient.h"
#include "InfoStruct.pb.h"
#include <arpa/inet.h>
#include <netinet/tcp.h>
#include <unistd.h>
// 连接成功回调,在连接成功后,通过protocolBuf进行序列化发送
void connectedCallback(int fd) {
InfoTypeSpace::InfoStruct info;
info.set_itype(100);
info.SerializePartialToFileDescriptor(fd);
::shutdown(fd,SHUT_WR);
}
int main()
{
HDU::NET::SampleTcpClient client; // 封装的客户端对象
// 设置连接成功的回调函数
client.setConnSuccCallback(std::bind(connectedCallback, std::placeholders::_1));
// 禁用Nagle
client.setTcpNodelay();
client.connectToHost("127.0.0.1", 9090); //连接服务器
return 0;
}