DEBUG.WIKI
ZgoCloud:多地区部署,按需选节点;日本、美国、香港、德国
搬瓦工;先看线路,再选 VPS;CN2 GIA · 对应线路套餐;查看线路 →
V.PS:让项目靠近用户,亚太、欧洲、北美 KVM VPS

ws send 返回后复用 Uint8Array,客户端偶尔收到被改写的消息

send可能仍在排队或等待socket写出,subarray仍共享原缓冲。按每条消息复制Uint8Array,或等对应发送回调后回收有界缓冲,保留背压和应用确认。

WebSocket 服务端调用 send 后立刻复用一块 Uint8Array,客户端偶尔收到下一条消息的内容,或消息结尾的分隔符跑到了中间。先检查发送参数是否仍指向那块可写内存,不能把 send 返回当成数据已经复制完。

ws 8.19.0、Node.js 22.19.0 的具体 TLS 场景暴露了这条时序:消息处理器反复填充同一个数组,再发送它的 subarray。后一次填充可能发生在前一份数据真正写出之前。正式 ws 8.22.0 仍会沿发送队列与 socket.write 处理数据,并未承诺每次调用都在返回前复制。

subarray 只创建视图

对同一个 Uint8Array 调用 subarray,得到的新对象仍共享原 ArrayBuffer。视图长度看起来恰好等于本次消息长度,也不会让内容自动独立。

发送路径可能在 ws 内部排队,也可能在 Node Writable、socket 中缓冲;压缩等步骤还会改变时间窗口。因此,这类现象不能只归为 TLS 缺陷,也不能假定普通连接上一定不会发生。输入进入发送流程后,应用仍写同一块内存,才是这里需要消除的所有权冲突。

allowSynchronousEvents: false 可能改变同一 tick 内消息处理的时序,让某个复现暂时消失,但它不提供通用的缓冲区复制保证。关压缩或插入一个固定延时也不能建立这样的保证。

给每次发送一份不再修改的数据

改动最小的处理,是在复用原数组之前复制本次实际范围。下面的 view 明确是 Uint8Array,conn 是已经连接的 ws 实例,reportSendError 是应用已有的错误处理函数:

const view = buffer.subarray(0, messageLength);
const owned = Buffer.from(view);

conn.send(owned, { binary: false }, error => {
  if (error) reportSendError(error);
});

这里 Buffer.from(Uint8Array) 复制所选字节范围;不是用 ArrayBuffer 创建共享视图。owned 交给发送路径后也不能再修改。binary: false 保留了这个文本消息场景的设置;若应用发的是二进制数据,应保留原来正确的消息类型,不能为照抄示例改变协议。

构造长度、数据写入与复制需要属于同一次消息处理。若多个异步任务共享 buffer,在复制之前就有别的任务改过它,复制到的仍会是错误内容;应把这段生产过程隔离,或为任务分配自己的缓冲。

应用需要减少分配时,也可以维护有界缓冲池,在对应 send 回调完成后才回收那一份内存。回调表示数据已写出,发送错误同样要结束其生命周期;它不代表远端应用已经处理消息。单纯把 message 监听器改成 async,并不会使所有事件自动串行,仍须显式管理同一块缓冲区的使用。

验证内容,保留背压和错误处理

比较接收端完整字节或明确的消息序号,而不是只看服务器没有抛错。不同长度的连续消息、突发消息、慢接收端,以及现有 TLS、压缩配置都要覆盖;前一条消息不能被后一条内容覆盖。

每次复制会增加分配,慢连接下已排队的数据也不会因此消失。保留现有的队列上限、背压与断连处理,不为修内容损坏无限堆积待发送副本。若已经发送的数据涉及业务操作,不能用发送回调代替对端确认,原来的应用层确认规则仍需保留。