WebSocket 与实时功能

用 Bun.serve 原生 WebSocket 实现连接管理、pub/sub、背压与保活

最后更新于

什么时候用

  • 聊天、协作编辑、实时仪表盘、Agent 进度推送:Bun 原生 WebSocket 是一等选择。
  • 需要 Socket.IO 的房间语义、自动重连和降级:ws 或 Socket.IO 包也能在 Bun 上跑,但要用客户端库自己实现广播,替代不了 server.publish 的原生 pub/sub。
  • 目标是 Lambda、Cloudflare Workers 等平台:本页形态不适用,见 Serverless 与边缘

最小回声服务

server.ts
const server = Bun.serve({
  port: 3000,
  fetch(req, server) {
    if (server.upgrade(req)) return; // 升级成功,不再返回 Response
    return new Response('WebSocket endpoint', { status: 426 });
  },
  websocket: {
    open(ws) {
      ws.send('welcome');
    },
    message(ws, message) {
      ws.send(message); // 回声
    },
    close(ws, code, reason) {
      console.log('closed', code, reason);
    },
  },
});

鉴权与连接上下文

升级前是普通 HTTP 请求,先鉴权再升级;上下文通过 data 注入,之后每个处理器都能读:

// 项目自己的鉴权:校验 Cookie/Token,返回用户 ID,失败返回 null
function authenticate(req: Request): string | null {
  return req.headers.get('x-demo-user'); // 演示用;生产换成真实会话校验
}

Bun.serve<{ userId: string }>({
  fetch(req, server) {
    const userId = authenticate(req);
    if (!userId) {
      return new Response('unauthorized', { status: 401 });
    }
    if (server.upgrade(req, { data: { userId } })) return;
    return new Response('WebSocket endpoint', { status: 426 });
  },
  websocket: {
    // 用 data 属性为 ws.data 提供类型
    data: {} as { userId: string },
    open(ws) {
      ws.subscribe(`user:${ws.data.userId}`);
    },
    message(ws, message) { /* ... */ },
  },
});

Pub/Sub

API语义
ws.subscribe(topic) / ws.unsubscribe(topic)管理单个连接的主题订阅
ws.publish(topic, data)发给该主题所有订阅者,不含自己
server.publish(topic, data)发给该主题所有订阅者,包含自己
ws.send(data)只发给当前连接;返回发送字节数

ws.send 的返回值要检查:0 表示连接异常被丢弃,-1 表示已入队但触发背压。高频推送时用 backpressureLimitcloseOnBackpressureLimit 决定是缓冲还是断开慢消费者,不要用无限内存为慢客户端兜底。

单进程内存广播

server.publish 的 pub/sub 存在当前进程内存里。多副本部署时,跨副本消息需要 Redis Pub/Sub 等外部总线转发,见 数据库与存储 的 Redis 边界。

空闲超时与保活

Bun 默认关闭 120 秒无活动的 WebSocket 连接(idleTimeout 可配置),但"无活动"包括协议层 ping:Bun 默认 sendPings: true,会自动发 ping,标准客户端自动回 pong,所以健康的纯订阅连接不会因静默被断开。真正需要处理的场景:

  1. 代理链路另有空闲超时:Nginx、Cloudflare、平台 LB 各自掐断空闲连接,应用层仍应按链路最短者设计 30–60 秒心跳与自动重连;
  2. 僵尸对端:TCP 半开连接下 ping 也可能无响应,idleTimeout 就是兜底;客户端必须实现重连并恢复订阅;
  3. 关闭了 sendPings 或客户端不标准时,才需要自己周期性 ws.send 保活。

生产边界

  • maxPayloadLength 限制单条消息大小;消息体按不可信输入校验。
  • 升级请求记录鉴权结果,不在日志里写 token。
  • 优雅退出时主动 ws.close() 并停止接受新升级,配合 容器与 Kubernetes 的 SIGTERM 流程。
  • 压测目标:连接数、每秒消息数、广播扇出和内存,而不是单连接回声延迟。

验收

  1. 未认证请求拿到 401,不出现升级后的匿名连接;
  2. 慢消费者触发背压策略,内存不随连接数失控;
  3. 空闲 120 秒边界下客户端能自动重连恢复订阅;
  4. 多副本场景经外部总线收到跨副本消息。

官方参考:Bun WebSocketBun.serve