bigywater.tech
gRPC · 21

反射:客户端怎么知道服务有哪些方法

没有 .proto 文件时,grpcurl 之类的工具怎么知道服务有哪些方法、请求长什么样?这篇用 grpc-go 的反射服务,自己写了一个迷你版:列服务、查描述符、动态调用。

4 个服务
reflection.Register 之后 ListServices 返回的服务数(含反射服务自己的 v1 与 v1alpha)
1 个 FileDescriptorProto
查询 demo.Demo 得到的描述符个数,足以列出 4 个方法与 2 个消息
NOT_FOUND = 5
查不存在的符号时 ErrorResponse 里的错误码(RPC 本身成功)

反射是什么

实测 · 本机执行输出
  v1 ListServices:err=<nil>;demo.Demo grpc.health.v1.Health grpc.reflection.v1.ServerReflection grpc.reflection.v1alpha.ServerReflection
  v1alpha ListServices:err=<nil>/<nil>;demo.Demo grpc.health.v1.Health grpc.reflection.v1.ServerReflection grpc.reflection.v1alpha.ServerReflection
  自定义 Services 只含 demo.Demo:err=<nil>;demo.Demo

服务端再注册一个 grpc.reflection 服务,把自己编译进二进制的 .proto 描述告诉客户端。客户端因此不需要 .proto 文件就能列出服务、方法和消息结构。

怎么拿到描述符

实测 · 本机执行输出
  err=<nil>;返回的文件描述符 1 个
  文件 pb/demo.proto(package demo,依赖 [])
      Demo.Unary(.demo.Req) returns (.demo.Resp)
      Demo.ServerStream(.demo.Req) returns (stream .demo.Resp)
      Demo.ClientStream(stream .demo.Req) returns (.demo.Resp)
      Demo.Bidi(stream .demo.Req) returns (stream .demo.Resp)
      message Req { msg=1(STRING), n=2(INT32), payload=3(BYTES) }
      message Resp { msg=1(STRING), n=2(INT32), payload=3(BYTES) }

用 FileContainingSymbol 按符号名(如 demo.Demo)请求,返回序列化的 FileDescriptorProto。客户端解析后就能知道方法的输入输出类型、是否是流、字段的名字和编号。

动态调用

实测 · 本机执行输出
  err=<nil>;响应(protojson){"msg":"echo:hello", "n":7}
  请求的 Go 类型 *dynamicpb.Message,响应的 Go 类型 *dynamicpb.Message(都是由反射返回的描述符构造的 dynamicpb.Message,不是生成的 pb.Req / pb.Resp)

有了描述符就能用 dynamicpb 构造消息,再用 conn.Invoke 按方法全名调用,不需要任何生成的代码。grpcurl 就是这么工作的。

错误与流

实测 · 本机执行输出
  err=<nil>;ErrorResponse code=5 message="proto:\u00a0not found"
  同一个流里问 3 次:客户端发出 HEADERS 1 个;开 3 个流各问 1 次:HEADERS 3 个

找不到符号时,RPC 是成功的,错误在响应的 ErrorResponse 里;反射是双向流,一个流里可以连续问多次。

分步演示

反射
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1 / grpc-go v1.84.0 / protobuf-go v1.36.12 / golang.org/x/net v0.57.0 / protoc 36.2(Homebrew);服务端和客户端都是本机起的 Go 进程(127.0.0.1,端口 9800 以上,不访问任何外部服务);需要看线上字节时用我自己写的 TCP 抓包代理加 golang.org/x/net/http2 的 Framer 解析 HTTP/2 帧;计时用 time.Now 或 perf_counter,数字每次运行会有小幅抖动。反射服务用 google.golang.org/grpc/reflection;客户端用 reflection v1 的客户端接口自己发请求,descriptor 用 protobuf-go 的 protodesc / dynamicpb 解析,没有用 grpcurl;HEADERS 个数来自抓包代理;“Register 同时注册 v1 与 v1alpha”读自 reflection/serverreflection.go
    • 没有验证:grpcurl / grpcui 等真实工具;依赖多个 .proto 文件时按依赖顺序取文件(FileByFilename);扩展(extension)相关的查询(AllExtensionNumbersOfType 等);反射的安全暴露面(只演示了可以限制公开的服务,没有测权限);反射对服务端启动时间或内存的影响
    京ICP备2026067502号-1京公网安备11010502063487号