go語言中protobuf高階使用

語言: CN / TW / HK

如果你僅僅是想玩玩go-web單機開發可以參考gin專案框架

一、微服務之間傳遞資料

  • 1、在微服務中不僅僅是可以通過入參和返回引數來進行資料互動,另外還可以通過metadata的方式傳遞引數

  • 2、定義一個簡單的proto檔案

```protobuf syntax = "proto3";

option go_package = ".;proto";

service HelloWorld { rpc SayHello(HelloRequest) returns(HelloResponse); }

message HelloRequest { string name = 1; }

message HelloResponse { string message = 1; } ```

  • 3、伺服器端定義介面客戶端傳遞過來的metadata的資料

```go type server struct { }

func (s server) SayHello(ctx context.Context, in proto.HelloRequest) (*proto.HelloResponse, error) { md, ok := metadata.FromIncomingContext(ctx) if !ok { log.Fatalf("獲取metadata資料失敗") } if nameSlice, ok := md["name"]; ok { fmt.Println("獲取到的資料", nameSlice[0]) } return &proto.HelloResponse{ Message: "hello" + in.Name, }, nil } func main() { listen, err := net.Listen("tcp", ":9000") if err != nil { log.Fatalf("監聽埠錯誤:" + err.Error()) } service := grpc.NewServer() proto.RegisterHelloWorldServer(service, &server{}) err = service.Serve(listen) if err != nil { fmt.Println(err.Error()) } } ```

  • 4、客戶端定義metadata傳遞引數給伺服器端

```go func main() { conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure()) if err != nil { fmt.Println(err.Error()) return } defer conn.Close()

c := proto.NewHelloWorldClient(conn)
// 自定義傳遞的metadata引數
md := metadata.New(map[string]string{
    "name":     "admin",
    "password": "123456",
})
ctx := metadata.NewOutgoingContext(context.Background(), md)
response, err := c.SayHello(ctx, &proto.HelloRequest{
    Name: "admin",
})
if err != nil {
    fmt.Println(err.Error())
    return
}
fmt.Println("伺服器端返回的資料" + response.Message)

} ```

二、攔截器的使用interceptor

  • 1、所謂的攔截器是在網路請求的過程中,攔截請求和響應的,以下先配置一個簡單的protobuf的網路請求,然後在這個基礎上新增攔截器,來攔截客戶端傳送的資料到伺服器端,也可以用來統計介面訪問時長

  • 1、proto檔案

```protobuf syntax = "proto3";

option go_package = ".;proto";

service HelloWorld { rpc SayHello(HelloRequest) returns(HelloResponse); }

message HelloRequest { string name = 1; }

message HelloResponse { string message = 1; } ```

  • 2、使用命令生成go檔案

sh protoc -I=. --go_out=plugins=grpc,paths=source_relative:. helloWorld.proto

  • 3、伺服器端的程式碼

    ```go type server struct { }

    func (s server) SayHello(ctx context.Context, in proto.HelloRequest) (*proto.HelloResponse, error) { return &proto.HelloResponse{ Message: "hello" + in.Name, }, nil }

    func main() { listen, err := net.Listen("tcp", ":9000") if err != nil { log.Fatalf("監聽埠錯誤:" + err.Error()) } service := grpc.NewServer() proto.RegisterHelloWorldServer(service, &server{}) err = service.Serve(listen) if err != nil { fmt.Println(err.Error()) } } ```

  • 4、客戶端

    ```go func main() { conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure()) if err != nil { fmt.Println(err.Error()) return } defer conn.Close()

    c := proto.NewHelloWorldClient(conn)
    response, err := c.SayHello(context.Background(), &proto.HelloRequest{
        Name: "admin",
    })
    if err != nil {
        fmt.Println(err.Error())
        return
    }
    fmt.Println("伺服器端返回的資料" + response.Message)
    

    }

  • 2、在伺服器端定義攔截器來攔截請求,客戶端的不變,啟動客戶端和伺服器端,觀察伺服器端

```go func main() { listen, err := net.Listen("tcp", ":9000") if err != nil { log.Fatalf("監聽埠錯誤:" + err.Error()) } // 定義攔截器 interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { fmt.Println("接收到一個新的請求") return handler(ctx, req) } // 使用攔截器 opt := grpc.UnaryInterceptor(interceptor) service := grpc.NewServer(opt)

proto.RegisterHelloWorldServer(service, &server{})
err = service.Serve(listen)
if err != nil {
    fmt.Println(err.Error())
}

} ```

  • 3、攔截在請求前和請求後都列印資訊出來

go ... // 定義攔截器 interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { fmt.Println("請求前列印") res, err := handler(ctx, req) fmt.Println("請求完成列印") return res, err } // 使用攔截器 opt := grpc.UnaryInterceptor(interceptor) service := grpc.NewServer(opt) ...

  • 4、客戶端攔截器的使用

```go 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() 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("localhost:9000", grpc.WithInsecure(), opt) if err != nil { fmt.Println(err.Error()) return } defer conn.Close()

c := proto.NewHelloWorldClient(conn)
response, err := c.SayHello(context.Background(), &proto.HelloRequest{
    Name: "admin",
})
if err != nil {
    fmt.Println(err.Error())
    return
}
fmt.Println("伺服器端返回的資料" + response.Message)

} ```

三、使用攔截器和metadata來做微服務之間的授權

  • 1、這裡授權我們可以簡單的做一個類似使用者登入後才能訪問的功能,把客戶端想象成瀏覽器,把伺服器端想象成之前我們熟悉的gin-web開發

  • 2、簡單的demo和上面的一樣的,只是在基礎上新增攔截器和metadata的操作

  • 3、在客戶端的攔截器中傳遞使用者名稱和密碼到伺服器端

```go 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.Printf("耗時:%s\n", time.Since(start)) // 使用metadata傳遞資料 md := metadata.New(map[string]string{ "username": "admin", "password": "123456", }) ctx = metadata.NewOutgoingContext(context.Background(), md) err := invoker(ctx, method, req, reply, cc, opts...) return err } // 使用攔截器 opt := grpc.WithUnaryInterceptor(interceptor) conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure(), opt) if err != nil { fmt.Println(err.Error()) return } defer conn.Close()

c := proto.NewHelloWorldClient(conn)
response, err := c.SayHello(context.Background(), &proto.HelloRequest{
    Name: "admin",
})
if err != nil {
    fmt.Println(err.Error())
    return
}
fmt.Println("伺服器端返回的資料" + response.Message)

} ```

  • 4、伺服器端在攔截中獲取到metadata中的資料並且判斷使用者名稱和密碼

```go func main() { listen, err := net.Listen("tcp", ":9000") if err != nil { log.Fatalf("監聽埠錯誤:" + err.Error()) } // 定義攔截器 interceptor := func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (resp interface{}, err error) { fmt.Println("請求前列印") // 解析metadata中的資料驗證 md, ok := metadata.FromIncomingContext(ctx) if !ok { return resp, status.Error(codes.Unauthenticated, "無效的引數") } var ( username string password string ) if val, ok := md["username"]; ok { username = val[0] } if val, ok := md["password"]; ok { password = val[0] } fmt.Println(username, password, "接收到的引數") if username != "admin" || password != "123456" { return resp, status.Error(codes.Unauthenticated, "使用者名稱和密碼錯誤") } res, err := handler(ctx, req) fmt.Println("請求完成列印") return res, err } // 使用攔截器 opt := grpc.UnaryInterceptor(interceptor) service := grpc.NewServer(opt)

proto.RegisterHelloWorldServer(service, &server{})
err = service.Serve(listen)
if err != nil {
    fmt.Println(err.Error())
}

} ```

四、引數校驗

五、返回狀態嗎

  • 1、我們在http請求的時候有狀態嗎,自然在grpc的時候也有狀態碼,來標註當前請求成功與失敗

  • 2、github參考文件

  • 3、伺服器端使用狀態碼返回給客戶端端

```go import ( ... "google.golang.org/grpc" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "log" "net" )

type server struct { }

func (s server) SayHello(ctx context.Context, in proto.HelloRequest) (*proto.HelloResponse, error) { //return &proto.HelloResponse{ // Message: "hello" + in.Name, //}, nil // 返回錯誤 return nil, status.Error(codes.NotFound, "記錄沒有找到") }

func main() { listen, err := net.Listen("tcp", ":9000") if err != nil { log.Fatalf("監聽埠錯誤:" + err.Error()) }

service := grpc.NewServer()

proto.RegisterHelloWorldServer(service, &server{})
err = service.Serve(listen)
if err != nil {
    fmt.Println(err.Error())
}

} ```

  • 4、客戶端接收錯誤,根據錯誤中的狀態碼提示

```go func main() {

conn, err := grpc.Dial("localhost:9000", grpc.WithInsecure())
if err != nil {
    fmt.Println(err.Error())
    return
}
defer conn.Close()

c := proto.NewHelloWorldClient(conn)
response, err := c.SayHello(context.Background(), &proto.HelloRequest{
    Name: "admin",
})
if err != nil {
    str, ok := status.FromError(err)
    if !ok {
        panic("解析錯誤資訊失敗")
    }
    fmt.Println(str.Message(), "獲取到的錯誤資訊")
    fmt.Println(str.Code(), "獲取到的錯誤code")
    fmt.Println(err.Error())
    return
}
fmt.Println("伺服器端返回的資料" + response.Message)

} ```

六、超時處理

  • 1、在客戶端設定一個最大時間限制

go // 設定時間 ctx, _ := context.WithTimeout(context.Background(), time.Second*3) response, err := c.SayHello(ctx, &proto.HelloRequest{ Name: "admin", })

  • 2、伺服器端休眠下返回資料