HTTP 与 gRPC 共存:对外 REST、对内 RPC

浏览器、移动端、第三方开放接口只认 HTTP + JSON,服务之间你希望用 gRPC 拿性能与契约;两套协议并存不是妥协,而是分工:边缘(edge)说 REST,内部说 gRPC。grpc-gateway 让一份 .proto 同时生成 gRPC 服务端骨架与 REST 反向代理,接口定义只有一处,不必两套接口各改一半。

为什么分层

位置协议原因
浏览器 / 小程序HTTP + JSON浏览器拿不到 HTTP/2 trailer,无法直接说 gRPC;gRPC-Web 还需要额外代理
第三方开放平台REST + JSON + OpenAPI文档、调试工具、SDK 生态成熟,签名鉴权好做
服务与服务gRPC + protobuf二进制体积小、强类型契约、原生支持流式、代码可生成

一句话记:对外求通用,对内求效率。网关只做协议转换,不放业务逻辑;内部服务之间的调用不该绕道 REST,那等于白白多两次序列化。

用注解定义 REST 映射

protoc-gen-grpc-gateway 读取 .proto 里的 google.api.http 注解,生成 *_pb.gw.go,内容是一个标准库 http.Handler,收到 REST 请求后转成 gRPC 客户端调用(默认直连远端,也可同进程直调):

curl/浏览器 --HTTP+JSON--> gateway mux(*_pb.gw.go)--gRPC--> 业务服务
syntax = "proto3";
package im.v1;
import "google/api/annotations.proto";
option go_package = "example.com/im/gen/im/v1;imv1";

service UserService {
  rpc GetUser(GetUserRequest) returns (User) {
    option (google.api.http) = {
      get: "/v1/users/{user_id}"            // 路径参数按同名字段绑定
      additional_bindings { get: "/v1/me" } // 一个方法暴露多个路径
    };
  }
  rpc ListUsers(ListUsersRequest) returns (ListUsersResponse) {
    option (google.api.http) = { get: "/v1/users" }; // 其余字段自动来自 query
  }
}

绑定规则要背下来:{user_id} 从路径取、其余字段从 query string 取;body: "*" 表示整个请求体反序列化成 message,body: "user" 表示只映射到 user 字段;additional_bindings 用来挂短路径或兼容旧版本;字段默认同时接受 camelCase 与 snake_case,需要别的名字用 json_name。

生成与挂载

# 需要的插件:protoc-gen-go、protoc-gen-go-grpc、protoc-gen-grpc-gateway、protoc-gen-openapiv2
protoc -I . -I third_party/googleapis \
  --go_out=gen --go_opt=paths=source_relative --go-grpc_out=gen --go-grpc_opt=paths=source_relative \
  --grpc-gateway_out=gen --grpc-gateway_opt=paths=source_relative \
  --openapiv2_out=docs --openapiv2_opt=allow_merge=true \
  proto/im/v1/user.proto

google/api/annotations.proto 要放进 third_party/ 或用 buf 依赖(buf.build/googleapis/googleapis),否则 protoc 直接报找不到。用 buf 的项目写进 buf.gen.yaml,一条 buf generate 出全部产物。挂载方式如下:

mux := runtime.NewServeMux(
    runtime.WithIncomingHeaderMatcher(func(k string) (string, bool) {
        switch http.CanonicalHeaderKey(k) {
        case "Authorization", "X-Request-Id", "X-Trace-Id":
            return k, true // metadata key 会被转成小写
        default: return runtime.DefaultHeaderMatcher(k)
        }
    }),
)
// 直连远端服务;同进程可换 RegisterUserServiceHandlerServer
if err := imv1.RegisterUserServiceHandlerFromEndpoint(ctx, mux, "127.0.0.1:9000", opts); err != nil {
    log.Fatal(err)
}
log.Fatal(http.ListenAndServe(":8080", mux))

Authorization 是特例:不显式加进白名单,下游拦截器就从 metadata 里读不到令牌,鉴权会全量 401。traceId 同理,前端传的 X-Request-Id 要能在 gRPC 侧日志里看到同一个值。

错误映射

gRPC 状态码必须翻译成 HTTP 状态码,否则前端只会收到一坨 500。

runtime.HTTPError = func(ctx context.Context, mux *runtime.ServeMux, m runtime.Marshaler,
    w http.ResponseWriter, _ *http.Request, err error) {
    st := status.Convert(err)
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(runtime.HTTPStatusFromCode(st.Code()))
    _ = json.NewEncoder(w).Encode(map[string]any{
        "code": int(st.Code()), "message": st.Message(),
        "details": st.Details(), // 保留 BadRequest.FieldViolation 等详情
    })
}
gRPC 状态码默认 HTTP 码说明
InvalidArgument / OutOfRange400参数问题,前端可直接提示
Unauthenticated401未登录或令牌过期,触发刷新
PermissionDenied403已登录但无权限,别提示「请登录」
NotFound404资源不存在
AlreadyExists / Aborted409冲突,配合幂等键重试
ResourceExhausted429限流,回 Retry-After
Unavailable / DeadlineExceeded503 / 504可重试类,但要先确认幂等
Internal / Unknown500别把内部堆栈与 SQL 原样吐给前端

流式接口怎么暴露

服务端流在 REST 上没有对应语义,grpc-gateway 以 chunked 分块 JSON 持续写出(每个对象包一层 {"result":...}),前端用 fetch 读 response.body 流即可;长连接推送更适合 SSE 或 WebSocket,另写 handler,不要硬套 gateway。客户端流与双向流别指望 REST 映射,改成「分片上传 + 一次提交」这类设计。

与其它方案的分工

方案适合代价
grpc-gateway已有 .proto,一份定义两套协议,Go 技术栈需维护 HTTP 注解,调试多一层
手写 REST handler(gin/echo)REST 资源建模复杂、文件上传、管理后台两套 DTO,改一处漏一处
Envoy / nginx 转码多语言服务、统一网关入口配置复杂,注解仍要写在 proto 里

落地顺序:内部服务先把 gRPC 跑顺,边缘用 grpc-gateway 暴露最小 REST 面(登录、列表、详情、提交),文件上传与管理后台继续手写 handler,两者在前置 nginx 上按路径分流。

上线检查清单

检查项通过标准
路径版本REST 路径带 /v1,与 proto package 版本一致
错误体前端能拿到稳定的 code/message/details,无内部堆栈
鉴权令牌能从 HTTP header 进 metadata,下游拦截器实测读得到
请求体大小gateway、后端 MaxRecvMsgSize、nginx client_max_body_size 三者一致
超时链客户端超时 < 网关超时 < 服务端 deadline,避免级联堆积
观测日志与 trace 里 REST 路径与 gRPC 方法能对上

小结:把「对外 REST、对内 gRPC」当固定边界,用 google.api.http 注解让一份 .proto 同时产出 gRPC 骨架与 REST 代理;重点盯住 metadata 透传、错误码映射、超时链与请求体大小四处;流式接口按 REST 的能力重新设计,别硬套。

笔记加载中…