Envoy Mobile HTTP 流式 API 实战:RequestHeaders、StreamPrototype 与 Stream 全解析
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
在 Envoy Mobile 中,HTTP 流(Stream)是一等公民:无论是下载大文件、接收服务器推送的数据,还是进行双向通信,都可以通过统一的流式接口完成;传统的 Unary 请求(单请求 / 单响应)也复用同一套类型,只需在写完请求后主动关闭流即可。本篇基于 mobile/docs/root/api/http.rst 官方 API 文档,结合仓库内 Kotlin / Swift / C++ 源码与集成测试,系统讲解 HTTP 流的创建、配置、发送、关闭与取消全流程,读完即可在 Android(Kotlin)与 iOS(Swift)两端写出可运行的 HTTP 流式客户端代码。
一、核心概念:为什么 Stream 是一等公民
Envoy Mobile 的公开 API 有一个明确目标:平台间一致性。因此文档按功能分组,并在每个小节同时给出 iOS(Swift)与 Android(Kotlin)示例(见 mobile/docs/root/api/api.rst)。
HTTP 请求在 Envoy Mobile 中统一抽象为「流」,其基本链路如下:
- 通过
EngineBuilder构建Engine,再调用streamClient()拿到StreamClient(详见 starting_envoy.rst); - 用
RequestHeadersBuilder构造RequestHeaders; - 由
StreamClient.newStreamPrototype()创建StreamPrototype,在启动前挂载响应回调; - 调用
StreamPrototype.start(...)得到Stream,通过它发送 headers / data / trailers,或关闭、取消流。
该链路的接口定义在源码中非常清晰:
- Kotlin 侧:StreamClient.kt 定义
interface StreamClient { fun newStreamPrototype(): StreamPrototype }; - Swift 侧:StreamClient.swift 定义等价的
protocol StreamClient。
也就是说,StreamClient本身不直接发起请求,它只负责孵化「流原型」,这与 Envoy 内核中「请求先建模、后执行」的思想一致。
二、快速开始:启动并交互一个 HTTP 流
官方文档给出了可直接运行的 Kotlin / Swift 快速示例,核心步骤是:构建 StreamClient → 构建 RequestHeaders → 创建原型 → 注册回调 → start → 发送。
Kotlin(Android)示例:
val streamClient = AndroidStreamClientBuilder(application).build() val headers = RequestHeadersBuilder(method = RequestMethod.POST, scheme = "https", authority = "api.envoyproxy.io", path = "/foo") .build() val stream = streamClient .newStreamPrototype() .setOnResponseHeaders { headers, endStream -> Log.d("MainActivity", "[${headers.httpStatus}] Headers received: $headers, end stream: $endStream") } .setOnResponseData { data, endStream -> Log.d("MainActivity", "Received data, end stream: $endStream") } .setOnResponseTrailers { trailers -> Log.d("MainActivity", "Trailers received: $trailers") } .setOnError { ... } .setOnCancel { ... } .start(Executors.newSingleThreadExecutor()) .sendHeaders(...) .sendData(...) // ... stream.close(...)Swift(iOS)示例:
let headers = RequestHeadersBuilder(method: .post, scheme: "https", authority: "api.envoyproxy.io", path: "/foo") .build() let streamClient = try StreamClientBuilder().build() let stream = streamClient .newStreamPrototype() .setOnResponseHeaders { headers, endStream in print("[\(headers.httpStatus)] Headers received: \(headers), end stream: \(endStream)") } .setOnResponseData { data, endStream in print("Received data, end stream: \(endStream)") } .setOnResponseTrailers { trailers in print("Trailers received: \(trailers)") } .setOnError { ... } .setOnCancel { ... } .start(queue: .main) .sendHeaders() .sendData(...) // ... stream.close(...)注意两端的两个差异点:
- 回调线程:Kotlin 侧通过
start(executor)指定回调执行的Executor;Swift 侧通过start(queue:)指定DispatchQueue,默认是.main。源码 StreamPrototype.kt 中start(executor: Executor? = null)允许为空,此时回调直接投递到引擎线程。 httpStatus:响应头对象(ResponseHeaders)提供了httpStatus属性,可直接读取 HTTP 状态码,用于快速判断请求结果。
三、RequestHeaders 与 RequestHeadersBuilder:构造请求头
创建流的入口是初始化一个RequestHeaders实例,途径是RequestHeadersBuilder,然后把它交给之前创建的StreamClient。构造器需要四个参数:method(请求方法)、scheme(URL scheme,如https)、authority(URL 权威部分,如api.envoyproxy.io)、path(URL 路径,如/foo)。
Kotlin:
val headers = RequestHeadersBuilder(RequestMethod.POST, "https", "api.envoyproxy.io", "/foo") .addRetryPolicy(RetryPolicy(...)) .addUpstreamHttpProtocol(UpstreamRequestProtocol.HTTP2) .add("x-custom-header", "foobar") // ... .build()Swift:
let headers = RequestHeadersBuilder(method: .post, scheme: "https", authority: "api.envoyproxy.io", path: "/foo") .addRetryPolicy(RetryPolicy(...)) .addUpstreamHttpProtocol(.http2) .add(name: "x-custom-header", value: "foobar") // ... .build()从源码看,RequestHeadersBuilder的构造器(RequestHeadersBuilder.kt、RequestHeadersBuilder.swift)会将四个参数映射为 HTTP/2 伪头字段存入内部的 headers 容器:
| 参数 | 映射头字段 | 示例值 |
|---|---|---|
authority | :authority | api.envoyproxy.io |
method | :method | POST |
path | :path | /foo |
scheme | :scheme | https |
scheme参数带默认值https,因此大多数场景可以省略。Kotlin 侧RequestMethod枚举(RequestMethod.kt)完整支持DELETE、GET、HEAD、OPTIONS、PATCH、POST、PUT、TRACE八种方法。
Builder 还提供通用的头操作链式方法:add(name, value)追加单值、set(name, list)覆盖多值、remove(name)删除,以及 Kotlin 端特有的addSocketTag(uid, tag)——通过x-envoy-mobile-socket-tag内部头把流量统计的 UID 与 tag 打到 socket 上(Android 数据用量统计场景)。addUpstreamHttpProtocol(...)则用于指定上游使用的 HTTP 协议版本(如 HTTP/2),最终作用于请求发送时的协议协商。
四、StreamPrototype:启动前配置流
StreamPrototype表示「尚未启动的流」,由StreamClient创建,用于在start()之前把响应回调绑定到流上。它的核心价值是:把「请求如何被响应」这件事,在流启动前一次性声明好。
Kotlin:
val prototype = streamClient .newStreamPrototype() .setOnResponseHeaders { headers, endStream -> Log.d("MainActivity", "[${headers.httpStatus}] Headers received: $headers, end stream: $endStream") } .setOnResponseData { data, endStream -> Log.d("MainActivity", "Received data, end stream: $endStream") } .setOnResponseTrailers { trailers -> Log.d("MainActivity", "Trailers received: $trailers") } .setOnError { ... } .setOnCancel { ... }Swift:
let prototype = streamClient .newStreamPrototype() .setOnResponseHeaders { headers, endStream in print("[\(headers.httpStatus)] Headers received: \(headers), end stream: \(endStream)") } .setOnResponseData { data, endStream in print("Received data, end stream: \(endStream)") } .setOnResponseTrailers { trailers in print("Trailers received: \(trailers)") } .setOnError { ... } .setOnCancel { ... }对照源码 StreamPrototype.kt(Swift 见 StreamPrototype.swift),原型上可配置的完整回调集如下:
setOnResponseHeaders(headers, endStream, streamIntel):收到响应头时触发;若endStream == true表示这是仅头响应(headers-only),流即将完成;setOnResponseData(data, endStream, streamIntel):收到响应体数据帧时触发;endStream == true表示最后一帧;setOnResponseTrailers(trailers, streamIntel):收到响应尾随头(trailers)时触发;setOnError(error, finalStreamIntel):内部 Envoy 异常时触发,流就此结束;setOnCancel(finalStreamIntel):流被取消时触发;setOnComplete(finalStreamIntel):流正常完成时触发;setOnSendWindowAvailable(streamIntel):显式流控模式下发送窗口恢复可用时触发。
其中streamIntel/finalStreamIntel是流信息对象,携带请求 / 响应各阶段的可观测数据(如尝试次数、耗时等)。除文档列出的五个回调外,原型还支持两个重要配置:
setExplicitFlowControl(enabled):开启显式流控。开启后调用方需提供缓冲区接收响应体;若缓冲区小于可用数据,回调会暂停,底层网络协议可能向服务器发出停止发送的信号,直到有更多空间。好处是限制响应内存占用,代价是吞吐量可能下降。- Kotlin 端
setUseByteBufferPosition(enabled):决定发送ByteBuffer时数据长度取position()还是capacity()(见下文 Stream 一节)。
五、RetryPolicy:定制请求重试规则
RetryPolicy用于定制出站请求的重试规则,通过RequestHeadersBuilder.addRetryPolicy(...)挂到请求上,在请求头发送时生效。其核心语义与 Envoy 的重试机制一致(自动重试、重试语义、指数退避等均由 Envoy 路由层实现)。
从源码 RetryPolicy.kt 可以看到它的完整参数:
| 参数 | 含义 | 默认值 |
|---|---|---|
maxRetryCount | 请求允许的最大重试次数 | 必填 |
retryOn | 触发重试的规则列表(RetryRule枚举) | 必填 |
retryStatusCodes | 额外的、应当重试的 HTTP 状态码列表 | 空列表 |
perRetryTimeoutMS | 单次重试的超时(毫秒),为正数时不得超过totalUpstreamTimeoutMS | null |
totalUpstreamTimeoutMS | 包含所有重试的总超时(毫秒),覆盖「下游请求处理完成」到「上游响应完全处理完成」的整个区间;为null或0表示禁用 | 15000 |
构造器带参数校验:若perRetryTimeoutMS为正数且大于totalUpstreamTimeoutMS(且后者非 0),会直接抛出IllegalArgumentException,防止配置出「单次重试比总超时还长」的矛盾策略。
RetryRule枚举支持的规则与 Envoy 的x-envoy-retry-on头一一对应:
STATUS_5XX(5xx):响应为 5xx 状态码时重试;GATEWAY_ERROR(gateway-error):网关类错误;CONNECT_FAILURE(connect-failure):连接失败;REFUSED_STREAM(refused-stream):流被拒绝(HTTP/2);RETRIABLE_4XX(retriable-4xx):可重试的 4xx 错误;RETRIABLE_HEADERS(retriable-headers):响应头本身可重试;RESET(reset):连接被重置。
实现上,RetryPolicy通过outboundHeaders()把这些规则翻译成x-envoy-max-retries、x-envoy-retry-on、x-envoy-retriable-status-codes、x-envoy-upstream-rq-per-try-timeout-ms、x-envoy-upstream-rq-timeout-ms等 Envoy 标准头注入请求;反向地,RetryPolicy.from(headers)可以从已有RequestHeaders中还原策略对象(x-envoy-retry-on的多值会被逗号拆分后逐一映射回枚举)。
六、Stream:启动、发送、关闭与取消
流通过StreamPrototype.start()启动,返回一个Stream对象,发送方用它与网络进行交互。
Kotlin:
val streamClient = AndroidStreamClientBuilder() // ... .build() val requestHeaders = RequestHeadersBuilder() // ... .build() val prototype = streamClient .newStreamPrototype() // ... val stream = prototype .start(Executors.newSingleThreadExecutor()) .sendHeaders(...) .sendData(...) // ... stream.close(...)Swift:
let streamClient = StreamClientBuilder() // ... .build() let requestHeaders = RequestHeadersBuilder() // ... .build() let prototype = streamClient .newStreamPrototype() // ... let stream = prototype .start(queue: .main) .sendHeaders(...) .sendData(...) // ... stream.close(...)对照 Stream.kt,Stream提供以下操作(除特别说明外均返回自身,支持链式调用):
sendHeaders(headers, endStream, idempotent = false):发送请求头。endStream为true表示 headers-only 请求;idempotent表示请求是否幂等——置为true时,Envoy Mobile 会在 HTTP/3 握手后失败的情况下自动重试(默认false)。文档的快速示例中 Kotlin 侧调用sendHeaders(...)为单参形式,即等价于endStream = false的普通发送。sendData(data):发送请求体ByteBuffer/Data。长度默认取capacity,若原型开启了setUseByteBufferPosition(true)则取position;传入的缓冲区不会被修改,但流关闭前对缓冲区的任何改动都可能导致不可预期的结果。readData(byteCount):显式流控模式下主动读取响应数据,byteCount是下一次 data 回调可携带的最大字节数,调用后立即返回。close(trailers)/close(data):分别用尾随头或最后一个数据帧关闭流(即结束请求方向)。close(data)等价于发送endStream = true的数据帧。cancel():取消整个流。
仓库中的 C++ 集成测试完整覆盖了这些操作:如 send_headers_test.cc、send_data_test.cc、send_trailers_test.cc 验证请求方向的发送,receive_headers_test.cc、receive_data_test.cc、receive_trailers_test.cc 验证响应方向的回调,可作为「每个 API 如何被底层引擎消费」的实现参考。
七、Unary 请求:把流当一次往返用
如前所述,Unary 请求复用流的全部类型,唯一区别是:写完 headers / data / trailers 后立即关闭流,让请求方向结束,随后在响应回调中接收完整响应。
Kotlin:
val streamClient = AndroidStreamClientBuilder() // ... .build() val requestHeaders = RequestHeadersBuilder() // ... .build() val stream = streamClient .newStreamPrototype() .start(Executors.newSingleThreadExecutor()) // Headers-only stream.sendHeaders(requestHeaders, true) // Close with data stream.close(ByteBuffer(...)) // Close with trailers stream.close(RequestTrailersBuilder().build()) // Cancel the stream stream.cancel()Swift:
let streamClient = StreamClientBuilder() // ... .build() let requestHeaders = RequestHeadersBuilder() // ... .build() let stream = streamClient .newStreamPrototype() .start(queue: .main) // Headers-only stream.sendHeaders(requestHeaders, endStream: true) // Close with data stream.close(Data(...)) // Close with trailers stream.close(RequestTrailersBuilder().build()) // Cancel the stream stream.cancel()四种「收尾」方式的适用场景:
- Headers-only(
sendHeaders(headers, endStream = true)):请求只有头没有体,如典型的GET,发出即代表请求方向结束; - Close with data:请求体较大时,把最后一个数据块连同「结束」标记一起发送;
- Close with trailers:请求需要携带尾随头(如分块元数据)时使用;
- Cancel:主动放弃请求,例如用户中途退出页面,此时不会再产生任何响应回调。
RequestTrailersBuilder与RequestHeadersBuilder结构对称,同样支持add/set/remove链式操作后build()(见 RequestTrailersBuilder.kt)。
八、StreamClient 从哪来:与 Engine 的关系
本文大量使用streamClient,它的获取方式在 starting_envoy.rst 中有完整说明:先通过EngineBuilder(Android 为AndroidEngineBuilder)配置并构建Engine,再调用engine.streamClient()取得可复用的StreamClient,之后所有网络请求都通过它发起:
val streamClient = AndroidEngineBuilder(getApplication()) .setLogLevel(LogLevel.WARN) // ... .build() .streamClient()let streamClient = try EngineBuilder() .setLogLevel(.warn) // ... .build() .streamClient()EngineBuilder还支持连接超时、DNS 刷新策略、HTTP/3(QUIC)、Gzip/Brotli 解压、xDS 动态配置、日志与事件追踪等一系列引擎级配置;若默认配置不够用,还可以直接传入自定义 Envoy YAML 配置(AndroidEngineBuilder(context, Yaml(yamlString))/EngineBuilder(yaml:)),但官方特别提醒:自定义 YAML 要到运行时才会被求值,且并非所有 Envoy 核心配置项都被 Envoy Mobile 支持,使用不当可能导致运行时崩溃。
九、小结与最佳实践
- 先建原型,再启流:所有响应回调必须在
start()之前通过StreamPrototype声明完毕,流一旦启动便不可再追加回调; - Unary 与流式用同一套 API:Unary 只是「写完即关」,流式则保持
Stream打开并持续sendData/ 在setOnResponseData中消费数据; - 重试交给 Envoy:通过
RetryPolicy声明式配置重试规则,由 Envoy 在请求头发送时统一注入并执行,业务层无需自建重试逻辑; - 留意线程模型:回调线程由
start时的Executor(Kotlin)或DispatchQueue(Swift)决定,UI 更新类回调建议传入主线程队列; - 大响应考虑显式流控:
setExplicitFlowControl(true)+readData(byteCount)可以在内存敏感场景下限制单次回调携带的数据量。
以上内容与接口签名均可对照仓库源码验证:Swift 侧实现见 StreamClient.swift 与 StreamPrototype.swift,Kotlin 侧见 StreamClient.kt、StreamPrototype.kt 与 Stream.kt,端到端行为可由 mobile/test/cc/integration 下的集成测试进一步佐证。
【免费下载链接】envoyCloud-native high-performance edge/middle/service proxy项目地址: https://gitcode.com/GitHub_Trending/en/envoy
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考