【實戰分享】從選型到項目落地,漫談 gRPC

什麼是 gRPC?

gRPC 的幾種常見模式html

在學習 gRPC 的時候,相信你們對於它的四種模式都有了解,咱們來簡單回顧一下:golang

  • 簡單模式(Simple RPC):這種模式最爲傳統,即客戶端發起一次請求,服務端響應一個數據,這和你們平時熟悉的 RPC 沒有什麼大的區別,因此再也不詳細介紹。
  • 服務端數據流模式(Server-side streaming RPC):這種模式是客戶端發起一次請求,服務端返回一段連續的數據流。典型的例子是客戶端向服務端發送一個股票代碼,服務端就把該股票的實時數據源源不斷的返回給客戶端。若是是使用咱們容器雲功能的同窗應該會發現,咱們的容器實時日誌流就是使用了這個典型模式。
  • 客戶端數據流模式(Client-side streaming RPC):與服務端數據流模式相反,此次是客戶端源源不斷地向服務端發送數據流,而在發送結束後,由服務端返回一個響應。典型的例子是物聯網終端向服務器報送數據。
  • 雙向數據流模式(Bidirectional streaming RPC):顧名思義,這是客戶端和服務端均可以向對方發送數據流,這個時候雙方的數據能夠同時互相發送,也就是能夠實現實時交互。典型的例子是聊天機器人。

接下來咱們經過一個小例子來看看 gRPC 具體的使用流程。api

假設咱們有一個聊天機器人,現須要增長一個對外提供服務的接口。具體需求爲,接口傳入參數是一我的名,返回一段內容是「Hello 人名」的音頻。若是這個是讓你在不使用 gRPC 的狀況下,你會怎麼作?你們可能會選擇使用 restful api 來實現這個功能,傳入人名,返回音頻二進制數據。跨域

那麼若是使用 gRPC,咱們須要怎麼來設計呢?數組

第一步,須要定義一個接口文檔,也就是 proto 文件。在定義內會定義一個 Service,接下來再在 Service 裏定義一個 SayHello 的方法。下面定義傳入參數,輸入 name 返回 message,須要注意 message 是 bytes 類型,即返回的格式是二進制數據。對於 Golang 底層對應的是一個 bytes 數據,對於其餘語言多是字節流或二進制。瀏覽器

syntax = "proto3";
package helloworld;
// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}
// The request message containing the user's name.
message HelloRequest {
string name = 1;
}
// The response message containing the greetings
message HelloReply {
bytes message = 1;

定義完成後 ,下一步就是使用 protoc 命令行工具生成代碼。下圖左側是初始化的項目,你會發現,有一個單獨的目錄 protoc,存放了 hello.proto 這個文件,這個文件就是前面定義好的。安全

下圖右側是自動生成代碼後的項目結構,生成了一個 pkg/helloworld 的包,裏面有一個 hello.pb.go,打開這個文件,你會發現剛纔定義的 proto 已經被翻譯成了 Go 語言。具體 protoc 命令行工具如何使用,能夠自行搜索下,這裏再也不過多展開。服務器

定義好了 proto 文件,以及生成了相應的 package 代碼,下一步咱們就能夠編寫業務邏輯了。restful

Hello gRPC - 服務端業務代碼數據結構

import (
"google.golang.org/grpc"
pb "grpc-hw/pkg/helloworld"
)
// server is used to implement helloworld.GreeterServer.
type server struct {
pb.UnimplementedGreeterServer
}
// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
tempFile := "srv.wav"
err := exec.Command("flite", "-t", "Hello "+in.GetName(), "-o", tempFile).Run()
if err != nil {
return nil, fmt.Errorf("make audio failed: %v", err)
}
data, _ := ioutil.ReadFile(tempFile)
return &pb.HelloReply{Message: data}, nil
}
func main() {
lis, _ := net.Listen("tcp", port)
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
s.Serve(lis)
}

在服務端側,須要實現 SayHello 的方法來知足 GreeterServer 接口的要求。SayHello 方法的傳入參數,是在 proto 文件中定義的 HelloRequest,傳出參數,是在 proto 文件中定義的 HelloReply,以及一個 error。

業務邏輯也比較簡單,獲取 HelloRequest 中 Name 字段,而後經過命令行行工具轉換成對應的音頻,將 bytes 數組存在在 HelloReply 中返回。

Hello gRPC - 客戶端業務代碼

func main() {
flag.StringVar(&address, "addr", address, "server address")
flag.StringVar(&name, "name", "world", "name")
flag.Parse()
// Set up a connection to the server.
conn, _ := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
defer conn.Close()
c := pb.NewGreeterClient(conn)
// Contact the server and print out its response.
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
tempFile := "cli.wav"
ioutil.WriteFile(tempFile, r.Message, 0666)
exec.Command("afplay", tempFile).Run()
}

咱們再來看一下如何實現 Client。首先,創建一個 gRPC 的鏈接,並初始化 GreeterClient,而後直接調用下 GreeterClient 的 SayHello 的方法,將把返回結果另存爲一個文件,經過播放器播放就能夠了。

整體而言,整個使用過程很簡單,而且很是容易上手,讓開發能夠更加關注在客戶端、服務端業務的實現,不用操心傳輸層的事情。

gRPC 的使用總結

經過剛剛的小例子,咱們來總結一下 gRPC 的使用:

  • 定義好接口文檔
  • 工具生成服務端/客戶端代碼
  • 服務端補充業務代碼
  • 客戶端創建 gRPC 鏈接後,使用自動生成的代碼調用函數
  • 編譯、運行

以上 5 步就是 gRPC 的簡單使用方法了。

gRPC 與 Protobuf

接下來咱們來聊聊 gRPC 跟 Protobuf 之間的聯繫,固然在這以前咱們須要先知道 Protobuf 是什麼。

Protobuf

Protobuf 是一個語言無關、平臺無關的可擴展的結構化數據序列化方案。你們可能會以爲它跟 JSON 好像沒什麼區別,功能上看起來是同樣的,但像上文那樣去定義 SayHello 的操做,JSON 是作不到的,JSON 只能定義一個請求體或者一個返回體,無法定義一個方法,可是 Protobuf 是能夠的。

Protobuf 多用於協議通信、數據存儲和其餘更多用途。它是一個比較靈活的、高效的、自動化的結構化數據序列機制,可是更小,更快而且更簡單。一旦定義好數據如何構造, 就可使用特殊生成的代碼來輕易地讀寫結構化數據,無需關心用什麼語言來實現。你甚至能夠更新數據結構而不打破已部署的使用"舊有"格式編譯的程序。

上圖是 Protobuf 與 JSON 以及 JSON stream 三者之間的性能比較,能夠明顯的看到在解碼的時候 Protobuf 比其餘兩項快了不僅一星半點。

Protobuf  與 XML、JSON 的吞吐量比較

上圖中,咱們能夠看到 Protobuf 仍是有一些缺點的,好比瀏覽器的支持沒有其餘幾個支持的好。可是,在數據安全方面,因爲傳輸過程當中採用的是加密壓縮後的字節流,通常沒法直接查看,安全性很是好。以及在處理速度方面,由於編解碼效率很高使得總體吞吐量有了顯著提高。還有一點,定義方法,這個是其餘兩種序列化協議所作不到的。

gRPC 跟 Protobuf 的聯繫

雖然每次 gRPC 與 Protobuf 都是同時出現的,可是其實二者之間並無很深的聯繫。只是由於二者都是由 Google 開發的,和 gRPC 自己負載無關,在使用時也能夠選擇 JSON 等等,可是考慮到 Protobuf 有定義方法的優點,在微服務裏仍是很推薦使用的。

gRPC vs Restful API

上圖是 gRPC 與 Restful API 的對比,日常咱們可能更多使用 Restful API。但從圖上能夠看到,由於 gRPC 用的是 Protobuf,自己就比較小因此很快,不像 JSON 體積比較大、比較慢。另外,gRPC 是加載 HTTP/2 上面的,延遲比較低,也由於 HTTP/2 支持連接複用,這就可讓多個 stream 共用一個鏈接,從而進一步提高速度。對比 Restful 則使用的是 HTTP 1.1,延遲比較高,並且在通常狀況下,每次請求都須要建一下新的鏈接。

gRPC 是雙向的。什麼是雙向呢?好比咱們日常作 Restful,都是從客戶端到服務端,可是服務端沒辦法直接主動向客戶端發送信息,gRPC 則能夠。gRPC 也支持流,Restful只支持Request/Response 這樣的機制。gRPC是面向 API 的,沒有限制,也面向增刪改查。gRPC 能夠經過 Protobuf 直接生成代碼,而 Restful 須要依賴第三方。

另外 gRPC 支持 RPC 能夠調用服務器上的一些方法,而 Restful 是基於HTTP的語法,不少東西須要本身去定義。這方面相信你們都有感觸,好比 REST 定義post/put/delete/get時,由於每一個人都有本身的習慣,因此協做時須要溝通討論進行指定。可是 gRPC 就不須要了,它支持定義函數式辦法,不須要你們去考慮如何設計語法。

以上就是 gRPC 跟 Restful API 的對比。

引入 gRPC 須要考慮哪些問題?

那麼當咱們引入 gRPC 的時候須要考慮什麼呢?如下幾點確定是不可避免的考慮項:

  • 是否能夠知足當前需求
  • 性能如何
  • 鏈接異常斷開後,是否須要客戶端重試
  • TCP 鏈接是否能夠複用
  • 業務層改動是否足夠便利
  • 業務後期迭代是否會出現問題,如何避免

這個也是咱們引入一項新的東西時,每每須要考慮到的問題。

回顧與總結

從選擇 gRPC 到整個項目落地,以及如今上線後正常使用。整個過程當中,我對於項目的思考能夠包括了過去、如今和將來三個階段。

對我而言,過去就是要去看我選擇的這個東西,用的人多很少,完善程度怎麼樣了?而如今則是要結合項目,看看合不合適,能不能使用。固然不能思考到能使用就結束,咱們還須要考慮這個項目在將來的 3-5 年的發展,你引入它後在這個時間內需不須要大的變更。這個是很是重要的,雖然咱們如今常說敏捷開發,也常常會進行不少的調整,可是在相似 gRPC 這種底層基礎來講,是固定的。

以上就是我今天的所有分享內容,講的比較簡單,但願能帶給你們一些收穫。

推薦閱讀

秋天的第一份「乾貨」 I Referer 防盜鏈,爲何少了個字母 R?

「網頁內容沒法訪問」多是跨域錯誤!