首页
/ 跑通 Socket.IO 的 WebTransport(HTTP/3)示例:从自签证书到双向流的完整实现

跑通 Socket.IO 的 WebTransport(HTTP/3)示例:从自签证书到双向流的完整实现

2026-09-04 23:47:54作者:滕妙奇

本文围绕仓库中的 WebTransport 示例 examples/webtransport/README.md 展开,完整继承其「生成自签证书 → 安装依赖 → 启动服务器 → 启动 Chrome」四步操作流程,并结合 examples/webtransport/index.jsexamples/webtransport/index.html 以及 engine.io 的 WebTransport 传输实现,深入讲解 Socket.IO 如何在 HTTPS 与 HTTP/3 双服务器架构下建立、升级和传输基于 QUIC 的低延迟连接。读完后你将能够在本机独立跑通该示例,并理解 WebTransport 会话从 UDP 端口进入 Socket.IO 引擎的完整链路。

一、示例定位与目录结构

Socket.IO 是一个面向各平台的双向、低延迟通信库,其底层引擎 engine.io 支持三种传输方式:pollingwebsocketwebtransport。本示例演示了第三种传输的完整服务端与客户端接入方式:服务器端额外监听一个 HTTP/3(UDP)端口,把来自 QUIC 的 WebTransport 会话"桥接"进 Socket.IO 引擎,使其与 polling/WebSocket 连接共享同一套 Socket 生命周期管理。

示例目录 examples/webtransport/ 包含以下文件,各自承担明确职责:

文件 职责
README.md 四步运行流程说明(本文主体骨架)
generate_cert.sh 生成自签名 EC 证书(WebTransport 依赖 HTTPS/TLS 1.3 的前提)
index.js 服务端入口:HTTPS 静态服务器 + Socket.IO + HTTP/3 服务器桥接
index.html 客户端页面:通过 transportOptions 配置 WebTransport 参数
open_chrome.sh 携带 QUIC 相关启动参数打开 Chrome 访问页面
package.json 声明依赖:socket.io 与 Node 侧 HTTP/3 实现

二、按 README 四步跑通示例

原文档给出的完整操作流程如下:

# generate a self-signed certificate
$ ./generate_cert.sh

# install dependencies
$ npm i

# start the server
$ node index.js

# open a Chrome browser
$ ./open_chrome.sh

下面对每一步做参数级展开,保证可直接复制执行。

2.1 生成自签名证书(generate_cert.sh)

WebTransport 构建于 QUIC 之上,QUIC 又构建于 TLS 1.3 之上,因此服务端必须提供证书。脚本 generate_cert.sh 使用 OpenSSL 生成一张自签名证书:

#!/bin/bash
openssl req -new -x509 -nodes \
    -out cert.pem \
    -keyout key.pem \
    -newkey ec \
    -pkeyopt ec_paramgen_curve:prime256v1 \
    -subj '/CN=127.0.0.1' \
    -days 14

关键参数含义:

  • -new -x509:直接生成自签名 X.509 证书(而非 CSR 请求);
  • -out cert.pem / -keyout key.pem:输出证书与私钥文件,index.js 中正是 readFileSync("./key.pem")readFileSync("./cert.pem") 读取这两个文件;
  • -newkey ec -pkeyopt ec_paramgen_curve:prime256v1:生成 EC(P-256)曲线密钥。这里值得注意:QUIC 握手要求使用 EC 或 RSA 密钥,RSA 2048 以下不被支持,而 EC 密钥握手成本更低,这也是 engine.io 其他测试夹具中普遍使用 EC 证书的原因;
  • -subj '/CN=127.0.0.1':证书 CN 为 127.0.0.1。后文客户端特意把 WebTransport 主机名配置为 127.0.0.1 而不是 localhost,就是为了与证书 CN 对齐(页面本身通过 https://localhost:3000 访问,由 Chrome 启动参数绕过证书校验);
  • -days 14:有效期 14 天,示例场景足够。

2.2 安装依赖(npm i)

package.json 声明了三组依赖:

{
  "type": "module",
  "dependencies": {
    "@fails-components/webtransport": "^1.0.8",
    "@fails-components/webtransport-transport-http3-quiche": "^1.0.8",
    "socket.io": "^4.7.4"
  }
}
  • "type": "module":示例使用 ESM,因此入口用 import { readFileSync } from "node:fs" 写法;
  • socket.io:提供 Socket.IO 服务端,版本要求 ^4.7.4;
  • @fails-components/webtransport:Node.js 环境的 WebTransport/HTTP/3 服务端实现,提供 Http3Server 类(浏览器则直接使用内建 WebTransport 对象);
  • @fails-components/webtransport-transport-http3-quiche:上述库使用的 QUIC 传输实现(quiche 引擎),提供真实的 UDP/QUIC 收发能力。

2.3 启动服务器(node index.js)

入口 index.js 是理解整个示例的关键,其完整逻辑分为三部分:

第一部分:HTTPS 服务器(HTTP/1.1 通道)

const httpsServer = createServer({
  key,
  cert
}, (req, res) => {
  if (req.method === "GET" && req.url === "/") {
    const content = readFileSync("./index.html");
    res.writeHead(200, { "content-type": "text/html" });
    res.write(content);
    res.end();
  } else {
    res.writeHead(404).end();
  }
});

const io = new Server(httpsServer, {
  transports: ["polling", "websocket", "webtransport"]
});

这里有两个要点:

  1. HTTPS 服务器同时承担两个职责:一是伺服示例页面 index.html,二是挂载 Socket.IO 引擎。polling 与 websocket 两种传统传输走这条 TCP 通道;
  2. new Server(httpsServer, { transports: [...] }) 中显式把 webtransport 加入允许的传输列表。从 packages/engine.io/lib/server.ts 可以看到,engine.io 的 TransportName 类型就是 "polling" | "websocket" | "webtransport",只有服务端声明支持,客户端才可能建立或升级到 WebTransport 传输。

随后监听端口:

const port = process.env.PORT || 3000;
httpsServer.listen(port, () => {
  console.log(`server listening at https://localhost:${port}`);
});

第二部分:HTTP/3 服务器(UDP 通道)与引擎桥接

const h3Server = new Http3Server({
  port,
  host: "0.0.0.0",
  secret: "changeit",
  cert,
  privKey: key,
});

(async () => {
  const stream = await h3Server.sessionStream("/socket.io/");
  const sessionReader = stream.getReader();

  while (true) {
    const { done, value } = await sessionReader.read();
    if (done) {
      break;
    }
    io.engine.onWebTransportSession(value);
  }
})();

h3Server.startServer();

这是示例最核心的一段:HTTP/3 服务器监听与 HTTPS 相同端口号的 UDP 通道(浏览器侧 QUIC 默认与 HTTPS 同端口),只接受路径前缀为 /socket.io/ 的 WebTransport 会话——这正好对应 Socket.IO 引擎在 HTTP 层的挂载路径。每收到一个新 QUIC 会话(value 即 session 对象),就调用 io.engine.onWebTransportSession(value) 将其交给引擎接管。secret: "changeit" 是 WebTransport 规范中的 HKDF 密钥,用于从 QUIC 连接密钥派生应用层数据,示例中使用了占位值。

第三部分:连接日志

io.on("connection", (socket) => {
  console.log(`connect ${socket.id}`);

  socket.conn.on("upgrade", (transport) => {
    console.log(`transport upgraded to ${transport.name}`);
  });

  socket.on("disconnect", (reason) => {
    console.log(`disconnect ${socket.id} due to ${reason}`);
  });
});

socket.conn 是底层的 engine.io Socket,upgrade 事件会在传输切换时触发并携带目标传输对象——运行示例时若观察到 transport upgraded to webtransport,即说明连接已成功从初始传输切换到 QUIC 通道。

2.4 启动 Chrome 并访问(open_chrome.sh)

WebTransport 目前主要依赖 Chrome 系浏览器支持,且需要绕过自签证书信任。open_chrome.sh 的完整内容:

#!/bin/bash
HASH=`openssl x509 -pubkey -noout -in cert.pem |
   openssl pkey -pubin -outform der |
   openssl dgst -sha256 -binary |
   base64`

/opt/google/chrome/chrome \
    --origin-to-force-quic-on=127.0.0.1:3000 \
    --ignore-certificate-errors-spki-list=$HASH \
    https://localhost:3000

逐行解读:

  • $HASH 计算证书公钥的 SHA-256 指纹(base64 编码),即 SPKI 哈希。它等价于 Chrome 证书错误页中提示你"复制并粘贴此值"的内容;
  • --origin-to-force-quic-on=127.0.0.1:3000:强制该 origin 使用 QUIC。注意这里写的是 127.0.0.1 而非页面 URL 中的 localhost,与证书 CN 保持一致;
  • --ignore-certificate-errors-spki-list=$HASH:仅对指纹匹配的证书忽略 TLS 校验错误,相比全局忽略更安全;
  • 脚本中 /opt/google/chrome/chrome 是 Linux 上 Chrome 的典型安装路径,在 macOS 或 Windows 上请替换为本机 Chrome 可执行文件路径,启动参数保持不变。

三、服务端源码级实现:onWebTransportSession 之后发生了什么

README 只说明了"怎么跑",而会话进入 io.engine.onWebTransportSession() 后的处理逻辑在 engine.io 源码中,值得追读。

3.1 会话入口:不经过 HTTP 的传输

packages/engine.io/lib/server.ts 中定义了入口方法:

public async onWebTransportSession(session: any) {
  if (this.middlewares.length > 0) {
    // middlewares expect an IncomingMessage argument, which cannot be created from the WebTransport session object
    debug(
      "closing session since WebTransport is not compatible with middlewares",
    );
    return session.close();
  }

  const timeout = setTimeout(() => {
    debug(
      "the client failed to establish a bidirectional stream in the given period",
    );
    session.close();
  }, this.opts.upgradeTimeout);

  const streamReader = session.incomingBidirectionalStreams.getReader();
  const result = await streamReader.read();
  // ...
}

可以确认三个实现事实:

  1. middlewares 不兼容:engine.io 的中间件期望 IncomingMessage(HTTP 请求)作为参数,而 WebTransport 会话对象无法构造出这样的请求,因此只要挂载了中间件,会话会被直接关闭。这是使用 WebTransport 传输时的一条硬性限制;
  2. 升级超时保护:若客户端未能在 opts.upgradeTimeout 时长内建立第一条双向流,服务端会主动关闭会话,防止半连接占用资源;
  3. 以双向流承载协议数据:服务端从 session.incomingBidirectionalStreams 读取客户端建好的第一条双向流,后续所有 engine.io 包都在这条流上收发。packages/engine.io/lib/server.ts 中的注释也明确说明:WebTransport 不走常规的 verify()(基于 HTTP 请求的传输协商)路径。

3.2 WebTransport 传输类:流式编解码

会话被接受后,引擎会为它创建一个 WebTransport 传输对象:

export class WebTransport extends Transport {
  constructor(session, stream, reader) {
    super({ _query: { EIO: "4" } });

    const transformStream = createPacketEncoderStream();
    transformStream.readable.pipeTo(stream.writable).catch(() => {
      debug("the stream was closed");
    });
    this.writer = transformStream.writable.getWriter();
    // ...读取循环:reader.read() -> this.onPacket(value)
  }
}

实现上有几个可验证的细节:

  • EIO: "4" 表明该实现遵循 Engine.IO 第 4 版协议,与 polling/WebSocket 传输处于同一协议代;
  • 发送方向采用 Web Streams 管道:createPacketEncoderStream()(来自 engine.io-parser)把结构化 Packet 对象编码为字节流,再 pipeTo(stream.writable) 写入双向流;send(packets) 只是把包写入 transform stream 的 writer,写完即触发 drainready 事件;
  • 接收方向是一个常驻的 reader.read() 循环,每读到一个解码后的包就调用 onPacket(value),交给上层 Socket 分发;
  • 会话关闭(session.closed)会映射为传输的 onClose(),与 engine.io 统一的生命周期事件模型保持一致。

四、客户端实现:从 polling 起步,升级到 QUIC

4.1 页面中的连接配置

index.html 的客户端脚本:

<script src="/socket.io/socket.io.js"></script>
<script>
  const socket = io({
    transportOptions: {
      webtransport: {
        hostname: "127.0.0.1"
      }
    }
  });

  socket.on("connect", () => {
    console.log(`connect ${socket.id}`);

    socket.io.engine.on("upgrade", (transport) => {
      console.log(`transport upgraded to ${transport.name}`);
    });
  });

  socket.on("connect_error", (err) => {
    console.log(`connect_error due to ${err.message}`);
  });

  socket.on("disconnect", (reason) => {
    console.log(`disconnect due to ${reason}`);
  });
</script>

要点说明:

  • 客户端库由服务端在 /socket.io/socket.io.js 直接伺服,无需本地构建;
  • io() 不带 transports 参数,因此客户端沿用默认策略:先用 polling 建立连接,之后按服务端允许的传输列表进行升级。transportOptions.webtransport.hostname 专门覆盖 WebTransport 连接使用的主机名——因为 QUIC 握手必须命中证书 CN 127.0.0.1,而页面 URL 是 localhost;
  • 示例同时监听三个事件:connect(连接成功)、engineupgrade(传输切换,可观察最终落在哪种传输)、disconnect(断开及原因)。

4.2 浏览器端 WT 传输的建连过程

客户端 WebTransport 实现在 packages/engine.io-client/lib/transports/webtransport.tsdoOpen() 中:

this._transport = new WebTransport(
  this.createUri("https"),
  this.opts.transportOptions[this.name],
);
// ...
this._transport.ready.then(() => {
  this._transport.createBidirectionalStream().then((stream) => {
    const decoderStream = createPacketDecoderStream(
      Number.MAX_SAFE_INTEGER,
      this.socket.binaryType,
    );
    const reader = stream.readable.pipeThrough(decoderStream).getReader();

    const encoderStream = createPacketEncoderStream();
    encoderStream.readable.pipeTo(stream.writable);
    this._writer = encoderStream.writable.getWriter();
    // 读取循环 -> this.onPacket(value)

    const packet: Packet = { type: "open" };
    if (this.query.sid) {
      packet.data = `{"sid":"${this.query.sid}"}`;
    }
    this._writer.write(packet).then(() => this.onOpen());
  });
});

与服务端形成镜像:浏览器内建 WebTransport 对象(其构造参数正是 transportOptions.webtransport 传入的对象)在 ready 后创建第一条双向流,用 engine.io-parser 的编解码 transform stream 挂接读写,最后写入一个 open 包完成引擎层握手——若属于已有会话的传输升级,open 包会携带 sid 以关联既有 Socket。这也解释了服务端 onWebTransportSession 中"等待客户端建立双向流"的那一步:客户端建流并写入 open 包,服务端读到该流后即完成会话到 Socket 的映射。

五、使用限制与注意事项

结合示例代码与引擎源码,实际部署时需要注意以下仓库内可确认的前提与限制:

  1. 浏览器支持:WebTransport 目前主要由 Chrome 系浏览器提供,因此示例必须通过 open_chrome.sh 以特定参数启动 Chrome;其他浏览器环境下该传输不可用,连接会退回 polling/WebSocket;
  2. middlewares 不兼容:如 packages/engine.io/lib/server.ts 所示,配置了 engine 中间件时 WebTransport 会话会被直接关闭;
  3. 超时控制:会话必须在 upgradeTimeout 内建立双向流,否则被服务端关闭,该值可通过引擎 opts.upgradeTimeout 调整;
  4. 端口复用:HTTPS 与 HTTP/3 使用同一端口号(默认 3000,可用环境变量 PORT 覆盖),分别占用 TCP 与 UDP,防火墙需同时放行两者;
  5. 证书要求:QUIC/TLS 1.3 对密钥类型有要求,示例采用 EC prime256v1 自签证书;生产环境应使用正规 CA 证书,且 WebTransport 主机名需与证书 SAN/CN 匹配;
  6. secret 参数:示例中 secret: "changeit" 为占位值,实际部署时应使用足够强的随机密钥。

六、小结

examples/webtransport/ 这个示例用不到百行代码演示了 Socket.IO 接入 WebTransport 传输的最小完整形态:HTTPS 服务器承载 polling/WebSocket 与静态页面,HTTP/3 服务器通过 sessionStream("/socket.io/") 捕获 QUIC 会话并交给 io.engine.onWebTransportSession() 接入引擎,客户端则只需在 io() 配置中指定 transportOptions.webtransport.hostname。配合 packages/engine.io/lib/transports/webtransport.tspackages/engine.io-client/lib/transports/webtransport.ts 中的传输实现,可以看到 WebTransport 在 engine.io 中被统一为"一条双向流 + engine.io-parser 编解码"的模型,从而与 polling、websocket 共享同一套 Socket 生命周期、升级与断开语义。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384