Skip to content

【Zig 日报】项目分享:一个带有 Zig 核心的 Python 无 I/O(sans-IO)HTTP 解析器!⚡ #352

Description

@jiacai2050

文档:https://zttp.marcelotryle.com
源码:https://github.com/Kludex/zttp

警告: zttp 目前处于实验阶段。API 和行为可能会随时更改,尚不适合用于生产环境。

zttp 是一个无 I/O (sans-IO) 的 HTTP 解析器,其引擎使用 Zig 编写。它支持 HTTP/1.1、HTTP/2 和 HTTP/3,并且不执行任何自身的 I/O 操作:你向它输入字节并提取事件,同时向它索取要发送的字节。它从不触碰套接字(socket),因此它可以与你喜欢的任何 I/O 方案配合使用。

它的理念与 h11 相同,但底层是用手写的 Zig 引擎代替了纯 Python。

核心特点是:

  • 无 I/O (Sans-IO): 干净的、基于事件的 API。通过 receive_data 输入字节,通过 next_event 提取 Request / Data / EndOfMessage 事件。没有回调,没有套接字,没有意外。
  • HTTP/1.1、HTTP/2 和 HTTP/3: 三者使用相同的事件 API,只需通过一个 protocol= 参数进行选择。
  • 速度快: 在 14 个基准测试工作负载中的 13 个上,比 httptools(一个 C 语言解析器)还要快,并且比纯 Python 替代方案快大约 15 倍。
  • 安全: 默认严格。它能够防御请求走私(request smuggling),拒绝纯 LF 换行符,限制每个缓冲区的大小,并且使用 Zig 开启了安全检查的编译模式发布。
  • 类型化: 带有完整类型提示的 py.typed 包。
  • 无依赖: wheel 安装包仅包含编译好的引擎,别无其他。

要求

zttp 需要 CPython 3.10+,并运行在 Linux、macOS 和 Windows 上。

安装

$ pip install zttp

安装包中已经预先编译好了 Zig 核心,因此无需构建,也无需其他配置。有关从源码构建的信息,请参阅安装文档

示例

你来扮演服务器:字节输入,事件输出。

import zttp

conn = zttp.Connection(zttp.SERVER)
conn.receive_data(b"GET /path?q=1 HTTP/1.1\r\nHost: example.com\r\n\r\n")

conn.next_event()  # Request(method=b'GET', target=b'/path?q=1', http_version=b'1.1', headers=[(b'Host', b'example.com')])
conn.next_event()  # EndOfMessage(trailers=[])
conn.next_event()  # NEED_DATA

# 构建响应:
conn.send_response(200, [(b"Content-Length", b"5")])
conn.send_data(b"hello")
conn.end_message()
conn.data_to_send()  # b'HTTP/1.1 200 OK\r\nContent-Length: 5\r\n\r\nhello'

读取端会产生 Request / Response / Data / EndOfMessage,或者在需要更多字节时产生 NEED_DATA 哨兵值。写入端则会序列化头部、正文数据和消息结尾,并为你处理好消息体分帧(Content-Length 或 chunked)。

无论你向 receive_data 传入什么——完整消息、片段还是单字节——zttp 都会进行缓冲并恢复。这里没有回调:你可以在准备好时主动拉取事件。这就是“无 I/O (sans-IO)”的含义。

一个 API,三个协议

protocol= 参数用于选择线路格式,而事件 API 保持不变:

import zttp

h1 = zttp.Connection(zttp.SERVER)  # HTTP/1.1
h2 = zttp.Connection(zttp.SERVER, protocol=zttp.HTTP2)  # HTTP/2
h3 = zttp.Connection(zttp.SERVER, protocol=zttp.HTTP3)  # HTTP/3

在 HTTP/2 中,单个连接可以多路复用多个请求,因此 Request / Response / Data / EndOfMessage 事件会携带一个 stream_id,并且你是在一个 Stream 句柄上进行发送。出站流量控制已为你处理好:send_data 会根据对端窗口允许的大小发送,其余部分则会暂存,直到收到信用额度(credit)。

stream = h2.stream(request.stream_id)
stream.send_response(200, [(b"content-type", b"text/plain")])
stream.send_data(b"Hello, HTTP/2!")
stream.end_message()
h2.data_to_send()  # 要放到网线上的 HTTP/2 帧

在 HTTP/3 中,传输层是 UDP,因此你通过 receive_datagram 输入完整的数据报(datagram),并拉取相同的事件。底层的 QUIC 传输(包保护、丢包恢复、拥塞控制、流重组)完全由 Zig 核心从头编写。HTTP/3 使用与 HTTP/2 相同的、基于流作用域的发送表面。

h3.receive_datagram(datagram)
h3.next_event()  # 带有 stream_id 标记的相同的 Request / Data / EndOfMessage

有关完整的故事(包括每个协议更改的内容及原因),请参阅 HTTP/2HTTP/3。HTTP/3 支持目前处于实验阶段:便利的服务器构造函数为本地使用创建了短暂的 TLS 身份,而非生产环境的服务器身份。

性能

httptools(Uvicorn 使用的 C 语言解析器)在相同请求下进行对比,两者均已验证能够提取相同的数据:

工作负载 zttp httptools zttp 对比 httptools
简单 GET ~1.24M 请求/秒 ~1.07M 请求/秒 ~1.16x
POST + JSON 请求体 ~1.42M 请求/秒 ~1.25M 请求/秒 ~1.14x

zttp 在基准测试套件的 14 个工作负载中的 13 个上击败了 httptools,同时保持了无 I/O (sans-IO) 和基于事件的特性,并且比纯 Python 替代方案快大约 15 倍。你可以通过运行 ./scripts/bench 来亲自验证。

这些是解析器的微基准测试,个位数的百分比优势接近运行时的噪声——有关完整的 14 个工作负载表格、方法论和注意事项,请参阅性能文档

为什么它这么快

  • Zig 核心中采用了 SWAR(SIMD Within A Register)换行符扫描器和编译时构建的字符类表,因此热循环是分支极少的数组查找。
  • 消息体作为对解析缓冲区进行切片的单一 Data 事件发出,而不是像 httptools 那样在每次回调时进行复制。
  • 头部列表直接在 Zig 中构建为 list[tuple[bytes, bytes]],没有每个头部触发的 Python 回调。

正确性与安全性

核心严格遵守 RFC 9112 第 6 节关于防范请求走私的分帧规则:处理 Content-Length 与 Transfer-Encoding 冲突、重复的 Content-Length 检查,并将多个 Transfer-Encoding 字段行合并为一个有序列表,以确保 chunked(分块传输)必须是唯一且最终的编码。默认情况下,行尾必须严格为 CRLF(拒绝纯 LF),块大小(chunk-size)必须严格为 1*HEXDIG,并且拒绝过时的行折叠(obsolate line folding)。

头部块、尾部(trailers)和接收缓冲区都受到保守的内置限制保护,因此恶意对端无法耗尽内存,出站序列化器会拒绝 CR/LF/控制字节以防止响应拆分。构建默认启用 Zig 的安全检查 ReleaseSafe 模式。格式错误的输入会引发 RemoteProtocolError;滥用发送 API 会引发 LocalProtocolError

HTTP/2 层采用了相同的姿态:每个流的状态机强制执行 RFC 9113 的流生命周期,并具备 RFC 要求的精确的“流错误”与“连接错误”分类。

该解析器已经过两次对抗性安全审计(一次代码 review,以及针对 Node、Go、Python、Rust 和 C 服务器中真实 HTTP 解析器 CVE 的 CVE 导向 review);zig build fuzz 会对核心进行对抗性输入的模糊测试(fuzzing)。有关 zttp 防御的内容、强制执行的具体限制以及集成商的责任,请参阅 THREAT_MODEL.md

许可证

本项目采用 BSD-3-Clause 许可证的条款授权。

加入我们

Zig 中文社区是一个开放的组织,我们致力于推广 Zig 在中文群体中的使用,有多种方式可以参与进来:

  1. 供稿,分享自己使用 Zig 的心得
  2. 改进 ZigCC 组织下的开源项目
  3. 加入微信群Telegram 群组Google Groups 与更多 Zig 爱好者交流

Metadata

Metadata

Assignees

No one assigned

    Labels

    日报daily report

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions