当涉及到REST API时,JSON(JavaScript对象表示法)已经成为数据交换的格式。很久以前,开发人员放弃了XML,转而支持JSON,因为JSON紧凑,无模式,易于阅读且易于在线传输。
JSON的无模式性质确保您可以添加或删除字段,并且仍然拥有有效的JSON。但是,这也意味着,由于添加或删除了字段,您现在功能全面的客户端将开始失败。当您具有微服务体系结构并且有100个服务通过JSON相互通信并且您不小心更改了其中一个服务的JSON响应时,此问题会放大。
此外,JSON通过重复字段名(如果你使用的是阵列)发生不必要的额外空间,变得相当难读的,一旦你开始建立你的数据结构。
2001年,Google开发了一种内部,平台和语言独立的数据序列化格式,称为Protobuf(协议缓冲区的缩写),以解决JSON的所有缺点。Protobuf的设计目标是简化和提高速度。
在本文中,我将分享什么是Protobuf,以及在REST API中替换JSON如何显着简化客户端和服务器之间的数据序列化。
表中的内容
- Protobuf是什么
- 工具
- Protobuf定义
- 创建REST端点
- 使用REST端点
- 与JSON相比
- 结论
1. Protobuf是什么
Protobuf的维基百科说:
协议缓冲区(Protobuf)是一种序列化结构化数据的方法。在开发程序时,通过线路相互通信或存储数据是很有用的。该方法涉及描述某些数据结构的接口描述语言和从该描述生成源代码的程序,用于生成或解析表示结构化数据的字节流。
在Protobuf中,开发人员在.proto
文件中定义数据结构(称为消息),然后在编译protoc
器的帮助下编译为代码。该编译器带有用于多种语言(来自Google和社区)的代码生成器,并生成用于存储数据的数据结构和用于对其进行序列化和反序列化的方法。
Protobuf消息被序列化为二进制格式,而不是诸如JSON之类的文本,因此Protobuf中的消息根本不是人类可读的。由于二进制性质,Protobuf消息可以压缩,并且比等效的JSON消息占用更少的空间。
一旦完成服务器的实现,就可以.proto
与客户端共享文件(就像共享API期望并返回的JSON模式一样),它们可以利用相同的代码生成来使用消息。
2.工具
我们需要安装以下工具来遵循本教程。
- VS代码或您最喜欢的代码编辑器。
- Golang编译器和工具(我们将在Go中编写服务器和客户端)
-
[protoc](https://github.com/protocolbuffers/protobuf/releases)
protobuf编译器。
请遵循每个工具的安装说明。为了简洁起见,我跳过了此处的说明,但是如果您遇到任何错误,请告诉我,我们将很乐意为您提供帮助。
3. Protobuf定义
在本节中,我们将创建一个.proto
文件,在整个演示过程中将使用该文件。该原始文件将包含两个消息EchoRequest
和EchoResponse
。
然后,我们将创建REST端点接受EchoRequest
并使用进行回复EchoResponse
。然后,我们将使用REST端点创建一个客户端(也在Go中)。
在开始之前,我希望您注意有关该项目目录结构的一些事情。
- 我已经在文件夹
github.com/kaysush
中创建了一个文件$GOPATH/src
夹。$GOPATH
安装go编译器和工具时会设置变量。 - 我将项目文件夹
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" //<-- 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
执行它。
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个字节。
JSON的请求大小是普通消息的两倍
客户端记录Protobuf和JSON请求的平均响应时间(以纳秒为单位)(封送请求+发送请求+封送响应)。
我运行了client.go
3次,尽管平均响应时间差异很小,但我们可以看到Protobuf请求的平均响应时间始终较小。
差异很小,因为我们的消息非常小,随着消息大小的增加,将其取消编组为JSON的成本也随之增加。
多个比较请求
7.结论
在REST API中使用Protobuf而不是JSON可以导致更小的请求大小和更快的响应时间。在我们的演示中,由于有效负载较小,因此响应时间效果并不明显,但是看到这种模式,可以肯定地说Protobuf的性能应优于JSON。
那里有它。在您的REST API中使用Protobuf替换JSON。
如果您发现我的代码有任何问题或有任何疑问,请随时发表评论。
直到快乐的编码!:)
翻译自:https://medium.com/@Sushil_Kumar/supercharge-your-rest-apis-with-protobuf-b38d3d7a28d3