WASI Preview 2 与 wasi-http:标准化的网络接口

详解 WASI 标准演进:Preview1→Preview2→Preview3,Preview2 的 sockets/clocks/filesystem 接口,wasi-http 的 incoming-handler 模型与无服务器落地,以及 Wasmtime/Wasmer 运行时对 wasi-http 的支持对比。

导语: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 1wasi_snapshot_preview1,C 风格 fd_* 函数,POSIX 子集无广泛部署,但设计受限
Preview 2WIT 接口 + 组件模型,按领域拆包(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:socketsTCP/UDP 网络套接字(network/tcp-stream/udp-socket)
wasi:clocks单调时钟、挂钟、时区
wasi:random随机数、不安全随机
wasi:iostreams/poll——Preview 2 的异步基石
wasi:httpHTTP 请求/响应/服务端/客户端
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 SpinWasmtime原生WASI 应用平台,spin build/spin up 部署 HTTP 应用
Fastly Compute自研 edge 引擎原生CDN 边缘执行 WASI HTTP 组件
Wasmtime serveWasmtime原生本地开发/小规模部署
Cloudflare WorkersV8 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 - 1s1ms - 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
特性WasmtimeWasmer
组件模型完整(WIT/canonical ABI)支持中
wasi-http serve✅ wasmtime serve⚠️ 部分场景
Preview 3 进度主力推进跟进
嵌入式 APIRust/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-httpincoming-handler + proxy world,request/response 均为资源化、流式
无服务器毫秒冷启动、~10MiB 内存、每核数千实例
运行时Wasmtime serve 最成熟;Spin/Fastly 封装为平台产品

实践建议:

  1. 新写 WASM 服务优先 wasi-http 组件:用 cargo component + wasi:http/proxy world,一条 wasmtime serve 即可本地联调,天然可移植到 Spin/Fastly
  2. 读 body 用流式:大请求体务必走 incoming-body 的 stream,别一次性 collect 进内存
  3. 网络能力最小化:养成"只授权监听/连接所需地址"的习惯(--tcplisten/--tcpconnect)
  4. 存量 Preview1 程序迁移:用 wasm-tools component new 包裹 + wasmtime run 的 preview1→preview2 适配层平滑过渡,再逐步换用新接口
  5. 关注 Preview 3:全异步化会让 wasi:io 成为唯一 I/O 底座,接口会再次统一

wasi-http 与边缘计算的结合是最热方向——推荐接着读 WASM 边缘计算与 CDN 落地,看 Fastly/Cloudflare 如何把它搬上 CDN 边缘。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「wasm」更多文章

  1. WASM 边缘计算与 CDN 落地实践
  2. WASM SIMD 高性能计算实战
  3. WASM 多线程与 SharedArrayBuffer