Protobuf 序列化

本文介绍了ProtocolBuffers(ProtoBuf),Google开发的高效、语言无关的序列化格式,特别强调其在gRPC中的应用及与XML和JSON的比较,展示了其在数据大小和解析速度的优势,包括TLV和WireType的工作原理,以及变长整形(Varint)的编码策略。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

Protocol Buffers(简称ProtoBuf)是一种语言无关、平台无关、可扩展的数据序列化格式。
它由Google开发,最初被用于解决大规模分布式系统中的数据存储与通信问题,现在也是gRPC协议的默认序列化方式。
在RPC框架中,数据序列化是非常关键的一环,序列化性能的高低直接影响RPC的调用性能。
ProtoBuf的设计目标是提供一种高效、简单、可扩展的数据交换格式。与其他常见的数据序列化格式(如XML和JSON)相比,ProtoBuf在数据大小和解析速度上都有很大的优势。

如下一段数据,使用JSON传输需要消耗25个字节,ProtoBuf仅仅需要11字节,缩小了一半多。

{
    "name":"Jackson",
    "id":1
}

消息定义

不管是服务还是消息,均通过.proto文件定义,如下是一个最简单的消息定义:

syntax = "proto3";
message Person {
  int32 id = 1;
  string name = 2;
}

第一行指定了proto文件的版本号。
第二行开始定义了一个Person消息类,它由2个字段组成,整形id和字符串name。

笔者使用Java语言,Maven引入Compiler插件,就可以把proto文件编译成Java类。
编译后的代码很长就不贴了,这里给出编译后的Person类图:
image.png
实现了MessageLite接口,方法MessageLite#toByteArray()可以把对象序列化成字节数组。
Person#parseFrom()方法可以把字节数组反序列化成对象。

序列化

如下是一个简单的序列化&反序列化示例:

public static void main(String[] args) throws Exception {
    PersonOuterClass.Person person = PersonOuterClass.Person.newBuilder()
            .setId(1).setName("Jackson")
            .build();
    // 序列化
    byte[] bytes = person.toByteArray();
    System.err.println(bytes.length);// 11
    // 反序列化
    person = PersonOuterClass.Person.parseFrom(bytes);
    System.err.println(person);
}

ProtoBuf采用TLV的数据存储方式,即Tag Length Value。其中Length部分是可选的,只有变长类型才需要,例如字符串、字节数组等。
序列化后的字节数组就是由若干个TLV紧凑的排列在一起的。
以生成的Person#writeTo()序列化代码,按照字段定义的顺序往Output写。

public void writeTo(com.google.protobuf.CodedOutputStream output)
                    throws java.io.IOException {
  if (id_ != 0) {
    output.writeInt32(1, id_);
  }
  if (!getNameBytes().isEmpty()) {
    com.google.protobuf.GeneratedMessageV3.writeString(output, 2, name_);
  }
  if (sex_ != false) {
    output.writeBool(3, sex_);
  }
  unknownFields.writeTo(output);
}

Tag用来标识字段序号和类型,采用无符号的Int32表示,Tag被设计用来表示两部分数据:

  • 低3位表示WIRE_TYPE
  • 高位表示字段序号

通过Tag就可以知道,当前这段数据是什么类型的,属于哪个字段。
生成Tag的代码:WireFormat#makeTag()

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

WIRE_TYPE的类型有:
image.png
WIRE_TYPE只占用3Bit,最多可以表示8种类型,够用吗???
目前WIRE_TYPE一共也只有6种类型,完全够用。因为ProtoBuf会把多种数据类型合并为一种类型,例如WIRE_TYPE=0,可以表示为int32,int64,uint32,unint64,bool,enum以及sint32和sint64等类型。
拿bool举例,ProtoBuf用1字节表示,0代表false,1代表true。

public final void writeBool(final int fieldNumber, final boolean value) throws IOException {
    this.writeTag(fieldNumber, 0);
    this.write((byte)(value ? 1 : 0));
}

对于变长类型,例如字符串,字节数组这些长度不确定的类型,则需要一个单独的Length字段来标记实际的Value长度。
以字节数组为例,在写入Tag后,紧接着会写一个无符号的Int32来记录长度。

public final void writeBytesNoTag(final ByteString value) throws IOException {
    this.writeUInt32NoTag(value.size());
    value.writeTo(this);
}

为了节省空间,ProtoBuf采用变长整形(Varint)的编码方式来表示整数。
每个字节的低7位用来表示数字,最高位用来表示整形是否结束。如果为1代表整形还没结束,还需要读取下一个字节,直到最高位为0才代表一个完整的整形。

  • 对于小于等于127的数值,使用一个字节表示,最高位标识为0,低7位存储数值本身;
  • 对于大于127的数值,使用多个字节表示,每个字节的最高位标识为1,低7位存储数值的一部分,最后一个字节的最高位为0。

基于以上规则,虽然单个字节表示的数字范围小了,但是基于概率学统计,大数出现的概率往往很小,采用变长整形来表示数字可以有效节省空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员小潘

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值