grpc快速使用

bytemode · · 168 次點擊 · · 開始瀏覽    
使用grpc和protobuf實現rpc通信的例子 ## 創建工程 ``` mkdir grpc_test cd grpc_test go mod init //使用github的grpc替換gp的grpc go mod edit -replace=google.golang.org/grpc=github.com/grpc/grpc-go@latest go mod edit -replace=golang.org/x/net=github.com/golang/net@latest go mod tidy go mod vendor go build -mod=vendor ``` 科學上網可以不用上面的replace,直接設置如下環境: `export GOPROXY=https://goproxy.io 或者 set GOPROXY=https://goproxy.io` ## 工程準備 ``` grpc_test /proto/ /search.proto /client/ /client.go /server/ /server.go ``` ## 生成proto ## 編寫proto ``` syntax = "proto3"; package proto; service SearchService { rpc Search(SearchRequest) returns (SearchResponse) {} } message SearchRequest { string request = 1; } message SearchResponse { string response = 1; } ``` 定義中包含了服務接口的定義. ### protobuf環境 首先下載protoc放入path `https://github.com/protocolbuffers/protobuf/releases ` ``` go get -u github.com/golang/protobuf/protoc-gen-go //下載proto go插件 export PATH=$PATH:$GOPATH/bin //protoc-gen-go 添加到path ``` ### 生成 確保proto和protoc-gen-go可用,protoc參數中--go_out會自動加載protoc-gen-go protoc --go_out=plugins=grpc: 生成目錄 proto文件或者目錄 `protoc --go_out=plugins=grpc:. *.proto` 會生成search.pb.go ## 編寫server ``` package main import ( "context" "log" "net" pb "grpctest/proto" "google.golang.org/grpc" ) type SearchService struct{} func (s *SearchService) Search(ctx context.Context, r *pb.SearchRequest) (*pb.SearchResponse, error) { return &pb.SearchResponse{Response: r.GetRequest() + " Server"}, nil } const PORT = "9001" func main() { server := grpc.NewServer() //創建 gRPC Server對象 //將 SearchService(其包含需要被調用的服務端接口)注冊到gRPC Server 的內部注冊中心 //這樣可以在接受到請求時,通過內部的服務發現,發現該服務端接口并轉接進行邏輯處理 pb.RegisterSearchServiceServer(server, &SearchService{}) lis, err := net.Listen("tcp", ":"+PORT) //創建 Listen,監聽 TCP 端口 if err != nil { log.Fatalf("net.Listen err: %v", err) } //gRPC Server開始 lis.Accept,直到 Stop 或 GracefulStop server.Serve(lis) } ``` ## 編寫client ``` package main import ( "context" "log" "google.golang.org/grpc" pb "grpctest/proto" ) const PORT = "9001" func main() { //連接grpc server conn, err := grpc.Dial(":"+PORT, grpc.WithInsecure()) if err != nil { log.Fatalf("grpc.Dial err: %v", err) } defer conn.Close() //創建 SearchService 的客戶端對象 client := pb.NewSearchServiceClient(conn) //發送 RPC 請求,等待同步響應,得到回調后返回響應結果 resp, err := client.Search(context.Background(), &pb.SearchRequest{ Request: "gRPC", }) if err != nil { log.Fatalf("client.Search err: %v", err) } log.Printf("resp: %s", resp.GetResponse()) } ``` ## 數據流模式RPC 在上面的例子中展示的一元rpc也就是簡單rpc的模式。gPrc還有流模式的rpc. 分為服務端流rpc\客戶端流rpc\雙向流迷失rpc ### 服務端流RPC 在服務端流模式的RPC實現中,服務端得到客戶端請求后,處理結束返回一個數據應答流。在發送完所有的客戶端請求的應答數據后,服務端的狀態詳情和可選的跟蹤元數據發送給客戶端 #### 服務接口定義 通過stream修飾的方式表示該接口調用時,服務端會以數據流的形式將數據返回給客戶端 ``` //訂單服務service定義 service OrderService { rpc GetOrderInfos (OrderRequest) returns (stream OrderInfo) {}; //服務端流模式 } ``` #### 生成代碼變化 `protoc --go_out=plugins=grpc:. *.proto` 在自動生成的go代碼程序當中,每一個流模式對應的服務接口,都會自動生成對應的單獨的client和server程序,以及對應的結構體實現。 ##### 服務端生成代碼 流模式下,服務接口的服務端提供Send方法,將數據以流的形式進行發送 ``` 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) } ``` ##### 客戶端生成代碼 流模式下,服務接口的客戶端提供Recv()方法接收服務端發送的流數據 ``` 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 } ``` #### 服務端實現 因為是流模式開發,服務端將數據以流的形式進行發送,因此,該方法的第二個參數類型為OrderService_GetOrderInfosServer,該參數類型是一個接口,其中包含Send方法,允許發送流數據。Send方法的具體實現在編譯好的pb.go文件中,進一步調用grpc.SeverStream.SendMsg方法 服務端注冊模式和一元rpc是沒區別的 ``` //訂單服務實現 type OrderServiceImpl struct { } //獲取訂單信息s func (os *OrderServiceImpl) GetOrderInfos(request *message.OrderRequest, stream message.OrderService_GetOrderInfosServer) 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 } ``` #### 客戶端實現 服務端使用Send方法將數據以流的形式進行發送,客戶端可以使用Recv()方法接收流數據,因為數據流失源源不斷的,因此使用for無限循環實現數據流的讀取,當讀取到io.EOF時,表示流數據結束. ``` for { orderInfo, err := orderInfoClient.Recv() if err == io.EOF { fmt.Println("讀取結束") return } if err != nil { panic(err.Error()) } fmt.Println("讀取到的信息:", orderInfo) } ``` ### 客戶端流RPC 服務端以數據流的形式返回數據的形式。對應的,也存在客戶端以流的形式發送請求數據的形式。 #### 服務接口定義 與服務端同理,客戶端流模式的RPC服務聲明格式,就是使用stream修飾服務接口的接收參數 ``` //訂單服務service定義 service OrderService { rpc AddOrderList (stream OrderRequest) returns (OrderInfo) {}; //客戶端流模式 } ``` #### 生成代碼的差異 SendAndClose和Recv方法是客戶端流模式下的服務端對象所擁有的方法 Send和CloseAndRecv是客戶端流模式下的客戶端對象所擁有的方法。 ### 雙向流模式 上文已經講過了服務端流模式和客戶端流模式。如果將客戶端和服務端兩種流模式結合起來,就是第三種模式,雙向流模式。即客戶端發送數據的時候以流數據發送,服務端返回數據也以流的形式進行發送,因此稱之為雙向流模式。 #### 服務接口定義 ``` //訂單服務service定義 service OrderService { rpc GetOrderInfos (stream OrderRequest) returns (stream OrderInfo) {}; //雙向流模式 } ``` #### 生成代碼的差異 服務端和客戶端都實現了send 和 recv方法用來接收和發送流式的數據 ## TLS驗證和Token認證 gRPC中默認支持兩種授權方式,分別是:SSL/TLS認證方式、基于Token的認證方式 ### SSL/TLS認證方式 SL全稱是Secure Sockets Layer,又被稱之為安全套接字層,是一種標準安全協議,用于在通信過程中建立客戶端與服務器之間的加密鏈接。 TLS的全稱是Transport Layer Security,TLS是SSL的升級版。在使用的過程中,往往習慣于將SSL和TLS組合在一起寫作SSL/TLS。 簡而言之,SSL/TLS是一種用于網絡通信中加密的安全協議。 使用SSL/TLS協議對通信連接進行安全加密,是通過非對稱加密的方式來實現的。所謂非對稱加密方式又稱之為公鑰加密,密鑰對由公鑰和私鑰兩種密鑰組成。私鑰和公鑰成對存在,先生成私鑰,通過私鑰生成對應的公鑰。公鑰可以公開,私鑰進行妥善保存。 在加密過程中:客戶端想要向服務器發起鏈接,首先會先向服務端請求要加密的公鑰。獲取到公鑰后客戶端使用公鑰將信息進行加密,服務端接收到加密信息,使用私鑰對信息進行解密并進行其他后續處理,完成整個信道加密并實現數據傳輸的過程。 公鑰加密私鑰解密,非對稱加密算法. #### 生成證書 ``` openssl ecparam -genkey -name secp384r1 -out server.key openssl req -new -x509 -sha256 -key server.key -out server.pem -days 3650 ``` #### 開啟TLS認證的服務端和客戶端連接代碼 ``` //TLS認證 creds, err := credentials.NewServerTLSFromFile("./keys/server.pem","./keys/server.key") if err != nil { grpclog.Fatal("加載在證書文件失敗", err) } //實例化grpc server, 開啟TLS認證 server := grpc.NewServer(grpc.Creds(creds)) ``` ``` //TLS連接 creds, err := credentials.NewClientTLSFromFile("./keys/server.pem", "go-grpc-example") if err != nil { panic(err.Error()) } //1、Dail連接 conn, err := grpc.Dial("localhost:8092", grpc.WithTransportCredentials(creds)) if err != nil { panic(err.Error()) } ``` ### 基于Token認證方式 在web應用的開發過程中會使用另外一種認證方式進行身份驗證,那就是:Token認證。基于Token的身份驗證是無狀態,不需要將用戶信息服務存在服務器或者session中. 基于Token認證的身份驗證主要過程是:客戶端在發送請求前,首先向服務器發起請求,服務器返回一個生成的token給客戶端。客戶端將token保存下來,用于后續每次請求時,攜帶著token參數。服務端在進行處理請求之前,會首先對token進行驗證,只有token驗證成功了,才會處理并返回相關的數據。 #### 自定義Token ``` grpc.WithPerRPCCredentials(PerRPCCredentials) type PerRPCCredentials interface { //組織token信息 GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) //設置是否基于tls認證進行安全傳輸 RequireTransportSecurity() bool } ``` 自定義token只需要實現PerRPCCredentials接口就可以了。 #### 在客戶端進行連接時,我們將自定義的token認證信息作為參數進行傳入 ``` //token認證 type TokenAuthentication struct { AppKey string AppSecret string } //組織token信息 func (ta *TokenAuthentication) RequestMetaData(ctx context.Context, uri ...string) (map[string]string, error) { return map[string]string{ "appid": ta.AppKey, "appkey": ta.AppSecret, }, nil } //是否基于TLS認證進行安全傳輸 func (a *TokenAuthentication) RequireTransportSecurity() bool { return true } auth := TokenAuthentication{ AppKey: "hello", AppSecret: "20190812", } conn, err := grpc.Dial("localhost:8093", grpc.WithTransportCredentials(creds), grpc.WithPerRPCCredentials(&auth)) if err != nil { panic(err.Error()) } ``` #### 服務端token校驗 在服務端的調用方法中實現對token請求參數的判斷,可以通過metadata獲取token認證信息 ``` func (mm *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] } } ``` ## 攔截器的使用 在服務端的方法中,每個方法都要進行token的判斷。程序效率太低,可以優化一下處理邏輯,在調用服務端的具體方法之前,先進行攔截,并進行token驗證判斷,這種方式稱之為攔截器處理。除了此處的token驗證判斷處理以外,還可以進行日志處理等. ### Interceptor 在grpc中編程實現中,可以在NewSever時添加攔截器設置,grpc框架中可以通過UnaryInterceptor方法設置自定義的攔截器 ``` grpc.UnaryInterceptor(UnaryServerInterceptor) type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error) ``` ### 自定義攔截器 ``` 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進行處理 ### 注冊攔截器 ``` server:=grpc.NewServer(grpc.Creds(creds),grpc.UnaryInterceptor(TokenInterceptor)) ```

入群交流(和以上內容無關):Go中文網 QQ 交流群:729884609 或加微信入微信群:274768166 備注:入群;關注公眾號:Go語言中文網

168 次點擊  
加入收藏 微博
暫無回復
添加一條新回復 (您需要 登錄 后才能回復 沒有賬號 ?)
  • 請盡量讓自己的回復能夠對別人有幫助
  • 支持 Markdown 格式, **粗體**、~~刪除線~~、`單行代碼`
  • 支持 @ 本站用戶;支持表情(輸入 : 提示),見 Emoji cheat sheet
  • 圖片支持拖拽、截圖粘貼等方式上傳