简介:这是一份面向 C++开发者的 protobuf 3.8.0 在 Visual Studio 2019 环境下的完整使用案例资源。资源不仅提供官方库文件,还包含可运行的演示工程与配套源码,清晰演示了从创建 .proto 文件定义 Person 等消息结构,到调用 protoc 编译器生成 message.pb.h 与 message.pb.cc,再到通过零拷贝流将对象序列化到字符串或内存、以及从字节流反序列化回对象的全过程,适合需要快速上手 ProtoBuf 的初、中级 C++ 开发者。压缩包共含 688 个文件,以 cc 源码、h 头文件、proto 定义文件为主体,同时包含 VS 工程与解决方案文件、编译中间产物、少量可执行的 protoc 工具以及调试信息,总计约 4.72MB,目录结构清晰,便于按模块直接嵌入现有项目。已有 1817 人学习下载。借助这份资源,读者可省去自行编译配置环境的繁琐过程,直接参照完整案例搭建项目,同时理解跨平台数据交换与高效二进制序列化的核心用法,为后续网络通信、数据存储等场景提供可直接复用的实现参考。
1. 项目概述与环境选型
1.1 为什么选 protobuf 3.8.0 + VS2019
做 C++ 服务端开发的兄弟应该都清楚,只要是涉及跨语言、跨平台的数据交换,Protocol Buffers(以下简称 protobuf)基本上是绕不开的标配。我在实际项目里用 protobuf 很多年了,从 2.x 一直追到 3.x,客观说,3.8.0 这个版本属于 proto3 语法稳定之后、性能和安全修复比较完善的阶段,没有早期 3.x 版本那么多坑,也比后边 3.20+ 的 cmake 配置变化小,对于在 Windows + VS2019 环境下做 C++ 开发来说,是相当省心的选择。
VS2019 同样是当前存量项目里占有率很高的 IDE。很多朋友在公司里接手的老项目就是 VS2019 建的,自己本地又装了新版本,结果开发的时候经常碰到工程格式兼容、工具集版本不一致的问题。用 VS2019 搭配 protobuf 3.8.0,正好是网上资料最丰富、教程最多的组合,遇到问题基本都能搜到现成答案,省得自己折腾。
这篇内容我按实际项目的落地顺序来写:怎么编译出 protobuf 库、怎么写 .proto 文件、怎么生成 C++ 类、怎么在 VS2019 里配置工程、最终怎么在代码里完成序列化和反序列化。新手照着一路点下来能跑通,老手也可以直接跳到常见问题部分,看看有没有自己之前没注意到的坑。
1.2 整体方案设计思路
protobuf 在 C++ 项目的使用链路其实可以拆成四个阶段:
- 用 .proto 文件描述数据结构,这是与语言无关的定义层;
- 用 protoc 编译器生成目标语言的类代码(C++ 生成 .pb.h 和 .pb.cc);
- 在 VS2019 工程里配置 include 路径、lib 路径和链接库;
- 在业务代码里调用生成类完成序列化(对象转字节流)和反序列化(字节流转对象)。
很多人卡住的位置不在业务代码,而在前两步:要么不会编译 protobuf 的 C++ 库,要么不知道 protoc 生成的代码应该放到工程的什么位置。下面我把每个环节的细节逐步展开,重点说那些编译不过、链接失败时容易忽略的地方。
2. 从源码编译 protobuf C++ 库
2.1 准备工作:需要下载哪些东西
要再强调一次,protobuf 的 C++ 运行时库并不是装个绿色版就能直接用的,你得拿源码配合 CMake 自己编,或者找现成的预编译包。3.8.0 那个年代官方没有直接提供 VS2019 对应的预编译二进制,所以自己编译是主流做法。
需要准备的材料如下:
| 组件 | 说明 | 获取方式 |
|---|---|---|
| protobuf 源码 v3.8.0 | 包含 protoc 源码、C++ 运行时库、cmake 配置 | GitHub 官方仓库 release 页面下载 protobuf-cpp-3.8.0.zip |
| CMake 3.14+ | 用于生成 VS2019 工程文件 | cmake.org 下载安装包,安装时勾选“添加 CMake 到系统 PATH” |
| VS2019 | 需要安装“使用 C++ 的桌面开发”工作负载 | Visual Studio Installer 中修改工作负载 |
这里有个经验:在 GitHub 下载源码包的时候,一定要认准protobuf-cpp-3.8.0.zip,不要下成protobuf-java-3.8.0.zip或者其他语言的包。C++ 这个包解压之后,根目录下能看到cmake文件夹和CMakeLists.txt,这才是我们需要的。
2.2 CMake 生成工程与编译步骤
protobuf 官方推荐直接用 CMake 构建。我习惯在源码根目录外建一个独立的 build 目录,避免污染源码目录:
# 源码解压到 E:\protobuf\protobuf-3.8.0 # 进入源码目录,创建 build 目录 cd E:\protobuf\protobuf-3.8.0 mkdir build cd build # 生成 VS2019 工程,注意指定 x64 还是 Win32 cmake .. -G "Visual Studio 16 2019" -A x64 -DCMAKE_INSTALL_PREFIX=E:\protobuf\install # 编译并安装,Debug 和 Release 可以都编,或者按需选择 cmake --build . --config Release cmake --install .这一步要说明几个关键点:
-A x64指定生成 64 位工程,这个必须和你后续主工程的平台一致。如果主工程是 Win32,就写-A Win32,不然后面链接阶段会非常痛苦,一堆 LNK2019。
-DCMAKE_INSTALL_PREFIX指定安装目录,编译完后 include、lib、exe 都会集中输出到这个目录下,方便我们后面配置 VS2019 工程时引用。我的习惯是统一装到E:\protobuf\install,这样头文件在E:\protobuf\install\include,库文件在E:\protobuf\install\lib,protoc 编译器在E:\protobuf\install\bin。
编译过程如果一切顺利,几分钟就能完事。如果中途报错,大概率是 CMake 版本太老,或者 Windows SDK 组件没装全。VS2019 安装器里记得勾选 Win10 SDK,哪怕你觉得不写 Windows 底层代码用不上,编译第三方库时经常会碰到头文件缺失,这时候再补装就有点浪费时间了。
2.3 编译产物验证
安装完成后,检查一下E:\protobuf\install目录:
bin\protoc.exe:编译器,负责把 .proto 转成 C++ 代码;include\google\protobuf\...:运行时头文件;lib\libprotobuf.lib、libprotobufd.lib:静态库(d 结尾是 Debug 版)。
这里有个细节要注意:protobuf 3.x 在 Windows 下默认生成的静态库,Debug 和 Release 是分开的。链接的时候如果 Release 工程链了 debug 库,或者反过来,都会出 LNK2038 或者运行时崩溃。后面配置 VS2019 工程时,我会用条件宏分别指定,避免手动改。
3. 编写 .proto 文件并生成 C++ 类
3.1 .proto 文件的基础语法
消息定义是 protobuf 的核心,我们先用一个常用的“用户信息”例子感受一下 proto3 的语法:
syntax = "proto3"; package demo; message UserInfo { int32 id = 1; string name = 2; string email = 3; repeated string tags = 4; Address address = 5; message Address { string country = 1; string city = 2; string street = 3; } }几个要点需要重点理解:
syntax = "proto3"必须写在第一行非注释位置,表示使用 proto3 语法。如果不写,protoc 默认用 proto2,字段的optional/required规则就会完全不一样,代码生成结果也会不同。每个字段后面都有一个数字编号(1、2、3...),这个编号是字段的二进制表示标识,一旦上线就不能乱改,否则会导致老数据反序列化失败。开发期可以调整,线上千万不要动。
repeated关键字表示数组/列表,在 C++ 里对应google::protobuf::RepeatedPtrField<std::string>。消息可以嵌套,比如
Address定义在UserInfo内部,生成的 C++ 类名会是双层命名,使用时写UserInfo::Address。
3.2 使用 protoc 生成 C++ 代码
写完 .proto 后,打开命令行(或者直接在 VS2019 的开发者命令行里执行)跑 protoc:
E:\protobuf\install\bin\protoc.exe --cpp_out=./ user.proto--cpp_out指定生成代码的输出目录。执行完后,当前目录多出两个文件:user.pb.h和user.pb.cc。这两个文件就是 protobuf 根据我们的消息定义生成的 C++ 类实现。
我不建议直接在 .proto 所在目录生成代码并手工拷贝,因为工程里文件多起来之后容易乱。规范做法是:源码目录下建一个proto文件夹放 .proto,再建一个generated文件夹放生成代码,每次生成后直接覆盖,全程不手工改生成代码。
3.3 生成代码的核心组成
打开user.pb.h会发现里面的内容非常多,但核心其实就是:
class UserInfo : public ::google::protobuf::Message:最终要操作的消息类;- 字段的 getter/setter:比如
name()和set_name(); - 序列化方法
SerializeToString()/SerializeToArray(); - 反序列化方法
ParseFromString()/ParseFromArray(); - 内存管理相关的
arena接口,一般场景用不到; - 反射相关的 metadata 接口,调试时有点用。
你要知道的一点是:生成代码不要手动改,也基本不需要细读所有内容。把它当成一次性的“胶水代码”,使用的时候直接调用公共接口就行。
4. 在 VS2019 中配置 C++ 工程并跑通示例
4.1 创建工程与引入生成代码
打开 VS2019,新建一个 C++ 控制台应用项目,项目名我这里用ProtoDemo。工程建好之后,把user.pb.h和user.pb.cc直接拖到“解决方案资源管理器”的“源文件”和“头文件”分类下,或者放在工程目录里再通过“添加现有项”引入。
这里有一个很多人不注意的点:user.pb.cc要参与编译,user.pb.h只是头文件声明。如果发现链接时找不到UserInfo类的符号,多半是.cc文件没有加进工程。
4.2 配置头文件路径与库路径
打开项目属性页(右键项目 -> 属性),重点看几个地方:
C/C++ -> 常规 -> 附加包含目录:添加E:\protobuf\install\include
链接器 -> 常规 -> 附加库目录:添加E:\protobuf\install\lib
链接器 -> 输入 -> 附加依赖项:这里要根据 Debug/Release 分别设置。
我自己的习惯是手动输入宏条件,不让它混在一起:
# Debug 版 $(SolutionDir)..\lib\libprotobufd.lib # Release 版 $(SolutionDir)..\lib\libprotobuf.lib但在用 CMake 安装目录的时候,更直接的做法是在“附加依赖项”里两个都写进去,只要路径存在,链接器会自己选。不过为了规范和减小体积,建议还是不同配置填不同值。
还需要注意运行库的设置。VS2019 默认是/MD(Release)和/MDd(Debug),我们用 CMake 编 protobuf 时生成的库也默认是 MD 族,所以一般情况下不用动。如果你在公司项目里被强制要求用/MT静态运行库,那就需要去 CMake 里把 protobuf 也改成/MT重新编,否则链接阶段一定报错。
4.3 代码示例:完整的序列化与反序列化
配置完成之后,我们来写一个完整可运行的示例。这段代码覆盖了字段赋值、序列化、反序列化、嵌套消息和 repeated 字段的操作:
#include <iostream> #include <string> #include "user.pb.h" int main() { // 1. 构造消息对象并赋值 demo::UserInfo user; user.set_id(1001); user.set_name("zhangsan"); user.set_email("zhangsan@example.com"); // 2. 给 repeated 字段添加值 user.add_tags("student"); user.add_tags("cpp"); // 3. 设置嵌套消息 demo::UserInfo::Address* addr = user.mutable_address(); addr->set_country("China"); addr->set_city("Shanghai"); addr->set_street("Nanjing Road"); // 4. 序列化成字符串 std::string data; bool ok = user.SerializeToString(&data); if (!ok) { std::cerr << "serialize failed" << std::endl; return -1; } std::cout << "serialized bytes: " << data.size() << std::endl; // 5. 反序列化出新的对象 demo::UserInfo new_user; if (!new_user.ParseFromString(data)) { std::cerr << "parse failed" << std::endl; return -1; } // 6. 读取字段 std::cout << "id: " << new_user.id() << std::endl; std::cout << "name: " << new_user.name() << std::endl; std::cout << "email: " << new_user.email() << std::endl; // 遍历 repeated 字段 for (const auto& tag : new_user.tags()) { std::cout << "tag: " << tag << std::endl; } // 读取嵌套消息 if (new_user.has_address()) { const demo::UserInfo::Address& address = new_user.address(); std::cout << "country: " << address.country() << std::endl; std::cout << "city: " << address.city() << std::endl; std::cout << "street: " << address.street() << std::endl; } return 0; }这段代码建议自己敲一遍,特别注意两点:
mutable_address()这个函数会先创建一个默认的Address对象再返回指针,所以可以直接给指针指向的对象赋值。网上有些老教程直接用set_allocated_address(),那个方法要手动管理内存,用不好容易出 double free,新手先用mutable_系列就行。
SerializeToString的性能问题:如果你在循环里高频调用,建议改成SerializeToArray(void* data, int size),提前分配一块缓冲区,减少 string 反复扩容的开销。这个优化在 QPS 高的服务里差别很明显。
5. 序列化与反序列化的进阶细节与性能优化
5.1 序列化结果分析
运行上面代码后,输出大致是:
serialized bytes: 42 id: 1001 name: zhangsan email: zhangsan@example.com tag: student tag: cpp country: China city: Shanghai street: Nanjing Road42 字节比等价的 JSON 短很多,这就是 protobuf 压缩二进制格式的优势。它是用字段编号 + wire type 来区分数据的,字符串字段只存原始字节内容,不存键名。所以字段名随便改都不会影响线上数据解析,但字段编号改了就会出大问题,这也是我在 3.1 强调过的那句话。
5.2 性能优化:复用对象与动态分配
实际项目里如果每秒要处理几千条消息,创建和销毁 protobuf 对象的成本不可忽视。最直接的优化方法是复用对象:
demo::UserInfo user; for (int i = 0; i < 100000; ++i) { user.Clear(); user.set_id(i); user.set_name("test"); // 只序列化不反序列化,或者反过来 }Clear()会重置对象的逻辑状态,但底层的字符串缓冲区、RepeatedPtrField 的存储空间会被复用,避免反复 malloc/free。这一点在服务端代码里很常见,对性能敏感的朋友可以重点关注。
此外,如果你同时处理大量小消息,可以考虑Arena机制。简单理解就是一次性分配一块大内存,消息对象都在里面申请空间,统一释放。但 Arena 会让代码可读性下降,小项目不用刻意引入。
5.3 字节序与平台兼容性
protobuf 序列化后的数据自带格式描述,整型都是用 little-endian 存储的,所以跨平台传输时不需要自己做字节序转换。这一点比手动拼二进制流方便太多。我自己以前做网络协议时用htonl转来转去,稍不留神就出错,换成 protobuf 后这类问题直接消失。
6. 常见问题排查与避坑实录
这个章节直接列我在实际使用过程中遇到的高频问题,每条都是踩过坑换来的。
6.1 protoc 不是内部或外部命令
如果直接在 cmd 里敲 protoc 提示找不到命令,是因为没把 protoc.exe 所在目录加进系统 PATH。两种解法:临时用绝对路径执行,或者在系统环境变量 Path 中添加E:\protobuf\install\bin。我推荐后者,因为后续写批处理脚本、嵌入 CMake 时都会方便很多。
6.2 链接错误 LNK2019 / LNK2001
报错信息通常长这样:
LNK2019 unresolved external symbol "public: std::string ...常见原因有四个:
- 工程没有包含
user.pb.cc源文件; - 附加依赖项没写
libprotobuf.lib或libprotobufd.lib; - 工程是 x64,编译的库是 Win32(或反过来);
- Debug/Release 混链了 protobuf 库。
这些从项目属性和构建日志里基本能定位。建议把构建输出切到“详细”级别,看实际链接了哪些库、在找哪些符号。
6.3 运行时崩溃或数据解析失败
程序能编过但运行崩,常见是这些情况:
- 反序列化时用了不匹配的 .proto 版本(比如加了新字段但编号和老数据冲突);
- 把
SerializeToString之后的结果用二进制方式算长度、拷贝到 char* 时没考虑二进制数据中间有\0; - 多线程里共享同一个消息对象,没有加锁。
最后一个在多线程环境下特别容易踩:多个线程同时调用SerializeToString或者同时修改字段,内部状态就乱了。解决办法是每个线程持有独立的消息对象,或者加互斥锁。
6.4 中文乱码
protobuf 的 string 类型存储的是 UTF-8 编码字节。如果你从 GBK 编码的文本文件或 Windows 控制台直接读入中文字符串再 set 进去,反序列化出来打印时会乱码。
建议工程内统一使用 UTF-8 for 代码文件,读取外部文件时做编码转换。Windows 控制台默认代码页如果是 936(GBK),打印 UTF-8 字符串也可能乱码,可以直接在 main 函数开头执行:
system("chcp 65001");或者在 VS2019 里勾选项目属性的“字符集”相关配置,让控制台切换到 UTF-8 模式。
6.5 VS2019 编译提示“无法打开包括文件 google/protobuf/message.h”
这种情况基本都是附加包含目录路径不对。检查一下E:\protobuf\install\include下是否存在google\protobuf\message.h这个文件。如果不存在,说明 protobuf 安装路径不完整,重新执行cmake --install .大概率能解决。
7. 从单体示例扩展到真实项目的建议
到这里,一个完整的 protobuf + VS2019 C++ 示例已经跑通了。不过建议你再往下想一步:真实项目里不会只有一个 .proto 文件,也不会只有一对消息。如何管理 .proto 、生成代码和工程依赖,往往比调通当前 demo 更重要。
我的实践做法是这样的:仓库根目录维护proto文件夹,按业务模块拆分子目录,例如proto/user、proto/order;用一个批处理脚本统一调用 protoc 生成 C++ 代码到generated目录;生成代码不提交到 Git 仓库(或者提交但要在 README 里注明生成命令),通过构建脚本自动生成。这样多人协作时,.proto 变更和生成代码变更就不容易出现“别人没跑 protoc 导致代码不一致”的问题。
另一个建议是版本管理。即使你当前项目用的是 3.8.0,也要在文档里记录你用的 protoc 版本,因为不同小版本的 protoc 生成的代码可能略有差别。以后升级 protobuf 大版本时,别忘了重新生成一遍所有 .pb.cc/.pb.h,别继续沿用旧文件,否则很容易出现隐性的数据兼容问题。
最后,如果你要把 protobuf 用在 UDP 这种面向报文的场景,建议自己封装一层消息边界和长度字段,不要指望 protobuf 自带流式分包能力。每次发送前先序列化为字节数组,再拼上一个固定长度的头部(比如 4 字节大端长度),接收端先收头部再按长度读消息体。这个封装我在多个项目中都重复实现过,没有翻过车。
本文还有配套的精品资源,点击获取