Linux高级--3.3.2 自定义协议设计--ProtoBuf
一、自定义协议设计的必要性
自定义通信协议通常有以下几个原因,尤其在IM即时通信、节点服务器、HTTP协议、Nginx、Redis协议、SOME/IP协议和DoIP协议等场景中,设计和使用自定义协议能带来特定的优势:
1. 性能优化
- 更高效的资源利用:标准协议(如HTTP、TCP/IP)通常是通用的,能够应对各种应用需求,但它们可能存在冗余的功能或无法针对特定场景优化。自定义协议可以移除不必要的开销,减少数据包的大小,从而提升传输效率,降低延迟,节省带宽和计算资源。
- 低延迟通信:对于需要实时响应的应用(如IM即时通信),自定义协议可以避免标准协议中存在的传输延迟,采用更精简、更高效的通信方式。
2. 安全性要求
- 定制加密方式:自定义协议可以根据业务需求定制加密和验证机制,以确保数据传输的安全性。例如,IM协议可能需要自定义的加密和身份验证机制,以保护消息内容。
- 数据保护:在一些敏感场景中,可能需要对数据传输进行额外的保护和校验,自定义协议允许开发者根据需求设计数据完整性检查、异常处理等安全特性。
3. 功能适配
- 业务需求驱动:标准协议如HTTP和TCP/IP设计时并没有针对特定行业或业务场景进行优化。例如,SOME/IP 是为汽车行业的服务通信而设计的协议,它能满足汽车电子系统对低延迟、实时性以及复杂服务交互的需求。
- 特殊的数据格式和字段:一些行业特定的协议需要定义特定格式的数据包,或者需要在传输过程中携带特定的元数据。例如,DoIP协议(诊断通信协议)在汽车诊断中需要对车辆的ECU(电子控制单元)进行通信,而其数据格式和通信方式与标准HTTP或TCP/IP并不相同。
4. 兼容性和扩展性
- 支持独特设备和系统:有时系统中存在不同类型的设备和服务,它们可能无法直接使用标准的协议进行通信。通过自定义协议,可以确保不同设备和平台之间的兼容性。例如,Nginx的协议可能会根据具体应用场景进行调整,优化与后端服务的通信。
- 可扩展性:自定义协议可以根据系统发展的需求不断扩展,支持新的功能和服务。例如,Redis协议是为高效缓存和数据存储设计的,而它的扩展性使得它可以很好地适应不同的应用场景和数据存储需求。
5. 简化协议解析
- 减少协议复杂性:标准协议,如HTTP、TCP/IP等具有复杂的状态机和处理流程。对于一些需求较为简单的场景(如IM通信、Redis缓存操作等),自定义协议能够减少不必要的协议复杂性,简化开发和维护。
- 紧耦合业务和协议:当业务逻辑非常依赖协议时,开发者可以直接根据业务需求定制协议,从而减少协议与应用层之间的适配成本。
6. 跨平台通信
- 自定义协议可以在不同平台和技术栈之间架起桥梁,尤其是对于多种设备、操作系统和网络环境下的系统,协议的标准化和定制可以保证跨平台的兼容性和通信顺畅。例如,DoIP协议就用于诊断设备与汽车之间的通信,它能适应不同的硬件环境。
总结
总的来说,自定义通信协议能帮助开发者根据应用的特定需求进行优化,以提高性能、安全性、扩展性等方面的表现。在IM即时通信、节点服务器、HTTP协议、Nginx、Redis协议、SOME/IP协议、DoIP协议等场景中,通用协议虽然普遍适用,但自定义协议能为特定领域的应用提供更高效、更安全、更灵活的解决方案。
二、常见几种协议介绍
自定义通信协议通常是为了满足特定应用场景、性能需求或是兼容性要求。针对你提到的这些协议,下面逐个分析自定义通信协议的原因:
1. IM即时通信协议:
- 原因:即时通信(IM)服务需要低延迟、高频率的消息交换。如果采用通用的通信协议(如HTTP),可能会导致性能瓶颈,尤其在高并发情况下。自定义IM协议可以根据具体需求,优化消息格式、传输方式、数据压缩和加密等,提高通信效率和可靠性。
- 特点:通常会关注实时性、持久连接、消息顺序、离线消息等特性。
2. 节点服务器通信协议:
- 原因:在分布式系统中,不同节点之间的通信往往有特定的需求,比如带宽、延迟、数据结构等。标准的协议(如HTTP或TCP)可能不适合高效地处理这些通信,尤其是在高并发、大数据量和低延迟场景下。自定义节点间协议可以提供更高效的消息传递、更低的资源消耗和更强的容错性。
- 特点:常见应用场景包括微服务架构、物联网(IoT)、边缘计算等。
3. HTTP协议:
- 原因:虽然HTTP协议是Web开发中最常用的协议,但它并不总是最优的。它是基于请求/响应模型的,通常开销较大,尤其在需要高频率通信或低延迟时。而且,HTTP是文本协议,对于数据结构复杂或需要二进制数据交换的场景可能不够高效。因此,很多系统会自定义HTTP协议的扩展(如WebSocket、gRPC)来优化性能。
- 特点:需要高效传输、降低协议开销、实现流式通信等。
4. Nginx协议:
- 原因:Nginx本身是一个Web服务器,但它的性能优化和灵活的配置能力使得它成为反向代理、负载均衡等功能的核心组件。自定义协议可能是为了适配特定的负载均衡策略、缓存控制或请求调度,以便更好地处理流量、降低延迟。
- 特点:通常用于高并发、高流量的Web服务场景。
5. Redis协议:
- 原因:Redis使用自定义协议(RESP)来实现高效的命令与响应交换。自定义协议让Redis能够快速、低开销地处理各种操作,尤其是在内存数据库场景下,要求极低的延迟和高吞吐量。通过自定义协议,Redis能够在高并发情况下保持其高效性。
- 特点:强调高性能、简洁、高效的数据交换。
6. SOME/IP协议:
- 原因:SOME/IP(Scalable service-Oriented Middleware over IP)协议用于车载系统中,具有特定的实时性、可靠性和可扩展性需求。为了满足汽车行业对数据交换高效性和安全性的要求,SOME/IP协议被设计为可以支持大规模分布式系统的服务发现、消息传输和远程调用。通过自定义协议,可以更好地满足车载网络中复杂的通信需求。
- 特点:针对汽车、智能驾驶等领域的高效数据交换和服务发现,具备低延迟、可靠性和实时性要求。
7. DoIP协议:
- 原因:DoIP(Diagnostics over IP)协议用于汽车诊断系统,目的是通过IP网络进行车辆诊断通信。与传统的OBDII(车载诊断)协议相比,DoIP协议允许在更大的网络范围内进行诊断、控制和数据传输,提供了更高的灵活性、可扩展性和安全性。自定义协议能够确保在复杂汽车电子系统中,诊断信息传输的高效性和安全性。
- 特点:专门针对汽车行业的诊断、测试和控制需求,能满足实时、低延迟的数据交换。
三、协议header的具体设计
以下是这些协议的Header设计和具体构成的简要介绍。我会为每个协议列出其常见的Header字段和排列方式,并使用表格形式进行呈现。
1. IM即时通信协议(如Protobuf、XMPP等)
IM协议的Header设计取决于具体的实现,但通常会包含以下字段:
字段名称 | 描述 |
---|---|
消息ID | 唯一标识消息的ID,用于去重和追踪消息 |
发送者 | 消息的发送者(可以是用户ID或设备ID) |
接收者 | 消息的接收者(可以是用户ID或设备ID) |
时间戳 | 消息的时间戳,用于排序和确保消息的顺序 |
消息类型 | 表示消息类型(文本、图片、视频等) |
加密标志 | 标识消息是否加密 |
消息体长度 | 指示消息体部分的字节长度 |
消息体 | 实际的消息内容 |
IM协议在序列化时会根据不同需求设计相应的Header,并且为了效率,通常采用二进制序列化(如Protobuf、Thrift)来减少数据量。
2. 节点服务器通信协议(如gRPC)
gRPC使用Protocol Buffers作为序列化方式,Header部分通常包括以下字段:
字段名称 | 描述 |
---|---|
消息ID | 唯一标识请求或响应 |
请求类型 | 请求类型(如:Unary、Server Streaming等) |
时间戳 | 请求或响应的时间戳 |
调用方法 | gRPC方法的名称 |
服务名 | gRPC服务的名称 |
压缩标志 | 标识消息是否被压缩 |
元数据 | 附加的元数据字段(如认证信息、会话信息等) |
消息体长度 | 消息体的长度 |
消息体 | 实际的数据内容(如:请求参数或响应数据) |
gRPC的Header设计高度优化,采用二进制格式传输,序列化时使用Protobuf来保证高效传输。
3. HTTP协议
HTTP协议的Header设计比较简单,结构化的文本格式。
字段名称 | 描述 |
---|---|
请求方法 | HTTP方法(如:GET、POST、PUT、DELETE等) |
请求路径 | 请求的资源路径 |
协议版本 | HTTP协议版本(如:HTTP/1.1、HTTP/2) |
主机 | 请求的主机名 |
用户代理 | 客户端的用户代理信息(浏览器类型等) |
内容类型 | 请求体或响应体的媒体类型(如:application/json、text/html等) |
内容长度 | 请求体或响应体的大小(字节数) |
接受 | 客户端支持的响应内容类型 |
Authorization | 用于认证的Authorization字段 |
Cookie | 客户端的Cookie信息 |
HTTP协议的Header是文本格式,通常没有自定义字段,但可以通过扩展机制添加自定义Header。
4. Nginx协议
Nginx协议本身并不定义数据的序列化方式,而是用于传输各种应用协议的数据。因此,Nginx作为反向代理时,Header取决于实际应用协议,例如HTTP、WebSocket等。对于反向代理,常见的Header包括:
字段名称 | 描述 |
---|---|
Host | 请求目标主机 |
X-Real-IP | 客户端的真实IP地址 |
X-Forwarded-For | 代理链中的客户端IP地址 |
X-Forwarded-Proto | 请求协议(如:HTTP、HTTPS) |
User-Agent | 客户端用户代理信息 |
Content-Length | 请求体或响应体的大小 |
Nginx通过修改或转发请求Header来进行反向代理。
5. Redis协议(RESP)
Redis的序列化协议(RESP)比较简单,由文本命令和数据类型组成。
字段名称 | 描述 |
---|---|
命令类型 | 请求类型,如:SET 、GET 、DEL 等 |
参数数量 | 参数的数量 |
数据类型标志 | 数据类型标志(如:$表示字符串,*表示数组) |
数据 | 实际的数据内容(如:字符串、数字、数组等) |
RESP协议的Header非常简洁,采用文本协议传输,命令和数据的结构清晰且易于解析。
6. SOME/IP协议
SOME/IP协议用于车载系统中的服务发现和消息传输,其Header设计包含服务发现、事件通知、RPC请求等不同类型的数据。
字段名称 | 描述 |
---|---|
消息长度 | 整个消息的字节长度 |
协议版本 | SOME/IP协议版本号 |
服务ID | 服务的标识符 |
事件ID | 事件的标识符 |
实例ID | 服务或事件的实例ID |
方法ID | RPC方法的标识符 |
消息ID | 消息的唯一标识符 |
传输标志 | 用于控制传输的标志,如确认、压缩等 |
负载数据长度 | 负载部分的字节长度 |
负载数据 | 实际的数据内容 |
SOME/IP协议高度关注低延迟和高可靠性,其Header设计支持多种消息类型,包括服务发现、RPC调用和事件通知。
7. DoIP协议
DoIP协议用于汽车诊断系统,它的Header设计专门用于诊断数据的传输。常见的字段包括:
字段名称 | 描述 |
---|---|
消息ID | 消息的唯一标识符 |
协议版本 | DoIP协议的版本号 |
会话ID | 会话标识符 |
诊断命令 | 诊断请求的命令类型 |
消息长度 | 消息体的长度 |
响应码 | 对请求的响应码 |
诊断数据 | 诊断数据的实际内容 |
DoIP协议的Header相对简洁,重点关注诊断信息的传输,包括会话管理、命令和响应。
总结
这些协议的Header设计各具特色,针对不同的应用场景和需求优化了字段构成。简单来说:
- IM协议和节点通信协议倾向于采用二进制格式(如Protobuf、Thrift)以提高效率。
- HTTP协议采用文本格式的Header,容易调试但可能相对较慢。
- Redis协议采用RESP协议,通过简洁的文本结构来传递命令和数据。
- SOME/IP和DoIP协议则主要面向汽车和嵌入式应用,Header设计多为二进制且带有服务标识和命令信息。
每种协议的Header设计都是为了优化其特定领域的性能和功能需求。
四、payload 与 序列化
序列化简介
序列化是指将数据结构或对象转换为可以存储或传输的格式的过程,通常是将内存中的数据表示(如对象、结构体等)转换为字节流。这样,数据可以通过网络传输、写入磁盘或数据库等。在网络通信中,序列化的目的是将复杂的数据结构以一种高效、平台无关的方式发送给远程系统,而接收方可以将字节流反序列化成原始数据结构。
序列化通常有两种主要的方式:
- 二进制序列化:直接将数据结构转换为二进制格式,通常更高效,但需要明确的数据格式标准。
- 文本序列化:将数据转换为可读的文本格式(如JSON、XML),易于调试和查看,但相对于二进制序列化,效率较低。
接下来,我们分别介绍下不同协议的序列化方式。
1. IM即时通信协议的序列化方式
IM协议通常使用高效的二进制序列化方式,因为即时通信要求低延迟、高频率的数据交换。常见的序列化格式包括:
- Protocol Buffers(Protobuf):Google提出的二进制序列化协议,具有较高的压缩率和解析效率,广泛用于即时通信协议中。
- Thrift:Apache的跨语言服务开发框架,它也提供了高效的二进制序列化方式。
- JSON:虽然不如Protobuf高效,但JSON作为文本格式在开发调试中常被使用。
例如,某些IM协议中消息可能通过Protobuf序列化来传输。Protobuf提供了一种紧凑且高效的二进制格式,它适用于需要快速交换大量小消息的场景。
2. 节点服务器通信协议的序列化方式
节点之间的通信协议通常需要在高效和简洁之间取得平衡。常见的序列化方式包括:
- Protocol Buffers(Protobuf):它支持多种语言,并且高效,特别适合于网络通信。
- FlatBuffers:与Protobuf类似,但它提供了更快的反序列化速度,并支持零拷贝读取,这对于低延迟、高频数据交换尤为重要。
- MessagePack:是JSON的二进制形式,相比JSON更加紧凑且高效,常用于需要兼容JSON的场景。
3. HTTP协议的序列化方式
HTTP协议本身是文本协议,但在实际应用中,HTTP常与以下几种序列化方式结合使用:
- JSON:最常见的文本序列化方式。HTTP请求和响应中的数据通常会以JSON格式进行传输。
- XML:早期的Web服务标准,虽然在现代应用中较少使用,但一些旧的系统仍然采用XML序列化。
- Protocol Buffers(Protobuf):尤其是在需要更高效通信(如微服务或移动应用)的情况下,Protobuf也常通过HTTP进行传输。
例如,现代Web API通常通过HTTP协议传输JSON数据,但在性能要求较高的场景中,也可以使用Protobuf通过HTTP/2进行通信。
4. Nginx协议的序列化方式
Nginx本身并不直接定义序列化方式,而是作为反向代理服务器将不同协议的数据传输进行转发。在Nginx作为负载均衡和反向代理时,常见的协议序列化方式有:
- JSON:在负载均衡的后端服务之间,JSON常用来传输数据。
- Protobuf/Thrift:在微服务架构中,Nginx可能会代理到使用Protobuf或Thrift的后端服务,尤其是当通信要求高效时。
5. Redis协议的序列化方式
Redis协议使用的是一种自定义的**RESP(Redis Serialization Protocol)**协议,它是一种简单且高效的文本协议。它的序列化方式如下:
- 简单文本格式:RESP格式是以简单的文本表示不同类型的对象(如字符串、整数、数组等)。这种序列化方式非常轻量,特别适合于快速交换数据。
- 二进制支持:虽然RESP本身是基于文本的,但也支持二进制数据的传输,这对于存储图片、音频等文件类型的数据非常有用。
6. SOME/IP协议的序列化方式
SOME/IP协议是一种针对车载网络的服务发现和数据交换协议,其序列化方式通常是基于二进制的:
- 自定义二进制格式:SOME/IP协议通过定义一个标准化的二进制格式来进行数据交换,这种格式包括了消息头、消息体、服务标识符等字段。它需要确保高效且准确地传输车辆数据。
- Service Discovery and Serialization:服务发现消息通常使用特定的二进制格式进行序列化和传输,以便在网络中自动发现可用的服务。
7. DoIP协议的序列化方式
DoIP协议用于汽车诊断,基于IP网络传输诊断消息。它的序列化方式通常也是二进制格式,符合汽车行业的通信要求:
- 自定义二进制格式:DoIP使用定制的二进制格式来编码和解码诊断信息,保证数据的紧凑性和高效性。
- 诊断数据传输:诊断消息包括车辆的各种控制信息,使用二进制序列化格式来确保传输过程的稳定性和精确性。
8.序列化、反序列的速度对比(测试10万次)
cJSON速度性能比其它json库要强,作为server端处理大量业务时的首选。如果是客户端可以考虑jsoncpp 使用简单和简洁。
protobuf 不管是速度还是内存占用 比其它的序列化方式要优秀。后面我们着重讲解下这个。
总结
不同协议的序列化方式依据其应用场景和性能要求而有所不同。常见的方式包括:
- 二进制序列化:如Protobuf、Thrift、MessagePack、FlatBuffers等,通常应用于高性能、低延迟的通信场景。
- 文本序列化:如JSON、XML,主要用于开发调试、易于查看的场景,或当网络传输不需要特别优化时使用。
五、protobuf的安装使用
1. 安装Protobuf编译器(protoc)和库
- 下载并安装编译器:
- 访问Protobuf的GitHub发布页面下载适合你操作系统的编译器版本。
- 解压下载的文件,并将
protoc
(编译器可执行文件)的路径添加到系统的环境变量中。
- 安装Protobuf库:
- 对于Linux,你可以使用包管理器(如
apt
或yum
)来安装Protobuf库,或者从源代码编译并安装。 - 对于Windows,你可能需要从源代码编译库,或者使用预编译的二进制文件,并将其包含在你的项目中。
- 对于Linux,你可以使用包管理器(如
2. 编写.proto文件
创建一个.proto
文件来定义你的消息结构。例如,创建一个名为person.proto
的文件,内容如下:
syntax = "proto3";
package tutorial;
message Person {
string name = 1;
int32 id = 2;
string email = 3;
}
3. 编译.proto文件
使用protoc
编译器将.proto
文件编译成C++代码。在命令行中运行以下命令:
protoc --cpp_out=. person.proto |
这将生成两个文件:person.pb.h
和person.pb.cc
。
4. 在C++项目中使用生成的代码
- 包含头文件:
在你的C++源文件中包含生成的头文件。
#include "person.pb.h" |
-
链接Protobuf库:
确保你的编译器和链接器能够找到Protobuf库。你可能需要在编译命令中添加-lprotobuf
(对于Linux)或设置相应的库路径(对于Windows)。 -
使用Protobuf消息:
使用生成的Person
类来创建、序列化和反序列化消息。
#include <iostream>
#include <fstream>
#include "person.pb.h"
int main() {
// 创建一个Person对象并设置其字段
tutorial::Person person;
person.set_name("John Doe");
person.set_id(1234);
person.set_email("johndoe@example.com");
// 序列化Person对象到文件
std::ofstream output("person.ser", std::ios::binary);
person.SerializeToOstream(&output);
output.close();
// 从文件反序列化Person对象
tutorial::Person new_person;
std::ifstream input("person.ser", std::ios::binary);
new_person.ParseFromIstream(&input);
input.close();
// 输出反序列化后的对象
std::cout << "Name: " << new_person.name() << std::endl;
std::cout << "ID: " << new_person.id() << std::endl;
std::cout << "Email: " << new_person.email() << std::endl;
return 0;
}
5. 编译和运行C++程序
确保你的编译器能够找到Protobuf的头文件和库文件,然后编译你的C++程序。例如,如果你使用的是g++,你可以运行以下命令:
g++ -std=c++11 -I/path/to/protobuf/include -L/path/to/protobuf/lib -lprotobuf -o my_program my_program.cpp person.pb.cc |
注意,你需要将/path/to/protobuf/include
替换为Protobuf头文件所在的目录,将/path/to/protobuf/lib
替换为Protobuf库文件所在的目录。
最后,运行编译生成的可执行文件:./my_program
这将输出序列化和反序列化后的Person
对象的信息。
六、.proto
文件中的message
对象与C++类的关系
- 消息类型与C++类:
.proto
文件中定义的每个message
都会生成一个对应的C++类。- 这个C++类包含了与
.proto
文件中定义的字段相对应的成员变量(通常是私有的)和访问这些变量的公共方法(如set_
和get_
方法)。
- 字段与成员变量:
.proto
文件中的每个字段都会成为C++类的一个成员变量。- 字段的类型在C++类中会被转换为相应的C++类型。例如,Protobuf的
int32
类型会转换为C++的int32_t
(或者在某些平台上可能是int
),string
类型会转换为std::string
。
- 嵌套消息:
.proto
文件中可以嵌套定义消息,这些嵌套消息在C++类中会成为嵌套类或结构体。- 嵌套消息的访问通常通过外部消息类的公共方法来处理。
1. 嵌套举例
下面是一个包含嵌套消息的.proto
文件示例:
syntax = "proto3";
package example;
message Address {
string street = 1;
string city = 2;
string state = 3;
string zipcode = 4;
}
message Person {
string name = 1;
int32 id = 2;
Address address = 3; // 嵌套消息
repeated string phones = 4; // 重复字段
}
对于上面的.proto
文件,protoc
编译器会生成两个C++类:example::Address
和example::Person
。
example::Address
类会包含street
、city
、state
和zipcode
这四个成员变量以及相应的访问方法。example::Person
类会包含name
、id
、address
(类型为example::Address
)和phones
(类型为std::vector<std::string>
,因为repeated
关键字在C++中被转换为std::vector
)这四个成员变量以及相应的访问方法。
在C++代码中,你可以这样使用这些类:
#include "example.pb.h"
int main() {
// 创建一个Person对象
example::Person person;
person.set_name("John Doe");
person.set_id(1234);
// 创建并设置Address对象
example::Address* address = person.mutable_address();
address->set_street("123 Main St");
address->set_city("Anytown");
address->set_state("CA");
address->set_zipcode("12345");
// 添加电话号码
person.add_phones("555-1234");
person.add_phones("555-5678");
// ... 这里可以序列化person对象到文件或进行其他操作 ...
return 0;
}
在这个例子中,mutable_address()
方法返回address
字段的可变指针,允许你修改它的内容。add_phones()
方法用于向phones
字段添加元素。这些都是Protobuf生成的C++类提供的公共方法,用于访问和修改消息字段。
2. 数据类型
在.proto
文件中,Protocol Buffers(Protobuf)定义了多种数据类型,这些数据类型可以分为基础数据类型、复杂数据类型以及特殊数据类型。下面分别对这些数据类型进行讲解,并区分可变类型和定长类型。
基础数据类型
Protobuf 类型 | C++ 类型 | 说明 | 可变/定长 |
---|---|---|---|
bool | bool | 布尔类型 | 定长 |
int32 | int32_t (或int ) | 32位有符号整数 | 可变 |
int64 | int64_t (或long long ) | 64位有符号整数 | 可变 |
uint32 | uint32_t (或unsigned int ) | 32位无符号整数 | 可变 |
uint64 | uint64_t (或unsigned long long ) | 64位无符号整数 | 可变 |
sint32 | int32_t (使用Zigzag编码) | 有符号整数,适合负数 | 可变 |
sint64 | int64_t (使用Zigzag编码) | 有符号整数,适合负数 | 可变 |
fixed32 | uint32_t | 32位固定长度整数 | 定长 |
fixed64 | uint64_t | 64位固定长度整数 | 定长 |
sfixed32 | int32_t | 32位固定长度有符号整数 | 定长 |
sfixed64 | int64_t | 64位固定长度有符号整数 | 定长 |
float | float | 32位浮点数 | 定长 |
double | double | 64位浮点数 | 定长 |
string | std::string | 字符串 | 可变 |
bytes | std::string (以字节形式存储) | 字节序列 | 可变 |
可变类型:
- 对于整数类型(
int32
,int64
,uint32
,uint64
,sint32
,sint64
),Protobuf使用的是变长编码(Variable-Length Integer Encoding),这意味着这些类型的实际存储长度取决于整数的值。例如,较小的整数可能只需要一个字节来存储,而较大的整数可能需要更多字节。 string
和bytes
类型也是可变的,因为它们的长度取决于字符串或字节序列的内容。
定长类型:
fixed32
,fixed64
,sfixed32
,sfixed64
,float
,double
这些类型具有固定的存储长度,不受其值的影响。例如,fixed32
总是占用4个字节,double
总是占用8个字节。
复杂数据类型
enum
:定义一个枚举类型。枚举常量必须有一个整数值,且0值常量必须存在且作为第一个元素(为了与proto2的语义兼容)。message
:定义一个嵌套消息类型,可以包含其他字段(包括其他消息类型、枚举类型等)。repeated
:表示该字段可以重复,即可以包含零个或多个元素。在C++中,这通常被映射为std::vector
。map
:键值对映射类型,键和值可以是任意Protobuf支持的类型(除了另一个map类型)。oneof
:允许在多个字段中只有一个字段有值,用于节省空间。
特殊数据类型
google.protobuf.Timestamp
:表示特定的日期和时间点,通常用于时间戳字段。Any
:表示任意类型的Protobuf消息,通常用于泛型编程。
综上所述,Protobuf提供了丰富的数据类型来满足不同的需求,通过合理使用可变类型和定长类型,可以优化数据的存储和传输效率。
3. 可变类型的编码方式
在Protobuf中,可变类型的编码方式主要涉及到整数类型(如int32
, int64
, uint32
, uint64
, sint32
, sint64
)、字符串(string
)以及字节序列(bytes
)等。这些类型的编码方式旨在高效地存储和传输数据,通过可变长度的编码来节省空间。以下是对这些可变类型编码方式的详细介绍:
整数类型的编码方式
- Varint编码:
- 对于无符号整数(
uint32
,uint64
)和有符号整数(但使用普通二进制表示,如int32
,int64
,注意这里并不直接对负数进行ZigZag编码,而是先当作无符号整数处理,但在解析时需要考虑符号位),Protobuf使用Varint编码。 - Varint是一种紧凑的数字表示方法,它使用一个或多个字节来表示一个数字,其中每个字节的最高位是继续位。如果最高位为1,则表示后续还有字节;如果为0,则表示这是最后一个字节。
- 较小的整数使用较少的字节,较大的整数使用更多的字节。这种编码方式避免了固定长度整数带来的额外空间浪费。
- 对于无符号整数(
- ZigZag编码:
- 对于有符号整数(
sint32
,sint64
),Protobuf使用ZigZag编码来处理负数。 - ZigZag编码的基本原理是将符号位放到最后一位,如果是负数,则剩下的位数取反。这样可以将负数映射为正数序列中的一个数,从而避免了在Varint编码中负数可能出现的较长表示。
- 编码后的整数再经过Varint编码进行存储。
- 对于有符号整数(
字符串和字节序列的编码方式
- 对于字符串(
string
)和字节序列(bytes
),Protobuf首先计算其长度,并将长度编码为Varint。 - 紧接着,字符串或字节序列的实际内容按照UTF-8编码(对于字符串)或原始字节序列(对于字节序列)进行存储。
- 这种编码方式允许Protobuf高效地处理不同长度的字符串和字节序列,同时保证了数据的完整性和可读性。
0voice · GitHub