gRPC介紹
gRPC是由Google公司開源的一款高性能的遠程過程調用(RPC)框架,可以在任何環境下運行。該框架提供了負載均衡,跟蹤,智能監控,身份驗證等功能,可以實現系統間的高效連接。另外,在分布式系統中,gRPC框架也有有廣泛應用,實現移動社會,瀏覽器等和服務器的連接。
gRPC開源庫支持諸如:C++,C#,Dart,Go,Java,Node,Objective-C,PHP,Python,Ruby,WebJS等多種語言,開發者可以自行在gRPC的github主頁庫選擇查看對應語言的實現。
gRPC調用執行過程
因為gRPC支持多種語言的實現,因此gRPC支持客戶端與服務器在多種語言環境中部署運行和互相調用。多語言環境交互示例如下圖所示:
gRPC中默認采用的數據格式化方式是protocol buffers。
grpc-go使用
定義服務
我們想要實現的是通過gRPC框架進行遠程服務調用,首先第一步應該是要有服務。利用之前所掌握的內容,gRPC框架支持對服務的定義和生成。
gRPC框架默認使用protocol buffers作為接口定義語言,用於描述網絡傳輸消息結構。除此之外,還可以使用protobuf定義服務接口。
OrderMessage.proto
syntax = "proto3"; package message; // 訂單請求參數 message OrderRequest { string orderId = 1; int64 timeStamp = 2; } // 訂單信息 message OrderInfo { string OrderId = 1; string OrderName = 2; string OrderStatus = 3; } // 訂單服務service定義 service OrderService { rpc GetOrderInfo(OrderRequest) returns (OrderInfo); }
我們通過proto文件定義了數據結構的同時,還定義了要實現的服務接口,GetOrderInfo即是具體服務接口的定義,在GetOrderInfo接口定義中,OrderRequest表示是請求傳遞的參數,OrderInfo表示處理結果返回數據參數
編譯.proto文件
如果定義的.proto文件,如本案例中所示,定義中包含了服務接口的定義,而我們想要使用gRPC框架實現RPC調用。開發者可以采用protocol-gen-go庫提供的插件編譯功能,生成兼容gRPC框架的golang語言代碼。只需要在基本編譯命令的基礎上,指定插件的參數,告知protoc編譯器即可。具體的編譯生成兼容gRPC框架的服務代碼的命令如下:
protoc --go_out=plugins=grpc:. *.proto
gRPC實現RPC編程
服務接口實現
在.proto定義好服務接口並生成對應的go語言文件后,需要對服務接口做具體的實現。定義服務接口具體由OrderServiceImpl進行實現,並實現GetOrderInfo詳細內容,服務實現邏輯與前文所述內容相同。不同點是服務接口參數的變化。詳細代碼實現如下:
package main import ( "context" "errors" "fmt" "gRPCProject/message" "google.golang.org/grpc" "net" "time" ) type OrderServiceImpl struct { } // 具體的方法實現 func (this *OrderServiceImpl) GetOrderInfo(ctx context.Context, request *message.OrderRequest) (*message.OrderInfo, error) { orderMap := map[string]message.OrderInfo{ "201907300001": message.OrderInfo{OrderId: "201907300001", OrderName: "衣服", OrderStatus: "已付款"}, "201907310001": message.OrderInfo{OrderId: "201907310001", OrderName: "零食", OrderStatus: "已付款"}, "201907310002": message.OrderInfo{OrderId: "201907310002", OrderName: "食品", OrderStatus: "未付款"}, } var response *message.OrderInfo current := time.Now().Unix() if (request.TimeStamp > current) { *response = message.OrderInfo{OrderId: "0", OrderName: "", OrderStatus: "訂單信息異常"} } else { result := orderMap[request.OrderId] if result.OrderId != "" { fmt.Println(result) return &result, nil } else { return nil, errors.New("server error") } } return response, nil } func main() { server := grpc.NewServer() message.RegisterOrderServiceServer(server, new(OrderServiceImpl)) lis, err := net.Listen("tcp", ":8090") if err != nil { panic(err.Error()) } server.Serve(lis) }
gRPC實現客戶端
實現完服務端以后,實現客戶端程序。和服務端程序關系對應,調用gRPC框架的方法獲取相應的客戶端程序,並實現服務的調用,具體編程實現如下:
package main import ( "context" "fmt" "gRPCProject/message" "google.golang.org/grpc" "time" ) func main() { conn, err := grpc.Dial("localhost:8090", grpc.WithInsecure()) if err != nil { panic(err.Error()) } defer conn.Close() orderServiceClient := message.NewOrderServiceClient(conn) orderRequest := &message.OrderRequest{OrderId: "201907300001", TimeStamp: time.Now().Unix()} orderInfo, err := orderServiceClient.GetOrderInfo(context.Background(), orderRequest) if err == nil { fmt.Println(orderInfo.GetOrderId()) fmt.Println(orderInfo.GetOrderName()) fmt.Println(orderInfo.GetOrderStatus()) } }
運行程序
201907300001
衣服
已付款
gRPC調用
一 服務端流 RPC
在服務端流RPC實現中,服務端得到客戶端請求后,處理結束返回一個數據應答流。在發送完所有的客戶端請求的應答數據后,服務端的狀態詳情和可靠的跟蹤元數據發送給客戶端:
1.1 服務接口定義
... //訂單服務service定義 service OrderService { rpc GetOrderInfos (OrderRequest) returns (stream OrderInfo) {}; //服務端流模式 }
我們可以看到與之前簡單模式下的數據作為服務接口的參數和返回值不同的是,此處服務接口的返回值使用了stream進行修飾。通過stream修飾的方式表示該接口調用時,服務端會以數據流的形式將數據返回給客戶羰。
1.2 編譯.proto文件,生成pb.go文件
protoc --go_out=plugins=grpc:. message.proto
1.3 自動生成文件的變化
與數據結構體發送攜帶數據實現不同的時,流模式下的數據發送和接收使用新的功能方法完成。在自動生成的go代碼程序當中,每一個流模式對應的服務接口,都會自動生成對應的單獨的client和server程序,以及對應的結構體實現。具體編程如下圖所示:
1.3.1 服務端自動生成
type OrderService_GetOrderInfosServer interface { Send(*OrderInfo) error grpc.ServerStream } type orderServiceGetOrderInfosServer struct { grpc.ServerStream } func (x *orderServiceGetOrderInfosServer) Send(m *OrderInfo) error { return x.ServerStream.SendMsg(m) }
流模式下,服務接口的服務端提供Send方法,將數據以流的形式進行發送
1.3.2 客戶端自動生成
type OrderService_GetOrderInfosClient interface { Recv() (*OrderInfo, error) grpc.ClientStream } type orderServiceGetOrderInfosClient struct { grpc.ClientStream } func (x *orderServiceGetOrderInfosClient) Recv() (*OrderInfo, error) { m := new(OrderInfo) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil }
流模式下,服務接口的客戶端提供Recv()方法接收服務端發送的流數據。
1.4 服務編碼實現
package main import ( "fmt" "gRPCProject/demo2/message" "google.golang.org/grpc" "net" "time" ) type OrderServiceImpl struct { } func (this *OrderServiceImpl) GetOrderInfo(request *message.OrderRequest, stream message.OrderService_GetOrderInfoServer) error { fmt.Println(" 服務端流RPC械 ") orderMap := map[string]message.OrderInfo{ "201907300001": message.OrderInfo{OrderId: "201907300001", OrderName: "衣服", OrderStatus: "已付款"}, "201907310001": message.OrderInfo{OrderId: "201907310001", OrderName: "零食", OrderStatus: "已付款"}, "201907310002": message.OrderInfo{OrderId: "201907310002", OrderName: "食品", OrderStatus: "未付款"}, } for id, info := range orderMap { if (time.Now().Unix() >= request.TimeStamp) { fmt.Println("訂單序列號ID:", id) fmt.Println("訂單詳情:", info) //通過流模式發送給客戶端 stream.Send(&info) } } return nil } func main() { server := grpc.NewServer() message.RegisterOrderServiceServer(server, new(OrderServiceImpl)) lis, err := net.Listen("tcp", ":8090") if err != nil { panic(err.Error()) } server.Serve(lis) }
GetOrderInfos方法就是服務接口的具體實現,因為是流模式開發,服務端將數據以流的形式進行發送,因此,該方法的第二個參數類型為OrderService_GetOrderInfosServer,該 參數類型是一個接口,其中包含Send方法,允許發送流數據,Send方法的具體實現現在編譯好的pb.go文件中,進一步調用grpc.SeverStream.SendMsg方法。
1.5 客戶端數據接收
服務端使用Send方法將數據以流的形式進行發送,客戶端可以使用Recv()方法接收流數據,因為數據流是源源不斷的,因此使用for無限循環實現數據流的讀取,當讀取到io.EOF時,表示流數據結束。客戶端數據讀取實現如下:
package main import ( "context" "fmt" "gRPCProject/demo2/message" "google.golang.org/grpc" "io" "time" ) func main() { conn, err := grpc.Dial("localhost:8090", grpc.WithInsecure()) if err != nil { panic(err.Error()) } defer conn.Close() orderClient := message.NewOrderServiceClient(conn) orderRequest := &message.OrderRequest{OrderId: "201907300001", TimeStamp: time.Now().Unix()} orderInfoClient, err := orderClient.GetOrderInfo(context.Background(), orderRequest) if err == nil { for { orderInfo, err := orderInfoClient.Recv() if err == io.EOF { fmt.Println("讀取結束") return } if err != nil { panic(err.Error()) } fmt.Println("讀取到的信息:", orderInfo) } } }
二 客戶端流模式
2.1 服務接口的定義
與服務端同理,客戶端流模式的RPC服務聲明格式,就是使用stream修飾服務接口的接收參數,具體如下所示:
... //訂單服務service定義 service OrderService { rpc AddOrderList (stream OrderRequest) returns (OrderInfo) {}; //客戶端流模式 }
2.2 編譯.proto文件
使用編譯命令編譯.proto文件。客戶端流模式中也會自動生成服務接口的接口。
2.2.1 自動生成的服務流接口實現
type OrderService_AddOrderListServer interface { SendAndClose(*OrderInfo) error Recv() (*OrderRequest, error) grpc.ServerStream } type orderServiceAddOrderListServer struct { grpc.ServerStream } func (x *orderServiceAddOrderListServer) SendAndClose(m *OrderInfo) error { return x.ServerStream.SendMsg(m) } func (x *orderServiceAddOrderListServer) Recv() (*OrderRequest, error) { m := new(OrderRequest) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil }
SendAndClose和Recv方法是客戶端流模式下的服務端對象所擁有的方法。
2.2.2 自動生成的客戶端流接口實現
type OrderService_AddOrderListClient interface { Send(*OrderRequest) error CloseAndRecv() (*OrderInfo, error) grpc.ClientStream } type orderServiceAddOrderListClient struct { grpc.ClientStream } func (x *orderServiceAddOrderListClient) Send(m *OrderRequest) error { return x.ClientStream.SendMsg(m) } func (x *orderServiceAddOrderListClient) CloseAndRecv() (*OrderInfo, error) { if err := x.ClientStream.CloseSend(); err != nil { return nil, err } m := new(OrderInfo) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil }
Send和CloseAndRecv是客戶端流模式下的客戶端對象所擁有的方法。
2.3 服務的實現
package main import ( "WXProjectDemo/clientStream/message" "fmt" "google.golang.org/grpc" "io" "net" ) type OrderServiceImpl struct { } func (this *OrderServiceImpl) AddOrderList(stream message.OrderService_AddOrderListServer) error { fmt.Println("客戶端RPC模式") for { //從流中讀取數據信息 orderRequest, err := stream.Recv() if err == io.EOF { fmt.Println("讀取數據結束") result := message.OrderInfo{OrderStatus: "讀取數據結束"} return stream.SendAndClose(&result) } if err != nil { fmt.Println(err.Error()) return err } fmt.Println(orderRequest) } } func main() { server := grpc.NewServer() message.RegisterOrderServiceServer(server, new(OrderServiceImpl)) lis, err := net.Listen("tcp", ":8090") if err != nil { panic(err.Error()) } server.Serve(lis) }
2.5 客戶端實現
package main import ( "WXProjectDemo/clientStream/message" "context" "fmt" "google.golang.org/grpc" "io" ) func main() { conn, err := grpc.Dial("localhost:8090", grpc.WithInsecure()) if err != nil { panic(err.Error()) } defer conn.Close() orderMap := map[string]message.OrderInfo{ "201907300001": message.OrderInfo{OrderId: "201907300001", OrderName: "衣服", OrderStatus: "已付款"}, "201907310001": message.OrderInfo{OrderId: "201907310001", OrderName: "零食", OrderStatus: "已付款"}, "201907310002": message.OrderInfo{OrderId: "201907310002", OrderName: "食品", OrderStatus: "未付款"}, } orderServiceClient := message.NewOrderServiceClient(conn) addOrderListClient, err := orderServiceClient.AddOrderList(context.Background()) if err != nil { panic(err.Error()) } for _, info := range orderMap { err = addOrderListClient.Send(&info) if err != nil { panic(err.Error()) } } for { orderInfo, err := addOrderListClient.CloseAndRecv() if err == io.EOF { fmt.Println(" 讀取數據結束了 ") return } if err != nil { fmt.Println(err.Error()) } fmt.Println(orderInfo.GetOrderStatus()) } }
三 雙向流模式
3.1 雙向流服務的定義
//訂單服務service定義 service OrderService { rpc GetOrderInfos (stream OrderRequest) returns (stream OrderInfo) {}; //雙向流模式 }
3.2 編譯.proto文件
3.2.1 服務端接口實現
type OrderService_GetOrderInfosServer interface { Send(*OrderInfo) error Recv() (*OrderRequest, error) grpc.ServerStream } type orderServiceGetOrderInfosServer struct { grpc.ServerStream } func (x *orderServiceGetOrderInfosServer) Send(m *OrderInfo) error { return x.ServerStream.SendMsg(m) } func (x *orderServiceGetOrderInfosServer) Recv() (*OrderRequest, error) { m := new(OrderRequest) if err := x.ServerStream.RecvMsg(m); err != nil { return nil, err } return m, nil }
3.2.2 客戶端接口實現
type OrderService_GetOrderInfosClient interface { Send(*OrderRequest) error Recv() (*OrderInfo, error) grpc.ClientStream } type orderServiceGetOrderInfosClient struct { grpc.ClientStream } func (x *orderServiceGetOrderInfosClient) Send(m *OrderRequest) error { return x.ClientStream.SendMsg(m) } func (x *orderServiceGetOrderInfosClient) Recv() (*OrderInfo, error) { m := new(OrderInfo) if err := x.ClientStream.RecvMsg(m); err != nil { return nil, err } return m, nil }
3.3 服務實現
package main import ( "WXProjectDemo/bothStream/message" "fmt" "google.golang.org/grpc" "io" "net" ) type OrderServiceImpl struct { } func (this *OrderServiceImpl) GetOrderInfos(stream message.OrderService_GetOrderInfosServer) error { for { orderRequest, err := stream.Recv() if err == io.EOF { fmt.Println(" 數據讀取結束 ") return err } if err != nil { panic(err.Error()) return err } fmt.Println(orderRequest.GetOrderId()) orderMap := map[string]message.OrderInfo{ "201907300001": message.OrderInfo{OrderId: "201907300001", OrderName: "衣服", OrderStatus: "已付款"}, "201907310001": message.OrderInfo{OrderId: "201907310001", OrderName: "零食", OrderStatus: "已付款"}, "201907310002": message.OrderInfo{OrderId: "201907310002", OrderName: "食品", OrderStatus: "未付款"}, } result := orderMap[orderRequest.GetOrderId()] err = stream.Send(&result) if err == io.EOF { fmt.Println(err) return err } if err != nil { fmt.Println(err.Error()) return err } } return nil } func main() { server := grpc.NewServer() message.RegisterOrderServiceServer(server,new(OrderServiceImpl)) lis, err := net.Listen("tcp", ":8090") if err != nil { panic(err.Error()) } server.Serve(lis) }
3.4客戶端實現
package main import ( "WXProjectDemo/bothStream/message" "context" "fmt" "google.golang.org/grpc" "io" ) func main() { conn, err := grpc.Dial("localhost:8090", grpc.WithInsecure()) if err != nil { panic(err.Error()) } defer conn.Close() orderServiceClient := message.NewOrderServiceClient(conn) fmt.Println("客戶端請求RPC調用:雙向流模式") orderIDs := []string{"201907300001", "201907310001", "201907310002"} orderInfoClient, err := orderServiceClient.GetOrderInfos(context.Background()) for _, orderID := range orderIDs { orderRequest := message.OrderRequest{OrderId: orderID} err := orderInfoClient.Send(&orderRequest) if err != nil { panic(err.Error()) } } orderInfoClient.CloseSend() for { orderInfo, err := orderInfoClient.Recv() if err == io.EOF { fmt.Println("讀取結束") return } if err != nil { return } fmt.Println("讀取到的信息是: ", orderInfo) } }
TLS驗證和Token認證
一 授權認證
gRPC中默認支持兩種授權方式,分別是:SSL/TLS認證方式、基於Token的認證方式。
1.1 SSL/TLS認證方式
SSL全稱是Secure Sockets Layer,又被稱之為安全套接字層,是一種標准安全協議,用於在通信過程中建立客戶端與服務器之間的加密鏈接。
TLS的全稱是Transport Layer Security,TLS是SSL的升級版。在使用的過程中,往往習慣於將SSL和TLS組合在一起寫作SSL/TLS。
簡而言之,SSL/TLS是一種用於網絡通信中加密的安全協議。
1.1.1 SSL/TLS工作原理
使用SSL/TLS協議對通信連接進行安全加密,是通過非對稱加密的方式來實現的。所謂非對稱加密方式又稱之為公鑰加密,密鑰對由公鑰和私鑰兩種密鑰組成。私鑰和公鑰成對存在,先生成私鑰,通過私鑰生成對應的公鑰。公鑰可以公開,私鑰進行妥善保存。
在加密過程中:客戶端想要向服務器發起鏈接,首先會先向服務端請求要加密的公鑰。獲取到公鑰后客戶端使用公鑰將信息進行加密,服務端接收到加密信息,使用私鑰對信息進行解密並進行其他后續處理,完成整個信道加密並實現數據傳輸的過程。
1.1.2 制作證書
可以自己在本機計算機上安裝openssl,並生成相應的證書。
openssl ecparam -genkey -name secp384r1 -out server.key
openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650
1.1.3 編程實現服務端
message.proto
syntax = "proto3"; package message; message RequestArgs { int32 args1 = 1; int32 args2 = 2; } message Response { int32 code = 1; string message = 2; } //服務 service MathService { //服務 rpc AddMethod (RequestArgs) returns (Response) { }; }
1.1.3 編程實現服務端
package main import ( "context" "fmt" "gRPCProject/opensslDemo/message" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/grpclog" "net" "os" ) type MathManager struct { } func (this *MathManager) AddMethod(ctx context.Context, request *message.RequestArgs) (response *message.Response, err error) { fmt.Println(" 服務端Add方法 ") result := request.Args1 + request.Args2 fmt.Println("計算的結果是: ", result) response = new(message.Response) response.Code = 1; response.Message = "執行成功" return response, nil } func main() { // TLS認證 dir,_ := os.Getwd() fmt.Println("當前路徑:",dir) creds, err := credentials.NewServerTLSFromFile("opensslDemo/keys/server.pem", "opensslDemo/keys/server.key") if err != nil { grpclog.Fatal(" 加載證書文件失敗", err) } // 實例化grpc server,開啟TLS認證 server := grpc.NewServer(grpc.Creds(creds)) message.RegisterMathServiceServer(server, new(MathManager)) lis, err := net.Listen("tcp", ":8090") if err != nil { panic(err.Error()) } server.Serve(lis) }
1.1.3 編程實現客戶端
package main import ( "context" "fmt" "gRPCProject/opensslDemo/message" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/grpclog" ) func main() { // TLS連接 creds, err := credentials.NewClientTLSFromFile("opensslDemo/keys/server.pem", "go-grpc-example") if err != nil { panic(err.Error()) } // 1.Dail連接 conn, err := grpc.Dial("localhost:8090", grpc.WithTransportCredentials(creds)) if err != nil { panic(err.Error()) } defer conn.Close() serviceClient := message.NewMathServiceClient(conn) addArgs := message.RequestArgs{Args1: 3, Args2: 5} response, err := serviceClient.AddMethod(context.Background(), &addArgs) if err != nil { grpclog.Fatal(err.Error()) } fmt.Println(response.GetCode(), response.GetMessage()) }
1.2 基於Token認證方式
1.2.1 Token認證介紹
在web應用的開發過程中,我們往往還會使用另外一種認證方式進行身份驗證,那就是:Token認證。基於Token的身份驗證是無狀態,不需要將用戶信息服務存在服務器或者session中。
1.2.2 Token認證過程
基於Token認證的身份驗證主要過程是:客戶端在發送請求前,首先向服務器發起請求,服務器返回一個生成的token給客戶端。客戶端將token保存下來,用於后續每次請求時,攜帶着token參數。服務端在進行處理請求之前,會首先對token進行驗證,只有token驗證成功了,才會處理並返回相關的數據。
1.2.3 gRPC的自定義Token認證
在gRPC中,允許開發者自定義自己的認證規則,通過
grpc.WithPerRPCCredentials()
設置自定義的認證規則。WithPerRPCCredentials方法接收一個PerRPCCredentials類型的參數,進一步查看可以知道PerRPCCredentials是一個接口,定義如下:
type PerRPCCredentials interface {
GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error)
RequireTransportSecurity() bool
}
因此,開發者可以實現以上接口,來定義自己的token信息。
1.2.4 服務端
package main import ( "context" "fmt" "gRPCProject/tokenDemo/message" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/grpclog" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "net" ) type MathManager struct { } func (this *MathManager) AddMethod(ctx context.Context, request *message.RequestArgs) (response *message.Response, err error) { // 通過metadata md, exist := metadata.FromIncomingContext(ctx) if !exist { return nil, status.Errorf(codes.Unauthenticated, "無Token認證信息") } var appKey string var appSecret string if key, ok := md["appid"]; ok { appKey = key[0] } if secret, ok := md["appkey"]; ok { appSecret = secret[0] } if appKey != "hello" || appSecret != "20200430" { return nil, status.Error(codes.Unauthenticated, "Token不合法") } fmt.Println(" 服務端 Add方法 ") result := request.Args1 + request.Args2 fmt.Println(" 計算結果是:", result) response = new(message.Response) response.Code = 1; response.Message = "執行成功" return response, nil } func main() { creds, err := credentials.NewServerTLSFromFile("tokenDemo/keys/server.pem", "tokenDemo/keys/server.key") if err != nil { grpclog.Fatal("加載證書文件失敗", err) } server := grpc.NewServer(grpc.Creds(creds)) message.RegisterMathServiceServer(server, new(MathManager)) lis, err := net.Listen("tcp", ":8090") if err != nil { panic(err.Error()) } server.Serve(lis) }
1.2.4 客戶端
package main import ( "context" "fmt" "gRPCProject/tokenDemo/message" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/grpclog" ) //token認證 type TokenAuthentication struct { AppKey string AppSecret string } //組織token認證的metadata信息 func (this *TokenAuthentication) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { return map[string]string{ "appid": this.AppKey, "appkey": this.AppSecret, }, nil } //是否基於TLS認證進行安全傳輸 func (this *TokenAuthentication) RequireTransportSecurity() bool { return true } func main() { creds, err := credentials.NewClientTLSFromFile("tokenDemo/keys/server.pem", "go-grpc-example") if err != nil { panic(err.Error()) } auth := TokenAuthentication{ AppKey: "hello", AppSecret: "20200430", } //1、Dail連接 conn, err := grpc.Dial("localhost:8090", grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(&auth)) if err != nil { panic(err.Error()) } defer conn.Close() serviceClient := message.NewMathServiceClient(conn) addArgs := message.RequestArgs{Args1: 89, Args2: 566} response, err := serviceClient.AddMethod(context.Background(), &addArgs) if err != nil { grpclog.Fatal(err.Error()) } fmt.Println(response.GetCode(), response.GetMessage()) }
攔截器的使用
在服務端的方法中,每個方法都要進行token的判斷。程序效率太低,可以優化一下處理邏輯,在調用服務端的具體方法之前,先進行攔截,並進行token驗證判斷,這種方式稱之為攔截器處理。
除了此處的token驗證判斷處理以外,還可以進行日志處理等。
Interceptor
使用攔截器,首先需要注冊。
在grpc中編程實現中,可以在NewSever時添加攔截器設置,grpc框架中可以通過UnaryInterceptor方法設置自定義的攔截器,並返回ServerOption。具體代碼如下:
grpc.UnaryInterceptor()
UnaryInterceptor()接收一個UnaryServerInterceptor類型,繼續查看源碼定義,可以發現UnaryServerInterceptor是一個func,定義如下:
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
通過以上代碼,如果開發者需要注冊自定義攔截器,需要自定義實現UnaryServerInterceptor的定義。
自定義UnaryServerInterceptor
接下來就自定義實現func,符合UnaryServerInterceptor的標准,在該func的定義中實現對token的驗證邏輯。自定義實現func如下:
func TokenInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { //通過metadata md, exist := metadata.FromIncomingContext(ctx) if !exist { return nil, status.Errorf(codes.Unauthenticated, "無Token認證信息") } var appKey string var appSecret string if key, ok := md["appid"]; ok { appKey = key[0] } if secret, ok := md["appkey"]; ok { appSecret = secret[0] } if appKey != "hello" || appSecret != "20190812" { return nil, status.Errorf(codes.Unauthenticated, "Token 不合法") } //通過token驗證,繼續處理請求 return handler(ctx, req) }
在自定義的TokenInterceptor方法定義中,和之前在服務的方法調用的驗證邏輯一致,從metadata中取出請求頭中攜帶的token認證信息,並進行驗證是否正確。如果token驗證通過,則繼續處理請求后續邏輯,后續繼續處理可以由grpc.UnaryHandler進行處理。grpc.UnaryHandler同樣是一個方法,其具體的實現就是開發者自定義實現的服務方法。grpc.UnaryHandler接口定義源碼定義如下:
type UnaryHandler func(ctx context.Context, req interface{}) (interface{}, error)
攔截器注冊
在服務端調用grpc.NewServer時進行攔截器的注冊。詳細如下:
server := grpc.NewServer(grpc.Creds(creds), grpc.UnaryInterceptor(TokenInterceptor))
server.go
package main import ( "WXProjectDemo/InterceptorDemo/message" "context" "fmt" "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/credentials" "google.golang.org/grpc/grpclog" "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" "net" ) type MathManager struct { } func (this *MathManager) AddMethod(ctx context.Context, request *message.RequestArgs) (response *message.Response, err error) { fmt.Println("服務端Add方法") result := request.Args1 + request.Args2 fmt.Println(" 計算結果是: ", result) response = new(message.Response) response.Code = 1 response.Message = "執行成功" return response, nil } func main() { // TLS認證 creds, err := credentials.NewServerTLSFromFile("InterceptorDemo/keys/server.pem", "InterceptorDemo/keys/server.key") if err != nil { grpclog.Fatal("加載證書文件失敗", err) } // 實例化grpc server,開啟TLS認證 server := grpc.NewServer(grpc.Creds(creds), grpc.UnaryInterceptor(TokenInterceptor)) message.RegisterMathServiceServer(server, new(MathManager)) lis, err := net.Listen("tcp", ":8090") if err != nil { panic(err.Error()) } server.Serve(lis) } // 自定義攔截器實現 func TokenInterceptor(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { // 通過metadata md, exist := metadata.FromIncomingContext(ctx) if !exist { return nil, status.Errorf(codes.Unauthenticated, "無Token認證信息") } var appKey string var appSecret string if key, ok := md["appid"]; ok { appKey = key[0] } if secret, ok := md["appkey"]; ok { appSecret = secret[0] } if appKey != "hello" || appSecret != "20200502" { return nil, status.Errorf(codes.Unauthenticated, "Token 不合法") } // 通過token驗證,繼續處理請求 return handler(ctx, req) }
client.go
package main import ( "WXProjectDemo/InterceptorDemo/message" "context" "fmt" "google.golang.org/grpc" "google.golang.org/grpc/credentials" "google.golang.org/grpc/grpclog" ) // token認證 type TokenAuthentication struct { AppKey string AppSecret string } // 組織token認證的metadata信息 func (this *TokenAuthentication) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) { return map[string]string{ "appid": this.AppKey, "appkey": this.AppSecret, }, nil } // 是否基於TLS認證進行安全傳輸 func (this *TokenAuthentication) RequireTransportSecurity() bool { return true } func main() { // TLS連接 creds, err := credentials.NewClientTLSFromFile("InterceptorDemo/keys/server.pem", "go-grpc-example") if err != nil { panic(err.Error()) } auth := TokenAuthentication{ AppKey: "hello", AppSecret: "20200502", } // 1.Dail連接 conn, err := grpc.Dial("localhost:8090", grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(&auth)) if err != nil { panic(err.Error()) } defer conn.Close() serviceClent := message.NewMathServiceClient(conn) addArgs := message.RequestArgs{Args1: 3, Args2: 97} response, err := serviceClent.AddMethod(context.Background(), &addArgs) if err != nil { grpclog.Fatal(err.Error()) } fmt.Println(response.GetCode(), response.GetMessage()) }
項目運行
依次運行server.go程序和client.go程序,可以得到程序運行的正確結果。修改token攜帶值,可以驗證token非法情況的攔截器效果。