gRPC入門—golang實現
1、RPC
1.1 什麼是RPC
RPC(Remote Procedure Call),即遠程過程調用,過程就是方法,簡單來說,它就是一種能夠像調用本地方法一樣調用遠程電腦進程中的方法的技術,在這種調用中,我們不需要了解任何網路通訊的細節(當然,就使用來說)
最終解決的問題:讓分散式或者微服務系統中不同服務之間的調用像本地調用一樣簡單
1.2 RPC和HTTP
調用遠程服務,HTTP 就可以完成的任務,為什麼還需要 RPC 呢?需要注意,這兩個並不是同一層次的概念,HTTP 是一種傳輸協議,RPC 應該是比 HTTP 更高層級的概念。完整的 RPC 實現包含有 傳輸協議 和 序列化協議,其中,傳輸協議既可以使用 HTTP,也可以使用 TCP 等,不同的選擇可以適應不同的場景
RPC 並不是一個嶄新的概念,它實際上就是遠程通訊的一個更高層級的封裝,不同傳輸協議和序列化協議的組合構成了不同的具體 RPC 實現,比如我們熟知的 RESTful,就是 HTTP + JSON + 一些其他細節構成
1.3 RPC技術的演化
早期有一些很流行的 RPC 實現,比如 CORBA(通用對象請求代理體系結構),Java RMI(遠程方法調用),它們都用來構建和鏈接服務或應用程式,但是,大多數傳統 RPC 實現極其複雜,因為它們構建在 TCP 之上,並且還有大量的規範限制
鑒於以上 RPC 實現的局限性,SOAP(簡單對象訪問協議)應運而生,SOAP 是 SOA(面向服務的架構)中的標準通訊技術,能夠基於任意底層通訊協議進行通訊,最常用的是 HTTP,序列化協議使用的是 XML
REST(描述性狀態轉移)是 ROA(面向資源的架構)的基礎,在這種架構中,將應用程式建模為各種資源的集合,客戶端可以變更這些資源的狀態(增刪改查)。REST 的通用實現是 HTTP + JSON,通過 HTTP 將應用程式建模為能夠通過唯一標識符表示的資源集合,狀態變更操作會採用 HTTP 動作(GET,POST,PUT,DELETE等)。實際上,REST 架構風格已經成為了各種服務間通訊中非常流行的方法,但是,隨著微服務大行其道以及網路交互的激增,REST 已經無法滿足現代化的需求了,其主要原因是以下三個主要的局限性:
- 基於文本的消息協議效率太低。REST 服務建立在基於文本的傳輸協議 HTTP1.x 上,使用人類可讀的文本格式如 JSON,但是,很多時候我們並不需要這種可讀性,如果能夠直接發送映射服務和客戶端業務邏輯的二進位內容,將大大提高效率
- 缺乏強類型介面。開發 REST 服務時,應用程式之間並不需要共享服務定義和類型定義,我們要麼通過網路查看文本格式,要麼通過 API 文檔,構建這種分散的應用程式時,會遇到很多不兼容、運行時錯誤和互操作等問題
- REST架構風格難以實施。REST 架構風格有很多 「好的實踐」,遵循這些實踐能構建出真正好用的 REST 服務,但是,它們並沒有作為協議的一部分進行強制要求,事實上,大多數 REST 服務不過是通過網路公開的 HTTP 服務,並沒有很好地遵循基礎的架構風格
由於 REST 的局限性,出現了許多新興的 RPC 技術,較為流行的有 gRPC、Thrift、GraphQL等
2、gRPC
2.1 gRPC簡介
gRPC 是一個現代化的開源 RPC 框架,一開始由 google 開發,是一款語言中立、平台中立、的 RPC 系統,與許多 RPC 系統類似,gRPC 也是基於以下理念:定義一個 服務,指定能夠被遠程調用的 方法(包含參數和返回類型)。在服務端實現這個介面,並運行一個gRPC 伺服器來處理客戶端調用,在客戶端擁有一個 stub 連接服務端上的方法
2.2 gRPC的優勢
gRPC 的優勢是它被越來越多人採用的關鍵所在,主要有以下幾個方面:
- 提供高效的進程間通訊。使用一個基於 protocol buffers 的二進位協議而不是文本格式與客戶端通訊,同時在 HTTP2 上實現,擁有更好的性能
- 具有簡單且定義良好的服務介面。契約優先,必須首先定義服務介面,然後才能去處理細節,簡單一致,可擴展
- 強類型。服務契約清晰地定義了應用程式間通訊所使用的類型,分散式應用程式的開發更加穩定
- 支援多語言。基於 protocol buffers 的服務定義是語言中立的,可以選擇任意一種語言具體實現
- 支援雙工流。與傳統的 REST 相比,gRPC 能夠同時構建傳統的請求-響應風格的消息以及客戶端流和服務端流
- 具備內置的商業化特性。如認證、加密、彈性時間、元數據交換、壓縮、負載均衡以及服務發現等
- 與雲原生生態進行了集成。gRPC 是 CNCF(雲原生計算基金會)的一部分,大多數現代框架和技術都對 gRPC 提供了原生支援
- 業界成熟。通過在Google進行的大量實戰測試,gRPC 已經發展成熟,被許多公司採用
2.3 gRPC的缺點
gRPC 也存在一定劣勢,選擇它用來構建應用程式時,需要注意以下三點:
- gRPC 不太適合面向外部的服務。gRPC 具有契約驅動、強類型等特點,這會限制向外部暴露服務的靈活性,對客戶端有諸多限制,所以更適合用在內部伺服器之間通訊
- 避免巨大的服務定義變更。如果出現巨大的服務定義變更,通常需要重新生成客戶端程式碼和服務端程式碼,會讓整個開發生命周期變得複雜,需要小心引入破壞性的變更
- 生態系統相對較小。與傳統 REST 等協議相比,gRPC 仍然處於起步階段,瀏覽器和移動應用程式對 gRPC 的支援才剛剛起步
3、一個簡單gRPC服務的golang實現
3.1 環境準備
- 下載 protoc 編譯器:protobuf,選擇合適的平台,解壓後將可執行文件加入環境變數,此編譯器用來編譯服務定義文件 .proto 生成指定語言的目標程式碼,這些程式碼用來實現 gRPC 服務以及客戶端 stub
- 安裝 grpc-go 插件用來生成 go 目標程式碼
go install google.golang.org/protobuf/cmd/protoc-gen-go
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc
- 創建程式碼目錄 product-info,實現一個簡單的查看商品資訊和添加商品的 rpc 服務,在其中新建三個文件夾 proto、server、client 分別存放服務定義文件和生成的目標程式碼、服務端程式實現、客戶端程式實現,然後執行
go mod init product-info
初始化模組。當然這裡只是示常式序,實際場景中服務程式碼和客戶端程式碼一般都不在同一個機器上,更不可能在同一個模組下了,最終目錄結構如下:
3.2 服務定義
開發 gRPC 應用程式時,要首先定義服務介面,然後生成服務端骨架和客戶端 stub,客戶端通過調用其中定義的方法來訪問遠程伺服器上的方法,服務定義都以 protocol buffers 的形式記錄,也就是 gRPC 所使用的服務定義語言
- 在 proto 目錄下新建服務定義文件 product-info.proto
// 版本
syntax = "proto3";
// proto文件所屬包名
package proto;
// 聲明生成的go文件所屬的包,路徑末尾為包名,相對路徑是相對於編譯生成目標程式碼時的工作路徑
option go_package = "./proto";
// 包含兩個遠程方法的 rpc 服務,遠程方法只能有一個參數和一個返回值
service ProductInfo {
rpc addProduct(Product) returns (ProductID);
rpc getProduct(ProductID) returns (Product);
}
// 自定義消息類型,用這種方法傳遞多個參數,必須使用唯一數字標識每個欄位
message Product {
string id = 1;
string name = 2;
string description = 3;
float price = 4;
}
message ProductID {
string value = 1;
}
- 編譯服務定義文件生成目標源程式碼,這一步之後在 proto 文件下生成了以下兩個文件:
- product-info.pb.go,包含用於填充、序列化、檢索請求和響應消息類型的所有 protocol buffers 程式碼
- product-info_grpc.pb.go,包含服務端需要繼承實現和客戶端進行調用的介面定義
# go_out 和 go-grpc-out 目錄是相對於服務定義文件中 go_package 指定的目錄
protoc proto/product-info.proto --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative
3.3 服務端實現
編譯生成服務端骨架的時候,已經得到了建立 gRPC 連接、相關消息類型和介面的基礎程式碼,接下來就是實現得到的介面,在 server 文件夾中新建服務端主程式 main.go:
package main
import (
"context"
"log"
"net"
pb "product-info/proto"
"github.com/gofrs/uuid"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
const (
port = ":50051"
)
// 對伺服器的抽象,用來實現服務方法
type server struct {
pb.UnimplementedProductInfoServer
}
// 存放商品,模擬業務邏輯
var productMap map[string]*pb.Product
// 實現 AddProduct 方法
func (s *server) AddProduct(ctx context.Context, in *pb.Product) (*pb.ProductID, error) {
out, err := uuid.NewV4()
if err != nil {
return nil, status.Errorf(codes.Internal, "Error while generating Product ID", err)
}
in.Id = out.String()
if productMap == nil {
productMap = make(map[string]*pb.Product)
}
productMap[in.Id] = in
log.Printf("Product %v : %v - Added.", in.Id, in.Name)
return &pb.ProductID{Value: in.Id}, nil
}
// 實現 GetProduct 方法
func (s *server) GetProduct(ctx context.Context, in *pb.ProductID) (*pb.Product, error) {
product, exists := productMap[in.Value]
if exists && product != nil {
log.Printf("Product %v : %v - Retrieved.", product.Id, product.Name)
return product, nil
}
return nil, status.Errorf(codes.NotFound, "Product does not exist.", in.Value)
}
func main() {
// 創建一個 tcp 監聽器
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
// 創建一個 gRPC 伺服器實例
s := grpc.NewServer()
// 將服務註冊到 gRPC 伺服器上
pb.RegisterProductInfoServer(s, &server{})
// 綁定 gRPC 伺服器到指定 tcp
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
3.4 客戶端實現
接下來創建客戶端程式來與伺服器對話,之前編譯服務定義文件生成的目標源程式碼已經包含了訪問細節的實現,我們只需要創建客戶端實例就可以直接調用遠程方法。在 client 文件夾中創建客戶端主程式 main.go:
package main
import (
"context"
"log"
"time"
pb "product-info/proto"
"google.golang.org/grpc"
)
const (
// 服務端地址
address = "localhost:50051"
)
func main() {
// 創建 gRPC 連接
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
// 創建客戶端 stub,利用它調用遠程方法
c := pb.NewProductInfoClient(conn)
name := "XiaoMi 11"
description := "XiaoMi 11 with MIUI 12.5"
price := float32(3999.00)
// 創建 Context 傳遞給遠程調用,包含一些元數據,如終端用戶標識、授權令牌以及請求截止時間等
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// 調用遠程方法
r, err := c.AddProduct(ctx, &pb.Product{Name: name, Description: description, Price: price})
if err != nil {
log.Fatalf("Could not add product: %v", err)
}
log.Printf("Product ID: %s added successfully", r.Value)
product, err := c.GetProduct(ctx, &pb.ProductID{Value: r.Value})
if err != nil {
log.Fatalf("Could not get product: %v", err)
}
log.Printf("Product: %v", product.String())
}
3.5 構建和運行
最終工作空間如下:
分別構建運行服務端和客戶端程式,go build 或者直接 go run
- 啟動服務端:
go run ./server/main.go
- 啟動客戶端:
go run ./client/main.go
- 服務端 log:
到這裡就成功構建了一個簡單的 gRPC 服務,並在客戶端調用成功。當然這只是一個簡單的入門程式,更多的細節還需要更加深入的學習,另外,gRPC 是支援多語言的,這裡採用 golang 實現了服務端和客戶端程式,其他的語言構建 gRPC 服務也都遵循類似的步驟,且客戶端和服務端程式碼無關,也可用不同的語言實現,其他語言的用法可見 官方文檔