Protocol Buffers v3.5.1 C++实战:编译、代码生成与避坑指南

发布时间:2026/10/30 17:03:17
Protocol Buffers v3.5.1 C++实战:编译、代码生成与避坑指南 简介这份资源是 Google Protocol Buffers v3.5.1 的 C 版本库面向需要在 C 项目中实现高效数据序列化与跨平台通信的开发者尤其适合使用 VS2015 环境、从事网络传输、配置文件解析或数据存储等场景的中高级程序员。压缩包共 738 个文件约 17.66MB以 308 个 .cc 源文件、299 个 .h 头文件和 69 个 .proto 协议定义文件为主体同时包含 17 个 dll、12 个 lib 动态与静态库以及 7 个可执行文件覆盖 x86 与 x64 两套编译产物。资源内附一个 x86 下可直接编译运行的测试 Demo并配有 .sln 解决方案与 vcxproj 工程文件便于快速验证环境与接口调用。目前已有 614 人学习下载读者可借此省去从源码编译 protobuf 的繁琐过程直接获得可用的库文件与示例工程快速理解 proto 文件定义、消息序列化及反序列化的完整流程为后续集成到实际项目提供可靠参考。1. Protocol Buffers v3.5.1 C库为什么老项目还在用它做序列化如果你维护过一个跑了五六年的 C 后端服务大概率会在某个third_party目录里翻出一个protobuf-3.5.1的源码包。它不像 JSON 那样随手就能读也不像 FlatBuffers 那样主打零拷贝但很多团队在跨语言、跨进程的数据交换上依然把它当成默认选项。原因很直接schema 明确、生成代码稳定、C 侧性能可控而且 3.5.1 这个版本在不少老编译器和老构建系统上都能编过。这篇笔记围绕 Protocol Buffers v3.5.1 的 C 库展开讲清楚三件事怎么从源码编出可用的库、怎么用protoc生成 C 代码并接进自己的工程、以及在实际项目里最容易翻车的几个点。适合正在维护老 C 服务、需要给现有系统加一套序列化协议或者被要求“把 JSON 换成 protobuf”但不知道从哪下手的读者。如果你用的是 vscode 写 C后面也会提到c_cpp_properties.json里怎么配头文件路径避免函数跳转失效。2. 从源码到可用库v3.5.1 的编译与安装路径2.1 为什么这个版本还在被使用Protocol Buffers 在 3.x 系列里迭代了很多版本3.5.1 属于比较早的 v3 稳定版。它没有后来版本里一些新特性比如optional字段的显式 presence 追踪在 3.15 之后才有较大变化但它的优势是依赖少、构建脚本简单、对老版本 GCC 和 MSVC 比较友好。很多存量的 C 项目尤其是那些用 CMake 2.8 或者手写 Makefile 的工程直接升级到高版本 protobuf 会碰到 C 标准要求提高、abseil 依赖引入等问题所以干脆锁在 3.5.1。常见做法是把 protobuf 源码作为第三方库放进项目用./configure make或者 CMake 编出静态库然后链接到自己的 target 上。下面按 Linux 和 Windows 两条线分别说。2.2 Linux 下编译静态库的完整命令先确认工具链g支持 C11make、autoconf、automake、libtool都在。解压源码后进入目录执行# 进入 protobuf 源码根目录 cd protobuf-3.5.1 # 生成 configure 脚本如果源码包里已经带了可以跳过 ./autogen.sh # 配置安装路径和编译选项 ./configure --prefix/usr/local/protobuf-3.5.1 \ --disable-shared \ --enable-static \ CXXFLAGS-stdc11 -O2 # 编译并安装 make -j$(nproc) make install--disable-shared表示只编静态库适合把 protobuf 嵌进自己的可执行文件避免部署时还要带.so。--prefix指定安装目录后面在 CMake 里找头文件和库时要用到。CXXFLAGS里显式加-stdc11因为 3.5.1 的代码在部分老编译器上默认标准可能不够。编完后检查$prefix/lib下是否有libprotobuf.a、libprotoc.a$prefix/bin下是否有protoc。如果protoc能执行protoc --version并输出libprotoc 3.5.1说明编译成功。2.3 Windows 下用 CMake 生成 Visual Studio 工程Windows 上更常见的是用 CMake 生成.sln然后用 Visual Studio 编译。需要提前装好 CMake 和对应版本的 Visual Studio。在 protobuf 源码目录下新建build文件夹mkdir build cd build cmake -G Visual Studio 15 2017 Win64 \ -DCMAKE_INSTALL_PREFIXC:/protobuf-3.5.1 \ -Dprotobuf_BUILD_TESTSOFF \ -Dprotobuf_MSVC_STATIC_RUNTIMEOFF \ ../cmakeprotobuf_BUILD_TESTSOFF可以跳过测试用例节省编译时间。protobuf_MSVC_STATIC_RUNTIME要和主工程的运行时库设置保持一致否则链接时会报LNK2038不匹配。生成后用 VS 打开protobuf.sln编译INSTALL目标头文件和库会输出到CMAKE_INSTALL_PREFIX指定的目录。注意Windows 下如果主工程用/MDprotobuf 也要用/MD如果主工程用/MTprotobuf 的protobuf_MSVC_STATIC_RUNTIME要设为ON。这个不一致是链接错误里最常见的一类。2.4 把 protobuf 接进自己的 CMake 工程假设安装路径是/usr/local/protobuf-3.5.1自己的工程用 CMake可以这样写# 查找 protobuf 头文件和库 set(PROTOBUF_ROOT /usr/local/protobuf-3.5.1) find_package(Protobuf REQUIRED PATHS ${PROTOBUF_ROOT} NO_DEFAULT_PATH) include_directories(${PROTOBUF_INCLUDE_DIRS}) # 假设已经用 protoc 生成了 addressbook.pb.cc 和 addressbook.pb.h add_executable(demo main.cpp addressbook.pb.cc) target_link_libraries(demo ${PROTOBUF_LIBRARIES})find_package的PATHS加上NO_DEFAULT_PATH是为了强制用我们指定的 3.5.1而不是系统里可能存在的其他版本。PROTOBUF_LIBRARIES会包含libprotobuf如果用到protoc编译.proto的 CMake 宏还需要PROTOBUF_PROTOC_EXECUTABLE。如果不用find_package也可以直接写死路径target_include_directories(demo PRIVATE /usr/local/protobuf-3.5.1/include) target_link_libraries(demo /usr/local/protobuf-3.5.1/lib/libprotobuf.a)这种方式更直接但换机器时要改路径。老项目里常见做法是把 protobuf 源码放在third_party下用add_subdirectory引入不过 3.5.1 的 CMake 脚本对子目录方式支持一般容易和主工程的编译选项冲突所以更推荐预编译后链接。3. 用 protoc 生成 C 代码从 .proto 到可编译的类3.1 一个最小可用的 .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; }syntax proto3表示用 proto3 语法字段默认值不再有optional/required区分所有字段都是可选的。package会映射到 C 的命名空间。字段后面的数字是 tag一旦上线就不能随便改否则解析旧数据会出错。3.2 生成 C 代码的命令与参数用刚才编出来的protoc生成 C 文件/usr/local/protobuf-3.5.1/bin/protoc \ --proto_path. \ --cpp_out. \ addressbook.proto--proto_path指定.proto文件的搜索路径.表示当前目录。--cpp_out指定 C 代码输出目录。执行后会得到addressbook.pb.h和addressbook.pb.cc。如果.proto里 import 了其他文件--proto_path要包含那些文件所在的目录。生成的头文件里Person类会有name()、set_name()、id()、set_id()等方法repeated字段会有phones_size()、phones(int index)、add_phones()等接口。这些方法名是 protobuf 根据字段名自动生成的不要手动改生成的文件因为下次重新生成会被覆盖。3.3 在 C 里读写 protobuf 消息下面是一个完整的读写示例#include iostream #include fstream #include addressbook.pb.h int main() { // 构造一个 AddressBook 并填充数据 tutorial::AddressBook book; tutorial::Person* person book.add_people(); person-set_name(Alice); person-set_id(1001); person-set_email(aliceexample.com); tutorial::Person::PhoneNumber* phone person-add_phones(); phone-set_number(13800000000); phone-set_type(tutorial::Person::MOBILE); // 序列化到文件 std::fstream output(book.bin, std::ios::out | std::ios::trunc | std::ios::binary); if (!book.SerializeToOstream(output)) { std::cerr serialize failed std::endl; return -1; } output.close(); // 从文件反序列化 tutorial::AddressBook book2; std::fstream input(book.bin, std::ios::in | std::ios::binary); if (!book2.ParseFromIstream(input)) { 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) { std::cout phone p.phones(j).number() type p.phones(j).type() std::endl; } } return 0; }SerializeToOstream和ParseFromIstream是最常用的两个接口分别把消息写到流和从流里读。add_people()会追加一个Person并返回指针add_phones()同理。读取时用people_size()和people(i)遍历不要用people()返回的容器直接操作因为 protobuf 内部用的是RepeatedPtrField接口和标准容器不完全一样。编译这个示例g -stdc11 main.cpp addressbook.pb.cc \ -I/usr/local/protobuf-3.5.1/include \ -L/usr/local/protobuf-3.5.1/lib \ -lprotobuf -lpthread \ -o demo-lpthread在部分平台上需要因为 protobuf 内部用了线程相关的东西。如果链接时报undefined reference to pthread_create加上它就能解决。3.4 vscode 下配置头文件路径让跳转生效用 vscode 写 C 时如果#include addressbook.pb.h下面有红色波浪线或者函数跳转失效通常是c_cpp_properties.json里的includePath没配全。按CtrlShiftP输入C/C: Edit Configurations (JSON)在includePath里加上 protobuf 的头文件目录和生成代码所在目录{ configurations: [ { name: Linux, includePath: [ ${workspaceFolder}/**, /usr/local/protobuf-3.5.1/include ], defines: [], compilerPath: /usr/bin/g, cStandard: c11, cppStandard: c11, intelliSenseMode: linux-gcc-x64 } ], version: 4 }cppStandard设为c11和编译时保持一致。如果生成代码在build目录下也要把那个目录加进includePath。改完后重启 vscode 或者执行C/C: Reset IntelliSense Database跳转一般就能恢复。4. 避坑与排查v3.5.1 C 库最常见的五类问题4.1 链接时报 undefined reference to google::protobuf现象编译通过链接阶段报大量undefined reference to google::protobuf::...。原因链接顺序不对或者库路径没找到。protobuf 的静态库依赖顺序有要求-lprotobuf要放在使用它的目标文件后面。解决把-lprotobuf放到编译命令的最后确保在.o文件之后。如果用 CMake用target_link_libraries(demo ${PROTOBUF_LIBRARIES})让 CMake 处理顺序。另外确认libprotobuf.a确实在-L指定的目录里。4.2 运行时提示 “Protocol message tag had invalid wire type”现象ParseFromIstream或ParseFromString返回 false日志里出现 tag 或 wire type 错误。原因序列化和反序列化用的.proto不一致或者数据在传输过程中被截断、被文本模式读写破坏。解决确认两端用的是同一份.proto生成的代码。文件读写时用std::ios::binary不要用文本模式。如果数据来自网络检查是否完整收到了所有字节。可以在序列化后打印ByteSize()反序列化前确认缓冲区长度。4.3 Windows 下 LNK2038 或运行时库不匹配现象VS 链接时报LNK2038: 检测到“RuntimeLibrary”的不匹配项。原因protobuf 库和主工程用的运行时库不一致一个是/MD一个是/MT。解决统一运行时库设置。如果主工程用/MD编译 protobuf 时protobuf_MSVC_STATIC_RUNTIME设为OFF如果主工程用/MT设为ON。改完后重新编译 protobuf 和主工程。4.4 protoc 生成代码时提示 “File not found”现象执行protoc时报addressbook.proto: File not found或 import 的文件找不到。原因--proto_path没包含.proto文件所在目录或者 import 路径写的是相对路径但搜索路径不对。解决把.proto文件所在目录和所有被 import 的目录都加到--proto_path里。如果.proto里写的是import common/types.proto那么--proto_path要包含common的父目录。可以用多个--proto_path参数protoc 会按顺序查找。4.5 升级或混用不同版本 protobuf 导致解析失败现象用 3.5.1 生成的数据用更高版本 protobuf 的库去解析偶尔失败或字段丢失。原因不同版本对 proto3 的默认值处理、未知字段保留策略可能有差异。3.5.1 对未知字段的处理和后续版本不完全一致。解决生产环境尽量锁定同一版本。如果必须混用先在小范围做兼容性测试重点验证未知字段和默认值场景。升级时不要只换库生成代码也要用对应版本的protoc重新生成。5. 进阶技巧用 protobuf 做配置文件与性能验证5.1 把 protobuf 当配置文件格式用除了网络传输protobuf 也适合做配置文件尤其是配置结构复杂、需要跨语言读取的场景。常见做法是定义一个Config消息用文本格式写配置运行时用TextFormat解析#include google/protobuf/text_format.h #include fstream #include config.pb.h bool LoadConfig(const std::string path, tutorial::Config* config) { std::ifstream input(path); if (!input.is_open()) { return false; } std::string content((std::istreambuf_iteratorchar(input)), std::istreambuf_iteratorchar()); google::protobuf::TextFormat::Parser parser; return parser.ParseFromString(content, config); }对应的配置文件config.txt可以写成listen_port: 8080 worker_threads: 4 log_level: 2TextFormat解析的是 protobuf 的文本表示不是 JSON但可读性足够而且字段名和类型由.proto约束比裸 JSON 更安全。注意TextFormat在 3.5.1 里对未知字段默认会报错如果希望忽略未知字段可以设置parser.AllowUnknownField(true)。5.2 验证序列化性能的一个简单方法想知道 protobuf 在你自己的数据结构上到底快不快可以写一个简单的 benchmark对比序列化、反序列化的耗时和字节数#include chrono #include iostream #include addressbook.pb.h int main() { tutorial::AddressBook book; for (int i 0; i 10000; i) { tutorial::Person* p book.add_people(); p-set_name(user std::to_string(i)); p-set_id(i); p-set_email(user std::to_string(i) test.com); } // 序列化计时 std::string buffer; auto start std::chrono::high_resolution_clock::now(); book.SerializeToString(buffer); auto end std::chrono::high_resolution_clock::now(); auto serialize_us std::chrono::duration_caststd::chrono::microseconds(end - start).count(); // 反序列化计时 tutorial::AddressBook book2; start std::chrono::high_resolution_clock::now(); book2.ParseFromString(buffer); end std::chrono::high_resolution_clock::now(); auto parse_us std::chrono::duration_caststd::chrono::microseconds(end - start).count(); std::cout serialize: serialize_us us std::endl; std::cout parse: parse_us us std::endl; std::cout bytes: buffer.size() std::endl; return 0; }这个 benchmark 只测单线程、单次调用实际项目里要关注的是消息越大protobuf 的优势越明显小消息频繁序列化时开销主要在对象构造和内存分配上。如果发现反序列化比预期慢检查是不是每次都在重新构造大对象或者repeated字段增长时频繁触发扩容。我自己的习惯是在把 protobuf 引入生产之前先用真实数据跑一遍这个 benchmark记录字节数和耗时和现有的 JSON 方案对比。如果字节数没有明显下降或者解析耗时反而更高就要重新评估是不是值得换。这个习惯帮我避免过好几次“为了用而用”的翻车。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询