Varint编码:
一种变长的编码方式。用字节表示数字值越小的数字,使用越少的字节数表示。通过减少表示数字的字节数从而进度数据的压缩。
变长:采用一个或者多个字节表示一个数字,对于小的数字使用一个字节,大的数需要5个字节。
实现方式:每个字节的最高位为1,表示后续的一个字节也是数字的一部分。如果字节的最高位为0,则表示结束。使用其它7位来表示数字。
所以小于128的数字,使用一个字节就可以表示,大于128字节的需要多个字节。
注:先不考虑负数
Varint编码
说明:
1、取低7位,高位补零。
2、右移7位后,如果右移后不为0,再取7位直接拼接;如果为0则终止。
所以Varint 为LE 字节序
Varint解码:
说明:
1、取第一个字节,取低7位。如果第一个字节小于0(高位为1,值为负数)进行下一步
2、取下一个字节移左移7位,拼接。不足字节数高位补0
java代码:
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.handler.codec.CorruptedFrameException;
/**
* netty ProtobufVarint32LengthFieldPrepender()、解码器ProtobufVarint32FrameDecoder()
*/
public class Test {
public static void main(String[] args) throws Exception {
int n = 300;
//编码,编码后的为LE 字节序
byte[] varint32 = varint32Encode(n);
//解码
ByteBuf buffer = ByteBufAllocator.DEFAULT.buffer();
buffer.writeBytes(varint32);
int decode = readRawVarint32(buffer);
System.out.println("decode:"+decode);
}
/**
* 解码,最多只有5个字节, 这个代码是ProtobufVarint32FrameDecoder.readRawVarint32 方法的源码
* @param buffer
* @return
*/
private static int readRawVarint32(ByteBuf buffer) {
//不可读
if (!buffer.isReadable()) {
return 0;
}
buffer.markReaderIndex();
//读取一个字节
byte tmp = buffer.readByte();
if (tmp >= 0) {
//如果大于 0 ,说明只有一个字节(如果大于一个字节,高位为1,值为负)
return tmp;
} else {
//取temp低7位
int result = tmp & 127;
if (!buffer.isReadable()) {
//如果读取一个字节后,不可再读(读指针大于等于写指针,源码注释有点问题),说明这个值只有一个字节,最高位为1,值错误返回 0。
buffer.resetReaderIndex();
return 0;
}
if ((tmp = buffer.readByte()) >= 0) {
//取第二个字节,如果第二个字节大于0,则左移7位(只有7位表示数字,不够一个字节则高位补0),
result |= tmp << 7;
} else {
//第二个字节取低7位,左移7位,与result 取或
result |= (tmp & 127) << 7;
if (!buffer.isReadable()) {
//值错误
buffer.resetReaderIndex();
return 0;
}
if ((tmp = buffer.readByte()) >= 0) {
//取第三个字节,和上面一样
result |= tmp << 14;
} else {
result |= (tmp & 127) << 14;
if (!buffer.isReadable()) {
buffer.resetReaderIndex();
return 0;
}
if ((tmp = buffer.readByte()) >= 0) {
//取第四个字节,和上面一样
result |= tmp << 21;
} else {
result |= (tmp & 127) << 21;
if (!buffer.isReadable()) {
buffer.resetReaderIndex();
return 0;
}
//取第五个字节
result |= (tmp = buffer.readByte()) << 28;
if (tmp < 0) {
throw new CorruptedFrameException("malformed varint.");
}
}
}
}
return result;
}
}
/**
* 编码
* @param n
*/
public static byte[] varint32Encode(int n){
//n编码后,占用size个字节
int size = size(n);
byte[] varint32 = new byte[size];
int idx = 0;
while (true) {
if ((n & ~0x7f) == 0) {
//小于等于127 (与上 -128 (只有低7位为0) 等于 0 )
varint32[idx++] = (byte) n;
break;
} else {
//大于127
/**
* n & 0x7f 取出 n的 后 7位
* | 0x80 把第8位补为 1
*/
varint32[idx++] = (byte) ((n & 0x7f) | 0x80);
//无符号右移7位
n >>>= 7;
}
}
for (byte b : varint32) {
//打印出数据,不够8位高位补0。 LE 字节序
//大于127的数字,最高位为1,所以编码后都是负数
System.out.print(Integer.toBinaryString(b)+" ");
}
System.out.println();
return varint32;
}
/**
* 计算n 需要byte 数组长度。 最多5个字节
* @param n
* @return
*/
public static int size(int n){
if ( (n & (0xffffff << 7)) ==0) return 1;
if ( (n & (0xffffff << 14)) ==0) return 2;
if ( (n & (0xffffff << 21)) ==0) return 3;
if ( (n & (0xffffff << 28)) ==0) return 4;
return 5;//如果是负数
}
}
打印结果:
11111111111111111111111110101100 10
decode:300
分析:
300 (00000001 00101100) Varint32 编码后二进制为:11111111111111111111111110101100 10
java 中int 为4字节32位,值为负数,所以这位全为1,只看最后8位即可
300 使用 varint32编码后战胜2个字节,如上面打印出的值,取两个字节所以上面的Varint32编码为:10101100 00000010
解码使用netty 的实现。参考netty 的实现(4.1.24.Final 版本)
ProtobufVarint32LengthFieldPrepender 编码
ProtobufVarint32FrameDecoder.readRawVarint32 方法就是解码Varint32的
Varint32编码不足:
如果很大的数(负数)会使用5个字节去表示
所有对于 int32、 int64 的类型, protobuf使用Varint32 编码
对于sint32、sint64 类型的字段值(负数),Protocol Buffer会先采用 Zigzag 编码,再采用 Varint编码
Zigzag 编码:
Zigzag 编码是补充Varint32编码在表示负数的不足,使用正数去表示一个负数。从而更好的帮助 Protocol Buffer进行数据的压缩。
代码:
public static void main(String[] args) {
int n = -2;
//编码
// 对于sint 64 数据类型 则为: return (n << 1> ^ (n >> 63) ;
int zigzagEncode = (n <<1) ^ (n >>31);
//解码
int zigzagDecode = (zigzagEncode >>> 1) ^ -(zigzagEncode & 1);
System.out.println("zigzagEncode:"+zigzagEncode);
System.out.println("zigzagDecode:"+zigzagDecode);
}
也就是两个公式。
对于可能是负数的值,使用有符号的类型。