gRPC進階

BigSun丶發表於2024-03-19

目錄
  • 一、grpc metadata機制
    • 1.1 proto
    • 1.2 生成go檔案
    • 1.3 服務端
    • 1.4 客戶端
  • 二、grpc 攔截器interceptor
    • 2.1 服務端攔截器grpc.UnaryInterceptor(interceptor)
    • 2.2 客戶端攔截器
    • 2.3 開源攔截器
  • 三、透過metadata+攔截器實現認證
    • 3.1 自定義
      • (1)服務端
      • (2)客戶端
      • (3)proto
    • 3.2 WithPerRPCCredentials
      • (1)服務端(程式碼不變)
      • (2)客戶端(使用內建的)
  • 四、驗證器
    • 4.1 linux/mac安裝
    • 4.2 win安裝
    • 4.3 程式碼
      • (1)proto--hello.proto
      • (2)proto--validate.proto
      • (3)執行命令
      • (4)服務端
      • (5)客戶端
  • 五、grpc 狀態碼
  • 六、grpc 錯誤
    • 6.1 服務端
    • 6.2 客戶端
  • 七、grpc超時機制
    • 7.1 客戶端使用ctx
    • 7.2 服務端睡5s

一、grpc metadata機制

  • gRPC讓我們可以像本地呼叫一樣實現遠端呼叫,對於每一次的RPC呼叫中,都可能會有一些有用的資料,而這些資料就可以透過metadata來傳遞。metadata是以key-value的形式儲存資料的,其中key是string型別,而value是[]string,即一個字串陣列型別。

  • metadata使得client和server能夠為對方提供關於本次呼叫的一些資訊,就像一次http請求的RequestHeader和ResponseHeader一樣。

  • http中header的生命週週期是一次http請求,那麼metadata的生命週期就是一次RPC呼叫

// ****************1建立metadata****************
//MD 型別實際上是map,key是string,value是string型別的slice。
type MD map[string][]string
//建立的時候可以像建立普通的map型別一樣使用new關鍵字進行建立:

//第一種方式
md := metadata.New(map[string]string{"key1": "val1", "key2": "val2"})
//第二種方式 key不區分大小寫,會被統一轉成小寫。
md := metadata.Pairs(
    "key1", "val1",
    "key1", "val1-2", // "key1" will have map value []string{"val1", "val1-2"}
    "key2", "val2",
)


// ****************2傳送metadata*****************
md := metadata.Pairs("key", "val")
// 新建一個有 metadata 的 context
ctx := metadata.NewOutgoingContext(context.Background(), md)
// 單向 RPC
response, err := client.SomeRPC(ctx, someRequest)


// ****************3接收metadata*****************

func (s *server) SomeRPC(ctx context.Context, in *pb.SomeRequest) (*pb.SomeResponse, err) {
    md, ok := metadata.FromIncomingContext(ctx)
    // do something with metadata
}

1.1 proto

syntax = "proto3";
option go_package = ".;proto";


service Hello{
  rpc Hello(HelloRequest) returns(HelloResponse);
}

message HelloRequest {
  string name = 1;
}

message HelloResponse {
  string reply = 1;
}

1.2 生成go檔案

protoc --go_out=. ./hello.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false  ./hello.proto

1.3 服務端

package main

import (
	"context"
	"fmt"
	"go_test_learn/meta_proto/proto"
	"google.golang.org/grpc"
	"google.golang.org/grpc/metadata"
	"net"
)

type HelloServer struct {
}

func (s *HelloServer) Hello(context context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
	// 取出meta
	md, ok := metadata.FromIncomingContext(context)
	if  ok {
		fmt.Println("獲取meta失敗")
		// 迴圈列印出來
		for key,value :=range md{
			fmt.Println(key,value)
		}
		// 只取出password來
		fmt.Println(md["password"][0])
	}


	fmt.Println(request.Name)
	return &proto.HelloResponse{
		Reply: "收到客戶端的資料:"+ request.Name,
	}, nil
}

func main() {
	g := grpc.NewServer()
	s := HelloServer{}
	proto.RegisterHelloServer(g, &s)
	lis, error := net.Listen("tcp", "0.0.0.0:50052")
	if error != nil {
		panic("啟動服務異常")
	}
	g.Serve(lis)

}

1.4 客戶端

package main

import (
	"context"
	"fmt"
	"go_test_learn/meta_proto/proto"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/metadata"
)

func main() {

	conn, err := grpc.Dial("127.0.0.1:50052", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		panic("連線服務異常")
	}
	defer conn.Close()
	client := proto.NewHelloClient(conn)
	request := proto.HelloRequest{Name: "lqz",}

	// 1 方式一:建立md物件
	//md := metadata.Pairs("name", "lqz","password","123")
	// 1 方式二:建立md物件
	md := metadata.New(map[string]string{"name": "lqznb", "password": "456"})

	// 2 新建一個有 metadata 的 context
	ctx := metadata.NewOutgoingContext(context.Background(), md)
	//3 傳送
	res, err := client.Hello(ctx, &request)
	if err != nil {
		panic("呼叫方法異常")
	}

	fmt.Println(res.Reply)
}

二、grpc 攔截器interceptor

  • 在 gRPC 呼叫過程中,我們可以攔截 RPC 的執行,在 RPC 服務執行前或執行後執行一些自定義邏輯,這在某些場景下很有用,例如身份驗證、日誌等,我們可以在 RPC 服務執行前檢查呼叫方的身份資訊,若未透過驗證,則拒絕執行,也可以在執行前後記錄下詳細的請求響應資訊到日誌。這種攔截機制與 Gin 中的中介軟體技術類似,在 gRPC 中被稱為 攔截器,它是 gRPC 核心擴充套件機制之一

2.1 服務端攔截器grpc.UnaryInterceptor(interceptor)

  • interceptor是自定義的攔截器函式,追蹤函式的引數可知,interceptor是一個:
type UnaryServerInterceptor func(ctx context.Context, req interface{}, info *UnaryServerInfo, handler UnaryHandler) (resp interface{}, err error)
  • 引數含義:
ctx context.Context:請求上下文
req interface{}:RPC 方法的請求引數
info *UnaryServerInfo:RPC 方法的所有資訊
handler UnaryHandler:RPC 方法真正執行的邏輯
  • 案例
//攔截器
interceptor :=  func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error){
		fmt.Println("接收到了一個新的請求")
		ctime:=time.Now()
		res,err := handler(ctx, req)
		fmt.Println("請求已完成")
		fmt.Println("耗時為:",time.Since(ctime))
		return res, err
}
opt := grpc.UnaryInterceptor(interceptor)
g := grpc.NewServer(opt)
//...

2.2 客戶端攔截器

  • interceptor是自定義的攔截器函式,追蹤函式的引數可知,interceptor是一個:
type UnaryClientInterceptor func(ctx context.Context, method string, req, reply interface{}, cc *ClientConn, invoker UnaryInvoker, opts ...CallOption) error
  • 案例
	// 建立客戶端攔截器
interceptor := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error{
		start := time.Now()
		fmt.Println("客戶端攔截器")
		err := invoker(ctx, method, req, reply, cc, opts...)
		fmt.Printf("耗時:%s\n", time.Since(start))
		return err
	}
opt := grpc.WithUnaryInterceptor(interceptor)
conn, err := grpc.Dial("127.0.0.1:50052", opt,grpc.WithTransportCredentials(insecure.NewCredentials()))

2.3 開源攔截器

  • https://github.com/grpc-ecosystem/go-grpc-middleware

image-20220521221658299

  • 案例(服務端)
	// 使用第三方攔截器,使用了grpc_auth和grpc_recovery
	myAuthFunction := func(ctx context.Context) (context.Context, error) {
		fmt.Println("走了認證")
		return ctx, nil
	}
	opt := grpc.UnaryInterceptor(grpc_middleware.ChainUnaryServer(
		grpc_auth.UnaryServerInterceptor(myAuthFunction),
		grpc_recovery.UnaryServerInterceptor(),
	))
	g := grpc.NewServer(opt)
	// 使用攔截器結束

三、透過metadata+攔截器實現認證

3.1 自定義

(1)服務端

package main

import (
   "context"
   "fmt"
   "go_test_learn/interpret_proto/proto"
   "google.golang.org/grpc"
   "google.golang.org/grpc/codes"
   "google.golang.org/grpc/metadata"
   "google.golang.org/grpc/status"
   "net"
)

type HelloServer struct {
}

func (s *HelloServer) Hello(context context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
   fmt.Println(request.Name)
   return &proto.HelloResponse{
      Reply: "收到客戶端的資料:" + request.Name,
   }, nil
}

func main() {
   //服務端攔截器
   interceptor :=  func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error){
      fmt.Println("進行使用者名稱密碼認證")
      // 取出MD
      md, ok := metadata.FromIncomingContext(ctx)
      if  !ok { // 沒有取出meta,返回錯誤-->這個錯誤的rpc的錯誤,狀態碼也是rpc的狀態碼
         return resp,status.Error(codes.Unauthenticated,"沒有攜帶認證資訊")
      }
      // 攜帶meta,取出name和password
      // 取出name來
      var (
         name string
         password string
      )
      if names,ok:=md["name"];ok{
         name=names[0]
      }
      // 取出password來
      if passwords,ok:=md["password"];ok{
         password=passwords[0]
      }
      fmt.Println(name,password)
      if name=="lqz" && password=="123"{
         res,err := handler(ctx, req)
         return res, err
      }
      return resp, status.Error(codes.Unauthenticated,"使用者名稱或密碼錯誤")
   }
   opt := grpc.UnaryInterceptor(interceptor)
   g := grpc.NewServer(opt)
   // 使用攔截器結束
   s := HelloServer{}
   proto.RegisterHelloServer(g, &s)
   lis, error := net.Listen("tcp", "0.0.0.0:50052")
   if error != nil {
      panic("啟動服務異常")
   }
   g.Serve(lis)

}

(2)客戶端

package main

import (
	"context"
	"fmt"
	"go_test_learn/interpret_proto/proto"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
	"google.golang.org/grpc/metadata"
	"time"
)

func main() {
	// 建立客戶端攔截器
	interceptor := func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error{
		start := time.Now()
		fmt.Println("在客戶端攔截器中加入使用者名稱密碼")
		md := metadata.New(map[string]string{"name": "lqz", "password": "123"})
		ctx = metadata.NewOutgoingContext(context.Background(), md)
		err := invoker(ctx, method, req, reply, cc, opts...)
		fmt.Printf("耗時:%s\n", time.Since(start))
		return err
	}
	opt := grpc.WithUnaryInterceptor(interceptor)
	conn, err := grpc.Dial("127.0.0.1:50052", opt,grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		panic("連線服務異常")
	}
	defer conn.Close()
	client := proto.NewHelloClient(conn)
	request := proto.HelloRequest{Name: "lqz",}
	res, err := client.Hello(context.Background(), &request)
	if err != nil {
		fmt.Println(err)
		panic("呼叫方法異常")
	}

	fmt.Println(res.Reply)
}

(3)proto

syntax = "proto3";
option go_package = ".;proto";


service Hello{
  rpc Hello(HelloRequest) returns(HelloResponse);
}

message HelloRequest {
  string name = 1;
}

message HelloResponse {
  string reply = 1;
}

3.2 WithPerRPCCredentials

(1)服務端(程式碼不變)

package main

import (
	"context"
	"fmt"
	"go_test_learn/interpret_proto/proto"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/metadata"
	"google.golang.org/grpc/status"
	"net"
)

type HelloServer struct {
}

func (s *HelloServer) Hello(context context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
	fmt.Println(request.Name)
	return &proto.HelloResponse{
		Reply: "收到客戶端的資料:" + request.Name,
	}, nil
}

func main() {
	//服務端攔截器
	interceptor :=  func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error){
		fmt.Println("進行使用者名稱密碼認證")
		// 取出MD
		md, ok := metadata.FromIncomingContext(ctx)
		if  !ok { // 沒有取出meta,返回錯誤-->這個錯誤的rpc的錯誤,狀態碼也是rpc的狀態碼
			return resp,status.Error(codes.Unauthenticated,"沒有攜帶認證資訊")
		}
		// 攜帶meta,取出name和password
		// 取出name來
		var (
			name string
			password string
		)
		if names,ok:=md["name"];ok{
			name=names[0]
		}
		// 取出password來
		if passwords,ok:=md["password"];ok{
			password=passwords[0]
		}
		fmt.Println(name,password)
		if name=="lqz" && password=="123"{
			res,err := handler(ctx, req)
			return res, err
		}
		return resp, status.Error(codes.Unauthenticated,"使用者名稱或密碼錯誤")
	}
	opt := grpc.UnaryInterceptor(interceptor)
	g := grpc.NewServer(opt)
	// 使用攔截器結束
	s := HelloServer{}
	proto.RegisterHelloServer(g, &s)
	lis, error := net.Listen("tcp", "0.0.0.0:50052")
	if error != nil {
		panic("啟動服務異常")
	}
	g.Serve(lis)

}

(2)客戶端(使用內建的)

package main

import (
	"context"
	"fmt"
	"go_test_learn/interpret_proto/proto"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"
)

// 1 第一步,定義結構體,實現GetRequestMetadata和RequireTransportSecurity方法
type CommonCredential struct {
}

func (c CommonCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
	return map[string]string{
		"name":     "lqz1",
		"password": "123",
	}, nil
}
func (c CommonCredential) RequireTransportSecurity() bool {
	//是否需要基於 TLS 認證進行安全傳輸
	return false
}
func main() {
	// 使用內建的WithPerRPCCredentials
	// 1 第一步,定義結構體,實現GetRequestMetadata和RequireTransportSecurity方法
	//2 第二步,生成DialOption物件
	opt := grpc.WithPerRPCCredentials(CommonCredential{})

	conn, err := grpc.Dial("127.0.0.1:50052", grpc.WithTransportCredentials(insecure.NewCredentials()),opt)
	if err != nil {
		fmt.Println(err)
		panic("連線服務異常")
	}
	defer conn.Close()
	client := proto.NewHelloClient(conn)
	request := proto.HelloRequest{Name: "lqz"}
	res, err := client.Hello(context.Background(), &request)
	if err != nil {
		fmt.Println(err)
		panic("呼叫方法異常")
	}

	fmt.Println(res.Reply)
}

四、驗證器

  • 我們使用第三方:https://github.com/envoyproxy/protoc-gen-validate

4.1 linux/mac安裝

// 執行命令
go get -d github.com/envoyproxy/protoc-gen-validate
make build

//執行make build之前需要先切換到protoc-gen-validate路徑下;因為make build執行的就是這個路徑下的Makefile;一定要確保在對應的路徑下,這樣make build才不會出錯

/*
mac位置在:Users/liuqingzheng/go/pkg/mod/github.com/envoyproxy/protoc-gen-validate@v0.6.7
// 許可權問題,cp到go路徑下
cp -r protoc-gen-validate@v0.6.7 /Users/liuqingzheng/go/validate
// export GO111MODULE=on  開啟go mod模式
*/

4.2 win安裝

  • 下載exe,將exe檔案複製到 go的根目錄的bin目錄下

  • 0.6.7版本exe

  • 最新版本查詢

下載完成,放到gopath的bin路徑下,加入環境變數

4.3 程式碼

(1)proto--hello.proto

syntax = "proto3";
import "validate.proto";

option go_package = "./;proto";



service Hello{
  rpc Hello(Person) returns(Person);
}


message Person {
  uint64 id    = 1 [(validate.rules).uint64.gt    = 999];

  string email = 2 [(validate.rules).string.email = true];

  string mobile  = 3 [(validate.rules).string = {
    pattern:   "^1[3-9][0-9]{9}$",
  }];

}

(2)proto--validate.proto

https://github.com/envoyproxy/protoc-gen-validate/blob/main/validate/validate.proto

(3)執行命令

protoc --go_out=. --validate_out="lang=go:." hello.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false   --validate_out="lang=go:." hello.proto

(4)服務端

package main

import (
	"awesomeProject/valdiate_proto/proto"
	"context"
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"
	"net"
)

type HelloServer struct {
}

func (s *HelloServer) Hello(context context.Context, request *proto.Person) (*proto.Person, error) {
	fmt.Println(request.Id)
	err:=request.Validate()

	if err != nil {
		panic(err)
	}else {

		return &proto.Person{
			Id:1000,
			Email:" 3@qq.com",
			Mobile: "18953675221",
		}, nil
	}



}

type Validator interface {
	Validate() error
}
func main() {
	//服務端攔截器
	interceptor :=  func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error){
		fmt.Println("進行資料校驗")
		if r,ok:=req.(Validator);ok{
			if err:=r.Validate();err!=nil{
				return resp, status.Error(codes.Unauthenticated,err.Error())
			}
		}
		return handler(ctx,req)


	}
	opt := grpc.UnaryInterceptor(interceptor)
	g := grpc.NewServer(opt)

	// 使用攔截器結束
	s := HelloServer{}
	proto.RegisterHelloServer(g, &s)
	lis, error := net.Listen("tcp", "0.0.0.0:50052")
	if error != nil {
		panic("啟動服務異常")
	}
	g.Serve(lis)

}

//func main() {
//	per:=new(proto.Person)
//	err:=per.Validate()
//	fmt.Println(err)
//}

(5)客戶端

package main

import (
	"context"
	"fmt"
	"awesomeProject/valdiate_proto/proto"
	"google.golang.org/grpc"
	"google.golang.org/grpc/credentials/insecure"

)



func main() {

	conn, err := grpc.Dial("127.0.0.1:50052", grpc.WithTransportCredentials(insecure.NewCredentials()))
	if err != nil {
		fmt.Println(err)
		panic("連線服務異常")
	}
	defer conn.Close()
	client := proto.NewHelloClient(conn)
	request := proto.Person{
		Id:1000,
		Email:" 3@qq.com",
		Mobile: "1895636255144",
	}
	res, err := client.Hello(context.Background(), &request)
	if err != nil {
		fmt.Println(err)
		panic("呼叫方法異常")
	}

	fmt.Println(res.Mobile)
}

五、grpc 狀態碼

  • gRPC提供的:https://github.com/grpc/grpc/blob/master/doc/statuscodes.md

image-20220522010411111

六、grpc 錯誤

6.1 服務端

status.Error(codes.Unauthenticated,"沒有攜帶認證資訊")

status.New(codes.Unauthenticated,"沒有攜帶認證資訊").Err()

status.Newf(codes.Unauthenticated,"沒有攜帶認證資訊%s","lqz").Err()

6.2 客戶端

s,ok:=status.FromError(err)
if !ok{
		fmt.Println(s.Message())
		fmt.Println(s.Code())
}

七、grpc超時機制

7.1 客戶端使用ctx

ctx,_:=context.WithTimeout(context.Background(),time.Second*3)
res, err := client.Hello(ctx, &request)
fmt.Println(err)
s,ok:=status.FromError(err)
if !ok{
		fmt.Println("ok")
		fmt.Println(s.Message())
		fmt.Println(s.Code())
}
fmt.Println(s.Message())
fmt.Println(s.Code())

7.2 服務端睡5s

func (s *HelloServer) Hello(context context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
	fmt.Println(request.Name)
	fmt.Println("睡5s")
	time.Sleep(5*time.Second)

	return &proto.HelloResponse{
		Reply: "收到客戶端的資料:" + request.Name,
	}, nil
}