grpc GateWay

gRPC 虽然性能强大,但它使用 HTTP/2Protobuf(二进制) 协议。这导致了一个大问题:浏览器、Postman、前端 JavaScript 无法直接调用 gRPC 接口

gRPC Gateway 就是为了解决这个问题而生的。

第一部分:什么是 gRPC Gateway?(通俗版)

想象你开了一家高档餐厅(gRPC 服务)

  • 内部厨房:只说“专业术语”(Protobuf),只用“内部对讲机”(HTTP/2)。效率高,但外人听不懂。
  • 外部顾客:说“大众语言”(JSON),用“手机点餐”(HTTP/1.1)。

gRPC Gateway 就是“服务员/翻译官”

  1. 顾客发 HTTP/JSON 请求给 Gateway
  2. Gateway 把 JSON 翻译成 Protobuf,把 HTTP 转成 gRPC。
  3. Gateway 发给 gRPC 服务
  4. 拿到结果后,再翻译回 JSON 给顾客。

核心价值

  • 对外:提供标准的 RESTful HTTP/JSON 接口(兼容浏览器、移动端)。
  • 对内:微服务之间依然用高效的 gRPC 通信。
  • 一份代码:只需写一次 .proto 文件,同时生成 gRPC 代码和 HTTP 接口代码。
  • 自动生成文档:可以自动生成 Swagger/OpenAPI 文档。

第二部分:环境准备(关键步骤)

gRPC Gateway 需要额外的插件。请确保完成以下安装。

1. 安装插件

1
2
3
4
5
# 1. 安装 gateway 插件 (生成反向代理代码)
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest

# 2. 安装 openapi 插件 (生成 Swagger 文档)
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2@latest

3. 获取 Google API 定义

Gateway 依赖 google/api/annotations.proto。你需要确保 protoc 能找到它。
最简单的方法是下载 googleapis 仓库:

1
2
3
# 在任意目录克隆 ( protoc 需要包含这个路径)
git clone https://github.com/googleapis/googleapis.git
# 记住这个路径,比如 /Users/yourname/googleapis

4. 初始化项目

1
2
3
4
5
6
mkdir grpc-gateway-demo
cd grpc-gateway-demo
go mod init example.com/grpc-gateway-demo
go get google.golang.org/grpc
go get google.golang.org/protobuf
go get github.com/grpc-ecosystem/grpc-gateway/v2

第三部分:定义 Proto 文件(核心变化)

这是与普通 gRPC 最大的不同。我们需要在 .proto 文件中添加 HTTP 映射注解

文件路径: proto/gateway.proto

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
syntax = "proto3";

package gateway;

// 1. 引入 google api 注解 (这是 Gateway 的灵魂)
import "google/api/annotations.proto";

message GetUserRequest {
string id = 1;
}

message User {
string id = 1;
string name = 2;
int32 age = 3;
}

message CreateUserRequest {
string name = 1;
int32 age = 2;
}

service UserService {
// 2. 定义 HTTP 映射
// get: 表示 HTTP GET 方法
// /v1/users/{id}: URL 路径,{id} 会映射到 GetUserRequest.id
rpc GetUser (GetUserRequest) returns (User) {
option (google.api.http) = {
get: "/v1/users/{id}"
};
}

// post: 表示 HTTP POST 方法
// body: "*": 表示整个 HTTP Body 映射到 CreateUserRequest
rpc CreateUser (CreateUserRequest) returns (User) {
option (google.api.http) = {
post: "/v1/users"
body: "*"
};
}
}

生成代码

运行以下命令(注意 I 参数指向 googleapis 路径):

1
2
3
4
5
6
7
# 假设 googleapis 在当前目录的上一级
protoc -I=. -I=../googleapis \
--go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
--grpc-gateway_out=. --grpc-gateway_opt=paths=source_relative \
--openapiv2_out=. --openapiv2_opt=paths=source_relative \
proto/gateway.proto

生成的文件:

  1. gateway.pb.go (消息定义)
  2. gateway_grpc.pb.go (gRPC 服务定义)
  3. gateway.pb.gw.go (Gateway 反向代理代码,重点!)
  4. gateway.swagger.json (Swagger 文档)

第四部分:完整 Demo 演示

我们将采用 单进程双端口 模式:

  • **gRPC 端口 (50051)**:供内部微服务调用。
  • **HTTP 端口 (8080)**:供 Gateway 监听,对外提供 REST 接口。

1. 实现 gRPC 业务逻辑 (server.go)

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
package main

import (
"context"
"example.com/grpc-gateway-demo/proto"
)

type UserServiceImpl struct {
proto.UnimplementedUserServiceServer
// 模拟数据库
users map[string]*proto.User
}

func NewUserServiceImpl() *UserServiceImpl {
return &UserServiceImpl{
users: map[string]*proto.User{
"1": {Id: "1", Name: "Alice", Age: 25},
},
}
}

// 实现 gRPC 方法
func (s *UserServiceImpl) GetUser(ctx context.Context, req *proto.GetUserRequest) (*proto.User, error) {
user, ok := s.users[req.Id]
if !ok {
return nil, grpc.Errorf(codes.NotFound, "user not found")
}
return user, nil
}

func (s *UserServiceImpl) CreateUser(ctx context.Context, req *proto.CreateUserRequest) (*proto.User, error) {
// 简单模拟生成 ID
newId := "100"
user := &proto.User{Id: newId, Name: req.Name, Age: req.Age}
s.users[newId] = user
return user, nil
}

2. 启动 gRPC 和 Gateway 服务 (main.go)

这是最关键的部分,将两者串联起来。

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
package main

import (
"context"
"log"
"net"
"net/http"
"example.com/grpc-gateway-demo/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
)

func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
defer cancel()

// ================= 1. 启动 gRPC 服务器 (内部) =================
grpcAddr := ":50051"
gs := grpc.NewServer()
proto.RegisterUserServiceServer(gs, NewUserServiceImpl())

go func() {
lis, err := net.Listen("tcp", grpcAddr)
if err != nil {
log.Fatalf("gRPC 监听失败:%v", err)
}
log.Printf("gRPC 服务启动在 %s", grpcAddr)
if err := gs.Serve(lis); err != nil {
log.Fatalf("gRPC 服务失败:%v", err)
}
}()

// ================= 2. 启动 Gateway 服务器 (外部 HTTP) =================
gwAddr := ":8080"
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithTransportCredentials(insecure.NewCredentials())}

// 关键:注册 Gateway Handler
// 它的作用是:收到 HTTP 请求 -> 转发给 grpcAddr (50051)
err := proto.RegisterUserServiceHandlerFromEndpoint(ctx, mux, "localhost"+grpcAddr, opts)
if err != nil {
log.Fatalf("注册 Gateway 失败:%v", err)
}

// 启动 HTTP 服务器
log.Printf("Gateway HTTP 服务启动在 %s", gwAddr)
log.Printf("Swagger 文档地址:http://localhost%s/swagger/ui/", gwAddr)
if err := http.ListenAndServe(gwAddr, mux); err != nil {
log.Fatalf("Gateway 服务失败:%v", err)
}
}

第五部分:测试与验证

启动程序后 (go run main.go),你可以用两种方式测试。

1. 使用 Curl 测试 HTTP 接口

Gateway 已经帮你把 HTTP 转成了 gRPC。

  • 测试 GetUser (GET 请求)

    1
    2
    curl http://localhost:8080/v1/users/1
    # 输出:{"id":"1","name":"Alice","age":25}
  • 测试 CreateUser (POST 请求)

    1
    2
    3
    4
    curl -X POST http://localhost:8080/v1/users \
    -H "Content-Type: application/json" \
    -d '{"name": "Bob", "age": 30}'
    # 输出:{"id":"100","name":"Bob","age":30}

2. 查看 Swagger UI

在浏览器打开 http://localhost:8080/swagger/ui/ (需要额外配置 swagger 服务,或者直接用 protoc 生成的 json 导入 Postman)。
注:为了简化 Demo,上面代码未直接挂载 swagger UI 文件,但生成了 gateway.swagger.json。你可以使用在线 Swagger Editor 导入该 JSON 查看文档。


第六部分:进阶技巧 (Proto 注解详解)

.proto 中,option (google.api.http) 非常灵活。

1. 路径参数 (Path Params)

1
2
3
4
5
6
// URL: /v1/users/123
rpc GetUser (GetUserRequest) returns (User) {
option (google.api.http) = {
get: "/v1/users/{id}" // {id} 自动映射到 request.id
};
}

2. 查询参数 (Query Params)

如果 URL 路径中没有定义的字段,会自动变成 Query 参数。

1
2
3
4
5
6
7
8
9
10
message ListUsersRequest {
int32 page = 1;
int32 page_size = 2;
}
// URL: /v1/users?page=1&page_size=10
rpc ListUsers (ListUsersRequest) returns (Users) {
option (google.api.http) = {
get: "/v1/users"
};
}

3. 请求体映射 (Body Mapping)

1
2
3
4
5
6
7
8
// body: "*" 表示整个 JSON Body 映射到请求消息
// body: "user" 表示 JSON 中的 { "user": { ... } } 映射到请求消息中的 user 字段
rpc CreateUser (CreateUserRequest) returns (User) {
option (google.api.http) = {
post: "/v1/users"
body: "*"
};
}

4. 忽略某个字段

如果你不想让某个字段通过 HTTP 暴露:

1
2
3
4
5
message Request {
string public_field = 1;
string internal_secret = 2;
}
// 在 http 配置中不映射 internal_secret,它就不会出现在 HTTP 请求中

第七部分:常见问题与最佳实践

1. 跨域问题 (CORS)

浏览器直接调用 Gateway (8080) 通常会报 CORS 错误。你需要在 HTTP 服务器层处理。
解决方案:使用 rs/cors 中间件包裹 mux

1
2
3
4
5
6
7
8
9
10
import "github.com/rs/cors"

// ...
c := cors.New(cors.Options{
AllowedOrigins: []string{"*"}, // 生产环境请指定具体域名
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowedHeaders: []string{"*"},
})
handler := c.Handler(mux)
http.ListenAndServe(gwAddr, handler)

2. 错误码映射

gRPC 的错误码(如 NotFound)会自动映射为 HTTP 状态码(如 404)。

  • OK -> 200
  • InvalidArgument -> 400
  • Unauthenticated -> 401
  • NotFound -> 404
  • Internal -> 500
  • 如果映射不符合预期,可以在 Gateway 配置中自定义 WithForwardResponseOption

3. 性能损耗

Gateway 增加了 HTTP->gRPC->HTTP 的转换过程,会有轻微的性能损耗(序列化/反序列化)。

  • 建议:内部微服务之间调用直接用 gRPC (50051),不要走 Gateway (8080)。Gateway 仅用于对外暴露。

4. 流式支持

早期的 gRPC Gateway 不支持流式 RPC。v2 版本支持了部分流式,但配置较复杂。

  • 建议:如果需要 WebSocket 或 SSE,建议单独实现,或者使用 gRPC-Web。

第八部分:总结归纳表

组件/概念 作用 关键配置/命令
protoc-gen-grpc-gateway 生成反向代理 Go 代码 go install ...@latest
google/api/annotations.proto 定义 HTTP 映射规则 需引入 googleapis 仓库
option (google.api.http) Proto 中的核心注解 定义 get, post, body
runtime.ServeMux Gateway 的路由处理器 mux := runtime.NewServeMux()
RegisterXxxHandlerFromEndpoint 将 gRPC 服务注册到 Gateway 指向 gRPC 地址 localhost:50051
protoc-gen-openapiv2 生成 Swagger 文档 生成 .swagger.json
CORS 解决浏览器跨域 使用 rs/cors 中间件
错误映射 gRPC Code -> HTTP Status 自动映射,可自定义

学习路线图

  1. 跑通 Demo:先确保上面的 main.go 能跑起来,Curl 能通。
  2. 理解注解:尝试修改 .proto 中的 get: "/v1/users/{id}",观察 URL 变化。
  3. 整合拦截器:之前的 gRPC 拦截器(认证、日志)在 Gateway 模式下依然有效,因为 Gateway 最终调用的是 gRPC 方法。你可以在 gRPC 层统一做鉴权。
  4. 生产部署:通常架构是 Nginx -> Gateway (HTTP) -> gRPC Service
[up主专用,视频内嵌代码贴在这]