我的gRPC之旅。本節介紹微服務架構、強弱類型介面、Rest、gRPC定義,proto編譯方式,並用golang編寫一個簡單的gRPC托管服務和客戶端。在調試中進步,感受gRPC的魅力。 ...
微服務架構
微服務是一種開發軟體的架構和組織方法,其中軟體由通過明確定義的API 進行通信的小型獨立服務組成。 這些服務由各個小型獨立團隊負責。 微服務架構使應用程式更易於擴展和更快地開發,從而加速創新並縮短新功能的上市時間。
將軟體應用程式構建為一組獨立、自治(獨立開發、部署和擴展)、松耦合、面向業務能力(強調能力,而不是完成任務)的服務。
為什麼微服務軟體系統需要藉助進程間(服務間,應用程式間)通信技術?
傳統軟體系統被進一步拆分為一組細粒度,自治和麵向業務能力的實體,也就是微服務。
強、弱類型介面
服務API介面有強、弱類型之分。
強類型介面
傳統的RPC服務(定製二進位協議 ,對消息進行編碼和解碼),採用TCP傳輸消息。RPC服務通常有嚴格的契約,開發服務前需要使用IDL(Interface description language)定義契約,最終通過契約自動生成強類型的伺服器端、客戶端的介面。服務調用直接使用強類型的客戶端。(GRPC、Thrift)
- 優點:不需要手動的編碼和解碼、介面規範、自動代碼生成、編譯器自動類型檢查。
- 缺點:服務端和客戶端強耦合、任何一方升級改動可能會造成另一方break。自動代碼生成需要工具支持,開發這些工具的成本比較高。強類型介面開發測試不友好、瀏覽器、Postman這些工具無法直接訪問這些強類型介面。
弱類型介面
Restful服務通常採用JSON作為傳輸消息,使用HTTP作為傳輸協議,沒有嚴格契約的概念,使用普通的HTTP Client即可調用。調用方需要對JSON消息進行手動的編碼和解碼工作。(Springboot)
- 優點:服務端和客戶端非強耦合、開發測試友好。
- 缺點:調用方手動編碼解碼,沒有自動代碼生成、沒有編譯期介面類型檢查、相對不規範、容易出現運行期錯誤。
Rest
描述性狀態轉移架構,是面向資源架構的基礎。將分散式應用程式建模為資源集合,訪問這些資源的客戶端可以變更這些資源的狀態。有三大局限性。
-
基於文本的低效消息協議
-
應用程式之間缺乏強類型介面
-
架構風格難以強制實施
gRPC
gRPC 是一項進程間通信技術,用來連接、調用、操作和調試分散式異構應用程式。
定義服務介面
開發gRPC應用需要先定義服務介面,使用的語言叫做 介面定義語言
- 確定消費者消費服務的方式
- 消費者遠程調用的方法和傳入的參數和消息格式
優勢
- 提供高效的進程間通信。不使用json、xml,基於在HTTP/2之上的protocol buffers的二進位協議
- 簡單且定義良好的服務介面和模式
- 屬於強類型介面。構建跨團隊、技術類型的雲原生應用程式,對於其所產生的的大多數運行時錯誤和互操作錯誤們可以通過靜態類型來剋服
- 支持多語言
- 支持雙工流。同時構建傳統請求-響應風格的消息以及客戶端流和服務端流
- 商業化特性內置支持
- 與雲原生生態系統進行了集成
劣勢
- 不太適合面向外部服務
- 巨大的服務定義變更是複雜的開發流程
- 生態系統相對較小
編寫gRPC服務
創建 client、service 目錄,分別用指令生成 go.mod 文件
go mod init productinfo/client
go mod init productinfo/service
目錄結構
PS C:\Users\小能喵喵喵\Desktop\Go\gRPC\chapter2\productinfo> tree /f
├─client
│ │ client.go
│ │ go.mod
│ │ go.sum
│ │
│ ├─bin
│ │ client.exe
│ │
│ └─ecommerce
│ product_info.pb.go
│ product_info.proto
│ product_info_grpc.pb.go
│
└─service
│ go.mod
│ go.sum
│ server.go
│ service.go
│
├─bin
│ server.exe
│
└─ecommerce
product_info.pb.go
product_info.proto
product_info_grpc.pb.go
product_info.proto 介面定義
syntax = "proto3";
package ecommerce;
option go_package = ".";
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;
}
編譯工具
安裝:Release Protocol Buffers v21.6 · protocolbuffers/protobuf (github.com)
教程:Go Generated Code | Protocol Buffers | Google Developers
註: $GOPATH/bin
要添加到系統環境變數里
protoc-gen-go-grpc: program not found or is not executable 解決方案
go get -u google.golang.org/protobuf/cmd/protoc-gen-go
go install google.golang.org/protobuf/cmd/protoc-gen-go
go get -u google.golang.org/grpc/cmd/protoc-gen-go-grpc
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc
Please specify either:
• a "go_package" option in the .proto source file, or
• a "M" argument on the command line.
解決方案 在syntax=”proto3″;下一行增加option go_package配置項。
編譯方法
protoc [opt...] file.proto
/* 例如 */
protoc --proto_path=src --go_out=out --go_opt=paths=source_relative foo.proto
- 當前版本編譯時,之前的方法
protoc --go_out=plugins=grpc:. *.proto
不再使用,轉而用protoc --go_out=. --go-grpc_out=. ./hello.proto
代替。 go_out=.
指定生成的pb.go
文件所在目錄(如果沒有該目錄,需要手動提前創建),.
代表當前protoc執行目錄,結合.proto
文件中的option go_package
,其最終的生成文件目錄為go_out指定目錄/go_package指定目錄
,go-grpc_out
針對_grpc.pb.go
文件,同理。--go_opt=paths=source_relative
,其含義代表生成的.pb.go
文件路徑不依賴於.proto
文件中的option go_package
配置項,直接在go_out
指定的目錄下生成.pb.go
文件(.pb.go
文件的package名還是由option go_package
決定)。--go-grpc_opt=paths=source_relative
,針對_grpc.pb.go
文件,同理。
PS C:\Users\小能喵喵喵\Desktop\Go\gRPC\chapter2\productinfo\service\ecommerce>
protoc --go_out=. --go-grpc_out=. --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative ./product_info.proto
Product和ProductID結構體定義在product_info.pb.go文件中,通過product_info.proto自動生成。
service.go 業務邏輯代碼
package main
import (
"context"
pb "productinfo/service/ecommerce" // ^ 導入 protobuf編譯器生成代碼的包
"github.com/google/uuid"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
// ^ 實現 service/ecommerce 服務
type service struct {
pb.UnimplementedProductInfoServer
productMap map[string]*pb.Product
}
// ^ 實現方法邏輯、添加商品
// ^ ctx 對象包含一些元數據,比如終端用戶授權令牌標識和請求的截止時間
func (s *service) AddProduct(ctx context.Context, in *pb.Product) (*pb.ProductID, error) {
out, err := uuid.NewUUID() // ^ 通用唯一標示符
if err != nil {
return nil, status.Errorf(codes.Internal, "生成產品編碼時出錯", err)
}
in.Id = out.String()
if s.productMap == nil {
s.productMap = make(map[string]*pb.Product)
}
s.productMap[in.Id] = in
return &pb.ProductID{Value: in.Id}, status.New(codes.OK, "").Err()
}
func (s *service) GetProduct(ctx context.Context, in *pb.ProductID) (*pb.Product, error) {
value, exists := s.productMap[in.Value]
if exists {
return value, status.New(codes.OK, "").Err()
}
return nil, status.Errorf(codes.NotFound, "商品條目不存在", in.Value)
}
server.go 托管服務的gRPC伺服器
前向相容
前向相容一般指向上相容。 向上相容(Upward Compatible)又稱向前相容(Forward Compatible),在某一平臺的較低版本環境中編寫的程式可以在較高版本的環境中運行。
無法傳遞給RegisterXXXService方法
新版protoc-gen-go不支持grpc服務生成,需要通過protoc-gen-go-grpc生成grpc服務介面,但是生成的Server端介面中會出現一個mustEmbedUnimplemented***方法,為瞭解決前向相容問題(現在的相容未來的),如果不解決,就無法傳遞給RegisterXXXService方法。
- 在grpc server實現結構體中匿名嵌入Unimplemented***Server結構體
- 使用protoc生成server代碼時命令行加上關閉選項,protoc --go-grpc_out=require_unimplemented_servers=false
package main
import (
"fmt"
"log"
"net"
pb "productinfo/service/ecommerce"
"google.golang.org/grpc"
)
const (
ip = "127.0.0.1"
port = "23333"
)
func main() {
lis, err := net.Listen("tcp", fmt.Sprintf("%v:%v", ip, port))
if err != nil {
log.Fatalf("無法監聽埠 %v %v", port, err)
}
s := grpc.NewServer()
pb.RegisterProductInfoServer(s, &service{})
log.Println("gRPC伺服器開始監聽", port)
if err := s.Serve(lis); err != nil {
log.Fatalf("提供服務失敗: %v", err)
}
}
2022/09/18 17:17:30 gRPC伺服器開始監聽 23333
client.go 客戶端代碼
重新編譯proto
創建一個client目錄,並重新之前mod init、編譯proto的操作。
PS C:\Users\小能喵喵喵\Desktop\Go\gRPC\chapter2\productinfo\client\ecommerce>
protoc --go_out=. --go-grpc_out=. --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative ./product_info.proto
grpc.WithInsecure已棄用
grpc.WithInsecure is Deprecated: use WithTransportCredentials and insecure.NewCredentials() instead. Will be supported throughout 1.x.
The function insecure.NewCredentials
returns an implementation of credentials.TransportCredentials
.
grpc.Dial(":9950", grpc.WithTransportCredentials(insecure.NewCredentials()))
代碼
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "productinfo/client/ecommerce"
)
const (
address = "localhost:23333"
)
func main() {
conn, err := grpc.Dial(address, grpc.WithTransportCredentials(insecure.NewCredentials())) // ^ 不安全地創建端到端連接
if err != nil {
log.Fatalf("did not connect: %v", err)
}
c := pb.NewProductInfoClient(conn) // ^ 傳遞連接並創建存根實例,包含所有遠程調用方法
name := "小米 10 Pro"
description := "雷軍說:Are you ok?"
price := float32(2000.0)
ctx, cancel := context.WithTimeout(context.Background(), time.Second) // ^ 用於傳遞元數據:用戶標識,授權令牌,請求截止時間
defer cancel()
r, err := c.AddProduct(ctx, &pb.Product{
Name: name, Price: price, Description: description,
})
if err != nil {
log.Fatalf("無法添加商品 %v", err)
}
log.Printf("添加商品成功 %v", r.Value)
/* -------------------------------------------------------------------------- */
product, err := c.GetProduct(ctx, &pb.ProductID{Value: r.Value})
if err != nil {
log.Fatalf("獲取不到商品 %v", err)
}
log.Println("Product: ", product.String())
}
構建運行
分別進入service,client文件夾執行如下命令。構建二進位文件並運行(可以交叉編譯運行在其他操作系統上)。
PS C:\Users\小能喵喵喵\Desktop\Go\gRPC\chapter2\productinfo\service>
go build -o bin/server.exe
PS C:\Users\小能喵喵喵\Desktop\Go\gRPC\chapter2\productinfo\client>
go build -o bin/client.exe
參考資料
-
《Go語言併發之道》Katherine CoxBuday
-
《Go語言核心編程》李文塔
-
《Go語言高級編程》柴樹彬、曹春輝
-
《Grpc 與雲原生應用開發》卡山·因德拉西里、丹尼什·庫魯普