Protobuf:从入门到精通的学习笔记(含 3 个项目及避坑指南)

前言

Protocol Buffers(简称Protobuf)是Google开源的一种高效、跨语言、可扩展的结构化数据序列化格式,广泛应用于微服务通信、数据存储、RPC框架等场景。相比于JSON、XML,Protobuf具有体积更小、解析更快、兼容性更强的优势。本文将从环境配置、语法细节、实战项目到高级特性,全方位、超详细记录Protobuf的学习过程,结合3个递进式通讯录项目,帮助读者从零基础到熟练运用Protobuf。

一、踩坑记录:环境配置常见问题

在 Windows 和 Linux 环境中使用 Protobuf 时,难免遇到编译报错、环境变量失效等问题,这里整理了 2 个高频问题的解决方案:

问题 1:Windows 编译.proto 文件生成的.pb.h/.pb.cc 找不到头文件

核心原因

protoc编译工具能正常生成代码,但 C++ 编译器(MSVC/MinGW/Clang)找不到google/protobuf/message.h等头文件,本质是未配置编译器的 include 路径

解决方案
  • 找到 Protobuf 安装目录下的include文件夹(如D:\protobuf\include
  • 在编译器配置中添加 include 路径:
    • MSVC:项目属性 → C/C++ → 常规 → 附加包含目录
    • MinGW/Clang:编译命令中添加-I D:\protobuf\include

问题 2:Linux 修改 /etc/profile 后环境变量不自动生效

核心原因
  • /etc/profile仅在登录shell(login shell)启动时自动加载(如通过SSH登录、Ctrl+Alt+F1进入终端)

  • 图形界面终端(如GNOME Terminal、Konsole)默认启动的是非登录shell ,不会自动读取/etc/profile

解决方案

重新远程连接远端Linux机器。

二、Protobuf核心语法(Proto3)详解

2.1 基础语法结构

2.1.1 语法版本声明
复制代码
syntax = "proto3"; // 必须放在文件首行
  • 作用:明确指定使用Proto3语法

  • 注意:若不声明,编译器默认使用Proto2,可能导致语法兼容问题

2.1.2 包声明(Package)
复制代码
package contacts; // 命名空间
  • 作用:避免不同.proto文件中message名称冲突

  • 类比:C++的namespace、Java的package

  • 生成代码影响:

    • C++:生成的类会放在contacts命名空间中

    • Java:生成的类会放在contacts包下(可通过option java_package自定义)

2.1.3 Message定义(核心)

message是Protobuf中定义结构化数据的核心单元,类似C/C++的struct或类。

复制代码
message PeopleInfo {
  string name = 1;  // 字符串类型:姓名,字段编号1
  int32 age = 2;    // 32位整数:年龄,字段编号2
}

message PeopleInfo { string name = 1; // 字符串类型:姓名,字段编号1 int32 age = 2; // 32位整数:年龄,字段编号2 }

字段编号(Field Numbers)
  • 每个字段后的数字(=1=2)是字段编号(field tag),用于序列化后标识字段

  • 关键规则:

    • 必须为正整数(≥1)

    • 同一message中必须唯一

    • 保留范围:19000--19999 是Protobuf内部保留编号,禁止使用

    • 有效范围:1 ~ 2²⁹-1(536,870,911)

  • 为什么需要字段编号?

序列化后的二进制数据不包含字段名 ,仅通过字段编号识别字段,解析时依靠编号还原数据。因此,编号一旦分配不可随意修改(否则会导致旧数据无法解析)。

  • 优化建议:

    • 1--15:编码仅占1字节,适合高频使用或必填字段(如nameid

    • 16--2047:编码占2字节,适合低频字段或扩展字段

2.1.4 字段类型与默认值(Proto3特性)

Proto3中所有字段默认都是optional(可选),且自动具有固定默认值(不可自定义):

|--------------|---------------|----------|
| 字段类型 | 默认值 | 说明 |
| int32/int64 | 0 | 整数类型默认值 |
| float/double | 0.0 | 浮点数类型默认值 |
| bool | false | 布尔类型默认值 |
| string | ""(空字符串) | 字符串类型默认值 |
| bytes | 空字节串 | 字节类型默认值 |
| enum | 第一个值(必须为0) | 枚举类型默认值 |
| message | 空对象(所有字段为默认值) | 嵌套消息默认值 |

  • 注意:无法区分"字段未设置"和"字段值为默认值"(如age=0可能是未设置,也可能是实际年龄为0)。若需明确判断,可使用google.protobuf.StringValue等包装类型(Proto3.15+支持optional关键字开启presence tracking)。

2.2 高级语法特性

2.2.1 重复字段(repeated)

表示字段可包含多个值,类似数组或std::vector

复制代码
message PeopleInfo {
  string name = 1;
  int32 age = 2;
  repeated string emails = 3;  // 多个邮箱
}

message PeopleInfo { string name = 1; int32 age = 2; repeated string emails = 3; // 多个邮箱 }

  • 生成C++代码后,会自动生成add_emails()(添加元素)、emails_size()(获取长度)、emails(i)(获取第i个元素)等方法。
2.2.2 枚举类型(enum)

用于定义一组命名的整数常量,适用于字段值有限的场景:

复制代码
message PeopleInfo {
  string name = 1;
  int32 age = 2;
  // 枚举类型:电话类型
  enum PhoneType {
    UNSPECIFIED = 0;  // 第一个值必须为0
    MP = 1;           // 移动电话
    TEL = 2;          // 固定电话
  }
  PhoneType phone_type = 3;
}
  • 注意:解析到未定义的枚举值时,Proto3会保留该数值,但不会映射到枚举名。

  • C++使用示例:

    PeopleInfo person;
    person.set_phone_type(PeopleInfo::MP);
    if (person.phone_type() == PeopleInfo::MP) {
    cout << "移动电话" << endl;
    }

2.2.3 嵌套消息(Nested Message)

message内部可定义另一个message,适用于复杂数据结构:

复制代码
message PeopleInfo {
  string name = 1;
  int32 age = 2;
  // 嵌套消息:电话信息
  message Phone {
    string number = 1;  // 电话号码
    enum PhoneType {
      MP = 0;
      TEL = 1;
    }
    PhoneType type = 2; // 电话类型
  }
  repeated Phone phones = 3;  // 多个电话
}
  • 生成C++代码后,嵌套消息会被命名为PeopleInfo_Phone,枚举类型为PeopleInfo_Phone_PhoneType
2.2.4 Any类型

google.protobuf.Any可存储任意类型的消息(类似void*但更安全),需导入google/protobuf/any.proto

复制代码
import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;  // 错误描述
  repeated google.protobuf.Any details = 2;  // 任意类型的错误详情
}
  • C++使用示例:

    #include <google/protobuf/any.pb.h>

    // 创建具体错误信息
    NetworkError net_err;
    net_err.set_code(404);
    net_err.set_url("http://example.com");

    // 打包到Any
    google::protobuf::Any any;
    any.PackFrom(net_err);

    // 放入ErrorStatus
    ErrorStatus status;
    status.set_message("请求失败");
    *status.add_details() = any;

    // 解包
    const auto& detail = status.details(0);
    if (detail.Is<NetworkError>()) {
    NetworkError unpacked;
    detail.UnpackTo(&unpacked);
    cout << "错误码:" << unpacked.code() << endl;
    }

2.2.5 oneof类型

表示多个字段中最多只能设置一个(互斥字段),节省存储空间:

复制代码
message SampleMessage {
  oneof test_oneof {
    string name = 1;
    int32 id = 2;
    bool active = 3;
  }
}
  • C++使用示例:

    SampleMessage msg;
    msg.set_name("Bob"); // 设置name
    // msg.set_id(100); // 会自动清除name
    if (msg.has_name()) {
    cout << "Name: " << msg.name() << endl;
    }

2.2.6 map类型

用于表示键值对集合,类似std::map

复制代码
message CountryMap {
  map<string, string> country_code_to_name = 1;  // 国家代码→国家名称
}
  • 规则:

    • 键类型不能是浮点数、bytes或message

    • 值类型可以是除map外的任意类型

  • C++使用示例:

    CountryMap cmap;
    (*cmap.mutable_country_code_to_name())["CN"] = "中国";
    (*cmap.mutable_country_code_to_name())["US"] = "美国";

    // 遍历
    for (const auto& kv : cmap.country_code_to_name()) {
    cout << kv.first << " → " << kv.second << endl;
    }

2.3 兼容性更新规则

设计.proto文件时,需保证后续更新的兼容性(旧程序能解析新数据,新程序能解析旧数据),核心规则如下:

  1. 不要修改已有字段的编号(tag number)

  2. 不要重复使用已删除字段的编号

  3. 新增字段必须是optional或repeated(Proto3中所有字段默认满足)

  4. 删除字段时,需用reserved声明保留其编号和名称,避免未来误用

reserved关键字(关键!)

用于保留已删除字段的编号和名称,防止团队协作中误用导致兼容性问题:

复制代码
// 原始版本
message Person {
  string name = 1;
  int32 age = 2;  // 后续要删除的字段
  string email = 3;
}

// 更新后版本
message Person {
  reserved 2;                // 保留已删除字段的编号
  reserved "age";            // 保留已删除字段的名称
  string name = 1;
  string email = 3;
  string phone = 4;          // 新增字段
}
  • 规则:

    • 可保留连续范围(如reserved 9 to 11表示9、10、11)

    • 编号和名称的reserved必须分开写(不能混在同一行)

    • 保留的编号/名称不能再用于定义新字段

  • 编译检查:若尝试使用保留的编号或名称,protoc会直接报错:

    Field number 2 has already been used for 'reserved'

2.4 option选项

option用于配置生成代码的行为或添加元信息,常见选项如下:

复制代码
syntax = "proto3";

// 文件级选项:指定Java包名
option java_package = "com.example.contacts";
// 文件级选项:优化方式(SPEED/CODE_SIZE/LITE_RUNTIME)
option optimize_for = SPEED;

message PeopleInfo {
  // 字段选项:标记字段已废弃
  string name = 1 [deprecated = true];
  // 字段选项:自定义JSON字段名
  int32 id = 2 [json_name = "user_id"];
}

// 消息选项:标记整个message已废弃
message OldMessage {
  option deprecated = true;
  string data = 1;
}
  • C++相关常用选项:

    • option cc_enable_arenas = true;:启用Arena内存分配(提升高频创建/销毁消息的性能)

    • option optimize_for = CODE_SIZE;:减小生成代码体积(牺牲部分速度)

三、Protobuf编译命令详解

protoc是Protobuf的命令行编译工具,用于将.proto文件编译为指定语言的源代码(C++、Python、Java等)。

3.1 基本语法

复制代码
protoc --cpp_out=DST_DIR path/to/file.proto

3.2 参数说明

|----------------------|----|--------------------------------------------|
| 参数 | 全称 | 说明 |
| --cpp_out=DST_DIR | - | 指定生成C++代码的输出目录,生成xxx.pb.hxxx.pb.cc文件。 |
| path/to/file.proto | - | 要编译的.proto文件路径(相对路径或绝对路径)。 |

3.3 示例

示例1:编译当前目录的.proto文件
复制代码
# 生成C++代码到当前目录
protoc --cpp_out=. contacts.proto
示例2:引用其他目录的.proto文件

假设contacts.proto引用了./protos/common.proto,编译命令如下:

复制代码
# -I 指定搜索路径为./protos,--cpp_out指定输出目录为./src
protoc -I ./protos --cpp_out=./src contacts.proto
示例3:批量编译多个.proto文件
复制代码
# 编译./protos目录下所有.proto文件,生成C++代码到./cpp_src
protoc -I ./protos --cpp_out=./cpp_src ./protos/*.proto

四、实战项目:3个版本通讯录深度解析

4.1 1.0版本:基础入门------序列化与反序列化

4.1.1 项目目标
  • 掌握Protobuf基本语法

  • 实现单个联系人信息的序列化(对象→二进制)与反序列化(二进制→对象)

  • 打印序列化结果和反序列化后的联系人信息

4.1.2 核心需求
  • 联系人包含:姓名(name)、年龄(age)

  • 序列化:将联系人对象转换为二进制字节序列并打印

  • 反序列化:将二进制字节序列转换为联系人对象并打印

4.1.3 实现步骤
步骤1:编写.proto文件(contacts.proto)
复制代码
syntax = "proto3";
package contacts;

// 联系人信息
message PeopleInfo {
  string name = 1;  // 姓名
  int32 age = 2;    // 年龄
}
步骤2:编译.proto文件
复制代码
protoc --cpp_out=. contacts.proto

生成contacts.pb.hcontacts.pb.cc文件。

步骤3:编写C++代码([main.cc](main.cc))
复制代码
#include<iostream>
#include"contacts.pb.h"

int main()
{ 
    std::string person_str;
    //用匿名的命名空间隔开
    {
        //对一个联系人的信息使用PB进行序列化,并将结果打印出来
        contacts::PeopleInfo person;
        person.set_name("张三");
        person.set_age(20);

        if(!person.SerializeToString(&person_str))
        {
            std::cout<<"序列化失败"<<std::endl;
            return -1;
        }
        std::cout<<"序列化成功,结果"<<person_str<<std::endl;
    }
    {
        //对序列化后的内容使用PB进行反序列化,解析出联系人信息并且打印出来
        contacts::PeopleInfo person;
        if(!person.ParseFromString(person_str))
        {
            std::cout<<"反序列化失败"<<std::endl;
            return -1;
        }
        std::cout<<"反序列化成功,结果:"<<person.name()<<" "<<person.age()<<std::endl;
    }
    return 0;
}
步骤4:编译运行
复制代码
g++ main.cc contacts.pb.cc -o contacts -std=c++11 -lprotobuf
./contacts

(二进制结果不可读,可能会错位,不显示等等)

4.2 2.0版本:进阶实战------文件读写+语法扩展

4.2.1 项目目标
  • 深入掌握Proto3高级语法(嵌套消息、枚举、repeated)

  • 实现序列化数据写入文件、从文件读取解析

  • 编写Makefile简化编译流程

4.2.2 核心需求
  • 联系人新增属性:电话信息(号码+类型)

  • 序列化:将多个联系人信息写入文件contacts.bin

  • 反序列化:从文件读取数据,解析并打印所有联系人信息

4.2.3 实现步骤
步骤1:升级.proto文件(contacts2.proto)
复制代码
syntax = "proto3";
package contacts2;

/*
    singular: 该字段最多出现一次(0 次或 1 次)。这是 Protobuf 默认的字段类型(在 proto3 中无需显式声明)。
    repeated: 该字段可以出现零次、一次或多次,相当于一个列表/数组(list/array)。顺序会被保留。在生成的代码中,通常表现为一个可变长度的容器(如 C++ 的 std::vector,Python 的 list 等)。
*/

message PeopleInfo{
	string name = 1;
	int32 age = 2;
    message Phone{
        string number = 1;
        enum PhoneType{
            MP = 0;
            TEL = 1;
        }
        PhoneType type = 2;
    }
    //一个联系人里面会有多个电话信息 -- repeated相当于C/C++中的数组
    repeated Phone phone = 3; //导入了phone的包之后,因为那里有phone的命名空间,所以这里要加上phone.Phone
    
}

//通讯录message
message Contacts{
    repeated PeopleInfo contacrs=1;
}
步骤2:编写write.cc(序列化写入文件)
复制代码
#include<iostream>
#include<fstream>
#include"contacts.pb.h"
using namespace std;

void AddPeopleInfo(contacts2::PeopleInfo* people)
{ 
    cout<<"   ------ 新增联系人 ------"<<endl;
    cout<<"   请输入联系人姓名:";
    string name;
    getline(cin, name);
    people->set_name(name);
    cout<<"   请输入联系人年龄:";
    int age;
    cin>>age;
    people->set_age(age);
    cin.ignore(256,'\n'); //清除输入缓冲区,直到\n, \n也会清除
    for(int i=1;;i++)
    {
        cout<<"请输入联系人" << i <<"的手机号码(输入回车就完成电话新增): ";
        string number;
        getline(cin, number); //getline会读取一行内容,直到\n,并且不包括\n
        if(number.empty()) break;
        contacts2::PeopleInfo_Phone* phone = people->add_phone();
        phone->set_number(number);
    }
    cout<<" ------ 添加联系人成功 ------"<<endl;
}
int main()
{
    contacts2::Contacts contacts;

    //读取本地已存在的通讯录文件
    fstream input("contacts.bin", ios::in | ios::binary);
    if(!input)
    {
        cerr << "contacts.bin not find,must create new file" << endl;
    } else if(!contacts.ParseFromIstream(&input)) //将input中的数据反序列化解析到contacts中
    {
        cerr << "parse error!" << endl;
        input.close();
        return -1;
    }
    //向通讯录中添加一个联系人  ---   对于repeated字段,需要使用add_xxx()方法,来添加元素
    AddPeopleInfo(contacts.add_contacrs());

    //将通讯录写入本地文件中
    fstream output("contacts.bin", ios::out | ios::trunc | ios::binary);
    if(!contacts.SerializeToOstream(&output))
    {
        cerr << "write error!" << endl;
        input.close();
        output.close();
        return -1;
    }
    cout<<" write success! " << endl;
    input.close();
    output.close();
    return 0;
}
步骤3:编写read.cc(从文件反序列化)
复制代码
#include<iostream>
#include<fstream>
#include"contacts.pb.h"
using namespace std;

void PrintContacts(contacts2::Contacts& contacts)
{ 
    for(int i = 0; i < contacts.contacrs_size(); i++)
    {
        cout << "----------- 联系人" << i+1 << " -----------" << endl;
        //获取到联系人信息
        const contacts2::PeopleInfo& info = contacts.contacrs(i); // .xxx(i),获取列表中的第i个元素
        //打印联系人信息
        cout << "联系人姓名:" << info.name() << endl;
        cout << "联系人年龄:" << info.age() << endl;
        for(int j = 0; j < info.phone_size(); j++) //  xxx_size() 获取列表的长度
        {
            const contacts2::PeopleInfo_Phone& phone = info.phone(j);
            cout << "联系人电话"<< j+1 <<":" << phone.number() << endl;
        }
        cout<<endl;
    }
}

int main()
{ 
    contacts2::Contacts contacts;
    //需要读取文件中的二进制内容 并且 反序列化出来
    fstream input("contacts.bin", ios::in | ios::binary);
    if(!input)
    {
        cerr << "contacts.bin not find,must create new file" << endl;
    } else if(!contacts.ParseFromIstream(&input)) //将input中的数据反序列化解析到contacts中
    {
        cerr << "parse error!" << endl;
        input.close();
        return -1;
    }
    //打印通讯录列表
    PrintContacts(contacts);

}
步骤4:编写Makefile
复制代码
all:write read

write:write.cc contacts.pb.cc
	g++ -o $@ $^ -std=c++11 -lprotobuf

read:read.cc contacts.pb.cc
	g++ -o $@ $^ -std=c++11 -lprotobuf

.PHONY: clean
clean:
	rm -f write read
步骤5:编译运行
复制代码
# 编译
make
# 运行write写入文件
./write
# 运行read读取文件并打印
./read
步骤6:快速解码技巧(无需编写read.cc

若仅需临时查看二进制文件内容,可直接使用protoc--decode参数:

复制代码
protoc --decode=contacts2.Contacts contacts2.proto < contacts.bin

4.3 3.0版本:高级实战------网络版通讯录(客户端+服务端)

4.3.1 项目目标
  • 结合httplib实现客户端-服务端网络通信

  • 掌握Protobuf在分布式场景中的应用

  • 实现联系人的增删改查功能

4.3.2 环境准备
  1. 两台Ubuntu 22.04服务器(或虚拟机),均安装Protobuf

  2. 下载httplib.h(轻量级HTTP库):

    wget https://raw.githubusercontent.com/yhirose/cpp-httplib/master/httplib.h

httplib.h放入项目目录(httplib是单文件头文件库,无需编译,直接包含即可)。

4.3.3 核心需求

|---------|------------|--------|------------------------------------------------------------------------|
| 功能 | 客户端操作 | 通信方式 | 数据传输 |
| 新增联系人 | 输入姓名、年龄、电话 | POST请求 | 客户端→服务端:AddContactRequest(序列化);服务端→客户端:AddContactResponse(序列化) |
| 删除联系人 | 输入联系人ID | POST请求 | 客户端→服务端:DeleteContactRequest(序列化);服务端→客户端:DeleteContactResponse(序列化) |
| 查看所有联系人 | 选择功能 | GET请求 | 服务端→客户端:FindAllContactsResponse(序列化) |
| 查看单个联系人 | 输入联系人ID | POST请求 | 客户端→服务端:FindOneContactRequest(序列化);服务端→客户端:FindOneContactResponse(序列化) |

4.3.4 实现步骤
步骤1:编写4个.proto文件(按功能拆分)
1.1 add_contact.proto(新增联系人)
复制代码
syntax = "proto3";
package add_contact;

message AddContactRequest{
    string name = 1;
    int32 age = 2;
    message Phone{
        string number = 1;
        enum PhoneType{
            MP = 0; //移动电话
            TEL = 1; //固定电话
        }
        PhoneType type = 2;
    }
    repeated Phone phone = 3; //电话信息
}

message AddContactResponse{
    bool sucess = 1; //服务是否调用成功
    string error_desc = 2; //错误原因
    string uid = 3;
}
1.2 delete_contact.proto(删除联系人)
复制代码
syntax = "proto3";
package delete_contact;

message DeleteContactRequest{
    string uid = 1;
}

message DeleteContactResponse{
    bool sucess = 1; //服务是否调用成功
    string error_desc = 2; //错误原因
    string uid = 3;
    bool exist = 4;
}
1.3 find_all.proto(查看所有联系人)
复制代码
syntax = "proto3";
package find_all;

message PeopleInfo{
    string uid = 1;
    string name = 2;
}

message FindAllContactsResponse{
    bool sucess = 1; //服务是否调用成功
    string error_desc = 2; //错误原因
    repeated PeopleInfo contacts = 3;
}
1.4 find_one.proto(查看单个联系人)
复制代码
syntax = "proto3";
package find_one;

message FindOneContactRequest{
    string uid = 1;
}
  
message FindOneContactResponse{
    bool sucess = 1; //服务是否调用成功
    string error_desc = 2; //错误原因
    string uid = 3;
    string name = 4;
    int32 age = 5;
    message Phone{
        string number = 1;
        enum PhoneType{
            MP = 0; //移动电话
            TEL = 1; //固定电话
        }
        PhoneType type = 2;
    }
    repeated Phone phone = 6;
}
步骤2:编写异常类头文件(ContactsException.h)

用于统一处理服务端和客户端的异常:

复制代码
#include<string>
//异常类
class ContactException
{
private:
    std::string message;
public:
    ContactException(std::string str = "A problem") : message(str){

    }
    std::string what() const{ //const修饰成员函数,保证这个成员函数的内部不会修改message这种成员变量
        return message;
    }
};
步骤3:服务端实现(service/main.cc)
复制代码
#include<iostream>
#include<fstream>
#include<regex>
#include"httplib.h"
#include"ContactException.h"
#include"add_contact.pb.h"
#include"delete_contact.pb.h"
#include"find_all.pb.h"
#include"find_one.pb.h"
using namespace httplib;
using namespace std;
#define HOST "0.0.0.0"
#define PORT 8132

//声明
void addContactText(add_contact::AddContactRequest &req,const string& uid);
void deleteContactByUid(const string& uid,bool& exist);
void handAddContact(const Request &req, Response &res);
void handDeleteContact(const Request &req, Response &res);
void handFindAllContact(const Request &req, Response &res);
void handFindOneContact(const Request &req, Response &res);
void findContactByUid(const string& uid,find_one::FindOneContactResponse &response);
//生成 UUID (通用唯一标识符)
static unsigned int random_char() {
    // 用于随机数引擎获得随机种子
    std::random_device rd;
    // mt19937是c++11新特性,它是一种随机数算法,用法与rand()函数类似,但是mt19937具有速度快,周期长的特点
    // 作用是生成伪随机数
    std::mt19937 gen(rd());
    // 随机生成一个整数i 范围[0, 255]
    std::uniform_int_distribution<> dis(0, 255);
    return dis(gen);
}

// 生成 UUID (通用唯一标识符)
static std::string generate_hex(const unsigned int len) {
    std::stringstream ss;
    // 生成 len 个16进制随机数,将其拼接而成
    for (auto i = 0; i < len; i++) {
        const auto rc = random_char();
        std::stringstream hexstream;
        hexstream << std::hex << rc;
        auto hex = hexstream.str();
        ss << (hex.length() < 2 ? '0' + hex : hex);
    }
    return ss.str();
}

int main()
{
    cout<<"----------服务启动----------"<<endl;
    Server server;
    server.Post("/contacts/add",handAddContact); //添加联系人端点
    server.Post("/contacts/delete",handDeleteContact); //删除联系人端点
    server.Get("/contacts/find_all",handFindAllContact); //查看所有联系人的uid和姓名
    server.Post("/contacts/find_one",handFindOneContact); //查看某个联系人
    //绑定8123端口,并且将端口号对外开放
    server.listen(HOST,PORT);
    return 0; 
}

void handAddContact(const Request &req, Response &res){
    cout<<"接收到AddContact请求"<<endl;
    //反序列化 request == req.body    1.定义request   2.反序列化
    add_contact::AddContactRequest request;
    add_contact::AddContactResponse response;
    try{
        if(!request.ParseFromString(req.body)){
            throw ContactException("反序列化失败");
        }
        //唯一uid
        string uid = generate_hex(10);
        //新增的联系人信息
        addContactText(request,uid);
        //构造response,之后是要存储到res.body中,作为res的主体放回给客户端
        response.set_sucess(true);
        response.set_uid(uid);
        //res.body(序列化resopnse)
        string response_str;
        if(!response.SerializeToString(&response_str)){
            throw ContactException("AddContactResponse序列化失败");
        }
        res.body = response_str;
        res.set_header("Content-Type","application/protobuf");
    }catch(ContactException &e){
        res.status = 500;
        response.set_sucess(false);
        response.set_error_desc(e.what()); //e.what()返回异常信息
        string response_str;
        if(response.SerializeToString(&response_str)){ // 这里是把出错的response_str序列化成二进制,放到res.body中。然后把res发送给客户端
            res.body = response_str;
            res.set_header("Content-Type","application/protobuf");
        }
        //走到这里,说明序列化失败,那么就返回500错误码,并且把错误信息返回给客户端
        cout<<"/conmtacts/add出错了,异常信息: "<<e.what()<<endl;
    }
}

void handDeleteContact(const Request &req, Response &res){ 
    cout<<"接收到DeleteContact请求"<<endl;
    //将客户端发送的uid反序列化,调用删除联系人函数
    delete_contact::DeleteContactRequest request;
    delete_contact::DeleteContactResponse response;
    try{
        if(!request.ParseFromString(req.body)){
            throw ContactException("反序列化失败");
        }
        bool exist = false; //一开始设置不存在
        deleteContactByUid(request.uid(),exist);
        //写res的内容,返回给客户端
        response.set_sucess(true);
        response.set_uid(request.uid());
        response.set_exist(exist);
        string response_str;
        if(!response.SerializeToString(&response_str)){
            throw ContactException("DeleteContactResponse序列化失败");
        }
        res.body = response_str;
        res.set_header("Content-Type","application/protobuf");
    }catch(ContactException &e){
        res.status = 500;
        response.set_sucess(false);
        response.set_error_desc(e.what());
        string response_str;
        if(response.SerializeToString(&response_str)){
            res.body = response_str;
            res.set_header("Content-Type","application/protobuf");
        }
        //走到这里,说明序列化失败,那么就返回500错误码,并且把错误信息返回给客户端
        cout<<"/conmtacts/delete出错了,异常信息: "<<e.what()<<endl;
    }
}

void handFindAllContact(const Request &req, Response &res){
    cout<<"接收到FindAllContact请求"<<endl;
    find_all::FindAllContactsResponse response;
    try{ 
        ifstream in("contacts.txt");
        if(!in){
            throw ContactException("打开contacts.txt文件失败");
        }
        string line;
        while(getline(in,line)){ 
            if(line.empty()) break;

            // 使用正则表达式解析每一行数据 
            regex pattern(R"(UID:([a-f0-9]+)\s+name:([^[:space:]]+)\s+age:(\d+)(.*))");
            smatch matches;
            
            if(regex_search(line, matches, pattern)) {
                string uid = matches[1].str();
                string name = matches[2].str();
                find_all::PeopleInfo* contact = response.add_contacts();
                contact->set_uid(uid);
                contact->set_name(name);
            }
        }
        in.close();
        //设置状态
        response.set_sucess(true);
        //序列化
        string response_str;
        if(!response.SerializeToString(&response_str)){
            throw ContactException("FindAllContactResponse序列化失败");
        }
        res.body = response_str;
        res.set_header("Content-Type","application/protobuf");

    }catch(ContactException &e){
        res.status = 500;
        response.set_sucess(false);
        response.set_error_desc("解析数据异常:"+e.what());
        string response_str;
        if(response.SerializeToString(&response_str)){
            res.body = response_str;
            res.set_header("Content-Type","application/protobuf");
        }
        throw ContactException("/conmtacts/find_all出错了,异常信息: "+e.what());
    }
}

void handFindOneContact(const Request &req, Response &res){ 
    cout<<"接收到FindOneContact请求"<<endl;
    find_one::FindOneContactRequest request;
    find_one::FindOneContactResponse response;
    try{
        if(!request.ParseFromString(req.body)){
            throw ContactException("反序列化失败");
        }
        findContactByUid(request.uid(),response);//调用查询联系人函数,补全response
        response.set_sucess(true);
        string response_str;
        if(!response.SerializeToString(&response_str)){
            throw ContactException("FindOneContactResponse序列化失败");
        }
        res.body = response_str;
        res.set_header("Content-Type","application/protobuf"); 
    }catch(ContactException &e){
        res.status = 500;
        response.set_sucess(false);
        response.set_error_desc(e.what());
        string response_str;
        if(response.SerializeToString(&response_str)){
            res.body = response_str;
            res.set_header("Content-Type","application/protobuf");
        }
    }
}

void addContactText(add_contact::AddContactRequest &req,const string& uid){
    ofstream out("contacts.txt",ios::app);
    if(out){
        out<<"UID:"<<uid<<" ";
        out<<"name:"<<req.name()<<" ";
        out<<"age:"<<req.age()<<" ";
        for(int i=0;i<req.phone_size();i++){
            const ::add_contact::AddContactRequest_Phone &phone = req.phone(i);
            out<<"phone:"<< phone.number()<<" ";
            out<<"phone_type:"<<phone.type()<<" ";
        }
        out<<endl;
        //关闭文件
        out.close();
    }else{
        throw ContactException("打开contacts.txt文件失败");
    }
}

void deleteContactByUid(const string& uid,bool& exist){
    vector<string> lines;
    string line;
    ifstream in("contacts.txt");
    if(!in){
        throw ContactException("打开contacts.txt文件失败");
    }
    while(getline(in,line)){
        lines.push_back(line);
    }
    in.close();
    ofstream out("contacts.txt");
    if(!out){
        throw ContactException("打开contacts.txt文件失败");
    }
    for (const auto& current_line : lines) {
        if (current_line.find("UID:" + uid) == string::npos) {
            out << current_line << endl;
        }else{
            exist = true;
        }
    }
    out.close();
}

void findContactByUid(const string& uid, find_one::FindOneContactResponse &response){
    ifstream in("contacts.txt");
    if(!in){
        throw ContactException("打开contacts.txt文件失败");
    }
    string line;
    while(getline(in,line)){ 
        if(line.empty() || line.find("UID:" + uid) == string::npos) continue;
        
/*
正则表达式:
    1.regex:定义正则表达式模式
    2.smatch:存储匹配结果
    3.regex_search:在字符串中搜索匹配项
*/


        // 使用正则表达式解析每一行数据
        regex pattern(R"(UID:([a-f0-9]+)\s+name:([^[:space:]]+)\s+age:(\d+)(.*))");
        smatch matches;
        
        if(regex_search(line, matches, pattern)) {
            string found_uid = matches[1].str();
            string name = matches[2].str();
            string age_str = matches[3].str();
            string phone_part = matches[4].str();
            
            // 设置基本信息
            response.set_uid(found_uid);
            response.set_name(name);
            response.set_age(stoi(age_str));
            
            // 解析可能存在的多个phone和phone_type
            regex phone_pattern(R"(phone:(\d+)\s+phone_type:(\d+))");
            smatch phone_matches;
            string::const_iterator search_start(phone_part.cbegin()); //设置搜索起始位置,到后面的时候,需要跳转搜索起始位置(因为有很多个电话信息)
            
            while(regex_search(search_start, phone_part.cend(), phone_matches, phone_pattern)) {
                string phone_number = phone_matches[1].str();
                string phone_type = phone_matches[2].str();
                
                // 添加电话信息
                auto phone_info = response.add_phone();
                phone_info->set_number(phone_number);
                
                // 将字符串转换为正确的枚举类型
                int phone_type_int = stoi(phone_type);
                find_one::FindOneContactResponse_Phone_PhoneType enum_type;
                
                switch(phone_type_int) {
                    case 0:
                        enum_type = find_one::FindOneContactResponse_Phone_PhoneType_MP;
                        break;
                    case 1:
                        enum_type = find_one::FindOneContactResponse_Phone_PhoneType_TEL;
                        break;
                }
                
                phone_info->set_type(enum_type);
                
                search_start = phone_matches.suffix().first;
            }
        }
        
        break; //到这里说明已经出现了一个与之匹配的uid,就可以退出循环了
    }
    in.close();
}
步骤4:客户端实现(client/main.cc)
复制代码
#include<iostream>
#include"httplib.h"
#include"add_contact.pb.h"
#include"delete_contact.pb.h"
#include"find_all.pb.h"
#include"find_one.pb.h"
#include"ContactsException.h"
#include<fstream>
using namespace std;
using namespace httplib;
#define CONTACTS_HOST "192.168.5.13"
#define CONTACTS_POST 8132

/*
前置声明:在 main 函数中会调用 addContact 函数,但实际的函数定义在后面
编译器需要:通过前置声明,编译器在编译 main 函数时就知道有这个函数存在
*/
void addContact();
bool buildAddContactRequest(add_contact::AddContactRequest* req);
void deleteContact();
void findAll();
void findOne();
void writeErrorLog(const std::string& message);
void writeUid(const std::string& name,const std::string& uid);
void DeleteUid(const string& uid);
bool isUidExist(const string& uid);
void printFindAllContactsResponse(find_all::FindAllContactsResponse& response);
void printFindOneContactResponse(find_one::FindOneContactResponse& response);
void menu()
{
    cout<< "----------------------------------------"<<endl;
    cout<< "-------- 请选择对通讯录的操作 ------------"<<endl;
    cout<< "------------ 1.新增联系人  --------------"<<endl;
    cout<< "------------ 2.删除联系人  --------------"<<endl;
    cout<< "---------- 3.查看联系人列表  -------------"<<endl;
    cout<< "--------- 4.查看联系人详细信息  -----------"<<endl;
    cout<< "------------    0.退出    --------------"<<endl;
}
int main()
{
    enum OPTION{QUIT=0,ADD,DEL,FIND_ALL,FIND_ONE};
    while(true)
    {
        menu();
        cout<< "--->请选择:";
        int choose;
        cin>>choose;
        cin.ignore(256,'\n');
        try{
            switch(choose)
            {
                case OPTION::QUIT:
                    cout<<"----> 程序退出"<<endl;
                    return 0;
                case OPTION::ADD:
                    addContact();
                    break;
                case OPTION::DEL:
                    deleteContact();
                    break;
                case OPTION::FIND_ALL:
                    findAll();
                    break;
                case OPTION::FIND_ONE:
                    findOne();
                    break;
                default:
                    cout<<"----> 输入有误,请重新选择! "<<endl;            
            }
        }catch(const ContactException &e){ //捕获异常类的对象, 这个对象是在try中会抛出来的
            writeErrorLog("---> 操作通讯录时发生异常, 异常信息: "+ e.what());
        }
    }
    return 0;
}

void addContact()
{
    //搭建客户端连接
    Client cli(CONTACTS_HOST,CONTACTS_POST); //创建一个HTTP客户端对象,用于向通讯录服务端发送请求
    //构造req
    add_contact::AddContactRequest req;
    if(!buildAddContactRequest(&req)){
        cout<<"---> 输入有误,请重新输入"<<endl;
        return;
    }
    //序列化req
    string req_str;
    if(!req.SerializeToString(&req_str)) //如果序列化失败
    {
        throw ContactException("AddContactRequest序列化失败!"); //构造异常对象,匿名对象
    }
    //发起post请求
    auto res = cli.Post("/contacts/add",req_str,"application/protobuf");
    if(!res) //如果请求失败
    {
        string err_desc;
        err_desc="/contacts/add链接失败,错误信息: "+httplib::to_string(res.error());
        throw ContactException(err_desc);
    }
    //反序列化resp -- res是Post请求的返回值,而需要反序列化的值是在res.body里面
    add_contact::AddContactResponse resp;
    bool parse = resp.ParseFromString(res->body); //反序列化之后的值保存在resp里面
    if(res->status != 200 || !parse){
        string err_desc;
        err_desc="/contacts/add 调用失败!"+std::to_string(res->status)+"("+res->reason+")";
        throw ContactException(err_desc);
    }else if(res->status!=200){
        string err_desc;
        err_desc="/contacts/add 调用失败!"+std::to_string(res->status)+"("+res->reason+") 错误原因: "+resp.error_desc();
        throw ContactException(err_desc);
    }else if(!resp.sucess()){
        string err_desc;
        err_desc="/contacts/add 结果异常! 异常原因: "+resp.error_desc();
        throw ContactException(err_desc);
    }
    //结果打印
    cout << "---> 添加成功! 联系人ID: " << resp.uid() << endl;
    //持久化存储resp.uid() ,uid是string类型
    // writeUid(req.name(),resp.uid());
}


bool buildAddContactRequest(add_contact::AddContactRequest* req)
{
    cout<<"---> 请输入联系人姓名: ";
    string name;
    getline(cin,name);
    if(name.empty()){return false;}
    req->set_name(name);
    cout<<"---> 请输入联系人年龄: ";
    string str_age;
    getline(cin,str_age);
    int age = -1;
    try {
        age = std::stoi(str_age);
        // 使用 value
    } catch (const std::invalid_argument& e) {
        // 处理无效参数异常
        std::cerr << "无效参数异常: " << e.what() << std::endl;
    } catch (const std::out_of_range& e) {
        // 处理数值超出范围异常
        std::cerr << "数值超出范围异常: " << e.what() << std::endl;
    }
    if(age<=0 || age>=200) {return false;}
    req->set_age(age);
    for(int i=1;;i++)
    {
        cout<<"---> 请输入第"<<i<<"个联系人信息(只输入回车完成电话的新增):";
        string number;
        getline(cin,number);
        if(number.empty()){break;}
        add_contact::AddContactRequest_Phone* phone = req->add_phone();
        phone->set_number(number); //第一段是获得联系人电话这个数组的地址,第二段是添加联系人电话
        cout<<"---> 请输入联系人电话类型(1.移动电话 2.固定电话): ";
        int type;
        cin>>type;
        cin.ignore(256,'\n');
        switch (type)
        {
        case 1:
            phone->set_type(add_contact::AddContactRequest_Phone_PhoneType::AddContactRequest_Phone_PhoneType_MP);
            break;
        case 2:
            phone->set_type(add_contact::AddContactRequest_Phone_PhoneType::AddContactRequest_Phone_PhoneType_TEL);
            break;
        default:
            cout<<"---> 输入有误,请重新输入"<<endl;
            i--;
            break;
        }
    }
    return true;
}

void deleteContact(){
    //搭建客户端连接
    Client cli(CONTACTS_HOST,CONTACTS_POST);
    //获取uid
    cout<<"---> 请输入联系人ID: ";
    string uid;
    getline(cin,uid);
    //创建req,设置uid值和序列化
    delete_contact::DeleteContactRequest req; 
    req.set_uid(uid);
    string req_str;
    if(!req.SerializeToString(&req_str)){
        throw ContactException("DeleteContactRequest序列化失败!");
    }
    // //先在前端检测是否有这个uid
    // if(!isUidExist(uid)){
    //     cout<<"---> 联系人不存在!"<<endl;
    //     return;
    // }
    //发起post请求       指定端点,    指定请求体数据,   告诉服务器请求体的数据格式
    auto res = cli.Post("/contacts/delete",req_str,"application/protobuf");
    if(!res){
        string err_desc;
        err_desc="/contacts/delete链接失败,错误信息: "+httplib::to_string(res.error());
        throw ContactException(err_desc);
    }
    delete_contact::DeleteContactResponse resp;
    bool parse = resp.ParseFromString(res->body);
    if(res->status != 200 || !parse){
        string err_desc;
        err_desc="/contacts/delete 调用失败!"+std::to_string(res->status)+"("+res->reason+")";
        throw ContactException(err_desc);
    }else if(res->status!=200){
        string err_desc;
        err_desc="/contacts/delete 调用失败!"+std::to_string(res->status)+"("+res->reason+") 错误原因: "+resp.error_desc();
        throw ContactException(err_desc);
    }else if(!resp.sucess()){
        string err_desc;
        err_desc="/contacts/delete 结果异常! 错误原因: "+resp.error_desc();
        throw ContactException(err_desc);
    }else if(!resp.exist()){
        cout<<"---> 联系人不存在!"<<endl;
    }else{
        // DeleteUid(uid);
        cout<<"---> 删除成功!"<<endl;
    }
}     


void writeErrorLog(const std::string& message)
{
    // 获取当前时间
    auto now = std::chrono::system_clock::now();
    auto time_t = std::chrono::system_clock::to_time_t(now);
    auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
        now.time_since_epoch() % 1000);
    
    std::stringstream ss;
    ss << std::put_time(std::localtime(&time_t), "%Y/%m/%d %H:%M:%S");
    
    //fstream是可读可写,ifstream只读,ofstream只写
    ofstream error("error.txt",ios::app);
    if(error.is_open()){
        error << "[" << ss.str() << "] " << message << endl;
        error.close();
    }else{
        cerr<<"---> 创建error.txt文件失败!"<<endl;
    }
}


void findAll(){
    //搭建客户端连接
    Client cli(CONTACTS_HOST,CONTACTS_POST);
    auto res = cli.Get("/contacts/find_all");
    if(!res){
        string err_desc;
        err_desc="/contacts/find_all链接失败,错误信息: "+httplib::to_string(res.error());
        throw ContactException(err_desc);
    }
    //反序列化
    find_all::FindAllContactsResponse response;
    bool parse =  response.ParseFromString(res->body);
    //处理异常
    if(res->status != 200 || !parse){ 
        string err_desc;
        err_desc="/contacts/find_all 调用失败!"+std::to_string(res->status)+"("+res->reason+")";
        throw ContactException(err_desc);
    }else if(res->status!=200){ 
        string err_desc;
        err_desc="/contacts/find_all 调用失败!"+std::to_string(res->status)+"("+res->reason+") 错误原因: "+response.error_desc();
        throw ContactException(err_desc);
    }else if(!response.sucess()){
        string err_desc;
        err_desc="/contacts/find_all 结果异常! 错误原因: "+response.error_desc();
        throw ContactException(err_desc);
    }
    //正常返回,打印结果
    printFindAllContactsResponse(response);
}

void printFindAllContactsResponse(find_all::FindAllContactsResponse& response){
    //如果为空
    if(response.contacts().size()==0){
        cout<<"---> 联系人列表为空!"<<endl;
        return;
    }
    cout<<"---> 联系人列表: "<<endl;
    for(auto& people_info:response.contacts()){ 
        cout<<"联系人ID: "<<people_info.uid()<<"  姓名: "<<people_info.name()<<endl;
    }
}

void findOne(){
    //搭建客户端连接
    Client cli(CONTACTS_HOST,CONTACTS_POST);
    cout<<"---> 请输入联系人ID: ";
    string uid;
    getline(cin,uid);
    find_one::FindOneContactRequest req;
    req.set_uid(uid);
    string req_str;
    if(!req.SerializeToString(&req_str)){
        throw ContactException("FindOneContactRequest序列化失败!");
    }
    auto res = cli.Post("/contacts/find_one",req_str,"application/protobuf");
    if(!res){
        string err_desc;
        err_desc="/contacts/find_one链接失败,错误信息: "+httplib::to_string(res.error());
        throw ContactException(err_desc);
    }
    find_one::FindOneContactResponse response;
    bool parse = response.ParseFromString(res->body);
    if(res->status != 200 || !parse){ 
        string err_desc;
        err_desc="/contacts/find_one 调用失败!"+std::to_string(res->status)+"("+res->reason+")";
        throw ContactException(err_desc);
    }else if(res->status!=200){
        string err_desc;
        err_desc="/contacts/find_one 调用失败!"+std::to_string(res->status)+"("+res->reason+") 错误原因: "+response.error_desc();
        throw ContactException(err_desc);
    }else if(!response.sucess()){
        string err_desc;
        err_desc="/contacts/find_one 结果异常! 错误原因: "+response.error_desc();
    }
    printFindOneContactResponse(response);
}

void printFindOneContactResponse(find_one::FindOneContactResponse& response)
{
    cout<<"联系人ID: "<<response.uid()<<endl;
    cout<<"姓名: "<<response.name()<<endl;
    for(auto& phone:response.phone()){
        cout<<"电话: "<<phone.number()<<"  类型: "<<find_one::FindOneContactResponse_Phone_PhoneType_Name(phone.type())<<endl;
    }
}

// void writeUid(const std::string& name,const std::string& uid){
//     ofstream uid_file("uid.txt",ios::app);
//     if(uid_file){
//         uid_file<<name<<": "<<uid<<endl;
//         uid_file.close();
//     }else{
//         throw ContactException("---> 创建uid.txt文件失败!");
//     }
// }

// void DeleteUid(const string& uid){
//     ifstream in("uid.txt");
//     if(!in){
//         throw ContactException("---> 读模式打开uid.txt文件失败!");
//     }
//     vector<string> uid_names;
//     string uid_name;
//     while(getline(in,uid_name)){
//         uid_names.push_back(uid_name);
//     }
//     in.close();
//     ofstream out("uid.txt");
//     if(!out){
//         throw ContactException("---> 写模式打开uid.txt文件失败!");
//     }
//     for(auto& name:uid_names){
//         if(name.find(uid) == name.npos){
//             out<<name<<endl;
//         }
//     }
//     out.close();
// }

// bool isUidExist(const string& uid){
//     ifstream in("uid.txt");
//     if(!in){
//         throw ContactException("---> 读模式打开uid.txt文件失败!");
//     }
//     string uid_name;
//     while(getline(in,uid_name)){
//         if(uid_name.find(uid) != uid_name.npos){
//             return true;
//         }
//     }
//     in.close();
//     return false;
// }

五、最佳实践与注意事项

5.1 语法设计最佳实践

  1. 字段编号:核心字段用1-15,数字越小占用字符越少,核心字段出现频率越高,使用的数字应该越小。

  2. 包名:使用有意义的包名(如com.company.project),避免冲突

  3. 字段命名:使用下划线命名法(phone_number),Protobuf会自动转换为对应语言的命名规范(C++为phone_number

  4. 兼容性:删除字段时必须用reserved保留编号和名称,新增字段用optional/repeated

  5. 枚举:第一个值必须为0,命名清晰(如UNSPECIFIED表示未指定)

5.2 性能优化建议

  1. 序列化方式:优先使用SerializeToString()/ParseFromString()(内存操作),避免频繁文件I/O

  2. 字段类型选择:

    1. 整数类型:根据实际范围选择(如int32适用于-2³¹++2³¹-1,`uint32`适用于0++2³²-1)

    2. 字符串:短字符串优先使用string,长二进制数据使用bytes

  3. 避免过度嵌套:嵌套层级过多会增加解析复杂度和内存占用

5.3 网络通信注意事项

  1. 数据传输:指定Content-Type为application/protobuf,避免服务端解析错误

  2. 异常处理:序列化/反序列化失败、网络连接异常需捕获并处理,返回友好提示

  3. 数据校验:客户端输入需校验(如年龄范围、电话号码格式),服务端接收数据后也需二次校验,避免恶意数据

  4. 跨语言通信:确保双方使用相同版本的.proto文件,字段编号和类型一致

5.4 调试技巧

  1. 二进制解码:使用protoc --decode=包名.消息名 proto文件 < 二进制文件快速查看二进制数据

  2. 日志输出:关键步骤打印日志(如序列化前的对象、反序列化后的结果),将错误信息通过writeErrorLog函数写入到error.txt文件中,便于定位问题

  3. 版本兼容测试:修改.proto文件后,用旧版本程序解析新数据,用新版本程序解析旧数据,验证兼容性

六、项目代码地址

本文所有实战项目的完整代码(含proto文件、源码、Makefile)已上传至Gitee,可直接下载编译运行:

Gitee仓库地址

实战一:源码在fast_start文件夹下。

实战二:源码在proto3文件夹下。

实战一:源码在http_contacts_service和http_contacts_client文件夹下。

结语

Protobuf的学习是一个"语法→基础实战→高级实战"的递进过程,通过本文的3个通讯录项目,读者可以逐步掌握Protobuf的核心语法、文件操作、网络通信等场景的应用。Protobuf作为一种高效的序列化格式,在分布式系统、微服务、物联网等领域有着广泛的应用前景,掌握它将为后续的技术发展打下坚实基础。希望本文的超详细笔记能帮助读者少踩坑、快速上手,在实际项目中灵活运用Protobuf!

相关推荐
JIngJaneIL2 小时前
基于java+ vue医院管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
予枫的编程笔记2 小时前
Redis 核心数据结构深度解密:从基础命令到源码架构
java·数据结构·数据库·redis·缓存·架构
信创天地2 小时前
信创国产化数据库的厂商有哪些?分别用在哪个领域?
数据库·python·网络安全·系统架构·系统安全·运维开发
JIngJaneIL2 小时前
基于java + vue校园跑腿便利平台系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
瀚高PG实验室3 小时前
highgo DB中数据库对象,模式,用户,权限之间的关系
数据库·瀚高数据库
越来越无动于衷3 小时前
odbc链接oracle数据源
数据库·oracle
李迟3 小时前
Golang实践录:使用sqlx操作sqlite3数据库
数据库·golang·sqlite
小Mie不吃饭3 小时前
Oracle - 闪回技术及生产实践
数据库·oracle
爱丽_3 小时前
MyBatis事务管理与缓存机制详解
数据库·缓存·mybatis