ProtoBuf相关教程(C++版本)
一、参考资料
官方文档:https://protobuf.dev/overview/
GitHub:https://github.com/protocolbuffers/protobuf
github上有关Protobuf的C++例子代码:protobuf/examples at main · protocolbuffers/protobuf · GitHub
一文搞懂Java应用ProtoBuf协议-CSDN博客
通信协议protobuf的原理与实现 - 知乎
Protocol Buffers(Protobuf)功能及使用方法_python_脚本之家
protobuf简介及使用流程_java_脚本之家
C++使用Protobuf/基本使用/Protobuf API/优缺点 - 知乎
Proto3 深度解读:Protobuf 的用法与实例分析(C++)-CSDN博客
二、ProtoBuf相关介绍
1. ProtoBuf简介
Protobuf(Protocol Buffers)是由 Google 开发的一种轻量级、高效的数据交换格式,它被用于结构化数据的序列化、反序列化和传输。相比于 XML 和 JSON 等文本格式,Protobuf 具有更小的数据体积、更快的解析速度和更强的可扩展性。
Protobuf 的核心思想是使用协议(Protocol)来定义数据的结构和编码方式。使用 Protobuf,可以先定义数据的结构和各字段的类型、字段等信息,然后使用Protobuf提供的编译器生成对应的代码,用于序列化和反序列化数据。由于 Protobuf 是基于二进制编码的,因此可以在数据传输和存储中实现更高效的数据交换,同时也可以跨语言使用。
Protocol Buffers 是一种二进制序列化格式,具有以下特点:
- 高效:比 XML 和 JSON 更小、更快。
- 跨语言:支持多种编程语言(如 C++, Java, Python, Go 等)。
- 可扩展:支持向后和向前兼容的字段更新。
- 结构化:通过
.proto
文件定义数据结构。
2. 为什么ProtoBuf高效?
高效数据传输的秘密武器Protobuf的使用教程_java_脚本之家
首先,Protobuf 使用二进制编码,会提高性能;其次 Protobuf 在将数据转换成二进制时,会对字段和类型重新编码,减少空间占用。它采用 TLV
格式来存储编码后的数据。详情可以参考官方文档:https://protobuf.dev/programming-guides/encoding/
其次,Protobuf 还会采用一种变长编码的方式来存储数据。这种编码方式能够保证数据占用的空间最小化,从而减少了数据传输和存储的开销。具体来说,Protobuf 会将整数和浮点数等类型变换成一个或多个字节的形式,其中每个字节都包含了一部分数据信息和一部分标识符信息。这种编码方式可以在数据值比较小的情况下,只使用一个字节来存储数据,以此来提高编码效率。
最后,Protobuf 还可以通过采用压缩算法来减少数据传输的大小。比如 GZIP 算法能够将原始数据压缩成更小的二进制格式,从而在网络传输中能够节省带宽和传输时间。Protobuf 还提供了一些可选的压缩算法,如 zlib 和 snappy,这些算法在不同的场景下能够适应不同的压缩需求。
综上所述,Protobuf 在实现高效编码和解码的过程中,采用了多种优化方式,从而在实际应用中能够有效地提升数据传输和处理的效率。
3. proto3 vs. proto2
proto3(proto3 revision of the Protocol Buffers language),是 .proto
⽂件最新的语法版本。proto3 简化了 ProtocolBuffers 语⾔,既易于使⽤,⼜可以在更⼴泛的编程语⾔中使⽤。它允许你使⽤ Java,C++,Python等多种语⾔⽣成 protocol buffer 代码。在 .proto
⽂件中,要使⽤ syntax = “proto3”;
来指定⽂件语法为 proto3,并且必须写在除去注释内容的第⼀⾏。 如果没有指定,编译器会使⽤proto2语法。
proto3 比 proto2 支持更多语言但更简洁。去掉了一些复杂的语法和特性,更强调约定而弱化语法。
4. protoc编译工具
protoc
是 Protobuf 的编译器,用于将 .proto
文件中定义的数据结构编译成目标编程语言的代码(如 Java、C++、Python 等)。
protoc
的主要功能是将 .proto
文件编译成目标语言的代码,生成的代码包括:
- 数据结构的类或结构体定义。
- 序列化和反序列化方法。
- 其他辅助方法(如字段访问、初始化等)。
protoc
的基本命令格式如下:
protoc --<language>_out=<output_dir> <proto_file>
参数解释:
--<language>_out
:指定目标语言和输出目录。例如:--cpp_out
生成 C++ 代码。--java_out
生成 Java 代码。--python_out
生成 Python 代码。
<output_dir>
:生成的代码的输出目录。<proto_file>
:输入的.proto
文件。--include_imports
:包含所有依赖的.proto
文件。--proto_path
:指定.proto
文件的搜索路径。
对于编译生成的 C++ 代码,包含以下内容 :
- 对于每个 message ,都会生成一个对应的消息类。
- 在消息类中,编译器为每个字段提供了get和set方法,以及一些其他能够操作字段的方法。
- 编译器会针对每个
.proto
文件生成xxx.pb.h
和xxx.pb.cc
两个文件,分别用来存放类的声明与类的实现,可将这两个文件放入需要集成的代码中。
5. 序列化与反序列化
C++使用protobuf实现序列化与反序列化-CSDN博客
主流的序列化和反序列化工具有:XML、JSON、ProtoBuf。
序列化:消息对象转换为字节流。
反序列化:字节流转换为消息对象。
什么时候需要序列化和反序列化?
- 网络传输:在网络传输数据时,我们不会将整体数据放在网络中传输,这样不仅传输效率低,而且两个主机之间可能存在兼容性问题,造成对同一个对象解释出不同的结果。因此,在网络传输前,需要将消息对象转换为字节流,也就是序列化,对端主机在接收到字节流数据后,对其进行反序列化,从而恢复原有的消息对象,然后再进行上层业务逻辑处理。
- 数据存储:当我们想把内存中某个消息对象的数据存储在磁盘时,OS实际存储的并不是对象本身,而是将对象序列化后的结果,当用户读取时,OS将磁盘中的数据进行反序列化,然后交付给上层应用。
在消息类的⽗类MessageLite 中,提供了读写消息实例的方法,包括序列化⽅法和反序列化⽅法。
class MessageLite {
public://序列化:bool SerializeToOstream(ostream* output) const;bool SerializeToArray(void *data, int size) const;bool SerializeToString(string* output) const;//反序列化:bool ParseFromIstream(istream* input);bool ParseFromArray(const void* data, int size);bool ParseFromString(const string& data);
};
解释说明:
- 序列化的结果为⼆进制字节序列,⽽⾮⽂本格式。
- 以上三种序列化的⽅法没有本质上的区别,只是序列化后输出的格式不同,可以供不同的应⽤场景使⽤。
- 序列化的 API 函数均为const成员函数,因为序列化不会改变类对象的内容, ⽽是将序列化的结果保存到函数⼊参指定的地址中。
简单示例:
contacts.proto
:
// 声明语法版本
syntax = "proto3";
// 声明代码的命名空间
package contacts;
//结构化对象的描述
message PeopleInfo{// 各个字段描述: 字段类型 字段名 = 字段唯一编号string name = 1;int32 age = 2;
}
test.cpp
:
#include <iostream>
#include "contacts.pb.h"
using namespace std;
int main()
{string people_str;{contacts::PeopleInfo people;people.set_age(20);people.set_name("忘忧");if(!people.SerializeToString(&people_str)){cout << "序列化联系人失败" <<endl;}cout << "序列化之后的 people_str: " << people_str << endl;// 反序列化{contacts::PeopleInfo people;if(!people.ParseFromString(people_str)){cout << "反序列化联系人失败" <<endl;}cout << "Parse age: " << people.age() << endl;cout << "Parse name: " << people.name() << endl;}}return 0;
}
编译执行:
g++ test.cc contacts.pb.cc -o test -std=c++11 -lprotobuf
输出结果:
序列化之后的 people_str:
忘忧
Parse age: 20
Parse name: 忘忧
6. 对比JSON/XML/ProtoBuf
华南理工大学经验分享|大数据传输利器ProtoBuf使用指南
XML、JSON、 ProtoBuf 都具有数据结构化和数据序列化的能力。XML、JSON更注重数据结构化,关注可读性和语义表达能力。ProtoBuf 更注重数据序列化,关注效率、空间、速度,可读性差,语义表达能力不足,为保证极致的效率,会舍弃一部分元信息。
序列化协议 | 通用性 | 格式 | 可读性 | 序列化大小 | 序列化性能 | 适用场景 |
---|---|---|---|---|---|---|
JSON | 通用 | 文本格式 | 好 | 轻量 | 中 | web项目,例如:HTTP网页注册账户。 |
XML | 通用 | 文本格式 | 好 | 重量 | 低 | HTML、RDF/RDFS,强调数据结构化的能力和可读性,例如:UI,游戏信息。 |
ProtoBuf | 独立 | 二进制格式 | 差 | 轻量 | 高 | 适合高性能,对响应速度有要求的数据传输场景,例如:rpc,游戏,即时通讯,tars brpc。 |
数据交互xml、json、protobuf格式比较:
- JSON: 一般的web项目中,最流行的主要还是json。因为浏览器对于json数据支持非常好,有很多内建的函数支持。
- XML: 在webservice中应用最为广泛,但是相比于json,它的数据更加冗余,因为需要成对的闭合标签。json使用了键值对的方式,不仅压缩了一定的数据空间,同时也具有可读性。
- protobuf:是后起之秀,是谷歌开源的一种数据格式,适合高性能,对响应速度有要求的数据传输场景。因为profobuf是二进制数据格式,需要编码和解码。数据本身不具有可读性。因此只能反序列化之后得到真正可读的数据。
相比于 XML 和 JSON,Protobuf 有以下几个优势:
- 更小的数据量:Protobuf 的二进制编码通常比 XML 和 JSON 小 3-10 倍,因此在网络传输和存储数据时可以节省带宽和存储空间。
- 更快的序列化和反序列化速度:由于 Protobuf 使用二进制格式,所以序列化和反序列化速度比 XML 和 JSON 快得多。
- 跨语言:Protobuf 支持多种编程语言,可以使用不同的编程语言来编写客户端和服务端。这种跨语言的特性使得 Protobuf 受到很多开发者的欢迎(JSON 也是如此)。
- 易于维护可扩展:Protobuf 使用 .proto 文件定义数据模型和数据格式,这种文件比 XML 和 JSON 更容易阅读和维护,且可以在不破坏原有协议的基础上,轻松添加或删除字段,实现版本升级和兼容性。
但是,ProtoBuf 也存在以下缺点:
- 学习成本较高,需要掌握其语法规则和使用方法;
- 需要先定义数据结构,然后才能对数据进行序列化和反序列化,增加了一定的开发成本;
- 由于二进制编码,可读性较差,这点不如 JSON 可以直接阅读。
7. protobuf优化级别
Protocol Buffer定义三种优化级别 :SPEED/CODE_SIZE/LITE_RUNTIME
。
SPEED
(默认): 表示⽣成的代码运⾏效率⾼,但是由此⽣成的代码编译后会占⽤更多的空间。CODE_SIZE
: 和SPEED恰恰相反,代码运⾏效率较低,但是由此⽣成的代码编译后会占⽤更少的空 间,通常⽤于资源有限的平台,如Mobile。LITE_RUNTIME
: ⽣成的代码执⾏效率⾼,同时⽣成代码编译后的所占⽤的空间也是⾮常少。
8. protobuf 的数据组织
深入protobuf(Protocol Buffers)原理:简化你的数据序列化-腾讯云开发者社区-腾讯云
⾸先来看⼀个例⼦,假设客户端和服务端使⽤ protobuf 作为数 据交换格式, proto 的具体定义为:
syntax = "proto3";
package pbTest;
message Request {int32 age = 1;
}
Request 中包含了⼀个名称为 age 的字段, 客户端和服务端双⽅都⽤同⼀份相同的 proto ⽂件是没有任 何问题的, 假设客户端⾃⼰将 proto ⽂件做了修改, 修改后的 proto ⽂件如下:
syntax = "proto3";
package pbTest;
message Request {int32 age_test = 1;
}
在这种情形下,服务端不修改应⽤程序仍能够正确地解码,原因在于序列化后的 Protobuf 没有使⽤字段名称,⽽仅仅采⽤了字段编号。与 json xml 等相⽐,Protobuf 不是⼀种完全⾃描述的协议格式,即接收端在没有 proto ⽂件定义的前提下是⽆法解码⼀个 protobuf 消息体。与此相对的,json、xml 等协议格式是完全⾃描述的,拿到了 json 消息体,便可以知道这段消息体中有哪些字段,每个字段的值分别是什么。其实,对于客户端和服务端通信双⽅来说,约定好了消息格式之后完全没有必要在每⼀条消息中都携带字段名称,Protobuf 在通信数据中移除字段名称,这可以⼤⼤降低消息的⻓度,提⾼通信效率。
Protobuf 进⼀步将通信线路上消息类型做了划分, 如下表所示:
Type | Meaning | Used For |
---|---|---|
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length- delimited (⻓度分割) | string, bytes, embedded messages, packed repeated fields |
3 | Start group | groups (deprecated) |
4 | End group | groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
对于 int32, int64, uint32 等数据类型在序列化之后都会转为
Varints 编码。 Protobuf 除了存储字段的值之外, 还存储了字段的编号以及字段在通信线路上的格式类型(wire- type),具体的存储⽅式为:
field_num << 3 | wire type
即将字段标号逻辑左移 3 位, 然后与该字段的 wire type 的编号按位或。接收端可以利⽤这些信息,结合 proto ⽂件来解码消息结构体。
上例子中,假设 age 为 5,由于 age 在 proto ⽂件中定义 的是 int32 类型, 因此序列化之后它的 wire type
为 0,其字段编号为 1,因此按照上⾯的计算⽅式, 即 1 << 3 | 0
, 所以其类型和字段编号的信息只占 1 个字节, 即 00001000
, 后⾯跟上字段值 5 的 Varints 编码, 所以整个结构体序列化之后为:
00001000 00000101
有了字段编号和 wire type,其后所跟的数据的⻓度便是确定的,因此 Protobuf 是⼀种⾮常紧密的数 据组织格式,其不需要特别地加⼊额外的分隔符来分割⼀个消息字段,这可⼤⼤提升通信的效率, 规避 冗余的数据传输。
三、安装protobuf
1. apt安装protobuf
使用包管理器安装:
sudo apt-get install protobuf-compiler ibprotobuf-dev
2. x86编译安装protobuf
2.1 下载源码
Releases · protocolbuffers/protobuf
下载并解压源码:
wget https://github.com/protocolbuffers/protobuf/releases/download/v21.6/protobuf-cpp-3.21.6.tar.gztar -zxvf protobuf-cpp-3.21.6.tar.gz
2.2 生成configure配置
./configure --prefix=/path/to/protobuf-cpp-3.21.6/x86_install
2.3 编译安装
make -j8
make install
2.4 验证是否安装
查看protoc版本:
protoc --version
3. 交叉编译安装protobuf
详细步骤,请参考另一篇博客:
四、解析ProtoBuf文件
1. 创建.proto
文件
userInfo.proto
syntax = "proto3";// 指定命名空间
package UserInfoFactory;// 表示生成C++序列化器的类名
message UserInfo {required int64 id = 1;string name = 2;optional EnumSex sex = 3;
}enum EnumSex {SEX_MAN = 0; // 男SEX_WOMAN = 1; // 女
}
解释说明:
syntax
,指定使用的 Protobuf 语法版本(proto2
或proto3
)。package
,是⼀个可选的声明符,能表示.proto
⽂件的命名空间,在项⽬中要有唯⼀性。它的作⽤是为了避免我们定义的消息出现冲突,类似C++中的namespace。message
:定义消息类型(数据结构),类似于类或结构体,message
里面定义所有包含的属性。
创建 .proto
文件时需要注意:
- ⽂件命名应该使⽤全⼩写字⺟命名,多个字⺟之间⽤
_
连接。 例如:lower_snake_case.proto
。 - 编辑
.proto
⽂件代码时,应使⽤ 2 个空格的缩进。
2. 消息类型(message
)
消息(message): 要定义的结构化对象,我们可以给这个结构化对象中定义其对应的属性内容。在网络传输中,我们需要为传输双⽅定制协议。定制协议说⽩了就是定义结构体或者结构化数据,⽐如,tcp,udp 报⽂就是结构化的。再⽐如将数据持久化存储到数据库时,会将⼀系列元数据统⼀⽤对象组织起来,再进⾏存储。ProtoBuf 就是以 message 的方式来⽀持我们定制协议字段,后期帮助我们形成类和⽅法来使用。
消息类型的格式:
// 消息类型命名规范:使⽤驼峰命名法,⾸字⺟⼤写。
message 消息类型名称{}
2.1 消息字段
在 message 中定义其属性字段,定义字段格式为:
# 属性格式
[字段规则] 字段类型 字段名称 = 字段编号
字段名称命名规范:全⼩写字⺟,多个字⺟之间⽤ _
连接。
2.2 字段规则
字段规格类似于MySQL中的约束,即对数据的约束条件。
ProtoBuf 常见的约束有以下几种:
optional
(recommended):表示可选字段,可以不赋值。repeated
:表示该字段可以出现任意多次(包括0
次),可以理解为定义一个数组。
2.3 字段类型
字段类型包括常用的标量类型和复合类型。
标量类型
标量类型类似于Java中的基本类型,对应关系如下表所示:
Proto Type | C++ Type |
---|---|
double | double |
float | float |
int32 | int32_t |
int64 | int64_t |
uint32 | uint32_t |
uint64 | uint64_t |
sint32 | int32_t |
sint64 | int64_t |
fixed32 | uint32_t |
fixed64 | uint64_t |
sfixed32 | int32_t |
sfixed64 | int64_t |
bool | bool |
string | std::string |
bytes | std::string |
复合类型
复合类型可以是其它message
消息,或enum
枚举等。
message UserInfo {// 字段类型为UserDetail消息UserDetail detail = 1;// 字段类型为EnumSex枚举EnumSex sex = 2;
}message UserDetail {int64 id = 1;string name = 2;
}enum EnumSex {SEX_MAN = 0; // 男(枚举编号默认从0开始)SEX_WOMAN = 1; // 女
}
2.4 字段编号
字段编号即字段唯一标识符,用于二进制编码,从 1
开始编号,⼀旦开始使⽤就不能够再改变。
字段后面的 =1,=2
是作为序列化后的二进制编码中的字段的对应标签,因为 Protobuf 消息在序列化后不包含字段名称,只有对应的字段编号,所以节省了空间。值得⼀提的是,范围为 1 ~ 15 的字段编号需要⼀个字节进⾏编码, 16 ~ 2047 内的数字需要两个字节进⾏编码。所以 1 ~ 15 要⽤来标记出现⾮常频繁的字段,要为将来有可能添加的、频繁出现的字段预留⼀些出来。且一旦定义,不要随意更改,否则可能会对不上序列化信息。
3. 嵌套类型
可以在一个消息中嵌套定义其他消息。
message AddressBook {message Person {string name = 1;int32 id = 2;string email = 3;}repeated Person people = 1;
}
4. 枚举类型(enum
)
enum PhoneType {MOBILE = 0;HOME = 1;WORK = 2;
}message PhoneNumber {string number = 1;PhoneType type = 2;
}
注意枚举类型的定义有以下几种规则:
- 枚举的 0 值常量必须存在,且要作为第一个元素。这是为了与 proto2 的语义兼容:第一个元素作为默认值,且值为 0。
- 枚举类型可以在消息外定义,也可以在消息体内定义(嵌套)。
- 枚举的常量值在 32 位整数的范围内。但因负值无效因而不建议使用(与编码规则有关)。
定义 enum
枚举需要注意:
- 同级(同层)的枚举类型,各个枚举类型中的常量不能重名。
- 单个 .proto 文件下,最外层枚举类型和嵌套枚举类型,不算同级。
- 多个 .proto 文件下,若一个文件引入了其他文件,且每个文件都未声明 package,每个 proto 文件中的枚举类型都在最外层,算同级。
- 多个 .proto 文件下,若一个文件引入了其他文件,且每个文件都声明了 package,不算同级。
5. any类型
Any 类型可以理解为泛型类型。使用时可以在 Any 中存储任意消息类型。Any 类型的字段也用 repeated 来修饰,Any 类型是 google 已经帮我们定义好的类型,在安装 ProtoBuf 时,其中的 include 目录下查找所有 google 已经定义好的 .proto
文件,在此目录下查找:/usr/local/protobuf/include/google/protobuf/
。
在 Any 字段中存储任意消息类型,这就要涉及到任意消息类型 和 Any 类型的互转。这部分代码就在 Google为我们写好的头文件 any.pb.h
中。使用方法:
- 使用
PackFrom()
方法可以将任意消息类型转为 Any 类型。 - 使用
UnpackTo()
方法可以将 Any 类型转回之前设置的任意消息类型。 - 使用
Is()
方法可以用来判断存放的消息类型是否为typename T
。
简单示例:
Address address;
cout << "请输入联系人家庭地址: ";
string home_address;
getline(cin, home_address);
address.set_home_address(home_address);
cout << "请输入联系人单位地址: ";
string unit_address;
getline(cin, unit_address);
address.set_unit_address(unit_address);
google::protobuf::Any * data = people_info_ptr->mutable_data();
data->PackFrom(address);if (people.has_data() && people.data().Is<Address>())
{Address address;people.data().UnpackTo(&address);if (!address.home_address().empty()) {cout << "家庭地址:" << address.home_address() << endl;}if (!address.unit_address().empty()) {cout << "单位地址:" << address.unit_address() << endl;}
}
五、搭建基于ProtoBuf的TCP服务
使用protobuf c实现TCP网络数据传输 - 夜空中最亮de星 - 博客园
基于ProtoBuf-3 实现的C++(客户端)与Python(服务端)通信 - 知乎
为了提高通信效率,可以采用 protobuf 替代 XML 和 Json 数据交互格式,protobuf 相对来说数据量小,在进程间通信或者设备之间通信能够提高通信速率。
1. 引言
客户端与服务器交互时都需要双方协商,确定消息的二进制格式。客户端在向服务器发起请求时会根据协议创建二进制数据块,然后依托tcp, udp, http等协议将二进制内容传递给服务器,后者根据协议的规则按照特定次序从接收到的二进制内存块中读取给定字段。
这种做法存在一些问题。一是自定义的协议往往缺乏好的可扩展性,例如以后需要添加新字段,特别是字段要插入到以前字段的中间时,客户端和服务端协议解析代码得做相同修改。随着业务的发展,原先某些字段得删除时,协议解析代码又得修改,因此自定义协议解析在面对协议变化上因为需要重新编码因此会提升工作量降低效率,特别时代码的修改非常容易引入错误。
目前业内也有一些通用协议格式,例如json, xml等,他们也存在一些问题。各种编程语言都有既定接口或模块之间解析这些格式,但是存在一个问题就是效率低下。当协议中的字段增多时,这些格式的解析耗时较长,我个人觉得这些格式存在一个不好使之处在于他们在发送二进制数据上。当协议字段对应字符串或是int这类长度较短的二进制数据时,他们的使用很方便,但如果使用他们传递图片内容能长度较长的二进制数据,那么我们需要进行base64编码后才方便将数据存储在这些格式中。
因此,我们最好能找到一种可扩展性强,也就是协议格式能灵活的应对字段的删减而不必引入过多的代码修改;同时字段的查询效率高,二进制数据发送接收也方便的协议格式,那么就能大大提升我们制定网络协议的效率。
Protocol Buffers(ProtoBuf)是一种语言无关、平台无关的序列化结构数据格式,非常适合用于网络通信。
2. 总体步骤
- 定义ProtoBuf消息:
使用ProtoBuf的.proto
文件定义消息格式。 - 生成C++代码:
使用protoc
工具,从.proto
文件生成C++代码。 - 实现TCP通信:
在C++中实现TCP服务端和客户端,使用 ProtoBuf 序列化和反序列化消息。
3. 创建.proto
文件
创建 commands.proto
文件,定义ProtoBuf消息格式:
syntax = "proto3";package commands;// 定义请求消息
message Request {enum Command {TAKE_PHOTO = 0;RECORD_VIDEO = 1;PREVIEW = 2;UPDATE_FIRMWARE = 3;}Command command = 1;bytes data = 2; // 用于固件更新等需要额外数据的命令
}// 定义响应消息
message Response {bool success = 1;string message = 2;
}
4. 生成C++代码
使用 protoc
工具生成C++代码:
protoc --cpp_out=. commands.proto
这将生成 commands.pb.h
和 commands.pb.cc
文件,包含ProtoBuf消息的C++定义和序列化/反序列化代码。
5. 实现TCP通信
以下是基于ProtoBuf的TCP服务端和客户端C++代码示例。
服务端代码 server.cpp
:
#include <iostream>
#include <thread>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "commands.pb.h"using namespace std;
using namespace commands;void handle_client(int clientfd) {while (true) {// 接收数据char buffer[1024];memset(buffer, 0, sizeof(buffer));int bytes_received = recv(clientfd, buffer, sizeof(buffer), 0);if (bytes_received <= 0) {if (bytes_received < 0) {perror("recv failed");} else {cout << "Client disconnected" << endl;}break;}// 解析ProtoBuf消息Request request;if (!request.ParseFromArray(buffer, bytes_received)) {cerr << "Failed to parse request" << endl;continue;}cout << "Received command: " << request.command() << endl;// 处理命令Response response;response.set_success(true);response.set_message("Command executed successfully");switch (request.command()) {case Request::TAKE_PHOTO:cout << "Command: Take Photo" << endl;break;case Request::RECORD_VIDEO:cout << "Command: Record Video" << endl;break;case Request::PREVIEW:cout << "Command: Preview" << endl;break;case Request::UPDATE_FIRMWARE:cout << "Command: Update Firmware" << endl;// 处理固件更新数据break;default:response.set_success(false);response.set_message("Unknown command");break;}// 发送响应string response_data;response.SerializeToString(&response_data);send(clientfd, response_data.data(), response_data.size(), 0);}close(clientfd);
}int main() {int serverfd = socket(AF_INET, SOCK_STREAM, 0);if (serverfd < 0) {perror("socket creation failed");return -1;}struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8081);server_addr.sin_addr.s_addr = INADDR_ANY;if (bind(serverfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("bind failed");close(serverfd);return -1;}if (listen(serverfd, 5) < 0) {perror("listen failed");close(serverfd);return -1;}cout << "Server listening on port 8081" << endl;while (true) {struct sockaddr_in client_addr;socklen_t client_len = sizeof(client_addr);int clientfd = accept(serverfd, (struct sockaddr *)&client_addr, &client_len);if (clientfd < 0) {perror("accept failed");continue;}cout << "Client connected, fd = " << clientfd << endl;thread client_thread(handle_client, clientfd);client_thread.detach();}close(serverfd);return 0;
}
客户端代码 client.cpp
:
#include <iostream>
#include <cstring>
#include <unistd.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include "commands.pb.h"using namespace std;
using namespace commands;int main() {struct sockaddr_in server_addr;memset(&server_addr, 0, sizeof(server_addr));server_addr.sin_family = AF_INET;server_addr.sin_port = htons(8081);server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");int sockfd = socket(AF_INET, SOCK_STREAM, 0);if (sockfd < 0) {perror("socket creation failed");return -1;}if (connect(sockfd, (struct sockaddr *)&server_addr, sizeof(server_addr)) < 0) {perror("connect failed");close(sockfd);return -1;}cout << "Connected to server" << endl;// 创建请求Request request;request.set_command(Request::TAKE_PHOTO);// 序列化请求string request_data;request.SerializeToString(&request_data);// 发送请求send(sockfd, request_data.data(), request_data.size(), 0);// 接收响应char buffer[1024];memset(buffer, 0, sizeof(buffer));int bytes_received = recv(sockfd, buffer, sizeof(buffer), 0);if (bytes_received > 0) {Response response;if (response.ParseFromArray(buffer, bytes_received)) {cout << "Response: " << response.message() << endl;} else {cerr << "Failed to parse response" << endl;}}close(sockfd);return 0;
}
6. 编译和运行
使用以下指令编译服务端和客户端代码:
g++ -std=c++11 -o server server.cpp commands.pb.cc -lprotobuf -lpthreadg++ -std=c++11 -o client client.cpp commands.pb.cc -lprotobuf -lpthread
启动服务端:
./server
启动客户端:
./client
六、相关经验
ProtoBuf+RPC
探索C++中的轻量级RPC:打造高性能网络通信-51CTO.COM
使用gRPC基于Protobuf传输大文件或数据流-腾讯云开发者社区-腾讯云
c++ grpc 实现一个传图服务(同步方式,流式传输)-CSDN博客
ProtoBuf+图传
python 将图片转为protobuff发送前端_mob64ca12e86bd4的技术博客_51CTO博客
使用protobuf实现任意文件的传输.proto-CSDN博客
七、FAQ
Q:terminate called after throwing an instance of 'std::system_error'
yoyo@yoyo:/media/sda3/ProjectsC++/test_opencv-x86$ ./client
terminate called after throwing an instance of 'std::system_error'what(): Unknown error -1
Aborted (core dumped)
错误原因:编译时没有加 -lpthread
。
解决方法:
g++ -std=c++11 -o client client.cpp commands.pb.cc -lprotobuf -lpthread