當涉及到REST API時,JSON(JAVAScript對象表示法)已經(jīng)成為數(shù)據(jù)交換的格式。很久以前,開發(fā)人員放棄了XML,轉(zhuǎn)而支持JSON,因為JSON緊湊,無模式,易于閱讀且易于在線傳輸。
JSON的無模式性質(zhì)確保您可以添加或刪除字段,并且仍然擁有有效的JSON。但是,這也意味著,由于添加或刪除了字段,您現(xiàn)在功能全面的客戶端將開始失敗。當您具有微服務(wù)體系結(jié)構(gòu)并且有100個服務(wù)通過JSON相互通信并且您不小心更改了其中一個服務(wù)的JSON響應(yīng)時,此問題會放大。
此外,JSON通過重復字段名(如果你使用的是陣列)發(fā)生不必要的額外空間,變得相當難讀的,一旦你開始建立你的數(shù)據(jù)結(jié)構(gòu)。
2001年,google開發(fā)了一種內(nèi)部,平臺和語言獨立的數(shù)據(jù)序列化格式,稱為Protobuf(協(xié)議緩沖區(qū)的縮寫),以解決JSON的所有缺點。Protobuf的設(shè)計目標是簡化和提高速度。
在本文中,我將分享什么是Protobuf,以及在REST API中替換JSON如何顯著簡化客戶端和服務(wù)器之間的數(shù)據(jù)序列化。
表中的內(nèi)容
- Protobuf是什么
- 工具
- Protobuf定義
- 創(chuàng)建REST端點
- 使用REST端點
- 與JSON相比
- 結(jié)論
1. Protobuf是什么
Protobuf的維基百科說:
協(xié)議緩沖區(qū)(Protobuf)是一種序列化結(jié)構(gòu)化數(shù)據(jù)的方法。在開發(fā)程序時,通過線路相互通信或存儲數(shù)據(jù)是很有用的。該方法涉及描述某些數(shù)據(jù)結(jié)構(gòu)的接口描述語言和從該描述生成源代碼的程序,用于生成或解析表示結(jié)構(gòu)化數(shù)據(jù)的字節(jié)流。
在Protobuf中,開發(fā)人員在.proto文件中定義數(shù)據(jù)結(jié)構(gòu)(稱為消息),然后在編譯protoc器的幫助下編譯為代碼。該編譯器帶有用于多種語言(來自Google和社區(qū))的代碼生成器,并生成用于存儲數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)和用于對其進行序列化和反序列化的方法。
Protobuf消息被序列化為二進制格式,而不是諸如JSON之類的文本,因此Protobuf中的消息根本不是人類可讀的。由于二進制性質(zhì),Protobuf消息可以壓縮,并且比等效的JSON消息占用更少的空間。
一旦完成服務(wù)器的實現(xiàn),就可以.proto與客戶端共享文件(就像共享API期望并返回的JSON模式一樣),它們可以利用相同的代碼生成來使用消息。
2.工具
我們需要安裝以下工具來遵循本教程。
- VS代碼或您最喜歡的代碼編輯器。
- Golang編譯器和工具(我們將在Go中編寫服務(wù)器和客戶端)
- [protoc](https://github.com/protocolbuffers/protobuf/releases) protobuf編譯器。
請遵循每個工具的安裝說明。為了簡潔起見,我跳過了此處的說明,但是如果您遇到任何錯誤,請告訴我,我們將很樂意為您提供幫助。
3. Protobuf定義
在本節(jié)中,我們將創(chuàng)建一個.proto文件,在整個演示過程中將使用該文件。該原始文件將包含兩個消息EchoRequest和EchoResponse。
然后,我們將創(chuàng)建REST端點接受EchoRequest并使用進行回復EchoResponse。然后,我們將使用REST端點創(chuàng)建一個客戶端(也在Go中)。
在開始之前,我希望您注意有關(guān)該項目目錄結(jié)構(gòu)的一些事情。
- 我已經(jīng)在文件夾github.com/kaysush中創(chuàng)建了一個文件$GOPATH/src夾。$GOPATH安裝go編譯器和工具時會設(shè)置變量。
- 我將項目文件夾protobuf-demo放入github.com/kaysush。
您可以在下圖中看到目錄結(jié)構(gòu)。
$GOPATH ├── bin ├── pkg └── src └── github.com └── kaysush └── protobuf-demo ├── server │ └── test.go ├── client └── proto └── echo ├── echo.proto └── echo.pb.go
創(chuàng)建一個echo.proto文件。
syntax = "proto3"; package echo; option go_package="echo"; message EchoRequest { string name = 1; } message EchoResponse { string message = 1; }
echo.proto
將proto文件編譯為golang代碼。
protoc echo.proto --go_out=.
這將生成一個echo.pb.go文件,該文件具有將我們的消息定義為的go代碼struct。
作為測試,我們將查看封送和反封送消息是否正常工作。
package main import ( "fmt" "log" "github.com/golang/protobuf/proto" "github.com/kaysush/protobuf-demo/proto/echo" //<-- Take a note that I've created my code folder in $GOPATH/src ) func main() { req := &echo.EchoRequest{Name: "Sushil"} data, err := proto.Marshal(req) if err != nil { log.Fatalf("Error while marshalling the object : %v", err) } res := &echo.EchoRequest{} err = proto.Unmarshal(data, res) if err != nil { log.Fatalf("Error while un-marshalling the object : %v", err) } fmt.Printf("Value from un-marshalled data is %v", res.GetName()) }
test.go
執(zhí)行它。
go run test.go
您應(yīng)該看到以下輸出。
Value from un-marshalled data is Sushil
這表明我們的Protobuf定義運行良好。在下一節(jié)中,我們將實現(xiàn)REST端點并接受Protobuf消息作為請求的有效負載。
4.創(chuàng)建REST端點
Golang的net.http軟件包足以創(chuàng)建REST API,但為了使我們更容易一點,我們將使用該[gorilla/mux](https://www.gorillatoolkit.org/pkg/mux)軟件包來實現(xiàn)REST端點。
使用以下命令安裝軟件包。
go get github.com/gorilla/mux
server.go在server文件夾中創(chuàng)建一個文件,然后開始編碼。
package main import ( "fmt" "io/ioutil" "log" "net/http" "time" "github.com/golang/protobuf/proto" "github.com/gorilla/mux" "github.com/kaysush/protobuf-demo/proto/echo" ) func Echo(resp http.ResponseWriter, req *http.Request) { contentLength := req.ContentLength fmt.Printf("Content Length Received : %vn", contentLength) request := &echo.EchoRequest{} data, err := ioutil.ReadAll(req.Body) if err != nil { log.Fatalf("Unable to read message from request : %v", err) } proto.Unmarshal(data, request) name := request.GetName() result := &echo.EchoResponse{Message: "Hello " + name} response, err := proto.Marshal(result) if err != nil { log.Fatalf("Unable to marshal response : %v", err) } resp.Write(response) } func main() { fmt.Println("Starting the API server...") r := mux.NewRouter() r.HandleFunc("/echo", Echo).Methods("POST") server := &http.Server{ Handler: r, Addr: "0.0.0.0:8080", WriteTimeout: 2 * time.Second, ReadTimeout: 2 * time.Second, } log.Fatal(server.ListenAndServe()) }
server.go
當前目錄如下所示。
$GOPATH ├── bin ├── pkg └── src └── github.com └── kaysush └── protobuf-demo ├── server │ ├── test.go │ └── server.go ├── client └── proto └── echo ├── echo.proto └── echo.pb.go
該Echo函數(shù)的代碼應(yīng)易于理解。我們http.Request使用讀取字節(jié)iotuil.ReadAll,然后從中讀取Unmarshal字節(jié)。EchoRequest``Name
然后,我們按照相反的步驟來構(gòu)造一個EchoResponse。
在Main()函數(shù)中,我們定義了一條路由/echo,該路由應(yīng)接受POST方法并通過調(diào)用Echo函數(shù)來處理請求。
啟動服務(wù)器。
go run server.go
您應(yīng)該會看到消息 Starting API server...
具有/echo端點接受POST功能的REST-ish API(因為我們未遵循POST請求的REST規(guī)范)已準備好接受來自客戶端的Protobuf消息。
5.使用REST端點
在本節(jié)中,我們將實現(xiàn)使用/echo端點的客戶端。
我們的客戶端和服務(wù)器都在相同的代碼庫中,因此我們不需要從proto文件中重新生成代碼。在實際使用中,您將proto與客戶端共享文件,然后客戶端將以其選擇的編程語言生成其代碼文件。
client.go在client文件夾中創(chuàng)建一個文件。
package main import ( "bytes" "fmt" "io/ioutil" "log" "net/http" "github.com/golang/protobuf/proto" "github.com/kaysush/protobuf-demo/proto/echo" ) func makeRequest(request *echo.EchoRequest) *echo.EchoResponse { req, err := proto.Marshal(request) if err != nil { log.Fatalf("Unable to marshal request : %v", err) } resp, err := http.Post("http://0.0.0.0:8080/echo", "Application/x-binary", bytes.NewReader(req)) if err != nil { log.Fatalf("Unable to read from the server : %v", err) } respBytes, err := ioutil.ReadAll(resp.Body) if err != nil { log.Fatalf("Unable to read bytes from request : %v", err) } respObj := &echo.EchoResponse{} proto.Unmarshal(respBytes, respObj) return respObj } func main() { request := &echo.EchoRequest{Name: "Sushil"} resp := makeRequest(request) fmt.Printf("Response from API is : %vn", resp.GetMessage()) }
client.go
客戶應(yīng)該更容易理解。我們正在使用http.Post將Protobuf字節(jié)發(fā)送到我們的API服務(wù)器,并讀回響應(yīng),然后將Unmarshal其發(fā)送給EchoResponse。
立即運行客戶端。
go run client.go
您應(yīng)該看到服務(wù)器的響應(yīng)。
Response from API is : Hello Sushil
6.與JSON相比
我們已經(jīng)成功實現(xiàn)了使用Protobuf而不是JSON的API。
在本節(jié)中,我們將實現(xiàn)一個終結(jié)點,該終結(jié)點EchoJsonRequest在JSON中接受類似內(nèi)容,并在JSON中也進行響應(yīng)。
我已經(jīng)structs為JSON 實現(xiàn)了另一個程序包。
package echojson type EchoJsonRequest struct { Name string } type EchoJsonResponse struct { Message string }
echo.json.go
然后將新功能添加到server.go。
func EchoJson(resp http.ResponseWriter, req *http.Request) { contentLength := req.ContentLength fmt.Printf("Content Length Received : %vn", contentLength) request := &echojson.EchoJsonRequest{} data, err := ioutil.ReadAll(req.Body) if err != nil { log.Fatalf("Unable to read message from request : %v", err) } json.Unmarshal(data, request) name := request.Name result := &echojson.EchoJsonResponse{Message: "Hello " + name} response, err := json.Marshal(result) if err != nil { log.Fatalf("Unable to marshal response : %v", err) } resp.Write(response) }
server.go
在中為此新功能添加綁定main()。
r.HandleFunc("/echo_json", EchoJson).Methods("POST")
讓我們修改客戶端,以將重復的請求發(fā)送到Protobuf和JSON端點,并計算平均響應(yīng)時間。
package main import ( "bytes" "encoding/json" "fmt" "io/ioutil" "log" "net/http" "time" "github.com/golang/protobuf/proto" "github.com/kaysush/protobuf-demo/proto/echo" "github.com/kaysush/protobuf-demo/proto/echojson" ) func makeRequest(request *echo.EchoRequest) *echo.EchoResponse { req, err := proto.Marshal(request) if err != nil { log.Fatalf("Unable to marshal request : %v", err) } resp, err := http.Post("http://0.0.0.0:8080/echo", "application/json", bytes.NewReader(req)) if err != nil { log.Fatalf("Unable to read from the server : %v", err) } respBytes, err := ioutil.ReadAll(resp.Body) if err != nil { log.Fatalf("Unable to read bytes from request : %v", err) } respObj := &echo.EchoResponse{} proto.Unmarshal(respBytes, respObj) return respObj } func makeJsonRequest(request *echojson.EchoJsonRequest) *echojson.EchoJsonResponse { req, err := json.Marshal(request) if err != nil { log.Fatalf("Unable to marshal request : %v", err) } resp, err := http.Post("http://0.0.0.0:8080/echo_json", "application/json", bytes.NewReader(req)) if err != nil { log.Fatalf("Unable to read from the server : %v", err) } respBytes, err := ioutil.ReadAll(resp.Body) if err != nil { log.Fatalf("Unable to read bytes from request : %v", err) } respObj := &echojson.EchoJsonResponse{} json.Unmarshal(respBytes, respObj) return respObj } func main() { var totalPBTime, totalJSONTime int64 requestPb := &echo.EchoRequest{Name: "Sushil"} for i := 1; i <= 1000; i++ { fmt.Printf("Sending request %vn", i) startTime := time.Now() makeRequest(requestPb) elapsed := time.Since(startTime) totalPBTime += elapsed.Nanoseconds() } requestJson := &echojson.EchoJsonRequest{Name: "Sushil"} for i := 1; i <= 1000; i++ { fmt.Printf("Sending request %vn", i) startTime := time.Now() makeJsonRequest(requestJson) elapsed := time.Since(startTime) totalJSONTime += elapsed.Nanoseconds() } fmt.Printf("Average Protobuf Response time : %v nano-secondsn", totalPBTime/1000) fmt.Printf("Average JSON Response time : %v nano-secondsn", totalJSONTime/1000) }
運行服務(wù)器和客戶端。
我們的服務(wù)器記錄了請求的內(nèi)容長度,您可以看到Protobuf請求為8個字節(jié),而相同的JSON請求為17個字節(jié)。
JSON的請求大小是普通消息的兩倍
客戶端記錄Protobuf和JSON請求的平均響應(yīng)時間(以納秒為單位)(封送請求+發(fā)送請求+封送響應(yīng))。
我運行了client.go3次,盡管平均響應(yīng)時間差異很小,但我們可以看到Protobuf請求的平均響應(yīng)時間始終較小。
差異很小,因為我們的消息非常小,隨著消息大小的增加,將其取消編組為JSON的成本也隨之增加。
多個比較請求
7.結(jié)論
在REST API中使用Protobuf而不是JSON可以導致更小的請求大小和更快的響應(yīng)時間。在我們的演示中,由于有效負載較小,因此響應(yīng)時間效果并不明顯,但是看到這種模式,可以肯定地說Protobuf的性能應(yīng)優(yōu)于JSON。
那里有它。在您的REST API中使用Protobuf替換JSON。
如果您發(fā)現(xiàn)我的代碼有任何問題或有任何疑問,請隨時發(fā)表評論。
直到快樂的編碼!:)
翻譯自:https://medium.com/@Sushil_Kumar/supercharge-your-rest-apis-with-protobuf-b38d3d7a28d3