Socket.IO 与 WebTransport
WebTransport 支持已在版本 4.7.0(2023 年 6 月)中添加。
简而言之,WebTransport 是 WebSocket 的替代方案,它修复了困扰 WebSocket 的一些性能问题,例如 队头阻塞。
如果您想了解更多关于此新 Web API 的信息,请查看
在本指南中,我们将创建一个接受 WebTransport 连接的 Socket.IO 服务器。
我们开始吧!
要求
请使用至少 Node.js 18(在撰写本文时,当前 LTS 版本)。
SSL 证书
首先,让我们为我们的项目创建一个新目录
mkdir webtransport-sample-project && cd webtransport-sample-project
WebTransport 仅在安全上下文中(HTTPS)工作,因此我们需要一个 SSL 证书。
您可以运行以下命令来颁发新证书
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
参考:https://www.openssl.org/docs/man3.1/man1/openssl-req.html
这将生成一个私钥和一个证书,它们符合 此处 列出的要求
- 有效期总长度不得超过两周
- 允许的公钥算法的精确列表[...]必须包括使用 secp256r1(NIST P-256)命名组的 ECDSA
好的,所以您现在应该拥有
.
├── cert.pem
└── key.pem
基本 HTTPS 服务器
然后,让我们创建一个基本的 Node.js HTTPS 服务器
{
"name": "webtransport-sample-project",
"version": "0.0.1",
"description": "Socket.IO with WebTransport",
"private": true,
"type": "module"
}
import { readFile } from "node:fs/promises";
import { createServer } from "node:https";
const key = await readFile("./key.pem");
const cert = await readFile("./cert.pem");
const httpsServer = createServer({
key,
cert
}, async (req, res) => {
if (req.method === "GET" && req.url === "/") {
const content = await readFile("./index.html");
res.writeHead(200, {
"content-type": "text/html"
});
res.write(content);
res.end();
} else {
res.writeHead(404).end();
}
});
const port = process.env.PORT || 3000;
httpsServer.listen(port, () => {
console.log(`server listening at https://localhost:${port}`);
});
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Socket.IO WebTransport example</title>
</head>
<body>
Hello world!
</body>
</html>
这里没什么特别的,我们只是在 /
上提供 index.html
文件的内容,并在其他情况下返回 HTTP 404 错误代码。
参考:https://node.org.cn/api/https.html
您可以通过运行 node index.js
来启动服务器
$ node index.js
server listening at https://localhost:3000
现在,让我们打开一个新的浏览器窗口
#!/bin/bash
HASH=`openssl x509 -pubkey -noout -in cert.pem |
openssl pkey -pubin -outform der |
openssl dgst -sha256 -binary |
base64`
chromium \
--ignore-certificate-errors-spki-list=$HASH \
https://localhost:3000
--ignore-certificate-errors-spki-list
标志告诉 Chromium 接受我们的自签名证书,而不会抱怨
我们的 SSL 证书确实被认为是有效的
太棒了!您现在应该拥有
.
├── cert.pem
├── index.html
├── index.js
├── key.pem
├── open_browser.sh
└── package.json
Socket.IO 服务器
现在,让我们安装 socket.io
包
npm i socket.io
我们现在创建一个 Socket.IO 服务器并将其附加到我们现有的 HTTPS 服务器
import { readFile } from "node:fs/promises";
import { createServer } from "node:https";
import { Server } from "socket.io";
const key = await readFile("./key.pem");
const cert = await readFile("./cert.pem");
const httpsServer = createServer({
key,
cert
}, async (req, res) => {
if (req.method === "GET" && req.url === "/") {
const content = await readFile("./index.html");
res.writeHead(200, {
"content-type": "text/html"
});
res.write(content);
res.end();
} else {
res.writeHead(404).end();
}
});
const port = process.env.PORT || 3000;
httpsServer.listen(port, () => {
console.log(`server listening at https://localhost:${port}`);
});
const io = new Server(httpsServer);
io.on("connection", (socket) => {
console.log(`connected with transport ${socket.conn.transport.name}`);
socket.conn.on("upgrade", (transport) => {
console.log(`transport upgraded to ${transport.name}`);
});
socket.on("disconnect", (reason) => {
console.log(`disconnected due to ${reason}`);
});
});
让我们相应地更新客户端
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Socket.IO WebTransport example</title>
</head>
<body>
<p>Status: <span id="status">Disconnected</span></p>
<p>Transport: <span id="transport">N/A</span></p>
<script src="/socket.io/socket.io.js"></script>
<script>
const $status = document.getElementById("status");
const $transport = document.getElementById("transport");
const socket = io();
socket.on("connect", () => {
console.log(`connected with transport ${socket.io.engine.transport.name}`);
$status.innerText = "Connected";
$transport.innerText = socket.io.engine.transport.name;
socket.io.engine.on("upgrade", (transport) => {
console.log(`transport upgraded to ${transport.name}`);
$transport.innerText = 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}`);
$status.innerText = "Disconnected";
$transport.innerText = "N/A";
});
</script>
</body>
</html>
一些解释
- 客户端捆绑包
<script src="/socket.io/socket.io.js"></script>
Socket.IO 客户端捆绑包由服务器在 /socket.io/socket.io.js
上提供。
我们也可以使用最小化捆绑包(/socket.io/socket.io.min.js
,没有调试日志)或 CDN(例如 https://cdn.socket.io/4.7.2/socket.io.min.js)。
- 传输
socket.on("connect", () => {
console.log(`connected with transport ${socket.io.engine.transport.name}`);
// ...
});
在 Socket.IO 行话中,传输是建立客户端和服务器之间连接的一种方式。从 4.7.0 版本开始,现在有 3 种可用的传输方式
- HTTP 长轮询
- WebSocket
- WebTransport
默认情况下,Socket.IO 客户端将始终首先尝试 HTTP 长轮询,因为它是最有可能成功建立连接的传输方式。然后,它将静默升级到性能更高的传输方式,例如 WebSocket 或 WebTransport。
有关此升级机制的更多信息,请参阅 此处。
好的,所以让我们重新启动我们的服务器。您现在应该看到
到目前为止,一切都很好。
WebTransport
在客户端,WebTransport 目前在所有主要浏览器中都可用,但 Safari 除外:https://caniuse.cn/webtransport
在服务器端,直到 WebTransport 支持 在 Node.js 中(以及 在 Deno 中),我们可以使用由 Marten Richter 维护的 @fails-components/webtransport
包。
npm i @fails-components/webtransport @fails-components/webtransport-transport-http3-quiche
来源:https://github.com/fails-components/webtransport
让我们创建一个 HTTP/3 服务器并将 WebTransport 会话转发到 Socket.IO 服务器
import { readFile } from "node:fs/promises";
import { createServer } from "node:https";
import { Server } from "socket.io";
import { Http3Server } from "@fails-components/webtransport";
const key = await readFile("./key.pem");
const cert = await readFile("./cert.pem");
const httpsServer = createServer({
key,
cert
}, async (req, res) => {
if (req.method === "GET" && req.url === "/") {
const content = await readFile("./index.html");
res.writeHead(200, {
"content-type": "text/html"
});
res.write(content);
res.end();
} else {
res.writeHead(404).end();
}
});
const port = process.env.PORT || 3000;
httpsServer.listen(port, () => {
console.log(`server listening at https://localhost:${port}`);
});
const io = new Server(httpsServer, {
transports: ["polling", "websocket", "webtransport"]
});
io.on("connection", (socket) => {
console.log(`connected with transport ${socket.conn.transport.name}`);
socket.conn.on("upgrade", (transport) => {
console.log(`transport upgraded to ${transport.name}`);
});
socket.on("disconnect", (reason) => {
console.log(`disconnected due to ${reason}`);
});
});
const h3Server = new Http3Server({
port,
host: "0.0.0.0",
secret: "changeit",
cert,
privKey: key,
});
h3Server.startServer();
(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);
}
})();
这应该已经足够了,但浏览器中仍然存在错误
如果有人知道这个问题,请告诉我们。
即使 WebTransport 失败(如果客户端和服务器之间的某些内容阻止了连接,也可能发生这种情况),连接也会使用 WebSocket 成功建立。
一个快速的解决方法是使用 127.0.0.1
而不是 localhost
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Socket.IO WebTransport example</title>
</head>
<body>
<p>Status: <span id="status">Disconnected</span></p>
<p>Transport: <span id="transport">N/A</span></p>
<script src="/socket.io/socket.io.js"></script>
<script>
const $status = document.getElementById("status");
const $transport = document.getElementById("transport");
const socket = io({
transportOptions: {
webtransport: {
hostname: "127.0.0.1"
}
}
});
socket.on("connect", () => {
console.log(`connected with transport ${socket.io.engine.transport.name}`);
$status.innerText = "Connected";
$transport.innerText = socket.io.engine.transport.name;
socket.io.engine.on("upgrade", (transport) => {
console.log(`transport upgraded to ${transport.name}`);
$transport.innerText = 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}`);
$status.innerText = "Disconnected";
$transport.innerText = "N/A";
});
</script>
</body>
</html>
#!/bin/bash
HASH=`openssl x509 -pubkey -noout -in cert.pem |
openssl pkey -pubin -outform der |
openssl dgst -sha256 -binary |
base64`
chromium \
--ignore-certificate-errors-spki-list=$HASH \
--origin-to-force-quic-on=127.0.0.1:3000 \
https://localhost:3000
然后,瞧!
结论
就像 10 多年前的 WebSocket 一样 (!),Socket.IO 现在允许您从 WebTransport 带来的性能改进中受益,**而无需担心浏览器兼容性**。
感谢您的阅读!