當(dāng)涉及到REST API時(shí),JSON(JAVAScript對(duì)象表示法)已經(jīng)成為數(shù)據(jù)交換的格式。很久以前,開發(fā)人員放棄了XML,轉(zhuǎn)而支持JSON,因?yàn)镴SON緊湊,無(wú)模式,易于閱讀且易于在線傳輸。
JSON的無(wú)模式性質(zhì)確保您可以添加或刪除字段,并且仍然擁有有效的JSON。但是,這也意味著,由于添加或刪除了字段,您現(xiàn)在功能全面的客戶端將開始失敗。當(dāng)您具有微服務(wù)體系結(jié)構(gòu)并且有100個(gè)服務(wù)通過(guò)JSON相互通信并且您不小心更改了其中一個(gè)服務(wù)的JSON響應(yīng)時(shí),此問(wèn)題會(huì)放大。
此外,JSON通過(guò)重復(fù)字段名(如果你使用的是陣列)發(fā)生不必要的額外空間,變得相當(dāng)難讀的,一旦你開始建立你的數(shù)據(jù)結(jié)構(gòu)。
2001年,google開發(fā)了一種內(nèi)部,平臺(tái)和語(yǔ)言獨(dú)立的數(shù)據(jù)序列化格式,稱為Protobuf(協(xié)議緩沖區(qū)的縮寫),以解決JSON的所有缺點(diǎn)。Protobuf的設(shè)計(jì)目標(biāo)是簡(jiǎn)化和提高速度。
在本文中,我將分享什么是Protobuf,以及在REST API中替換JSON如何顯著簡(jiǎn)化客戶端和服務(wù)器之間的數(shù)據(jù)序列化。
表中的內(nèi)容
- Protobuf是什么
- 工具
- Protobuf定義
- 創(chuàng)建REST端點(diǎn)
- 使用REST端點(diǎn)
- 與JSON相比
- 結(jié)論
1. Protobuf是什么
Protobuf的維基百科說(shuō):
協(xié)議緩沖區(qū)(Protobuf)是一種序列化結(jié)構(gòu)化數(shù)據(jù)的方法。在開發(fā)程序時(shí),通過(guò)線路相互通信或存儲(chǔ)數(shù)據(jù)是很有用的。該方法涉及描述某些數(shù)據(jù)結(jié)構(gòu)的接口描述語(yǔ)言和從該描述生成源代碼的程序,用于生成或解析表示結(jié)構(gòu)化數(shù)據(jù)的字節(jié)流。
在Protobuf中,開發(fā)人員在.proto文件中定義數(shù)據(jù)結(jié)構(gòu)(稱為消息),然后在編譯protoc器的幫助下編譯為代碼。該編譯器帶有用于多種語(yǔ)言(來(lái)自Google和社區(qū))的代碼生成器,并生成用于存儲(chǔ)數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)和用于對(duì)其進(jìn)行序列化和反序列化的方法。
Protobuf消息被序列化為二進(jìn)制格式,而不是諸如JSON之類的文本,因此Protobuf中的消息根本不是人類可讀的。由于二進(jìn)制性質(zhì),Protobuf消息可以壓縮,并且比等效的JSON消息占用更少的空間。
一旦完成服務(wù)器的實(shí)現(xiàn),就可以.proto與客戶端共享文件(就像共享API期望并返回的JSON模式一樣),它們可以利用相同的代碼生成來(lái)使用消息。
2.工具
我們需要安裝以下工具來(lái)遵循本教程。
- VS代碼或您最喜歡的代碼編輯器。
- Golang編譯器和工具(我們將在Go中編寫服務(wù)器和客戶端)
- [protoc](https://github.com/protocolbuffers/protobuf/releases) protobuf編譯器。
請(qǐng)遵循每個(gè)工具的安裝說(shuō)明。為了簡(jiǎn)潔起見,我跳過(guò)了此處的說(shuō)明,但是如果您遇到任何錯(cuò)誤,請(qǐng)告訴我,我們將很樂(lè)意為您提供幫助。
3. Protobuf定義
在本節(jié)中,我們將創(chuàng)建一個(gè).proto文件,在整個(gè)演示過(guò)程中將使用該文件。該原始文件將包含兩個(gè)消息EchoRequest和EchoResponse。
然后,我們將創(chuàng)建REST端點(diǎn)接受EchoRequest并使用進(jìn)行回復(fù)EchoResponse。然后,我們將使用REST端點(diǎn)創(chuàng)建一個(gè)客戶端(也在Go中)。
在開始之前,我希望您注意有關(guān)該項(xiàng)目目錄結(jié)構(gòu)的一些事情。
- 我已經(jīng)在文件夾github.com/kaysush中創(chuàng)建了一個(gè)文件$GOPATH/src夾。$GOPATH安裝go編譯器和工具時(shí)會(huì)設(shè)置變量。
- 我將項(xiàng)目文件夾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)建一個(gè)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=.
這將生成一個(gè)echo.pb.go文件,該文件具有將我們的消息定義為的go代碼struct。
作為測(cè)試,我們將查看封送和反封送消息是否正常工作。
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定義運(yùn)行良好。在下一節(jié)中,我們將實(shí)現(xiàn)REST端點(diǎn)并接受Protobuf消息作為請(qǐng)求的有效負(fù)載。
4.創(chuàng)建REST端點(diǎn)
Golang的net.http軟件包足以創(chuàng)建REST API,但為了使我們更容易一點(diǎn),我們將使用該[gorilla/mux](https://www.gorillatoolkit.org/pkg/mux)軟件包來(lái)實(shí)現(xiàn)REST端點(diǎn)。
使用以下命令安裝軟件包。
go get github.com/gorilla/mux
server.go在server文件夾中創(chuàng)建一個(gè)文件,然后開始編碼。
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
當(dāng)前目錄如下所示。
$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
然后,我們按照相反的步驟來(lái)構(gòu)造一個(gè)EchoResponse。
在Main()函數(shù)中,我們定義了一條路由/echo,該路由應(yīng)接受POST方法并通過(guò)調(diào)用Echo函數(shù)來(lái)處理請(qǐng)求。
啟動(dòng)服務(wù)器。
go run server.go
您應(yīng)該會(huì)看到消息 Starting API server...
具有/echo端點(diǎn)接受POST功能的REST-ish API(因?yàn)槲覀兾醋裱璓OST請(qǐng)求的REST規(guī)范)已準(zhǔn)備好接受來(lái)自客戶端的Protobuf消息。
5.使用REST端點(diǎn)
在本節(jié)中,我們將實(shí)現(xiàn)使用/echo端點(diǎn)的客戶端。
我們的客戶端和服務(wù)器都在相同的代碼庫(kù)中,因此我們不需要從proto文件中重新生成代碼。在實(shí)際使用中,您將proto與客戶端共享文件,然后客戶端將以其選擇的編程語(yǔ)言生成其代碼文件。
client.go在client文件夾中創(chuàng)建一個(gè)文件。
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。
立即運(yùn)行客戶端。
go run client.go
您應(yīng)該看到服務(wù)器的響應(yīng)。
Response from API is : Hello Sushil
6.與JSON相比
我們已經(jīng)成功實(shí)現(xiàn)了使用Protobuf而不是JSON的API。
在本節(jié)中,我們將實(shí)現(xiàn)一個(gè)終結(jié)點(diǎn),該終結(jié)點(diǎn)EchoJsonRequest在JSON中接受類似內(nèi)容,并在JSON中也進(jìn)行響應(yīng)。
我已經(jīng)structs為JSON 實(shí)現(xiàn)了另一個(gè)程序包。
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ù)的請(qǐng)求發(fā)送到Protobuf和JSON端點(diǎn),并計(jì)算平均響應(yīng)時(shí)間。
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)
}
運(yùn)行服務(wù)器和客戶端。
我們的服務(wù)器記錄了請(qǐng)求的內(nèi)容長(zhǎng)度,您可以看到Protobuf請(qǐng)求為8個(gè)字節(jié),而相同的JSON請(qǐng)求為17個(gè)字節(jié)。
JSON的請(qǐng)求大小是普通消息的兩倍
客戶端記錄Protobuf和JSON請(qǐng)求的平均響應(yīng)時(shí)間(以納秒為單位)(封送請(qǐng)求+發(fā)送請(qǐng)求+封送響應(yīng))。
我運(yùn)行了client.go3次,盡管平均響應(yīng)時(shí)間差異很小,但我們可以看到Protobuf請(qǐng)求的平均響應(yīng)時(shí)間始終較小。
差異很小,因?yàn)槲覀兊南⒎浅P?,隨著消息大小的增加,將其取消編組為JSON的成本也隨之增加。
多個(gè)比較請(qǐng)求
7.結(jié)論
在REST API中使用Protobuf而不是JSON可以導(dǎo)致更小的請(qǐng)求大小和更快的響應(yīng)時(shí)間。在我們的演示中,由于有效負(fù)載較小,因此響應(yīng)時(shí)間效果并不明顯,但是看到這種模式,可以肯定地說(shuō)Protobuf的性能應(yīng)優(yōu)于JSON。
那里有它。在您的REST API中使用Protobuf替換JSON。
如果您發(fā)現(xiàn)我的代碼有任何問(wèn)題或有任何疑問(wèn),請(qǐng)隨時(shí)發(fā)表評(píng)論。
直到快樂(lè)的編碼!:)
翻譯自:https://medium.com/@Sushil_Kumar/supercharge-your-rest-apis-with-protobuf-b38d3d7a28d3






