刘沙河 刘沙河
首页
  • Go语言基础

    • 数据类型
    • 反射
    • Go指针
  • Go语言进阶

    • go泛型
    • go条件编译
    • cgo教程
    • Go协程调度原理及GPM模型
    • Go内存管理
    • Go垃圾回收机制
    • Go语言内存对齐
  • Go语言实现原理

    • channel 实现原理
    • slice 实现原理
    • map 实现原理
    • sync.Mutex 实现原理
    • 乐观锁CAS 实现原理
    • singlefight 实现原理
  • gin框架

    • gin中间件原理
    • gin路由原理
  • gorm

    • GORM介绍和使用
    • GORM_CURD操作指南
  • go测试

    • benchmark基准测试
    • pprof 性能分析
  • python进阶

    • Numpy&Pandas
    • celery分布式任务队列
  • Django

    • Django 常见命令
    • middleware中间件
    • Django缓存系统
    • Django信号系统
    • Django REST Framework
  • Flask

    • Flask基础知识总结
    • Flask-SQLAlchemy
  • 爬虫

    • aiohttp
    • scrapy框架
  • Mysql

    • Mysql存储引擎和索引
    • MySQL主从复制
    • Mysql读写分离
    • 数据库分库分表
    • Mysql锁
    • Mysql事务和MVCC原理
    • 分库分表带来的读扩散问题
  • Redis

    • redis基础和数据类型
    • redis主从架构
    • redis哨兵架构
    • redis集群模式
    • 如何保证缓存和数据库双写一致
    • redis底层数据结构
    • redis分布式锁
  • Elasticsearch

    • es基本概念
    • es基础语法
    • es倒排索引
  • etcd

    • Go操作etcd
    • Raft原理
    • etcd分布式锁
  • kafka

    • 消息队列MQ总结
    • kafka 概述及原理
    • kafka 消费问题记录
    • 零拷贝技术
    • kafka分区规范
  • RabbitMQ

    • rabbitMQ基础
    • Go操作rabbitmq
  • RocketMQ

    • 可靠消息队列 rocketMQ
  • Http&Https

    • http&https
    • TCP和UDP
    • Ping 原理
  • RPC

    • RPC初识
    • grpc初识和实现
  • gRPC

    • grpc 初识
    • grpc 上下文 metadata
    • grpc 健康检查
    • grpc keepalive
    • grpc 命名解析
    • grpc 中间件&拦截器
    • grpc 负载均衡
    • grpc 身份认证
    • grpc 超时重试
    • grpc 链路追踪
    • grpc-gw将gRPC转RESTfu api
    • grpc-gw自定义选项
  • protobuf

    • protobuf 进阶
    • protobuf 编码原理
  • Docker

    • Docker基础
    • Docker常用命令
    • Dockerfile
    • Docker-Compose
    • Docker多阶段构建
    • Docker Config 教程
    • Docker Swarm 教程
    • Docker Stack 教程
    • Docker Buildx 教程
  • k8s

    • k8s 基础概念
    • k8s 集群架构
    • k8s 工作负载
    • Pod 网络
    • Service 网络
    • 外部接入网络
    • 一张图搞懂k8s各种pod
    • k8s 存储抽象
    • mac快速启动k8s
    • 自制申威架构k8s-reloader
  • go-kit

    • go-kit初识
    • go-kit启动http服务
    • go-kit集成gin启动服务
    • go-kit集成grpc和protobuf
    • go-kit中间件
    • go-kit服务注册发现与负载均衡
    • go-kit限流和熔断
    • go-kit链路追踪
    • go-kit集成Prometheus
  • 设计模式

    • 初识设计模式
    • 创建型模式
    • 结构型模式
    • 行为模式
  • 数据结构

    • 时间轮
    • 堆、双向链表、环形队列
    • 队列:优先队列
    • 队列:延迟队列
  • 算法

    • 递归算法
    • 枚举算法
    • 动态规划
    • 回溯算法
    • 分治算法
    • 贪心算法
    • LRU和LFU
    • 一致性哈希

花开半夏,半夏花开
首页
  • Go语言基础

    • 数据类型
    • 反射
    • Go指针
  • Go语言进阶

    • go泛型
    • go条件编译
    • cgo教程
    • Go协程调度原理及GPM模型
    • Go内存管理
    • Go垃圾回收机制
    • Go语言内存对齐
  • Go语言实现原理

    • channel 实现原理
    • slice 实现原理
    • map 实现原理
    • sync.Mutex 实现原理
    • 乐观锁CAS 实现原理
    • singlefight 实现原理
  • gin框架

    • gin中间件原理
    • gin路由原理
  • gorm

    • GORM介绍和使用
    • GORM_CURD操作指南
  • go测试

    • benchmark基准测试
    • pprof 性能分析
  • python进阶

    • Numpy&Pandas
    • celery分布式任务队列
  • Django

    • Django 常见命令
    • middleware中间件
    • Django缓存系统
    • Django信号系统
    • Django REST Framework
  • Flask

    • Flask基础知识总结
    • Flask-SQLAlchemy
  • 爬虫

    • aiohttp
    • scrapy框架
  • Mysql

    • Mysql存储引擎和索引
    • MySQL主从复制
    • Mysql读写分离
    • 数据库分库分表
    • Mysql锁
    • Mysql事务和MVCC原理
    • 分库分表带来的读扩散问题
  • Redis

    • redis基础和数据类型
    • redis主从架构
    • redis哨兵架构
    • redis集群模式
    • 如何保证缓存和数据库双写一致
    • redis底层数据结构
    • redis分布式锁
  • Elasticsearch

    • es基本概念
    • es基础语法
    • es倒排索引
  • etcd

    • Go操作etcd
    • Raft原理
    • etcd分布式锁
  • kafka

    • 消息队列MQ总结
    • kafka 概述及原理
    • kafka 消费问题记录
    • 零拷贝技术
    • kafka分区规范
  • RabbitMQ

    • rabbitMQ基础
    • Go操作rabbitmq
  • RocketMQ

    • 可靠消息队列 rocketMQ
  • Http&Https

    • http&https
    • TCP和UDP
    • Ping 原理
  • RPC

    • RPC初识
    • grpc初识和实现
  • gRPC

    • grpc 初识
    • grpc 上下文 metadata
    • grpc 健康检查
    • grpc keepalive
    • grpc 命名解析
    • grpc 中间件&拦截器
    • grpc 负载均衡
    • grpc 身份认证
    • grpc 超时重试
    • grpc 链路追踪
    • grpc-gw将gRPC转RESTfu api
    • grpc-gw自定义选项
  • protobuf

    • protobuf 进阶
    • protobuf 编码原理
  • Docker

    • Docker基础
    • Docker常用命令
    • Dockerfile
    • Docker-Compose
    • Docker多阶段构建
    • Docker Config 教程
    • Docker Swarm 教程
    • Docker Stack 教程
    • Docker Buildx 教程
  • k8s

    • k8s 基础概念
    • k8s 集群架构
    • k8s 工作负载
    • Pod 网络
    • Service 网络
    • 外部接入网络
    • 一张图搞懂k8s各种pod
    • k8s 存储抽象
    • mac快速启动k8s
    • 自制申威架构k8s-reloader
  • go-kit

    • go-kit初识
    • go-kit启动http服务
    • go-kit集成gin启动服务
    • go-kit集成grpc和protobuf
    • go-kit中间件
    • go-kit服务注册发现与负载均衡
    • go-kit限流和熔断
    • go-kit链路追踪
    • go-kit集成Prometheus
  • 设计模式

    • 初识设计模式
    • 创建型模式
    • 结构型模式
    • 行为模式
  • 数据结构

    • 时间轮
    • 堆、双向链表、环形队列
    • 队列:优先队列
    • 队列:延迟队列
  • 算法

    • 递归算法
    • 枚举算法
    • 动态规划
    • 回溯算法
    • 分治算法
    • 贪心算法
    • LRU和LFU
    • 一致性哈希
  • rpc

  • grpc

  • protobuf

    • 【protobuf】protobuf 进阶
    • 【protobuf】protobuf 编码原理
      • protobuf 定义
      • protobuf 数据格式
      • 编码原理
        • 1. varint 编码原理
        • 2. Zigzag 编码原理
      • protobuf与json的编码对比
      • protobuf 特性
        • 1. 基本特性
        • 2. 编码特性
  • rpc+grpc
  • protobuf
bigox
2023-05-13
目录

【protobuf】protobuf 编码原理

# protobuf 定义

syntax = "proto3";

// 我是注释
message Person {
  string name = 1;
  int32 id = 2;
}
1
2
3
4
5
6
7
  • 字段编号:消息类型中的每个字段都需要定义唯一的编号,该编号会用来识别二进制数据中字段。编号在[1,15]范围内可以用一个字节编码表示。在[16,2047]范围可以用两个字节编码表示。所以将15以内的编号留给频繁出现的字段可以节省空间。编号的最小值为1,最大值为2^29-1=536870911. 不能使用[19000,19999]范围内的数字,因为该范围内的数字被proto编译器内部使用。同理,其他预先已经被保留的数字也不能使用。

  • 字段规则:每个字段可以被singular或者repeated修饰。在proto3语法中,如果不指定修饰类型,默认值为singular. singular: 表示被修饰的字段最多出现1次,即出现0次或1次。repeated: 表示被修饰的字段可以出现任意次,包括0次。在proto3语法中,repeated修饰的字段默认采用packed编码

  • 保留字段:当删掉或者注释掉message中的一个字段时,将来其他开发人员在更新message定义时可以重用之前的字段编号。如果他们意外载入了旧版本的.proto文件将会导致严重的问题,例如数据损坏。一种避免问题产生的方式是指定保留的字段编号和字段名称。如果将来有人用了这些字段编号将在编译proto的时候产生错误,显示提醒proto有问题。NOTE,不要对同一个字段混合使用字段名称和字段编号。

    message Foo {
      reserved 2, 15, 9 to 11;
      reserved "foo", "bar";
    }	
    
    1
    2
    3
    4

# protobuf 数据格式

protobuf 是用二进制来表示数据,可读性差

  • xml 格式

    <person>
        <id>91890</id>
    </person>
    
    1
    2
    3
  • json 格式

    {
        "id":91890
    }
    
    1
    2
    3
  • 用protobuf表示如下, 它直接用二进制来表示数据,不像上面XML和JSON格式那么直观的看到表示的内容。

    00010000 10100001 11001101 00000101  // 二进制
    
    1
    1. 00010000 10100001 11001101 00000101的第一个字节 :表示的是数据的序号和类型,编码方式是varient
    2. 00010000 表示是否解析到了本次varient的最后一个字节
    3. 00010000 表示序号,十进制2,即id的序号
    4. 00010000 表示该字段的类型,000表示int32类型

# 编码原理

  • protobuf高效的秘密在于它的编码格式,它采用了TLV(tag-length-value)编码格式。

  • 每个字段都有唯一的tag值,它是字段的唯一标识。length表示value数据的长度,length不是必须的,对于固定长度的value,是没有length的。value是数据本身的内容。

    image-20230513200552517

  • 对于tag值,它有field_number和wire_type两部分组成。field_number就是在前面的message中我们给每个字段的编号,wire_type表示类型,是固定长度还是变长的。wire_type当前有0到5一共6个值,所以用3个bit就可以表示这6个值。tag结构如下图。

    图片

  • wire_type值如下表, 其中3和4已经废弃,我们只需要关心剩下的4种。对于Varint编码数据,不需要存储字节长度length.这种情况下,TLV编码格式退化成TV编码。对于64-bit和32-bit也不需要length,因为type值已经表明了长度是8字节还是4字节。

    图片

# 1. varint 编码原理

Varint顾名思义就可变的int,是一种变长的编码方式。值越小的数字,使用越少的字节表示,通过减少表示数字的字节数从而进行数据压缩。对于int32类型的数字,一般需要4个字节表示,但是采用Varint编码,对于小于128的int32类型的数字,用1个字节来表示。对于很大的数字可能需要5个字节来表示,但是在大多数情况下,消息中一般不会有很大的数字,所以采用Varint编码可以用更少的字节数来表示数字。

Varint是变长编码,那它是怎么区分出各个字段的呢?也就是怎么识别出这个数字是1个字节还是2个字节,Varint通过每个字节的最高位来识别,如果字节的最高位是1,表示后续的字节也是该数字的一部分,如果是0,表示这是最后一个字节,且剩余7位都用来表示数字。虽然这样每个字节会浪费掉1bit空间,也就是1/8=12.5%的浪费,但是如果有很多数字不用固定的4字节,还是能节省不少空间。

  • int32类型的数字 1的varint编码
    • 1 的二进制编码00000000 00000000 00000000 00000001, 从左到右是高位->低位
    • 取最低位7位000 00001,余下都是0,高位补0,表示这是最后一个字节,得到0000 0001
    • 因为其没有后续字节, 因此其最高有效位为 0, 其余的 7 位以补码形式存放 1
  • int32类型的数字 666 的varint编码
    • 666 的二进制编码 00000000 00000000 00000010 10011010
    • 取最低位7位0011010,余下不都是0,高位补1 10011010
    • 依次再取低位7位0000101,余下都是0,高位补0 00000101
    • 拼接得到varint编码:10011010 00000101, Base128 Varints 采用小端字节序, 因此数字的高位存放于低地址上
  • 通过 varint编码 推导int32
    • varint编码:10011010 00000101
    • 移除标志位(每个字节首位)并交换字节顺序 得到 000010 10011010
    • 与666的二进制相同

# 2. Zigzag 编码原理

负数的符号位为数字的最高位,它的最高位是1,所以对于负数用Varint编码一定为占用5个字节。这是不划算的,明明是4字节可以搞定的,现在统统都需要5个字节。所以protobuf定义了sint32和sint64类型来表示负数,先采用Zigzag编码,将有符号的数转成无符号的数,在采用Varint编码,从而减少编码后字节数。

Zigzag采用无符号数来表示有符号数,使得绝对值小的数字可以采用比较少的字节来表示。在理解Zigzag编码之前,我们先来看几个概念。

原码:最高位为符号位,剩余位表示绝对值

反码:除符号位外,对原码剩余位依次取反

补码:对于正数,补码为其本身,对于负数,除符号位外对原码剩余位依次取反然后+1

  • int32 的数字 -2 Zigzag编码示例

    图片

  • 总结起来,对于负数对其补码做运算操作,对于数n,如果是sint32类型,则执行(n<<1)^(n>>31)操作,如果是sint64则执行(n<<1)^(n>>63), 通过前面的操作将一个负数变成了正数。这个过程就是Zigzag编码,最后在采用Varint编码。

  • 因为Varint和Zigzag编码可以自解析内容的长度,所以可以省略长度项。TLV存储简化为了TV存储,不需length项。

    图片

# protobuf与json的编码对比

  • json数据

    {
        "id": 666
    }
    
    1
    2
    3
  • protobuf编码后的数据格式

    00010000 10100001 11001101 00000101
    // 第一个字节表示序号和字段类型 即序号为2,类型为int的字段, 后三个字节表示数据的值,值为91890
    
    1
    2

问题:id这个字段名去哪儿了?

答案:id的字段名被protobuf舍弃了,protobuf最终的编码结果是抛弃了所有的字段名,仅仅保留了字段的序号、类型和数据的值。

# protobuf 特性

# 1. 基本特性

image-20230513195736650

# 2. 编码特性

  • protobuf的解码不需要类型相同,也不需要字段名相同
  • protobuf的解码依赖于序号的正确性
  • protobuf中的序号大小会影响最终编码大小
  • protobuf的对象类型可以向String类型兼容
  • protobuf可以和json完全兼容,且编码字节数要比json少

参考资料

https://zhuanlan.zhihu.com/p/537871378

https://mp.weixin.qq.com/s/wZJzkKqNAsRE7Wyan_RXfg

https://mp.weixin.qq.com/s/wJpTgAcFX50naaBNXvjEIw

#rpc
上次更新: 2023/05/13, 20:57:06
【protobuf】protobuf 进阶

← 【protobuf】protobuf 进阶

最近更新
01
go与http代理
05-24
02
自制申威架构k8s-reloader
12-06
03
Docker Buildx 教程
12-01
更多文章>
Theme by Vdoing | Copyright © 2020-2024 小刘扎扎 | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式