
OpenClaw 设计解析(三):Gateway 核心——WebSocket 服务与会话管理
OpenClaw 设计解析(三):Gateway 核心——WebSocket 服务与会话管理
系列第 3 篇。内容:Gateway 的 WebSocket 服务、认证、会话管理、并发控制。
1. Gateway 的角色
Gateway 是 OpenClaw 的运行时核心。无论你用的是 macOS 菜单栏应用、iOS/Android 客户端、Web 界面、终端 TUI,还是 Telegram/Discord/Slack 等消息渠道,所有请求最终都汇聚到同一个 Gateway 进程。
它同时暴露 HTTP 和 WebSocket 两种协议:HTTP 服务静态资源和 REST 端点(如 OpenAI 兼容的 /v1/chat/completions、健康检查、Slack webhook 回调等),WebSocket 承载主要的双向 RPC 通信(聊天流式输出、事件推送、命令审批等需要服务端主动推送的场景)。30 多个 RPC handler 模块覆盖了聊天、会话、模型管理、渠道控制、定时任务等全部能力。
这是一个”单进程、双协议、多客户端”的架构——一个 Gateway 进程就能服务所有客户端和所有协议。部署上,Gateway 通过绑定模式(--bind)控制网络可达性:默认绑定 loopback(仅本机访问),也可以通过 Tailscale 或反向代理安全地暴露给远程设备(详见 3.4 节)。
2. 启动流程
命令行入口:
1 | openclaw gateway run --port 18789 --bind loopback |
主要启动参数:
| 参数 | 说明 | 示例值 |
|---|---|---|
--bind |
网络绑定模式 | loopback / lan / tailnet / auto / custom |
--port |
监听端口 | 18789(默认) |
--auth |
认证模式 | none / token / password / trusted-proxy |
--token |
共享认证令牌 | |
--tailscale |
Tailscale 集成模式 | off / serve / funnel |
--force |
强制抢占端口 |
启动链分为四步:
其中有一条硬性安全规则:非 loopback 绑定必须配置 token/password/trusted-proxy 之一,否则拒绝启动。
3. WebSocket 服务实现
3.1 服务器初始化
Gateway 的网络层初始化核心逻辑(src/gateway/server-runtime-state.ts):
1 | // 伪代码:Gateway 网络层初始化 |
关键设计点:noServer: true 让 WebSocket 不自行监听端口,而是复用 HTTP 服务的 upgrade 事件,HTTP API 和 WebSocket 共享同一端口。
3.2 HTTP 与 WebSocket 的关系
很多人会疑惑:既然 Gateway 主要用 WebSocket 通信,为什么还需要 HTTP 服务?
首先,WebSocket 连接的建立依赖 HTTP。 这是协议规范决定的,不是设计选择:
没有 HTTP 服务器,就没有人接收这个初始的 upgrade 请求,WebSocket 握手无法完成。
其次,Gateway 本身需要大量纯 HTTP 接口。 WebSocket 适合双向、持续的通信(如聊天流式输出、事件推送),但很多场景天然是单次请求-响应模式,或者对端只支持 HTTP:
| 端点 | 用途 | 为什么是 HTTP |
|---|---|---|
GET /health, /healthz |
容器健康检查 | Docker/K8s 探活不支持 WebSocket |
POST /v1/chat/completions |
OpenAI 兼容接口 | IDE(Cursor 等)只认 HTTP API 格式 |
POST /v1/responses |
OpenResponses 接口 | 同上,行业标准 REST/SSE 格式 |
POST /tools/invoke |
工具调用 | 一次性请求-响应,不需要长连接 |
POST /slack/events |
Slack 事件回调 | Slack 只发 HTTP POST |
POST /hooks/wake |
外部触发唤醒 | cron/GitHub webhook 只会发 HTTP |
GET /ui/** |
Control UI 静态资源 | 浏览器加载页面用 HTTP GET |
为什么不用 SSE 替代 WebSocket? SSE(Server-Sent Events)能做服务端推送,但它是单向的。OpenClaw 的核心场景是双向高频交互——用户发消息、AI 流式回复、中途弹出审批请求、用户批准、AI 继续执行——这些消息在同一条连接上交错进行。SSE 做不到客户端随时发消息,每次都得单独发 HTTP 请求,状态关联和流控都会变复杂。
对于不需要双向的场景(如 /v1/chat/completions),OpenClaw 确实用了 HTTP + SSE 流式返回。
3.3 为什么需要多个 HTTP 服务器
你可能注意到 3.1 的代码里用 for 循环创建了多个 HTTP 服务器。通常写法是一个 server 就够了:
1 | // 常见写法:一个 server,一对一绑定 |
但 OpenClaw 不能这么做。Loopback 模式下,Gateway 需要同时绑定 127.0.0.1(IPv4)和 ::1(IPv6)。这是因为 localhost 在不同环境下解析结果不同:
- Node.js 18+ 默认优先解析 IPv6,
localhost→::1 - 有些 curl 版本优先 IPv4,
localhost→127.0.0.1 - 浏览器行为因操作系统而异
如果只绑定 127.0.0.1,那些解析到 ::1 的客户端就会 connection refused。
而 server.listen(port, host) 的 host 只接受一个地址,这是 Node.js/操作系统层面的限制,所以需要两个 HTTP 服务器分别绑定。
为什么不用 server.listen(18789) 省事? 不指定 host 时,Node.js 绑定到 :: (所有接口),IPv4 和 IPv6 都能连。但 :: 意味着局域网里的其他机器也能连进来,失去了 loopback 的安全保护。
两个 HTTP 服务器不会导致消息重复。 它们只是两扇门,进来之后是同一个房间:
一个客户端只会选其中一个地址连接(由操作系统 DNS 解析决定),广播时遍历的是 clients 列表,跟有几个 HTTP 服务器无关。
这也是为什么代码用 noServer: true + 手动挂载 upgrade handler,而不是 new WebSocketServer({ server })——后者只能绑定一个 HTTP 服务器。
容错方面:127.0.0.1 绑定失败则 Gateway 启动失败;::1 绑不上只是警告,降级为单 HTTP 服务器继续运行。
3.4 Loopback 模式与部署方案
Loopback 模式是 Gateway 的默认绑定模式,安全保证来自操作系统:绑定 127.0.0.1 后,内核的 TCP/IP 协议栈会直接丢弃不是来自本机的数据包,不需要防火墙规则。
但如果 Gateway 部署在 VPS 上,需要从外部访问,就有几种方案:
方案一:SSH 隧道(最简单)
Gateway 保持 loopback 模式,通过 SSH 端口转发访问:
1 | # VPS 上 |
适合个人 CLI 使用,不适合手机访问(手机不好开 SSH)。
方案二:Tailscale Serve(推荐多设备方案)
Tailscale 是一个组网工具,安装后所有设备加入一个私有虚拟网络。Gateway 保持 loopback,Tailscale Serve 提供 HTTPS 代理:
1 | openclaw config set gateway.bind loopback |
各设备装 Tailscale 并登录同一账号即可访问,不需要开防火墙端口、不需要配 Nginx、不需要买域名证书。Tailscale 自动提供 HTTPS 和身份认证。
方案三:反向代理(需要公网访问时)
当需要接收 Slack webhook 等外部回调时,必须有公网可达的 URL:
1 | openclaw config set gateway.bind lan |
这种方案需要注意一个安全陷阱:OpenClaw 默认信任来自 localhost 的连接(跳过认证),而反向代理转发的请求在 TCP 层显示为 127.0.0.1,会导致认证被绕过。必须正确配置 trustedProxies,让 Gateway 从 X-Forwarded-For 头读取真实客户端 IP。
| 方案 | bind 模式 | 安全性 | 复杂度 | 适合场景 |
|---|---|---|---|---|
| SSH 隧道 | loopback | 最高 | 低 | 个人,仅 CLI |
| Tailscale Serve | loopback | 高 | 低 | 个人,多设备 |
| 反向代理 | lan | 取决于配置 | 高 | 需要公网访问 |
3.5 连接生命周期
每个 WebSocket 连接经历三个阶段:握手挑战 → 认证 → 已认证 RPC 通道。
握手挑战:连接建立后,服务端发送一个包含随机 nonce 和时间戳的 challenge。客户端必须在超时时间内用 connect 请求响应,否则连接被关闭。
认证与注册:认证通过后,客户端被加入全局 clients 集合,后续所有 RPC 调用和事件推送都通过这个集合管理。
断开清理:连接关闭时,清理已注册的节点、广播存在状态变更、记录最后消息元数据。
3.6 消息解析与路由
WebSocket 收到的每一帧消息都经过统一处理:
- 握手阶段:第一条消息必须是
connect请求,参数格式不对则直接断开(关闭码1008) - 已认证阶段:后续消息走统一的 RPC 路由,根据
method字段分发到对应 handler - 洪泛防护:如果检测到客户端反复发送未授权请求,会抑制日志并主动断开连接,防止日志被刷爆
4. 协议定义
OpenClaw 使用 TypeBox(@sinclair/typebox)定义运行时 schema,兼顾 TypeScript 类型推导和运行时校验。
连接参数(客户端 → 服务端)
客户端连接时需要携带的信息:
| 字段 | 说明 |
|---|---|
minProtocol / maxProtocol |
协议版本协商 |
client.id / mode / version |
客户端身份(如 cli、openclaw-macos、webchat) |
caps / commands / permissions |
能力声明(客户端支持哪些特性) |
device |
设备认证(publicKey + ECDSA 签名) |
auth |
认证凭据(token / deviceToken / password) |
握手响应(服务端 → 客户端)
认证成功后,服务端返回 hello-ok:
1 | // hello-ok 响应结构概览 |
所有 schema 都设置了 additionalProperties: false,确保严格校验——客户端不能偷塞未知字段。
跨平台一致性:协议 schema 导出为 JSON Schema 文件,Swift 端通过 codegen 生成对应的模型代码,保证 iOS/macOS 客户端与服务端的协议定义始终同步。
5. RPC 方法分发
5.1 Handler 聚合
Gateway 将 28 个 handler 模块聚合为一个统一的方法表,覆盖全部功能域:
| 模块 | 功能 |
|---|---|
chat |
聊天(发送、中止、流式输出) |
sessions |
会话管理(创建、切换、删除) |
models |
模型配置 |
channels |
渠道控制(Telegram、Discord 等) |
cron |
定时任务 |
devices |
设备管理和配对 |
tools-catalog |
工具目录 |
browser |
浏览器集成 |
health |
健康检查 |
voicewake |
语音唤醒 |
| … | 其余 handler 模块 |
5.2 方法路由
路由逻辑很简单——根据请求的 method 字段在方法表中查找对应 handler:
1 | // 伪代码:RPC 路由 |
先检查插件注入的 handler(插件可以扩展 RPC 方法),再回退到核心方法表。找不到就返回错误。
5.3 方法授权
每个 RPC 调用在分发前都要经过角色 + 作用域双层授权检查:
- operator(默认角色):拥有大部分读写权限
- node:远程节点角色,有特殊的方法白名单
- admin scope:万能钥匙,全权通过
这套机制让移动端可以使用受限作用域连接,降低凭据泄露风险。
6. 认证机制
6.1 六种认证模式
Gateway 支持六种认证方式,适应从本地开发到远程部署的不同场景:
| 模式 | 适用场景 | 说明 |
|---|---|---|
none |
仅 loopback 部署 | 不做任何认证 |
token |
远程访问 | 共享密钥,常量时间比较防时序攻击 |
password |
简单远程访问 | 密码认证,常量时间比较 |
tailscale |
Tailscale 网络 | 通过 Tailscale whois 验证身份 |
device-token |
移动设备 | 设备专属令牌(绑定 deviceId + clientId) |
trusted-proxy |
反向代理部署 | 从代理头提取身份 |
认证结果中的 rateLimited 和 retryAfterMs 字段让客户端知道何时可以重试——这比简单返回 403 要友好得多。
6.2 认证速率限制
Gateway 实现了 per-IP 的认证速率限制,防止暴力破解:
- 滑动窗口:1 分钟内最多 10 次失败尝试
- 锁定:超过阈值后锁定 5 分钟
- 重置:认证成功后清除计数
- 豁免:Loopback 地址(127.0.0.1、::1)默认不受限制
token 和 password 认证都使用常量时间比较(safeEqualSecret),防止时序攻击——攻击者无法通过响应时间差异逐字符猜测密钥。
7. 会话管理
会话(Session)是 Gateway 的核心状态单元,每个对话都对应一个 SessionEntry。会话数据以 JSON 文件持久化在磁盘上,Gateway 在其上做了三层处理来保证性能、一致性和兼容性。
7.1 TTL 缓存 + mtime 校验
Gateway 频繁读取会话数据(每次聊天、心跳、事件推送都可能触发),如果每次都读磁盘,IO 压力很大。解决方案是内存缓存 + 双重失效机制:
为什么需要 mtime 校验? 单靠 TTL 有个问题:如果另一个进程(比如同时开了两个 CLI)修改了 sessions.json,你要等 45 秒才能看到最新数据。mtime 校验在每次缓存命中时快速 stat 一下文件修改时间(只读元数据,不读内容),发现变化就立即重新读磁盘。
为什么需要深拷贝? 缓存返回的是 structuredClone() 深拷贝,防止调用方修改对象后污染缓存:
1 | // 没有深拷贝的问题: |
7.2 数据迁移
OpenClaw 经过多次迭代,会话的路由字段有过好几种写法。加载会话时自动归一化,让上层代码不用关心历史格式:
1 | // 老格式:字段散落在顶层,命名不统一 |
这样上层代码只需要读 deliveryContext,不用到处判断”这个字段是叫 provider 还是 channel?是 room 还是 groupChannel?”
7.3 文件锁与并发安全
多个进程可能同时读写同一个 sessions.json(比如 Gateway 进程 + CLI 进程),不加锁会导致写入丢失:
锁的实现是创建 .lock 文件(用 fs.open("wx") 原子创建),并带有防死锁机制:锁文件记录了持有者的 PID 和进程启动时间,超时 30 分钟或持有者进程已退出时自动回收。
会话生命周期:创建 → 活跃(有运行中的 agent)→ 持久化。
8. 并发控制:Lane-based 队列
Gateway 同时处理来自多个客户端和渠道的请求。如果不加控制,多个 agent 同时运行会导致资源竞争。OpenClaw 的解决方案是 Lane-based 队列——按任务类型分 lane,每个 lane 独立控制并发度。
8.1 四种 Lane
| Lane | 用途 | 默认并发度 |
|---|---|---|
| Main | 用户对话 | 来自配置 |
| Cron | 定时任务 | 1(防止互踩) |
| Subagent | 子 agent 任务 | 独立上限 |
| Nested | 嵌套调用 | 独立上限 |
8.2 队列核心机制
每个 Lane 维护一个队列和活跃任务集合:
| 机制 | 说明 |
|---|---|
| maxConcurrent | 活跃任务数达到上限时,新任务排队等待 |
| draining | 优雅关闭时,拒绝新任务入队,已在执行的任务继续完成 |
| generation | 每次重置时递增,旧任务自动失效,保证重启后状态一致 |
| 自动推进 | 任务完成后自动从队列取出下一个执行 |
8.3 配置热重载
用户可以通过配置文件调整各 lane 的并发度,不需要重启 Gateway——配置变更时自动生效。
9. 小结
回顾 Gateway 的核心设计:
双协议共享端口:HTTP 和 WebSocket 共享同一端口,HTTP 服务外部集成(健康检查、OpenAI 兼容接口、webhook),WebSocket 承载内部 RPC 通信。loopback 模式下通过创建多个 HTTP 服务器同时覆盖 IPv4 和 IPv6。
TypeBox schema 驱动:一份定义同时服务于 TypeScript 类型推导和运行时校验,再通过 JSON Schema 导出驱动 Swift codegen。协议一致性通过自动化保证。
Lane-based 队列:按任务类型分 lane、按配置控并发。用户对话不被定时任务阻塞,子 agent 有独立的并发池。draining 机制支持优雅关闭。
多认证模式:从零认证(loopback 开发)到 Tailscale 身份验证(远程部署),六种模式覆盖不同部署场景。速率限制 + 常量时间比较防止暴力破解和时序攻击。
会话缓存:45 秒 TTL + mtime 校验,平衡性能和一致性。
- 感谢你的欣赏!




