当前位置: 首页 > news >正文

【网络编程】网络传输-protobuf

一、protobuf介绍

1.1 protobuf 是什么?

  • Protocol Buffers (Protobuf) 是 Google 开发的一种高效、跨语言的序列化机制,用于结构化数据的编解码。它通过定义数据结构(.proto 文件),生成对应语言的代码,实现高效的数据传输和存储。

  • google 提供了多种语言的实现:java、c#、c++、go 和 python 等,每一种实现都包含了相应语言的编译器以及库文件
    在这里插入图片描述

1.2 protobuf 工作原理

1.2.1 定义数据结构文件(.proto文件)

使用 .proto 文件定义消息结构,语法类似C语言,每个数据类型由message组成,比如C++语言有如下结构体:

struct node{int a;std::string b;std::string c;
};

那么对应的.proto文件的写法如下,有以下几点规则:

  1. 每个类型成员后面需要添加上=,并且标注上编号,编号必须从1开始,不能重复
  2. 每个成员最后以;结尾
message{int32 a = 1;bytes b = 2;bytes c = 2;
}

.proto文件还需要在文件的开头添加上protobuf的版本号,protobuf3的版本声明如下:

syntax = "proto3"

一个简单的.proto完整如下

syntax = "proto3"message{int32 a = 1;bytes b = 2;bytes c = 2;
}

protobuf中的数据类型对应C++的数据类型对照表如下:

Protobuf 类型C++ 类型备注
doubledouble64位浮点数
floatfloat32位浮点数
int32int32位整数
int64long64位整数
uint32unsigned int32位无符号整数
uint64unsigned long64位无符号整数
sint32signed int32位整数,处理负数效率比int32更高
sint64signed long64位整数,处理负数效率比int64更高
fixed32unsigned int(32位)总是4个字节。如果数值总是比总是比228大的话,这个类型会比uint32高效。
fixed64unsigned long(64位)总是8个字节。如果数值总是比总是比256大的话,这个类型会比uint64高效。
sfixed32int (32位)总是4个字节
sfixed64long (64位)总是8个字节
boolbool布尔类型
stringstring一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本
bytesstring处理多字节的语言字符、如中文, 建议protobuf中字符型类型使用 bytes
enumenum枚举
messageobject of class自定义的消息类型

1.2.2 protobuf 命令行

完成了.proto文件的编写,我们可以使用protoc可执行文件执行命令来生成.h.cc文件了,用于我们后续的序列化和反序列化:

假设我们当前路径下有上述的node.proto文件,下述命令用于在当前路径下生成对应的cpp文件:

  • protoc为可执行程序,是我们编译protobuf库生成的,添加运行库到环境变量中

  • -I后面添加.proto文件所在路径,当前路径即为.,然后空格,添加文件的名称,这里是node.proto

  • --cpp_out表示输出的文件为C++编程语言,后面加上生成文件的路径,这里是生成到当前路径下,因此之间使用.即可

protoc -I . node.proto --cpp_out=.

1.2.3 使用生成的文件

使用命令行生成文件后,会有node.pb.hnode.pb.cc文件,使用其提供的接口就可以完成序列化反序列化了

二、protobuf的配置

我们这里介绍在ubuntu下配置protobuf3.20版本

  1. github上下载protobuf的源码,下载链接:https://github.com/protocolbuffers/protobuf/releases/tag/v3.20.0

  2. 解压源码压缩包,进入到对应的目录,执行configure命令检查配置环境

cd protobuf-cpp-3.20.0/protobuf-3.20.0
./configure
  1. 编译、安装,安装后的动态库在/usr/local/lib
make -j4 && sudo make install
  1. 添加环境变量,将libprotobuf.so动态库添加到/etc/ld.so.conf配置文件
sudo gedit /etc/ld.so.conf

在这里插入图片描述

  1. 更新配置文件
sudo ldconfig
  1. 查看protoc可执行程序是否配置完成,查看对应的版本
protoc --verison

在这里插入图片描述

三、使用protobuf

3.1 简单使用protobuf

3.1.1 编写.proto文件

假设我们C++需要传输如下的数据

struct login_msg{std::string user;std::string password;
};

我们可以这样写login.proto文件

syntax = "proto3";message login_msg{bytes user = 1;bytes password = 2;
}

3.1.2 生成*.pb.h*.pb.cc文件

使用命令行生成对应的login.pb.hlogin.pb.cc文件

protoc -I . login.proto --cpp_out=./protobuf/

3.1.3 序列化数据

查看生成的.h文件,可以看到有set_user的接口,以及set_password接口,这些是用于对login_msg类的赋值接口

在这里插入图片描述

在这里插入图片描述

因此,我们可以这样赋值:

#include"protobuf/login.pb.h"//初始化
login_msg loginMsg;
loginMsg.set_user("Antonio");
loginMsg.set_password("xx123456");
  • 观察login.pb.h文件,内部有很多序列化的成员函数,比如SerializeToString()序列化为字符串,SerializeToArray序列化为数组、SerializeToIstream序列化到磁盘等等

  • 这些函数接口不再我们生成的login.pb.cc文件中,而在我们编译生成的google/protobuf/message_lite头文件中,具体实现在libprotobuf.so动态库中,因此我们最后编译需要链接这个库

在这里插入图片描述

赋值后的login_msg类就可以通过提供的Serialize*()接口实现序列化了,比如序列化为字符串:

//序列化 
void serializeProtobuf(login_msg* pLoginMsg,std::string& serializedMsg){pLoginMsg->SerializeToString(&serializedMsg);
}

3.1.3 反序列化

序列化后的数据为二进制数据,无法直接读取,可以使用protobuf提供的反序列化接口来实现结构体成员的读取:

  • 同样是在google/protobuf/message_lite头文件定义,并且和序列化呼应,提供ParseFromStringParseFromArray等接口来反序列化

在这里插入图片描述

  • 反序列化后的数据存储在调用成员函数的类内,通过调用成员函数来获取反序列化后的值,比如在login.pb.ccuser字段有user()成员函数来获取对应的值

在这里插入图片描述

3.1.4 测试结果

完整的序列化、非序列化的测试代码如下

#include"protobuf/login.pb.h"
#include<iostream>//序列化 
void serializeProtobuf(loginPackage::login_msg* pLoginMsg,std::string& serializedMsg){pLoginMsg->SerializeToString(&serializedMsg);std::cout << "===== serialize =====" << std::endl;std::cout << pLoginMsg->DebugString() << std::endl;
}//反序列化
void deserializeProtobuf(loginPackage::login_msg*pLoginMsg,std::string& serializedMsg){pLoginMsg->ParseFromString(serializedMsg);
}int main(){//初始化login_msg loginMsg;loginMsg.set_user("Antonio");loginMsg.set_password("xx123456");    //序列化std::string loginByteStream;serializeProtobuf(&loginMsg,loginByteStream);//反序列化login_msg parsedLoginMsg;deserializeProtobuf(&parsedLoginMsg,loginByteStream);//获取结果printResult(&parsedLoginMsg);return 0;
}

编译的时候需要添加动态库protobuf

g++ main.cpp protobuf/login.pb.cc -o protobuf-test -lprotobuf
./protobuf-test

测试的结果如下:

在这里插入图片描述

3.2 嵌套message

3.2.1 编写.proto文件

C++我们假设有这样的结构体嵌套数据:

struct date_msg{int year;int month;int day;
};struct login_msg{std::string user;std::string password;date_msg date;
};

我们在.proto文件中可以对照着这样写:

syntax = "proto3";message date_msg{int32 year = 1;int32 month = 2;int32 day = 3;
}message login_msg{bytes user = 1;bytes password = 2;date_msg date = 3;
}

也可以分开写,类似C++中将不同的结构体分开在不同的.h头文件,然后包含一样,不过这里的.proto文件使用的import关键字

  1. date.proto文件如下,编写关于日期的一些数据信息
syntax = "proto3";message date_msg{int32 year = 1;int32 month = 2;int32 day = 3;
}
  1. login.proto文件如下,引用date.proto文件
syntax = "proto3";import "date.proto";message login_msg{bytes user = 1;bytes password = 2;date_msg date = 3;
}

3.2.2 生成*.pb.h*.pb.cc文件

分两个.proto文件,使用命令行生成的是对应的两套文件,新增date.pb.hdate.pb.cc文件

protoc -I . date.proto  login.proto --cpp_out=./protobuf

在这里插入图片描述

编译的时候也需要多编译一个文件

g++ main.cpp protobuf/login.pb.cc protobuf/date.pb.cc -o protobuf-test -lprotobuf 

3.2.3 序列化数据

这里使用到了另一个接口mutable_*(),这个函数返回的是对应类型的指针,比如mutable_date(),返回date_msg类型,可以支持修改

在这里插入图片描述

因此,赋值操作这样写:

loginMsg.mutable_date()->set_year(2025);
loginMsg.mutable_date()->set_month(7);
loginMsg.mutable_date()->set_day(21);

序列化保持不变

//序列化 
void serializeProtobuf(login_msg* pLoginMsg,std::string& serializedMsg){pLoginMsg->SerializeToString(&serializedMsg);
}

3.2.4 反序列化数据

由于date()返回的是一个引用,因此支持链式调用,所以获取date内部的数据,只需要进行两次调用即可:

std::cout << "year = " << pLoginMsg->date().year() << std::endl;
std::cout << "month = " << pLoginMsg->date().month() << std::endl;
std::cout << "day = " << pLoginMsg->date().day() << std::endl;

在这里插入图片描述

3.2.5 测试结果

完整的代码如下:

#include"protobuf/login.pb.h"
#include<iostream>//序列化 
void serializeProtobuf(loginPackage::login_msg* pLoginMsg,std::string& serializedMsg){pLoginMsg->SerializeToString(&serializedMsg);std::cout << "===== serialize =====" << std::endl;std::cout << pLoginMsg->DebugString() << std::endl;
}//反序列化
void deserializeProtobuf(loginPackage::login_msg*pLoginMsg,std::string& serializedMsg){pLoginMsg->ParseFromString(serializedMsg);
}void printResult(login_msg*pLoginMsg){std::cout << "===== deserialzie ======" << std::endl;std::cout << "user = " << pLoginMsg->user() << std::endl;std::cout << "password = " << pLoginMsg->password() << std::endl;std::cout << "year = " << pLoginMsg->date().year() << std::endl;std::cout << "month = " << pLoginMsg->date().month() << std::endl;std::cout << "day = " << pLoginMsg->date().day() << std::endl;}int main(){//初始化login_msg loginMsg;loginMsg.set_user("Antonio");loginMsg.set_password("xx123456");loginMsg.mutable_date()->set_year(2025);loginMsg.mutable_date()->set_month(7);loginMsg.mutable_date()->set_day(21);//序列化std::string loginByteStream;serializeProtobuf(&loginMsg,loginByteStream);//反序列化login_msg parsedLoginMsg;deserializeProtobuf(&parsedLoginMsg,loginByteStream);//获取结果printResult(&parsedLoginMsg);return 0;
}

序列化、反序列化测试结果

在这里插入图片描述

3.3 package

3.3.1 编写.proto文件

protobuf中,package的作用类似C++中的名字空间namespace,假设我们有如下的C++代码

namespace datePackage{struct date_msg{int year;int month;int day;};date_msg msg;
}namespace loginPackage{struct date_msg{int year;int month;int day;};std::string user;std::string password;datePackage::date_msg date;date_msg msg date2;
}

对于同名类型,我们可以使用namespace来区分,在.proto文件中可以使用package实现类似功能:

  1. date.proto文件,以datePackage区分
syntax = "proto3";package datePackage;message date_msg{int32 year = 1;int32 month = 2;int32 day = 3;
}
  1. login.proto文件,以loginPackage区分
syntax = "proto3";import "date.proto";package loginPackage;message date_msg{int32 year = 1;int32 month = 2;int32 day = 3;
}message login_msg{bytes user = 1;bytes password = 2;datePackage.date_msg date = 3;date_msg date2 = 4;
}

3.3.2 序列化数据

序列化数据的时候,赋值数据需要指定命名空间,比如:

  1. 指定dateloginPackage::命名空间
  2. 指定date2datePacket::命名空间
//初始化
loginPackage::login_msg loginMsg;
loginMsg.set_user("Antonio");
loginMsg.set_password("xx123456");datePackage::date_msg* date = loginMsg.mutable_date();
date->set_year(2025);
date->set_month(7);
date->set_day(21);loginPackage::date_msg* date2 = loginMsg.mutable_date2();
date2->set_year(2024);
date2->set_month(6);
date2->set_day(20);

在生成*.pb.cc中也有实现命名空间

在这里插入图片描述

3.3.3 反序列化数据

反序列化数据使用方法不变,直接指定datedate2即可

std::cout << "===== date =====" << std::endl;
std::cout << "year = " << pLoginMsg->date().year() << std::endl;
std::cout << "month = " << pLoginMsg->date().month() << std::endl;
std::cout << "day = " << pLoginMsg->date().day() << std::endl;std::cout << "===== date2 =====" << std::endl;
std::cout << "year = " << pLoginMsg->date2().year() << std::endl;
std::cout << "month = " << pLoginMsg->date2().month() << std::endl;
std::cout << "day = " << pLoginMsg->date2().day() << std::endl;

3.3.4 测试结果

完整代码如下

#include"protobuf/login.pb.h"
#include<iostream>//序列化 
void serializeProtobuf(loginPackage::login_msg* pLoginMsg,std::string& serializedMsg){pLoginMsg->SerializeToString(&serializedMsg);std::cout << "===== serialize =====" << std::endl;std::cout << pLoginMsg->DebugString() << std::endl;
}//反序列化
void deserializeProtobuf(loginPackage::login_msg*pLoginMsg,std::string& serializedMsg){pLoginMsg->ParseFromString(serializedMsg);
}void printResult(loginPackage::login_msg*pLoginMsg){std::cout << "===== deserialzie ======" << std::endl;std::cout << "user = " << pLoginMsg->user() << std::endl;std::cout << "password = " << pLoginMsg->password() << std::endl;std::cout << "===== date =====" << std::endl;std::cout << "year = " << pLoginMsg->date().year() << std::endl;std::cout << "month = " << pLoginMsg->date().month() << std::endl;std::cout << "day = " << pLoginMsg->date().day() << std::endl;std::cout << "===== date2 =====" << std::endl;std::cout << "year = " << pLoginMsg->date2().year() << std::endl;std::cout << "month = " << pLoginMsg->date2().month() << std::endl;std::cout << "day = " << pLoginMsg->date2().day() << std::endl;}int main(){//初始化loginPackage::login_msg loginMsg;loginMsg.set_user("Antonio");loginMsg.set_password("xx123456");datePackage::date_msg* date = loginMsg.mutable_date();date->set_year(2025);date->set_month(7);date->set_day(21);loginPackage::date_msg* date2 = loginMsg.mutable_date2();date2->set_year(2024);date2->set_month(6);date2->set_day(20);//序列化std::string loginByteStream;serializeProtobuf(&loginMsg,loginByteStream);//反序列化loginPackage::login_msg parsedLoginMsg;deserializeProtobuf(&parsedLoginMsg,loginByteStream);//获取结果printResult(&parsedLoginMsg);return 0;
}

序列化和反序列化输出结果对比

在这里插入图片描述

3.4 repeated 字段

3.4.1 编写.proto文件

repeat字段可以修饰一个字段可能有多个不同的值,类似一个可变长数组的作用,比如我们需要一个可变长数组vector<std::string>存储邮箱信息,邮箱可以有一个也可以有多个,在C++中需要定义这样的数据结构:

namespace datePackage{struct date_msg{int year;int month;int day;};date_msg msg;
}namespace loginPackage{struct date_msg{int year;int month;int day;};std::string user;std::string password;datePackage::date_msg date;date_msg msg date2;std::vector<std::string>emalis;
}

protobuf中可以这样定义,使用repeated修饰bytes字段,代表emails可以有多个值

syntax = "proto3";package loginPackage;
import "date.proto";message date_msg{int32 year = 1;int32 month = 2;int32 day = 3;
}message login_msg{bytes user = 1;bytes password = 2;datePackage.date_msg date = 3;date_msg date2 = 4;repeated bytes emalis = 5;
}

3.4.2 序列化数据

  • 对于repeat关键字修饰的字段,生成的.pb.h文件中有对应的add_*()接口,比如我们这里的add_emails()接口

在这里插入图片描述

  • 相当于是顺序表,按从小到大的索引插入,支持随机访问,也可以配合*_size()接口获取数组大小,遍历所有的结果

因此,可以依次加入我们的邮箱:

loginMsg.add_emalis("123456789@qq.com");
loginMsg.add_emalis("23333@163.com");

3.4.3 反序列化数据

反序列化数据,可以使用下标索引随机访问

std::cout << "===== emailis =====" << std::endl;
std::cout << "emalis(0) = " << pLoginMsg->emalis(0) << std::endl;;
std::cout << "emalis(1) = " << pLoginMsg->emalis(1) << std::endl;;

3.4.4 测试结果

完整代码如下:

#include"protobuf/login.pb.h"
#include<iostream>//序列化 
void serializeProtobuf(loginPackage::login_msg* pLoginMsg,std::string& serializedMsg){pLoginMsg->SerializeToString(&serializedMsg);std::cout << "===== serialize =====" << std::endl;std::cout << pLoginMsg->DebugString() << std::endl;
}//反序列化
void deserializeProtobuf(loginPackage::login_msg*pLoginMsg,std::string& serializedMsg){pLoginMsg->ParseFromString(serializedMsg);
}void printResult(loginPackage::login_msg*pLoginMsg){std::cout << "===== deserialzie ======" << std::endl;std::cout << "user = " << pLoginMsg->user() << std::endl;std::cout << "password = " << pLoginMsg->password() << std::endl;std::cout << "===== emailis =====" << std::endl;std::cout << "emalis(0) = " << pLoginMsg->emalis(0) << std::endl;;std::cout << "emalis(1) = " << pLoginMsg->emalis(1) << std::endl;;std::cout << "===== date =====" << std::endl;std::cout << "year = " << pLoginMsg->date().year() << std::endl;std::cout << "month = " << pLoginMsg->date().month() << std::endl;std::cout << "day = " << pLoginMsg->date().day() << std::endl;std::cout << "===== date2 =====" << std::endl;std::cout << "year = " << pLoginMsg->date2().year() << std::endl;std::cout << "month = " << pLoginMsg->date2().month() << std::endl;std::cout << "day = " << pLoginMsg->date2().day() << std::endl;
}int main(){//初始化loginPackage::login_msg loginMsg;loginMsg.set_user("Antonio");loginMsg.set_password("xx123456");loginMsg.add_emalis("123456789@qq.com");loginMsg.add_emalis("23333@163.com");datePackage::date_msg* date = loginMsg.mutable_date();date->set_year(2025);date->set_month(7);date->set_day(21);loginPackage::date_msg* date2 = loginMsg.mutable_date2();date2->set_year(2024);date2->set_month(6);date2->set_day(20);//序列化std::string loginByteStream;serializeProtobuf(&loginMsg,loginByteStream);//反序列化loginPackage::login_msg parsedLoginMsg;deserializeProtobuf(&parsedLoginMsg,loginByteStream);//获取结果printResult(&parsedLoginMsg);return 0;
}

序列化和反序列化测试结果:

在这里插入图片描述

3.5 enum 字段

3.5.1 编写.proto文件

现在,我们需要对login_msg结构体中添加一个枚举类型,代表性别,在C++中可以这样写:

namespace datePackage{struct date_msg{int year;int month;int day;};date_msg msg;
}namespace loginPackage{enum Sex{MALE = 0;FEMALE,SECRET};struct date_msg{int year;int month;int day;};std::string user;std::string password;datePackage::date_msg date;date_msg msg date2;
}

protobuf中,同样支持enum字段,但是需要注意的是,在C++中,enum字段的第一个值可以是任意值,但是在protobuf中,第一个字段的值必须是0

syntax = "proto3";package loginPackage;
import "date.proto";enum Sex{MALE = 0;FEMALE = 1;SECRET = 2;
}message date_msg{int32 year = 1;int32 month = 2;int32 day = 3;
}message login_msg{bytes user = 1;bytes password = 2;datePackage.date_msg date = 3;date_msg date2 = 4;repeated bytes emalis = 5;Sex sex = 6;
}

3.5.2 序列化数据

序列化enum类型和普通类型一致,这里存在命名空间,因此前面添加上命名空间的前缀即可:

loginMsg.set_sex(loginPackage::FEMALE);

login.pb.h中生成了这样的枚举类型,后面两个字段是Protobuf 内部实现的一部分,一般不使用:

在这里插入图片描述

3.5.3 反序列化

我们在调试的时候,输出调试信息得到的枚举类型是对应的枚举名称,比如这里的FEMALE

std::cout << pLoginMsg->DebugString() << std::endl;

实际上,我们如果直接通过反序列化得到的枚举值是对应的枚举数值,比如FEMALE对应的1

std::cout << "sex = " << pLoginMsg->sex() << std::endl;

3.5.4 运行结果

完整的代码如下:

#include"protobuf/login.pb.h"
#include<iostream>//序列化 
void serializeProtobuf(loginPackage::login_msg* pLoginMsg,std::string& serializedMsg){pLoginMsg->SerializeToString(&serializedMsg);std::cout << "===== serialize =====" << std::endl;std::cout << pLoginMsg->DebugString() << std::endl;
}//反序列化
void deserializeProtobuf(loginPackage::login_msg*pLoginMsg,std::string& serializedMsg){pLoginMsg->ParseFromString(serializedMsg);
}void printResult(loginPackage::login_msg*pLoginMsg){std::cout << "===== deserialzie ======" << std::endl;std::cout << "user = " << pLoginMsg->user() << std::endl;std::cout << "password = " << pLoginMsg->password() << std::endl;std::cout << "sex = " << pLoginMsg->sex() << std::endl;std::cout << "===== emailis =====" << std::endl;std::cout << "emalis(0) = " << pLoginMsg->emalis(0) << std::endl;;std::cout << "emalis(1) = " << pLoginMsg->emalis(1) << std::endl;;std::cout << "===== date =====" << std::endl;std::cout << "year = " << pLoginMsg->date().year() << std::endl;std::cout << "month = " << pLoginMsg->date().month() << std::endl;std::cout << "day = " << pLoginMsg->date().day() << std::endl;std::cout << "===== date2 =====" << std::endl;std::cout << "year = " << pLoginMsg->date2().year() << std::endl;std::cout << "month = " << pLoginMsg->date2().month() << std::endl;std::cout << "day = " << pLoginMsg->date2().day() << std::endl;}int main(){//初始化loginPackage::login_msg loginMsg;loginMsg.set_user("Antonio");loginMsg.set_password("xx123456");loginMsg.add_emalis("123456789@qq.com");loginMsg.add_emalis("23333@163.com");loginMsg.set_sex(loginPackage::FEMALE);datePackage::date_msg* date = loginMsg.mutable_date();date->set_year(2025);date->set_month(7);date->set_day(21);loginPackage::date_msg* date2 = loginMsg.mutable_date2();date2->set_year(2024);date2->set_month(6);date2->set_day(20);//序列化std::string loginByteStream;serializeProtobuf(&loginMsg,loginByteStream);//反序列化loginPackage::login_msg parsedLoginMsg;deserializeProtobuf(&parsedLoginMsg,loginByteStream);//获取结果printResult(&parsedLoginMsg);return 0;
}

查看调试信息中的枚举值与反序列化中的枚举值的不同:

在这里插入图片描述

更多资料:https://github.com/0voice

http://www.dtcms.com/a/291385.html

相关文章:

  • Prometheus+altermanager搭配钉钉报警
  • 【PTA数据结构 | C语言版】旅游规划
  • qwen 提示词
  • 试用SAP BTP 02B:试用SAP HANA Cloud
  • Spring处理器和Bean的生命周期
  • Jenkins 不同节点间文件传递:跨 Job 与 同 Job 的实现方法
  • 西门子 WinCC预定义报警控件过滤条件
  • 尚庭公寓的结构
  • claude code提示词设计
  • 【前端】jszip+file-saver:多个视频url下载到zip、页面预加载视频、预览视频、强制刷新视频
  • AV1平滑缓冲区
  • 闲庭信步使用图像验证平台加速FPGA的开发:第二十七课——图像腐蚀的FPGA实现
  • Spring Boot05-热部署
  • Android开发中ANR治理方案
  • RSTP协议
  • Windows 编程辅助技能:联机搜索
  • Ubuntu 安装 Odoo 17 详细教程
  • 网络协议与层次对应表
  • Spring 中的 Bean 作用域(Scope)有哪些?各自适用于什么场景?
  • Android Studio 的 Gradle 究竟是什么?
  • Telink BLE 低功耗学习
  • Vue接口平台学习十一——业务流测试
  • AWS Certified Cloud Practitioner 认证考试总结
  • GoLand安装指南
  • docker 容器学习
  • LeetCode 刷题【10. 正则表达式匹配】
  • CCF-GESP 等级考试 2025年6月认证C++六级真题解析
  • OTA升级失败,端口占用bind: Address already in use
  • 酵母杂交技术解析
  • 微服务项目文档