寫給 go 開發者的 gRPC 教程 - gRPC Gateway


gRPC 使用 protobuf 來序列化數據,使用 protobuf 序列化的好處這裏就不再贅述了

但 protobuf 不是明文,不方便我們進行調試,如果能像 HTTP1.x 一樣進行訪問,就能減輕調試的負擔;在特殊場景下 client 側無法使用 HTTP2.0,因而也無法使用 gRPC 來進行調用,需要提供降級方案

gRPC-Gateway 是 protobuf 編譯器 protoc 的插件。它讀取 protobuf 文件中service 定義的內容,並生成反向代理服務器 (reverse-proxy server) ,該服務器可以將RESTful API轉換爲 gRPC,於是我們就可以像普通的 HTTP1.x 服務器一樣使用 JSON 請求 gRPC 服務

安裝

既然是protoc的插件,那麼和其他插件的使用類似。先安裝 gRPC-Gateway 插件:protoc-gen-grpc-gateway。當然protoc-gen-goprotoc-gen-go-grpc肯定是需要安裝的,它們兩個用於從 pb 文件生成數據結構和 grpc 服務

$ go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

生成 gRPC-Gateway 反向代理服務器

在不使用 gRPC-Gateway 時,我們定義的 pb 文件如下

syntax = "proto3";

package ecommerce;

import "google/protobuf/wrappers.proto";

option go_package = "/ecommerce";

service OrderManagement {
  rpc getOrder(google.protobuf.StringValue) returns (Order);
  rpc addOrder(Order) returns (google.protobuf.StringValue);
}

message Order {
  string id = 1;
  repeated string items = 2;
  string description = 3;
  float price = 4;
  google.protobuf.StringValue destination = 5;
}

目前有三種方式可以反向代理服務器

下面演示第二種方式

給 protobuf 添加 annotations

gRPC-Gateway 反向代理服務器根據servicegoogle.api.http 的批註 (annotations) 生成。

所以我們需要import "google/api/annotations.proto";

google.api.http 可以定義 HTTP 服務的 Method 和 Path 等

syntax = "proto3";

package ecommerce;

option go_package = "ecommerce/";

import "google/protobuf/wrappers.proto";
import "google/api/annotations.proto";

service OrderManagement {
    rpc getOrder(google.protobuf.StringValue) returns (Order){
        option(google.api.http) = {
            get: "/v1/getOrder"
        };
    }
}

message Order {
    string id = 1;
    repeated string items = 2;
    string description = 3;
    float price = 4;
    string destination = 5;
}

代碼生成

因爲使用了非內置的 pb 定義google/api/annotations.proto,所以需要在生成代碼前需要添加 pb 依賴:從官方倉庫 [1] 複製對應的 pb 文件到本地。添加依賴後目錄結構如下

pb
├── google
│   └── api
│       ├── annotations.proto
│       └── http.proto
└── product.proto

之後執行 protoc 來生成代碼

protoc -I ./pb \
  --go_out ./ecommerce --go_opt paths=source_relative \
  --go-grpc_out ./ecommerce --go-grpc_opt paths=source_relative \
  --grpc-gateway_out ./ecommerce --grpc-gateway_opt paths=source_relative \
  ./pb/product.proto

生成出來的代碼,對比非 gRPC-Gateway 的版本會多出了一個*.gw.pb.go文件

使用 buf

使用 protoc 命令不僅需要複製依賴到本地,執行的命令行也比較長

之前介紹過 buf 工具,此時 buf 就能體現出作用了。buf 工具不僅可以簡化代碼生成的命令,還可以解決依賴的問題

🌲 首先在 pb 文件的目錄中初始化 buf

buf mod init

buf 命令會創建buf.yaml文件,在此文件中添加依賴buf.build/googleapis/googleapis

version: v1
breaking:
  use:
    - FILE
lint:
  use:
    - DEFAULT
## add 
deps:
  - buf.build/googleapis/googleapis

🌲 更新依賴

buf mod update

buf 命令從 Buf Schema Registry (BSR)[2] 中獲取依賴,把你所有的 deps 更新到最新版。並且會生成 buf.lock 來固定版本

pb
├── buf.lock
├── buf.yaml
└── product.proto

🌲 創建一個buf.gen.yaml

它是 buf 生成代碼的配置。上面的protoc同等功能的buf.gen.yaml可以寫成如下形式,相對 protoc 更加直觀

version: v1
plugins:
  - plugin: go
    out: ecommerce
    opt:
      - paths=source_relative
  - plugin: go-grpc
    out: ecommerce
    opt:
      - paths=source_relative
  - name: grpc-gateway
    out: ecommerce
    opt:
      - paths=source_relative
      - generate_unbound_methods=true

🌲 生成代碼

buf generate pb

執行的效果和上文中 protoc 命令一樣

server 端的實現

只生成代碼還不夠,還得啓動 gRPC-Gateway 的反向代理服務器

啓動

有兩種方式來啓動 gRPC-Gateway

🌲 第一種先啓動 gRPC 服務,再以 gRPC 服務的連接爲基礎創建 grpc-gateway 服務

代碼中,我們啓動了 gRPC 的端口8009,同時也啓用了普通 HTTP 端口8010,gRPC-Gateway 通過 rpc 的方式訪問 gRPC,所以 gRPC 服務是必須要啓動的

package main

import (
 "context"
 "net"
 "net/http"

 "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
 pb "github.com/liangwt/note/grpc/ecosystem/grpc-gateway/ecommerce"
 "google.golang.org/grpc"
)

func main() {
 grpcPort, gwPort := ":8009"":8010"

 go func() {
  lis, err := net.Listen("tcp", grpcPort)
  if err != nil {
   panic(err)
  }

  s := grpc.NewServer()

  pb.RegisterOrderManagementServer(s, &OrderManagementImpl{})
  if err := s.Serve(lis); err != nil {
   panic(err)
  }
 }()

 // 建立一個到gRPC Port的連接
 conn, err := grpc.DialContext(
  context.Background(),
  "127.0.0.1"+grpcPort,
  grpc.WithBlock(),
  grpc.WithInsecure(),
 )
 if err != nil {
  panic(err)
 }

 gwmux := runtime.NewServeMux()
 err = pb.RegisterOrderManagementHandler(context.Background(), gwmux, conn)
 if err != nil {
  panic(err)
 }

 http.ListenAndServe(gwPort, gwmux)

 // 以下和http.ListenAndServe(gwPort, gwmux)等價
 
 // gwServer := &http.Server{
 //  Addr:    gwPort,
 //  Handler: gwmux,
 // }

 // if err := gwServer.ListenAndServe(); err != nil {
 //  panic(err)
 // }
}

🌲 還有一種方式不依賴 grpc 服務,以本地函數調用的方式實現

第一種方式使用RegisterOrderManagementHandler函數,這種方式使用RegisterOrderManagementHandlerServer。可以看到我們僅啓用了8010的 HTTP 端口,gRPC-Gateway 通過本地函數調用的方式訪問 gRPC

package main

import (
 "context"
 "net/http"

 "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
 pb "github.com/liangwt/note/grpc/ecosystem/grpc-gateway/ecommerce"
)

func main() {
 gwmux := runtime.NewServeMux()

 err := pb.RegisterOrderManagementHandlerServer(context.Background(), gwmux, &OrderManagementImpl{})
 if err != nil {
  panic(err)
 }

 http.ListenAndServe(":8010", gwmux)
}

測試訪問

無論哪種方式啓動 gRPC-Gateway,都可以通過 HTTP 進行訪問

$ curl -s -X GET \
  '127.0.0.1:8010/v1/getOrder?value=101' \
  --header 'Accept: */*' | jq
{
  "id""101",
  "items"[
    "Google",
    "Baidu"
  ],
  "description""example",
  "price": 0,
  "destination""example"
}

這裏有個細節需要注意,google.protobuf.StringValue在映射到 HTTP 的時候默認參數名爲value,所以訪問時請求參數寫成value=101

service OrderManagement {
    rpc getOrder(google.protobuf.StringValue) returns (Order){
        option(google.api.http) = {
            get: "/v1/getOrder"
        };
    }
}

進階

GET 請求參數

對於 GET 請求參數也可以定義到 HTTP 的 PATH 中,pb 文件這樣寫

service OrderManagement {
    rpc getOrder(google.protobuf.StringValue) returns (Order){
        option(google.api.http) = {
            get: "/v1/getOrder/{value}"
        };
    }
}
curl -X GET \
  '127.0.0.1:8010/v1/getOrder/101' \
  --header 'Accept: */*' \

我們還可以給參數設定個名字

service OrderManagement {
    rpc getOrder(getOrderReq) returns (Order){
        option(google.api.http) = {
            get: "/v1/getOrder"
        };
    }
}

message getOrderReq {
    google.protobuf.StringValue id = 1;
}
curl -X GET \
  '127.0.0.1:8010/v1/getOrder?id=101' \
  --header 'Accept: */*' \

依舊可以把參數放到 path 中

service OrderManagement {
    rpc getOrder(getOrderReq) returns (Order){
        option(google.api.http) = {
            get: "/v1/getOrder/{id}"
        };
    }
}

message getOrderReq {
    google.protobuf.StringValue id = 1;
}
curl -X GET \
  '127.0.0.1:8010/v1/getOrder/101' \
  --header 'Accept: */*' \

POST 請求

gRPC-Gatewway 當然可以生成 POST 請求,示例的 pb 文件如下

syntax = "proto3";

package ecommerce;

import "google/protobuf/wrappers.proto";
import "google/api/annotations.proto";

option go_package = "/ecommerce";

service OrderManagement {
  rpc getOrder(google.protobuf.StringValue) returns (Order) {
    option (google.api.http) = {
      get : "/v1/getOrder"
    };
  }

  rpc addOrder1(Order) returns (google.protobuf.StringValue) {
    option (google.api.http) = {
      post : "/v1/addOrder1"
      body : "*"
    };
  }

  rpc addOrder2(OrderRequest) returns (google.protobuf.StringValue) {
    option (google.api.http) = {
      post : "/v1/addOrder2"
      body : "order"
    };
  }
}

message OrderRequest { 
  Order order = 1; 
}

message Order {
  string id = 1;
  repeated string items = 2;
  string description = 3;
  float price = 4;
  google.protobuf.StringValue destination = 5;
}

🌲 對於addOrder1接口

通過 gRPC 請求時,入參需要一個Orderbody : "*"表示在 HTTP 的請求中的 body 需要包含Order的所有字段

$ curl -s -X POST \
  '127.0.0.1:8010/v1/addOrder1' \
  --header 'Accept: */*' \
  --data '{"id": "102","items": ["Google","Baidu"],"description": "example","price": 0,"destination": "example"}'

🌲 對於addOrder2接口

通過 gRPC 請求時,入參需要一個OrderRequestbody : "order"表示在 HTTP 的請求中的 body 需要包含OrderRequestorder字段

$ curl -s -X POST \
  '127.0.0.1:8010/v1/addOrder2' \
  --header 'Accept: */*' \
  --data '{"id": "102","items": ["Google","Baidu"],"description": "example","price": 0,"destination": "example"}'

添加自定義路由

我們還可以在 pb 文件生成的路由基礎上添加自定義的路由

func main() {
 gwmux := runtime.NewServeMux()

 err := pb.RegisterOrderManagementHandlerServer(context.Background(), gwmux, &OrderManagementImpl{})
 if err != nil {
  panic(err)
 }

 err = gwmux.HandlePath("GET""/hello/{name}", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
  w.Write([]byte("hello " + pathParams["name"]))
 })

 http.ListenAndServe(":8010", gwmux)
}

自動生成 swagger

pb 除了可以生成 HTTP 的 gateway,還可以生成 swagger 文件,用於文檔生成,使用到的插件爲protoc-gen-openapiv2

🌲 安裝

go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest

🌲 生成 swagger 文件

和生成 grpc 和 grpc-gateway 一起執行,或者單獨執行

protoc -I ./pb --openapiv2_out ./doc --openapiv2_opt logtostderr=true \
    ./pb/product.proto

當然更推薦使用 buf 工具

version: v1
plugins:
  - plugin: go
    out: ecommerce
    opt:
      - paths=source_relative
  - plugin: go-grpc
    out: ecommerce
    opt:
      - paths=source_relative
  - name: grpc-gateway
    out: ecommerce
    opt:
      - paths=source_relative
      - generate_unbound_methods=true
  - name: openapiv2
    out: doc
    opt:
      - logtostderr=true

於是便可以得到doc/product.swagger.json文件

🌲 可視化展示

product.swagger.json可以使用 swagger UI 來進行文檔的可視化展示


參考資料

[1]

官方倉庫: https://github.com/googleapis/googleapis

[2]

Buf Schema Registry (BSR): https://docs.buf.build/bsr/overview

[3]

grpc-gateway 官方文檔: https://grpc-ecosystem.github.io/grpc-gateway/

[4]

Transcoding HTTP/JSON to gRPC: https://cloud.google.com/endpoints/docs/grpc/transcoding#where_to_configure_transcoding

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/FfiNu7L5t65fJsRcdEdZIQ