VS2019下编译使用Protobuf 3.8.0:C++序列化与反序列化实战
简介这是一份面向 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.zipCMake 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_PREFIXE:\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\libprotoc 编译器在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::RepeatedPtrFieldstd::string。消息可以嵌套比如Address定义在UserInfo内部生成的 C 类名会是双层命名使用时写UserInfo::Address。3.2 使用 protoc 生成 C 代码写完 .proto 后打开命令行或者直接在 VS2019 的开发者命令行里执行跑 protocE:\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 默认是/MDRelease和/MDdDebug我们用 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(zhangsanexample.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: zhangsanexample.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 控制台默认代码页如果是 936GBK打印 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 字节大端长度接收端先收头部再按长度读消息体。这个封装我在多个项目中都重复实现过没有翻过车。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联
返回资讯列表 →