Protoc Buffer 优化传输大小的一个细节

Protoc Buffer 是我们比较常用的序列化框架,Protocol Buffer 序列化后的占空间小,传输高效,可以在不同编程语言以及平台之间传输。今天这篇文章主要介绍 Protocol Buffer 使用 VarInt32 减少序列化后的数据大小。

VarInt32 编码

VarInt32 (vary int 32),即:长度可变的 32 为整型类型。一般来说,int 类型的长度固定为 32 字节。但 VarInt32 类型的数据长度是不固定的,VarInt32 中每个字节的最高位有特殊的含义。如果最高位为 1 代表下一个字节也是该数字的一部分。因此,表示一个整型数字最少用 1 个字节,最多用 5 个字节表示。如果某个系统中大部分数字需要 >= 4 字节才能表示,那其实并不适合用 VarInt32 来编码。下面以一个例子解释 VarInt32 的编码方式:

以 129 为例,它的二进制为 1000 0001 。由于每个字节最高位用于特殊标记,因此只能有 7 位存储数据。第一个字节存储最后 7 位 (000 0001),但并没有存下所有的比特,因此最高位置位 1,剩下的部分用后续字节表示。所以,第一个字节为:1000 0001第二个字节只存储一个比特位即可,因此最高位为 0 ,所以,第二个字节为:0000 0001这样,我们就不必用 4 字节的整型存储 129 ,可以节省存储空间

在 Protoc buffer 中,每一个 ProtoBuf 对象都有一个方法 public void writeDelimitedTo(final OutputStream output),该方法将 ProtoBuf 对象序列化后的长度以及序列化数据本身写入到输出流 output 中。多个对象调用该方法可以将序列化后的数据写入到同一个输出流。由于每次写入都有长度,所以反序列化时先解析长度,在读取对应长度的字节数据,即可解析出每个对象。该方法中对序列化后长度的编码便使用 VarInt32,因为一个 Protobuf 对象序列化后的长度不会太大,因此使用 VarInt32 编码能够有效的节省存储空间。接下来我们看下 Protoc Buffer 中如何实现 VarInt32 编码,跟进 writeDelimitedTo 方法,可以看到 VarInt32 编码的源码如下:

  /**
   * Encode and write a varint.  {@code value} is treated as
   * unsigned, so it won‘t be sign-extended if negative.
   */
  public void writeRawVarint32(int value) throws IOException {
    while (true) {
      if ((value & ~0x7F) == 0) {//代表只有低7位有值,因此只需1个字节即可完成编码
        writeRawByte(value);
        return;
      } else {
        writeRawByte((value & 0x7F) | 0x80);//代表编码不止一个字节,value & 0x7f 只取低 7 位,与 0x80 进行按位或(|)运算为了将最高位置位 1 ,代表后续字节也是改数字的一部分
        value >>>= 7;
      }
    }
  }

该方法对 int 类型的值进行 VarInt32 编码,可以验证最多 5 个字节即可完成编码。

VarInt32 解码

理解了编码后,解码就没什么可说的了。就是从输入字节流中,读取一个字节判断最高位,将真实数据位拼接成最终的数字即可。Hadoop RPC 中使用了 Protoc Buffer 作为数据序列化框架。其中,Hadoop 针对 writeDelimitedTo 方法实现了对 VarInt32 的解码。源码如下:

/**
   * Read a variable length integer in the same format that ProtoBufs encodes.
   * @param in the input stream to read from
   * @return the integer
   * @throws IOException if it is malformed or EOF.
   */
  public static int readRawVarint32(DataInput in) throws IOException {
    byte tmp = in.readByte();
    if (tmp >= 0) {// tmp >= 0 代表最高位是 0 ,否则 tmp < 0 代表最高位是 1 ,需要继续往下读
      return tmp;
    }
    int result = tmp & 0x7f;
    if ((tmp = in.readByte()) >= 0) {
      result |= tmp << 7;
    } else {
      result |= (tmp & 0x7f) << 7;
      if ((tmp = in.readByte()) >= 0) {
        result |= tmp << 14;
      } else {
        result |= (tmp & 0x7f) << 14;
        if ((tmp = in.readByte()) >= 0) {
          result |= tmp << 21;
        } else {
          result |= (tmp & 0x7f) << 21;
          result |= (tmp = in.readByte()) << 28;
          if (tmp < 0) {//我们说 VarInt32 最多 5 个字节表示,当程序执行到这里,tmp < 0,说明,编码格式有问题// Discard upper 32 bits.
            for (int i = 0; i < 5; i++) {
              if (in.readByte() >= 0) {
                return result;
              }
            }
            throw new IOException("Malformed varint");
          }
        }
      }
    }
    return result;
  }

在 Hadoop 源码中并没有使用循环去解码,而是使用多个 if 条件判断,根据 tmp 的正负号来判断最高位是否是 1。如果读取的该数字用了 5 个字节编码,当读到了第 5 个字节,理论上 tmp 应该大于 0 。但是如果 tmp 小于 0 ,说明编码格式有问题。在 Hadoop 源码中程序会继续往下读,最多再向下读 5 个字节且丢掉最高位仍然 < 0 的字节。如果在该过程某个字节最高位为 0 ,便停止读取直接返回。这个处理逻辑在其他框架源码中也有出现。

看完 Hadoop 的源码,我们在看看 Protoc Buffer 自己提供的解析源码:

  /**
   * Like {@link #readRawVarint32(InputStream)}, but expects that the caller
   * has already read one byte.  This allows the caller to determine if EOF
   * has been reached before attempting to read.
   */
  public static int readRawVarint32(
      final int firstByte, final InputStream input) throws IOException {
    if ((firstByte & 0x80) == 0) {
      return firstByte;
    }

    int result = firstByte & 0x7f;
    int offset = 7;
    for (; offset < 32; offset += 7) {
      final int b = input.read();
      if (b == -1) {
        throw InvalidProtocolBufferException.truncatedMessage();
      }
      result |= (b & 0x7f) << offset;
      if ((b & 0x80) == 0) {
        return result;
      }
    }
    // Keep reading up to 64 bits.
    for (; offset < 64; offset += 7) {
      final int b = input.read();
      if (b == -1) {
        throw InvalidProtocolBufferException.truncatedMessage();
      }
      if ((b & 0x80) == 0) {
        return result;
      }
    }
    throw InvalidProtocolBufferException.malformedVarint();
  }

可以看到 Protoc Buffer 自己提供的解码方式与 Hadoop 是一样的,包括遇到错误的编码时候的异常处理方式也是一样的。

小结

本篇文章主要介绍了 VarInt32 编解码,VarInt32 表示一个整型数字最少用 1 个字节, 最多用 5 个字节。所以在传输数字大部分都比较小的场景下适合使用。当然,我们也可以用 VarInt64 来表示长整型的数字。 在介绍 VarInt32 的同时我们也看到了 ProtoBuf 和 Hadoop 这样的框架在传输数据的优化上不放过任何一个细节,值得我们学习。

原文地址:https://www.cnblogs.com/duma/p/11111427.html

时间: 2024-08-10 14:33:46

Protoc Buffer 优化传输大小的一个细节的相关文章

hashMap源码中的一个细节问题

public V put(K key, V value) {        if (key == null)           return putForNullKey(value);        int hash = hash(key.hashCode());         int i = indexFor(hash, table.length);        for (Entry<K,V> e = table[i]; e != null; e = e.next) {        

packetdrill框架点滴剖析以及TCP重传的一个细节

本来周末想搞一下scapy呢,一个python写的交互式数据包编辑注入框架,功能异常强大.然而由于python水平太水,对库的掌握程度完全达不到信手拈来的水平,再加上前些天pending的关于OpenVPN的事情,还有一系列关于虚拟网卡的事情,使我注意到了一个很好用的packetdrill,可以完成本应该由scapy完成的事,恰巧这个东西跟我最近的工作也有关系,就抛弃scapy了,稍微研究了一下它的基本框架,写下本文. packetdrill的框架概览 喜欢packetdrill是因为它让我想起

关于Linux LOOPBACK网口抓包的一个细节

这个问题其实是我几个月前碰到,只是那时好像还在回忆着什么,心系上海,还没有完全适应这个新环境,加上这个问题也不是什么太深奥的问题,觉得太简单了,就搁置了.今天周末闲来无事就顺便写来来了.加上深圳经常下雨,越来越喜欢了. 本文没什么深度,仅为记录,以及阐述一个"看文档学习原理->猜测并自行实现->对比标准实现确认"的方法. 问题是这样的:在Linux上如果使用tcpdump去抓取lo口的数据包,你只能抓到一遍,而不是两遍,按常理来讲,数据包在outgoing路径上和incom

android创建一个细节页面,以及argument的应用

大家好..以前我也好奇过有没有一些设计模式可以用来创建一个细节页面,直到最近我看了一些设计模式相关的博文和书后,我终于发现了以下的应用. 下面我们介绍一个Listview 点击后出现一个detail 页面的功能,功能简单,但是我们看的是思想. 首先我们实现自定义的ListView,ListItem 里面有一个TextView,一个EditText,一个Checkbox,对应存放数据的是一个Crime的类.ListView里面有好多个ListItem,而对应的是CrimeList 类,为了方便创建

【JOB】Oracle中JOB的创建方法以及一个细节的探究

在Oracle中可以使用JOB来实现一些任务的自动化执行,类似于UNIX操作系统crontab命令的功能.简单演示一下,供参考. 1.创建表T,包含一个X字段,定义为日期类型,方便后面的定时任务测试.[email protected]ora10g> create table t (x date); Table created. 2.创建存储过程p_insert_into_t,每次执行该存储过程都会向T表中插入一条系统当前时间.[email protected]ora10g> create or

onCreateView的一个细节--Fragment

public View onCreateView(LayoutInflater inflater, ViewGroup contaiiner, Bundle savedInstanceState) 在写一个Fragment的时候,继承Fragment基类,然后,要重写的其中一个回调方法是onCreateView.如果该Fragment有界面,那么,返回的View是非空的:如果该Fragment 是没有界面的,返回的是Null. 这是在写Fragment中经常做的事情.不过,这里有个小细节,那就是

BUFFER CACHE之调整buffer cache的大小

Buffer Cache存放真正数据的缓冲区,shared Pool里面存放的是sql指令(LC中一次编译,多次运行,加快处理性能,cache hit ratio要高),而buffer cache里面存放真正的查询结果.Buffer Cache:由彼此独立的三个子cache(subcaches,也叫主buffer cache:keep,recycle,default)组成支持多种数据块的多缓冲池.注意system表空间只能用主数据块 Step1: 查看各个组件size(看buffer cache

一个细节可能使游戏毙命

让玩家讨厌一款游戏的原因有很多,通常,玩家总是说,这不是一款好游戏.有时候,你自认为自己创造出了一款出色的游戏,殊不知一些很小的细节上的缺陷不但会破坏一些玩家爱的游戏体验,而且可能会让他们真正的走向愤怒. 在笔者最喜欢的十款游戏中,<荒野大镖客:救赎>一直处于前三名的位置,2010年,我第一次接触这款游戏的时候,有一个瞬间差点让我流失了. 玩过这款游戏的人可能知道,其中有一个主人公骑着马穿过河流来到墨西哥的情节,这事你会听到一首经典的歌曲,Jose Gonzalez的Far Away这是一个十

我在小学和中学里都遇到了很好的老师(背诵很重要,做事要规范,习惯很重要,习惯是靠平时每一个细节重复出来的)

我在小学和中学里都遇到了很好的老师,现在回想起来,对我帮助最大的地方有下面几点. 一,养成了作检查的习惯.我小学四年级的时候,遇到了一位极好的数学老师.那时候刚刚学习多位数乘法,很容易算错的.他教给我们用同余的原理,分别计算两个乘数和结果除9的余数,用来验算结果.方法很简单,也实际提高了考试的分数,但最使我受益终身的是,从此养成了进行验算的习惯,这在后来的学习和工作中对我的帮助极大.通常在得出一个结果之后,特别是经过复杂推导和计算才得出的,我都会试着另用某种近似的办法重新估算一次,作为验证. 二