深圳幻海软件技术有限公司 欢迎您!

Go:gRPC-Gateway 完全指南,你懂了吗?

2023-02-28

大家好,我是程序员幽鬼。gRPC越来越流行,相关的插件也很多,今天介绍的就是一个gRPC插件。gRPC-Gateway是一个插件,它为gRPC服务生成反向代理服务器,将Restful/JSON转换为gRPC,反之亦然。换句话说,gRPC-Gateway将在你的gRPC服务上创建一个层,该层将充当客户

大家好,我是程序员幽鬼。

gRPC 越来越流行,相关的插件也很多,今天介绍的就是一个 gRPC 插件。

gRPC-Gateway 是一个插件,它为 gRPC 服务生成反向代理服务器,将 Restful/JSON 转换为 gRPC,反之亦然。

换句话说,gRPC-Gateway 将在你的 gRPC 服务上创建一个层,该层将充当客户端的 Restful/JSON 服务。gRPC-Gateway 从 gRPC 服务的 Protocol Buffer 的定义生成代码。

1.介绍

gRPC-Gateway 是 protoc 的插件,将从 gRPC 定义生成 Go 代码。

生成的代码可以用作独立服务器或安装在现有代码库上。gRPC-Gateway 是高度可定制的,支持从 protoc 文件生成开放 API 文档。

在本教程指南中,我们将详细介绍独立服务器以及与现有代码的集成。查看此流程图以了解 gRPC 网关的工作原理。

gRPC-Gateway flowchart diagram

2.为什么选择 gRPC-Gateway?

gRPC 网关为 gRPC 服务构建代理,该代理充当客户端的 Restful/JSON 应用程序。它开启了使用相同代码库同时支持 Restful/JSON 和 gRPC 的可能性。有两个主要的场景:

  • 旧版客户端可能不支持 gRPC 并需要 Restful/JSON 接口
  • 浏览器可能不支持开箱即用的 gRPC;因此对于想要与 gRPC 服务交互的 Web 客户端,gRPC-Gateway 是首选选项。

最常见的 gRPC-Gateway 模式是创建单个 gRPC 网关服务器(可能在多台机器上运行),作为客户端的代理与多个 gRPC 服务交互。

下图解释了此服务的工作原理。

gRPC-Gateway and service requests flowchart diagram

gRPC 网关生成的反向代理被水平扩展以在多台机器上运行,并且在这些实例之前使用负载均衡器。单个实例可以托管多个 gRPC 服务的反向代理。

3.设置 gRPC 网关

gRPC-Gateway 是 protoc 的插件。在使用它之前,必须在系统上安装 protocol buffer 编译器。按照官方 gRPC 网站[1]上的指南,根据你使用的操作系统在你的系统上安装 protoc。

gRPC-Gateway 使用并生成 Go 代码。要安装 Go,请按照官方[2]网站上的指南进行操作。只要你的系统上安装了 Go,你就可以安装 gRPC-Gateway 插件了。

创建一个名为 grpc-gateway-demo 的目录,该目录将保存 gRPC-Gateway 项目。为了构建 protocol buffer 和生成 gRPC 网关反向代理,将使用 Buf。你可以按照官方网站[3]上的指南安装 Buf 。

项目结构

所有的 Protocol Buffers 文件都将在 proto目录中,而 Go 文件将在 root。要设置 Go 项目,请使用 go mod init grpc-gateway-demo 并创建一个 main.go 文件。你的项目应如下所示:

├── main.go
├── go.mod
└── proto
  • 1.
  • 2.
  • 3.

配置 BufBuf

需要三个不同的文件来生成存根和反向代理。

buf.gen.yaml
  • 1.

这些文件指定编译器应该使用的所有插件和相关选项。

QQqXL">使用 Buf,你可以简单地在 YAML 文件中指定名称和选项。Buf 还允许构建代码使用远程插件(即,指定的插件将在构建过程中由 Buf 自动下载并由本地系统上的 Buf 维护)。

version: v1
plugins:
  # generate go structs for protocol buffer defination
  - remote: buf.build/library/plugins/go:v1.27.1-1
    out: gen/go
    opt:
      - paths=source_relative
  # generate gRPC stubs in golang
  - remote: buf.build/library/plugins/go-grpc:v1.1.0-2
    out: gen/go
    opt:
      - paths=source_relative
  # generate reverse proxy from protocol definations
  - remote: buf.build/grpc-ecosystem/plugins/grpc-gateway:v2.6.0-1
    out: gen/go
    opt:
      - paths=source_relative
  # generate openapi documentation for api
  - remote: buf.build/grpc-ecosystem/plugins/openapiv2:v2.6.0-1
    out: gen/openapiv2
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
buf.yaml
  • 1.

该文件应位于所有 proto 文件的根目录中。这些文件指定编译 proto 文件(例如 Google API)所需的依赖项。

 version: v1
 deps:
 # adding well known types by google
  - buf.build/googleapis/googleapis
  • 1.
  • 2.
  • 3.
  • 4.
buf.work.yaml
  • 1.

此文件指定工作空间中包含 Protocol Buffer 定义的所有文件夹/目录。

version: v1
directories:
  - proto
  • 1.
  • 2.
  • 3.

完成后,你的项目结构应与此类似:

├── buf.gen.yaml
├── buf.work.yaml
├── go.mod
├── main.go
└── proto
    ├── buf.yaml
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

在项目根目录中你可以通过运行 buf build 命令来测试你的配置。

4.使用 gRPC 网关

到目前为止,你已将 gRPC-Gateway 设置为插件,但现在出现的问题是如何定义基本的 API 规范,如 HTTP 方法、URL 或请求正文。

为了定义这些规范选项在 Protocol Buffers 上的 rpc 方法定义中使用的 service 内容,下面的示例将使其更加清晰。

proto/hello/hello_world.proto:

// define syntax used in proto file
syntax = "proto3";
// options used by gRPC golang plugin(not related to gRPC gateway)
option go_package = "github.com/anshulrgoyal/grpc-gateway-demo;grpc_gateway_demo";

// well know type by google, gRPC gateway uses HTTP annotation.
import "google/api/annotations.proto";

package hello_world;

// simple message
message HelloRequest {
  string name = 1;
}

message HelloReply {
  string message = 1;
}

// a gRPC service
service Greeter {
 // SayHello is a rpc call and a option is defined for it
  rpc SayHello (HelloRequest) returns (HelloReply) {
  // option type is http
    option (google.api.http) = {
    // this is url, for RESTfull/JSON api and method
    // this line means when a HTTP post request comes with "/v1/sayHello" call this rpc method over this service
      post: "/v1/sayHello"
      body: "*"
    };
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.

option关键字用于为 Rest 请求添加规范。选择该 option 方法并指定该请求的路径。

在上面的例子中,post 是请求的 HTTP 方法,/v1/sayHello 是响应。

你现在可以在项目根目录中使用 buf generate 命令来构建代码。

命令完成后,项目的根目录中应该有一个 gen 目录,里面有 Go 代码。这些文件包含 gRPC 和 gRPC 网关反向代理的存根。openapiv2 包含 Swagger UI 的开放 API 文档。

gen
|-- go
|   `-- hello
|       |-- hello_world.pb.go
|       |-- hello_world.pb.gw.go
|       `-- hello_world_grpc.pb.go
`-- openapiv2
    `-- hello
        `-- hello_world.swagger.json
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

实现服务

本教程将在 Go 中实现 gRPC 服务器。任何 gRPC 实现对于 gRPC 网关都可以正常工作。

使用 Go 的优点是你可以在同一进程中运行 gRPC 服务和 gRPC-Gateway 生成的代码。这是 Go 的 Greeter 服务实现。

sever/main.go:

package main
import (
    "context"
    "fmt"
    "log"
    "net"
    // importing generated stubs
    gen "grpc-gateway-demo/gen/go/hello"
    "google.golang.org/grpc"
)
// GreeterServerImpl will implement the service defined in protocol buffer definitions
type GreeterServerImpl struct {
    gen.UnimplementedGreeterServer
}
// SayHello is the implementation of RPC call defined in protocol definitions.
// This will take HelloRequest message and return HelloReply
func (g *GreeterServerImpl) SayHello(ctx context.Context, request *gen.HelloRequest) (*gen.HelloReply, error) {
    return &gen.HelloReply{
        Message: fmt.Sprintf("hello %s",request.Name),
    },nil
}
func main() {
    // create new gRPC server
    server := grpc.NewServer()
    // register the GreeterServerImpl on the gRPC server
    gen.RegisterGreeterServer(server, &GreeterServerImpl{})
    // start listening on port :8080 for a tcp connection
    if l, err := net.Listen("tcp", ":8080"); err != nil {
        log.Fatal("error in listening on port :8080", err)
    } else {
        // the gRPC server
        if err:=server.Serve(l);err!=nil {
            log.Fatal("unable to start server",err)
        }
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.

上述文件是 gRPC 服务的基本实现。它侦听端口 8080。你可以在任何 gRPC 客户端上对其进行测试。

在 gRPC 网关代理上注册服务

gRPC 网关代理支持的每个 gRPC 服务器都需要在其上进行注册。

在底层,gRPC 网关服务器将创建一个 gRPC 客户端并使用它向提供的端点发出 gRPC 请求。你可以提供各种 DailOptions 注册函数。

proxy/main.go

package main
import (
    "context"
    "log"
    "net"
    "net/http"
    "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    "google.golang.org/grpc"
    gen "grpc-gateway-demo/gen/go/hello"
)

func main() {
    // creating mux for gRPC gateway. This will multiplex or route request different gRPC service
    mux:=runtime.NewServeMux()
    // setting up a dail up for gRPC service by specifying endpoint/target url
    err := gen.RegisterGreeterHandlerFromEndpoint(context.Background(), mux, "localhost:8080", []grpc.DialOption{grpc.WithInsecure()})
    if err != nil {
        log.Fatal(err)
    }
    // Creating a normal HTTP server
    server:=http.Server{
        Handler: mux,
    }
    // creating a listener for server
    l,err:=net.Listen("tcp",":8081")
    if err!=nil {
        log.Fatal(err)
    }
    // start server
    err = server.Serve(l)
    if err != nil {
        log.Fatal(err)
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.

ServerMux 是一个多路复用器,它将根据 JSON/Restful 请求的路径将请求路由到各种注册服务。

grpc.WithInsecure() dial 选项用于允许服务在不使用身份验证的情况下连接到 gRPC 。localhost:8080 是一个 gPRC 服务正在运行的 URL - 因为 Greet(gRPC 服务构建之前看到)服务正在端口 8080 上运行,所以 localhost:8080 被使用。

一旦注册了处理程序,mux 就可以处理 HTTP 请求了。在这里,http 包中的 Go 标准 HTTP 服务器被使用。你也可以自由地使用其他实现,本文稍后将通过 gRPC 网关代理使用 Gin 来演示这一点[4]。

ServerMux 实现 ServeHTTP 接口——它可以像Handler在 HTTP 服务器中一样使用。服务在 8081 端口上运行。

要启动服务,只需在项目目录的根目录中运行 go run proxy/main.go 。

使用路径参数

现在,如果你想让 v1/sayHello API 在 POST 调用中成为 GET 调用并将数据作为路径参数传递,那么在完成 gRPC 网关设置后,你无需更改代码中的任何内容 — 只需更改协议缓冲区定义并重新生成存根,你都可以使用新的 API。

message HelloRequest {
  string name = 1;
}

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {
    option (google.api.http) = {
     get:"/v1/sayHello/{name}"
    };
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

上述代码段中提供的路径是 /v1/sayHello/{name}。你可以使用请求有效负载(HelloRequest 在本例中)中的任何键作为路径参数。如果你使用带有 path 的 GET 请求/v1/sayHello/jane,该请求将被路由到 Greeter.sayHello gRPC 调用。你可以在 URL 中使用任意数量的路径参数。

现在你对 gRPC 网关及其设置有了一些基本的了解。

我们使用的示例只是对 gRPC 网关的介绍,但要在生产环境中运行某些东西,你需要进行日志记录、跟踪和错误处理。

5.常见的使用模式

对于任何准备好用于生产的系统,它都应该有一些错误处理并允许某种错误日志记录。

添加日志记录

本文的这一部分将演示如何将中间件与 gRPC 网关生成的代理一起使用。

ServerMux 实现了一个 Handler 接口,因此你可以使用任何中间件来包装 ServerMux 和记录传入和输出请求。

type Handler interface {
  ServeHTTP(ResponseWriter, *Request)
}
  • 1.
  • 2.
  • 3.

要创建用于日志记录的中间件,你可以从 *Request 中提取与 HTTP 请求相关的信息,并从正在使用的 httpsnoop 包中提取有关响应的信息。

func withLogger(handler http.Handler) http.Handler {
    // the create a handler
    return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
        // pass the handler to httpsnoop to get http status and latency
        m:=httpsnoop.CaptureMetrics(handler,writer,request)
        // printing exracted data
        log.Printf("http[%d]-- %s -- %s\n",m.Code,m.Duration,request.URL.Path)
    })
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.

该 withLogger 方法将封装 Handler 接口并调用 snoop 以提取信息。在后台,该 ServerHTTP 方法由 httpsnoop 包调用。

server := http.Server{
  Handler: withLogger(mux),
}
  • 1.
  • 2.
  • 3.

这与 Go 生态系统中使用的任何其他处理程序没有什么不同。由于 ServerMux 是一个普通的处理程序,任何可用的中间件也可以与 gRPC 网关生成的反向代理一起使用。

错误处理

gRPC 网关已经带有将 gRPC 错误代码转换为客户端使用的 HTTP 状态的映射。例如,它会自动将众所周知的和使用过的 gRPC 代码映射到 HTTP 状态。

InvalidArgument 转换为 400(错误请求)。如需完整列表,你可以查看此链接[5]。如果你有自定义要求,例如需要非常规状态代码,则可以使用 WithErrorhandler 带有错误处理函数的选项——所有错误都将通过请求和响应编写器传递给该函数。

runtime.WithErrorHandler(
  func(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, writer http.ResponseWriter, request *http.Request, err error) {}
)
  • 1.
  • 2.
  • 3.

错误处理函数获取以下参数:

  • ctx:上下文,保存有关执行的元数据
  • mux:这是 ServerMux;它保存有关服务器的配置数据,例如应将哪个标头传递给响应
  • marshaler:将 Protocol Buffer 响应转换为 JSON 响应
  • writer:这是客户端的响应编写器
  • request:这请求包含客户端发送的信息的对象
  • err:gRPC 服务发送的错误

这是一个简单的 WithErrorHandler 例子。在此示例中,无论错误如何,发生错误时请求的 HTTP 状态都会更改为 400。

mux: = runtime.NewServeMux(
        runtime.WithErrorHandler(func(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, writer http.ResponseWriter, request *http.Request, err error) {
            //creating a new HTTTPStatusError with a custom status, and passing error
            newError:=runtime.HTTPStatusError{
                HTTPStatus: 400,
                Err:        err,
            }
            // using default handler to do the rest of heavy lifting of marshaling error and adding headers
            runtime.DefaultHTTPErrorHandler(ctx,mux,marshaler,writer,request,&newError)
        }))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

通过创建一个新错误并将其传递给 DefaultHTTPErrorHandler。重要的是要注意,DefaultHTTPErrorHandler 在将错误转换为有效的 JSON 响应时,在后台执行了大量工作——尽可能尝试使用它。

HTTP 头和 gRPC 元数据

gRPC 和 Restful/JSON 以不同的方式传递元数据。

在 Restful/JSON HTTP 中,标头用于发送 HTTP 头,而 gRPC 通过根据所使用的语言提供元数据接口来抽象发送元数据。

gRPC 网关提供了一个简单的映射接口来将 gRPC 元数据转换为 HTTP 标头,反之亦然。它还允许使用两种不同的方法来处理标头到元数据的转换。

首先,WithOutgoingHeaderMatcher 处理从 gRPC 网关返回到客户端的标头。它将元数据转换为 HTTP 标头(即,任何由 gRPC 服务传递的元数据都将作为 HTTP 标头发送回客户端)。

var allowedHeaders=map[string]struct{}{
    "x-request-id": {},
}
func isHeaderAllowed(s string)( string,bool) {
// check if allowedHeaders contain the header
    if _,isAllowed:=allowedHeaders[s];isAllowed {
// send uppercase header
       return strings.ToUpper(s),true
    }
// if not in the allowed header, don't send the header
     return s, false
}
// usage
mux:=runtime.NewServeMux(
// convert header in response(going from gateway) from metadata received.
runtime.WithOutgoingHeaderMatcher(isHeaderAllowed))
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

此方法接受一个字符串,如果将标头传递给客户端,则返回 true,否则返回 false。

其次,WithMetadata处理传入的 HTTP 标头(即 cookie、内容类型等)。它最常见的用例是获取身份验证令牌并将其传递给元数据。此处提取的 HTTP 标头将在元数据中发送到 gRPC 服务。

mux := runtime.NewServeMux(
  // handle incoming headers
  runtime.WithMetadata(func(ctx context.Context, request *http.Request) metadata.MD {
  header := request.Header.Get("Authorization")
  // send all the headers received from the client
  md := metadata.Pairs("auth",header)
  return md
}),
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

它接受一个请求并返回元数据的函数。请注意转换为元数据的标头,因为客户端、浏览器、负载均衡器和 CDN 都在其中。gRPC 的密钥也有一些限制。

这是一个完整的例子:

package main
import (
    "context"
    "log"
    "net"
    "net/http"
    "strings"
    "github.com/felixge/httpsnoop"
    "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    "google.golang.org/grpc"
    "google.golang.org/grpc/metadata"
    gen "grpc-gateway-demo/gen/go/hello"
)
func withLogger(handler http.Handler) http.Handler {
    return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
        m:=httpsnoop.CaptureMetrics(handler,writer,request)
        log.Printf("http[%d]-- %s -- %s\n",m.Code,m.Duration,request.URL.Path)
    })
}
var allowedHeaders=map[string]struct{}{
    "x-request-id": {},
}
func isHeaderAllowed(s string)( string,bool) {
    // check if allowedHeaders contain the header
    if _,isAllowed:=allowedHeaders[s];isAllowed {
        // send uppercase header
        return strings.ToUpper(s),true
    }
    // if not in the allowed header, don't send the header
    return s, false
}
func main() {
    // creating mux for gRPC gateway. This will multiplex or route request different gRPC service
    mux := runtime.NewServeMux(
        // convert header in response(going from gateway) from metadata received.
        runtime.WithOutgoingHeaderMatcher(isHeaderAllowed),
        runtime.WithMetadata(func(ctx context.Context, request *http.Request) metadata.MD {
            header:=request.Header.Get("Authorization")
            // send all the headers received from the client
            md:=metadata.Pairs("auth",header)
            return md
        }),
        runtime.WithErrorHandler(func(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, writer http.ResponseWriter, request *http.Request, err error) {
            //creating a new HTTTPStatusError with a custom status, and passing error
            newError:=runtime.HTTPStatusError{
                HTTPStatus: 400,
                Err:        err,
            }
            // using default handler to do the rest of heavy lifting of marshaling error and adding headers
            runtime.DefaultHTTPErrorHandler(ctx,mux,marshaler,writer,request,&newError)
        }))
    // setting up a dail up for gRPC service by specifying endpoint/target url
    err := gen.RegisterGreeterHandlerFromEndpoint(context.Background(), mux, "localhost:8080", []grpc.DialOption{grpc.WithInsecure()})
    if err != nil {
        log.Fatal(err)
    }
    // Creating a normal HTTP server
    server:=http.Server{
        Handler: withLogger(mux),
    }
    // creating a listener for server
    l,err:=net.Listen("tcp",":8081")
    if err!=nil {
        log.Fatal(err)
    }
    // start server
    err = server.Serve(l)
    if err != nil {
        log.Fatal(err)
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.
  • 69.
  • 70.
  • 71.

查询参数

默认支持查询参数。你可以使用消息定义中的相同键将它们添加到路径中。因此,如果你 HelloResponse 中有一个名为 last_name 的密钥,你可以输入路径 v1/sayHello/anshul?last_name=goyal 而无需更改网关代码中的任何内容。

自定义响应gRPC-Gateway 允许你在原始案例或 camelCase 中自定义响应中的键。。默认情况下它是 camelCase,但你可以编辑 Marshaler 配置来更改它。

mux := runtime.NewServeMux(runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.HTTPBodyMarshaler{
            Marshaler: &runtime.JSONPb{
                MarshalOptions: protojson.MarshalOptions{
                    UseProtoNames:   true,
                    EmitUnpopulated: true,
                },
                UnmarshalOptions: protojson.UnmarshalOptions{
                    DiscardUnknown: true,
                },
            },
        }),)
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

6.将 gRPC-Gateway 与 Gin 一起使用

Gin 是一个非常流行的 Go web 框架。你可以将 gRPC-Gateway 与 Gin 一起使用,因为它只是一个处理程序。它将允许你在你的服务器上添加可能不是由 gRPC-Gateway 生成的其他路由。

package main
import (
    "context"
    "log"
    "net/http"
    "strings"
    "github.com/gin-gonic/gin"
    "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    "google.golang.org/grpc"
    "google.golang.org/grpc/metadata"
    gen "grpc-gateway-demo/gen/go/hello"
)
var allowedHeaders=map[string]struct{}{
    "x-request-id": {},
}
func isHeaderAllowed(s string)( string,bool) {
    // check if allowedHeaders contain the header
    if _,isAllowed:=allowedHeaders[s];isAllowed {
        // send uppercase header
        return strings.ToUpper(s),true
    }
    // if not in the allowed header, don't send the header
    return s, false
}
func main() {
    // creating mux for gRPC gateway. This will multiplex or route request different gRPC service
    mux:=runtime.NewServeMux(
        // convert header in response(going from gateway) from metadata received.
        runtime.WithOutgoingHeaderMatcher(isHeaderAllowed),
        runtime.WithMetadata(func(ctx context.Context, request *http.Request) metadata.MD {
            header:=request.Header.Get("Authorization")
            // send all the headers received from the client
            md:=metadata.Pairs("auth",header)
            return md
        }),
        runtime.WithErrorHandler(func(ctx context.Context, mux *runtime.ServeMux, marshaler runtime.Marshaler, writer http.ResponseWriter, request *http.Request, err error) {
            //creating a new HTTTPStatusError with a custom status, and passing error
            newError:=runtime.HTTPStatusError{
                HTTPStatus: 400,
                Err:        err,
            }
            // using default handler to do the rest of heavy lifting of marshaling error and adding headers
            runtime.DefaultHTTPErrorHandler(ctx,mux,marshaler,writer,request,&newError)
        }))
    // setting up a dail up for gRPC service by specifying endpoint/target url
    err := gen.RegisterGreeterHandlerFromEndpoint(context.Background(), mux, "localhost:8080", []grpc.DialOption{grpc.WithInsecure()})
    if err != nil {
        log.Fatal(err)
    }
    // Creating a normal HTTP server
    server:=gin.New()
    server.Use(gin.Logger())
    server.Group("v1/*{grpc_gateway}").Any("",gin.WrapH(mux))
    // additonal route
    server.GET("/test", func(c *gin.Context) {
        c.String(http.StatusOK,"Ok")
    })

    // start server
    err = server.Run(":8081")
    if err != nil {
        log.Fatal(err)
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.

只需使用 gin.WrapH 带有通配符路径的方法,你就可以在服务器上使用 gin 了。如果需要,它允许你添加到服务器的路由。你还可以使用 HandlePath 将路由直接添加到 ServerMux 。

err = mux.HandlePath("GET", "test", func(w http.ResponseWriter, r *http.Request, pathParams map[string]string) {
    w.Write([]byte("ok")
})
  • 1.
  • 2.
  • 3.

7.在同一端口上运行反向代理和 gRPC 服务

可以在一个端口上运行这两种服务。你可以通过使用 cmux 包来做到这一点。

cmux 将通过区分使用的协议来拆分 gRPC 流量和 RestFull/JSON,因为 gRPC 将使用 HTTP2,而 RestFull/JSON 将使用 HTTP1。

package main
import (
    "context"
    "fmt"
    "log"
    "net"
    "net/http"
    "github.com/felixge/httpsnoop"
    "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
    "github.com/soheilhy/cmux"
    // importing generated stubs
    gen "grpc-gateway-demo/gen/go/hello"
    "google.golang.org/grpc"
)
// GreeterServerImpl will implement the service defined in protocol buffer definitions
type GreeterServerImpl struct {
    gen.UnimplementedGreeterServer
}
// SayHello is the implementation of RPC call defined in protocol definitions.
// This will take HelloRequest message and return HelloReply
func (g *GreeterServerImpl) SayHello(ctx context.Context, request *gen.HelloRequest) (*gen.HelloReply, error) {
    if err:=request.Validate();err!=nil {
        return nil,err
    }
    return &gen.HelloReply{
        Message: fmt.Sprintf("hello %s %s",request.Name,request.LastName),
    },nil
}
func main() {
    // create new gRPC server
    grpcSever := grpc.NewServer()
    // register the GreeterServerImpl on the gRPC server
    gen.RegisterGreeterServer(grpcSever, &GreeterServerImpl{})
    // creating mux for gRPC gateway. This will multiplex or route request different gRPC service
    mux:=runtime.NewServeMux()
    // setting up a dail up for gRPC service by specifying endpoint/target url
    err := gen.RegisterGreeterHandlerFromEndpoint(context.Background(), mux, "localhost:8081", []grpc.DialOption{grpc.WithInsecure()})
    if err != nil {
        log.Fatal(err)
    }
    // Creating a normal HTTP server
    server:=http.Server{
        Handler: withLogger(mux),
    }
    // creating a listener for server
    l,err:=net.Listen("tcp",":8081")
    if err!=nil {
        log.Fatal(err)
    }
    m := cmux.New(l)
    // a different listener for HTTP1
    httpL := m.Match(cmux.HTTP1Fast())
    // a different listener for HTTP2 since gRPC uses HTTP2
    grpcL := m.Match(cmux.HTTP2())
    // start server
    // passing dummy listener
    go server.Serve(httpL)
    // passing dummy listener
    go grpcSever.Serve(grpcL)
    // actual listener
    m.Serve()
}
func withLogger(handler http.Handler) http.Handler {
    return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
        m:=httpsnoop.CaptureMetrics(handler,writer,request)
        log.Printf("http[%d]-- %s -- %s\n",m.Code,m.Duration,request.URL.Path)
    })
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
  • 40.
  • 41.
  • 42.
  • 43.
  • 44.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
  • 60.
  • 61.
  • 62.
  • 63.
  • 64.
  • 65.
  • 66.
  • 67.
  • 68.

8.结论

本教程解释了为你的 gRPC 服务构建出色的 gRPC-Gateway 反向代理所需的所有要素。

自 gRPC-Gateway 以来,ServerMux 现在只是一个处理程序,你可以通过添加更多中间件(如主体压缩、身份验证和恐慌处理)来构建它。

你还可以使用 gRPC 网关配置。所有的代码示例都可以在这里[6]找到。

原文链接:https://dev.to/logrocket/an-all-in-one-guide-to-grpc-gateway-4g11

参考资料:

[1]官方 gRPC 网站: https://grpc.io/docs/protoc-installation/

[2]官方: https://go.dev/doc/install

[3]你可以按照官方网站: https://docs.buf.build/installation

[4]使用 Gin 来演示这一点: https://blog.logrocket.com/building-microservices-go-gin/

[5]链接: https://github.com/grpc-ecosystem/grpc-gateway/blob/7094a052b3287b9e99f52d95230789ab34d2d7c4/runtime/errors.go#L36

[6]这里: https://github.com/anshulrgoyal/grpc-gateway-demo