Skip to content

WebSocket 支持:upgradeWebSocket(c) 在 Workers 中的限制

Cloudflare Workers 无原生 WebSocket server,需使用 Durable Objects

Hono 提供了 upgradeWebSocket 工具,让你可以方便地在支持 WebSocket 的环境中(如 Node.js、Deno)处理 WebSocket 连接。然而,当你将 Hono 部署到 Cloudflare Workers 时,会遇到一个关键限制:Cloudflare Workers 本身不支持作为 WebSocket 服务器

这并不意味着你无法在 Cloudflare 上使用 WebSocket —— 而是必须通过 Durable Objects 来实现。

一、问题根源:Cloudflare Workers 的架构限制

环境是否支持原生 WebSocket Server
Node.js支持 (wsuWebSockets.js)
Deno支持 (Deno.upgradeWebSocket)
Cloudflare Workers (常规)不支持
Cloudflare Workers + Durable Objects支持

核心限制
Cloudflare Workers 是无状态、短暂运行的函数,而 WebSocket 是长连接、有状态的协议。Workers 的执行模型无法维持长时间的 TCP 连接。

二、解决方案:Durable Objects (DO)

Durable Objects (DO) 是 Cloudflare 提供的有状态、持久化对象,每个 DO 实例可以:

  • 维持一个或多个 WebSocket 连接;
  • 存储状态(内存中);
  • 跨请求共享数据;
  • 实现聊天室、实时协作、游戏等场景。

Durable Objects 是 Cloudflare 上运行 WebSocket Server 的唯一方式

三、Hono + Durable Objects 实现 WebSocket

虽然 Hono 的 upgradeWebSocket(c) 不能直接用于 Workers 的常规路由,但你可以:

  1. Durable Object Class 中手动处理 WebSocket 升级;
  2. 使用 Hono 作为 HTTP 路由层,将 WebSocket 请求引导至 DO。
1. 定义 Durable Object Class
ts
// durable-object.ts
export class WebSocketRoom {
  private connections: WebSocket[] = []
  private state: DurableObjectState

  constructor(state: DurableObjectState, env: Env) {
    this.state = state
  }

  async fetch(request: Request) {
    const upgradeHeader = request.headers.get('Upgrade')
    if (!upgradeHeader || upgradeHeader !== 'websocket') {
      return new Response('Expected WebSocket', { status: 426 })
    }

    // 创建 WebSocket 对象
    const [client, server] = Object.values(new WebSocketPair())

    // 在 DO 端处理连接
    await this.handleWebSocket(server)

    // 返回客户端连接
    return new Response(null, {
      status: 101,
      webSocket: client,
    })
  }

  private async handleWebSocket(webSocket: WebSocket) {
    webSocket.accept()

    // 加入广播列表
    this.connections.push(webSocket)

    // 监听消息
    webSocket.addEventListener('message', (event) => {
      const data = JSON.parse(event.data)
      // 广播给所有连接
      this.broadcast({
        type: 'message',
        data: data.content,
        from: data.user,
        time: new Date().toISOString()
      })
    })

    // 连接关闭
    webSocket.addEventListener('close', () => {
      this.connections = this.connections.filter(ws => ws !== webSocket)
    })
  }

  private broadcast(message: any) {
    const payload = JSON.stringify(message)
    this.connections.forEach(ws => {
      if (ws.readyState === WebSocket.OPEN) {
        ws.send(payload)
      }
    })
  }
}
2. 在 Hono 中创建连接入口(HTTP 路由)
ts
// routes/chat.ts
import { Hono } from 'hono'

const app = new Hono()

// 前端通过此接口获取 WebSocket 连接
app.get('/chat/connect', async (c) => {
  const { env } = c
  const roomId = 'global' // 可动态生成

  // 获取 Durable Object Stub
  const id = env.WS_ROOM.idFromName(roomId)
  const obj = env.WS_ROOM.get(id)

  // 转发请求到 Durable Object
  const resp = await obj.fetch(c.req.url, c.req.raw.clone())

  // 返回 WebSocket 升级响应
  return resp
})

export default app
3. wrangler.toml 配置
toml
name = "hono-ws-bff"
main = "src/index.ts"
compatibility_date = "2025-03-01"

[[d1_databases]]
binding = "DB"
database_name = "my-db"
migrations_dir = "./migrations"

# 定义 Durable Object
[[durable_objects.bindings]]
name = "WS_ROOM"
class_name = "WebSocketRoom"

# 生成 Durable Object 脚本
[[migrations]]
tag = "v1"
new_classes = ["WebSocketRoom"]
4. 前端连接 WebSocket
ts
// composables/useChat.ts
export function useChat() {
  const messages = ref<any[]>([])
  const ws = ref<WebSocket | null>(null)

  const connect = async () => {
    // 1. 请求连接入口
    const resp = await fetch('/chat/connect')
    
    // 2. 提取 WebSocket(浏览器自动升级)
    const socket = resp.webSocket
    if (!socket) throw new Error('WebSocket upgrade failed')

    socket.accept()
    ws.value = socket

    socket.addEventListener('message', (event) => {
      const data = JSON.parse(event.data)
      messages.value.push(data)
    })

    socket.addEventListener('close', () => {
      console.log('WebSocket closed')
    })
  }

  const send = (content: string, user: string) => {
    if (ws.value?.readyState === WebSocket.OPEN) {
      ws.value.send(JSON.stringify({ user, content }))
    }
  }

  return { messages, connect, send }
}

四、Hono 的 upgradeWebSocket(c) 在哪里可用?

ts
import { upgradeWebSocket } from 'hono/ws'

// 仅在 Node.js / Deno 环境中可用
app.get('/ws', (c) => {
  return upgradeWebSocket(c, (ws) => {
    ws.on('message', (event) => {
      ws.send(`Echo: ${event.data}`)
    })
  })
})

在 Cloudflare Workers 中使用此代码会报错,因为 WebSocket 构造函数不可用。

五、替代方案对比

方案是否支持说明
upgradeWebSocket(c)❌ Workers仅 Node.js/Deno
Durable Objects官方推荐,有状态
WebTransport✅ (实验性)UDP 基础,低延迟
Polling / SSE简单场景可用,但非全双工
第三方服务 (Pusher, Socket.IO)增加依赖和成本

六、最佳实践

  1. 状态分离

    • Hono 处理 REST API;
    • Durable Objects 处理 WebSocket 状态。
  2. 连接管理

    • 使用 Map<roomId, Stub> 管理多个房间;
    • 设置空闲超时自动销毁 DO 实例。
  3. 安全

    • 验证 JWT 或 Session;
    • 限制每用户连接数。
  4. 监控

    • 记录连接数、消息量;
    • 使用 Cloudflare Analytics。

七、总结

问题解决方案
Workers 不能做 WebSocket Server使用 Durable Objects
Hono upgradeWebSocket 不可用手动在 DO 中处理 WebSocketPair
需要全局状态DO 天然支持有状态实例

关键结论
在 Cloudflare Workers 上实现 WebSocket,必须使用 Durable Objects。Hono 可作为 HTTP 入口层,将请求转发至 DO,实现前后端分离的实时通信架构。

通过这种模式,你可以在边缘网络中构建高性能、低延迟的实时应用,如聊天室、协作编辑、实时仪表盘等。