protobuf varint 编解码原理
编码原理分析
- 定义消息结构
proto
message Test1 {
int32 a = 1;
}- 设置值
js
a = 150- 序列化后得到 3 个字节:
08 96 0108
- 为什么 08 表示 Tag:"字段 a = 1号字段,类型是 varint(int32)"
- 公式:(field_number << 3) | wire_type
- field_number = 1
- int32 在线上对应的 wire type 是 0
- (1 << 3) | 0 => 8 | 0 => 08
96 01
- varint 字节
- varint = variable-width integer,可变长度整数
- 小整数少占字节,大整数多占字节。
[1 bit continuation][7 bit payload]- 最高位 MSB:是否还有下一字节,低 7 位:真正的数据
- MSB = 1 → 后面还有字节
- MSB = 0 → 这是最后一个字节
- 为什么 96 01 表示 Value:"值 150"
- 150 二进制 => 10010110
- varint 按 7 位一组
- 1 | 0010110
- 第 1 组(低 7 位):0010110
- 第 2 组(更高位):0000001
- varint 输出时是低位组先写,高位组后写
- 0000001 | 0010110
- 先处理低位组 0010110,continuation bit 要设成 1 => 10010110 => 96(16进制)
- 再处理高位组 0000001,continuation bit 要设成 0 => 00000001 => 01(16进制)
- => 96 01
varint 正向编码,本质就是:每次取低 7 位;如果后面还有剩余,就把最高位置 1;最后一组最高位置 0。
ts
export function encodeVarint(num: number): number[] {
const bytes: number[] = []
// 循环处理,直到 num < 0x80
while (num >= 0x80) {
// & 0x7f 取低7位
// | 0x80 设置最高位为1
bytes.push((num & 0x7f) | 0x80)
// 右移7位,处理下一个7位
num >>= 7
}
// 最后一组,不加最高位为1
bytes.push(num)
return bytes
}Tag 公式 (field_number << 3) | wire_type
- 为什么是3位?
- tag 的低 3 位专门留给 wire type,剩下的高位留给 field number
- wire type 一共只需要少量取值,而要表示这些取值,3 个 bit 就够了
- 3 bit => 000 ~ 111 => 0 ~ 7 => 8种取值
- wire type 主要js
0 = VARINT 1 = I64 2 = LEN 3 = SGROUP(旧) 4 = EGROUP(旧) 5 = I32 - 最大也就到 5,所以 3 位足够装下 wire type
- protobuf 想让编码尽量紧凑,把它们塞进一个数里
- 为什么不是 <<2 或 <<4
- <<2 => 0 ~ 3 不够
- <<4 => 0 ~ 15 浪费
它本质上就是“位打包”,常见的二进制技巧:把多个小字段塞进一个整数的不同 bit 区域里。
ts
export function encodeTag(fieldNumber: number, wireType: number): number {
return (fieldNumber << 3) | wireType
}ts
export function decodeTag(tag: number): { fieldNumber: number; wireType: number } {
return {
fieldNumber: tag >> 3,
wireType: tag & 0x07
}
}解码原理分析
96 01 => 150
- 96 = 10010110
- 01 = 00000001
- 10010110 MSB = 1 → 后面还有
- 00000001 MSB = 0 → 这是最后一个
- 去掉每个字节的最高位
- 10010110 -> 0010110 第一个字节装的是 低 7 位
- 00000001 -> 0000001 第二个字节装的是 更高的 7 位
- 高位在左,低位在右
- 0000001 0010110
- 00000010010110 => 150
工程方式理解
- 结果 = 第1组 + (第2组 << 7) + (第3组 << 14) + ...
- 第1组:0010110
- 第2组:0000001
- 结果 = 0010110 + (0000001 << 7) => 150
ts
export function decodeVarint(bytes: number[]): number | null {
let result = 0
// 循环处理,直到 bytes.length === 0
for (let i = bytes.length-1; i >= 0; i--) {
// & 0x7f 取低7位
// << 7 左移7位
// (result << 7) | (bytes[i] & 0x7f) 将低7位拼接到结果中
result = (result << 7) | (bytes[i] & 0x7f)
}
return result
}编码与解码的本质:位权变化
- 编码:把值按照权重分配到不同位置
- 解码:把原本被拆开的低位块、高位块,放回它们原来的权重位置。
为什么很多编码都喜欢“拆位、移位”,因为二进制世界里,位权是最自然的表达方式。
操作本质:把不同权重的二进制位拆开、搬运、再放回去
真正不会丢值的根本原因:这是可逆变换