Protocol Buffer (сокращенно Protobuf) — это высокопроизводительная межъязыковая кроссплатформенная библиотека сериализации, разработанная Google.
Для большей эффективности оборудования числа в компьютерах обычно представляются целыми числами фиксированной длины. Однако сегодня, когда микросервисы повсюду, необходим более гибкий способ передачи чисел для экономии полосы пропускания. Varint (целые числа переменной длины) — это метод кодирования целых чисел, с помощью которого можно гибко настроить пространство, необходимое для целочисленных значений.
Кодировка варинта
Varint в Protobuf выполняет двоичное кодирование переменной длины в соответствии с размером целого числа, меньше 128 () занимает 1 байт после целочисленного кодирования, меньше 16384 () после целочисленного кодирования, занять 2 и т. д., до 10 байтов можно использовать для указания больше или равноЦелочисленный тип , принцип его реализации показан на следующем рисунке:
Для каждого закодированного байта первый бит идентифицирует, является ли он хвостом, а последующие 7 битов используются для записи двоичных битов исходного числа.Например, двоичное число 299 в int32 равно
00000000 00000000 00000001 00101011
Закодированный результат10101011 00000010
, что экономит 2 байта на передачу.
Поскольку только 7 бит каждого байта в результате кодирования Varint являются значимыми битами (хранят исходные данные), менееInt32 или int64 могут быть сжаты кодировкой Varint. Конечно, использование небольших чисел обычно намного больше, чем использование больших чисел, поэтому кодирование Varint может сжиматься для большинства сценариев.
Кодировка варинта реализована следующим образом:
const maxVarintBytes = 10 // maximum length of a varint
// EncodeVarint returns the varint encoding of x.
// This is the format for the
// int32, int64, uint32, uint64, bool, and enum
// protocol buffer types.
// Not used by the package itself, but helpful to clients
// wishing to use the same encoding.
func EncodeVarint(x uint64) []byte {
var buf [maxVarintBytes]byte
var n int
for n = 0; x > 127; n++ {
// 首位记 1, 写入原始数字从低位始的 7 个 bit
buf[n] = 0x80 | uint8(x&0x7F)
// 移走记录过的 7 位
x >>= 7
}
// 剩余不足 7 位的部分直接以 8 位形式存下来,故首位为 0
buf[n] = uint8(x)
n++
return buf[0:n]
}
В нем используются 2 магических числа, это можно понять, взглянув на их двоичный код.& 0x7f
Получите 7 бит,| 0x80
Отметьте первую позицию как 1.
0x80 => 0000000010000000
0x7f => 0000000001111111
Таким образом, метод десериализации Varint состоит в том, чтобы взять последние 7 битов каждого байта и объединить их в обратном порядке, как показано на следующем рисунке (в качестве примера взят результат кодирования 299):
Исходный код выглядит следующим образом:
// DecodeVarint reads a varint-encoded integer from the slice.
// It returns the integer and the number of bytes consumed, or
// zero if there is not enough.
// This is the format for the
// int32, int64, uint32, uint64, bool, and enum
// protocol buffer types.
func DecodeVarint(buf []byte) (x uint64, n int) {
for shift := uint(0); shift < 64; shift += 7 {
if n >= len(buf) {
return 0, 0
}
b := uint64(buf[n])
n++
// 弃首位取 7 位并加回 x
x |= (b & 0x7F) << shift
// 首位为 0
if (b & 0x80) == 0 {
return x, n
}
}
// The number is too large to represent in a 64-bit value.
return 0, 0
}
Здесь есть проблема,EncodeVarint
а такжеDecodeVarint
Все типы uint64, что, если нам нужно иметь дело с отрицательными числами? Посмотрите, что вы получите, напрямую закодировав отрицательные числа как uint64:
fmt.Println(EncodeVarint(uint64(-299)))
// output:
// [213 253 255 255 255 255 255 255 255 1]
Результатом будет 10-байтовая кодировка, т.к.uint64(-299)
Значение - это дополнение 299, которое должно быть представлено в 64 битах! То есть в итоге будет получена максимальная длина варинтной кодировки.Исходный код для расчета длины закодированных байтов в официальной библиотеке выглядит следующим образом:
// SizeVarint returns the varint encoding size of an integer.
func SizeVarint(x uint64) int {
switch {
case x < 1<<7:
return 1
case x < 1<<14:
return 2
case x < 1<<21:
return 3
case x < 1<<28:
return 4
case x < 1<<35:
return 5
case x < 1<<42:
return 6
case x < 1<<49:
return 7
case x < 1<<56:
return 8
case x < 1<<63:
return 9
}
return 10
}
Zigzag
Зигзагообразное кодирование сопоставляет целые числа со знаком с целыми числами без знака, как следует из названия, а закодированное значение колеблется между положительными и отрицательными целыми числами, как показано в следующей таблице:
Signed Original | Encoded As |
---|---|
0 | 0 |
-1 | 1 |
1 | 2 |
-2 | 3 |
2147483647 | 4294967294 |
-2147483648 | 4294967295 |
Он реализован следующим образом:
func Zigzag64(x uint64) uint64 {
// 左移一位 XOR (-1 / 0 的 64 位补码)
return (x << 1) ^ uint64(int64(x) >> 63)
}
Здесь следует отметить, что если x отрицательно, левая часть XOR является дополнением -x и сдвигается влево на единицу. На следующем рисунке в качестве примера используется -299, сначала вычислите дополнение 299, затем дополнение знака XOR (-1 / 0), результат равен 597; если это положительное число, результат зигзага в 2 раза больше оригинальный номер.
резюме
При написании этой статьи я просмотрел волну базовых знаний, например, почему результат кодирования Varint отрицательного числа составляет 10 байт, потому что отрицательное число хранится в виде дополнения. Таким образом, концепции, которые кажутся вводными в университете, на самом деле встречаются в программировании, а путь долог.
обновление: 2020.01.22 исправить описание ошибки