云原生 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
公众号:黑客下午茶
加我微信(互相学习交流),关注公众号(获取更多学习资料~)