hyperf websocket服务必须严格遵循协程上下文隔离原则,禁止将连接信息存入控制器属性,应使用context::set/get管理请求级数据,心跳依赖swoole原生配置,连接状态须持久化至redis并配合pub/sub实现跨进程广播。

Hyperf 本身不是为“有状态长连接”设计的,强行在 HTTP 服务里维持用户级长连接(比如 WebSocket 连接不释放、协程里死循环 hold 住变量)会直接破坏其进程常驻模型的核心约束——状态必须隔离、资源必须复用、IO 必须非阻塞。你不是在搭建“有状态服务”,而是在制造内存泄漏和数据污染。
WebSocket 连接必须绑定协程上下文,不能存类属性
常见错误是把 $connection 或 $userId 直接写进 WebSocket 控制器的属性里:
class WebSocketController
{
private $fd; // ❌ 危险!单例控制器被所有连接共享
public function onMessage($server, $frame) {
$this->fd = $frame->fd;
// 后续逻辑可能误用这个 fd,覆盖上一个连接的状态
}
}
正确做法只在当前协程内使用参数或 Context:
- 所有业务逻辑通过
$frame->fd参数传递,不缓存到实例属性 - 需要跨方法传用户 ID?用
Context::set('user_id', $uid)+Context::get('user_id') - 连接关闭时务必清理 Context:
Context::destroy($cid),否则协程复用后残留旧数据
心跳与超时必须靠 Swoole 原生机制,别自己 sleep
有人写 while (true) { sleep(30); $server->push($fd, 'ping'); } —— 这会让整个 Worker 进程卡死,其他连接全部阻塞。
Hyperf 的 WebSocket 服务依赖 Swoole 的 heartbeat_idle_time 和 heartbeat_check_interval 设置,必须在 config/autoload/server.php 里配:
'settings' => [
'websocket_heartbeat_idle_time' => 600, // 10 分钟无消息断连
'websocket_heartbeat_check_interval' => 30, // 每 30 秒发一次 ping
],
- 不要在
onOpen里启协程做定时器;Swoole 已内置心跳管理 - 自定义 ping/pong 响应必须走
$server->push(),且需判断连接是否还存活($server->exist($fd)) - 手动 close 连接后,记得调用
Context::destroy($cid)清理上下文
连接元数据必须存 Redis,禁止用 PHP 数组或静态变量
想查“某用户当前在线哪些设备”?别用 static $online = [] 或全局数组。Worker 进程之间不共享内存,且重启即丢。
所有连接生命周期数据必须落地到外部存储:
- 上线:用
Redis::hSet('ws:online:'.$uid, $fd, json_encode([...])) - 下线:在
onClose回调中执行Redis::hDel('ws:online:'.$uid, $fd) - 广播给某用户所有连接:
Redis::hKeys('ws:online:'.$uid)拿到 fd 列表,逐个$server->push() - 注意:Redis 操作必须用
Hyperf\Redis\Redis协程客户端,不能用phpredis同步驱动
多 Worker 场景下广播必须走 Redis Pub/Sub,不能直连其他 Worker
一个 Worker 收到消息要推给“所有在线用户”,它无法知道其他 Worker 上有哪些连接。硬编码 $server->connections 只返回本 Worker 的连接列表。
正确路径是发布-订阅模式:
- 收到消息后,用
Redis::publish('ws:channel:all', $payload) - 每个 Worker 启动时订阅该 channel(在
onWorkerStart回调里) - 订阅回调中调用
$server->push()推送本 Worker 管理的连接 - 别忘了配置独立 Redis 连接池,避免和业务 Redis 混用导致阻塞
真正的难点不在代码怎么写,而在想清楚“哪部分状态必须跨进程共享”和“哪部分只需本协程临时持有”。Hyperf 不反对有状态,但反对把状态留在不该留的地方——比如类属性、静态变量、未销毁的 Context、或同步 Redis 客户端。这些地方一旦出错,不会报错,只会静默污染后续请求。











