1. 为什么我们需要Protobuf?从“语言不通”到“高效沟通”
如果你写过网络程序,或者做过需要保存数据的应用,肯定遇到过数据交换的难题。比如,你用C++写了一个服务端,用Python写了一个客户端,它们之间怎么传数据?最简单的办法可能是用字符串,比如拼成"name:张三,age:25"这种格式。但这种方式太脆弱了,字段一多、结构一变,解析起来就是一场灾难。再高级一点,用JSON或者XML。这俩确实是通用语言,人也能看懂,但问题也来了:体积大、解析慢。在网络传输和存储场景下,每一字节、每一毫秒都很珍贵。
这时候,Protobuf(Protocol Buffers)就该登场了。你可以把它想象成一种为机器设计的、极度高效的“摩斯电码”。它不像JSON那样把字段名"name"、"age"明文打出来,而是用数字编号来代表每个字段。传输和存储时,只传编号和对应的值,数据自然就小了很多。而且它的编码格式是二进制的,计算机解析起来飞快。我最早接触Protobuf是在一个实时对战游戏的后台项目里,当时从JSON切换到Protobuf,网络包大小直接减少了60%以上,服务器CPU压力肉眼可见地降了下来,那种性能提升带来的爽感,至今记忆犹新。
Protobuf的另一个核心优势是“契约先行”。你需要先定义一个.proto文件,在里面像写结构体一样,明确规定好数据的格式(有哪些字段,叫什么名字,是什么类型)。这个.proto文件就是你和所有参与方(不同服务、不同语言)签的一份“数据合同”。之后,无论你用C++、Go、Java还是Python,都可以用Protobuf提供的编译器,把这份合同翻译成你自己语言的代码。这样一来,大家读写数据都遵循同一套规则,彻底杜绝了“鸡同鸭讲”的问题。这种强制的、清晰的接口定义,对于团队协作和长期维护来说,价值巨大。
2. 手把手准备:Linux下的编译环境搭建
万事开头难,但把环境准备好,后面就一马平川了。我强烈建议你在自己的Linux虚拟机或者云服务器上跟着操作一遍,光看是记不住的。咱们的目标是把Protobuf的C++版本从源代码编译成我们自己的工具和库。
2.1 安装必需的“建筑材料”
编译软件就像盖房子,需要先把各种工具和材料备齐。在Ubuntu或者Debian这类系统上,打开你的终端,输入下面这条命令,把编译需要的工具链和库都装好:
sudo apt-get update
sudo apt-get install autoconf automake libtool curl make g++ unzip -y
我来简单说说这几个工具是干嘛的:
- autoconf, automake, libtool:这“三剑客”是GNU家族里用来生成跨平台编译脚本(
configure)的老将。很多开源软件都用它们来适应不同的Unix-like系统。 - curl:用来从网络下载文件,我们可能会用到它来抓取源码包。
- make, g++:这是核心的编译工具。
make负责指挥整个构建流程,g++就是C++编译器本人。 - unzip:解压工具,以防源码包是zip格式。
运行命令后,静静等待安装完成。如果系统提示你确认,输入y回车就行。这一步通常很顺利,如果遇到网络问题,可以换个软件源试试。
2.2 获取Protobuf的“源代码图纸”
有两种主流方式可以拿到Protobuf的源代码。第一种是下载官方打包好的稳定版压缩包,这种方式最直接,适合新手和追求稳定的生产环境。我们以protobuf-cpp-3.9.1.tar.gz这个版本为例(虽然现在有更新的21.x、22.x版本,但3.9.1版本非常经典稳定,很多老项目还在用,学习原理完全足够)。
你可以用浏览器访问Protobuf在GitHub的发布页面,找到对应版本下载。也可以在终端里直接用wget命令下载:
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.9.1/protobuf-cpp-3.9.1.tar.gz
下载完成后,用tar命令解压这个“压缩包”:
tar -xzvf protobuf-cpp-3.9.1.tar.gz
解压后会得到一个protobuf-3.9.1的目录,这就是我们的源代码目录。
第二种方式是通过git克隆整个仓库。这种方式能拿到最新的代码(包括可能还没发版的特性),适合想尝鲜或为社区做贡献的开发者。命令如下:
git clone https://github.com/protocolbuffers/protobuf.git
cd protobuf
git submodule update --init --recursive
注意,克隆仓库后,一定要执行git submodule update --init --recursive这条命令。因为Protobuf项目引用了一些子模块(比如测试用的gtest),这条命令就是把这些子模块的代码也拉取下来,否则后续编译测试时会出错。这是我曾经踩过的一个坑,编译一切顺利,但make check时死活过不去,排查了半天才发现是子模块没初始化。
3. 编译三部曲:配置、构建与安装
源码到手,工具就位,接下来就是施展“魔法”,把源代码变成可执行文件和库了。这个过程是标准的GNU软件安装流程,理解后对安装其他开源软件也大有裨益。
3.1 配置(Configure):告诉系统你的“建房蓝图”
首先,进入我们解压后的源代码目录:
cd protobuf-3.9.1
接下来运行一个非常重要的脚本:./configure。这个脚本会检查你的系统环境,比如编译器在哪儿、依赖库齐不齐,然后根据检查结果生成一个量身定制的Makefile文件。Makefile可以理解为一份详细的“施工图纸”,后面make命令就靠它来指挥编译。
我们可以通过一个参数来指定Protobuf的安装位置,我习惯把它安装到/usr/local/protobuf目录下。/usr/local是系统级软件安装的常用位置,独立于此,便于管理。
./configure --prefix=/usr/local/protobuf
运行这个命令后,终端会刷出一大堆检查信息。你只要关注最后几行,如果没有出现明显的error,一般就说明配置成功了。如果遇到报错,最常见的原因是前面“安装依赖”那一步有遗漏,根据错误提示安装对应的开发包即可(通常名字里会带-dev,比如libssl-dev)。
3.2 编译与测试(Make & Check):开始“施工”并“验收”
配置成功,生成了Makefile,现在可以开始编译了。输入一个简单的命令:
make
这个阶段会比较耗时,具体时间取决于你的电脑CPU性能。你会看到屏幕上不断滚动编译信息,各种.cc文件被编译成.o目标文件,最后链接成库和可执行文件。这是整个过程中最“吃资源”的一步,耐心等待它完成。
编译完成后,强烈建议运行测试套件,这是验证我们编译产物是否健康的关键一步:
make check
测试过程会运行大量的单元测试和集成测试。如果所有测试都通过,你会看到类似PASS: ...的总结信息,最后提示All tests passed。这就像房子盖好后进行的质量验收,确保每一块“砖瓦”都结实可靠。我第一次编译时跳过了测试,结果后来在链接自己项目的时候遇到了奇怪的符号错误,回头重跑测试才发现是编译环境不纯导致某个库没编好。所以,这个时间值得花。
3.3 安装(Install):将成果“搬入新家”
测试通过,万事大吉。现在可以把编译好的文件安装到我们之前指定的/usr/local/protobuf目录了:
sudo make install
这里需要sudo权限,因为要向系统级的/usr/local目录写入文件。这条命令会把可执行文件protoc(Protobuf编译器)复制到/usr/local/protobuf/bin,把头文件(.h)复制到/usr/local/protobuf/include,把库文件(.so或.a)复制到/usr/local/protobuf/lib。
安装完成后,你可以立刻尝试一下最重要的工具——编译器protoc:
/usr/local/protobuf/bin/protoc --version
如果输出类似libprotoc 3.9.1的版本信息,恭喜你,核心工具安装成功了!
4. 让系统找到它:配置环境变量
安装好了,但如果你直接在终端里输入protoc,系统大概率会告诉你“命令未找到”。这是因为系统只在固定的几个路径(比如/usr/bin)里寻找命令,而我们安装的protoc在/usr/local/protobuf/bin里。我们需要通过设置环境变量,把这个新路径告诉系统。
4.1 永久生效的配置方法
为了让每次打开终端都能使用protoc,并且让编译器能找到Protobuf的库,我们需要修改shell的配置文件。对于所有用户生效的配置文件是/etc/profile(对于当前用户,也可以是~/.bashrc或~/.zshrc)。
使用vim或你喜欢的文本编辑器(如nano)打开它:
sudo vim /etc/profile
在文件的末尾,添加以下几行:
# Protobuf Environment Settings
export PATH=$PATH:/usr/local/protobuf/bin
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/protobuf/lib
export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:/usr/local/protobuf/lib/pkgconfig
我来解释一下这三个变量:
- PATH:系统查找可执行文件的路径列表。我们把
protoc所在的bin目录加进去,以后在任意位置都能直接敲protoc命令了。 - LD_LIBRARY_PATH:系统在运行时查找动态链接库(
.so文件)的路径。把我们安装的库目录加进去,运行依赖Protobuf库的程序时,系统才能找到正确的库文件。这是解决运行时cannot open shared object file错误的关键。 - PKG_CONFIG_PATH:
pkg-config工具用来查找库的编译和链接参数的路径。这对于后续用g++编译你自己的C++项目时,能方便地获取到-I和-l参数。
保存文件并退出编辑器。然后,让这个配置文件立即生效:
source /etc/profile
现在,你再试试:
protoc --version
应该能直接看到版本号了,而不需要输入完整路径。这一步配置非常重要,我见过不少新手编译安装很顺利,但到了自己写程序链接时就各种报错,八成是环境变量没设对。
5. 实战演练:定义你的第一个数据协议并读写
理论说了这么多,不动手永远学不会。让我们来玩一个具体的例子:定义一个“用户信息”的数据结构,并用C++程序把它写到一个文件里,再读出来。
5.1 编写.proto文件——定义“数据合同”
首先,创建一个工作目录,比如protobuf_demo,然后在这个目录里创建我们的合同文件user.proto:
// user.proto
// 指定使用proto3的语法,这是目前主流的版本
syntax = "proto3";
// package相当于命名空间,用于防止消息类型名冲突
package tutorial;
// 定义一个“用户”消息类型
message User {
// 字段格式:规则 类型 字段名 = 唯一的数字标签;
string name = 1; // 用户名,标签为1
int32 id = 2; // 用户ID,标签为2
string email = 3; // 邮箱,标签为3
// 枚举类型:定义用户状态
enum UserStatus {
OFFLINE = 0; // 枚举值必须从0开始
ONLINE = 1;
BUSY = 2;
}
UserStatus status = 4; // 使用枚举
// 重复字段(相当于列表或数组)
repeated string phone_numbers = 5;
}
这个文件定义了一个User类型,里面有字符串、整数、枚举和字符串列表。每个字段后面的=1、=2是字段标签,这是Protobuf编码的核心。它一旦定义,就尽量不要修改,因为它是字段在二进制流中的唯一标识。
5.2 使用protoc生成C++代码
合同写好了,现在用protoc这个“翻译官”,把它翻译成C++能直接用的类。在工作目录下执行:
protoc --cpp_out=. user.proto
--cpp_out=.:告诉protoc生成C++代码,并输出到当前目录(.)。user.proto:是我们的输入文件。
执行成功后,你会看到当前目录下生成了两个文件:user.pb.h和user.pb.cc。这两个文件包含了根据user.proto定义自动生成的C++类,类名就是tutorial::User。这个类提供了完整的接口,让你可以像操作普通C++对象一样,设置字段、读取字段,并且自带序列化和反序列化的方法。千万不要手动修改这两个生成的文件,因为只要.proto文件一变,它们就会被重新生成覆盖。
5.3 编写C++程序:写入与读取
接下来,我们写两个小程序:一个负责创建一个User对象并序列化到文件(writer.cc),另一个负责从文件读回来并打印内容(reader.cc)。
writer.cc - 数据写入程序
// writer.cc
#include <iostream>
#include <fstream>
#include "user.pb.h"
int main() {
// 1. 创建一个User对象
tutorial::User user;
user.set_name("张三");
user.set_id(2024001);
user.set_email("zhangsan@example.com");
user.set_status(tutorial::User::ONLINE); // 设置枚举值
// 添加重复字段(电话号码)
user.add_phone_numbers("13800138000");
user.add_phone_numbers("010-88886666");
// 2. 将对象序列化成二进制字符串
std::string serialized_data;
if (!user.SerializeToString(&serialized_data)) {
std::cerr << "Failed to serialize user data." << std::endl;
return -1;
}
// 3. 将二进制字符串写入文件
std::ofstream ofs("user_data.bin", std::ios::binary);
if (!ofs.write(serialized_data.c_str(), serialized_data.size())) {
std::cerr << "Failed to write to file." << std::endl;
return -1;
}
ofs.close();
std::cout << "User data has been written to 'user_data.bin'." << std::endl;
std::cout << "Original size: " << user.ByteSizeLong() << " bytes." << std::endl;
return 0;
}
reader.cc - 数据读取程序
// reader.cc
#include <iostream>
#include <fstream>
#include "user.pb.h"
int main() {
// 1. 从文件读取二进制数据
std::ifstream ifs("user_data.bin", std::ios::binary);
if (!ifs) {
std::cerr << "Failed to open file." << std::endl;
return -1;
}
// 获取文件大小
ifs.seekg(0, std::ios::end);
int file_size = ifs.tellg();
ifs.seekg(0, std::ios::beg);
char* buffer = new char[file_size];
ifs.read(buffer, file_size);
ifs.close();
std::string serialized_data(buffer, file_size);
delete[] buffer;
// 2. 反序列化二进制数据到User对象
tutorial::User user;
if (!user.ParseFromString(serialized_data)) {
std::cerr << "Failed to parse user data." << std::endl;
return -1;
}
// 3. 打印对象内容
std::cout << "=== Parsed User Info ===" << std::endl;
std::cout << "Name: " << user.name() << std::endl;
std::cout << "ID: " << user.id() << std::endl;
std::cout << "Email: " << user.email() << std::endl;
std::cout << "Status: " << user.status() << std::endl; // 输出枚举对应的整数值
std::cout << "Phone Numbers: ";
for (int i = 0; i < user.phone_numbers_size(); ++i) {
std::cout << user.phone_numbers(i) << " ";
}
std::cout << std::endl;
return 0;
}
5.4 编译并运行你的程序
最后一步,把我们的程序和Protobuf库链接起来。在终端里执行编译命令:
# 编译写入程序
g++ -std=c++11 writer.cc user.pb.cc -o writer -lprotobuf -pthread
# 编译读取程序
g++ -std=c++11 reader.cc user.pb.cc -o reader -lprotobuf -pthread
这里有几个关键点:
-std=c++11:指定使用C++11标准,Protobuf生成的代码需要这个或更高标准。writer.cc user.pb.cc:将我们写的源码和protoc生成的源码一起编译。-lprotobuf:链接Protobuf的动态库。这就是为什么之前要设置LD_LIBRARY_PATH,否则链接器会找不到这个库。-pthread:因为Protobuf库内部可能使用了线程,所以需要链接线程库。
编译成功后,你会得到writer和reader两个可执行文件。先运行写入程序:
./writer
你会看到它生成了一个user_data.bin文件,并打印出数据大小。用ls -lh看看,这个文件通常只有几十到几百字节,非常紧凑。然后运行读取程序:
./reader
如果一切正常,reader会完美地还原出之前写入的所有用户信息。这个过程就是Protobuf工作的核心:通过预先定义好的.proto契约,实现高效、准确的数据序列化与反序列化。你可以尝试修改writer.cc里的数据,或者给.proto文件增加新的字段(注意给新字段新的标签号),重新生成.pb文件并编译,体验一下协议扩展的流程。在实际项目中,正是这种清晰、可扩展的协议定义,让多服务、多语言团队之间的数据交互变得可靠而高效。
265



被折叠的 条评论
为什么被折叠?



