在 gRPC(1):入門及簡單使用(go) 中,我們實現了一個簡單的 gRPC 應用程式,其中雙方通訊是簡單的請求—響應模式,沒發出一個請求都會得到一個響應,然而,藉助 gRPC 可以實現不同的通訊模式,這裡介紹四種 gRPC 應用程式的基礎通訊模式:一元RPC、服務端流RPC、客戶端流RPC、雙向流RPC
1、一元RPC
一元 RPC 也被稱為簡單 RPC, 其實就是 gRPC(1):入門及簡單使用(go) 中實現的請求—響應模式,每呼叫一次得到一個結果,這裡再以一個簡單的訂單管理程式做說明,實現兩個服務:addOrder 用於新增訂單;getOrder 用於根據 id 獲取訂單:
- 服務定義
syntax = "proto3";
package proto;
option go_package = "./proto";
service OrderManagement {
rpc addOrder(Order) returns (StringValue);
rpc getOrder(StringValue) returns (Order);
}
message Order {
string id = 1;
repeated string items = 2; // repeated 表示列表
string description = 3;
float price = 4;
string destination = 5;
}
message StringValue {
string value = 1;
}
- 服務端實現
package main
import (
"context"
"fmt"
"log"
"net"
"strings"
pb "order/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.UnimplementedOrderManagementServer
}
// 模擬儲存
var orderMap = make(map[string]*pb.Order)
func (s *server) AddOrder(ctx context.Context, order *pb.Order) (*pb.StringValue, error) {
id, err := uuid.NewV4()
if err != nil {
return nil, status.Errorf(codes.Internal, "Error while generating Product ID", err)
}
order.Id = id.String()
orderMap[order.Id] = order
log.Printf("Order %v : %v - Added.", order.Id, order.Description)
return &pb.StringValue{Value: order.Id}, nil
}
func (s *server) GetOrder(ctx context.Context, orderID *pb.StringValue) (*pb.Order, error) {
order, exists := orderMap[orderID.Value]
if exists && order != nil {
log.Printf("Order %v : %v - Retrieved.", order.Id, order.Description)
return order, nil
}
return nil, status.Errorf(codes.NotFound, "Order does not exist.", orderID.Value)
}
func main() {
lis, err := net.Listen("tcp", port)
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterOrderManagementServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
- 客戶端實現
package main
import (
"context"
"io"
"log"
"time"
pb "order/proto"
"google.golang.org/grpc"
)
const (
address = "localhost:50051"
)
func main() {
conn, err := grpc.Dial(address, grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewOrderManagementClient(conn)
orderID, err := c.AddOrder(context.Background(),
&pb.Order{
Items: []string{"XiaoMI 11"},
Description: "XiaoMI 11",
Price: 3999,
Destination: "suzhou",
})
if err != nil {
log.Fatalf("could not add order: %v", err)
}
log.Printf("Added order: %v", orderID.Value)
}
2、服務端流RPC
與一元 RPC 不同的是,流模式下響應或者請求都可以是一個序列,這個序列也被稱為”流“,服務端流 RPC 下,客戶端發出一個請求,但不會立即得到一個響應,而是在服務端與客戶端之間建立一個單向的流,服務端可以隨時向流中寫入多個響應訊息,最後主動關閉流,而客戶端需要監聽這個流,不斷獲取響應直到流關閉
下面以一個簡單的關鍵詞搜尋功能為例,客戶端傳送關鍵字,服務端進行匹配,每找到一個就寫進流中,在之前的基礎上新增程式碼:
- 服務定義
service OrderManagement {
...
// stream 將返回引數指定為訂單流
rpc searchOrders(StringValue) returns (stream Order);
}
- 服務端實現
func (s *server) SearchOrders(searchQuery *pb.StringValue, stream pb.OrderManagement_SearchOrdersServer) error {
for key, order := range orderMap {
for _, item := range order.Items {
if strings.Contains(item, searchQuery.Value) {
err := stream.Send(&order)
if err != nil {
return fmt.Errorf("error sending message to stream: %v", err)
}
log.Printf("order found: " + key)
break
}
}
}
return nil
}
- 客戶端實現
...
// 獲得建立的流物件
stream, err := c.SearchOrders(context.Background(), &pb.StringValue{Value: "XiaoMI"})
if err != nil {
log.Fatalf("search error: %v", err)
}
for {
// 迴圈讀取
order, err := stream.Recv()
if err == io.EOF {
log.Print("EOF")
break
}
if err != nil {
log.Fatal("error: ", err)
}
log.Print(order)
}
3、客戶端流RPC
客戶端流,和服務端流一樣的道理,只不過流的方向變為從客戶端到服務端,可以傳送多條響應,服務端只會響應一次,但何時響應取決於服務端的邏輯,以更新訂單序列為例,客戶端可以傳送一系列訂單,服務端可以選擇在任意時候停止讀取併傳送響應:
- 服務定義
service OrderManagement {
...
rpc updateOrders(stream Order) returns (StringValue);
}
- 服務端實現
func (s *server) UpdateOrders(stream pb.OrderManagement_UpdateOrdersServer) error {
for {
order, err := stream.Recv()
if err == io.EOF {
return stream.SendAndClose(&pb.StringValue{Value: "finished"})
}
if err != nil {
return err
}
orderMap[order.Id] = order
log.Print("OrderID " + order.Id + " updated")
}
}
- 客戶端實現
// 取得流
updateStream, err := c.UpdateOrders(context.Background())
if err != nil {
log.Fatalf("update err: %v", err)
}
// 傳送 Order1
if err = updateStream.Send(&pb.Order{
Id: "1",
Items: []string{"Huawei P50"},
Description: "Huawei P50",
Price: 5999,
Destination: "suzhou",
}); err != nil {
log.Fatalf("send error: %v", err)
}
// 傳送 Order2
if err = updateStream.Send(&pb.Order{
Id: "2",
Items: []string{"iphone 12"},
Description: "iphone 12",
Price: 8999,
Destination: "suzhou",
}); err != nil {
log.Fatalf("send error: %v", err)
}
...
// 關閉流,結束髮送
updateRes, err := updateStream.CloseAndRecv()
if err != nil {
log.Fatalf("update stream close error: %v", err)
}
log.Printf("update res: %v", updateRes)
4、雙向流RPC
雙向流,顧名思義,由客戶端發起呼叫後,將建立起雙向的流,在這之後,通訊將完全基於雙方的應用邏輯,流的操作完全獨立,客戶端和服務端可以按照任意順序進行讀取和寫入,以一個訂單篩選過程為例,客戶端傳送一串訂單 ID 序列,服務端進行檢查,每遇到一個有效的 ID 就寫入流中響應:
- 服務定義
service OrderManagement {
...
rpc processOrders(stream StringValue) returns (stream StringValue);
}
- 服務端實現
func (s *server) ProcessOrders(stream pb.OrderManagement_ProcessOrdersServer) error {
for {
orderId, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
order, exists := orderMap[orderId.Value]
if exists && order != nil {
stream.Send(&pb.StringValue{Value: order.Id})
}
}
}
- 客戶端實現
...
// 取得雙向流
processStream, err := c.ProcessOrders(context.Background())
// 同步channel,防止主程式提前退出
waitc := make(chan struct{})
// 雙向流是完全非同步的,開一個協程用於讀取響應
go func() {
for {
orderId, err := processStream.Recv()
if err == io.EOF {
close(waitc)
return
}
if err != nil {
log.Fatalf("recv error: %v", err)
}
log.Print("recv " + orderId.Value)
}
}()
// 請求
if err = processStream.Send(&pb.StringValue{Value: "1"}); err != nil {
log.Fatalf("1 send error: %v", err)
}
if err = processStream.Send(&pb.StringValue{Value: "2"}); err != nil {
log.Fatalf("2 send error: %v", err)
}
if err = processStream.Send(&pb.StringValue{Value: "3"}); err != nil {
log.Fatalf("3 send error: %v", err)
}
if err = processStream.CloseSend(); err != nil {
log.Fatal(err)
}
// 等待讀取結束
<-waitc
這就是 gRPC 中主要的四種通訊模式,基於它們可以實現各種 gRPC 場景下的互動,至於選擇哪種,還需根據具體的場景考慮