导语:WASI 的第二次标准化
WASI Preview 1 让 WASM 走出了浏览器,但它存在结构性缺陷:接口用 C 风格 fd_* 函数硬编码、没有异步、wasi_unstable 版本管理混乱。字节码联盟从 2023 年开始全面重写——这就是 WASI Preview 2:接口全部用 WIT 组件模型 定义,类型安全、模块化、可异步。
而 wasi-http 是 Preview 2 中最具变革性的接口:它让"WASM 作为 HTTP 服务"成为标准能力,直接催生了无服务器 WASM 运行时(Fermyon Spin、Fastly)的新范式。
一句话总结:WASI Preview 2 = 组件化 + 类型化 + 异步化的系统接口重写;wasi-http 让 WASM 模块第一次以标准方式收发 HTTP 请求/响应。
1. WASI 标准演进
1.1 三次快照的差异
| 版本 | 接口定义方式 | 异步 | 状态 |
|---|---|---|---|
| Preview 1 | wasi_snapshot_preview1,C 风格 fd_* 函数,POSIX 子集 | 无 | 广泛部署,但设计受限 |
| Preview 2 | WIT 接口 + 组件模型,按领域拆包(cli/http/sockets/…) | 支持(stream/future) | 主流标准,工具链成熟 |
| Preview 3 | 全接口异步化、去 POSIX 化,底层基于 io/poll | 全面异步 | 制定中 |
Preview 1 → Preview 2 的关键转变:
Preview 1(函数式):
wasi_snapshot_preview1.fd_read(fd=3, iovs=[...], nread=*)
Preview 2(WIT 资源式):
interface wasi:io/streams {
resource input-stream { read: func(len: u64) -> result<list<u8>, error>; }
}
interface wasi:filesystem {
resource descriptor { read: func(len: u64, offset: u64) -> ...; }
}
1.2 模块化的接口包
Preview 2 把系统能力拆成独立的 WIT package,宿主按需实现:
| Package | 职责 |
|---|---|
wasi:cli | 命令行参数、环境变量、标准输入输出 |
wasi:filesystem | 文件与目录操作(descriptor 资源) |
wasi:sockets | TCP/UDP 网络套接字(network/tcp-stream/udp-socket) |
wasi:clocks | 单调时钟、挂钟、时区 |
wasi:random | 随机数、不安全随机 |
wasi:io | streams/poll——Preview 2 的异步基石 |
wasi:http | HTTP 请求/响应/服务端/客户端 |
wasi:keyvalue | 键值存储(无服务器场景常用) |
异步基石 wasi:io:所有阻塞 I/O 都变成"返回 stream/future",配合 poll 实现非阻塞:
// wasi:io/poll(Preview 2 异步核心)
interface poll {
type pollable = resource<...>;
poll: func(in: list<borrow<pollable>>) -> list<u32>;
}
一句话总结:Preview 2 用 WIT 把系统接口彻底模块化,
wasi:io的 stream/future + poll 让所有 I/O 具备异步能力,为 Preview 3 的全异步化铺路。
2. wasi:sockets 与网络能力
2.1 套接字接口(WIT 节选)
// wasi:sockets/tcp
interface tcp {
resource tcp-socket {
bind: func(address: ip-address) -> result<_, error-code>;
connect: func(address: ip-address) -> result<_, error-code>;
accept: func() -> result<borrow<tcp-stream>, error-code>;
start-listening: func() -> result<_, error-code>;
address-family: func() -> address-family;
}
}
interface ip {
type ip-address = variant { ipv4(u32), ipv6(tuple<u64, u64>) };
type ipv4-socket-address = record {
port: u16,
address: ipv4-address,
};
}
2.2 Rust 编写一个 TCP 服务
注意 Preview 2 里网络是能力式的:模块默认无任何网络权限,运行时需显式授予。
use wasi::sockets::tcp::{TcpSocket, AddressFamily};
use wasi::sockets::network::{Ipv4SocketAddress, Ipv4Address, Network};
use wasi::io::poll::{poll, Pollable};
async fn serve() -> Result<(), String> {
let network = Network::default()?;
let socket = TcpSocket::new(AddressFamily::Ipv4)?;
// 绑定并监听 0.0.0.0:8080
socket.bind(&network, Ipv4SocketAddress {
address: Ipv4Address::new(0, 0, 0, 0),
port: 8080,
})?;
socket.start_listening()?;
loop {
let conn = socket.accept()?; // 返回 tcp-stream
let stream = conn.subscribe(); // pollable
poll(&[&stream])?; // 异步等待可读
// 读写字节流...
}
}
网络权限在 Wasmtime 中注入:
wasmtime run --tcplisten 127.0.0.1:8080 target/wasm32-wasip1/release/tcp-server.wasm
# --tcplisten 显式授权监听 127.0.0.1:8080,其余地址一律拒绝
一句话总结:
wasi:sockets把 TCP/UDP 建模为 WIT 资源,异步用poll驱动;网络访问默认关闭,必须由运行时按"能力"显式授权。
3. wasi-http:无服务器的 HTTP 标准
3.1 两大 world:incoming-handler 与 proxy
wasi-http 定义了组件世界(world),最常用的是 proxy——一个"接收 HTTP 请求、返回响应"的通用无服务器模型:
// wasi:http/proxy(标准代理 world)
world proxy {
import wasi:io/poll;
import wasi:io/streams;
import wasi:http/types;
import wasi:http/incoming-handler;
import wasi:http/outgoing-handler;
export wasi:http/incoming-handler;
export wasi:cli/exit;
}
核心接口 wasi:http/types:
interface types {
// 请求:方法 + 路径 + 头部 + body 流
resource request {
method: func() -> method;
path-with-query: func() -> option<string>;
scheme: func() -> option<scheme>;
headers: func() -> headers;
consume-body: func() -> incoming-body;
}
type method = variant {
get, head, post, put, delete, connect, options, trace,
patch, other(string),
};
type status-code = u16;
// 响应
resource response {
status-code: func() -> status-code;
headers: func() -> headers;
consume-body: func() -> outgoing-body;
}
// 头部(大小写不敏感的字段集合)
resource headers {
get: func(name: string) -> list<u8>;
set: func(name: string, value: list<u8>) -> result<_, header-error>;
}
}
3.2 编写一个 wasi-http 服务组件(Rust)
用 cargo component 创建 HTTP 服务,依赖 wasi:http/proxy world:
cargo component new http-echo --lib
# 在 wit/deps.toml 声明对 wasi:http 的依赖(wkg 解析)
src/lib.rs:
wit_bindgen::generate!({
path: "wit",
world: "wasi:http/proxy",
});
use exports::wasi::http::incoming_handler::{IncomingRequest, ResponseOutparam};
use wasi::http::types::{Fields, IncomingBody, OutgoingBody, OutgoingResponse, Method};
struct HttpEcho;
impl exports::wasi::http::incoming_handler::Guest for HttpEcho {
fn handle(request: IncomingRequest, response_out: ResponseOutparam) {
let method = request.method().to_string();
let path = request.path_with_query().unwrap_or_default();
// 读取请求 body(流式)
let body = request.consume_body().unwrap();
let body_stream = body.stream().unwrap();
let mut buf = Vec::new();
// ... 从 body_stream 读取字节 ...
let body = format!("{method} {path} → body: {}", String::from_utf8_lossy(&buf));
let headers = Fields::new();
headers.set("content-type", b"text/plain".to_vec()).unwrap();
let response = OutgoingResponse::new(headers);
response.set_status_code(200).unwrap();
let out_body = response.body().unwrap();
let out_stream = out_body.write().unwrap();
out_stream.write(body.as_bytes()).unwrap();
out_stream.close().unwrap();
OutgoingBody::finish(out_body);
ResponseOutparam::set(response_out, Ok(response));
}
}
export!(HttpEcho);
3.3 本地运行:wasmtime serve
Wasmtime 21+ 提供 wasmtime serve,直接驱动 wasi-http 组件:
# 构建组件
cargo component build --release
# 本地起 HTTP 服务(默认 127.0.0.1:8080)
wasmtime serve target/wasm32-wasip1/release/http_echo.wasm
# 另一个终端测试
curl -X POST http://127.0.0.1:8080/hello -d '你好 WASI'
# POST /hello → body: 你好 WASI
流式特性:wasi-http 的 body 是 incoming-body 资源,基于 wasi:io 的 stream——天然支持大文件/流式响应,无需一次性读入内存。
一句话总结:wasi-http 的
incoming-handler把无服务器函数抽象成"接收 request、产出 response"的标准组件 world;wasmtime serve一行命令即可本地起服务。
4. 无服务器场景落地
wasi-http 直接催生了 WASM 无服务器运行时的标准底座:
| 平台/运行时 | 底层引擎 | wasi-http 支持 | 定位 |
|---|---|---|---|
| Fermyon Spin | Wasmtime | 原生 | WASI 应用平台,spin build/spin up 部署 HTTP 应用 |
| Fastly Compute | 自研 edge 引擎 | 原生 | CDN 边缘执行 WASI HTTP 组件 |
| Wasmtime serve | Wasmtime | 原生 | 本地开发/小规模部署 |
| Cloudflare Workers | V8 isolates | 不支持 wasi-http(自有 API) | 通过 componentize-js 间接复用组件 |
| Wasmer | 自研引擎 | 部分支持 | 多语言运行 wasi-http |
Fermyon Spin 实战:
# 安装
curl -fsSL https://developer.fermyon.com/downloads/install.sh | bash
# 初始化 HTTP 模板
spin new --help
spin new http-rust my-app
cd my-app && spin build # 编译为 wasi-http 组件
# 本地运行(内部就是 wasmtime + wasi-http)
spin up --listen 127.0.0.1:3000
# 部署到 Fermyon Cloud
spin deploy
无服务器的杀手锏——毫秒级冷启动:wasi-http 组件是纯净的字节码,无 OS 启动、无解释器预热,实例化在个位数毫秒。与容器无服务器对比:
| 维度 | AWS Lambda(容器层) | WASM + wasi-http |
|---|---|---|
| 冷启动 | 300ms - 1s | 1ms - 10ms |
| 运行时内存 | 128MiB 起步 | ~10MiB |
| 并发密度 | 受限 | 每核数千实例 |
| 多语言 | 每语言独立运行时 | 统一组件二进制 |
一句话总结:wasi-http 让"HTTP 即组件",无服务器平台因此获得毫秒级冷启动与超高密度——这是 WASM 无服务器区别于传统 FaaS 的核心竞争力。
5. 运行时支持对比
5.1 Wasmtime
字节码联盟官方,组件模型 + wasi-http 支持最完整:
# 完整运行 wasi-http 组件的命令矩阵
wasmtime run --cli component.wasm # 纯 CLI world
wasmtime serve --addr 0.0.0.0:8080 app.wasm # wasi-http proxy world
wasmtime compile app.wasm -o app.cwasm # AOT 编译加速启动
# 能力注入
wasmtime serve \
--tcplisten 127.0.0.1:8080 \
--env REDIS_URL=redis://localhost:6379 \
--dir /tmp::/mnt \
app.wasm
| 特性 | Wasmtime | Wasmer |
|---|---|---|
| 组件模型 | 完整(WIT/canonical ABI) | 支持中 |
| wasi-http serve | ✅ wasmtime serve | ⚠️ 部分场景 |
| Preview 3 进度 | 主力推进 | 跟进 |
| 嵌入式 API | Rust/Python/C/Go | 多语言更广 |
| 独特卖点 | 标准化标杆、安全加固 | WAPM 包管理、多后端编译 |
5.2 兼容性矩阵(wasi-http 相关)
| 能力 | Wasmtime 21+ | Wasmer 4+ | Spin 1.x |
|---|---|---|---|
wasi:http/proxy world | ✅ | ⚠️ 需配置 | ✅ |
| 流式 request body | ✅ | ⚠️ 实验 | ✅ |
| 并发连接 | ✅ | ⚠️ | ✅ |
| TLS(wasi:http 内置) | 部分 | ⚠️ | 依赖平台 |
wasmtime serve CLI | ✅ | — | — |
一句话总结:Wasmtime 是 wasi-http 的标准参照实现;Wasmer 通过自定义 API 实现类似能力;上层平台(Spin)把 wasi-http 封装为一行命令的应用体验。
6. 总结与实践建议
| 主题 | 核心结论 |
|---|---|
| WASI 演进 | Preview1(C 函数)→ Preview2(WIT 组件接口)→ Preview3(全异步) |
| 模块化接口 | cli/filesystem/sockets/clocks/random/io/http 按包拆分 |
| wasi-http | incoming-handler + proxy world,request/response 均为资源化、流式 |
| 无服务器 | 毫秒冷启动、~10MiB 内存、每核数千实例 |
| 运行时 | Wasmtime serve 最成熟;Spin/Fastly 封装为平台产品 |
实践建议:
- 新写 WASM 服务优先 wasi-http 组件:用
cargo component+wasi:http/proxyworld,一条wasmtime serve即可本地联调,天然可移植到 Spin/Fastly - 读 body 用流式:大请求体务必走
incoming-body的 stream,别一次性collect进内存 - 网络能力最小化:养成"只授权监听/连接所需地址"的习惯(
--tcplisten/--tcpconnect) - 存量 Preview1 程序迁移:用
wasm-tools component new包裹 +wasmtime run的 preview1→preview2 适配层平滑过渡,再逐步换用新接口 - 关注 Preview 3:全异步化会让
wasi:io成为唯一 I/O 底座,接口会再次统一
wasi-http 与边缘计算的结合是最热方向——推荐接着读 WASM 边缘计算与 CDN 落地,看 Fastly/Cloudflare 如何把它搬上 CDN 边缘。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。