前言

虽然 gRPC 已经比较成熟了,但是在前端领域的兼容性问题依然存在,目前的解决方案就两种,一种是在后端使用 RESTful API 服务对 gRPC 服务再做一层封装,另一种方法是直接在前端使用 gRPC-Web 之类的项目,实现前端直接调用 gRPC!

为了让项目既能够支持 RESTful API 又能够支持 gRPC,人们创造了 gRPC-Gateway 这个项目,主要就是为了解决 RESTful 反向代理 gRPC 服务路由的问题。

无论哪种方式,对于刚接触的开发者来说,都有一定的心智负担。后来在一个开源项目中我发现他们使用的是有 Buf 开源的 ConnectRPC 项目来实现这一层的兼容。

ConnectRPC 的 slogan 是:

Connect is a family of libraries for building browser and gRPC-compatible APIs. If you’re tired of hand-written boilerplate and turned off by massive frameworks, Connect is for you.

安装所需工具

mkdir example
cd example
go mod init example
go install github.com/bufbuild/buf/cmd/buf@latest
go install github.com/fullstorydev/grpcurl/cmd/grpcurl@latest
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest

定义服务

创建一个 greet/v1/greet.proto 文件,内容如下:

syntax = "proto3";

package greet.v1;

option go_package = "example/gen/greet/v1;greetv1";

message GreetRequest {
  string name = 1;
}

message GreetResponse {
  string greeting = 1;
}

service GreetService {
  rpc Greet(GreetRequest) returns (GreetResponse) {}
}

生成代码

首先为项目生成 buf.yaml:

version: v2
plugins:
  - local: protoc-gen-go
    out: gen
    opt: paths=source_relative
  - local: protoc-gen-connect-go
    out: gen
    opt: paths=source_relative

然后运行 buf generate 命令生成代码:

gen
└── greet
    └── v1
        ├── greet.pb.go
        └── greetv1connect
            └── greet.connect.go

实现业务逻辑

package main

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

    "connectrpc.com/connect"
    "golang.org/x/net/http2"
    "golang.org/x/net/http2/h2c"

    greetv1 "example/gen/greet/v1" // generated by protoc-gen-go
    "example/gen/greet/v1/greetv1connect" // generated by protoc-gen-connect-go
)

type GreetServer struct{}

func (s *GreetServer) Greet(ctx context.Context, req *connect.Request[greetv1.GreetRequest]) (*connect.Response[greetv1.GreetResponse], error) {
    log.Println("Request headers: ", req.Header())
    res := connect.NewResponse(&greetv1.GreetResponse{
        Greeting: fmt.Sprintf("Hello, %s!", req.Msg.Name),
    })
    res.Header().Set("Greet-Version", "v1")
    return res, nil
}

func main() {
    greeter := &GreetServer{}
    mux := http.NewServeMux()
    path, handler := greetv1connect.NewGreetServiceHandler(greeter)
    mux.Handle(path, handler)

    // Use h2c so we can serve HTTP/2 without TLS.
    http.ListenAndServe("localhost:8080", h2c.NewHandler(mux, &http2.Server{}))
}

安装依赖并运行

go get golang.org/x/net/http2
go get connectrpc.com/connect
go run ./cmd/server/main.go

发送请求

下面是基于 RESTFul 的 JSON 请求:

curl \
    --header "Content-Type: application/json" \
    --data '{"name": "Jane"}' \
    http://localhost:8080/greet.v1.GreetService/Greet

下面是基于 gRPC 的 Protobuf 请求:

grpcurl \
    -protoset <(buf build -o -) -plaintext \
    -d '{"name": "Jane"}' \
    localhost:8080 greet.v1.GreetService/Greet

总结

到这里,基本上就已经完成了对 RESTful API 和 gRPC 的兼容,但是默认的路由与 RESTful API 规范是背道而驰的,要想做到符合 RESTful API 的规范,还得使用 connectrpc/vanguard-go 这个库。

Vanguard 是一个功能强大的 Go net/http 服务器库,可实现 REST 和 RPC 协议之间的无缝转码。无论您需要弥合 gRPC、gRPC-Web、Connect 还是 REST 之间的差距,Vanguard 都能满足您的需求。借助对 Google 的 HTTP 转码选项的支持,它可以使用强类型 Protobuf 定义轻松转换协议。

与 gRPC-Gateway 等传统方法不同,Vanguard 可在 Go 服务器中高效运行,并与 Connect 和 gRPC 等各种服务器兼容。它不依赖于广泛的代码生成,因此无需额外的代码生成步骤。这种灵活性可确保您的代码能够动态适应,从配置、架构注册表或通过 gRPC 服务器反射加载服务定义,使其非常适合代理,而无需在每次 RPC 服务架构更改时重新编译和重新部署。