ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Envoy gRPC-JSON Transcoder 过滤器实战:让 RESTful JSON API 无缝对接 gRPC 服务

2026/9/13 15:17:26 拓冰建站 浏览量
Envoy gRPC-JSON Transcoder 过滤器实战:让 RESTful JSON API 无缝对接 gRPC 服务 Envoy gRPC-JSON Transcoder 过滤器实战让 RESTful JSON API 无缝对接 gRPC 服务【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoyEnvoy 的 gRPC-JSON transcoderenvoy.filters.http.grpc_json_transcoder过滤器允许 RESTful JSON API 客户端通过 HTTP 向 Envoy 发起请求由 Envoy 将其转码transcode为 gRPC 调用并代理到后端的 gRPC 服务同时把 gRPC 响应还原为 JSON。本文以 官方配置文档 为主线结合仓库中的 proto 定义、过滤器源码与集成测试完整讲解该过滤器的 JSON 映射规则、proto descriptor set 生成、路由配置、google.api.HttpBody任意内容下发、透传头部以及全部配置参数的语义与取值帮助读者搭建一个「同时接受 gRPC 与 RESTful JSON 请求」的统一入口。过滤器定位与工作原理gRPC-JSON transcoder 是一个 HTTP 过滤器其配置类型 URL 为type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder对应的 v3 API 定义见 transcoder.proto注册名为envoy.filters.http.grpc_json_transcoder参见 config.h。它解决的问题是gRPC 服务只能被 gRPC 客户端调用而基于 HTTP/1.1 的 RESTful JSON API 客户端浏览器、curl、传统后端无法直接调用 gRPC。该过滤器位于 HTTP 连接管理器HCM的过滤链中位于 Router 过滤器之前执行双向转码请求方向decode把符合google.api.http注解所定义 HTTP 映射的 RESTful JSON 请求转码为 gRPC 请求JSON → protobuf再交给 Router 转发到 gRPC 上游响应方向encode把 gRPC 服务返回的 protobuf 消息转码为 JSONprotobuf → JSON写回给下游 HTTP 客户端。gRPC 服务的 HTTP 映射必须通过 google.api.http 就展示了完整用法例如rpc GetShelf(GetShelfRequest) returns (Shelf) { option (google.api.http) { get: /shelves/{shelf} }; }表示GET /shelves/{shelf}会被映射到GetShelfRPC路径中的{shelf}变量会绑定到请求消息的shelf字段。在源码层面过滤器的转码状态机由 json_transcoder_filter.cc 实现核心类GrpcJsonTranscoderFilter重写了decodeHeaders、decodeData、decodeTrailers与encodeHeaders、encodeData、encodeTrailers等回调见 json_transcoder_filter.h内部通过Grpc::Decoder与Transcoder完成双向的协议转换。protobuf 与 JSON 的映射规则转码过程中的 protobuf 到 JSON 映射遵循 proto3 JSON 映射规范proto3 JSON Mapping。需要注意两个流式stream场景的特殊约定gRPC 流式请求参数对于 client-streaming / bidi-streaming RPCEnvoy 期望下游以JSON 消息数组的形式发送请求体一个元素对应一条 stream 消息gRPC 流式响应参数对于 server-streaming / bidi-streaming RPCEnvoy 默认以JSON 消息数组的形式返回响应。也就是说流式 RPC 在 HTTP 侧表现为「请求发数组、响应收数组」。此外通过 print_options 中的stream_newline_delimited与stream_sse_style_delimited选项可以改变流式响应的输出格式详见下文配置参数详解。如何生成 proto descriptor set要进行转码Envoy 必须知道 gRPC 服务的proto descriptor即服务的方法签名、消息字段、HTTP 映射。因此需要先用protoc为 gRPC 服务生成 descriptor set 文件。由于要定义 HTTP 映射proto 文件会import google/api/annotations.proto所以编译前需要先克隆 googleapis 仓库把annotations.proto放入 include path$ git clone https://github.com/googleapis/googleapis $ GOOGLEAPIS_DIRyour-local-googleapis-folder然后用protoc生成 descriptor set。以 Envoy 仓库自带的测试用 bookstore.proto 为例$ protoc -I${GOOGLEAPIS_DIR} -I. --include_imports --include_source_info \ --descriptor_set_outproto.pb test/proto/bookstore.proto命令要点-I${GOOGLEAPIS_DIR} -I.include path 必须同时包含 googleapis 目录与 proto 源文件所在目录缺一不可--include_imports把依赖的annotations.proto、http.proto、httpbody.proto等一并打入 descriptor setEnvoy 转码时才能解析 HTTP 映射--include_source_info把 proto 源文件信息含注释写入 descriptor set便于调试与错误定位--descriptor_set_outproto.pb指定输出文件名多个 proto 源文件可以一次性全部传入同一个命令例如protoc ... a.proto b.proto c.proto最终合并为一个 descriptor set。生成后将proto.pb文件路径或二进制内容配置给过滤器的proto_descriptor/proto_descriptor_bin字段即可。注意descriptor set 是「部署期编译产物」proto 服务定义一旦变更新增 RPC、改 HTTP 映射、增删字段需要重新生成并加载。被转码请求的路由配置转码请求的路由配置应与 gRPC 路由保持一致。原因在于经过 transcoder 处理后请求的路径会被改写为/package.service/method形式方法固定为POST。因此路由匹配必须针对这个 gRPC 风格的路径即/包名.服务名/方法名而不是下游传入的原始 RESTful 路径。这样同一组路由可以同时服务「纯 gRPC 请求」与「gRPC-JSON 转码请求」。例如下面的 proto见 helloworld.protosyntax proto3; package helloworld; import google/api/annotations.proto; // The greeting service definition. service Greeter { // Sends a greeting rpc SayHello(HelloRequest) returns (HelloReply) { option (google.api.http) { get: /say }; } } // The request message containing the users name. message HelloRequest { string name 1; } // The response message containing the greetings message HelloReply { string message 1; }SayHello的 HTTP 映射为GET /say但转码后 Router 看到的路径是/helloworld.Greeter/SayHello。因此路由配置中prefix: /say是匹配不到该请求的必须写成prefix: /helloworld.Greeter或精确匹配/helloworld.Greeter/SayHello才能同时命中原生 gRPC 客户端发来的POST /helloworld.Greeter/SayHelloRESTful 客户端发来的GET /say经 transcoder 改写后的内部路径。匹配原始路径match_incoming_request_route如果你希望路由直接按下游传入的原始请求路径如/say匹配可以设置match_incoming_request_route: true。启用后过滤器在把出站请求头改写为 gRPC 服务格式后仍保留进入时的路由。需要注意该字段的语义限制见 transcoder.proto 注释这意味着未被转码的 gRPC 服务路由无法与match_incoming_request_route组合使用在按路由per-route配置 transcoder 时语义相反per-route 场景下先匹配路由再应用过滤器因此应匹配原始路径而全局配置场景下应匹配 gRPC 风格路径过滤器改写后的路径。这一差异在 proto 注释中有明确说明transcoder.proto。路由匹配失败时的行为默认情况下请求无法映射到services中指定的任何服务时过滤器会静默透传pass-through不做转码services列表为空时过滤器视为禁用。若开启 request_validation_options.reject_unknown_method则无法匹配的请求会被直接拒绝并返回HTTP 404 Not Found。发送任意内容google.api.HttpBody默认情况下转码发生时 gRPC-JSON 会把 gRPC 服务方法的输出消息编码为 JSON并将 HTTP 响应的Content-Type设置为application/json。如果 gRPC 服务方法想返回任意内容图片、文件、HTML 等非 JSON 数据可以将其输出消息类型声明为 google.api.HttpBody实现方需要设置content_type字段对应 HTTP 响应头Content-Type的值data字段对应 HTTP 响应体。在 server-streaming 场景下gRPC 服务端可以连续发送多个google.api.HttpBody此时 HTTP 响应头Content-Type取第一个HttpBody的content-type。仓库的 bookstore.proto 导入了google/api/httpbody.proto作为示例参考过滤器的实现中还提供了专门的HttpBody处理逻辑 http_body_utils.cc用于在转码输出阶段组装任意内容响应。转码请求携带的头部gRPC-JSON 会向 gRPC 服务端转发以下两个头部携带原始 HTTP 请求的信息x-envoy-original-path原始 HTTP 请求路径x-envoy-original-method原始 HTTP 请求方法。gRPC 服务端可以利用这两个头部感知「这次调用最初来自哪个 RESTful 端点、用了什么 HTTP 动词」例如用于审计日志或流量统计。完整示例 Envoy 配置下面是一个可直接运行的示例配置原始文件见 grpc-transcoder-filter.yaml它代理到运行在localhost:50051的 gRPC 服务。监听端口51051同时支持两种调用方式直接发 gRPC 请求或发 RESTful JSON 请求二者都会被转码并转发到 gRPC 后端。admin: address: socket_address: address: 0.0.0.0 port_value: 9901 static_resources: listeners: - name: listener1 address: socket_address: address: 0.0.0.0 port_value: 51051 filter_chains: - filters: - name: envoy.filters.network.http_connection_manager typed_config: type: type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager stat_prefix: grpc_json codec_type: AUTO route_config: name: local_route virtual_hosts: - name: local_service domains: [*] routes: - match: prefix: /helloworld.Greeter route: cluster: grpc timeout: 60s http_filters: - name: envoy.filters.http.grpc_json_transcoder typed_config: type: type.googleapis.com/envoy.extensions.filters.http.grpc_json_transcoder.v3.GrpcJsonTranscoder proto_descriptor: protos/helloworld.pb services: - helloworld.Greeter print_options: add_whitespace: true always_print_primitive_fields: true always_print_enums_as_ints: false preserve_proto_field_names: false - name: envoy.filters.http.router typed_config: type: type.googleapis.com/envoy.extensions.filters.http.router.v3.Router clusters: - name: grpc type: LOGICAL_DNS lb_policy: ROUND_ROBIN dns_lookup_family: V4_ONLY typed_extension_protocol_options: envoy.extensions.upstreams.http.v3.HttpProtocolOptions: type: type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions explicit_http_config: http2_protocol_options: {} load_assignment: cluster_name: grpc endpoints: - lb_endpoints: - endpoint: address: socket_address: address: host.docker.internal port_value: 50051配置要点逐项说明监听器listener0.0.0.0:51051为统一入口端口同时接收 gRPC 与 RESTful JSON 流量路由route_config按上文原则使用prefix: /helloworld.Greeter匹配转码后以及原生 gRPC的路径超时设为60s过滤链顺序grpc_json_transcoder必须位于envoy.filters.http.router之前转码后的请求由 Router 负责转发到上游集群proto_descriptor: protos/helloworld.pb指向按前文命令生成的 descriptor set 文件路径对应 helloworld.proto生成命令见下节services: [helloworld.Greeter]声明需要转码的完整服务名包名.服务名descriptor 中可以包含更多服务但只有列出的才会被转码print_options控制响应 JSON 的输出格式详见下文上游集群clusterLOGICAL_DNSROUND_ROBIN通过explicit_http_config.http2_protocol_options显式启用 HTTP/2因为 gRPC 依赖 HTTP/2端点地址示例使用host.docker.internal以便在 Docker 中访问宿主机50051端口的 gRPC 服务注释提醒Docker v18.03.0 之前需改用docker.for.mac.localhost。若 proto 文件以protos/helloworld.proto保存可按如下命令生成其 descriptor set前提已按前文克隆 googleapis$ protoc -I$(GOOGLEAPIS_DIR) -I. --include_imports --include_source_info \ --descriptor_set_outprotos/helloworld.pb protos/helloworld.proto过滤器完整配置参数详解除示例中的字段外GrpcJsonTranscoder还提供丰富的可调参数全部定义见 transcoder.protodescriptor 与 services必选字段类型说明proto_descriptorstringdescriptor set 的文件路径oneof与proto_descriptor_bin二选一必须设置其一proto_descriptor_binbytesdescriptor set 的二进制内容适合动态下发场景servicesrepeated string完整服务名列表包名.服务名。服务名不存在于 descriptor 中时 Envoy启动即失败descriptor 中多余的服务不会被转码。列表为空视为过滤器禁用print_options响应 JSON 输出控制对应 Google 的JsonPrintOptions直接透传底层实现字段默认值说明add_whitespacefalse是否添加空格、换行与缩进使 JSON 输出易于阅读always_print_primitive_fieldsfalse是否总是输出原始类型字段。默认情况下值为默认值如 int32 的 0的原始字段会被省略开启后无条件输出always_print_enums_as_intsfalse是否总是以整数形式输出枚举。默认渲染为字符串preserve_proto_field_namesfalse是否保留 proto 字段原名。默认按json_name或 lower camel case 生成 JSON 字段名开启后保留原名stream_newline_delimitedfalse流式响应是否以「换行分隔的 JSON 消息」输出替代默认的逗号分隔数组stream_sse_style_delimitedfalse是否强制使用 Server-Sent EventsSSE消息帧格式data: message\n\n。开启时stream_newline_delimited被忽略request_validation_options严格请求校验默认情况下transcoder 对无法转码的请求静默透传包括未知 query 参数、未注册路径等gRPC 请求始终透传不转码。开启这些选项可改为拒绝并返回明确的HTTP 4xx避免「透传给 gRPC 上游后被 TCP reset下游只收到 503」这种误导性错误字段行为reject_unknown_methodtrue 时无法映射到任何指定 service 的请求返回HTTP 404 Not Foundreject_unknown_query_parameterstrue 时无法映射到请求消息的 query 参数导致请求被拒返回HTTP 400 Bad Request优先级低于ignore_unknown_query_parameters、capture_unknown_query_parameters与ignored_query_parametersreject_binding_body_field_collisions默认绑定binding如路径/query 变量与请求体body字段冲突时以 body 为准true 时若两者值不一致则拒绝请求路由与路径相关字段说明match_incoming_request_routetrue 时按原始请求路径匹配路由见前文路由配置章节的语义限制auto_mapping是否允许转码没有google.api.http选项的方法。开启后客户端可对/bookstore.Bookstore/GetShelfRequest发送 JSON body{shelf: 1234}调用未定义 HTTP 映射的 RPCmatch_unregistered_custom_verb是否匹配未注册的 custom verbHTTP template 末尾的: LITERAL。例如:baz未注册时默认不视为 custom verb 从而可匹配/foo/{x*}开启后视为 custom verb将因模板无 custom verb 而不再匹配query 参数处理字段说明ignored_query_parameters指定要忽略的 query 参数名列表。默认任何未知/非法 query 参数都会使转码失败透传例如GetShelf的映射get: /shelves/{shelf}收到/shelves/100?foobar时因foo无绑定而无法映射把foo加入该列表后即可正常映射ignore_unknown_query_parameterstrue 时忽略所有无法映射到 protobuf 字段的 query 参数无法预先枚举参数名时使用默认 falsecapture_unknown_query_parameterstrue 时无法映射到 protobuf 字段的 query 参数会被捕获到HttpBody扩展UnknownQueryParams中见 transcoder.protoUnknownQueryParams.key为参数名到值列表的映射query_param_unescape_plustrue 时提取 query 参数变量时把还原为空格支持 HTML 2.0 的表单编码约定URL 反转义策略url_unescape_spec该策略仅作用于多段multiple segments路径变量的提取。例如/foo/{x*}/bar/{yprefix/*}/{z**}中x是单段变量y、z是多段变量对于路径/foo/first/bar/prefix/second/third/fourth得到xfirst、yprefix/second、zthird/fourth。三个取值默认ALL_CHARACTERS_EXCEPT_RESERVED取值行为示例对%2f%23/%20%2523反转义ALL_CHARACTERS_EXCEPT_RESERVED默认不解码 RFC 6570 保留字符%2f%23/ %23ALL_CHARACTERS_EXCEPT_SLASH完全 URI 解码但单段匹配的保留展开中%2F保持编码%2f#/ %23ALL_CHARACTERS完全 URI 解码/#/ %23错误码转码与大小限制字段说明convert_grpc_statustrue 时把 gRPC 状态头转码为 JSON 错误响应。当 trailer 指示 gRPC 错误且无 HTTP body 时优先取grpc-status-details-bin头中的google.rpc.Status作为 JSON body无该头时由grpc-status、grpc-message构造google.rpc.Status。要求错误详情类型如google.rpc.RequestInfo来自google/rpc/error_details.proto包含在 descriptor set 中。例如上游返回grpc-status: 5及 base64 编码的grpc-status-details-bin时下游得到HTTP/1.1 404 Not Found与 body{code:5,details:[{type:type.googleapis.com/google.rpc.RequestInfo,requestId:r-1}]}case_insensitive_enum_parsingtrue 时允许 JSON 请求使用非大写枚举值proto 规范要求枚举在 JSON 中大写max_request_body_size可转码请求体最大字节数必须 0。超限返回HTTP 413 Request Entity Too Large过大值在高并发下可能占用大量内存未设置时使用当前 stream buffer 大小max_response_body_size可转码响应体最大字节数必须 0。超限返回HTTP 500 Internal Server Error未设置时使用当前 stream buffer 大小关于 body 大小限制源码中有对应的decoderBufferLimitReached/encoderBufferLimitReached处理见 json_transcoder_filter.h分别触发 413 与 500 响应与 proto 注释行为一致。源码实现层面的纵深理解透传判定content-type 是第一道闸门过滤器并非对所有请求都转码。在 json_transcoder_filter.cc 中可以看到如果请求头content-type是application/grpc原生 gRPC 请求过滤器会记录日志并直接透传不进行 JSON 转码响应侧同理json_transcoder_filter.cc非application/grpccontent-type 的响应直接透传。这就解释了为什么同一监听端口能同时服务 gRPC 与 RESTful JSON 两种协议——「协议识别 选择性转码」在过滤器内部完成。双向转码的完整调用链从 json_transcoder_filter.h 可以梳理出过滤器的处理链路decode 方向decodeHeaders完成请求方法与路径的改写RESTful → gRPC、变量绑定与头部处理decodeData逐块消费请求体把 JSON 解析为 protobuf 消息decodeTrailers收尾encode 方向encodeHeaders改写响应头Content-Type: application/json等encodeData把上游 protobuf 响应序列化为 JSONencodeTrailers处理 gRPC trailer结合convert_grpc_status输出错误 JSON。转码核心依赖Grpc::Decodertranscoder_input_stream_impl.cc提供了输入流适配而 JSON 编解码能力由底层 protobuf util 提供。配置加载与 per-route 能力通过 config.cc 与 config.h 可以看到该过滤器既支持全局per-filter配置也支持per-route / per-virtual-host 配置createRouteSpecificFilterConfigTyped遵循「取最具体配置」的原则。按路由配置时的注意事项先匹配路由、后应用过滤器因此 per-route 场景下路由应匹配原始路径而全局场景应匹配 gRPC 风格路径详见 transcoder.proto 注释。集成测试验证仓库提供了端到端集成测试 grpc_json_transcoder_integration_test.cc覆盖了 RESTful 请求转码、streaming 响应、HttpBody 内容、错误状态转码等核心路径并配合 bookstore.proto其中包含BulkCreateShelf(stream ...) returns (stream ...)等流式 RPC 定义验证数组形式的流式 JSON 编解码。这些测试是理解过滤器行为边界的第一手资料也适合作为新 proto 服务接入时的对照模板。使用建议与注意事项路由匹配务必使用 gRPC 风格路径/package.service/method除非显式开启match_incoming_request_route同时保证 HTTP 映射get/post/put等与路由前缀不冲突descriptor set 要与服务版本保持一致RPC 或 HTTP 映射变更后需重新生成services中声明了 descriptor 不存在的服务名会导致 Envoy 启动失败按需开启严格校验生产环境建议评估request_validation_options把「透传后上游 reset、下游只见 503」的模糊故障变成明确的 400/404 响应流式接口的 JSON 形态请求/响应均为 JSON 数组除非设置stream_newline_delimited或stream_sse_style_delimitedSSE 场景任意内容用google.api.HttpBody输出消息类型改为HttpBody后content_type与data直接决定 HTTP 响应头与响应体server-streaming 下可连续发送多个Content-Type取第一个上游必须启用 HTTP/2gRPC 依赖 HTTP/2集群侧需通过explicit_http_config.http2_protocol_options显式配置如示例配置所示关注 body 大小限制高并发或大消息场景下合理设置max_request_body_size/max_response_body_size避免内存峰值。借助 gRPC-JSON transcoder团队可以在保留 gRPC 高性能内部通信的同时为外部客户端提供标准 RESTful JSON 接口一套 Envoy 配置即可完成协议桥接与统一代理。【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考