DEBUG.WIKI
OnlineSIM:在线接收短信,虚拟号码租用
Evoxt;最高 6.0GHz CPU;高频云服务器 · 免费每周备份;查看配置 →
RackNerd;$21.99/年 · 1GB KVM

Socket.IO 确认回调首参变成 null,检查 timeout 影响了哪次发送

socket.io-client 4.8.4 的 timeout 修改同一实例,标记由下一次 emit 消耗;注册确认时决定签名,连接时机不是唯一原因。

Socket.IO 客户端里,同一个服务端回调明明只返回一个值,某处收到的却是 null,真正的值跑到第二个参数;另一次发送又恢复成一个参数。先查同一个 socket 上是否单独调用过 socket.timeout(1000),然后隔了一段代码才调用 emit()。

在 socket.io-client 4.8.4 中,timeout() 修改当前 socket 的发送 flags,并返回同一个对象。它不是一个独立的超时配置对象;裸调用留下的标记会影响下一次发送,并在那次 emit() 结束时清空。

参数形状在注册确认回调时确定

未使用超时设置时,服务端 callback('pong') 对应普通的单参数确认回调。设置了本次 timeout() 或全局 ackTimeout 后,客户端需要传递超时、断开等错误,成功回调就采用错误优先形式:(null, 'pong')。

对已进入发送缓冲区的消息,这个选择发生在调用 emit()、注册确认回调时,不是连上服务器后实际发送时。因而“连接之前或之后”本身不足以解释参数差异。

没有全局 ackTimeout、且未启用 retries 的普通 emit 路径中,下面的调用关系容易让人误判:

socket.emit('test', (value) => {
  console.log(value);
});

socket.timeout(1000);

// 中间经过其他代码后,这一次发送消耗了上面的 timeout 标记
socket.emit('test', (error, value) => {
  if (error) return;
  console.log(value);
});

socket 是应用已有连接。这不要求两条消息恰好分列 connect 事件两侧;如果别处先发送了消息,标记也可能被那一次消耗。

把超时和对应发送放在一起

对需要超时的消息,紧邻写出调用与错误优先回调:

socket.timeout(1000).emit('test', (error, value) => {
  if (error) {
    handleAcknowledgementFailure(error);
    return;
  }
  handleAcknowledgement(value);
});

两个 handle 函数代表应用已有处理逻辑。不要把 socket.timeout() 的返回值长期存成“专属发送器”,再与其他代码共享原 socket;4.8.4 返回的仍是同一实例。

配置了全局 ackTimeout 时,所有受它影响的确认回调都应按错误优先方式接收。不能为了让第一个参数重新成为业务值,直接删掉错误参数;发生超时或断开时,那样会把 Error 当成响应。

检查时保留服务端真实确认参数,覆盖连接前缓冲、已连接发送和超时未确认三条路径。再连续发送两次,确认裸 flags 不会泄漏到非预期消息;普通发送与超时发送各自使用正确签名。超时只表示限定时间内没有得到确认,不能据此判定服务端业务完全未执行,重试具有副作用的事件仍需业务去重。