【网络编程】网络传输-protobuf
一、protobuf介绍
1.1 protobuf 是什么?
-
Protocol Buffers (Protobuf) 是 Google 开发的一种高效、跨语言的序列化机制,用于结构化数据的编解码。它通过定义数据结构(
.proto
文件),生成对应语言的代码,实现高效的数据传输和存储。 -
google 提供了多种语言的实现:java、c#、c++、go 和 python 等,每一种实现都包含了相应语言的编译器以及库文件
1.2 protobuf 工作原理
1.2.1 定义数据结构文件(.proto文件)
使用 .proto
文件定义消息结构,语法类似C
语言,每个数据类型由message
组成,比如C++
语言有如下结构体:
struct node{int a;std::string b;std::string c;
};
那么对应的.proto
文件的写法如下,有以下几点规则:
- 每个类型成员后面需要添加上
=
,并且标注上编号,编号必须从1
开始,不能重复 - 每个成员最后以
;
结尾
message{int32 a = 1;bytes b = 2;bytes c = 2;
}
.proto
文件还需要在文件的开头添加上protobuf
的版本号,protobuf3
的版本声明如下:
syntax = "proto3"
一个简单的.proto
完整如下
syntax = "proto3"message{int32 a = 1;bytes b = 2;bytes c = 2;
}
protobuf
中的数据类型对应C++
的数据类型对照表如下:
Protobuf 类型 | C++ 类型 | 备注 |
---|---|---|
double | double | 64位浮点数 |
float | float | 32位浮点数 |
int32 | int | 32位整数 |
int64 | long | 64位整数 |
uint32 | unsigned int | 32位无符号整数 |
uint64 | unsigned long | 64位无符号整数 |
sint32 | signed int | 32位整数,处理负数效率比int32更高 |
sint64 | signed long | 64位整数,处理负数效率比int64更高 |
fixed32 | unsigned int(32位) | 总是4个字节。如果数值总是比总是比228大的话,这个类型会比uint32高效。 |
fixed64 | unsigned long(64位) | 总是8个字节。如果数值总是比总是比256大的话,这个类型会比uint64高效。 |
sfixed32 | int (32位) | 总是4个字节 |
sfixed64 | long (64位) | 总是8个字节 |
bool | bool | 布尔类型 |
string | string | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本 |
bytes | string | 处理多字节的语言字符、如中文, 建议protobuf中字符型类型使用 bytes |
enum | enum | 枚举 |
message | object of class | 自定义的消息类型 |
1.2.2 protobuf 命令行
完成了.proto
文件的编写,我们可以使用protoc
可执行文件执行命令来生成.h
和.cc
文件了,用于我们后续的序列化和反序列化:
假设我们当前路径下有上述的node.proto
文件,下述命令用于在当前路径下生成对应的cpp
文件:
-
protoc
为可执行程序,是我们编译protobuf
库生成的,添加运行库到环境变量中 -
-I
后面添加.proto
文件所在路径,当前路径即为.
,然后空格,添加文件的名称,这里是node.proto
, -
--cpp_out
表示输出的文件为C++
编程语言,后面加上生成文件的路径,这里是生成到当前路径下,因此之间使用.
即可
protoc -I . node.proto --cpp_out=.
1.2.3 使用生成的文件
使用命令行生成文件后,会有node.pb.h
和node.pb.cc
文件,使用其提供的接口就可以完成序列化反序列化了
二、protobuf的配置
我们这里介绍在ubuntu
下配置protobuf3.20
版本
-
在
github
上下载protobuf
的源码,下载链接:https://github.com/protocolbuffers/protobuf/releases/tag/v3.20.0 -
解压源码压缩包,进入到对应的目录,执行
configure
命令检查配置环境
cd protobuf-cpp-3.20.0/protobuf-3.20.0
./configure
- 编译、安装,安装后的动态库在
/usr/local/lib
make -j4 && sudo make install
- 添加环境变量,将
libprotobuf.so
动态库添加到/etc/ld.so.conf
配置文件
sudo gedit /etc/ld.so.conf
- 更新配置文件
sudo ldconfig
- 查看
protoc
可执行程序是否配置完成,查看对应的版本
protoc --verison
三、使用protobuf
3.1 简单使用protobuf
3.1.1 编写.proto
文件
假设我们C++
需要传输如下的数据
struct login_msg{std::string user;std::string password;
};
我们可以这样写login.proto
文件
syntax = "proto3";message login_msg{bytes user = 1;bytes password = 2;
}
3.1.2 生成*.pb.h
和*.pb.cc
文件
使用命令行生成对应的login.pb.h
和login.pb.cc
文件
protoc -I . login.proto --cpp_out=./protobuf/
3.1.3 序列化数据
查看生成的.h
文件,可以看到有set_user
的接口,以及set_password
接口,这些是用于对login_msg
类的赋值接口
因此,我们可以这样赋值:
#include"protobuf/login.pb.h"//初始化
login_msg loginMsg;
loginMsg.set_user("Antonio");
loginMsg.set_password("xx123456");
-
观察
login.pb.h
文件,内部有很多序列化的成员函数,比如SerializeToString()
序列化为字符串,SerializeToArray
序列化为数组、SerializeToIstream
序列化到磁盘等等 -
这些函数接口不再我们生成的
login.pb.cc
文件中,而在我们编译生成的google/protobuf/message_lite
头文件中,具体实现在libprotobuf.so
动态库中,因此我们最后编译需要链接这个库
赋值后的login_msg
类就可以通过提供的Serialize*()
接口实现序列化了,比如序列化为字符串:
//序列化
void serializeProtobuf(login_msg* pLoginMsg,std::string& serializedMsg){pLoginMsg->SerializeToString(&serializedMsg);
}
3.1.3 反序列化
序列化后的数据为二进制数据,无法直接读取,可以使用protobuf
提供的反序列化接口来实现结构体成员的读取:
- 同样是在
google/protobuf/message_lite
头文件定义,并且和序列化呼应,提供ParseFromString
、ParseFromArray
等接口来反序列化
- 反序列化后的数据存储在调用成员函数的类内,通过调用成员函数来获取反序列化后的值,比如在
login.pb.cc
的user
字段有user()
成员函数来获取对应的值
3.1.4 测试结果
完整的序列化、非序列化的测试代码如下
#include"protobuf/login.pb.h"
#include<iostream>//序列化
void serializeProtobuf(loginPackage::login_msg* pLoginMsg,std::string& serializedMsg){pLoginMsg->SerializeToString(&serializedMsg);std::cout << "===== serialize =====" << std::endl;std::cout << pLoginMsg->DebugString() << std::endl;
}//反序列化
void deserializeProtobuf(loginPackage::login_msg*pLoginMsg,std::string& serializedMsg){pLoginMsg->ParseFromString(serializedMsg);
}int main(){//初始化login_msg loginMsg;loginMsg.set_user("Antonio");loginMsg.set_password("xx123456"); //序列化std::string loginByteStream;serializeProtobuf(&loginMsg,loginByteStream);//反序列化login_msg parsedLoginMsg;deserializeProtobuf(&parsedLoginMsg,loginByteStream);//获取结果printResult(&parsedLoginMsg);return 0;
}
编译的时候需要添加动态库protobuf
g++ main.cpp protobuf/login.pb.cc -o protobuf-test -lprotobuf
./protobuf-test
测试的结果如下:
3.2 嵌套message
3.2.1 编写.proto
文件
在C++
我们假设有这样的结构体嵌套数据:
struct date_msg{int year;int month;int day;
};struct login_msg{std::string user;std::string password;date_msg date;
};
我们在.proto
文件中可以对照着这样写:
syntax = "proto3";message date_msg{int32 year = 1;int32 month = 2;int32 day = 3;
}message login_msg{bytes user = 1;bytes password = 2;date_msg date = 3;
}
也可以分开写,类似C++
中将不同的结构体分开在不同的.h
头文件,然后包含一样,不过这里的.proto
文件使用的import
关键字
date.proto
文件如下,编写关于日期的一些数据信息
syntax = "proto3";message date_msg{int32 year = 1;int32 month = 2;int32 day = 3;
}
login.proto
文件如下,引用date.proto
文件
syntax = "proto3";import "date.proto";message login_msg{bytes user = 1;bytes password = 2;date_msg date = 3;
}
3.2.2 生成*.pb.h
和*.pb.cc
文件
分两个.proto
文件,使用命令行生成的是对应的两套文件,新增date.pb.h
和date.pb.cc
文件
protoc -I . date.proto login.proto --cpp_out=./protobuf
编译的时候也需要多编译一个文件
g++ main.cpp protobuf/login.pb.cc protobuf/date.pb.cc -o protobuf-test -lprotobuf
3.2.3 序列化数据
这里使用到了另一个接口mutable_*()
,这个函数返回的是对应类型的指针,比如mutable_date()
,返回date_msg
类型,可以支持修改
因此,赋值操作这样写:
loginMsg.mutable_date()->set_year(2025);
loginMsg.mutable_date()->set_month(7);
loginMsg.mutable_date()->set_day(21);
序列化保持不变
//序列化
void serializeProtobuf(login_msg* pLoginMsg,std::string& serializedMsg){pLoginMsg->SerializeToString(&serializedMsg);
}
3.2.4 反序列化数据
由于date()
返回的是一个引用,因此支持链式调用,所以获取date
内部的数据,只需要进行两次调用即可:
std::cout << "year = " << pLoginMsg->date().year() << std::endl;
std::cout << "month = " << pLoginMsg->date().month() << std::endl;
std::cout << "day = " << pLoginMsg->date().day() << std::endl;
3.2.5 测试结果
完整的代码如下:
#include"protobuf/login.pb.h"
#include<iostream>//序列化
void serializeProtobuf(loginPackage::login_msg* pLoginMsg,std::string& serializedMsg){pLoginMsg->SerializeToString(&serializedMsg);std::cout << "===== serialize =====" << std::endl;std::cout << pLoginMsg->DebugString() << std::endl;
}//反序列化
void deserializeProtobuf(loginPackage::login_msg*pLoginMsg,std::string& serializedMsg){pLoginMsg->ParseFromString(serializedMsg);
}void printResult(login_msg*pLoginMsg){std::cout << "===== deserialzie ======" << std::endl;std::cout << "user = " << pLoginMsg->user() << std::endl;std::cout << "password = " << pLoginMsg->password() << std::endl;std::cout << "year = " << pLoginMsg->date().year() << std::endl;std::cout << "month = " << pLoginMsg->date().month() << std::endl;std::cout << "day = " << pLoginMsg->date().day() << std::endl;}int main(){//初始化login_msg loginMsg;loginMsg.set_user("Antonio");loginMsg.set_password("xx123456");loginMsg.mutable_date()->set_year(2025);loginMsg.mutable_date()->set_month(7);loginMsg.mutable_date()->set_day(21);//序列化std::string loginByteStream;serializeProtobuf(&loginMsg,loginByteStream);//反序列化login_msg parsedLoginMsg;deserializeProtobuf(&parsedLoginMsg,loginByteStream);//获取结果printResult(&parsedLoginMsg);return 0;
}
序列化、反序列化测试结果
3.3 package
3.3.1 编写.proto
文件
在protobuf
中,package
的作用类似C++
中的名字空间namespace
,假设我们有如下的C++
代码
namespace datePackage{struct date_msg{int year;int month;int day;};date_msg msg;
}namespace loginPackage{struct date_msg{int year;int month;int day;};std::string user;std::string password;datePackage::date_msg date;date_msg msg date2;
}
对于同名类型,我们可以使用namespace
来区分,在.proto
文件中可以使用package
实现类似功能:
date.proto
文件,以datePackage
区分
syntax = "proto3";package datePackage;message date_msg{int32 year = 1;int32 month = 2;int32 day = 3;
}
login.proto
文件,以loginPackage
区分
syntax = "proto3";import "date.proto";package loginPackage;message date_msg{int32 year = 1;int32 month = 2;int32 day = 3;
}message login_msg{bytes user = 1;bytes password = 2;datePackage.date_msg date = 3;date_msg date2 = 4;
}
3.3.2 序列化数据
序列化数据的时候,赋值数据需要指定命名空间,比如:
- 指定
date
在loginPackage::
命名空间 - 指定
date2
在datePacket::
命名空间
//初始化
loginPackage::login_msg loginMsg;
loginMsg.set_user("Antonio");
loginMsg.set_password("xx123456");datePackage::date_msg* date = loginMsg.mutable_date();
date->set_year(2025);
date->set_month(7);
date->set_day(21);loginPackage::date_msg* date2 = loginMsg.mutable_date2();
date2->set_year(2024);
date2->set_month(6);
date2->set_day(20);
在生成*.pb.cc
中也有实现命名空间
3.3.3 反序列化数据
反序列化数据使用方法不变,直接指定date
和date2
即可
std::cout << "===== date =====" << std::endl;
std::cout << "year = " << pLoginMsg->date().year() << std::endl;
std::cout << "month = " << pLoginMsg->date().month() << std::endl;
std::cout << "day = " << pLoginMsg->date().day() << std::endl;std::cout << "===== date2 =====" << std::endl;
std::cout << "year = " << pLoginMsg->date2().year() << std::endl;
std::cout << "month = " << pLoginMsg->date2().month() << std::endl;
std::cout << "day = " << pLoginMsg->date2().day() << std::endl;
3.3.4 测试结果
完整代码如下
#include"protobuf/login.pb.h"
#include<iostream>//序列化
void serializeProtobuf(loginPackage::login_msg* pLoginMsg,std::string& serializedMsg){pLoginMsg->SerializeToString(&serializedMsg);std::cout << "===== serialize =====" << std::endl;std::cout << pLoginMsg->DebugString() << std::endl;
}//反序列化
void deserializeProtobuf(loginPackage::login_msg*pLoginMsg,std::string& serializedMsg){pLoginMsg->ParseFromString(serializedMsg);
}void printResult(loginPackage::login_msg*pLoginMsg){std::cout << "===== deserialzie ======" << std::endl;std::cout << "user = " << pLoginMsg->user() << std::endl;std::cout << "password = " << pLoginMsg->password() << std::endl;std::cout << "===== date =====" << std::endl;std::cout << "year = " << pLoginMsg->date().year() << std::endl;std::cout << "month = " << pLoginMsg->date().month() << std::endl;std::cout << "day = " << pLoginMsg->date().day() << std::endl;std::cout << "===== date2 =====" << std::endl;std::cout << "year = " << pLoginMsg->date2().year() << std::endl;std::cout << "month = " << pLoginMsg->date2().month() << std::endl;std::cout << "day = " << pLoginMsg->date2().day() << std::endl;}int main(){//初始化loginPackage::login_msg loginMsg;loginMsg.set_user("Antonio");loginMsg.set_password("xx123456");datePackage::date_msg* date = loginMsg.mutable_date();date->set_year(2025);date->set_month(7);date->set_day(21);loginPackage::date_msg* date2 = loginMsg.mutable_date2();date2->set_year(2024);date2->set_month(6);date2->set_day(20);//序列化std::string loginByteStream;serializeProtobuf(&loginMsg,loginByteStream);//反序列化loginPackage::login_msg parsedLoginMsg;deserializeProtobuf(&parsedLoginMsg,loginByteStream);//获取结果printResult(&parsedLoginMsg);return 0;
}
序列化和反序列化输出结果对比
3.4 repeated 字段
3.4.1 编写.proto
文件
repeat
字段可以修饰一个字段可能有多个不同的值,类似一个可变长数组的作用,比如我们需要一个可变长数组vector<std::string>
存储邮箱信息,邮箱可以有一个也可以有多个,在C++
中需要定义这样的数据结构:
namespace datePackage{struct date_msg{int year;int month;int day;};date_msg msg;
}namespace loginPackage{struct date_msg{int year;int month;int day;};std::string user;std::string password;datePackage::date_msg date;date_msg msg date2;std::vector<std::string>emalis;
}
在protobuf
中可以这样定义,使用repeated
修饰bytes
字段,代表emails
可以有多个值
syntax = "proto3";package loginPackage;
import "date.proto";message date_msg{int32 year = 1;int32 month = 2;int32 day = 3;
}message login_msg{bytes user = 1;bytes password = 2;datePackage.date_msg date = 3;date_msg date2 = 4;repeated bytes emalis = 5;
}
3.4.2 序列化数据
- 对于
repeat
关键字修饰的字段,生成的.pb.h
文件中有对应的add_*()
接口,比如我们这里的add_emails()
接口
- 相当于是顺序表,按从小到大的索引插入,支持随机访问,也可以配合
*_size()
接口获取数组大小,遍历所有的结果
因此,可以依次加入我们的邮箱:
loginMsg.add_emalis("123456789@qq.com");
loginMsg.add_emalis("23333@163.com");
3.4.3 反序列化数据
反序列化数据,可以使用下标索引随机访问
std::cout << "===== emailis =====" << std::endl;
std::cout << "emalis(0) = " << pLoginMsg->emalis(0) << std::endl;;
std::cout << "emalis(1) = " << pLoginMsg->emalis(1) << std::endl;;
3.4.4 测试结果
完整代码如下:
#include"protobuf/login.pb.h"
#include<iostream>//序列化
void serializeProtobuf(loginPackage::login_msg* pLoginMsg,std::string& serializedMsg){pLoginMsg->SerializeToString(&serializedMsg);std::cout << "===== serialize =====" << std::endl;std::cout << pLoginMsg->DebugString() << std::endl;
}//反序列化
void deserializeProtobuf(loginPackage::login_msg*pLoginMsg,std::string& serializedMsg){pLoginMsg->ParseFromString(serializedMsg);
}void printResult(loginPackage::login_msg*pLoginMsg){std::cout << "===== deserialzie ======" << std::endl;std::cout << "user = " << pLoginMsg->user() << std::endl;std::cout << "password = " << pLoginMsg->password() << std::endl;std::cout << "===== emailis =====" << std::endl;std::cout << "emalis(0) = " << pLoginMsg->emalis(0) << std::endl;;std::cout << "emalis(1) = " << pLoginMsg->emalis(1) << std::endl;;std::cout << "===== date =====" << std::endl;std::cout << "year = " << pLoginMsg->date().year() << std::endl;std::cout << "month = " << pLoginMsg->date().month() << std::endl;std::cout << "day = " << pLoginMsg->date().day() << std::endl;std::cout << "===== date2 =====" << std::endl;std::cout << "year = " << pLoginMsg->date2().year() << std::endl;std::cout << "month = " << pLoginMsg->date2().month() << std::endl;std::cout << "day = " << pLoginMsg->date2().day() << std::endl;
}int main(){//初始化loginPackage::login_msg loginMsg;loginMsg.set_user("Antonio");loginMsg.set_password("xx123456");loginMsg.add_emalis("123456789@qq.com");loginMsg.add_emalis("23333@163.com");datePackage::date_msg* date = loginMsg.mutable_date();date->set_year(2025);date->set_month(7);date->set_day(21);loginPackage::date_msg* date2 = loginMsg.mutable_date2();date2->set_year(2024);date2->set_month(6);date2->set_day(20);//序列化std::string loginByteStream;serializeProtobuf(&loginMsg,loginByteStream);//反序列化loginPackage::login_msg parsedLoginMsg;deserializeProtobuf(&parsedLoginMsg,loginByteStream);//获取结果printResult(&parsedLoginMsg);return 0;
}
序列化和反序列化测试结果:
3.5 enum 字段
3.5.1 编写.proto
文件
现在,我们需要对login_msg
结构体中添加一个枚举类型,代表性别,在C++
中可以这样写:
namespace datePackage{struct date_msg{int year;int month;int day;};date_msg msg;
}namespace loginPackage{enum Sex{MALE = 0;FEMALE,SECRET};struct date_msg{int year;int month;int day;};std::string user;std::string password;datePackage::date_msg date;date_msg msg date2;
}
在protobuf
中,同样支持enum
字段,但是需要注意的是,在C++
中,enum
字段的第一个值可以是任意值,但是在protobuf
中,第一个字段的值必须是0
syntax = "proto3";package loginPackage;
import "date.proto";enum Sex{MALE = 0;FEMALE = 1;SECRET = 2;
}message date_msg{int32 year = 1;int32 month = 2;int32 day = 3;
}message login_msg{bytes user = 1;bytes password = 2;datePackage.date_msg date = 3;date_msg date2 = 4;repeated bytes emalis = 5;Sex sex = 6;
}
3.5.2 序列化数据
序列化enum
类型和普通类型一致,这里存在命名空间,因此前面添加上命名空间的前缀即可:
loginMsg.set_sex(loginPackage::FEMALE);
在login.pb.h
中生成了这样的枚举类型,后面两个字段是Protobuf 内部实现的一部分,一般不使用:
3.5.3 反序列化
我们在调试的时候,输出调试信息得到的枚举类型是对应的枚举名称,比如这里的FEMALE
:
std::cout << pLoginMsg->DebugString() << std::endl;
实际上,我们如果直接通过反序列化得到的枚举值是对应的枚举数值,比如FEMALE
对应的1
std::cout << "sex = " << pLoginMsg->sex() << std::endl;
3.5.4 运行结果
完整的代码如下:
#include"protobuf/login.pb.h"
#include<iostream>//序列化
void serializeProtobuf(loginPackage::login_msg* pLoginMsg,std::string& serializedMsg){pLoginMsg->SerializeToString(&serializedMsg);std::cout << "===== serialize =====" << std::endl;std::cout << pLoginMsg->DebugString() << std::endl;
}//反序列化
void deserializeProtobuf(loginPackage::login_msg*pLoginMsg,std::string& serializedMsg){pLoginMsg->ParseFromString(serializedMsg);
}void printResult(loginPackage::login_msg*pLoginMsg){std::cout << "===== deserialzie ======" << std::endl;std::cout << "user = " << pLoginMsg->user() << std::endl;std::cout << "password = " << pLoginMsg->password() << std::endl;std::cout << "sex = " << pLoginMsg->sex() << std::endl;std::cout << "===== emailis =====" << std::endl;std::cout << "emalis(0) = " << pLoginMsg->emalis(0) << std::endl;;std::cout << "emalis(1) = " << pLoginMsg->emalis(1) << std::endl;;std::cout << "===== date =====" << std::endl;std::cout << "year = " << pLoginMsg->date().year() << std::endl;std::cout << "month = " << pLoginMsg->date().month() << std::endl;std::cout << "day = " << pLoginMsg->date().day() << std::endl;std::cout << "===== date2 =====" << std::endl;std::cout << "year = " << pLoginMsg->date2().year() << std::endl;std::cout << "month = " << pLoginMsg->date2().month() << std::endl;std::cout << "day = " << pLoginMsg->date2().day() << std::endl;}int main(){//初始化loginPackage::login_msg loginMsg;loginMsg.set_user("Antonio");loginMsg.set_password("xx123456");loginMsg.add_emalis("123456789@qq.com");loginMsg.add_emalis("23333@163.com");loginMsg.set_sex(loginPackage::FEMALE);datePackage::date_msg* date = loginMsg.mutable_date();date->set_year(2025);date->set_month(7);date->set_day(21);loginPackage::date_msg* date2 = loginMsg.mutable_date2();date2->set_year(2024);date2->set_month(6);date2->set_day(20);//序列化std::string loginByteStream;serializeProtobuf(&loginMsg,loginByteStream);//反序列化loginPackage::login_msg parsedLoginMsg;deserializeProtobuf(&parsedLoginMsg,loginByteStream);//获取结果printResult(&parsedLoginMsg);return 0;
}
查看调试信息中的枚举值与反序列化中的枚举值的不同:
更多资料:https://github.com/0voice