使用Protobuf增強您的REST API

當涉及到REST API時,JSON(JavaScript對象表示法)已經成為數據交換的格式。很久以前,開發人員放棄了XML,轉而支持JSON,因為JSON緊湊,無模式,易於閱讀且易於在線傳輸。

JSON的無模式性質確保您可以添加或刪除字段,並且仍然擁有有效的JSON。但是,這也意味著,由於添加或刪除了字段,您現在功能全面的客戶端將開始失敗。當您具有微服務體系結構並且有100個服務通過JSON相互通信並且您不小心更改了其中一個服務的JSON響應時,此問題會放大。

此外,JSON通過重複字段名(如果你使用的是陣列)發生不必要的額外空間,變得相當難讀的,一旦你開始建立你的數據結構。

2001年,Google開發了一種內部,平臺和語言獨立的數據序列化格式,稱為Protobuf(協議緩衝區的縮寫),以解決JSON的所有缺點。Protobuf的設計目標是簡化和提高速度。

在本文中,我將分享什麼是Protobuf,以及在REST API中替換JSON如何顯著簡化客戶端和服務器之間的數據序列化。

表中的內容

  1. Protobuf是什麼
  2. 工具
  3. Protobuf定義
  4. 創建REST端點
  5. 使用REST端點
  6. 與JSON相比
  7. 結論

1. Protobuf是什麼

Protobuf的維基百科說:

協議緩衝區(Protobuf)是一種序列化結構化數據的方法。在開發程序時,通過線路相互通信或存儲數據是很有用的。該方法涉及描述某些數據結構的接口描述語言和從該描述生成源代碼的程序,用於生成或解析表示結構化數據的字節流。

在Protobuf中,開發人員在.proto文件中定義數據結構(稱為消息),然後在編譯protoc器的幫助下編譯為代碼。該編譯器帶有用於多種語言(來自Google和社區)的代碼生成器,並生成用於存儲數據的數據結構和用於對其進行序列化和反序列化的方法。

Protobuf消息被序列化為二進制格式,而不是諸如JSON之類的文本,因此Protobuf中的消息根本不是人類可讀的。由於二進制性質,Protobuf消息可以壓縮,並且比等效的JSON消息佔用更少的空間。

一旦完成服務器的實現,就可以.proto與客戶端共享文件(就像共享API期望並返回的JSON模式一樣),它們可以利用相同的代碼生成來使用消息。

2.工具

我們需要安裝以下工具來遵循本教程。

  1. VS代碼或您最喜歡的代碼編輯器。
  2. Golang編譯器和工具(我們將在Go中編寫服務器和客戶端)
  3. [protoc](https://github.com/protocolbuffers/protobuf/releases) protobuf編譯器。

請遵循每個工具的安裝說明。為了簡潔起見,我跳過了此處的說明,但是如果您遇到任何錯誤,請告訴我,我們將很樂意為您提供幫助。

3. Protobuf定義

在本節中,我們將創建一個.proto文件,在整個演示過程中將使用該文件。該原始文件將包含兩個消息EchoRequest和EchoResponse。

然後,我們將創建REST端點接受EchoRequest並使用進行回覆EchoResponse。然後,我們將使用REST端點創建一個客戶端(也在Go中)。

在開始之前,我希望您注意有關該項目目錄結構的一些事情。

  1. 我已經在文件夾github.com/kaysush中創建了一個文件$GOPATH/src夾。$GOPATH安裝go編譯器和工具時會設置變量。
  2. 我將項目文件夾protobuf-demo放入github.com/kaysush。

您可以在下圖中看到目錄結構。

$GOPATH
├── bin
├── pkg
└── src
└── github.com
└── kaysush
└── protobuf-demo
├── server
│ └── test.go
├── client
└── proto
└── echo
├── echo.proto
└── echo.pb.go

創建一個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" //)
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

執行它。

go run test.go

您應該看到以下輸出。

Value from un-marshalled data is Sushil

這表明我們的Protobuf定義運行良好。在下一節中,我們將實現REST端點並接受Protobuf消息作為請求的有效負載。

4.創建REST端點

Golang的net.http軟件包足以創建REST API,但為了使我們更容易一點,我們將使用該[gorilla/mux](https://www.gorillatoolkit.org/pkg/mux)軟件包來實現REST端點。

使用以下命令安裝軟件包。

go get github.com/gorilla/mux

server.go在server文件夾中創建一個文件,然後開始編碼。

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 : %v\\n", 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函數的代碼應易於理解。我們http.Request使用讀取字節iotuil.ReadAll,然後從中讀取Unmarshal字節。EchoRequest``Name

然後,我們按照相反的步驟來構造一個EchoResponse。

在Main()函數中,我們定義了一條路由/echo,該路由應接受POST方法並通過調用Echo函數來處理請求。

啟動服務器。

go run server.go

您應該會看到消息 Starting API server...

具有/echo端點接受POST功能的REST-ish API(因為我們未遵循POST請求的REST規範)已準備好接受來自客戶端的Protobuf消息。

5.使用REST端點

在本節中,我們將實現使用/echo端點的客戶端。

我們的客戶端和服務器都在相同的代碼庫中,因此我們不需要從proto文件中重新生成代碼。在實際使用中,您將proto與客戶端共享文件,然後客戶端將以其選擇的編程語言生成其代碼文件。

client.go在client文件夾中創建一個文件。

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 : %v\\n", resp.GetMessage())
}

client.go

客戶應該更容易理解。我們正在使用http.Post將Protobuf字節發送到我們的API服務器,並讀迴響應,然後將Unmarshal其發送給EchoResponse。

立即運行客戶端。

go run client.go

您應該看到服務器的響應。

Response from API is : Hello Sushil

6.與JSON相比

我們已經成功實現了使用Protobuf而不是JSON的API。

在本節中,我們將實現一個終結點,該終結點EchoJsonRequest在JSON中接受類似內容,並在JSON中也進行響應。

我已經structs為JSON 實現了另一個程序包。

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 : %v\\n", 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")

讓我們修改客戶端,以將重複的請求發送到Protobuf和JSON端點,並計算平均響應時間。

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 %v\\n", 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 %v\\n", i)
startTime := time.Now()
makeJsonRequest(requestJson)
elapsed := time.Since(startTime)
totalJSONTime += elapsed.Nanoseconds()
}
fmt.Printf("Average Protobuf Response time : %v nano-seconds\\n", totalPBTime/1000)
fmt.Printf("Average JSON Response time : %v nano-seconds\\n", totalJSONTime/1000)
}

運行服務器和客戶端。

我們的服務器記錄了請求的內容長度,您可以看到Protobuf請求為8個字節,而相同的JSON請求為17個字節


使用Protobuf增強您的REST API


JSON的請求大小是普通消息的兩倍

客戶端記錄Protobuf和JSON請求的平均響應時間(以納秒為單位)(封送請求+發送請求+封送響應)。

我運行了client.go3次,儘管平均響應時間差異很小,但我們可以看到Protobuf請求的平均響應時間始終較小。

差異很小,因為我們的消息非常小,隨著消息大小的增加,將其取消編組為JSON的成本也隨之增加。


使用Protobuf增強您的REST API


多個比較請求

7.結論

在REST API中使用Protobuf而不是JSON可以導致更小的請求大小和更快的響應時間。在我們的演示中,由於有效負載較小,因此響應時間效果並不明顯,但是看到這種模式,可以肯定地說Protobuf的性能應優於JSON。

那裡有它。在您的REST API中使用Protobuf替換JSON。

如果您發現我的代碼有任何問題或有任何疑問,請隨時發表評論。

直到快樂的編碼!:)

翻譯自:https://medium.com/@Sushil_Kumar/supercharge-your-rest-apis-with-protobuf-b38d3d7a28d3


分享到:


相關文章: