前言
Protocol Buffers(简称Protobuf)是Google开源的一种高效、跨语言、可扩展的结构化数据序列化格式,广泛应用于微服务通信、数据存储、RPC框架等场景。相比于JSON、XML,Protobuf具有体积更小、解析更快、兼容性更强的优势。本文将从环境配置、语法细节、实战项目到高级特性,全方位、超详细记录Protobuf的学习过程,结合3个递进式通讯录项目,帮助读者从零基础到熟练运用Protobuf。
一、踩坑记录:环境配置常见问题
在 Windows 和 Linux 环境中使用 Protobuf 时,难免遇到编译报错、环境变量失效等问题,这里整理了 2 个高频问题的解决方案:
问题 1:Windows 编译.proto 文件生成的.pb.h/.pb.cc 找不到头文件
核心原因
protoc编译工具能正常生成代码,但 C++ 编译器(MSVC/MinGW/Clang)找不到google/protobuf/message.h等头文件,本质是未配置编译器的 include 路径。
解决方案
- 找到 Protobuf 安装目录下的
include文件夹(如D:\protobuf\include) - 在编译器配置中添加 include 路径:
- MSVC:项目属性 → C/C++ → 常规 → 附加包含目录
- MinGW/Clang:编译命令中添加
-I D:\protobuf\include
问题 2:Linux 修改 /etc/profile 后环境变量不自动生效
核心原因
-
/etc/profile仅在登录shell(login shell)启动时自动加载(如通过SSH登录、Ctrl+Alt+F1进入终端) -
图形界面终端(如GNOME Terminal、Konsole)默认启动的是非登录shell ,不会自动读取
/etc/profile
解决方案
重新远程连接远端Linux机器。
二、Protobuf核心语法(Proto3)详解
2.1 基础语法结构
2.1.1 语法版本声明
syntax = "proto3"; // 必须放在文件首行
-
作用:明确指定使用Proto3语法
-
注意:若不声明,编译器默认使用Proto2,可能导致语法兼容问题
2.1.2 包声明(Package)
package contacts; // 命名空间
-
作用:避免不同
.proto文件中message名称冲突 -
类比:C++的
namespace、Java的package -
生成代码影响:
-
C++:生成的类会放在
contacts命名空间中 -
Java:生成的类会放在
contacts包下(可通过option java_package自定义)
-
2.1.3 Message定义(核心)
message是Protobuf中定义结构化数据的核心单元,类似C/C++的struct或类。
message PeopleInfo {
string name = 1; // 字符串类型:姓名,字段编号1
int32 age = 2; // 32位整数:年龄,字段编号2
}
message PeopleInfo { string name = 1; // 字符串类型:姓名,字段编号1 int32 age = 2; // 32位整数:年龄,字段编号2 }
字段编号(Field Numbers)
-
每个字段后的数字(
=1、=2)是字段编号(field tag),用于序列化后标识字段 -
关键规则:
-
必须为正整数(≥1)
-
同一
message中必须唯一 -
保留范围:19000--19999 是Protobuf内部保留编号,禁止使用
-
有效范围:1 ~ 2²⁹-1(536,870,911)
-
-
为什么需要字段编号?
序列化后的二进制数据不包含字段名 ,仅通过字段编号识别字段,解析时依靠编号还原数据。因此,编号一旦分配不可随意修改(否则会导致旧数据无法解析)。
-
优化建议:
-
1--15:编码仅占1字节,适合高频使用或必填字段(如
name、id) -
16--2047:编码占2字节,适合低频字段或扩展字段
-
2.1.4 字段类型与默认值(Proto3特性)
Proto3中所有字段默认都是optional(可选),且自动具有固定默认值(不可自定义):
|--------------|---------------|----------|
| 字段类型 | 默认值 | 说明 |
| int32/int64 | 0 | 整数类型默认值 |
| float/double | 0.0 | 浮点数类型默认值 |
| bool | false | 布尔类型默认值 |
| string | ""(空字符串) | 字符串类型默认值 |
| bytes | 空字节串 | 字节类型默认值 |
| enum | 第一个值(必须为0) | 枚举类型默认值 |
| message | 空对象(所有字段为默认值) | 嵌套消息默认值 |
- 注意:无法区分"字段未设置"和"字段值为默认值"(如
age=0可能是未设置,也可能是实际年龄为0)。若需明确判断,可使用google.protobuf.StringValue等包装类型(Proto3.15+支持optional关键字开启presence tracking)。
2.2 高级语法特性
2.2.1 重复字段(repeated)
表示字段可包含多个值,类似数组或std::vector:
message PeopleInfo {
string name = 1;
int32 age = 2;
repeated string emails = 3; // 多个邮箱
}
message PeopleInfo { string name = 1; int32 age = 2; repeated string emails = 3; // 多个邮箱 }
- 生成C++代码后,会自动生成
add_emails()(添加元素)、emails_size()(获取长度)、emails(i)(获取第i个元素)等方法。
2.2.2 枚举类型(enum)
用于定义一组命名的整数常量,适用于字段值有限的场景:
message PeopleInfo {
string name = 1;
int32 age = 2;
// 枚举类型:电话类型
enum PhoneType {
UNSPECIFIED = 0; // 第一个值必须为0
MP = 1; // 移动电话
TEL = 2; // 固定电话
}
PhoneType phone_type = 3;
}
-
注意:解析到未定义的枚举值时,Proto3会保留该数值,但不会映射到枚举名。
-
C++使用示例:
PeopleInfo person;
person.set_phone_type(PeopleInfo::MP);
if (person.phone_type() == PeopleInfo::MP) {
cout << "移动电话" << endl;
}
2.2.3 嵌套消息(Nested Message)
message内部可定义另一个message,适用于复杂数据结构:
message PeopleInfo {
string name = 1;
int32 age = 2;
// 嵌套消息:电话信息
message Phone {
string number = 1; // 电话号码
enum PhoneType {
MP = 0;
TEL = 1;
}
PhoneType type = 2; // 电话类型
}
repeated Phone phones = 3; // 多个电话
}
- 生成C++代码后,嵌套消息会被命名为
PeopleInfo_Phone,枚举类型为PeopleInfo_Phone_PhoneType。
2.2.4 Any类型
google.protobuf.Any可存储任意类型的消息(类似void*但更安全),需导入google/protobuf/any.proto:
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1; // 错误描述
repeated google.protobuf.Any details = 2; // 任意类型的错误详情
}
-
C++使用示例:
#include <google/protobuf/any.pb.h>
// 创建具体错误信息
NetworkError net_err;
net_err.set_code(404);
net_err.set_url("http://example.com");// 打包到Any
google::protobuf::Any any;
any.PackFrom(net_err);// 放入ErrorStatus
ErrorStatus status;
status.set_message("请求失败");
*status.add_details() = any;// 解包
const auto& detail = status.details(0);
if (detail.Is<NetworkError>()) {
NetworkError unpacked;
detail.UnpackTo(&unpacked);
cout << "错误码:" << unpacked.code() << endl;
}
2.2.5 oneof类型
表示多个字段中最多只能设置一个(互斥字段),节省存储空间:
message SampleMessage {
oneof test_oneof {
string name = 1;
int32 id = 2;
bool active = 3;
}
}
-
C++使用示例:
SampleMessage msg;
msg.set_name("Bob"); // 设置name
// msg.set_id(100); // 会自动清除name
if (msg.has_name()) {
cout << "Name: " << msg.name() << endl;
}
2.2.6 map类型
用于表示键值对集合,类似std::map:
message CountryMap {
map<string, string> country_code_to_name = 1; // 国家代码→国家名称
}
-
规则:
-
键类型不能是浮点数、bytes或message
-
值类型可以是除map外的任意类型
-
-
C++使用示例:
CountryMap cmap;
(*cmap.mutable_country_code_to_name())["CN"] = "中国";
(*cmap.mutable_country_code_to_name())["US"] = "美国";// 遍历
for (const auto& kv : cmap.country_code_to_name()) {
cout << kv.first << " → " << kv.second << endl;
}
2.3 兼容性更新规则
设计.proto文件时,需保证后续更新的兼容性(旧程序能解析新数据,新程序能解析旧数据),核心规则如下:
-
不要修改已有字段的编号(tag number)
-
不要重复使用已删除字段的编号
-
新增字段必须是optional或repeated(Proto3中所有字段默认满足)
-
删除字段时,需用
reserved声明保留其编号和名称,避免未来误用
reserved关键字(关键!)
用于保留已删除字段的编号和名称,防止团队协作中误用导致兼容性问题:
// 原始版本
message Person {
string name = 1;
int32 age = 2; // 后续要删除的字段
string email = 3;
}
// 更新后版本
message Person {
reserved 2; // 保留已删除字段的编号
reserved "age"; // 保留已删除字段的名称
string name = 1;
string email = 3;
string phone = 4; // 新增字段
}
-
规则:
-
可保留连续范围(如
reserved 9 to 11表示9、10、11) -
编号和名称的
reserved必须分开写(不能混在同一行) -
保留的编号/名称不能再用于定义新字段
-
-
编译检查:若尝试使用保留的编号或名称,
protoc会直接报错:Field number 2 has already been used for 'reserved'
2.4 option选项
option用于配置生成代码的行为或添加元信息,常见选项如下:
syntax = "proto3";
// 文件级选项:指定Java包名
option java_package = "com.example.contacts";
// 文件级选项:优化方式(SPEED/CODE_SIZE/LITE_RUNTIME)
option optimize_for = SPEED;
message PeopleInfo {
// 字段选项:标记字段已废弃
string name = 1 [deprecated = true];
// 字段选项:自定义JSON字段名
int32 id = 2 [json_name = "user_id"];
}
// 消息选项:标记整个message已废弃
message OldMessage {
option deprecated = true;
string data = 1;
}
-
C++相关常用选项:
-
option cc_enable_arenas = true;:启用Arena内存分配(提升高频创建/销毁消息的性能) -
option optimize_for = CODE_SIZE;:减小生成代码体积(牺牲部分速度)
-
三、Protobuf编译命令详解
protoc是Protobuf的命令行编译工具,用于将.proto文件编译为指定语言的源代码(C++、Python、Java等)。
3.1 基本语法
protoc --cpp_out=DST_DIR path/to/file.proto
3.2 参数说明
|----------------------|----|--------------------------------------------|
| 参数 | 全称 | 说明 |
| --cpp_out=DST_DIR | - | 指定生成C++代码的输出目录,生成xxx.pb.h和xxx.pb.cc文件。 |
| path/to/file.proto | - | 要编译的.proto文件路径(相对路径或绝对路径)。 |
3.3 示例
示例1:编译当前目录的.proto文件
# 生成C++代码到当前目录
protoc --cpp_out=. contacts.proto
示例2:引用其他目录的.proto文件
假设contacts.proto引用了./protos/common.proto,编译命令如下:
# -I 指定搜索路径为./protos,--cpp_out指定输出目录为./src
protoc -I ./protos --cpp_out=./src contacts.proto
示例3:批量编译多个.proto文件
# 编译./protos目录下所有.proto文件,生成C++代码到./cpp_src
protoc -I ./protos --cpp_out=./cpp_src ./protos/*.proto
四、实战项目:3个版本通讯录深度解析
4.1 1.0版本:基础入门------序列化与反序列化
4.1.1 项目目标
-
掌握Protobuf基本语法
-
实现单个联系人信息的序列化(对象→二进制)与反序列化(二进制→对象)
-
打印序列化结果和反序列化后的联系人信息
4.1.2 核心需求
-
联系人包含:姓名(name)、年龄(age)
-
序列化:将联系人对象转换为二进制字节序列并打印
-
反序列化:将二进制字节序列转换为联系人对象并打印
4.1.3 实现步骤
步骤1:编写.proto文件(contacts.proto)
syntax = "proto3";
package contacts;
// 联系人信息
message PeopleInfo {
string name = 1; // 姓名
int32 age = 2; // 年龄
}
步骤2:编译.proto文件
protoc --cpp_out=. contacts.proto
生成contacts.pb.h和contacts.pb.cc文件。
步骤3:编写C++代码([main.cc](main.cc))
#include<iostream>
#include"contacts.pb.h"
int main()
{
std::string person_str;
//用匿名的命名空间隔开
{
//对一个联系人的信息使用PB进行序列化,并将结果打印出来
contacts::PeopleInfo person;
person.set_name("张三");
person.set_age(20);
if(!person.SerializeToString(&person_str))
{
std::cout<<"序列化失败"<<std::endl;
return -1;
}
std::cout<<"序列化成功,结果"<<person_str<<std::endl;
}
{
//对序列化后的内容使用PB进行反序列化,解析出联系人信息并且打印出来
contacts::PeopleInfo person;
if(!person.ParseFromString(person_str))
{
std::cout<<"反序列化失败"<<std::endl;
return -1;
}
std::cout<<"反序列化成功,结果:"<<person.name()<<" "<<person.age()<<std::endl;
}
return 0;
}
步骤4:编译运行
g++ main.cc contacts.pb.cc -o contacts -std=c++11 -lprotobuf
./contacts
(二进制结果不可读,可能会错位,不显示等等)

4.2 2.0版本:进阶实战------文件读写+语法扩展
4.2.1 项目目标
-
深入掌握Proto3高级语法(嵌套消息、枚举、repeated)
-
实现序列化数据写入文件、从文件读取解析
-
编写Makefile简化编译流程
4.2.2 核心需求
-
联系人新增属性:电话信息(号码+类型)
-
序列化:将多个联系人信息写入文件
contacts.bin -
反序列化:从文件读取数据,解析并打印所有联系人信息
4.2.3 实现步骤
步骤1:升级.proto文件(contacts2.proto)
syntax = "proto3";
package contacts2;
/*
singular: 该字段最多出现一次(0 次或 1 次)。这是 Protobuf 默认的字段类型(在 proto3 中无需显式声明)。
repeated: 该字段可以出现零次、一次或多次,相当于一个列表/数组(list/array)。顺序会被保留。在生成的代码中,通常表现为一个可变长度的容器(如 C++ 的 std::vector,Python 的 list 等)。
*/
message PeopleInfo{
string name = 1;
int32 age = 2;
message Phone{
string number = 1;
enum PhoneType{
MP = 0;
TEL = 1;
}
PhoneType type = 2;
}
//一个联系人里面会有多个电话信息 -- repeated相当于C/C++中的数组
repeated Phone phone = 3; //导入了phone的包之后,因为那里有phone的命名空间,所以这里要加上phone.Phone
}
//通讯录message
message Contacts{
repeated PeopleInfo contacrs=1;
}
步骤2:编写write.cc(序列化写入文件)
#include<iostream>
#include<fstream>
#include"contacts.pb.h"
using namespace std;
void AddPeopleInfo(contacts2::PeopleInfo* people)
{
cout<<" ------ 新增联系人 ------"<<endl;
cout<<" 请输入联系人姓名:";
string name;
getline(cin, name);
people->set_name(name);
cout<<" 请输入联系人年龄:";
int age;
cin>>age;
people->set_age(age);
cin.ignore(256,'\n'); //清除输入缓冲区,直到\n, \n也会清除
for(int i=1;;i++)
{
cout<<"请输入联系人" << i <<"的手机号码(输入回车就完成电话新增): ";
string number;
getline(cin, number); //getline会读取一行内容,直到\n,并且不包括\n
if(number.empty()) break;
contacts2::PeopleInfo_Phone* phone = people->add_phone();
phone->set_number(number);
}
cout<<" ------ 添加联系人成功 ------"<<endl;
}
int main()
{
contacts2::Contacts contacts;
//读取本地已存在的通讯录文件
fstream input("contacts.bin", ios::in | ios::binary);
if(!input)
{
cerr << "contacts.bin not find,must create new file" << endl;
} else if(!contacts.ParseFromIstream(&input)) //将input中的数据反序列化解析到contacts中
{
cerr << "parse error!" << endl;
input.close();
return -1;
}
//向通讯录中添加一个联系人 --- 对于repeated字段,需要使用add_xxx()方法,来添加元素
AddPeopleInfo(contacts.add_contacrs());
//将通讯录写入本地文件中
fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
if(!contacts.SerializeToOstream(&output))
{
cerr << "write error!" << endl;
input.close();
output.close();
return -1;
}
cout<<" write success! " << endl;
input.close();
output.close();
return 0;
}
步骤3:编写read.cc(从文件反序列化)
#include<iostream>
#include<fstream>
#include"contacts.pb.h"
using namespace std;
void PrintContacts(contacts2::Contacts& contacts)
{
for(int i = 0; i < contacts.contacrs_size(); i++)
{
cout << "----------- 联系人" << i+1 << " -----------" << endl;
//获取到联系人信息
const contacts2::PeopleInfo& info = contacts.contacrs(i); // .xxx(i),获取列表中的第i个元素
//打印联系人信息
cout << "联系人姓名:" << info.name() << endl;
cout << "联系人年龄:" << info.age() << endl;
for(int j = 0; j < info.phone_size(); j++) // xxx_size() 获取列表的长度
{
const contacts2::PeopleInfo_Phone& phone = info.phone(j);
cout << "联系人电话"<< j+1 <<":" << phone.number() << endl;
}
cout<<endl;
}
}
int main()
{
contacts2::Contacts contacts;
//需要读取文件中的二进制内容 并且 反序列化出来
fstream input("contacts.bin", ios::in | ios::binary);
if(!input)
{
cerr << "contacts.bin not find,must create new file" << endl;
} else if(!contacts.ParseFromIstream(&input)) //将input中的数据反序列化解析到contacts中
{
cerr << "parse error!" << endl;
input.close();
return -1;
}
//打印通讯录列表
PrintContacts(contacts);
}
步骤4:编写Makefile
all:write read
write:write.cc contacts.pb.cc
g++ -o $@ $^ -std=c++11 -lprotobuf
read:read.cc contacts.pb.cc
g++ -o $@ $^ -std=c++11 -lprotobuf
.PHONY: clean
clean:
rm -f write read
步骤5:编译运行
# 编译
make
# 运行write写入文件
./write
# 运行read读取文件并打印
./read
步骤6:快速解码技巧(无需编写read.cc)
若仅需临时查看二进制文件内容,可直接使用protoc的--decode参数:
protoc --decode=contacts2.Contacts contacts2.proto < contacts.bin
4.3 3.0版本:高级实战------网络版通讯录(客户端+服务端)
4.3.1 项目目标
-
结合httplib实现客户端-服务端网络通信
-
掌握Protobuf在分布式场景中的应用
-
实现联系人的增删改查功能
4.3.2 环境准备
-
两台Ubuntu 22.04服务器(或虚拟机),均安装Protobuf
-
下载httplib.h(轻量级HTTP库):
wget https://raw.githubusercontent.com/yhirose/cpp-httplib/master/httplib.h
将httplib.h放入项目目录(httplib是单文件头文件库,无需编译,直接包含即可)。
4.3.3 核心需求
|---------|------------|--------|------------------------------------------------------------------------|
| 功能 | 客户端操作 | 通信方式 | 数据传输 |
| 新增联系人 | 输入姓名、年龄、电话 | POST请求 | 客户端→服务端:AddContactRequest(序列化);服务端→客户端:AddContactResponse(序列化) |
| 删除联系人 | 输入联系人ID | POST请求 | 客户端→服务端:DeleteContactRequest(序列化);服务端→客户端:DeleteContactResponse(序列化) |
| 查看所有联系人 | 选择功能 | GET请求 | 服务端→客户端:FindAllContactsResponse(序列化) |
| 查看单个联系人 | 输入联系人ID | POST请求 | 客户端→服务端:FindOneContactRequest(序列化);服务端→客户端:FindOneContactResponse(序列化) |
4.3.4 实现步骤
步骤1:编写4个.proto文件(按功能拆分)
1.1 add_contact.proto(新增联系人)
syntax = "proto3";
package add_contact;
message AddContactRequest{
string name = 1;
int32 age = 2;
message Phone{
string number = 1;
enum PhoneType{
MP = 0; //移动电话
TEL = 1; //固定电话
}
PhoneType type = 2;
}
repeated Phone phone = 3; //电话信息
}
message AddContactResponse{
bool sucess = 1; //服务是否调用成功
string error_desc = 2; //错误原因
string uid = 3;
}
1.2 delete_contact.proto(删除联系人)
syntax = "proto3";
package delete_contact;
message DeleteContactRequest{
string uid = 1;
}
message DeleteContactResponse{
bool sucess = 1; //服务是否调用成功
string error_desc = 2; //错误原因
string uid = 3;
bool exist = 4;
}
1.3 find_all.proto(查看所有联系人)
syntax = "proto3";
package find_all;
message PeopleInfo{
string uid = 1;
string name = 2;
}
message FindAllContactsResponse{
bool sucess = 1; //服务是否调用成功
string error_desc = 2; //错误原因
repeated PeopleInfo contacts = 3;
}
1.4 find_one.proto(查看单个联系人)
syntax = "proto3";
package find_one;
message FindOneContactRequest{
string uid = 1;
}
message FindOneContactResponse{
bool sucess = 1; //服务是否调用成功
string error_desc = 2; //错误原因
string uid = 3;
string name = 4;
int32 age = 5;
message Phone{
string number = 1;
enum PhoneType{
MP = 0; //移动电话
TEL = 1; //固定电话
}
PhoneType type = 2;
}
repeated Phone phone = 6;
}
步骤2:编写异常类头文件(ContactsException.h)
用于统一处理服务端和客户端的异常:
#include<string>
//异常类
class ContactException
{
private:
std::string message;
public:
ContactException(std::string str = "A problem") : message(str){
}
std::string what() const{ //const修饰成员函数,保证这个成员函数的内部不会修改message这种成员变量
return message;
}
};
步骤3:服务端实现(service/main.cc)
#include<iostream>
#include<fstream>
#include<regex>
#include"httplib.h"
#include"ContactException.h"
#include"add_contact.pb.h"
#include"delete_contact.pb.h"
#include"find_all.pb.h"
#include"find_one.pb.h"
using namespace httplib;
using namespace std;
#define HOST "0.0.0.0"
#define PORT 8132
//声明
void addContactText(add_contact::AddContactRequest &req,const string& uid);
void deleteContactByUid(const string& uid,bool& exist);
void handAddContact(const Request &req, Response &res);
void handDeleteContact(const Request &req, Response &res);
void handFindAllContact(const Request &req, Response &res);
void handFindOneContact(const Request &req, Response &res);
void findContactByUid(const string& uid,find_one::FindOneContactResponse &response);
//生成 UUID (通用唯一标识符)
static unsigned int random_char() {
// 用于随机数引擎获得随机种子
std::random_device rd;
// mt19937是c++11新特性,它是一种随机数算法,用法与rand()函数类似,但是mt19937具有速度快,周期长的特点
// 作用是生成伪随机数
std::mt19937 gen(rd());
// 随机生成一个整数i 范围[0, 255]
std::uniform_int_distribution<> dis(0, 255);
return dis(gen);
}
// 生成 UUID (通用唯一标识符)
static std::string generate_hex(const unsigned int len) {
std::stringstream ss;
// 生成 len 个16进制随机数,将其拼接而成
for (auto i = 0; i < len; i++) {
const auto rc = random_char();
std::stringstream hexstream;
hexstream << std::hex << rc;
auto hex = hexstream.str();
ss << (hex.length() < 2 ? '0' + hex : hex);
}
return ss.str();
}
int main()
{
cout<<"----------服务启动----------"<<endl;
Server server;
server.Post("/contacts/add",handAddContact); //添加联系人端点
server.Post("/contacts/delete",handDeleteContact); //删除联系人端点
server.Get("/contacts/find_all",handFindAllContact); //查看所有联系人的uid和姓名
server.Post("/contacts/find_one",handFindOneContact); //查看某个联系人
//绑定8123端口,并且将端口号对外开放
server.listen(HOST,PORT);
return 0;
}
void handAddContact(const Request &req, Response &res){
cout<<"接收到AddContact请求"<<endl;
//反序列化 request == req.body 1.定义request 2.反序列化
add_contact::AddContactRequest request;
add_contact::AddContactResponse response;
try{
if(!request.ParseFromString(req.body)){
throw ContactException("反序列化失败");
}
//唯一uid
string uid = generate_hex(10);
//新增的联系人信息
addContactText(request,uid);
//构造response,之后是要存储到res.body中,作为res的主体放回给客户端
response.set_sucess(true);
response.set_uid(uid);
//res.body(序列化resopnse)
string response_str;
if(!response.SerializeToString(&response_str)){
throw ContactException("AddContactResponse序列化失败");
}
res.body = response_str;
res.set_header("Content-Type","application/protobuf");
}catch(ContactException &e){
res.status = 500;
response.set_sucess(false);
response.set_error_desc(e.what()); //e.what()返回异常信息
string response_str;
if(response.SerializeToString(&response_str)){ // 这里是把出错的response_str序列化成二进制,放到res.body中。然后把res发送给客户端
res.body = response_str;
res.set_header("Content-Type","application/protobuf");
}
//走到这里,说明序列化失败,那么就返回500错误码,并且把错误信息返回给客户端
cout<<"/conmtacts/add出错了,异常信息: "<<e.what()<<endl;
}
}
void handDeleteContact(const Request &req, Response &res){
cout<<"接收到DeleteContact请求"<<endl;
//将客户端发送的uid反序列化,调用删除联系人函数
delete_contact::DeleteContactRequest request;
delete_contact::DeleteContactResponse response;
try{
if(!request.ParseFromString(req.body)){
throw ContactException("反序列化失败");
}
bool exist = false; //一开始设置不存在
deleteContactByUid(request.uid(),exist);
//写res的内容,返回给客户端
response.set_sucess(true);
response.set_uid(request.uid());
response.set_exist(exist);
string response_str;
if(!response.SerializeToString(&response_str)){
throw ContactException("DeleteContactResponse序列化失败");
}
res.body = response_str;
res.set_header("Content-Type","application/protobuf");
}catch(ContactException &e){
res.status = 500;
response.set_sucess(false);
response.set_error_desc(e.what());
string response_str;
if(response.SerializeToString(&response_str)){
res.body = response_str;
res.set_header("Content-Type","application/protobuf");
}
//走到这里,说明序列化失败,那么就返回500错误码,并且把错误信息返回给客户端
cout<<"/conmtacts/delete出错了,异常信息: "<<e.what()<<endl;
}
}
void handFindAllContact(const Request &req, Response &res){
cout<<"接收到FindAllContact请求"<<endl;
find_all::FindAllContactsResponse response;
try{
ifstream in("contacts.txt");
if(!in){
throw ContactException("打开contacts.txt文件失败");
}
string line;
while(getline(in,line)){
if(line.empty()) break;
// 使用正则表达式解析每一行数据
regex pattern(R"(UID:([a-f0-9]+)\s+name:([^[:space:]]+)\s+age:(\d+)(.*))");
smatch matches;
if(regex_search(line, matches, pattern)) {
string uid = matches[1].str();
string name = matches[2].str();
find_all::PeopleInfo* contact = response.add_contacts();
contact->set_uid(uid);
contact->set_name(name);
}
}
in.close();
//设置状态
response.set_sucess(true);
//序列化
string response_str;
if(!response.SerializeToString(&response_str)){
throw ContactException("FindAllContactResponse序列化失败");
}
res.body = response_str;
res.set_header("Content-Type","application/protobuf");
}catch(ContactException &e){
res.status = 500;
response.set_sucess(false);
response.set_error_desc("解析数据异常:"+e.what());
string response_str;
if(response.SerializeToString(&response_str)){
res.body = response_str;
res.set_header("Content-Type","application/protobuf");
}
throw ContactException("/conmtacts/find_all出错了,异常信息: "+e.what());
}
}
void handFindOneContact(const Request &req, Response &res){
cout<<"接收到FindOneContact请求"<<endl;
find_one::FindOneContactRequest request;
find_one::FindOneContactResponse response;
try{
if(!request.ParseFromString(req.body)){
throw ContactException("反序列化失败");
}
findContactByUid(request.uid(),response);//调用查询联系人函数,补全response
response.set_sucess(true);
string response_str;
if(!response.SerializeToString(&response_str)){
throw ContactException("FindOneContactResponse序列化失败");
}
res.body = response_str;
res.set_header("Content-Type","application/protobuf");
}catch(ContactException &e){
res.status = 500;
response.set_sucess(false);
response.set_error_desc(e.what());
string response_str;
if(response.SerializeToString(&response_str)){
res.body = response_str;
res.set_header("Content-Type","application/protobuf");
}
}
}
void addContactText(add_contact::AddContactRequest &req,const string& uid){
ofstream out("contacts.txt",ios::app);
if(out){
out<<"UID:"<<uid<<" ";
out<<"name:"<<req.name()<<" ";
out<<"age:"<<req.age()<<" ";
for(int i=0;i<req.phone_size();i++){
const ::add_contact::AddContactRequest_Phone &phone = req.phone(i);
out<<"phone:"<< phone.number()<<" ";
out<<"phone_type:"<<phone.type()<<" ";
}
out<<endl;
//关闭文件
out.close();
}else{
throw ContactException("打开contacts.txt文件失败");
}
}
void deleteContactByUid(const string& uid,bool& exist){
vector<string> lines;
string line;
ifstream in("contacts.txt");
if(!in){
throw ContactException("打开contacts.txt文件失败");
}
while(getline(in,line)){
lines.push_back(line);
}
in.close();
ofstream out("contacts.txt");
if(!out){
throw ContactException("打开contacts.txt文件失败");
}
for (const auto& current_line : lines) {
if (current_line.find("UID:" + uid) == string::npos) {
out << current_line << endl;
}else{
exist = true;
}
}
out.close();
}
void findContactByUid(const string& uid, find_one::FindOneContactResponse &response){
ifstream in("contacts.txt");
if(!in){
throw ContactException("打开contacts.txt文件失败");
}
string line;
while(getline(in,line)){
if(line.empty() || line.find("UID:" + uid) == string::npos) continue;
/*
正则表达式:
1.regex:定义正则表达式模式
2.smatch:存储匹配结果
3.regex_search:在字符串中搜索匹配项
*/
// 使用正则表达式解析每一行数据
regex pattern(R"(UID:([a-f0-9]+)\s+name:([^[:space:]]+)\s+age:(\d+)(.*))");
smatch matches;
if(regex_search(line, matches, pattern)) {
string found_uid = matches[1].str();
string name = matches[2].str();
string age_str = matches[3].str();
string phone_part = matches[4].str();
// 设置基本信息
response.set_uid(found_uid);
response.set_name(name);
response.set_age(stoi(age_str));
// 解析可能存在的多个phone和phone_type
regex phone_pattern(R"(phone:(\d+)\s+phone_type:(\d+))");
smatch phone_matches;
string::const_iterator search_start(phone_part.cbegin()); //设置搜索起始位置,到后面的时候,需要跳转搜索起始位置(因为有很多个电话信息)
while(regex_search(search_start, phone_part.cend(), phone_matches, phone_pattern)) {
string phone_number = phone_matches[1].str();
string phone_type = phone_matches[2].str();
// 添加电话信息
auto phone_info = response.add_phone();
phone_info->set_number(phone_number);
// 将字符串转换为正确的枚举类型
int phone_type_int = stoi(phone_type);
find_one::FindOneContactResponse_Phone_PhoneType enum_type;
switch(phone_type_int) {
case 0:
enum_type = find_one::FindOneContactResponse_Phone_PhoneType_MP;
break;
case 1:
enum_type = find_one::FindOneContactResponse_Phone_PhoneType_TEL;
break;
}
phone_info->set_type(enum_type);
search_start = phone_matches.suffix().first;
}
}
break; //到这里说明已经出现了一个与之匹配的uid,就可以退出循环了
}
in.close();
}
步骤4:客户端实现(client/main.cc)
#include<iostream>
#include"httplib.h"
#include"add_contact.pb.h"
#include"delete_contact.pb.h"
#include"find_all.pb.h"
#include"find_one.pb.h"
#include"ContactsException.h"
#include<fstream>
using namespace std;
using namespace httplib;
#define CONTACTS_HOST "192.168.5.13"
#define CONTACTS_POST 8132
/*
前置声明:在 main 函数中会调用 addContact 函数,但实际的函数定义在后面
编译器需要:通过前置声明,编译器在编译 main 函数时就知道有这个函数存在
*/
void addContact();
bool buildAddContactRequest(add_contact::AddContactRequest* req);
void deleteContact();
void findAll();
void findOne();
void writeErrorLog(const std::string& message);
void writeUid(const std::string& name,const std::string& uid);
void DeleteUid(const string& uid);
bool isUidExist(const string& uid);
void printFindAllContactsResponse(find_all::FindAllContactsResponse& response);
void printFindOneContactResponse(find_one::FindOneContactResponse& response);
void menu()
{
cout<< "----------------------------------------"<<endl;
cout<< "-------- 请选择对通讯录的操作 ------------"<<endl;
cout<< "------------ 1.新增联系人 --------------"<<endl;
cout<< "------------ 2.删除联系人 --------------"<<endl;
cout<< "---------- 3.查看联系人列表 -------------"<<endl;
cout<< "--------- 4.查看联系人详细信息 -----------"<<endl;
cout<< "------------ 0.退出 --------------"<<endl;
}
int main()
{
enum OPTION{QUIT=0,ADD,DEL,FIND_ALL,FIND_ONE};
while(true)
{
menu();
cout<< "--->请选择:";
int choose;
cin>>choose;
cin.ignore(256,'\n');
try{
switch(choose)
{
case OPTION::QUIT:
cout<<"----> 程序退出"<<endl;
return 0;
case OPTION::ADD:
addContact();
break;
case OPTION::DEL:
deleteContact();
break;
case OPTION::FIND_ALL:
findAll();
break;
case OPTION::FIND_ONE:
findOne();
break;
default:
cout<<"----> 输入有误,请重新选择! "<<endl;
}
}catch(const ContactException &e){ //捕获异常类的对象, 这个对象是在try中会抛出来的
writeErrorLog("---> 操作通讯录时发生异常, 异常信息: "+ e.what());
}
}
return 0;
}
void addContact()
{
//搭建客户端连接
Client cli(CONTACTS_HOST,CONTACTS_POST); //创建一个HTTP客户端对象,用于向通讯录服务端发送请求
//构造req
add_contact::AddContactRequest req;
if(!buildAddContactRequest(&req)){
cout<<"---> 输入有误,请重新输入"<<endl;
return;
}
//序列化req
string req_str;
if(!req.SerializeToString(&req_str)) //如果序列化失败
{
throw ContactException("AddContactRequest序列化失败!"); //构造异常对象,匿名对象
}
//发起post请求
auto res = cli.Post("/contacts/add",req_str,"application/protobuf");
if(!res) //如果请求失败
{
string err_desc;
err_desc="/contacts/add链接失败,错误信息: "+httplib::to_string(res.error());
throw ContactException(err_desc);
}
//反序列化resp -- res是Post请求的返回值,而需要反序列化的值是在res.body里面
add_contact::AddContactResponse resp;
bool parse = resp.ParseFromString(res->body); //反序列化之后的值保存在resp里面
if(res->status != 200 || !parse){
string err_desc;
err_desc="/contacts/add 调用失败!"+std::to_string(res->status)+"("+res->reason+")";
throw ContactException(err_desc);
}else if(res->status!=200){
string err_desc;
err_desc="/contacts/add 调用失败!"+std::to_string(res->status)+"("+res->reason+") 错误原因: "+resp.error_desc();
throw ContactException(err_desc);
}else if(!resp.sucess()){
string err_desc;
err_desc="/contacts/add 结果异常! 异常原因: "+resp.error_desc();
throw ContactException(err_desc);
}
//结果打印
cout << "---> 添加成功! 联系人ID: " << resp.uid() << endl;
//持久化存储resp.uid() ,uid是string类型
// writeUid(req.name(),resp.uid());
}
bool buildAddContactRequest(add_contact::AddContactRequest* req)
{
cout<<"---> 请输入联系人姓名: ";
string name;
getline(cin,name);
if(name.empty()){return false;}
req->set_name(name);
cout<<"---> 请输入联系人年龄: ";
string str_age;
getline(cin,str_age);
int age = -1;
try {
age = std::stoi(str_age);
// 使用 value
} catch (const std::invalid_argument& e) {
// 处理无效参数异常
std::cerr << "无效参数异常: " << e.what() << std::endl;
} catch (const std::out_of_range& e) {
// 处理数值超出范围异常
std::cerr << "数值超出范围异常: " << e.what() << std::endl;
}
if(age<=0 || age>=200) {return false;}
req->set_age(age);
for(int i=1;;i++)
{
cout<<"---> 请输入第"<<i<<"个联系人信息(只输入回车完成电话的新增):";
string number;
getline(cin,number);
if(number.empty()){break;}
add_contact::AddContactRequest_Phone* phone = req->add_phone();
phone->set_number(number); //第一段是获得联系人电话这个数组的地址,第二段是添加联系人电话
cout<<"---> 请输入联系人电话类型(1.移动电话 2.固定电话): ";
int type;
cin>>type;
cin.ignore(256,'\n');
switch (type)
{
case 1:
phone->set_type(add_contact::AddContactRequest_Phone_PhoneType::AddContactRequest_Phone_PhoneType_MP);
break;
case 2:
phone->set_type(add_contact::AddContactRequest_Phone_PhoneType::AddContactRequest_Phone_PhoneType_TEL);
break;
default:
cout<<"---> 输入有误,请重新输入"<<endl;
i--;
break;
}
}
return true;
}
void deleteContact(){
//搭建客户端连接
Client cli(CONTACTS_HOST,CONTACTS_POST);
//获取uid
cout<<"---> 请输入联系人ID: ";
string uid;
getline(cin,uid);
//创建req,设置uid值和序列化
delete_contact::DeleteContactRequest req;
req.set_uid(uid);
string req_str;
if(!req.SerializeToString(&req_str)){
throw ContactException("DeleteContactRequest序列化失败!");
}
// //先在前端检测是否有这个uid
// if(!isUidExist(uid)){
// cout<<"---> 联系人不存在!"<<endl;
// return;
// }
//发起post请求 指定端点, 指定请求体数据, 告诉服务器请求体的数据格式
auto res = cli.Post("/contacts/delete",req_str,"application/protobuf");
if(!res){
string err_desc;
err_desc="/contacts/delete链接失败,错误信息: "+httplib::to_string(res.error());
throw ContactException(err_desc);
}
delete_contact::DeleteContactResponse resp;
bool parse = resp.ParseFromString(res->body);
if(res->status != 200 || !parse){
string err_desc;
err_desc="/contacts/delete 调用失败!"+std::to_string(res->status)+"("+res->reason+")";
throw ContactException(err_desc);
}else if(res->status!=200){
string err_desc;
err_desc="/contacts/delete 调用失败!"+std::to_string(res->status)+"("+res->reason+") 错误原因: "+resp.error_desc();
throw ContactException(err_desc);
}else if(!resp.sucess()){
string err_desc;
err_desc="/contacts/delete 结果异常! 错误原因: "+resp.error_desc();
throw ContactException(err_desc);
}else if(!resp.exist()){
cout<<"---> 联系人不存在!"<<endl;
}else{
// DeleteUid(uid);
cout<<"---> 删除成功!"<<endl;
}
}
void writeErrorLog(const std::string& message)
{
// 获取当前时间
auto now = std::chrono::system_clock::now();
auto time_t = std::chrono::system_clock::to_time_t(now);
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
now.time_since_epoch() % 1000);
std::stringstream ss;
ss << std::put_time(std::localtime(&time_t), "%Y/%m/%d %H:%M:%S");
//fstream是可读可写,ifstream只读,ofstream只写
ofstream error("error.txt",ios::app);
if(error.is_open()){
error << "[" << ss.str() << "] " << message << endl;
error.close();
}else{
cerr<<"---> 创建error.txt文件失败!"<<endl;
}
}
void findAll(){
//搭建客户端连接
Client cli(CONTACTS_HOST,CONTACTS_POST);
auto res = cli.Get("/contacts/find_all");
if(!res){
string err_desc;
err_desc="/contacts/find_all链接失败,错误信息: "+httplib::to_string(res.error());
throw ContactException(err_desc);
}
//反序列化
find_all::FindAllContactsResponse response;
bool parse = response.ParseFromString(res->body);
//处理异常
if(res->status != 200 || !parse){
string err_desc;
err_desc="/contacts/find_all 调用失败!"+std::to_string(res->status)+"("+res->reason+")";
throw ContactException(err_desc);
}else if(res->status!=200){
string err_desc;
err_desc="/contacts/find_all 调用失败!"+std::to_string(res->status)+"("+res->reason+") 错误原因: "+response.error_desc();
throw ContactException(err_desc);
}else if(!response.sucess()){
string err_desc;
err_desc="/contacts/find_all 结果异常! 错误原因: "+response.error_desc();
throw ContactException(err_desc);
}
//正常返回,打印结果
printFindAllContactsResponse(response);
}
void printFindAllContactsResponse(find_all::FindAllContactsResponse& response){
//如果为空
if(response.contacts().size()==0){
cout<<"---> 联系人列表为空!"<<endl;
return;
}
cout<<"---> 联系人列表: "<<endl;
for(auto& people_info:response.contacts()){
cout<<"联系人ID: "<<people_info.uid()<<" 姓名: "<<people_info.name()<<endl;
}
}
void findOne(){
//搭建客户端连接
Client cli(CONTACTS_HOST,CONTACTS_POST);
cout<<"---> 请输入联系人ID: ";
string uid;
getline(cin,uid);
find_one::FindOneContactRequest req;
req.set_uid(uid);
string req_str;
if(!req.SerializeToString(&req_str)){
throw ContactException("FindOneContactRequest序列化失败!");
}
auto res = cli.Post("/contacts/find_one",req_str,"application/protobuf");
if(!res){
string err_desc;
err_desc="/contacts/find_one链接失败,错误信息: "+httplib::to_string(res.error());
throw ContactException(err_desc);
}
find_one::FindOneContactResponse response;
bool parse = response.ParseFromString(res->body);
if(res->status != 200 || !parse){
string err_desc;
err_desc="/contacts/find_one 调用失败!"+std::to_string(res->status)+"("+res->reason+")";
throw ContactException(err_desc);
}else if(res->status!=200){
string err_desc;
err_desc="/contacts/find_one 调用失败!"+std::to_string(res->status)+"("+res->reason+") 错误原因: "+response.error_desc();
throw ContactException(err_desc);
}else if(!response.sucess()){
string err_desc;
err_desc="/contacts/find_one 结果异常! 错误原因: "+response.error_desc();
}
printFindOneContactResponse(response);
}
void printFindOneContactResponse(find_one::FindOneContactResponse& response)
{
cout<<"联系人ID: "<<response.uid()<<endl;
cout<<"姓名: "<<response.name()<<endl;
for(auto& phone:response.phone()){
cout<<"电话: "<<phone.number()<<" 类型: "<<find_one::FindOneContactResponse_Phone_PhoneType_Name(phone.type())<<endl;
}
}
// void writeUid(const std::string& name,const std::string& uid){
// ofstream uid_file("uid.txt",ios::app);
// if(uid_file){
// uid_file<<name<<": "<<uid<<endl;
// uid_file.close();
// }else{
// throw ContactException("---> 创建uid.txt文件失败!");
// }
// }
// void DeleteUid(const string& uid){
// ifstream in("uid.txt");
// if(!in){
// throw ContactException("---> 读模式打开uid.txt文件失败!");
// }
// vector<string> uid_names;
// string uid_name;
// while(getline(in,uid_name)){
// uid_names.push_back(uid_name);
// }
// in.close();
// ofstream out("uid.txt");
// if(!out){
// throw ContactException("---> 写模式打开uid.txt文件失败!");
// }
// for(auto& name:uid_names){
// if(name.find(uid) == name.npos){
// out<<name<<endl;
// }
// }
// out.close();
// }
// bool isUidExist(const string& uid){
// ifstream in("uid.txt");
// if(!in){
// throw ContactException("---> 读模式打开uid.txt文件失败!");
// }
// string uid_name;
// while(getline(in,uid_name)){
// if(uid_name.find(uid) != uid_name.npos){
// return true;
// }
// }
// in.close();
// return false;
// }
五、最佳实践与注意事项
5.1 语法设计最佳实践
-
字段编号:核心字段用1-15,数字越小占用字符越少,核心字段出现频率越高,使用的数字应该越小。
-
包名:使用有意义的包名(如
com.company.project),避免冲突 -
字段命名:使用下划线命名法(
phone_number),Protobuf会自动转换为对应语言的命名规范(C++为phone_number) -
兼容性:删除字段时必须用
reserved保留编号和名称,新增字段用optional/repeated -
枚举:第一个值必须为0,命名清晰(如
UNSPECIFIED表示未指定)
5.2 性能优化建议
-
序列化方式:优先使用
SerializeToString()/ParseFromString()(内存操作),避免频繁文件I/O -
字段类型选择:
-
整数类型:根据实际范围选择(如
int32适用于-2³¹++2³¹-1,`uint32`适用于0++2³²-1) -
字符串:短字符串优先使用
string,长二进制数据使用bytes
-
-
避免过度嵌套:嵌套层级过多会增加解析复杂度和内存占用
5.3 网络通信注意事项
-
数据传输:指定Content-Type为
application/protobuf,避免服务端解析错误 -
异常处理:序列化/反序列化失败、网络连接异常需捕获并处理,返回友好提示
-
数据校验:客户端输入需校验(如年龄范围、电话号码格式),服务端接收数据后也需二次校验,避免恶意数据
-
跨语言通信:确保双方使用相同版本的
.proto文件,字段编号和类型一致
5.4 调试技巧
-
二进制解码:使用
protoc --decode=包名.消息名 proto文件 < 二进制文件快速查看二进制数据 -
日志输出:关键步骤打印日志(如序列化前的对象、反序列化后的结果),将错误信息通过writeErrorLog函数写入到error.txt文件中,便于定位问题
-
版本兼容测试:修改
.proto文件后,用旧版本程序解析新数据,用新版本程序解析旧数据,验证兼容性
六、项目代码地址
本文所有实战项目的完整代码(含proto文件、源码、Makefile)已上传至Gitee,可直接下载编译运行:
实战一:源码在fast_start文件夹下。
实战二:源码在proto3文件夹下。
实战一:源码在http_contacts_service和http_contacts_client文件夹下。
结语
Protobuf的学习是一个"语法→基础实战→高级实战"的递进过程,通过本文的3个通讯录项目,读者可以逐步掌握Protobuf的核心语法、文件操作、网络通信等场景的应用。Protobuf作为一种高效的序列化格式,在分布式系统、微服务、物联网等领域有着广泛的应用前景,掌握它将为后续的技术发展打下坚实基础。希望本文的超详细笔记能帮助读者少踩坑、快速上手,在实际项目中灵活运用Protobuf!