雲原生 API 網關,gRPC-Gateway V2 初探

gRPC-Gateway 簡介

咱們都知道 gRPC 並非萬能的工具。 在某些狀況下,咱們仍然想提供傳統的 HTTP/JSON API。緣由可能從保持向後兼容性到支持編程語言或 gRPC 沒法很好地支持的客戶端。可是僅僅爲了公開 HTTP/JSON API 而編寫另外一個服務是一項很是耗時且乏味的任務。git

那麼,有什麼方法能夠只編寫一次代碼,卻能夠同時在 gRPCHTTP/JSON 中提供 APIgithub

答案是 Yesgolang

gRPC-GatewayGoogle protocol buffers compiler protoc 的插件。 它讀取 protobuf service 定義並生成反向代理服務器( reverse-proxy server) ,該服務器將 RESTful HTTP API 轉換爲 gRPC。 該服務器是根據服務定義中的 google.api.http 批註(annotations)生成的。編程

這有助於你同時提供 gRPCHTTP/JSON 格式的 APIjson

開始以前

在開始編碼以前,咱們必須安裝一些工具。api

在示例中,咱們將使用 Go gRPC Server,所以請首先從 https://golang.org/dl/ 安裝 Go服務器

安裝 Go 以後,請使用 go get 下載如下軟件包:微信

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

這將安裝咱們生成存根所需的協議生成器插件。確保將 $GOPATH/bin 添加到 $PATH 中,以便經過 go get 安裝的可執行文件在 $PATH 中可用。curl

咱們將在本教程的新模塊中進行工做,所以,請當即在您選擇的文件夾中建立該模塊:tcp

建立 go.mod 文件

使用 go mod init 命令啓動你的 module 以建立 go.mod 文件。

運行 go mod init 命令,給它代碼所在 module 的路徑。在這裏,使用 github.com/myuser/myrepo 做爲 module 路徑—在生產代碼中,這將是能夠從其中下載 moduleURL

$ go mod init github.com/myuser/myrepo
go: creating new go.mod: module github.com/myuser/myrepo

go mod init 命令建立一個 go.mod 文件,該文件將您的代碼標識爲能夠從其餘代碼中使用的 module。 您剛建立的文件僅包含模塊名稱和代碼支持的 Go 版本。 可是,當您添加依賴項(即其餘模塊的軟件包)時,go.mod 文件將列出要使用的特定 module 版本。 這樣能夠使構建具備可複製性,並使您能夠直接控制要使用的 module 版本。

用 gRPC 建立一個簡單的 hello world

爲了瞭解 gRPC-Gateway,咱們首先要製做一個 hello world gRPC 服務。

使用 protocol buffers 定義 gRPC service

在建立 gRPC 服務以前,咱們應該建立一個 proto 文件來定義咱們須要的東西,這裏咱們在 proto/helloworld/ 目錄下建立了一個名爲 hello_world.proto 的文件。

gRPC service 使用 Google Protocol Buffers 定義的。這裏定義以下:

syntax = "proto3";

package helloworld;

// The greeting service definition
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

使用 buf 生成 stubs

Buf 是一個工具,它提供了各類 protobuf 實用程序,如 linting, breaking change detectiongeneration。請在 https://docs.buf.build/installation/ 上找到安裝說明。

它是經過 buf.yaml 文件配置的,應將其檢入你存儲庫的根目錄中。 若是存在,Buf 將自動讀取此文件。 也能夠經過命令行標誌 --config 提供配置,該標誌接受 .json.yaml 文件的路徑,或是直接 JSONYAML 數據。

全部使用本地 .proto 文件做爲輸入的 Buf 操做都依賴於有效的構建配置。這個配置告訴 Buf 在哪裏搜索 .proto 文件,以及如何處理導入。與 protoc(全部 .proto 文件都是在命令行上手動指定的)不一樣,buf 的操做方式是遞歸地發現配置下的全部 .proto 文件並構建它們。

下面是一個有效配置的示例,假設您的 .proto 文件根位於相對於存儲庫根的 proto 文件夾中。

version: v1beta1
name: buf.build/myuser/myrepo
build:
  roots:
    - proto

要爲 Go 生成 typegRPC stubs,請在存儲庫的根目錄下建立文件 buf.gen.yaml

version: v1beta1
plugins:
  - name: go
    out: proto
    opt: paths=source_relative
  - name: go-grpc
    out: proto
    opt: paths=source_relative

咱們使用 gogo-grpc 插件生成 Go typesgRPC service 定義。咱們正在輸出相對於 proto 文件夾的生成文件,並使用 path=source_relative 選項,這意味着生成的文件將與源 .proto 文件顯示在同一目錄中。

而後運行:

$ buf generate

這將爲咱們的 proto 文件層次結構中的每一個 protobuf 軟件包生成一個 *.pb.go*_grpc.pb.go 文件。

使用 protoc 生成 stubs

這是一個 protoc 命令可能會生成 Go stubs 的示例,假設您位於存儲庫的根目錄,而且您的 proto 文件位於一個名爲 proto 的目錄中:

$ protoc -I ./proto \
   --go_out ./proto --go_opt paths=source_relative \
   --go-grpc_out ./proto --go-grpc_opt paths=source_relative \
   ./proto/helloworld/hello_world.proto

咱們使用 gogo-grpc 插件生成 Go typesgRPC service 定義。咱們正在輸出相對於 proto 文件夾的生成文件,並使用 path=source_relative 選項,這意味着生成的文件將與源 .proto 文件顯示在同一目錄中。

這將爲 proto/helloworld/hello_world.proto 生成一個 *.pb.go*_grpc.pb.go 文件。

建立 main.go

在建立 main.go 文件以前,咱們假設用戶已經建立了一個名爲 github.com/myuser/myrepogo.mod。此處的 import 使用的是相對於存儲庫根目錄的 proto/helloworld 中生成的文件的路徑。

package main

import (
	"context"
	"log"
	"net"

	"google.golang.org/grpc"

	helloworldpb "github.com/myuser/myrepo/proto/helloworld"
)

type server struct{}

func NewServer() *server {
	return &server{}
}

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

func main() {
	// Create a listener on TCP port
	lis, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatalln("Failed to listen:", err)
	}

	// Create a gRPC server object
	s := grpc.NewServer()
	// Attach the Greeter service to the server
	helloworldpb.RegisterGreeterServer(s, &server{})
	// Serve gRPC Server
	log.Println("Serving gRPC on 0.0.0.0:8080")
	log.Fatal(s.Serve(lis))
}

將 gRPC-Gateway 批註添加到現有的 proto 文件中

如今,咱們已經能夠使用 Go gRPC 服務器,咱們須要添加 gRPC-Gateway 批註。

批註定義了 gRPC 服務如何映射到 JSON 請求和響應。 使用 protocol buffers 時,每一個 RPC 必須使用 google.api.http 批註定義 HTTP 方法和路徑。

所以,咱們須要將 google/api/http.proto 導入添加到 proto 文件中。咱們還須要添加所需的 HTTP->gRPC 映射。在這種狀況下,咱們會將 POST /v1/example/echo 映射到咱們的 SayHello RPC

syntax = "proto3";

package helloworld;

import "google/api/annotations.proto";

// Here is the overall greeting service definition where we define all our endpoints
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {
    option (google.api.http) = {
      post: "/v1/example/echo"
      body: "*"
    };
  }
}

// The request message containing the user's name
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

生成 gRPC-Gateway stubs

如今咱們已經將 gRPC-Gateway 批註添加到了 proto 文件中,咱們須要使用 gRPC-Gateway 生成器來生成存根(stubs)。

使用 buf

咱們須要將 gRPC-Gateway 生成器添加到生成配置中:

version: v1beta1
plugins:
  - name: go
    out: proto
    opt: paths=source_relative
  - name: go-grpc
    out: proto
    opt: paths=source_relative,require_unimplemented_servers=false
  - name: grpc-gateway
    out: proto
    opt: paths=source_relative

咱們還須要將 googleapis 依賴項添加到咱們的 buf.yaml 文件中:

version: v1beta1
name: buf.build/myuser/myrepo
deps:
  - buf.build/beta/googleapis
build:
  roots:
    - proto

而後,咱們須要運行 buf beta mod update 以選擇要使用的依賴項版本。

就是這樣!如今,若是您運行:

$ buf generate

它應該產生一個 *.gw.pb.go 文件。

使用 protoc

在使用 protoc 生成 stubs 以前,咱們須要將一些依賴項複製到咱們的 proto 文件結構中。將一部分 googleapis 從官方存儲庫複製到您本地的原始文件結構中。以後看起來應該像這樣:

proto
├── google
│   └── api
│       ├── annotations.proto
│       └── http.proto
└── helloworld
    └── hello_world.proto

如今咱們須要將 gRPC-Gateway 生成器添加到 protoc 調用中:

$ protoc -I ./proto \
  --go_out ./proto --go_opt paths=source_relative \
  --go-grpc_out ./proto --go-grpc_opt paths=source_relative \
  --grpc-gateway_out ./proto --grpc-gateway_opt paths=source_relative \
  ./proto/helloworld/hello_world.proto

這將生成一個 *.gw.pb.go 文件。

咱們還須要在 main.go 文件中添加 gRPC-Gateway 多路複用器(mux)併爲其提供服務。

package main

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

	"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
	"google.golang.org/grpc"

	helloworldpb "github.com/myuser/myrepo/proto/helloworld"
)

type server struct{
	helloworldpb.UnimplementedGreeterServer
}

func NewServer() *server {
	return &server{}
}

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

func main() {
	// Create a listener on TCP port
	lis, err := net.Listen("tcp", ":8080")
	if err != nil {
		log.Fatalln("Failed to listen:", err)
	}

	// Create a gRPC server object
	s := grpc.NewServer()
	// Attach the Greeter service to the server
	helloworldpb.RegisterGreeterServer(s, &server{})
	// Serve gRPC server
	log.Println("Serving gRPC on 0.0.0.0:8080")
	go func() {
		log.Fatalln(s.Serve(lis))
	}()

	// Create a client connection to the gRPC server we just started
	// This is where the gRPC-Gateway proxies the requests
	conn, err := grpc.DialContext(
		context.Background(),
		"0.0.0.0:8080",
		grpc.WithBlock(),
		grpc.WithInsecure(),
	)
	if err != nil {
		log.Fatalln("Failed to dial server:", err)
	}

	gwmux := runtime.NewServeMux()
	// Register Greeter
	err = helloworldpb.RegisterGreeterHandler(context.Background(), gwmux, conn)
	if err != nil {
		log.Fatalln("Failed to register gateway:", err)
	}

	gwServer := &http.Server{
		Addr:    ":8090",
		Handler: gwmux,
	}

	log.Println("Serving gRPC-Gateway on http://0.0.0.0:8090")
	log.Fatalln(gwServer.ListenAndServe())
}

測試 gRPC-Gateway

如今咱們能夠啓動服務器了:

$ go run main.go

而後,咱們使用 cURL 發送 HTTP 請求:

$ curl -X POST -k http://localhost:8090/v1/example/echo -d '{"name": " hello"}'
{"message":"hello world"}

Refs

我是爲少
微信:uuhells123
公衆號:黑客下午茶
加我微信(互相學習交流),關注公衆號(獲取更多學習資料~)
相關文章
相關標籤/搜索