不能直接用$fd作为用户标识,因为$fd是swoole分配的临时文件描述符,仅在当前worker进程生命周期内有效,会复用、不可跨进程共享,且服务重启后失效;必须通过redis等共享存储建立user_id↔fd双向映射,并在onopen绑定、onclose清除,推送前还需用$server->exist($fd)二次校验连接有效性。

为什么不能直接用 $fd 作为用户标识
WebSocket 连接的 $fd 是 Swoole 分配的整数,只在当前 Worker 进程内有效,重启服务、Worker 进程崩溃或负载均衡下会失效;不同进程间也无法共享。私聊必须绑定业务侧的 user_id,否则发消息时根本不知道该找谁。
所以必须建立 user_id → fd 的双向映射,且需跨进程可用——Redis 是最直接的选择。
常见错误现象:
• 消息发到别的用户(fd 被复用)
• 私聊失败但无报错(查 Redis 发现 user_id 对应的 fd 已过期)
• 多实例部署时私聊完全不可用(没走 Redis,只存内存)
如何设计 Redis 存储结构:用 Hash 还是 String
推荐用 Redis HASH,键为 ws:fd_map,字段为 user_id,值为 fd。这样能原子性地 HSET/HGET/HDEL,也方便用 HSCAN 批量清理过期连接。
不建议用 String(如 ws:fd:{user_id}),原因:
• 删除用户所有连接需遍历 key(无法批量)
• 无法快速判断某 user_id 是否在线(得 EXISTS + GET 两步)
• 占用更多 key 数量,影响 Redis 内存碎片和集群 slot 分布
示例操作:HSET ws:fd_map 1001 123HGET ws:fd_map 1001 → 返回 "123"HDEL ws:fd_map 1001
如何在 onOpen 和 onClose 中维护 Redis 映射
Hyperf 的 WebSocket Server 使用 onOpen 回调接收新连接,onClose 清理断连。这两个钩子必须严格配对,且加异常兜底。
实操要点:
• onOpen 中先校验 token 或 session 获取 user_id,失败立即 close(),**不要存映射**
• 校验通过后,用 redis->hSet('ws:fd_map', $user_id, $fd) 写入,建议设置过期时间(如 3600 秒),防脏数据堆积
• onClose 中必须执行 redis->hDel('ws:fd_map', $user_id),并忽略返回值是否为 0(可能已超时自动删除)
• 若使用多 Redis 实例或 Cluster 模式,确保 ws:fd_map 在同一 slot(可加 {ws:fd_map} 前缀强制哈希)
发送私聊消息时,如何安全获取并校验 fd
调用 $server->push($fd, $data) 前,必须确认 fd 有效且属于目标用户——Swoole 不保证 fd 未被释放,直接 push 可能触发 warning 或静默失败。
正确流程:
• 先 redis->hGet('ws:fd_map', $target_user_id) 获取 $fd
• 判空:若返回 null 或 false,说明用户不在线,直接返回或走离线消息逻辑
• 再调用 $server->exist($fd) 确认连接仍存活(这是关键一步,避免 push 到已关闭的 fd)
• 最后 $server->push($fd, $data),捕获异常(如 InvalidArgumentException 表示 fd 无效)
注意:$server->exist() 是进程内检查,必须配合 Redis 映射使用,二者缺一不可。
容易被忽略的地方:Redis 映射和 exist() 检查之间存在微小时间窗口,极端情况下 fd 可能刚断开但 Redis 还没删掉。生产环境建议加一层重试或降级(比如 failover 到通知网关)。











