Codec 抽象(H1/H2)
HTTP/1、HTTP/2、HTTP/3 差异巨大,Envoy 怎么让上层代码"无感"地支持它们?答案是 codec 抽象——统一接口。今天看这层抽象,并用端到端 11 步串讲收官第 2 周。
Codec 是什么
dispatch 给 codec 解析。但 HTTP/1.1(GET /x HTTP/1.1\r\n 纯文本、请求串行)和 HTTP/2(二进制帧、一条连接并发多请求)的"线上格式"天差地别。如果 HCM 和所有 L7 过滤器都要写"if 是 H1 这么处理、if 是 H2 那么处理",那代码会被协议差异撕成两半,还没法加 H3。decodeHeaders/decodeData 这套统一回调)。接待处(HCM + L7 过滤器)只听普通话,根本不用知道客人原本说哪国话。换句话说:协议差异被 codec 这层"翻译"彻底吸收,上层代码一份就够、零改动支持多协议。这就是"面向接口编程"的威力。onData 靠它)。编码:把 HTTP 响应转回字节流写出去。难点在于 HTTP/1 和 HTTP/2 的"线上格式"完全不同(H1 是文本、H2 是二进制帧 + 多路复用)。Codec 抽象把这种差异藏起来——给上层(HCM、过滤器)一套统一接口,不管底层是 H1 还是 H2,上层代码一个样。统一接口
// 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(data)。• 客户端用 HTTP/1.1:
codec_ 实际是 H1 实现,把 GET /a HTTP/1.1\r\nHost:...\r\n\r\n 文本解析出来,回调 decodeHeaders。• 客户端用 HTTP/2:
codec_ 实际是 H2 实现,把二进制 HEADERS 帧解析出来,同样回调 decodeHeaders。HCM 那行代码一个字都不改——它只管调
dispatch,具体谁在解析、怎么解析,多态帮它隐藏了。dispatch 解码
HCM 的 onData(Day 07)调 codec_->dispatch(data)。codec 逐字节解析,解析到 HTTP 结构就回调上层(newStream/decodeHeaders/decodeData)。
dispatch 是"字节 → HTTP 事件"的转换器。它是有状态的解析器:喂进来的字节可能是半个请求,codec 攒着,攒够一个完整头就回调 decodeHeaders。这种"流式增量解析"适配网络数据分片到达的特点。newStream 回调
// codec.h:658 ServerConnectionCallbacks
virtual RequestDecoder& newStream(ResponseEncoder& response_encoder, ...) PURE;
// codec 解析到新请求时回调,返回一个 RequestDecoder(即 HCM 的 ActiveStream)
// response 和 request 共享同一个 Stream
newStream 问 HCM 要一个 RequestDecoder(ActiveStream),之后把解析出的头/体都往这个 decoder 送。response_encoder 同时给出——请求和响应共享一个 Stream。HTTP/1 实现
// source/common/http/http1/codec_impl.cc(基于 balsa/legacy parser 逐字节解析)
// :1268 onMessageBeginBase:新请求开始 → newStream 拿 RequestDecoder
// :1201 onHeadersCompleteBase:请求头解析完 → decodeHeaders 回调进 HCM
// :1292 onBody → decodeData
GET /path HTTP/1.1\r\n...),解析到不同阶段回调不同方法(消息开始、头完成、body、消息完成)。为了和 HTTP/2 语义统一,无 body 的请求会"延迟"到 message complete 才回调 decodeHeaders(模拟 H2 的 end_stream 语义)。关键:无论 H1 怎么解析,回调给 HCM 的都是统一的 decodeHeaders/decodeData——上层不用知道这是 H1。HTTP/2 多路复用
source/common/http/http2/codec_impl.cc(基于 nghttp2):一条连接多路复用多条 stream,每个 H2 stream 对应一次 newStream/ActiveStream。同样实现 dispatch(),通过 nghttp2 回调驱动 decodeHeaders/decodeData。
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 没有这套,只能一条连接一次一个请求排队走。
端到端 11 步串讲
FilterManagerImpl::onContinueReading 遍历 L4 过滤器onData → codec_->dispatch(data)newStream 建 ActiveStream → 头解析完 decodeHeadersActiveStream::decodeHeaders:refreshCachedRoute(算路由)+ 建 L7 链filter_manager_.decodeHeaders 启动 L7 decode 链迭代onUpstreamHeaders → encodeHeaders 启动 encode 链ActiveStream::encodeHeaders第 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
Registry::RegisterFactory 工厂模式,理解它才能看懂 Wasm、过滤器等一切扩展。