Protocol Buffers(簡稱 protobuf)
,是 Google 推出的一種數據交換格式,採用 Varint 和 ZigZag 等二進制編碼,數據壓縮效果顯著,可用來傳輸數據或持久化數據。
JSON 是什麼? JSON 全稱爲 JavaScript Object Notation(JavaScript對象標記)
,即 JS對象的字符串表示。其採用文本編碼,是現今最通用的數據交換格式。2001年3月,State Software公司的聯合創始人設計了此種格式,並隨後進行了標準化。現在有 ECMA-404(2013年)和 RFC-8259(2017年)兩種標準。
protobuf 是什麼? protobuf 全稱爲 Protocol Buffers(“協議緩衝”)
,是一種數據壓縮性能優秀的數據存儲和交換格式。其採用二進制編碼,通常跟 gRPC 一起使用。
2001年 Google公司內部誕生了proto1版本,並隨後在2008年以BSD協議開源了proto2,2016年釋出proto3正式版。
對於 proto2,官方推出了針對 C++、Java、C# 和 Python 語言的 protobuf編譯器 protoc;而在 proto3 中,增加了對 Dart、GO、Kotlin 和 Ruby 的官方支持。另外,第三方有提供對 JavaScript 和 PHP 等等語言的支持。
開始使用 protobuf (1)下載編譯器並設置環境變量。
PB_REL="https://github.com/protocolbuffers/protobuf/releases" curl -LO $PB_REL /download/v3.15.8/protoc-3.15.8-osx-x86_64.zip unzip protoc-3.15.8-osx-x86_64.zip -d ~/go/bin/protoc-3.15.8 cp ~/go/bin/protoc-3.15.8/bin/protoc ~/go/bingo get google.golang.org/protobuf/cmd/protoc-gen-go \ google.golang.org/grpc/cmd/protoc-gen-go-grpc export PATH="$PATH :$(go env GOPATH) /bin"
(2)選擇使用 proto2 還是 proto3,不同的版本有不同的語法,且不兼容。相對而言,proto3 的語法更簡單。
(3)根據語法編寫 proto 文件。
syntax = "proto3" ; option go_package = "/pb" ;option java_package = "com.example.m.pb" ;option java_outer_classname = "AnimalProto" ;package pb;message Animal { int64 id = 1 ; string name = 2 ; }
(4)生成需要的語言代碼
protoc --go_out=. animal.proto
生成後的文件 animal.pb.go 如下:
package pbimport ( protoreflect "google.golang.org/protobuf/reflect/protoreflect" protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" ) const ( _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20 ) ) type Animal struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields Id int64 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` } ...
JSON 和 protobuf 的編碼大不同 我們舉一個例子來說明會比較直觀。
這是一隻柯基犬,它叫 Dokky,編號 12。我們可以將這些信息結構化成對象如下:
type Animal struct { Id int64 Name string }
編寫程序分別對該 Animal 進行 JSON 和 protobuf 序列化:
func marshal () { dokky := pb.Animal{ Id: 12 , Name: "Dokky" , } pbs, _ := proto.Marshal(&dokky) printBytes(pbs) jbs, _ := json.Marshal(&dokky) printBytes(jbs) } func printBytes (bs []byte ) { println (len (bs), hex.EncodeToString(bs)) }
結果:
9 080c1205446f6b6b79 24 7b226964223a31322c226e616d65223a22446f6b6b79227d
(1)JSON 序列化
{ "id" : 12 , "name" : "Dokky" }
二進制形式(Unicode編碼):
7b 22 6964 22 3a 3132 2c 22 6e616d65 22 3a 22 446f6b6b79 22 7d
共佔用 24 個字節。
(2)protobuf 序列化
共佔用 9 個字節。
通過輸出的結果簡單對比兩種序列化方式可見,protobuf
不編碼變量名;
沒有額外的 {}[],:""
等字符;
對整型不使用文本編碼。
要具體解讀這串序列,我們還得先來了解下 protobuf 的編碼方式。
Varint 編碼 Varint 即 Variable int,可變長整型編碼,是 protobuf 中最主要的編碼方式。其採用小端二進制編碼,即 LSB(Least Significant Bit,最低有效位)
置於低地址。每個字節的首位即 MSB(Most Significant Bit,最高有效位)
用來標識下個字節是否需要讀取, 1 表示需要,0 反之。
以數字 12 為例(爲簡便起見,假定類型爲 int16):
二進制序列:0000 0000 0000 1100 從末尾開始7位一組: 000 1100 添加MSB:0000 1100 十六進制表示:0C
再以數字 255 為例(爲簡便起見,假定類型爲 int16):
二進制序列:0000 0000 1111 1110 從末尾開始7位一組: 111 1110 000 0001 添加MSB:1111 1110 0000 0001 十六進制表示:FE01
protobuf 編碼規則 一般來說,在protobuf 中每個字段拆分成四個部分進行編碼,依次是:
number:字段序號;
type:字段類型,佔用 3 個比特位,與 number 一起構成字段標識 tag,佔用一個或多個字節,採用 Varint 編碼;其取值含義見下表;
length(可選):字段長度,當 type 爲 2 時有值;
value:字段值,爲零值的字段不會進行編碼。
Type
Meaning
Used for
0(000)
Varint(可變長整型)
int32、int64、uint32、uint64、sint32、sint64、bool、enum
1(001)
64-bit(固定64位)
fixed64、sfixed64、double
2(010)
Length-delimited(指定長度)
string、bytes、embedded message、packed repeated fields
5(101)
32-bit(固定32位)
fixed32、sfixed32、float
解讀序列 086c 1205 446fb6b79 08 => 00001 000 => number : 1 , type : 0 0C => value : 12
序號爲 1 的字段,類型爲 Varint,值爲 12。
12 => 00010 010 => number : 2 , type : 2 05 => length : 5 446fb6b79 => value : Dokky
序號爲 2 的字段,類型爲 Length-delimited,值爲 Dokky。
ZigZag 編碼 前面我們討論了非負數的 Varint 編碼,現在我們來看看負數。其實對於負數,並不建議使用 int32 或 int64 等類型,而應使用 sint32 或 sint64,即帶符號的整型。為什麼呢?下面我們對 -1 分別指定 int 32 和 sint32 類型進行編碼:
(1)int32(-1)
使用 int32 或 int64 類型時,進行 Varint 編碼,負數總是佔用 10 個字節。因此 int32(-1) 編碼爲:
二進制序列:11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 從尾部開始7位一組: 1111111 1111111 1111111 1111111 1111111 1111111 1111111 1111111 1111111 0000001 添加MSB:11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111 00000001 十六進制表示:FF FF FF FF FF FF FF FF FF 01
(2)sint32(-1)
當使用 sint32 或 sint64 時,會先進行 ZigZag 編碼再進行 Varint 編碼。ZigZag 就是將負數放到整數前面,交替進行編碼,呈“Z” 或 “之” 字形,故有此稱。
對 sint32, 數字 n 的 ZigZag 編碼爲:(n << 1) ^ (n >> 31)
對 sint64, 數字 n 的 ZigZag 編碼爲:(n << 1) ^ (n >> 63)
因此 sint32(-1) 編碼爲:
二進制序列: 11111111 11111111 11111111 11111111 n << 1: 11111111 11111111 11111111 11111110 n >> 31: 11111111 11111111 11111111 11111111 異或: 00000000 00000000 00000000 00000001 從尾部開始7位一組: 0000001 添加MSB: 00000001 十六進制表示: 01
可見,int32(-1) 佔用 10 個字節,而 sint32(-1) 佔用 1 個字節。在值可能爲負數的情況下,應使用帶符號整型,可以大大減少負數的編碼量。
proto3 語法 syntax = "proto3" ; option go_package = "/pb" ;option java_package = "com.example.m.pb" ;option java_outer_classname = "AnimalProto" ;package pb;message Animal { int64 id = 1 ; string name = 2 ; reserved 3 to 5 ; reserved "kind" ; Vision vision = 6 ; oneof difference{ HumanDifference human = 8 ; OtherAnimalDifference other = 9 ; } } enum Vision { VISION_UNSPECSIFIED = 0 ; VISION_GREAT = 1 ; VISION_BURRED = 2 ; VISION_BLIND = 3 ; } message HumanDifference { repeated string langs = 1 ; } message OtherAnimalDifference { repeated string natural_enemies = 1 ; }
注意:
建议将变量序号 1~15 的给最常用的字段用,并保留扩展;
message 可以导入;
字段有默认零值,零值不会被序列化;
字段序號一旦指定,一般不改動,否則會向後不兼容。
當值為 1~128(2^7) 時使用 int32 類型僅用 1 個字節,當值為 129~2^14 時僅用 2 個字節;
對大於 2^28 的數,需要用到 5 個字節及以上的空間,這時候使用 fixed32 或 fixed64 更為划算。
proto3 定義了以下的標量類型(Scalar Type, hold one value at a time)
:
Scalar Type
Meaning
Go Type
Java/Kotlin Type
double
雙精度浮點型
float64
double
float
浮點型
float
float
int32
使用Varint編碼的32位整型
int32
int
int64
使用Varint編碼的64位整型
int64
long
uint32
使用Varint編碼的無符號32位整型
uint32
long
uint64
使用Varint編碼的無符號64位整型
uint64
long
sint32
使用Varint編碼的有符號32位整型
int32
int
sint64
使用Varint編碼的有符號64位整型
int64
long
fixed32
固定32位的整型
uint32
int
fixed64
固定64位的整型
uint64
long
sfixed32
固定長度的有符號32位整型
int32
int
sfixed64
固定長度的有符號64位整型
int64
long
bool
布爾型
bool
boolean
string
採用UTF-8或者7位ASCII碼編碼的字符串
string
String
bytes
字節序列
[]byte
ByteString
proto 風格
文件名、字段名建議使用全小寫字母加下划線(lower_snake_case)
;
每行長度限定在 80 個字符;
使用 2 個空格作為縮緊;
字符串使用雙引號包裹;
repeated 類型的字段名使用複數形式;
enum 類型第一個枚舉值建議以 UNSPECIFIED 為後綴標明為「未指定值」,值為 0;
RPC 服務名及方法名採用大駝峰。
不適用 protobuf 的情況
目前官方僅支持 C++、C#、Dart、Go、Java、Kotlin、Python 和 Ruby,其它語言有的有第三方支持,需慎重考慮;
傳輸超過 1MB 的 message 時建議另擇策略,protobuf 並非爲大數據集而設計;
當枚舉值的零值在應用中有特殊含義時,不應使用 protobuf 中的 enum,因 protobuf 中的 enum 默認零值只應該是未指定的意思;
需要使用到 protobuf 中未定義或不完善的類型時。
問答 為什麼 protobuf 叫 protobuf? Protocol 即協議,指的是商定的消息協議或者說消息格式,它存放在 proto 文件中。
而 Buffers 呢?
在早期的實現中並沒有編譯器自動生成類,而是有一個名為 ProtocolBuffer 的類實現了一個 Buffer,用戶通過 AddValue(tag, value) 方法添加 tag/value 對(以原始字節方式存儲)到 Buffer中,然後再讀取出來。這個 tag 其實是用來標示字段序號和類型,value 則是編碼後的字段值。
這個 Buffer 在新的版本中已經不在,但名稱保留下來了。
protobuf 是否可以在 HTTP 中使用? gRPC 基於 HTTP2 二進制協議,比 HTTP 文本協議傳輸數據量小;而且長連接,減少建立連接的消耗。protobuf 是一種二進制的數據交換格式,序列化後傳輸的數據量很小,可以說 protobuf + gRPC 結合很登對。
通常 JSON + HTTP 在上層應用中很流行,JSON 是一種文本型的數據交換格式,易讀,簡單地將對象轉為 JSON 字符串,然後再以其二進制形式傳輸即可。
但其實 protobuf 是可以在 HTTP 協議中運用的,使用 Content-Type 為 application/x-protobuf,將使用 proto 序列化後的二進制流傳輸到客戶端,客戶端再使用 proto 的反序列化方法解析即可。
示例:HTTP over protobuf Go 服務端 server.go
package mainimport ( "encoding/hex" "example.com/m/pb" "github.com/gin-gonic/gin" "google.golang.org/protobuf/proto" ) func main () { s := gin.Default() s.GET("/whoami" , func (c *gin.Context) { animal := pb.Animal{ Id: 12 , Name: "Dokky" , } bs, _ := proto.Marshal(&animal) println (hex.EncodeToString(bs)) c.Data(200 , "application/x-protobuf" , bs) }) s.Run() }
server_test.go
package mainimport ( "encoding/hex" "io/ioutil" "net/http" "testing" "example.com/m/pb" "google.golang.org/protobuf/proto" ) func TestWhoami (t *testing.T) { request, err := http.NewRequest("GET" , "http://localhost:8080/whoami" , nil ) if err != nil { t.Error(err) } request.Header.Add("Content-Type" , "application/x-protobuf" ) response, err := http.DefaultClient.Do(request) if err != nil { t.Error(err) } bs, err := ioutil.ReadAll(response.Body) if err != nil { t.Error(err) } t.Log(hex.EncodeToString(bs)) whami := &pb.Animal{} proto.Unmarshal(bs, whami) t.Log(whami) }
運行命令:
go run server.go go test -test.v server_test.go curl --header "Content-Type: application/x-protobuf" localhost:8080/whoami
Java 服務端 Server.java
package com.example.m;import com.example.m.pb.AnimalProto;import com.sun.net.httpserver.HttpExchange;import com.sun.net.httpserver.HttpHandler;import com.sun.net.httpserver.HttpServer;import org.apache.commons.codec.binary.Hex;import java.io.IOException;import java.io.OutputStream;import java.net.InetSocketAddress;import java.util.List;public class Server { public static void main (String[] args) throws IOException { HttpServer server = HttpServer.create(new InetSocketAddress ("localhost" , 8080 ), 0 ); server.createContext("/whoami" , new WhoamiHandler ()); server.start(); } static class WhoamiHandler implements HttpHandler { @Override public void handle (HttpExchange exchange) throws IOException { List<String> contentTypes = exchange.getRequestHeaders().get("Content-Type" ); if (contentTypes==null || (contentTypes.size()>0 && !contentTypes.get(0 ).equals("application/x-protobuf" ))){ exchange.sendResponseHeaders(404 ,0 ); exchange.close(); return ; } OutputStream outputStream = exchange.getResponseBody(); AnimalProto.Animal animal = AnimalProto.Animal.newBuilder().setId(12 ).setName("Dokky" ).build(); byte [] bs = animal.toByteArray(); System.out.println(Hex.encodeHexString(bs)); exchange.sendResponseHeaders(200 , bs.length); outputStream.write(bs); outputStream.flush(); outputStream.close(); exchange.close(); } } }
ServerTest.java
package com.example.m;import com.example.m.pb.AnimalProto;import org.apache.commons.codec.binary.Hex;import org.apache.hc.client5.http.classic.methods.HttpGet;import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;import org.apache.hc.client5.http.impl.classic.HttpClients;import org.apache.hc.core5.http.io.entity.EntityUtils;import org.junit.jupiter.api.Test;import sun.misc.IOUtils;import java.io.IOException;class ServerTest { @Test public void testServer () throws IOException { CloseableHttpClient httpClient = HttpClients.createDefault(); HttpGet httpGet = new HttpGet ("http://localhost:8080/whoami" ); httpGet.setHeader("Content-Type" ,"application/x-protobuf" ); CloseableHttpResponse response = httpClient.execute(httpGet); byte [] bs = IOUtils.readAllBytes(response.getEntity().getContent()); System.out.printf("%d, %s\n" ,response.getCode(), Hex.encodeHexString(bs)); AnimalProto.Animal animal = AnimalProto.Animal.parseFrom(bs); System.out.printf("%d, %s\n" , animal.getId(),animal.getName()); EntityUtils.consume(response.getEntity()); httpClient.close(); } }
閱讀更多
protobuf 官方文檔
protobuf 代碼倉庫