一、什么是Protobuf
Protobuf(Google Protocol Buffers)是Google提供一个具有高效的协议数据交换格式工具库,类似于常用的XML及JSON,但具有更小的传输体积、更高的编码、解码能力,特别适合于数据存储、网络数据传输等对存储体积、实时性要求高的领域。
二、Protobuf真的能压缩空间吗?
经过对比我们发现使用了Protobuf的的数据比没有使用Protobuf编码的数据长度还多了两个字节,这到底是为什么?我们接着往下看一个Long类型的对比
比如Long类型发现,没有Protobuf编码的Long类型的数据占了8个字节,而使用Protobuf的只占了数据只占了四个字节,这是为什么?接下来我们一起来分析下原因
三、Protobuf编码结构
Tag
field_number: message 定义字段时指定的字段编号
wire_type: ProtoBuf 编码类型,根据这个类型选择不同的 Value 编码方案。Length 是可选的,不同类型的数据编码结构可能会变成 Tag - Value 的格式。没有Length如何确认Value的边界?答案就是 Varint 编码。
Value 数据内容根据数据类型不同使用不同方式的编码的数据
1、Varint这个类型编码方式会对针对sint 32和sint64做一个特殊处理,先用ZigTag编码处理再进行Varint编码。
2、Start group 和 End group 两种类型已被废弃。
四、Tag编解码
#Login.proto
message LoginRequest{
string username = 1;
string password = 2;
}
Tag编码
Tag= (field_number << 3 ) | wire_type
username 属性的tag编码
usernameTag = 1 << 3 | 2
usernameTag = 00001010
Tag解码
wire_type = Tag & 3
wire_type = 00001010 & 3 = 010
field_number = Tag >> 3
field_number = 00001010 >> 3 = 00001
Wire Type 的类型如下表所示
Type | Meaning | Used For |
---|---|---|
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimi | string, bytes, embedded messages, packed repeated fields |
3 | Start group | Groups (deprecated) |
4 | End group | Groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
五、Varints 编解码
Varints 编码的规则主要特点:
在每个字节第一个 bit 设置了 msb(most significant bit ),标识是否需要继续读取下一个字节
存储数值对应的二进制补码
补码的低位排在前面
- 编码
对388888的补码进行编码
00000000 00000000 00000000 00000000 00000000 00000101 11101111 00011000
1、去掉高位多余的0
101 11101111 00011000
2、从后依次向前取 7 位组并反转排序
0011000 1011110 0010111
3、加上 msb
1#0011000 1#1011110 0#0010111
- 解码
10011000 11011110 00010111 …
每个字节的第一个 bit 为 msb 位,msb = 1 表示需要再读一个字节(还未结束),msb = 0 表示无需再读字节(读取到此为止)。
1、读取Value的值
10011000 11011110 00010111
2、去掉msb
0011000 1011110 0010111
3、将这三个 7-bit 组反转得到补码
0010111 1011110 0011000
好像一彻都辣么美好, but, 如果是负数怎么办?
-2
1111111111111111111111111111111111111111111111111111111111111110
答案接着往下看
六、ZigZag编解码
- 编码
对-2的补码进行编码
1111111111111111111111111111111111111111111111111111111111111110
1、先将符号位放最右边
1111111111111111111111111111111111111111111111111111111111111101
2、取反(除符号位)
00000000000000000000000000000000000000000000000000000011
3、Varints编码
0#0000011
- 解码
1、Varints解码(去掉高位的msg)
0000011
2、高位补0
00000000000000000000000000000000000000000000000000000011
5、低位换高位
10000000000000000000000000000000000000000000000000000001
6、取反(除符号位)
1111111111111111111111111111111111111111111111111111111111111110
七、Length-delimited
-
string , bytes :
Length-delimited类型编码主要应用类型string、bytes、embedded messages、repeated,Length-delimited编码结构是TLV,在这里我们终于见到了Length了,也是唯一一个TLV结构的编码方式。
String Value部分解码参考:
https://baike.baidu.com/item/ASCII/309296?fr=aladdin -
embedded message
嵌套message先解出第一层
- Tag
field = 00110 (4)
wire_type = 010(2)- Length
10 (2) 个字节- Value
00001000 00000001
再将Value按照上面的方式解析,只不过嵌套的message 里面只有一个wire_type = 0 编码的一个数值,所以Length被省略了。
- repeated
repeated 字符串和数值类型有一定区别,string和bytes 编码结构变为 Tag-Length-Value-Tag-Length-Value, 数值类型会进行打包处理,编码结构变为 Tag-Length-Value-Value-Value。
field_numbe = 00111(5)
wire_type= 010(2)
length = 011 (3字节)
11010111 00001000 00000010
通过msb判断可解析为 1#1010111 0#0001000 和 0#0000010
解析得到:
00010001010111(1111) 、0000010(2)
Tag-Length-Value-Tag-Length-Value则比较简单,他们的TAG都是一样的,解析Tag,通过Length获取对应长度的字符Value即可
八、32-bit、64-bit
这个比较简单,32-bit、64-bit Value都是定长的分别是32bit(4个字节) 、64bit(8个字节),直接取得对应的字节数即可,当值通常大于 2的28次方和2的56次方,则比 uint32和uint64 更高效,因为Varints编码每个字节会占用一个bit的msb(most significant bit)。
九、如何落地?
在SpringMVC进入业务方法前,会根据@RequestBody注解选择适当的HttpMessageConverter实现类来将请求参数解析到string变量中,具体来说是使用了StringHttpMessageConverter类,它的canRead()方法返回true,然后它的read()方法会从请求中读出请求参数,绑定到业务方法的变量中。
当SpringMVC执行业务方法后,由于返回值标识了@ResponseBody,SpringMVC将使用StringHttpMessageConverter的write()方法,将业务方法的返回值写入响应报文,当然,此时canWrite()方法返回true。
public class ProtobufHttpMessageConverter extends AbstractHttpMessageConverter<Message> {
public static final Charset DEFAULT_CHARSET;
public static final MediaType PROTOBUF;
private static final Map<Class<?>, Method> methodCache;
static {
DEFAULT_CHARSET = StandardCharsets.UTF_8;
PROTOBUF = new MediaType("application", "x-protobuf", DEFAULT_CHARSET);
methodCache = new ConcurrentReferenceHashMap();
}
@Override
protected boolean supports(Class<?> clazz) {
return Message.class.isAssignableFrom(clazz);
}
@Override
protected Message readInternal(Class<? extends Message> aClass, HttpInputMessage httpInputMessage) throws IOException, HttpMessageNotReadableException {
MediaType contentType = httpInputMessage.getHeaders().getContentType();
Message.Builder builder = this.getMessageBuilder(aClass);
if (PROTOBUF.isCompatibleWith(contentType)) {
builder.mergeFrom(httpInputMessage.getBody());
} else {
Log.error("Request must be set Content-Type = application/x-protobuf");
return null;
}
return builder.build();
}
@Override
protected void writeInternal(Message message, HttpOutputMessage httpOutputMessage) throws IOException, HttpMessageNotWritableException {
MediaType contentType = httpOutputMessage.getHeaders().getContentType();
if (PROTOBUF.isCompatibleWith(contentType)) {
this.setProtoHeader(httpOutputMessage, message);
//CodedOutputStream codedOutputStream = CodedOutputStream.newInstance(httpOutputMessage.getBody());
//message.writeTo(codedOutputStream);
//codedOutputStream.flush();
httpOutputMessage.getBody().write(message.toByteArray());
httpOutputMessage.getBody().flush();
}
}
private void setProtoHeader(HttpOutputMessage response, Message message) {
response.getHeaders().set("X-Protobuf-Schema", message.getDescriptorForType().getFile().getName());
response.getHeaders().set("X-Protobuf-Message", message.getDescriptorForType().getFullName());
}
private Message.Builder getMessageBuilder(Class<? extends Message> clazz) {
try {
Method method = (Method)methodCache.get(clazz);
if (method == null) {
method = clazz.getMethod("newBuilder");
methodCache.put(clazz, method);
}
return (Message.Builder)method.invoke(clazz);
} catch (Exception var3) {
throw new HttpMessageConversionException("Invalid Protobuf Message type: no invocable newBuilder() method on " + clazz, var3);
}
}
}
具体落地细节请参考Url: https://github.com/anthony3669000/web_protobuf
十、附录
Protobuf数据类型
proto | Notes | C++ | Java | C# |
---|---|---|---|---|
double | double | double | double | |
float | float | float | float | |
int32 | 使用可变长度编码。编码负数的效率低 - 如果你的字段可能有负值,请改用 sint32 | int32 | int | int |
int64 | 使用可变长度编码。编码负数的效率低 - 如果你的字段可能有负值,请改用 sint64 | int64 | long | long |
uint32 | 使用可变长度编码(无符号) | uint32 | int | uint |
uint64 | 使用可变长度编码(无符号 | uint64 | long | ulong |
sint32 | 使用可变长度编码。有符号的 int 值。这些比常规 int32 对负数能更有效地编码 | int32 | int | int |
sint64 | 使用可变长度编码。有符号的 long 值。这些比常规 int64 对负数能更有效地编码 | uint64 | long | long |
fixed32 | 总是四个字节。如果值通常大于 2的28次方,则比 uint32 更有效。 | uint32 | int | uint |
fixed64 | 总是八个字节。如果值通常大于 2的56次方,则比 uint64 更有效。 | uint64 | long | ulong |
sfixed32 | 总是四个字节 | int32 | int | int |
sfixed64 | 总是八个字节 | int64 | long | long |
bool | bool | boolean | boolean | |
string | 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本 | string | String | string |
bytes | 可以包含任意字节序列 | string | ByteString | string |
十一、参考资料
https://github.com/anthony3669000/web_protobuf
https://github.com/protocolbuffers/protobuf/releases
https://developers.google.com/protocol-buffers/docs/overview