Protobuf 编码及序列化的记录

2023-05-16

     工作中用到了protobuf,然后之前在面试的时候面试官就问了一个问题,如果将int32类型的字段的值设置为0,那还会将该值进行序列化吗?当时是懵了的,因为自己还没有研究这部分。当时给的结果是不会,猜测protobuf中int32的默认值是0,既然默认值是0的,那应该就不会进行序列化了。

      那次面试之后就觉得自己应该了解一下这部分了,结果这两天了解完之后,发现自己猜错了。好记性不如烂笔头,也顺便记录下这两天了解到的吧。如果觉得写得有点乱了,请原谅。这里使用的是protobuf版本是2.6.1。

1. protobuf简单介绍

       即Protocol Buffer,是一个灵活的、高效的、自动化的用于对结构化数据进行序列化的协议,与XML相比,Protocol buffers序列化后的码流更小、速度更快、操作更简单,还是支持向前兼容和向后兼容的。

      protobuf中使用了反射机制,可以根据字段的名字直接得到字段的值,更深入的我还没有了解,所以需要大家去百度下了。后面了解完的话,我会再写篇博客介绍的。

      一个简单的proto文件内容如下:

message PbInfo
{
    optional uint64 uid     = 1;
    optional uint32 time    = 2;
    optional uint32 type    = 3;
    required string account = 5;    
    repeated string key     = 7;   
}

         可以看到每个字段都是由字段规则、字段类型、字段值、字段的编号组成,字段的规则有三种:optional fields(可选字段)、required fields(必须字段)、repeated fields(可重复字段),message内每个字段的编号都要是唯一的。

         那protobuf是怎么做到向前及向后兼容的呢?靠的就是这个字段的编号,在反序列化的时候,protobuf会从输入流中读取出字段编号,然后再设置message中对应的值。如果读出来的字段编号是message中没有的,就直接忽略,如果message中有字段编号是输入流中没有的,则该字段不会被设置。所以即使通信的两端存在一方比另一方多出编号,也不会影响反序列化。但是如果两端同一编号的字段规则或者字段类型不一样,那就肯定会影响反序列化了。所以一般调整proto文件的时候,尽量选择加字段或者删字段,而不是修改字段编号或者字段类型。

2. protobuf怎么知道哪些字段需要序列化

     下面的代码,是上面的文件编译生成c文件的一部分。

inline bool PbInfo::has_uid() const {
  return (_has_bits_[0] & 0x00000001u) != 0;
}
inline void PbInfo::set_has_uid() {
  _has_bits_[0] |= 0x00000001u;
}
inline void PbInfo::clear_has_uid() {
  _has_bits_[0] &= ~0x00000001u;
}
inline void PbInfo::clear_uid() {
  uid_ = GOOGLE_ULONGLONG(0);
  clear_has_uid();
}

inline void PbInfo::set_uid(::google::protobuf::uint64 value) {
  set_has_uid();
  uid_ = value;
  // @@protoc_insertion_point(field_set:proto.PbInfo.uid)
}

      通过上面的代码,我们明显的看到对于每个message,protobuf都会生成一个对应的类,并且类中会有一个_has_bits_的成员变量(位图)来记录哪个字段是被设置过的。如上面的,当调用set_uid设置uid字段的值时,就会调用set_has_uid来设置_has_bits_中对应的位,序列化的时候在根据_has_bits_的值来决定序列化哪些字段。这里字段的顺序决定每个字段对应_has_bits_中哪个位,而不是根据字段的编号。

     所以上面面试官问我的那个问题我的回答是错的,即使是设置成0,也会被序列化。

      接下来,让我们再继续深入了解下,看看protobuf是怎么序列化及反序列化的。

3. protobuf序列化与反序列化

      提前说下,我这里关于序列化与反序列化的介绍,只是大概介绍下流程而已,可能需要结合源码查看。

3.1 序列化

      序列化一般是用SerializeToString或者SerializeToArray,这里只跟踪了SerializeToArray函数,其调用的过程如下图:

protobuf序列化

        通过调用过程可以看到,序列化的最后是先用ListFields来获取message中所有被设置过的字段,然后再对每个字段调用SerializeFieldWithCachedSizes进行序列化。ListFields函数定义在generated_message_reflection.cc中,内容如下:


void GeneratedMessageReflection::ListFields(
    const Message& message,
    vector<const FieldDescriptor*>* output) const {
  output->clear();

  // Optimization:  The default instance never has any fields set.
  if (&message == default_instance_) return;

  for (int i = 0; i < descriptor_->field_count(); i++) {
    const FieldDescriptor* field = descriptor_->field(i);
    if (field->is_repeated()) {
      if (FieldSize(message, field) > 0) {
        output->push_back(field);
      }
    } else {
      if (field->containing_oneof()) {
        if (HasOneofField(message, field)) {
          output->push_back(field);
        }
      } else if (HasBit(message, field)) {
        output->push_back(field);
      }
    }
  }

  ...
}

         上面去掉了一小部分内容。可以看到,对于字段规则为repeated的,如果长度大于0则会被序列化,containing_oneof目前还没找到是什么作用,但是可以看到有个HasBit的判断,即使判断这个字段是否被设置过,如果被设置过则添加到vector中。

3.2 protobuf反序列化

         反序列化一般调用ParseFromArray或者ParseFromString,这里分析了ParseFromArray的调用过程,如下图:

protobuf反序列化

        可以看到,InlineMergeFromCodedStream中是让传入的message自己去反序列化输入的数据。而最终的反序列化函数ParseAndMergePartial中会不断调用ReadTag从输入数据中读出一个tag,再从tag中获取字段编号,进而获取到对应field,最终调用ParseAndMergeField来反序列化这个字段的数据。

         而在ParseAndMergeField函数中,则会根据对应field的类型(注意这里是根据本地proto文件,对端并不会传送字段类型的信息),调用对应的反序列化代码。例如如果是int32,则调用AddInt32或者SetInt32函数,对于enum类型,则调用SetEnum。但是注意到一点比较奇怪的,SetInt32在解析输入并将值设置到message的时候,没有调用SetBit函数去设置该message中的_has_bits_的对应位,但是在SetString和SetEnum的时候,跟进后可以看到最终都会调用SetBit函数设置_has_bits_。

// generated_message_reflection.cc

inline void GeneratedMessageReflection::SetBit(                               
    Message* message, const FieldDescriptor* field) const {                                                   
    MutableHasBits(message)[field->index() / 32] |= (1 << (field->index() % 32));                                                
}  

inline uint32* GeneratedMessageReflection::MutableHasBits(
    Message* message) const {
  void* ptr = reinterpret_cast<uint8*>(message) + has_bits_offset_;
  return reinterpret_cast<uint32*>(ptr);
}

        上面是SetBit和MutableHasBits的函数定义,可以看到在message的对象中,保存了_has_bits_在message空间中的偏移量has_bits_offset_,这样子就可以直接得到_has_bits_了,而每个field中又存有该field对应在_has_bits_中的哪一位(filed->index()),这样子就可以直接通过SetBit函数执行和上面set_has_uid()一样的操作了。这就是映射机制的其中一部分吧。

3.3 序列化的格式

        这里仅做简单介绍。

        对于String类型的,是直接将字符串数据写入到缓冲区中,使用的是WriteString(io/coded_stream.h),WriteString中调用WriteRaw(io/coded_stream.cc)写入。

        对于整数型的,int32的函数如下:


void CodedOutputStream::WriteVarint32(uint32 value) {
  if (buffer_size_ >= kMaxVarint32Bytes) {
    // Fast path:  We have enough bytes left in the buffer to guarantee that
    // this write won't cross the end, so we can skip the checks.
    uint8* target = buffer_;
    uint8* end = WriteVarint32FallbackToArrayInline(value, target);
    int size = end - target;
    Advance(size);
  } else {
    // Slow path:  This write might cross the end of the buffer, so we
    // compose the bytes first then use WriteRaw().
    uint8 bytes[kMaxVarint32Bytes];
    int size = 0;
    while (value > 0x7F) {
      bytes[size++] = (static_cast<uint8>(value) & 0x7F) | 0x80;
      value >>= 7;
    }
    bytes[size++] = static_cast<uint8>(value) & 0x7F;
    WriteRaw(bytes, size);
  }
}

          通过上面的函数可以看到protobuf对于整数的序列化方式是用一个字节的低七位来保存数值的七位,第八个位则用来记录下一个字节是否也是属于该数字的,并且是反向的。也就是说原值中的第二个低七位会被保存到下一个字节,这样子不论序列化还是反序列化的时候都很方便。看下面例子即可:

保存值             二进制            实际保存二进制
3                 00000 0011        0000 0011
258               1 0000 0010       1000 0010 0000 0010

3.4 字段信息的保存

           protobuf是怎么保存字段相关的信息的呢?通过查看上面反序列化时用的ReadTag涉及到的函数,我们就可以很清楚的了解了。

// coded_stream.h
inline uint32 CodedInputStream::ReadTag() {
  if (GOOGLE_PREDICT_TRUE(buffer_ < buffer_end_) && buffer_[0] < 0x80) {
    last_tag_ = buffer_[0];    
    Advance(1);
    return last_tag_;
  } else {
    last_tag_ = ReadTagFallback();  
    return last_tag_;
  }
}

// wire_format_lite.h   
static const int kTagTypeBits = 3;
static const uitn32 kTagTypeMask = (1 << kTagTypeBits) - 1;
inline WireFormatLite::WireType WireFormatLite::GetTagWireType(uint32 tag) {                                      
{   
    return static_cast<WireType>(tag & kTagTypeMask);
}

inline int WireFormatLite::GetTagFieldNumber(uint32 tag) {
    return static_cast<int>(tag >> kTagTypeBits);
}

       可以看到对于读到的一个tag,低三位是字段规则,而除此以外的都是字段编号使用。但是在读取该字段对应的tag的时候,如果tag用到了不止一个字节(和整型值一样的压缩方式),则会调用ReadTagFallback函数读取tag,这里代码就不贴出来了,也很容易知道大概的读取操作了。

 

4. 总结

       以上,就是这段时间根据源码学到的protobuf相关的知识了,有点杂,不过应该能加深下对protobuf的理解了吧。接下来需要找下时间了解下反射机制了。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

Protobuf 编码及序列化的记录 的相关文章

  • Jetson TK1学习(一) 刷机

    前言 最早接触TK1是在2015年7月份的DJI ROBOMASTER夏令营里 xff0c 当时DJI推出了用于无人机平台的skymind嵌入式计算机 xff0c 也就是后来的 妙算 34 当时我并没有关注这款计算机的架构 只是当时同组的算
  • Jetson TK1学习(二)安装无线网卡

    上一篇文章讲了Jetson TK1的刷机方法 由于TK1自身没有无线网卡 xff0c 单位里面也没有有线的路由器 xff0c 所以需要在TK1上安装个无线网卡 TK1上有PCIE的接口 xff0c 目前对Intel 7260AC这款无线网卡
  • 教你创建Custom Device自定义设备

    1 问题描述 在半物理仿真测试中需要调用仿真机上的硬件设备实现仿真设备间数据通信 模拟和数字信号的采集或驱动等功能 在Veristand实时测试配置环境中可以通过创建Custom Device自定义设备实现PXI机箱 xff08 仿真机 x
  • 汉字编码(【Unicode】 【UTF-8】 【Unicode与UTF-8之间的转换】 【汉字 Unicode 编码范围】【中文标点Unicode码】【GBK编码】【批量获取汉字UNICODE码】)

    参考博客 xff1a Unicode与UTF 8互转 C语言实现 xff1a http blog csdn net tge7618291 article details 7599902 汉字 Unicode 编码范围 xff1a http
  • Visual Stdio实现云+端跨平台开发优势,Windows Azure实现移动跨平台

    Visual Stdio实现云 43 端跨平台开发优势 xff08 csdn会议总结 xff09 现代企业的架构平台 xff0c 目前移动开发的平台有哪些 xff1a 如何实现跨平台 xff1a Windows Azure 云端服务在 上海
  • C++类库

    如果你有一定的C基础可能学起来比较容易些 但是学习C 43 43 的过程中又要尽量避免去使用一些C中的思想 平时还要多看一些高手写的代码 遇到问题多多思考 怎样才能把问题抽象化 以使自己头脑中有类的概念 最后别忘了经常上机自己调调程序 这是
  • Mac上安装node和npm

    通过安装包来安装 第 1 步 xff1a 下载适用于 macOS 的 NPM 包 去Node JS官网下载Mac操作系统的npm包 您可以单击此处打开下载页面并为您的系统选择合适的文件 第 2 步 xff1a 按照指南运行包管理器 打开下载
  • Java Maven项目问题整理

    文章目录 src main java文件夹src test java文件夹src main java和src test java文件夹区别读取maven项目中src test resources里的配置文件maven 项目导入本地jar包
  • Lombok使用总结

    文章目录 介绍Lombok原理常用注解 64 Data 64 Getter 64 Setter 64 ToString 64 EqualsAndHashCode 64 NoArgsConstructor 64 AllArgsConstruc
  • 解决:PDFBox报的java.io.IOException: Missing root object specification in trailer

    文章目录 问题描述原因分析解决方案 问题描述 使用pdfbox类库操作pdf文件时 xff0c 遇到下面的报错信息 xff1a java io IOException Missing root object specification in
  • KEIL每次都要编译全部文件并且每个文件编译三次

    SYD8801是一款低功耗高性能蓝牙低功耗SOC xff0c 集成了高性能2 4GHz射频收发机 32位ARM Cortex M0处理器 128kB Flash存储器 以及丰富的数字接口 SYD8801片上集成了Balun无需阻抗匹配网络
  • Mac上Golang语言环境搭建

    文章目录 官网其他参考安装golang源码安装安装包安装使用homebrew安装 配置GOROOTGOPATHGOPROXYGOPRIVATEGONOSUMDB 安装测试 官网 目前无法在家里的直接访问golang org网站 xff1a
  • [已解决] Mac上docker安装prometheus报错:Are you trying to mount a directory onto a file (or vice-versa)?

    文章目录 项目场景问题描述原因分析解决方案 项目场景 Mac上通过docker安装prometheus 问题描述 docker run时 xff0c 会出现下面的报错 xff0c 导致容器启动失败 xff1a docker Error re
  • Mac上安装Node Exporter

    文章目录 安装Node Exporter方法一 xff1a 手动安装方法二 xff1a docker安装 运行测试 node exporter 可以采集机器 xff08 物理机 虚拟机 云主机等 xff09 的监控指标数据 xff0c 能够
  • Docker安装Grafana

    文章目录 Grafana介绍拉取镜像准备相关挂载目录及文件启动容器访问测试添加 Prometheus 数据源常见问题 看板配置 Grafana介绍 上篇博客介绍了prometheus的安装 xff1a Docker部署Prometheus
  • Springboot应用接入Prometheus监控

    文章目录 接入介绍操作步骤修改应用的依赖及配置步骤1 xff1a 修改 pom 依赖步骤2 xff1a 修改配置 本地验证prometheus配置 接入介绍 在使用 Spring Boot 作为开发框架时 xff0c 需要监控应用的状态 x
  • Spring Boot自带监控组件—Actuator介绍

    文章目录 Actuator介绍启用与暴露的区别Spring Boot集成Actuator应用监控框架Actuator监控端点启用端点端点的默认暴露规则案例 自定义端点 Actuator介绍 Actuator是Spring Boot提供的应用
  • Git Commit提交规范总结

    文章目录 前言git commit 提交规范提交消息头 commit message header 提交消息具体内容 commit message body 提交消息尾述 commit message footer Revert 表情 Em
  • 常用kubectl命令总结

    文章目录 配置kubeconfig帮助信息命令查看具体某一个命令的帮助信息列出全局的选项参数 xff08 适用所有的命令 xff09 显示合并的 kubeconfig 配置或一个指定的 kubeconfig 文件 基本命令罗列所支持的完整资
  • 解决:org.apache.catalina.connector.ClientAbortException: java.io.IOException: 断开的管道

    文章目录 项目场景问题描述原因分析解决方案 项目场景 jdk11 Spring Boot 2 x 项目 xff0c Tomcat容器 Nginx 问题描述 系统日志中 xff0c 时不时会出现下面的异常信息 xff1a org apache

随机推荐

  • 解决:No converter for [xxxx] with preset Content-Type ‘text/plain;version=0.0.4;charset=utf-8‘

    文章目录 项目背景问题描述问题分析解决方案方案一 xff1a 修改Controller定义方案二 xff1a 修改Controller返回值方案三 xff1a 全局处理 项目背景 Spring Boot 2 X 问题描述 错误信息如下 xf
  • SYD8821 串口模块使用说明【串口0中断要屏蔽底层调用】

    SYD8821是具有全球领先低功耗 RX 2 4mA 64 94 5dBm灵敏度 xff0c TX 4 3mA 64 0dBm输出功率 的蓝牙低功耗SOC芯片 xff0c 在极低电流下实现了优异的射频性能 xff0c 搭配176kB SRA
  • MySQL的information_schema库下的常用sql

    文章目录 information schema TABLES查看该数据库实例下所有库大小 MB为单位 查看该实例下各个库大小 MB为单位 查看表大小 MB为单位 熟练使用 information schema库里的表 显示在库里的表 xff
  • shell脚本批量转文件格式:dos2unix

    文章目录 可以使用shell脚本实现 xff1a span class token shebang important bin sh span span class token assign left variable dir span s
  • 解决:com.atomikos.icatch.SysException: Error in init: Log already in use? tmlog in ./

    文章目录 项目场景问题描述原因分析详细分析 解决方案 项目场景 Spring Boot 2 x xff0c 集成 atomikos 问题描述 今天在同一个环境启动两个项目时报错 xff0c 因为两个项目同时涉及到分布式事物和切换数据源相关
  • Nginx日志介绍

    文章目录 access log日志流量统计 access log 日志文件一般存放在 var log nginx 下 xff0c 可以使用 tail f命令查看access日志 span class token function tail
  • JVM (Micrometer)-4701面板参数介绍

    文章目录 Quick Facts 概览 堆和非堆内存有以下几个概念 I O Overview xff08 服务黄金指标 xff09 JVM Memory xff08 JVM内存 xff09 JVM Misc xff08 JVM负载 xff0
  • curl文件传输命令

    CURL curl transfer a URL curl 是一个利用URL语法在命令行下工作的文件传输工具 支持文件上传和下载 格式 curl options URLs URL xff1a 通过大括号指定多个url 示例 xff1a cu
  • RS-485信号解析

    这次来看看RS 485信号 使用绿联的USB转RS485模块 线用的颜色不对 xff0c 类型也不对 xff0c 实际使用中请用带屏蔽层的双绞线 示波器CH1是R xff08 B xff09 示波器CH2是R 43 xff08 A xff0
  • T t与T t = T()的区别

    主要的区别就是默认构造函数对内置类型的初始化上 如果没有T中没有定义构造函数 xff0c 则对于 T t xff0c 并不会对 t 中内置类型设置初始值 xff0c 是一个随机值 但对于 T t 61 T xff0c 对 t 中内置类型会设
  • Effective STL:杂记(一)

    1 避免使用vector lt bool gt vector lt bool gt 实际上并不能算是一个STL容器 xff0c 实际上也并不存储bool 因为一个对象要成为STL容器 xff0c 就必须满足C 43 43 标准的第23 1节
  • 限制长度双向链表的插入操作

    面试遇到的问题 xff0c 一开始面试官是问我有什么方案可以实现排行榜 xff0c 当时给出了两个方案 后面面试官又在我的其中一种方案上让我手写代码实现排序双线链表的插入 xff0c 根据score值插入 xff0c 并且链表长度限制在10
  • SYD8821 IIC模块使用说明

    SYD8821是具有全球领先低功耗 RX 2 4mA 64 94 5dBm灵敏度 xff0c TX 4 3mA 64 0dBm输出功率 的蓝牙低功耗SOC芯片 xff0c 在极低电流下实现了优异的射频性能 xff0c 搭配176kB SRA
  • Python下载文件时出现乱码的解决方法之一:Content-Encoding: gzip

    之前写过一个简单的爬虫程序 xff0c 这次想试着再写一个下载固定文件的爬虫程序 写完之后发现下载的文件 xff0c 有些是可以正常打开的 xff0c 而有些是提示了编码错误 xff0c 用wireshark抓包 xff0c 过滤出http
  • Python爬虫判断url链接的是下载文件还是html文件

    最近在写一个网络爬虫的代码 xff0c 提供命令行来下载文件或者是打印根域名下指定节点及深度的子节点 用的是urllib2库 xff0c 算是比较简单 xff0c 但是功能并没有很强大 说重点吧 xff0c 在实际爬网页的过程中 xff0c
  • recvmsg和sendmsg函数

    在unp第14章讲了这两个函数 xff0c 但是只是讲了两个数据结构及参数而已 xff0c 所以自己想根据介绍来重构udp回射的客户端程序 但是sendmsg和recvmsg都遇到了问题 xff0c 并且纠结了很久 xff0c 所以在此记录
  • STL中map的[]运算导致程序挂掉的问题

    在项目的开发中 xff0c 使用 设置map变量时 xff0c 出现了Segment Fault的问题 xff0c 使用GDB bt命令得到调用栈 xff08 中间部分被我去掉了 xff09 如下 xff1a 0 0x00000000008
  • 结构体的vector resize()与初始化

    转自 xff1a https www cnblogs com kongse qi p 6798873 html 序 xff1a 我们在使用vector的时候可以自定义里面的数据类型 例如这样 xff1a struct Edge int fr
  • new(p) T1(value) 的操作

    最近开始看 STL源码剖析 xff0c 看到空间配置器的时候 xff0c 发现这么一段代码 xff1a template lt class T1 class T2 gt inline void construct T1 p const T2
  • Protobuf 编码及序列化的记录

    工作中用到了protobuf xff0c 然后之前在面试的时候面试官就问了一个问题 xff0c 如果将int32类型的字段的值设置为0 xff0c 那还会将该值进行序列化吗 xff1f 当时是懵了的 xff0c 因为自己还没有研究这部分 当