简介:这份资源是 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++支持 C++11,make、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="-std=c++11 -O2" # 编译并安装 make -j$(nproc) make install--disable-shared表示只编静态库,适合把 protobuf 嵌进自己的可执行文件,避免部署时还要带.so。--prefix指定安装目录,后面在 CMake 里找头文件和库时要用到。CXXFLAGS里显式加-std=c++11,因为 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_PREFIX=C:/protobuf-3.5.1 \ -Dprotobuf_BUILD_TESTS=OFF \ -Dprotobuf_MSVC_STATIC_RUNTIME=OFF \ ../cmakeprotobuf_BUILD_TESTS=OFF可以跳过测试用例,节省编译时间。protobuf_MSVC_STATIC_RUNTIME要和主工程的运行时库设置保持一致,否则链接时会报LNK2038不匹配。生成后用 VS 打开protobuf.sln,编译INSTALL目标,头文件和库会输出到CMAKE_INSTALL_PREFIX指定的目录。
注意:Windows 下如果主工程用
/MD,protobuf 也要用/MD;如果主工程用/MT,protobuf 的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("alice@example.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++ -std=c++11 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没配全。按Ctrl+Shift+P,输入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": "c++11", "intelliSenseMode": "linux-gcc-x64" } ], "version": 4 }cppStandard设为c++11,和编译时保持一致。如果生成代码在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_iterator<char>(input)), std::istreambuf_iterator<char>()); 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_cast<std::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_cast<std::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 方案对比。如果字节数没有明显下降,或者解析耗时反而更高,就要重新评估是不是值得换。这个习惯帮我避免过好几次“为了用而用”的翻车。希望帮到你。
本文还有配套的精品资源,点击获取