一泉沾、什么是Protobuf
Protobuf(Google Protocol Buffers)是Google提供一個具有高效的協(xié)議數(shù)據(jù)交換格式工具庫侥衬,類似于常用的XML及JSON梯投,但具有更小的傳輸體積钦扭、更高的編碼、解碼能力汇鞭,特別適合于數(shù)據(jù)存儲凄敢、網(wǎng)絡(luò)數(shù)據(jù)傳輸?shù)葘Υ鎯w積碌冶、實時性要求高的領(lǐng)域。
二涝缝、Protobuf真的能壓縮空間嗎扑庞?
經(jīng)過對比我們發(fā)現(xiàn)使用了Protobuf的的數(shù)據(jù)比沒有使用Protobuf編碼的數(shù)據(jù)長度還多了兩個字節(jié),這到底是為什么拒逮?我們接著往下看一個Long類型的對比
比如Long類型發(fā)現(xiàn)罐氨,沒有Protobuf編碼的Long類型的數(shù)據(jù)占了8個字節(jié),而使用Protobuf的只占了數(shù)據(jù)只占了四個字節(jié)滩援,這是為什么栅隐?接下來我們一起來分析下原因
三、Protobuf編碼結(jié)構(gòu)
Tag
field_number: message 定義字段時指定的字段編號
wire_type: ProtoBuf 編碼類型玩徊,根據(jù)這個類型選擇不同的 Value 編碼方案租悄。Length 是可選的,不同類型的數(shù)據(jù)編碼結(jié)構(gòu)可能會變成 Tag - Value 的格式恩袱。沒有Length如何確認Value的邊界泣棋?答案就是 Varint 編碼。
Value 數(shù)據(jù)內(nèi)容根據(jù)數(shù)據(jù)類型不同使用不同方式的編碼的數(shù)據(jù)
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 編碼的規(guī)則主要特點:
在每個字節(jié)第一個 bit 設(shè)置了 msb(most significant bit )技竟,標識是否需要繼續(xù)讀取下一個字節(jié)
存儲數(shù)值對應(yīng)的二進制補碼
補碼的低位排在前面
- 編碼
對388888的補碼進行編碼
00000000 00000000 00000000 00000000 00000000 00000101 11101111 00011000
1、去掉高位多余的0
101 11101111 00011000
2屈藐、從后依次向前取 7 位組并反轉(zhuǎn)排序
0011000 1011110 0010111
3榔组、加上 msb
1#0011000 1#1011110 0#0010111
- 解碼
10011000 11011110 00010111 …
每個字節(jié)的第一個 bit 為 msb 位,msb = 1 表示需要再讀一個字節(jié)(還未結(jié)束)联逻,msb = 0 表示無需再讀字節(jié)(讀取到此為止)搓扯。
1、讀取Value的值
10011000 11011110 00010111
2包归、去掉msb
0011000 1011110 0010111
3锨推、將這三個 7-bit 組反轉(zhuǎn)得到補碼
0010111 1011110 0011000
好像一徹都辣么美好, but, 如果是負數(shù)怎么辦?
-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 :
image.png
Length-delimited類型編碼主要應(yīng)用類型string休弃、bytes吞歼、embedded messages、repeated玫芦,Length-delimited編碼結(jié)構(gòu)是TLV浆熔,在這里我們終于見到了Length了,也是唯一一個TLV結(jié)構(gòu)的編碼方式桥帆。
String Value部分解碼參考:
https://baike.baidu.com/item/ASCII/309296?fr=aladdin -
embedded message
image.png
嵌套message先解出第一層
- Tag
field = 00110 (4)
wire_type = 010(2)- Length
10 (2) 個字節(jié)- Value
00001000 00000001
再將Value按照上面的方式解析医增,只不過嵌套的message 里面只有一個wire_type = 0 編碼的一個數(shù)值,所以Length被省略了老虫。
- repeated
repeated 字符串和數(shù)值類型有一定區(qū)別叶骨,string和bytes 編碼結(jié)構(gòu)變?yōu)?Tag-Length-Value-Tag-Length-Value, 數(shù)值類型會進行打包處理,編碼結(jié)構(gòu)變?yōu)?Tag-Length-Value-Value-Value祈匙。
field_numbe = 00111(5)
wire_type= 010(2)
length = 011 (3字節(jié))
11010111 00001000 00000010
通過msb判斷可解析為 1#1010111 0#0001000 和 0#0000010
解析得到:
00010001010111(1111) 忽刽、0000010(2)
Tag-Length-Value-Tag-Length-Value則比較簡單天揖,他們的TAG都是一樣的,解析Tag跪帝,通過Length獲取對應(yīng)長度的字符Value即可
八今膊、32-bit、64-bit
這個比較簡單伞剑,32-bit斑唬、64-bit Value都是定長的分別是32bit(4個字節(jié)) 、64bit(8個字節(jié))黎泣,直接取得對應(yīng)的字節(jié)數(shù)即可恕刘,當(dāng)值通常大于 2的28次方和2的56次方,則比 uint32和uint64 更高效抒倚,因為Varints編碼每個字節(jié)會占用一個bit的msb(most significant bit)褐着。
九、如何落地托呕?
在SpringMVC進入業(yè)務(wù)方法前含蓉,會根據(jù)@RequestBody注解選擇適當(dāng)?shù)腍ttpMessageConverter實現(xiàn)類來將請求參數(shù)解析到string變量中,具體來說是使用了StringHttpMessageConverter類镣陕,它的canRead()方法返回true谴餐,然后它的read()方法會從請求中讀出請求參數(shù),綁定到業(yè)務(wù)方法的變量中呆抑。
當(dāng)SpringMVC執(zhí)行業(yè)務(wù)方法后岂嗓,由于返回值標識了@ResponseBody,SpringMVC將使用StringHttpMessageConverter的write()方法鹊碍,將業(yè)務(wù)方法的返回值寫入響應(yīng)報文厌殉,當(dāng)然,此時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);
}
}
}
具體落地細節(jié)請參考Url: https://github.com/anthony3669000/web_protobuf
十公罕、附錄
Protobuf數(shù)據(jù)類型
proto | Notes | C++ | Java | C# |
---|---|---|---|---|
double | double | double | double | |
float | float | float | float | |
int32 | 使用可變長度編碼。編碼負數(shù)的效率低 - 如果你的字段可能有負值耀销,請改用 sint32 | int32 | int | int |
int64 | 使用可變長度編碼楼眷。編碼負數(shù)的效率低 - 如果你的字段可能有負值,請改用 sint64 | int64 | long | long |
uint32 | 使用可變長度編碼(無符號) | uint32 | int | uint |
uint64 | 使用可變長度編碼(無符號 | uint64 | long | ulong |
sint32 | 使用可變長度編碼熊尉。有符號的 int 值罐柳。這些比常規(guī) int32 對負數(shù)能更有效地編碼 | int32 | int | int |
sint64 | 使用可變長度編碼。有符號的 long 值狰住。這些比常規(guī) int64 對負數(shù)能更有效地編碼 | uint64 | long | long |
fixed32 | 總是四個字節(jié)张吉。如果值通常大于 2的28次方,則比 uint32 更有效催植。 | uint32 | int | uint |
fixed64 | 總是八個字節(jié)肮蛹。如果值通常大于 2的56次方勺择,則比 uint64 更有效。 | uint64 | long | ulong |
sfixed32 | 總是四個字節(jié) | int32 | int | int |
sfixed64 | 總是八個字節(jié) | int64 | long | long |
bool | bool | boolean | boolean | |
string | 字符串必須始終包含 UTF-8 編碼或 7 位 ASCII 文本 | string | String | string |
bytes | 可以包含任意字節(jié)序列 | string | ByteString | string |
十一伦忠、參考資料
https://github.com/anthony3669000/web_protobuf
https://github.com/protocolbuffers/protobuf/releases
https://developers.google.com/protocol-buffers/docs/overview