mcpField
W
web-realtime-websockets

web-realtime-websockets

github.com/agents-inc开发2637 次安装2 天前更新
代码仓库

关于 web-realtime-websockets

面向实现实时 Web 功能的开发者,介绍浏览器 WebSocket 的常用模式,涵盖连接恢复、消息处理、二进制传输和页面生命周期。

主要功能

  1. 01使用抖动和上限进行指数重连
  2. 02通过心跳检测连接状态
  3. 03断连期间暂存消息
  4. 04类型化消息约定与二进制帧
  5. 05React Hook 与共享连接
  6. 06处理认证和 bfcache 生命周期

适用场景

  1. 01构建聊天或协同编辑功能
  2. 02在网络中断后恢复连接
  3. 03分块传输文件并显示进度
  4. 04在多个 UI 组件间共享连接

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/agents-inc/skills --skill web-realtime-websockets

安装后在 Claude Code 中输入/web-realtime-websockets确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

metadata.yaml
reference.md
examples/
binary.md
core.md
presence.md
state-machine.md

详细介绍

WebSocket 实时通信模式

快速指南: 原生 WebSocket API 只提供全双工通道,其他一概不管——重新连接、存活检测、连接中断期间的消息传递和消息类型都需要你自行实现,而本指南介绍了这些功能各自的实现方式。会影响方案选择的事实包括:API 不接受自定义标头,因此身份验证需要通过第一条消息而非标头进行;onerror 后总会触发 onclose,因此恢复逻辑应集中在一处;开放的连接会阻止浏览器使用前进/后退缓存;而且 readyState 的变化与触发它们的调用并不同步。

详细资源:

  • examples/core.md — 生命周期、带抖动的退避、心跳、排队、类型化消息、二进制协议、身份验证、房间、useWebSocket、共享连接、bfcache
  • examples/state-machine.md — 将连接状态作为 reducer 处理,以及布尔值何时不再够用
  • examples/binary.md — 带进度显示的分块文件上传
  • examples/presence.md — 在线状态和离开检测
  • reference.md — 带重新连接建议的关闭代码表、readyState 值、附带代码的反模式、部署检查清单

适用的方案

  • 一个组件拥有连接 — 由一个钩子创建套接字、管理退避和心跳计时器,并在卸载时将其关闭。从 examples/core.md 开始。
  • 多个组件共享一个连接 — 由一个 provider 拥有套接字,并通过返回自身取消订阅函数的 subscribe(type, handler) API 按消息类型路由。每个组件都打开一个套接字,会让针对同一服务的握手和心跳成倍增加。另请参阅 examples/core.md。

一旦连接需要区分的状态超过两三个——例如带尝试次数的重新连接、永久失败——布尔标志就难以组合,examples/state-machine.md 介绍了替代方案。


<critical_requirements>

编写 WebSocket 代码前

采用带抖动的指数退避,并限制延迟和尝试次数。 一次服务重启后断开的所有客户端都会同时尝试重新连接,而抖动能将它们错开,避免再次造成服务中断。

为每条消息添加 type 字段,并将消息集合建模为可辨识联合类型。 这样,在默认分支中赋值给 never,就能让新增的服务端消息触发编译错误,而不是作为未处理的值漏过去。

套接字未处于 OPEN 状态时,将发送请求加入队列,并在重连后刷新队列。 在已关闭的套接字上调用 send(),根据状态不同,可能会抛出异常,也可能丢弃消息;有界队列能将两秒的断连转变为延迟,而不是数据丢失。

运行心跳检测,并将缺少回复视为连接已失效。 代理和 NAT 会在没有关闭帧的情况下丢弃空闲连接,因此套接字可能在停止传输任何内容后数分钟内仍显示为 OPEN。

只要页面通过 HTTPS 提供服务,就使用 wss://。 除 localhost 外,浏览器会阻止安全源使用不安全协议。

在 pagehide 时关闭连接,并在 event.persisted 为真时于 pageshow 时重新连接。 打开的套接字会使页面无法进入前进/后退缓存,因此这能确保即时返回导航正常工作。

</critical_requirements>


自动检测: new WebSocket、wss://、ws://、socket.onmessage、socket.onopen、socket.onclose、socket.onerror、readyState、WebSocket.OPEN、bufferedAmount、binaryType、CloseEvent、event.code、pagehide、pageshow、event.persisted、WebSocketStream

适用场景:

  • 需要低延迟的双向消息传递——聊天、协作编辑、实时仪表盘
  • 采用退避、抖动和重试上限的重连
  • 通过心跳和关闭代码解释检测连接是否存活
  • 通过有界队列确保短暂断连期间的消息送达
  • 类型化消息契约和穷尽式处理
  • 二进制帧,包括带进度的分块传输
  • 在组件树中共享连接

其他部分会处理:

  • 客户端只接收、不发送的服务端推送——这种情况下,双工通道及其整套重连机制毫无用处,而长连接 HTTP 响应是更轻量的选择。
  • 协议层提供的房间、命名空间、确认机制和传输回退——这里的房间模式是基于原始 API 手动构建的,而提供这些功能的库属于不同主题,并使用不同的线格式。
  • 服务端的连接注册表、广播扇出和房间成员关系。
  • 接收的消息存储在何处,以及如何渲染这些消息。
  • 签发和刷新首条消息携带的令牌。

WebSocket 是一条保持打开状态的 TCP 连接,其上叠加了帧。HTTP 原本免费提供的一切都不复存在,而你现在需要负责的内容,正是本技能的主题。

  • 网络会断开连接。 重连并非边缘情况,因此退避和抖动应从第一个版本就纳入,而不是留到以后。
  • 失效的连接看起来只是安静的连接。 只有心跳才能区分两者,因为被中间设备切断的连接不会发送关闭帧。
  • 断连期间发送的消息会丢失。 有界队列能将其转化为延迟。
  • 帧是无类型字符串。 你在其上建模的内容决定了类型安全性。
  • 保持连接打开的代价不只是带宽。 这会使页面无法进入 bfcache,表现为返回导航变慢,而非报错。
CONNECTING -> OPEN <-> (messages) -> CLOSING -> CLOSED
                 |                      |
             (error) <- reconnect <- (close)

<decision_framework>

阅读关闭事件

event.code 表示是否适合重新连接:1000 表示正常、有意关闭,重新连接会违背用户的意愿;1006 表示异常关闭且没有收到帧,这是常见的网络断开情形,也是设置退避机制的原因;1012 和 1013 表示服务端要求等待更长时间;100x 协议和数据错误意味着客户端有问题,重试只会再次遇到相同错误。包含重连列的完整表格见 reference.md。

请将有意关闭与关闭代码分开记录——用户按下断开按钮和服务端发送 1000 都会以 1000 到达,但只有前者应该彻底停止重试循环。

选择消息格式

默认使用带判别联合的 JSON,主要原因是它在线路上可读。只有当测量结果确实显示负载大小造成影响时,才使用二进制帧;并保留混合结构——控制消息使用 JSON,负载使用二进制——而不是将所有内容都编码成同一种格式。

发送大型负载

send() 接受任何内容,并会缓冲网络尚未传输的部分,因此快速生产数据会让 bufferedAmount 不断增长。发送大型负载前请检查该值并分块发送,这样也便于报告进度。除此数值外,没有内置的背压信号。

</decision_framework>


核心模式

模式 1:基本连接

四个处理程序都各有用途:onerror 不会提供有用的细节,而且总会接着触发 onclose,因此只需在 onclose 中处理恢复。

const socket = new WebSocket(WS_URL);

socket.onopen = () => flushQueue();
socket.onmessage = (event: MessageEvent) => handle(event.data);
socket.onerror = () => markUnhealthy(); // no detail available, onclose follows
socket.onclose = (event: CloseEvent) => maybeReconnect(event.code);

完整代码:examples/core.md


模式 2:带抖动的指数退避

关键在于抖动:没有它,所有同时断开的客户端也会同时重连。

function calculateBackoff(attempt: number): number {
  const exponential = Math.min(
    INITIAL_BACKOFF_MS * Math.pow(BACKOFF_MULTIPLIER, attempt),
    MAX_BACKOFF_MS,
  );
  const jitter = exponential * JITTER_FACTOR * (Math.random() * 2 - 1);
  return Math.floor(exponential + jitter);
}

完整代码:examples/core.md


模式 3:心跳与 ping-pong

按间隔发送消息,设置一个更短的超时,并在收到回复时清除超时。超时触发就意味着连接已断开,无论 readyState 看起来多么正常。

const ping = setInterval(() => {
  socket.send(JSON.stringify({ type: "ping" }));
  pongTimer = setTimeout(
    () => socket.close(4000, "heartbeat timeout"),
    HEARTBEAT_TIMEOUT_MS,
  );
}, HEARTBEAT_INTERVAL_MS);

// on receiving { type: "pong" }: clearTimeout(pongTimer)

完整代码:examples/core.md


模式 4:断开连接期间的消息排队

每次发送前都检查 readyState,因为它的变化独立于前后调用。

public send(data: unknown): void {
  if (this.socket?.readyState === WebSocket.OPEN) {
    this.socket.send(JSON.stringify(data));
  } else {
    this.queueMessage(data); // bounded — oldest dropped at MAX_QUEUE_SIZE
  }
}

完整代码:examples/core.md


模式 5:使用判别联合实现类型安全的消息

按方向分别定义联合类型,并在接收端进行穷尽性检查。

type ServerMessage =
  | { type: "subscribed"; channel: string; members: string[] }
  | { type: "message"; channel: string; content: string; sender: string }
  | { type: "error"; code: number; message: string };

function handleServerMessage(message: ServerMessage): void {
  switch (message.type) {
    case "subscribed":
      return onSubscribed(message);
    case "message":
      return onMessage(message);
    case "error":
      return onError(message);
    default: {
      const exhaustive: never = message;
      return exhaustive;
    }
  }
}

完整代码:examples/core.md


模式 6:二进制数据处理

binaryType = "arraybuffer" 可通过 DataView 同步读取帧;默认的 Blob 则会强制每条消息都进行异步读取。

socket.binaryType = "arraybuffer";

socket.onmessage = (event: MessageEvent) => {
  if (event.data instanceof ArrayBuffer) {
    const view = new DataView(event.data);
    const messageType = view.getUint8(0);
  } else {
    handleJson(JSON.parse(event.data));
  }
};

完整代码:examples/core.md · 分块上传:examples/binary.md


模式 7:通过 WebSocket 进行身份验证

该 API 不支持设置自定义标头,因此令牌要放在第一帧中,而不是会被记录到日志里的 URL 中。其他所有操作都要等验证结果返回后再进行。

socket.onopen = () => {
  socket.send(JSON.stringify({ type: "auth", token }));
};
// queue all other sends until { type: "auth_result", success: true } arrives

完整代码:examples/core.md


模式 8:房间与频道

协议中没有房间概念——房间只是消息约定加本地成员状态;而防止向未加入的房间发送消息的检查,正是保留此状态的意义所在。

public joinRoom(roomId: string): void {
  if (this.rooms.has(roomId)) return;
  this.rooms.set(roomId, { id: roomId, members: new Set(), joined: false });
  this.send({ type: "join_room", roomId });
}
// the server's room_joined reply flips joined to true and seeds members

完整代码:examples/core.md


模式 9:自定义 React Hook

由一个 Hook 管理套接字、退避计时器、心跳和队列,并提供状态和操作方法。

const { status, send, close, reconnect } = useWebSocket(WS_URL, {
  onMessage: handleServerMessage,
  heartbeatIntervalMs: HEARTBEAT_INTERVAL_MS,
});

完整代码:examples/core.md


模式 10:通过上下文共享连接

由提供程序管理套接字,并按消息类型分发;每个订阅者都会获得自己的取消订阅函数,以便从 effect 中返回。

const { status, send, subscribe } = useWebSocketContext();

useEffect(() => subscribe("notification", handleNotification), [subscribe]);

完整代码:examples/core.md


模式 11:与 bfcache 兼容

在 pagehide 时关闭连接,以使页面符合缓存条件;只有页面确实从缓存中恢复时,才在 pageshow 时重新连接。

window.addEventListener("pagehide", () => socket?.close(1000, "Page hidden"));

window.addEventListener("pageshow", (event: PageTransitionEvent) => {
  if (event.persisted) connect();
});

完整代码:examples/core.md


<red_flags>

注意事项

会导致运行时问题:

  • 在 HTTPS 页面上使用 ws://——除 localhost 外,浏览器会拒绝连接。
  • 未检查 readyState 就调用 send()——状态会独立于外围代码变化,因此消息会丢失或抛出异常。
  • 卸载时不清理——套接字、心跳间隔和重试计时器都会在组件销毁后继续运行。
  • 不使用退避机制重新连接——一次重启断开的客户端会同时重新连接,导致故障再次发生。
  • 未使用 try 就对 event.data 调用 JSON.parse——一帧格式错误的数据会导致处理程序失效,后续所有帧也无法处理。
  • 假设 event.data 是文本——开始处理二进制数据后,应先检查 instanceof ArrayBuffer。
  • 在连接 URL 中放置令牌——令牌会出现在服务器和代理日志中——应将其作为第一帧发送。
  • 使用 beforeunload 进行清理——仅注册该事件就足以使页面失去使用 bfcache 的资格——应使用 pagehide。
  • 收到关闭代码 1000 时重新连接——这表示正常、有意的关闭,包括用户主动请求的关闭。

容易引起意外的行为:

  • onerror 不包含诊断细节,而且后面总会触发 onclose;如果在两处都编写恢复逻辑,就会运行两次。
  • 中间设备中断连接时不会发送关闭帧,因此 readyState 可能会在数分钟内保持 OPEN——只有心跳才能发现问题。
  • bufferedAmount 是唯一的背压信号;快速生产数据、速度超过网络传输能力时,不会抛出任何异常。
  • 默认的 binaryType 是 Blob,这会导致每条消息都需要异步读取——在收到第一帧二进制数据前,将其设为 arraybuffer。
  • 关闭代码 1006 不会在线路上传输;浏览器会在异常关闭时合成该代码,因此它不包含服务器提供的原因。
  • 能够提供真正背压的 WebSocketStream 仅适用于 Chromium。

</red_flags>

评论

支持 Markdown