解决连接问题
常见/已知问题:
¥Common/known issues:
其他常见问题:
¥Other common gotchas:
问题:套接字无法连接
¥Problem: the socket is not able to connect
故障排除步骤
¥Troubleshooting steps
在客户端,connect_error
事件提供了附加信息:
¥On the client side, the connect_error
event provides additional information:
socket.on("connect_error", (err) => {
// the reason of the error, for example "xhr poll error"
console.log(err.message);
// some additional description, for example the status code of the initial HTTP response
console.log(err.description);
// some additional context, for example the XMLHttpRequest object
console.log(err.context);
});
在服务器端,connection_error
事件也可能提供一些额外的见解:
¥On the server side, the connection_error
event may also provide some additional insights:
io.engine.on("connection_error", (err) => {
console.log(err.req); // the request object
console.log(err.code); // the error code, for example 1
console.log(err.message); // the error message, for example "Session ID unknown"
console.log(err.context); // some additional error context
});
以下是可能的错误代码列表:
¥Here is the list of possible error codes:
代码 | 消息 | 可能的解释 |
---|---|---|
0 | "运输未知" | 正常情况下不应发生这种情况。 |
1 | "会话 ID 未知" | 通常,这意味着粘性会话未启用(参见 如下)。 |
2 | "错误的握手方式" | 正常情况下不应发生这种情况。 |
3 | "错误的请求" | 通常,这意味着服务器前面的代理没有正确转发 WebSocket 标头(请参阅 此处)。 |
4 | "禁止" | 连接被 allowRequest() 方法拒绝。 |
5 | "不支持的协议版本" | 客户端版本与服务器不兼容(参见 此处)。 |
可能的解释
¥Possible explanations
你正在尝试访问普通的 WebSocket 服务器
¥You are trying to reach a plain WebSocket server
如 "Socket.IO 不是什么" 部分所述,Socket.IO 客户端不是 WebSocket 实现,因此无法与 WebSocket 服务器建立连接,即使使用 transports: ["websocket"]
也是如此:
¥As explained in the "What Socket.IO is not" section, the Socket.IO client is not a WebSocket implementation and thus will not be able to establish a connection with a WebSocket server, even with transports: ["websocket"]
:
const socket = io("ws://echo.websocket.org", {
transports: ["websocket"]
});
服务器无法访问
¥The server is not reachable
请确保 Socket.IO 服务器实际上可以通过给定的 URL 访问。你可以使用以下方法进行测试:
¥Please make sure the Socket.IO server is actually reachable at the given URL. You can test it with:
curl "<the server URL>/socket.io/?EIO=4&transport=polling"
它应该返回类似这样的内容:
¥which should return something like this:
0{"sid":"Lbo5JLzTotvW3g2LAAAA","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":20000}
如果情况并非如此,请检查 Socket.IO 服务器是否正在运行,以及中间是否存在任何阻止连接的因素。
¥If that's not the case, please check that the Socket.IO server is running, and that there is nothing in between that prevents the connection.
v1/v2 服务器(实现协议的 v3,因此是 EIO=3
)将返回如下内容:
¥v1/v2 servers (which implement the v3 of the protocol, hence the EIO=3
) will return something like this:
96:0{"sid":"ptzi_578ycUci8WLB9G1","upgrades":["websocket"],"pingInterval":25000,"pingTimeout":5000}2:40
客户端与服务器版本不兼容
¥The client is not compatible with the version of the server
保持向后兼容性是我们的首要任务,但在某些特殊情况下,我们必须在协议级别实现一些重大更改:
¥Maintaining backward compatibility is a top priority for us, but in some particular cases we had to implement some breaking changes at the protocol level:
从 v1.x 到 v2.0.0(2017 年 5 月发布),提高与非 Javascript 客户端的兼容性(参见 此处)
¥from v1.x to v2.0.0 (released in May 2017), to improve the compatibility with non-Javascript clients (see here)
从 v2.x 到 v3.0.0(2020 年 11 月发布),一次性修复协议中一些长期存在的问题(见 此处)
¥from v2.x to v3.0.0 (released in November 2020), to fix some long-standing issues in the protocol once for all (see here)
例如,使用 v1/v2 客户端到达 v3/v4 服务器将导致以下响应:
¥For example, reaching a v3/v4 server with a v1/v2 client will result in the following response:
< HTTP/1.1 400 Bad Request
< Content-Type: application/json
{"code":5,"message":"Unsupported protocol version"}
以下是 JS 客户端 的兼容性表:
¥Here is the compatibility table for the JS client:
JS 客户端版本 | Socket.IO 服务器版本 | |||
---|---|---|---|---|
1.x | 2.x | 3.x | 4.x | |
1.x | 是的 | 不 | 不 | 不 |
2.x | 不 | 是的 | 是的1 | 是的1 |
3.x | 不 | 不 | 是的 | 是的 |
4.x | 不 | 不 | 是的 | 是的 |
[1] 是的,与 允许 EIO3:true 一起
¥[1] Yes, with allowEIO3: true
以下是 Java 客户端 的兼容性表:
¥Here is the compatibility table for the Java client:
Java 客户端版本 | Socket.IO 服务器版本 | ||
---|---|---|---|
2.x | 3.x | 4.x | |
1.x | 是的 | 是的1 | 是的1 |
2.x | 不 | 是的 | 是的 |
[1] 是的,与 允许 EIO3:true 一起
¥[1] Yes, with allowEIO3: true
以下是 迅捷客户端 的兼容性表:
¥Here is the compatibility table for the Swift client:
Swift 客户端版本 | Socket.IO 服务器版本 | ||
---|---|---|---|
2.x | 3.x | 4.x | |
v15.x | 是的 | 是的1 | 是的2 |
v16.x | 是的3 | 是的 | 是的 |
[1] 是的,使用 允许 EIO3:true(服务器)和 .connectParams(["EIO": "3"])
(客户端):
¥[1] Yes, with allowEIO3: true (server) and .connectParams(["EIO": "3"])
(client):
SocketManager(socketURL: URL(string:"http://localhost:8087/")!, config: [.connectParams(["EIO": "3"])])
[2] 是的,允许 EIO3:true(服务器)
¥[2] Yes, allowEIO3: true (server)
[3] 是的,与 .version(.two)
(客户端):
¥[3] Yes, with .version(.two)
(client):
SocketManager(socketURL: URL(string:"http://localhost:8087/")!, config: [.version(.two)])
服务器不发送必要的 CORS 标头
¥The server does not send the necessary CORS headers
如果你在控制台中看到以下错误:
¥If you see the following error in your console:
Cross-Origin Request Blocked: The Same Origin Policy disallows reading the remote resource at ...
大概意思就是:
¥It probably means that:
要么你实际上没有到达 Socket.IO 服务器(参见 above)
¥either you are not actually reaching the Socket.IO server (see above)
或者你没有在服务器端启用 跨域资源共享 (CORS)。
¥or you didn't enable Cross-Origin Resource Sharing (CORS) on the server-side.
请参阅文档 此处。
¥Please see the documentation here.
你没有启用粘性会话(在多服务器设置中)
¥You didn't enable sticky sessions (in a multi server setup)
当扩展到多个 Socket.IO 服务器时,你需要确保给定 Socket.IO 会话的所有请求都到达同一台 Socket.IO 服务器。解释可以参见 此处。
¥When scaling to multiple Socket.IO servers, you need to make sure that all the requests of a given Socket.IO session reach the same Socket.IO server. The explanation can be found here.
如果不这样做,将导致 HTTP 400 响应,代码如下:{"code":1,"message":"Session ID unknown"}
¥Failure to do so will result in HTTP 400 responses with the code: {"code":1,"message":"Session ID unknown"}
请参阅文档 此处。
¥Please see the documentation here.
请求路径两边不匹配
¥The request path does not match on both sides
默认情况下,客户端发送(并且服务器期望)带有 "/socket.io/" 请求路径的 HTTP 请求。
¥By default, the client sends — and the server expects — HTTP requests with the "/socket.io/" request path.
这可以通过 path
选项进行控制:
¥This can be controlled with the path
option:
服务器
¥Server
import { Server } from "socket.io";
const io = new Server({
path: "/my-custom-path/"
});
io.listen(3000);
客户端
¥Client
import { io } from "socket.io-client";
const socket = io(SERVER_URL, {
path: "/my-custom-path/"
});
在这种情况下,HTTP 请求将类似于 <SERVER_URL>/my-custom-path/?EIO=4&transport=polling[&...]
。
¥In that case, the HTTP requests will look like <SERVER_URL>/my-custom-path/?EIO=4&transport=polling[&...]
.
问题:套接字断开连接
¥Problem: the socket gets disconnected
故障排除步骤
¥Troubleshooting steps
首先,请注意,即使在稳定的互联网连接上,断开连接也是常见且预期的情况:
¥First and foremost, please note that disconnections are common and expected, even on a stable Internet connection:
用户和 Socket.IO 服务器之间的任何事情都可能会遇到临时故障或重新启动
¥anything between the user and the Socket.IO server may encounter a temporary failure or be restarted
作为自动缩放策略的一部分,服务器本身可能会被终止
¥the server itself may be killed as part of an autoscaling policy
如果使用移动浏览器,用户可能会失去连接或从 WiFi 切换到 4G
¥the user may lose connection or switch from WiFi to 4G, in case of a mobile browser
浏览器本身可能会冻结非活动选项卡
¥the browser itself may freeze an inactive tab
话虽这么说,Socket.IO 客户端将始终尝试重新连接,除非明确告知 otherwise。
¥That being said, the Socket.IO client will always try to reconnect, unless specifically told otherwise.
disconnect
事件提供了更多信息:
¥The disconnect
event provides additional information:
socket.on("disconnect", (reason, details) => {
// the reason of the disconnection, for example "transport error"
console.log(reason);
// the low-level reason of the disconnection, for example "xhr post error"
console.log(details.message);
// some additional description, for example the status code of the HTTP response
console.log(details.description);
// some additional context, for example the XMLHttpRequest object
console.log(details.context);
});
可能的原因列出 此处。
¥The possible reasons are listed here.
可能的解释
¥Possible explanations
服务器和客户端之间的某些操作关闭了连接
¥Something between the server and the client closes the connection
如果定期发生断开连接,则可能表明服务器和客户端之间的某些内容未正确配置并关闭连接:
¥If the disconnection happens at a regular interval, this might indicate that something between the server and the client is not properly configured and closes the connection:
- nginx
nginx 的 proxy_read_timeout
(默认 60 秒)的值必须大于 Socket.IO 的 pingInterval + pingTimeout
(默认 45 秒),否则如果在给定的延迟后没有发送数据,它将强制关闭连接,客户端将收到 "传输关闭" 错误 。
¥The value of nginx's proxy_read_timeout
(60 seconds by default) must be bigger than Socket.IO's pingInterval + pingTimeout
(45 seconds by default), else it will forcefully close the connection if no data is sent after the given delay and the client will get a "transport close" error.
Apache HTTP 服务器
¥Apache HTTP Server
httpd 的 ProxyTimeout
(默认 60 秒)的值必须大于 Socket.IO 的 pingInterval + pingTimeout
(默认 45 秒),否则如果在给定的延迟后没有发送数据,它将强制关闭连接,客户端将收到 "传输关闭" 错误 。
¥The value of httpd's ProxyTimeout
(60 seconds by default) must be bigger than Socket.IO's pingInterval + pingTimeout
(45 seconds by default), else it will forcefully close the connection if no data is sent after the given delay and the client will get a "transport close" error.
浏览器选项卡已最小化且心跳失败
¥The browser tab was minimized and heartbeat has failed
当浏览器选项卡未获得焦点时,某些浏览器(例如 Chrome)会限制 JavaScript 计时器,这可能会导致 Socket.IO v2 中的 ping 超时而断开连接,因为心跳机制依赖于客户端的 setTimeout
功能。
¥When a browser tab is not in focus, some browsers (like Chrome) throttle JavaScript timers, which could lead to a disconnection by ping timeout in Socket.IO v2, as the heartbeat mechanism relied on setTimeout
function on the client side.
作为解决方法,你可以增加服务器端的 pingTimeout
值:
¥As a workaround, you can increase the pingTimeout
value on the server side:
const io = new Server({
pingTimeout: 60000
});
请注意,升级到 Socket.IO v4(至少 socket.io-client@4.1.3
,由于 this)应该可以防止此类问题,因为心跳机制已被逆转(服务器现在发送 PING 数据包)。
¥Please note that upgrading to Socket.IO v4 (at least socket.io-client@4.1.3
, due to this) should prevent this kind of issues, as the heartbeat mechanism has been reversed (the server now sends PING packets).
客户端与服务器版本不兼容
¥The client is not compatible with the version of the server
由于通过 WebSocket 传输发送的数据包格式在 v2 和 v3/v4 中类似,因此你可能能够与不兼容的客户端连接(请参阅 above),但连接最终将在给定的延迟后关闭。
¥Since the format of the packets sent over the WebSocket transport is similar in v2 and v3/v4, you might be able to connect with an incompatible client (see above), but the connection will eventually be closed after a given delay.
因此,如果你遇到 30 秒后定期断开连接的情况(这是 Socket.IO v2 中 pingTimeout 和 pingInterval 值的总和),这肯定是由于版本不兼容造成的。
¥So if you are experiencing a regular disconnection after 30 seconds (which was the sum of the values of pingTimeout and pingInterval in Socket.IO v2), this is certainly due to a version incompatibility.
你正在尝试发送巨大的有效负载
¥You are trying to send a huge payload
如果你在发送巨大负载时断开连接,这可能意味着你已达到 maxHttpBufferSize
值,默认为 1 MB。请根据你的需要进行调整:
¥If you get disconnected while sending a huge payload, this may mean that you have reached the maxHttpBufferSize
value, which defaults to 1 MB. Please adjust it according to your needs:
const io = require("socket.io")(httpServer, {
maxHttpBufferSize: 1e8
});
上传时间超过 pingTimeout
选项值的巨大有效负载也可能会触发断开连接(因为 心跳机制 在上传过程中失败)。请根据你的需要进行调整:
¥A huge payload taking more time to upload than the value of the pingTimeout
option can also trigger a disconnection (since the heartbeat mechanism fails during the upload). Please adjust it according to your needs:
const io = require("socket.io")(httpServer, {
pingTimeout: 60000
});
问题:套接字卡在 HTTP 长轮询中
¥Problem: the socket is stuck in HTTP long-polling
故障排除步骤
¥Troubleshooting steps
在大多数情况下,你应该看到类似这样的内容:
¥In most cases, you should see something like this:
Engine.IO 握手(包含会话 ID — 此处为
zBjrh...AAAK
— 用于后续请求)¥the Engine.IO handshake (contains the session ID — here,
zBjrh...AAAK
— that is used in subsequent requests)Socket.IO 握手请求(包含
auth
选项的值)¥the Socket.IO handshake request (contains the value of the
auth
option)Socket.IO 握手响应(包含 Socket#id)
¥the Socket.IO handshake response (contains the Socket#id)
WebSocket 连接
¥the WebSocket connection
第一个 HTTP 长轮询请求,一旦 WebSocket 连接建立就关闭
¥the first HTTP long-polling request, which is closed once the WebSocket connection is established
如果你没有看到第四个请求的 HTTP 101 切换协议 响应,则意味着服务器和浏览器之间的某些原因阻止了 WebSocket 连接。
¥If you don't see a HTTP 101 Switching Protocols response for the 4th request, that means that something between the server and your browser is preventing the WebSocket connection.
请注意,这不一定是阻塞,因为连接仍然是通过 HTTP 长轮询建立的,但效率较低。
¥Please note that this is not necessarily blocking since the connection is still established with HTTP long-polling, but it is less efficient.
你可以通过以下方式获取当前传输的名称:
¥You can get the name of the current transport with:
客户端
¥Client-side
socket.on("connect", () => {
const transport = socket.io.engine.transport.name; // in most cases, "polling"
socket.io.engine.on("upgrade", () => {
const upgradedTransport = socket.io.engine.transport.name; // in most cases, "websocket"
});
});
服务器端
¥Server-side
io.on("connection", (socket) => {
const transport = socket.conn.transport.name; // in most cases, "polling"
socket.conn.on("upgrade", () => {
const upgradedTransport = socket.conn.transport.name; // in most cases, "websocket"
});
});
可能的解释
¥Possible explanations
服务器前面的代理不接受 WebSocket 连接
¥A proxy in front of your servers does not accept the WebSocket connection
如果像 nginx 或 Apache HTTPD 这样的代理没有正确配置为接受 WebSocket 连接,那么你可能会收到 TRANSPORT_MISMATCH
错误:
¥If a proxy like nginx or Apache HTTPD is not properly configured to accept WebSocket connections, then you might get a TRANSPORT_MISMATCH
error:
io.engine.on("connection_error", (err) => {
console.log(err.code); // 3
console.log(err.message); // "Bad request"
console.log(err.context); // { name: 'TRANSPORT_MISMATCH', transport: 'websocket', previousTransport: 'polling' }
});
这意味着 Socket.IO 服务器没有收到必要的 Connection: upgrade
标头(你可以检查 err.req.headers
对象)。
¥Which means that the Socket.IO server does not receive the necessary Connection: upgrade
header (you can check the err.req.headers
object).
请参阅文档 此处。
¥Please see the documentation here.
express-status-monitor
运行自己的 socket.io 实例
¥express-status-monitor
runs its own socket.io instance
请参阅解决方案 此处。
¥Please see the solution here.
其他常见问题
¥Other common gotchas
重复的事件注册
¥Duplicate event registration
在客户端,每次套接字重新连接时都会触发 connect
事件,因此必须在 connect
事件监听器之外注册事件监听器:
¥On the client side, the connect
event will be emitted every time the socket reconnects, so the event listeners must be registered outside the connect
event listener:
不好:警告:
¥BAD ⚠️
socket.on("connect", () => {
socket.on("foo", () => {
// ...
});
});
GOOD 👍
socket.on("connect", () => {
// ...
});
socket.on("foo", () => {
// ...
});
如果情况并非如此,你的事件监听器可能会被多次调用。
¥If that's not the case, your event listener might be called multiple times.
延迟事件处理程序注册
¥Delayed event handler registration
不好:警告:
¥BAD ⚠️
io.on("connection", async (socket) => {
await longRunningOperation();
// WARNING! Some packets might be received by the server but without handler
socket.on("hello", () => {
// ...
});
});
GOOD 👍
io.on("connection", async (socket) => {
socket.on("hello", () => {
// ...
});
await longRunningOperation();
});
socket.id
属性的用法
¥Usage of the socket.id
attribute
请注意,除非启用 连接状态恢复,否则 id
属性是一个临时 ID,不适合在你的应用中使用(或仅用于调试目的),因为:
¥Please note that, unless connection state recovery is enabled, the id
attribute is an ephemeral ID that is not meant to be used in your application (or only for debugging purposes) because:
每次重新连接后都会重新生成此 ID(例如当 WebSocket 连接被切断时,或者当用户刷新页面时)
¥this ID is regenerated after each reconnection (for example when the WebSocket connection is severed, or when the user refreshes the page)
两个不同的浏览器选项卡将有两个不同的 ID
¥two different browser tabs will have two different IDs
服务器上没有为给定 ID 存储消息队列(即如果客户端断开连接,从服务器发送到该 ID 的消息就会丢失)
¥there is no message queue stored for a given ID on the server (i.e. if the client is disconnected, the messages sent from the server to this ID are lost)
请改用常规会话 ID(在 cookie 中发送,或存储在 localStorage 中并在 auth
有效负载中发送)。
¥Please use a regular session ID instead (either sent in a cookie, or stored in the localStorage and sent in the auth
payload).
也可以看看:
¥See also:
在无服务器平台上部署
¥Deployment on a serverless platform
由于大多数无服务器平台(例如 Vercel)按请求处理程序的持续时间计费,因此不建议与 Socket.IO(甚至普通 WebSocket)维持长期运行的连接。
¥Since most serverless platforms (such as Vercel) bill by the duration of the request handler, maintaining a long-running connection with Socket.IO (or even plain WebSocket) is not recommended.
参考:
¥References: