Day 10 / 共 20 天 · 第 2 周收官

Codec 抽象(H1/H2)

HTTP/1、HTTP/2、HTTP/3 差异巨大,Envoy 怎么让上层代码"无感"地支持它们?答案是 codec 抽象——统一接口。今天看这层抽象,并用端到端 11 步串讲收官第 2 周。

📍 第 2 周收官 · 补上"字节↔HTTP"的最后一块,再用 11 步串讲全程
D6 L4 过滤器 D7 HCM D8 L7 过滤器 D9 Router D10 Codec(收官)
L01

Codec 是什么

🤔 痛点:HTTP/1 是文本、HTTP/2 是二进制帧,上层代码要为它们各写一套吗? Day 07 说 HCM 把字节 dispatch 给 codec 解析。但 HTTP/1.1(GET /x HTTP/1.1\r\n 纯文本、请求串行)和 HTTP/2(二进制帧、一条连接并发多请求)的"线上格式"天差地别。如果 HCM 和所有 L7 过滤器都要写"if 是 H1 这么处理、if 是 H2 那么处理",那代码会被协议差异撕成两半,还没法加 H3。
💡 本质:Codec = 配了"同声传译"的接待处(今天的类比世界观) 解法是加一层同声传译:不管客人说的是"H1 语"还是"H2 语",都由对应的翻译(H1 codec / H2 codec)翻成接待处内部统一的"普通话"(decodeHeaders/decodeData 这套统一回调)。接待处(HCM + L7 过滤器)只听普通话,根本不用知道客人原本说哪国话。换句话说:协议差异被 codec 这层"翻译"彻底吸收,上层代码一份就够、零改动支持多协议。这就是"面向接口编程"的威力。
Codec = "编解码器" Codec = coder + decoder(编码器 + 解码器)。解码:把网络字节流解析成结构化的 HTTP 请求(HCM 的 onData 靠它)。编码:把 HTTP 响应转回字节流写出去。难点在于 HTTP/1 和 HTTP/2 的"线上格式"完全不同(H1 是文本、H2 是二进制帧 + 多路复用)。Codec 抽象把这种差异藏起来——给上层(HCM、过滤器)一套统一接口,不管底层是 H1 还是 H2,上层代码一个样。
L02

统一接口

// envoy/http/codec.h:572 —— codec 连接基类
class Connection {
  virtual Status dispatch(Buffer::Instance& data) PURE;  // :582 喂字节,解析并回调
  virtual Protocol protocol() PURE;                       // :593 我是 H1/H2/H3
};
// ServerConnection(:665)服务端、ClientConnection(:671)客户端
// 解码方向:RequestDecoder(:246,decodeHeaders/decodeData)—— ActiveStream 实现它
// 编码方向:ResponseEncoder(:146,encodeHeaders)—— HCM 的 response_encoder_ 是它
读法:核心是 dispatch(data)——喂字节进去,codec 解析并回调上层。RequestDecoder(解析出的请求往哪送,ActiveStream 实现)和 ResponseEncoder(响应怎么编码写出,HCM 持有)是两个方向的接口。H1/H2 都实现这套接口——这就是抽象的价值。
同一套接口,多个实现——上层只认接口 codec 接口:dispatch + newStream HCM / L7 过滤器只依赖这一层(普通话) HTTP/1 codec文本解析·请求串行 HTTP/2 codec二进制帧·多路复用 HTTP/3 codec未来再加一个即可 ↑ 各自实现同一套接口
图注:H1/H2(以及未来 H3)都实现同一套 codec 接口。上层代码只依赖接口,新增协议只是"再写一个实现",上层零改动。
📝 举个例子:同一行 HCM 代码,H1 和 H2 都能跑 HCM 里就一句 codec_->dispatch(data)
• 客户端用 HTTP/1.1codec_ 实际是 H1 实现,把 GET /a HTTP/1.1\r\nHost:...\r\n\r\n 文本解析出来,回调 decodeHeaders
• 客户端用 HTTP/2codec_ 实际是 H2 实现,把二进制 HEADERS 帧解析出来,同样回调 decodeHeaders
HCM 那行代码一个字都不改——它只管调 dispatch,具体谁在解析、怎么解析,多态帮它隐藏了。
L03

dispatch 解码

HCM 的 onData(Day 07)调 codec_->dispatch(data)。codec 逐字节解析,解析到 HTTP 结构就回调上层(newStream/decodeHeaders/decodeData)。

读法:dispatch 是"字节 → HTTP 事件"的转换器。它是有状态的解析器:喂进来的字节可能是半个请求,codec 攒着,攒够一个完整头就回调 decodeHeaders这种"流式增量解析"适配网络数据分片到达的特点。
L04

newStream 回调

// codec.h:658 ServerConnectionCallbacks
virtual RequestDecoder& newStream(ResponseEncoder& response_encoder, ...) PURE;
// codec 解析到新请求时回调,返回一个 RequestDecoder(即 HCM 的 ActiveStream)
// response 和 request 共享同一个 Stream
读法:这是 codec 和 HCM 的接头点(Day 07 见过):codec 解析出新请求,调 newStream 问 HCM 要一个 RequestDecoder(ActiveStream),之后把解析出的头/体都往这个 decoder 送。response_encoder 同时给出——请求和响应共享一个 Stream。
L05

HTTP/1 实现

// source/common/http/http1/codec_impl.cc(基于 balsa/legacy parser 逐字节解析)
// :1268 onMessageBeginBase:新请求开始 → newStream 拿 RequestDecoder
// :1201 onHeadersCompleteBase:请求头解析完 → decodeHeaders 回调进 HCM
// :1292 onBody → decodeData
HTTP/1 的特点:一次一条消息 HTTP/1 是文本协议,一条连接上请求串行(一个处理完才下一个)。codec 用 parser 逐字节解析文本(GET /path HTTP/1.1\r\n...),解析到不同阶段回调不同方法(消息开始、头完成、body、消息完成)。为了和 HTTP/2 语义统一,无 body 的请求会"延迟"到 message complete 才回调 decodeHeaders(模拟 H2 的 end_stream 语义)。关键:无论 H1 怎么解析,回调给 HCM 的都是统一的 decodeHeaders/decodeData——上层不用知道这是 H1。
L06

HTTP/2 多路复用

source/common/http/http2/codec_impl.cc(基于 nghttp2):一条连接多路复用多条 stream,每个 H2 stream 对应一次 newStream/ActiveStream。同样实现 dispatch(),通过 nghttp2 回调驱动 decodeHeaders/decodeData

抽象的价值在这里闪光 HTTP/2 是二进制协议、一条连接能并发多个请求(多路复用)——和 HTTP/1 天差地别。但因为都实现同一套 codec 接口(dispatch + newStream + RequestDecoder),HCM 和所有 L7 过滤器代码零改动就同时支持 H1 和 H2!H2 的多路复用体现为"一条连接上多次 newStream、多个 ActiveStream 并存"(Day 07 的 streams_ 链表)。这就是"面向接口编程"的终极威力:新增 HTTP/3 也只是再写一个 codec 实现,上层不动。

👶 小白:H2 一条连接并发多个请求,codec 怎么不让它们的字节混成一锅粥?

👨‍🏫 老师:靠 H2 协议本身的"帧 + stream id"设计。H2 把数据切成一个个,每个帧头上标了它属于哪条 stream(stream id=1/3/5…)。codec 解析时按 id 分拣:id=1 的帧组装成请求 1、id=3 的组装成请求 3……然后对每条 stream 分别回调一次 newStream,对应 Day 07 的一个个 ActiveStream。所以字节层面虽然交错到达,codec 靠 id 把它们"归位",上层看到的仍是一个个独立完整的请求。H1 没有这套,只能一条连接一次一个请求排队走。

L07

端到端 11 步串讲

Listener accept 连接,装 L4 读过滤器链(HCM 是终端)
网络数据到达 → FilterManagerImpl::onContinueReading 遍历 L4 过滤器
到 HCM 的 onDatacodec_->dispatch(data)
codec 解析 → newStream 建 ActiveStream → 头解析完 decodeHeaders
HCM ActiveStream::decodeHeadersrefreshCachedRoute(算路由)+ 建 L7 链
filter_manager_.decodeHeaders 启动 L7 decode 链迭代
逐个 L7 过滤器(认证/限流…),按 FilterHeadersStatus 继续/停止
到 Router(终端):读路由、选集群、选主机、发上游
上游响应 → Router onUpstreamHeadersencodeHeaders 启动 encode 链
L7 encode 链逆序走 → HCM ActiveStream::encodeHeaders
codec 编码 → L4 写链 → 写回下游 socket
读法:这 11 步把第 1-2 周全部串起来——请求从进到出的完整旅程。建议对照这 11 步在源码里各跳一遍,画一张时序图。
L08

第 2 周收官 🎓

🧠 第 2 周(过滤器与请求生命周期)你已掌握

  • Day 06 L4 网络过滤器:FilterStatus、ReadFilter/WriteFilter、FIFO/LIFO 洋葱模型
  • Day 07 HCM:L4→L7 桥、onData→dispatch、newStream/ActiveStream
  • Day 08 L7 HTTP 过滤器:丰富状态码、decode/encode 双向、sendLocalReply
  • Day 09 Router 终端过滤器:选集群主机、发上游、响应回程、重试超时
  • Day 10 Codec 抽象:H1/H2 统一接口、端到端 11 步

你现在完全理解了"一个请求怎么被 Envoy 处理"。第 1 周是骨架引擎、第 2 周是请求处理,第 3 周(已学)是动态配置与上游,第 4 周是扩展生态。

✋ 动手

cd /Users/bitmart/work/codes/github/higress-group/envoy
sed -n '572,620p' envoy/http/codec.h                       # Connection 接口
grep -n 'onMessageBeginBase\|onHeadersCompleteBase' source/common/http/http1/codec_impl.cc | head
grep -n 'newStream' source/common/http/http2/codec_impl.cc | head
下一步 · Day 16(第4周):第 3 周(Day 11-15 xDS 与上游)你已学过。接下来是第 4 周 扩展机制——Registry::RegisterFactory 工厂模式,理解它才能看懂 Wasm、过滤器等一切扩展。
← Day 09 Router Day 16 · 扩展机制 →