ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

protobuf-3.8.0 配 VS2019:从编译到序列化实战

protobuf-3.8.0 配 VS2019:从编译到序列化实战 简介本资源面向在 Visual Studio 2019 环境下使用 C 进行数据序列化的开发者提供 protobuf-3.8.0 的完整使用案例。内容围绕 .proto 文件定义、protoc 编译器调用、生成 C 源码集成以及序列化与反序列化代码编写展开帮助读者掌握跨平台数据交换与高效网络传输的实现方式。压缩包共 688 个文件以 293 个 cc 源文件、274 个 h 头文件、71 个 proto 定义文件为主另含 lib 库文件、exe 可执行程序、pdb 调试符号及 sln、vcxproj 工程文件整体约 4.72MB目录结构完整便于直接编译验证。目前已有 1818 人学习下载。通过该案例读者可获取可运行的工程模板、proto 定义示例与序列化反序列化代码快速理解 protobuf 在 VS2019 中的集成流程并借助相对路径配置提升项目可移植性适合需要落地数据存储与网络通信场景的中级 C 开发者参考。1. protobuf-3.8.0 配 VS2019为什么老版本组合反而更省心如果你手上有一个 VS2019 的 C 工程需要跟后端或其他进程交换结构化数据又不想手写一堆 JSON 解析代码那 protobuf 基本是绕不开的选择。protobuf-3.8.0 是一个在 Windows VS2019 环境下被大量项目验证过的版本它的 C 代码生成器、静态库和头文件都能直接用 MSVC 编译不需要额外折腾 CMake 工具链。这篇文章讲的就是怎么在 VS2019 里把 protobuf-3.8.0 从源码编译成可用的 lib怎么写出第一个能跑通的序列化/反序列化案例以及中间那些让人翻车的参数和路径问题。适合谁看正在用 C 做桌面端、服务端或嵌入式通信需要一套稳定 IDL 方案的人被 JSON 性能或字段兼容性折磨过的人以及第一次在 Windows 上编译第三方 C 库、对 VS 工程配置还不够熟的人。整条路径我会按“编译库 → 写 proto → 生成代码 → 接入工程 → 排错”的顺序走每一步都给可复制的命令和配置。2. 把 protobuf-3.8.0 编译成 VS2019 能用的静态库2.1 为什么选 3.8.0 而不是最新版protobuf 的版本迭代很快但 3.8.0 在 Windows 上有几个实际优势。第一它自带cmake构建脚本对 VS2019 的生成器支持已经稳定不像更早的版本需要手动改工程文件。第二3.8.0 的 C 运行时依赖较少编译出来的libprotobuf.lib和libprotoc.lib可以直接静态链接进你的 exe不需要额外分发 DLL。第三很多存量项目锁定了这个版本你如果跟已有系统对接用相同版本能避免 wire format 的细微差异。常见做法是下载protobuf-cpp-3.8.0.zip和protoc-3.8.0-win64.zip。前者是源码后者是预编译的 protoc 可执行文件。如果你只想快速生成代码可以只用预编译的 protoc但如果你要自己编译 lib 来链接就必须下源码。2.2 用 CMake 生成 VS2019 解决方案先确认你装了 CMake 3.14 以上和 VS2019 的“使用 C 的桌面开发”工作负载。解压源码后在源码目录旁边建一个build文件夹然后执行cd protobuf-3.8.0 mkdir build cd build cmake -G Visual Studio 16 2019 -A x64 ^ -Dprotobuf_BUILD_TESTSOFF ^ -Dprotobuf_BUILD_SHARED_LIBSOFF ^ -DCMAKE_INSTALL_PREFIX../install ^ ../cmake这里几个参数值得说明。-G Visual Studio 16 2019指定生成 VS2019 的解决方案-A x64指定 64 位平台如果你要 32 位就改成Win32。protobuf_BUILD_TESTSOFF关掉测试用例能省掉大量编译时间也避免测试代码里的 gtest 依赖问题。protobuf_BUILD_SHARED_LIBSOFF表示编译静态库这样你的最终程序不依赖libprotobuf.dll。CMAKE_INSTALL_PREFIX指定安装目录后面INSTALL目标会把头文件、lib 和 protoc 都复制过去。执行完 cmake 后build 目录里会出现protobuf.sln。用 VS2019 打开把配置切到Release和x64然后生成ALL_BUILD再生成INSTALL。INSTALL 完成后install目录下会有include、lib、bin三个文件夹。2.3 验证编译产物是否完整编译成功后检查install/lib下是否有libprotobuf.lib、libprotoc.lib、libprotobuf-lite.lib。install/bin下应该有protoc.exe。如果缺少libprotoc.lib说明你只生成了libprotobuf目标需要把解决方案里的libprotoc项目也编译一遍。提示如果你在 cmake 阶段遇到 “Could NOT find ZLIB” 之类的报错可以在 cmake 命令里加-Dprotobuf_WITH_ZLIBOFFprotobuf 对 zlib 的依赖是可选的关掉不影响基本序列化功能。3. 写第一个 proto 文件并生成 C 代码3.1 proto3 语法的最小可用示例新建一个addressbook.proto内容如下syntax proto3; package tutorial; message Person { string name 1; int32 id 2; string email 3; enum PhoneType { MOBILE 0; HOME 1; WORK 2; } message PhoneNumber { string number 1; PhoneType type 2; } repeated PhoneNumber phones 4; } message AddressBook { repeated Person people 1; }这个例子覆盖了 protobuf 最常用的几个特性标量字段、嵌套消息、枚举、repeated 字段。字段后面的数字是字段编号一旦发布就不能随意改动否则会导致解析错乱。syntax proto3表示用 proto3 语法它的字段默认值不再区分“未设置”和“设置为默认值”这一点跟 proto2 不同后面排错时会提到。3.2 用 protoc 生成 .pb.h 和 .pb.cc假设你把protoc.exe放在install/binproto 文件放在proto/目录执行install\bin\protoc.exe --proto_pathproto ^ --cpp_outgenerated ^ proto\addressbook.proto--proto_path是 proto 文件的搜索根目录--cpp_out指定 C 代码输出目录。执行成功后generated目录下会出现addressbook.pb.h和addressbook.pb.cc。这两个文件就是你要加入 VS 工程的源文件。如果你在 proto 里用了import其他 proto 文件--proto_path要包含被导入文件所在的根目录否则 protoc 会报 “File not found”。另外--cpp_out的目录必须提前存在protoc 不会自动创建。3.3 生成代码里几个关键接口打开addressbook.pb.h你会看到Person类有set_name()、name()、mutable_phones()、add_phones()这些方法。set_用于赋值不带set_的同名方法用于读取。对于repeated字段用add_追加元素用_size()获取数量。嵌套消息用mutable_获取指针后修改。序列化用SerializeToString(std::string*)反序列化用ParseFromString(const std::string)。这两个方法返回bool失败时不会抛异常所以必须检查返回值。很多人第一次用的时候直接忽略返回值结果数据没解析出来还找不到原因。4. 在 VS2019 工程里接入 protobuf 并跑通读写4.1 新建控制台工程并配置头文件路径在 VS2019 里新建一个 C 控制台应用然后把addressbook.pb.h、addressbook.pb.cc和install/include下的所有头文件加入工程。右键项目 → 属性 → C/C → 常规 → 附加包含目录添加install/includegenerated你的 .pb.h 所在目录如果你把 proto 生成的文件放在子目录也要把那个子目录加进去。注意不要只加install/include/google/protobuf要加到include这一层因为代码里引用的是#include google/protobuf/...。4.2 链接 libprotobuf.lib 和运行时库设置在项目属性 → 链接器 → 常规 → 附加库目录添加install/lib。然后在链接器 → 输入 → 附加依赖项里加上libprotobuf.lib。如果你用了libprotoc里的功能比如动态编译 proto再加libprotoc.lib。关键一步C/C → 代码生成 → 运行时库必须和编译 protobuf 时一致。前面我们用 Release 编译的 protobuf默认是Multi-threaded DLL (/MD)。如果你的工程用了/MDdDebug 版链接时会报LNK2038或RuntimeLibrary不匹配。解决办法是 Debug 配置也去编译一份 Debug 版的 protobuf或者把你的工程切成 Release。4.3 完整读写案例代码#include iostream #include fstream #include string #include addressbook.pb.h int main() { // 构造一个 AddressBook 并写入数据 tutorial::AddressBook book; tutorial::Person* person book.add_people(); person-set_name(Zhang San); person-set_id(1001); person-set_email(zhangsanexample.com); tutorial::Person::PhoneNumber* phone person-add_phones(); phone-set_number(13800000000); phone-set_type(tutorial::Person::MOBILE); // 序列化到字符串 std::string buffer; if (!book.SerializeToString(buffer)) { std::cerr Serialize failed std::endl; return -1; } std::cout Serialized size: buffer.size() std::endl; // 反序列化 tutorial::AddressBook book2; if (!book2.ParseFromString(buffer)) { std::cerr Parse failed std::endl; return -1; } for (int i 0; i book2.people_size(); i) { const tutorial::Person p book2.people(i); std::cout Name: p.name() , ID: p.id() , Email: p.email() std::endl; for (int j 0; j p.phones_size(); j) { const tutorial::Person::PhoneNumber ph p.phones(j); std::cout Phone: ph.number() type: ph.type() std::endl; } } return 0; }这段代码的逻辑很直接先构造对象并填充字段然后SerializeToString把二进制数据写入std::string再用另一个对象ParseFromString还原。注意add_people()返回的是指针people(i)返回的是 const 引用这是 protobuf 生成代码的固定风格。phones_size()和people_size()是 repeated 字段的数量接口。参数方面SerializeToString接受std::string*也支持SerializeToArray写入预分配缓冲区。如果你要写入文件可以用SerializeToOstream。反序列化时ParseFromString要求字符串是完整的二进制数据不能有截断。4.4 编译运行与常见链接错误按上面的配置编译如果一切正常控制台会输出序列化后的字节数和解析出来的字段。如果遇到LNK2019 unresolved external symbol先检查附加依赖项里有没有漏掉libprotobuf.lib再检查平台是 x64 还是 Win32 是否和 lib 一致。如果遇到LNK2038 mismatch detected for RuntimeLibrary就是运行时库不匹配按 4.2 的方法统一。还有一个容易忽略的点addressbook.pb.cc必须加入工程编译不能只包含头文件。很多人只把.pb.h拖进项目结果链接时找不到Person类的实现。5. 避坑与排查protobuf 在 VS2019 下的 5 个血泪教训5.1 编译 protobuf 时 LNK1104 找不到 libprotobuf.lib现象生成INSTALL项目时提示无法打开libprotobuf.lib。原因通常是ALL_BUILD没有完全成功或者你只生成了libprotobuf项目但没生成libprotoc。解决方法是先单独生成libprotobuf项目再生成libprotoc最后生成INSTALL。如果还是不行检查build目录下是否有Debug和Release混用的情况把解决方案配置切到 Release 重新生成。5.2 运行时库不匹配导致 LNK2038现象链接阶段报RuntimeLibrary不匹配MT_StaticRelease 和 MD_DynamicRelease 冲突。原因是 protobuf 编译时用了/MD你的工程用了/MT或者 Debug/Release 混用。解决办法是在项目属性 → C/C → 代码生成 → 运行时库里把工程改成和 protobuf 一致的选项。如果你必须用/MT就重新编译一份/MT的 protobuf。5.3 proto 字段编号改动导致解析出空值现象反序列化后字段全是默认值或者部分字段丢失。原因是 proto 文件里字段编号被改过或者两端用的 proto 文件版本不一致。protobuf 的 wire format 依赖字段编号编号变了旧数据就解析不到。解决方法是把字段编号当成数据库 schema 一样管理新增字段用新编号废弃字段保留编号并标记reserved。5.4 忘记检查 ParseFromString 返回值现象程序不崩溃但读出来的数据不对。原因是ParseFromString返回false时没有处理对象处于部分解析状态。protobuf 不会因为解析失败就清空对象所以你会看到一些残留数据。解决办法是每次调用ParseFromString都判断返回值失败时记录日志并丢弃该对象。5.5 Debug 版 protobuf 和 Release 版混用现象Debug 工程链接 Release 版 lib程序运行到序列化时随机崩溃。原因是 protobuf 内部有assert和迭代器调试Debug 和 Release 的 STL 布局不同。解决办法是 Debug 工程链接 Debug 版 protobufRelease 工程链接 Release 版。如果你只编译了一份 Release就在 VS 里把工程切到 Release 再调试。6. 进阶用 protobuf 做配置文件读写和版本兼容验证把 protobuf 当配置文件格式用比 JSON 多几个实际好处二进制体积小、解析快、字段类型强约束。我一般会定义一个Configmessage然后用SerializeToOstream写文件用ParseFromIstream读文件。下面是一个可复用的读写封装bool SaveConfig(const std::string path, const tutorial::AddressBook book) { std::ofstream output(path, std::ios::binary); if (!output) return false; return book.SerializeToOstream(output); } bool LoadConfig(const std::string path, tutorial::AddressBook* book) { std::ifstream input(path, std::ios::binary); if (!input) return false; return book-ParseFromIstream(input); }注意文件流必须用std::ios::binary否则 Windows 下会把\n转成\r\n破坏二进制数据。这是我在 Windows 上踩过的最隐蔽的坑之一文本模式读写二进制数据序列化出来的文件在 Linux 上解析直接失败。版本兼容验证可以这样做先写一个只包含name和id的旧版 proto生成代码并序列化一份数据。然后修改 proto新增email字段用新版代码反序列化旧数据。如果email为空但name和id正常说明向前兼容成立。反过来用旧版代码解析新版数据未知字段会被保留在UnknownFieldSet里不会导致解析失败。这个验证流程建议在 CI 里跑一遍避免字段编号冲突。我自己的习惯是每个 proto 文件头部写清楚字段编号分配表新增字段前先查表绝不复用已删除的编号。这个习惯帮我省掉了至少两次线上数据解析事故。希望帮到你。本文还有配套的精品资源点击获取
返回列表