protobuf 序列化

Nacos 源码分析05 通信协议和序列化_序列化

Protobuf(Protocol Buffers)协议😉 Protobuf 是一种由 Google 开发的二进制序列化格式和相关的技术,它用于高效地序列化和反序列化结构化数据,通常用于网络通信、数据存储等场景。

高效性:Protobuf 序列化后的二进制数据通常比其他序列化格式(比如超级常用的JSON)更小,并且序列化和反序列化的速度更快,这对于性能敏感的应用非常有益。

简洁性:Protobuf 使用一种定义消息格式的语法,它允许定义字段类型、顺序和规则(消息结构更加清晰和简洁)

版本兼容性:Protobuf 支持向前和向后兼容的版本控制,使得在消息格式发生变化时可以更容易地处理不同版本的通信。

语言无关性:Protobuf 定义的消息格式可以在多种编程语言中使用,这有助于跨语言的通信和数据交换(截至本文发布目前官方支持的有C++/C#/Dart/Go/Java/Kotlin/python)

自动生成代码:Protobuf 通常与相应的工具一起使用,可以自动生成代码,包括序列化/反序列化代码和相关的类。

Protobuf数据类型

Nacos 源码分析05 通信协议和序列化_序列化_02

Nacos 源码分析05 通信协议和序列化_序列化_03

Nacos 源码分析05 通信协议和序列化_序列化_04

Nacos 源码分析05 通信协议和序列化_序列化_05

Protobuf使用

1.创建proto文件

Nacos 源码分析05 通信协议和序列化_自动生成代码_06

其中 syntax = "proto3" 表示协议版本,option java_package = "com.aqin.protobuf" 表示生成的类所处的层级,option java_multiple_files = true 表示需要將生成的类拆分为多个(false 的话就是不需要),MyRequest 和 Header 是消息结构。

2.生成java文件

进入到文件📃AQin.proto所在目录下,执行如下代码

protoc -I=./ --java_out=./ ./AQin.proto
-I 用于指定 .proto 文件所在的路径(也可以用-proto_path代替)
--java_out 用于指定生成的 java 文件的路径
./AQin.proto 就是需要编译的.proto文件

需要注意的是这三个路径要使用相对路径就都使用相对路径,要使用绝对路径就都使用绝对路径(不要混着用)

Nacos 源码分析05 通信协议和序列化_自动生成代码_07

然后我们就可以通过生成的Java类来对接收到的信息进行解析,或者封装数据进对象进行发送

3.解析 封装数据

Nacos 源码分析05 通信协议和序列化_序列化_08

ProtoBuf性能为什么快

1.压缩率高

Nacos 源码分析05 通信协议和序列化_序列化_09

一条消息数据,用protobuf序列化后的大小是json的10分之一,是xml格式的20分之一,但是性能却是它们的5~100倍。

Nacos 源码分析05 通信协议和序列化_序列化_10

static int makeTag(final int fieldNumber, final int wireType) {
  return (fieldNumber << 3) | wireType;
}

fieldNumber表示后面的value所对应的字段的编号是多少,比如fieldNumber为1,就表示age,如果为2,就表示name等;wireType表示value的数据类型,以此来计算value占用字节的大小。

因为tag一般占用一个字节,开销还算是比较小的,所以protobuf整体的存储空间占用还是相对小了很多的

2.编码

在实际的传输过程中,会传递整数,我们知道整数在计算机当中占据4个字节,但是绝大部分的整数,比如价格,库存等,都是比较小的整数,实际用不了4个字节,像127这种数,在计算机中的二进制是:

00000000 00000000 00000000 01111111(4字节32位)

完全可以用最后1个字节来进行存储,protobuf当中定义了Varint这种数据类型,可以以不同的长度来存储整数,将数据进一步的进行了压缩。

在计算机当中的负数是用补码表示的,对于-1,它的二进制表示方式为:

11111111 11111111 11111111 11111111(4字节32位)

显然无法用1个字节来表示了,但-1确实是一个比较简单的数,这个时候就可以使用zigzag算法来对负数进行进一步的压缩,最终我们可以使用2个字节来表示-1。

如果value是字符串类型的,具体value有多长,我们无法从tag当中了解到,但是如果不知道value的长度,我们就不得不做字符串匹配操作,要知道字符串匹配是非常耗时的。

为了能够快速解析字符串类型的数据,protobuf在存储的时候,做了特殊的处理,分成了三部分:tag|leg|value,其中的leg记录了字符串的长度,同样使用了varint来存储,一般一个字节就能搞定,然后程序从leg后截取leg个字节的数据作为value,解析速度非常快。

Protocol Buffer 不是自我描述的,离开了数据描述 .proto 文件,就无法理解二进制数据流。这点即是优点,使数据具有一定的“加密性”,也是缺点,数据可读性极差。所以 Protocol Buffer 非常适合内部服务之间 RPC 调用和传递数据。

GRPC 通信协议

在gRPC中,客户端应用程序可以不同的机器上像调用本地方法一样,直接调用服务器应用程序上的方法,使得更容易创建分布式应用和服务。

gRPC是基于定义服务的思想,指定可以远程调用的方法及其参数和返回类型。在服务端,服务端实现此接口并运行gRPC服务器来处理客户端调用。在客户端,客户端有一个stub,提供与服务器相同的方法。

Nacos 源码分析05 通信协议和序列化_序列化_11

Nacos 源码分析05 通信协议和序列化_自动生成代码_12

RPC 会给对应的服务接口名生成一个代理类,即客户端 Stub。使用代理类可以屏蔽掉 RPC 调用的具体底层细节,使得用户无感知的调用远程服务。

客户端 Stub 会将当前调用的方法的方法名、参数类型、实参数等根据协议组装成网络传输的消息体,将其序列化成二进制流后,通过 Sockect 发送给 RPC 服务端。

服务端收到二进制数据流后,根据约定的协议解析出请求数据,然后反序列化得到参数,通过内部路由找到具体调用的方法,调用该方法拿到执行结果,将其序列化二进制流后,通过 Socket 返回给 RPC 客户。


缺省情况下,gRPC使用protocol buffers作为接口定义语言(IDL)来描述服务接口和负载消息的结构

service HelloService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}
 
message HelloRequest {
  string greeting = 1;
}
 
message HelloResponse {
  string reply = 1;
}
  1. Unary 模式:即请求响应模式
  2. Client Streaming 模式:Client 发送 多次,Server 回复一次
  3. Server Streaming 模式:Client 发送一次,Server 发送多次
  4. 双向 Streaming 模式:Client/Server 都发送多次

Nacos 源码分析05 通信协议和序列化_自动生成代码_13

gRPC 服务端创建采用 Build 模式,对底层服务绑定、NettyServer和gRPC Server 的创建和实例化做了封装和屏蔽,让服务调用者不用关心 RPC 调用细节。

通信协议基于标准的 HTTP/2 设计,支持·双向流、消息头压缩、单 TCP 的多路复用、服务端推送等特性,这些特性使得 gRPC 在移动端设备上更加省电和节省网络流量;

gRPC大量使用HTTP/2功能,没有浏览器提供支持gRPC客户机的Web请求所需的控制级别。gRPC Web并非支持所有gRPC功能。不支持客户端和双向流,并且对服务器流的支持有限。