Socket.IO 4.8
概述
此技能收录了 Socket.IO 4.8 中容易出错的关键规则:用作用户键的临时 socket.id、重复注册的监听器、受 timeout() 影响的确认回调签名、不保护 WebSocket 的 CORS、负载均衡器后遗忘的粘性会话,以及 v2 或 v3 中不再兼容的 API。每个规则都附有来源页面的 URL。参考文件中包含了每个域名的详细信息。
版本覆盖
Socket.IO 4.8(服务器 socket.io,客户端 socket.io-client),文档 v4。JS 客户端 3.x 和 4.x 可与服务器 3.x 和 4.x 通信。https://socket.io/docs/v4/client-installation/#version-compatibility
快速开始
- 将 Socket.IO 绑定到你创建的 HTTP 服务器上,并在该服务器上调用
listen,绝不要在 Express 的app上调用:app.listen()会创建另一个 Socket.IO 未绑定的 HTTP 服务器。https://socket.io/docs/v4/server-initialization/#with-express
import express from "express";
import { createServer } from "node:http";
import { Server } from "socket.io";
const app = express();
const httpServer = createServer(app);
const io = new Server(httpServer, { cors: { origin: ["https://example.com"] } });
io.on("connection", (socket) => {
socket.on("hello", (arg, callback) => callback("got it"));
});
httpServer.listen(3000);
https://socket.io/docs/v4/server-initialization/#with-express
- 客户端:同源时使用
io(),跨域时使用io("https://server-domain.com")(必须在服务器上启用 CORS)。使用import { io } from "socket.io-client"导入。https://socket.io/docs/v4/client-initialization/#from-a-different-domain
import { io } from "socket.io-client";
const socket = io("https://server-domain.com", { auth: { token: "abc" } });
const answer = await socket.timeout(5000).emitWithAck("hello", "world");
https://socket.io/docs/v4/client-api/#socketemitwithackeventname-args
- 类型定义随
socket.io-client一起提供。不要安装@types/socket.io-client,它会与内置类型冲突。https://socket.io/docs/v4/client-installation/#from-npm
规则与陷阱
连接与身份
- Socket.IO 不是一个 WebSocket 实现:普通的 WebSocket 客户端无法连接 Socket.IO 服务器,Socket.IO 客户端也无法连接普通的 WebSocket 服务器。https://socket.io/docs/v4/#what-socketio-is-not
- 除非启用了连接状态恢复,否则
socket.id会在每次重连和页面刷新时改变,每个标签页都不同,并且没有消息队列。使用你自己的会话 ID(通过 cookie 或auth发送)来标识用户,并通过以用户 ID 命名的房间到达用户的每个标签页。https://socket.io/docs/v4/server-socket-instance/#socketid
io.on("connection", async (socket) => {
const userId = await computeUserIdFromHeaders(socket.handshake.headers);
socket.join(userId);
io.to(userId).emit("hi");
});
https://socket.io/docs/v4/rooms/#sample-use-cases
- 在连接中间件中使用
socket.handshake.auth进行身份验证。在每个代码路径中调用next(),否则连接会一直挂起,直到超时后被关闭。next(err)会作为connect_error事件到达客户端,err.data提供详细信息。https://socket.io/docs/v4/middlewares/
io.use((socket, next) => {
if (isValid(socket.handshake.auth.token)) return next();
const err = new Error("not authorized");
err.data = { content: "Please retry later" };
next(err);
});
https://socket.io/docs/v4/middlewares/#handling-middleware-error
- 被中间件拒绝的连接不会自动重试。在
connect_error事件中,socket.active === false表示连接被服务器拒绝,客户端必须自行调用socket.connect()来重新连接。https://socket.io/docs/v4/client-socket-instance/#connect_error - 在顶层注册客户端监听器,永远不要在
connect处理函数内注册:connect会在每次重连时再次触发,每次都会添加一个重复的监听器。https://socket.io/docs/v4/client-socket-instance/#connect - 在服务器端,在
connection处理函数中同步注册socket.on(...),在任何await之前,否则早期到达的数据包将无法被处理。https://socket.io/docs/v4/troubleshooting-connection-issues/#delayed-event-handler-registration - Express 中间件应使用
io.engine.use(...),而不是io.use(...)。https://socket.io/docs/v4/middlewares/#compatibility-with-express-middleware
事件与确认
- 按原样传递对象。永远不要对有效载荷调用
JSON.stringify()。Date会变成字符串,Map和Set必须手动转换。https://socket.io/docs/v4/emitting-events/#basic-emit - 确认回调是最后一个参数。没有
timeout()时,它接收(response);使用timeout(ms)时,它接收(err, response)。https://socket.io/docs/v4/emitting-events/#with-timeout - 优先使用
{try}块中的await socket.timeout(ms).emitWithAck(...):超时时会 reject。接收方调用其回调时传递 1 个参数。https://socket.io/docs/v4/client-api/#socketemitwithackeventname-args - 带确认的广播需要使用
timeout()并返回每个客户端的 1 个响应。https://socket.io/docs/v4/server-api/#serveremiteventname-args
const responses = await io.timeout(10000).emitWithAck("some-event");
https://socket.io/docs/v4/server-api/#serveremitwithackeventname-args
- 监听器没有内置的错误处理和验证。在每个处理函数中捕获错误,使用模式库验证有效载荷,并在使用回调前检查
typeof callback === "function"。https://socket.io/docs/v4/listening-to-events/#validation - 永远不要使用
connect、connect_error、disconnect、disconnecting、newListener或removeListener作为应用事件:触发诸如socket.emit("disconnecting")等保留名称会抛出错误。https://socket.io/docs/v4/emit-cheatsheet/
广播与房间
socket.to(room)和socket.broadcast排除发送者,io.to(room)不排除。socket.to(socket.id).emit()不发送任何内容:请使用io.to(socket.id).emit()。https://socket.io/docs/v4/server-api/#sockettoroom- 链式
to()调用是并集,每个套接字只接收一次事件。https://socket.io/docs/v4/rooms/#joining-and-leaving - 每个广播修饰符都返回一个新的不可变操作符:将其保存在变量中,而不是先调用
io.to()再调用io.emit()。https://socket.io/docs/v4/migrating-from-3-x-to-4-0/#ioto-is-now-immutable - 房间名称不会强制转换:
socket.join(42)不匹配io.to("42")。https://socket.io/docs/v4/migrating-from-2-x-to-3-0/#room-names-are-not-coerced-to-string-anymore - 断开连接时房间会自动退出。在
disconnecting事件中读取它们,而不是在disconnect中,因为此时socket.rooms已为空。https://socket.io/docs/v4/rooms/#disconnection - 使用
io.of("/").adapter读取适配器:文档警告io.adapter()用于设置,而不是读取。https://socket.io/docs/v4/adapter/#api
投递保证
- 默认保证是至多一次,服务器不存储消息:断开的客户端将永远错过广播。使用数据库中的偏移量(通过
auth发送)进行重放,或启用连接状态恢复。https://socket.io/docs/v4/delivery-guarantees#at-most-once - 客户端在离线时缓冲事件,并在重连时刷新。检查
socket.connected或使用socket.volatile.emit(...)发送可丢弃的数据。https://socket.io/docs/v4/client-offline-behavior/#buffered-events - 客户端到服务器至少一次:
io({ retries: 3, ackTimeout: 10000 })。要实现恰好一次,请在唯一约束下存储客户端生成的 ID。在成功和重复时确认,但不要在其它任何错误时确认,以便客户端超时并重试。https://socket.io/docs/v4/tutorial/step-8 - 连接状态恢复需要选择启用,需要有限的
maxDisconnectionDuration,并且可能失败:根据socket.recovered进行分支处理。Redis(发布/订阅)适配器不支持它,Redis Streams 适配器支持。https://socket.io/docs/v4/connection-state-recovery
const io = new Server(httpServer, {
connectionStateRecovery: { maxDisconnectionDuration: 2 * 60 * 1000, skipMiddlewares: false },
});
https://socket.io/docs/v4/connection-state-recovery#usage
CORS 与部署
- CORS 默认关闭。选项是
cors.origin,绝不是origins。结合credentials: true使用origin: "*"在浏览器中会失败:请列出明确的来源。https://socket.io/docs/v4/server-options/#cors - CORS 仅覆盖 HTTP 长轮询的浏览器,不覆盖 WebSocket。使用
allowRequest限制可以连接的用户。https://socket.io/docs/v4/handling-cors/ - 代理必须转发
Upgrade和Connection标头,并且其读取超时时间必须超过pingInterval + pingTimeout(默认 45 秒),否则客户端会看到transport close。https://socket.io/docs/v4/reverse-proxy/#nginx - 当有多个服务器且启用了 HTTP 长轮询时,无论使用什么适配器,仍然需要粘性会话(替代方案是禁用长轮询或客户端负载均衡)。https://socket.io/docs/v4/using-multiple-nodes/ 和 https://socket.io/docs/v4/adapter/
- 没有粘性会话时,客户端会收到 HTTP 400 响应,内容为
{"code":1,"message":"Session ID unknown"}。https://socket.io/docs/v4/troubleshooting-connection-issues/#you-didnt-enable-sticky-sessions-in-a-multi-server-setup path(默认为/socket.io/)必须在两端匹配。io("/my-path/")选择一个命名空间,而不是一个路径。https://socket.io/docs/v4/troubleshooting-connection-issues/#the-request-path-does-not-match-on-both-sides- 使用
io.close()关闭,它也会关闭 HTTP 服务器。https://socket.io/docs/v4/server-api/#serverclosecallback
参考文件
| 文件 | 阅读内容 |
|---|---|
| references/server-setup.md | 安装、使用 Express、Koa、Fastify、Hono、HTTPS、HTTP/2、Bun、uWebSockets.js 进行初始化、服务器选项和默认值、打包、处理程序布局 |
| references/middleware-and-auth.md | 连接、套接字和引擎中间件、凭据、用户身份、标头钩子、allowRequest、客户端证书 |
| references/cors-and-proxies.md | CORS 选项和错误、Cookie、nginx、Apache、Caddy、http-proxy、云负载均衡器、与 ws 共享端口 |
| references/server-api.md | 服务器和套接字实例、io 上的命名空间、实用方法、serverSideEmit、断开原因、引擎事件和错误代码、关闭 |
| references/client.md | 包、初始化、套接字事件、connect_error 和 disconnect、Manager 和多路复用、每个客户端选项、打包工具 |
| references/events-rooms-namespaces.md | 序列化、确认、全捕获监听器、验证、错误处理、广播、房间、动态命名空间 |
| references/delivery-and-recovery.md | 投递保证、重试、恰好一次、偏移量重放、连接状态恢复 |
| references/multiple-nodes.md | 粘性会话、多个节点的代理配置、替代方案、Node.js 集群、PM2 |
| references/adapters.md | Redis、分片 Redis、Redis Streams、Postgres、MongoDB、Cluster、AWS SQS、Azure、GCP 适配器、功能矩阵、发射器 |
| references/performance-and-operations.md | 原生插件、wsEngine、内存、自定义解析器、操作系统限制、负载测试、管理 UI、调试日志 |
| references/typescript.md | 服务器、客户端和命名空间上的事件接口、SocketData、类型化测试 |
| references/testing-and-troubleshooting.md | 服务器集成测试、使用虚假套接字的客户端测试、连接失败、断开、长轮询卡住 |
| references/migrations-and-protocol.md | v2 到 v3 和 v3 到 v4 的更改、使用 allowEIO3 滚动升级、Engine.IO 和 Socket.IO 线协议 |