安装方式
手动下载安装
下载 ZIP 后解压到技能目录即可安装。若在桌面客户端 WebView中直接下载出现异常,本站会改为提示页 + 原始链接,请按页内说明操作。
下载 ZIP (shub-websocket-hub-patterns-v1.0.0.zip)触发指令
/websocket-hub-patterns
跨平台安装指引
该技能声明兼容以下 1 个平台,将 ZIP 解压到对应目录即可被识别。
unzip shub-websocket-hub-patterns-v1.0.0.zip -d ~/.claude/skills/
mkdir -p 创建;启用 Skill 后请重启对应 Agent 让配置生效。
使用指南
WebSocket 中心模式
围绕 WebSocket 中心模式:房间、广播与多实例水平扩展参考;与「websocket-engineer」可对照阅读。 无需在每次任务前把零散英文说明手工拼进上下文,也 减少 与客户端默认行为脱节的试错;具体命令、钩子与 JSON 参数仍以 ZIP 包内 SKILL.md 为权威。下文结构与站内 MCP CLI 类专题稿相同:何时用、前置、流程、速查与故障。
何时使用
- 房间、广播与多实例水平扩展参考
- 与「websocket-engineer」可对照阅读
- 已获取本技能 ZIP,并准备在 Claude Code / OpenClaw 中按 SKILL.md 挂载。
- 希望用中文专题稿快速判断「该不该启用」,再深入英文 SKILL 查参数与边界。
- 需要与团队对齐同一套触发方式、目录约定或回调格式时。
前置条件
- 通用:可运行 Claude Code 或文档要求的客户端;有可读写的项目工作区(或 SKILL.md 指定的沙箱目录)。
- 权威细节:API Key / OAuth、钩子路径、环境变量以 ZIP 内 SKILL.md 为准。
典型流程
- 从 ClawHub / 站内分发获取技能 ZIP,校验版本与校验和(若提供)。
- 阅读 SKILL.md 的安装段落:目录落点、客户端类型(Claude Code / OpenClaw / 脚本)。
- 用文档中的最小示例完成第一次调用(单文件修改、单次查询或单次委派)。
- 确认工作目录、权限边界与输出路径后,再处理多文件或长耗时任务。
- 需要回调 / Webhook / 通知时,按 SKILL.md 配置端点并在测试环境先验通。
与 ZIP / SKILL.md 的关系
站内专题稿与 MCP CLI 类 oss 稿同样:概括何时用、怎么接、怎么排错;命令模板、钩子名、JSON 字段、版本矩阵一律以 ZIP 内 SKILL.md 与 ClawHub 上游为准。
命令示例(摘自包内 SKILL.md)
以下为从上游 SKILL.md(或入库正文)自动抽取的终端/脚本片段;路径、环境变量与参数以当前 ZIP 与官方说明为准。
ClawHub slug:websocket-hub-patterns(安装命令以 SKILL.md / claw CLI 为准)。
npx clawhub@latest install websocket-hub-patterns
站内入库时的触发命令(完整语义见 ZIP):
# 使用本技能时可在对话中引用或执行上述指令;完整参数与示例见下载包内 SKILL.md。
/websocket-hub-patterns
最佳实践
- 先 SKILL.md 再猜参数;站内专题稿不替代 schema 与必填字段说明。
- 委派任务时写清验收标准(命令、文件路径、测试命令),减少来回追问。
- 长任务用文档推荐的回调 / 日志落盘代替高频轮询,省 Token 也省机器负载。
- 多技能同时启用时,注意钩子加载顺序与重复工具调用(以 SKILL.md 冲突说明为准)。
调试与排错
- 打开 stderr 与客户端日志;PTY/tmux 场景同时看面板最后几十行输出。
- 参数错误时对照 SKILL.md 中的 JSON/CLI 示例(引号、转义、工作目录)。
- 网络类失败:查代理、防火墙、MCP 传输方式(stdio / HTTP / SSE)。
速查
| 动作 | 说明 |
|------|------|
| 获取技能包 | ClawHub / 站内 ZIP,核对版本 |
| 权威步骤 | 优先阅读 ZIP 内 SKILL.md |
| 首次试跑 | 使用 SKILL.md 最小示例 |
| 验收 | 对照路径、测试命令或回调负载 |
常见故障
- 无输出或立即退出 → 工作目录错误、依赖未装、或 Claude Code 未登录;按 SKILL.md 自检清单执行。
- 权限被拒绝 → 检查沙箱路径、
--permission-mode与工具白名单。 - 与简介不符 → 以英文 SKILL 与上游仓库为准,站内稿仅作结构化导读。
# WebSocket Hub Patterns
Production patterns for horizontally-scalable WebSocket connections with Redis-backed coordination.
## Installation
### OpenClaw / Moltbot / Clawbot
```bash
npx clawhub@latest install websocket-hub-patterns
```
---
## When to Use
- Real-time bidirectional communication
- Chat applications, collaborative editing
- Live dashboards with client interactions
- Need horizontal scaling across multiple gateway instances
---
## Hub Structure
```go
type Hub struct {
// Local state
connections map[*Connection]bool
subscriptions map[string]map[*Connection]bool // channel -> connections
// Channels
register chan *Connection
unregister chan *Connection
broadcast chan *Event
// Redis for scaling
redisClient *redis.Client
redisSubs map[string]*goredis.PubSub
redisSubLock sync.Mutex
// Optional: Distributed registry
connRegistry *ConnectionRegistry
instanceID string
// Shutdown
done chan struct{}
wg sync.WaitGroup
}
```
---
## Hub Main Loop
```go
func (h *Hub) Run() {
for {
select {
case <-h.done:
return
case conn := <-h.register:
h.connections[conn] = true
if h.connRegistry != nil {
h.connRegistry.RegisterConnection(ctx, conn.ID(), info)
}
case conn := <-h.unregister:
if _, ok := h.connections[conn]; ok {
if h.connRegistry != nil {
h.connRegistry.UnregisterConnection(ctx, conn.ID())
}
h.removeConnection(conn)
}
case event := <-h.broadcast:
h.broadcastToChannel(event)
}
}
}
```
---
## Lazy Redis Subscriptions
Subscribe to Redis only when first local subscriber joins:
```go
func (h *Hub) subscribeToChannel(conn *Connection, channel string) error {
// Add to local subscriptions
if h.subscriptions[channel] == nil {
h.subscriptions[channel] = make(map[*Connection]bool)
}
h.subscriptions[channel][conn] = true
// Lazy: Only subscribe to Redis on first subscriber
h.redisSubLock.Lock()
defer h.redisSubLock.Unlock()
if _, exists := h.redisSubs[channel]; !exists {
pubsub := h.redisClient.Subscribe(context.Background(), channel)
h.redisSubs[channel] = pubsub
go h.forwardRedisMessages(channel, pubsub)
}
return nil
}
func (h *Hub) unsubscribeFromChannel(conn *Connection, channel string) {
if subs, ok := h.subscriptions[channel]; ok {
delete(subs, conn)
// Cleanup when no local subscribers
if len(subs) == 0 {
delete(h.subscriptions, channel)
h.closeRedisSubscription(channel)
}
}
}
```
---
## Redis Message Forwarding
```go
func (h *Hub) forwardRedisMessages(channel string, pubsub *goredis.PubSub) {
ch := pubsub.Channel()
for {
select {
case <-h.done:
return
case msg, ok := <-ch:
if !ok {
return
}
h.broadcast <- &Event{
Channel: channel,
Data: []byte(msg.Payload),
}
}
}
}
func (h *Hub) broadcastToChannel(event *Event) {
subs := h.subscriptions[event.Channel]
for conn := range subs {
select {
case conn.send <- event.Data:
// Sent
default:
// Buffer full - close slow client
h.removeConnection(conn)
}
}
}
```
---
## Connection Write Pump
```go
func (c *Connection) writePump() {
ticker := time.NewTicker(54 * time.Second) // Ping interval
defer func() {
ticker.Stop()
c.conn.Close()
}()
for {
select {
case message, ok := <-c.send:
c.conn.SetWriteDeadline(time.Now().Add(10 * time.Second))
if !ok {
c.conn.WriteMessage(websocket.CloseMessage, []byte{})
return
}
c.conn.WriteMessage(websocket.TextMessage, message)
// Batch drain queue
for i := 0; i < len(c.send); i++ {
c.conn.WriteMessage(websocket.TextMessage, <-c.send)
}
case <-ticker.C:
if err := c.conn.WriteMessage(websocket.PingMessage, nil); err != nil {
return
}
}
}
}
```
---
## Connection Registry for Horizontal Scaling
```go
type ConnectionRegistry struct {
client *redis.Client
instanceID string
}
func (r *ConnectionRegistry) RegisterConnection(ctx context.Context, connID string, info ConnectionInfo) error {
info.InstanceID = r.instanceID
data, _ := json.Marshal(info)
return r.client.Set(ctx, "ws:conn:"+connID, data, 2*time.Minute).Err()
}
func (r *ConnectionRegistry) HeartbeatInstance(ctx context.Context, connectionCount int) error {
info := InstanceInfo{
InstanceID: r.instanceID,
Connections: connectionCount,
}
data, _ := json.Marshal(info)
return r.client.Set(ctx, "ws:instance:"+r.instanceID, data, 30*time.Second).Err()
}
```
---
## Graceful Shutdown
```go
func (h *Hub) Shutdown() {
close(h.done)
// Close all Redis subscriptions
h.redisSubLock.Lock()
for channel, pubsub := range h.redisSubs {
pubsub.Close()
delete(h.redisSubs, channel)
}
h.redisSubLock.Unlock()
// Close all connections
for conn := range h.connections {
conn.Close()
}
h.wg.Wait()
}
```
---
## Decision Tree
| Situation | Approach |
|-----------|----------|
| Single instance | Skip ConnectionRegistry |
| Multi-instance | Enable ConnectionRegistry |
| No subscribers to channel | Lazy unsubscribe from Redis |
| Slow client | Close on buffer overflow |
| Need message history | Use Redis Streams + Pub/Sub |
---
## Related Skills
- **Meta-skill:** [ai/skills/meta/realtime-dashboard/](../../meta/realtime-dashboard/) — Complete realtime dashboard guide
- [dual-stream-architecture](../dual-stream-architecture/) — Event publishing
- [resilient-connections](../resilient-connections/) — Connection resilience
---
## NEVER Do
- **NEVER block on conn.send** — Use select with default to detect overflow
- **NEVER skip graceful shutdown** — Clients need close frames
- **NEVER share pubsub across channels** — Each channel needs own subscription
- **NEVER forget instance heartbeat** — Dead instances leave orphaned connections
- **NEVER send without ping/pong** — Load balancers close "idle" connections