让 gRPC 服务同时支持 HTTP/JSON 的gRPC-Gateway

0 阅读3分钟

让 gRPC 服务同时支持 HTTP/JSON 的gRPC-Gateway

gRPC 基于 HTTP/2 和 Protobuf,性能优秀但二进制协议不便于前端或传统 HTTP 客户端直接调用。

gRPC-Gateway 是 Google 官方开源的插件,它通过 Protobuf 扩展注解,自动将 gRPC 服务转换为 RESTful HTTP/JSON 接口,让一套服务同时支持 gRPC 和 HTTP 两种调用方式,是微服务架构中解决 gRPC 兼容性问题的首选方案。

什么是 gRPC-Gateway?

gRPC-Gateway 是一个反向代理,它读取 Protobuf 文件中的 google.api.http 注解,自动生成 HTTP 接口代码,将 HTTP 请求转换为 gRPC 请求转发给后端服务,再将 gRPC 响应转换为 JSON 返回给客户端。

工作流程

  1. 定义 Protobuf:在 gRPC 服务定义中添加 google.api.http 注解,指定 HTTP 方法、路径和请求体映射。
  2. 生成代码:使用 protoc-gen-grpc-gateway 插件生成 HTTP 网关代码。
  3. 启动服务:同时启动 gRPC 服务和 gRPC-Gateway HTTP 服务。
  4. 请求处理:客户端发送 HTTP 请求 → Gateway 转换为 gRPC 请求 → 后端 gRPC 服务处理 → Gateway 转换为 JSON 响应 → 返回客户端。

编写 Protobuf 文件

这是 gRPC-Gateway 最关键的一步,通过 google.api.http 注解定义 HTTP 接口映射。

导入解析

// 导入 google/api/annotations.proto(必须)
import "google/api/annotations.proto";

定义消息

message User {
  uint32 id = 1;
  string username = 2;
  string email = 3;
}

message GetUserRequest { uint32 user_id = 1; }
message GetUserResponse { User user = 1; }

message CreateUserRequest {
  string username = 1;
  string email = 2;
}
message CreateUserResponse { User user = 1; }

定义 gRPC 服务

service UserService {

  // 1. GET 请求:路径参数映射
  rpc GetUser(GetUserRequest) returns (GetUserResponse) {
    // google.api.http 注解:定义 HTTP 接口
    option (google.api.http) = {
      get: "/api/v1/user/{user_id}" // {user_id} 映射到 GetUserRequest.user_id
    };
  }

  // 2. POST 请求:请求体映射
  rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {
    option (google.api.http) = {
      post: "/api/v1/user"
      body: "*" // "*" 表示整个请求体映射到 CreateUserRequest
    };
  }
}
注解参数作用示例
get/post/put/delete/patch指定 HTTP 方法get: "/api/user/{id}"
{field_name}路径参数映射到消息字段{user_id}GetUserRequest.user_id
body: "*"整个 HTTP 请求体映射到消息body: "*"
body: "field_name"请求体映射到消息的指定字段body: "user"

生成 Go 代码

# 在项目根目录执行,生成 gRPC 代码和 Gateway 代码
protoc -I . \
  --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 \
  proto/user.proto

生成文件:

  • user.pb.go:消息结构
  • user_grpc.pb.go:gRPC 服务接口
  • user_gw.pb.go:gRPC-Gateway HTTP 接口(核心)

实现 gRPC-Gateway

gRPC 服务端(可跳过):注意:需要异步启动 gRPC 服务

type UserServer struct {
	pb.UnimplementedUserServiceServer
}

// 实现 GetUser 方法
func (s *UserServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
	// 核心业务逻辑:查询数据库等
	user := &pb.User{
		Id:       req.UserId,
		Username: "zhangsan",
		Email:    "zhangsan@example.com",
	}
	return &pb.GetUserResponse{User: user}, nil
}

// 实现 CreateUser 方法
func (s *UserServer) CreateUser(ctx context.Context, req *pb.CreateUserRequest) (*pb.CreateUserResponse, error) {
	// 核心业务逻辑:创建用户等
	user := &pb.User{
		Id:       1,
		Username: req.Username,
		Email:    req.Email,
	}
	return &pb.CreateUserResponse{User: user}, nil
}

func main() {
	// 启动 gRPC 服务(逻辑简化)
	lis, _ := net.Listen("tcp", ":50051")
	s := grpc.NewServer()
	pb.RegisterUserServiceServer(s, &UserServer{})
	go s.Serve(lis) // 异步启动 gRPC 服务

	// 启动 Gateway HTTP 服务(见下文)
	// ...
}

启动Gateway服务

func main() {
	// 1. 异步启动 gRPC 服务(见上文)
	// ...

	// 2. 创建 Gateway mux
	gwMux := runtime.NewServeMux()

	// 3. 配置 gRPC 服务端连接(开发环境跳过 TLS)
	opts := []grpc.DialOption{
	grpc.WithTransportCredentials(insecure.NewCredentials()),
	}

	// 4. 注册 UserService 的 Gateway handler
	err := pb.RegisterUserServiceHandlerFromEndpoint(
		context.Background(),
		gwMux,
		"localhost:50051", // gRPC 服务端地址
		opts,
	)
	if err != nil {
		panic(err)
	}

	// 5. 启动 HTTP 服务器
	http.ListenAndServe(":8080", gwMux)
}
语法说明
runtime.NewServeMux()创建 Gateway 的 HTTP 路由 mux
pb.RegisterXxxServiceHandlerFromEndpoint(ctx, mux, endpoint, opts)注册 gRPC 服务的 Gateway handler
http.ListenAndServe(addr, mux)启动 Gateway HTTP 服务

自定义 HTTP 响应格式

默认的 Gateway 响应格式可能不符合项目规范(如需要统一的 code/msg/data 结构),可以通过自定义 Marshaler 实现。

// 自定义响应格式
type CustomResponse struct {
	Code int    `json:"code"`
	Msg  string `json:"msg"`
	Data any    `json:"data"`
}

// 自定义 Marshaler(逻辑简化,实际需实现完整接口)
func main() {
	// 创建 Gateway mux 时使用自定义 Marshaler
	gwMux := runtime.NewServeMux(
		runtime.WithMarshalerOption(runtime.MIMEWildcard, &CustomJSONMarshaler{}),
	)
	// ... 后续代码不变
}