webman是实时对战类游戏后端的刚性选择,因其协程非阻塞特性可满足毫秒级响应要求;必须用协程驱动(如co::mysql、co::readfile)、避免同步i/o阻塞worker;连接状态须存swoole\table或redis并配对清理心跳与onclose。

Webman 不是“适合”做游戏后端,而是实时对战类游戏后端的刚性选择——同步阻塞模型在这里根本跑不起来。
onMessage 里写 file_get_contents 或 mysqli_query 就会卡住所有玩家
实时对战(如 MOBA 技能帧同步、FPS 位置广播)要求每个 onMessage 执行必须在毫秒级内返回。但 file_get_contents、mysqli_query 这类原生 PHP 同步 I/O 会直接阻塞当前 Worker 进程,导致该进程无法响应其他连接的任何事件——包括心跳、移动指令、断线检测。
常见错误现象:
- WebSocket 连接未断,但玩家操作明显延迟、掉帧
- 用
strace -p [pid]能看到进程长期停在read或connect系统调用上 - 增加 Worker 数量无效,因为每个进程都被单次阻塞拖垮
正确做法:
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
- 数据库必须用
webman/database+ Swoole 协程驱动(如co::mysql),不是 PDO 模拟协程 - HTTP 请求改用
Workerman\Http\Client,它底层走协程 socket - 文件读取用
co::readFile,而非file_get_contents - 若未启用 Swoole,
webman/database会退化为同步模式,此时应换用hyperf/database或封装连接池+异步客户端
Connection 对象不能存玩家状态,$connection->playerId = 123 是无效的
Webman\WebSocket\Connection 实例只在当前回调生命周期内有效。你在 onMessage 里给它加属性,下一次回调时该对象已被销毁重建,属性丢失——这不是 bug,是设计使然。
容易踩的坑:
- 在
onMessage中写$connection->roomId = 5,然后在后续消息里读不到 - 在
onClose里没清理外部状态映射,导致 Redis/内存表里残留“幽灵玩家” - 高频查询状态时用普通 PHP 数组,多 Worker 下不共享,且无并发保护
正确做法:
- 用
$connection->getFd()作 key,把玩家实体存到Swoole\Table或 Redis Hash(如HSET player:fd_123 room_id 5 score 99) - 务必在
onClose回调中执行DEL或HDEL清理对应记录 - 若用内存数组映射,需配合
Swoole\Lock加锁,或改用Swoole\Table原生支持多进程读写
广播技能释放消息时别 foreach $connections->send() 串行发
一个玩家释放技能,要立刻广播给同房间其余 9 人。如果用 foreach ($connections as $conn) { $conn->send($packet); },只要其中某条连接网络抖动、发送缓冲区满,整个循环就卡住——其余 8 人被迫等待,延迟飙升。
正确做法是解耦广播逻辑:
- 在
onMessage中只触发事件:Event::fire('room:skill_cast', [$roomId, $packet]) - 监听器里用
foreach分发,但每个$conn->send()必须包裹在go(function () use ($conn, $packet) { ... })协程中 - 或者更稳妥:把广播任务推入 Redis List(
LPUSH broadcast:room_5 $packet),另起一个Worker::onWorkerStart定时器协程消费,避免阻塞主事件循环
性能注意:不要在监听器里做耗时计算(如序列化、拼包),提前准备好二进制包体;广播前先过滤掉已断开连接(查 $conn->isConnected())。
心跳超时清理和 onClose 清理必须成对出现
玩家切后台、断网、关页面,通常不会发标准 WebSocket close 帧,连接会“悬空”。若只依赖 onClose 清理,就会积累僵尸连接,导致广播错发、匹配失败、内存泄漏。
必须同时做两件事:
- 客户端每 15 秒发一次
ping,服务端收到后更新$connection->last_time = time() - 在
Worker::onWorkerStart中启动定时器:Timer::add(10, function () { ... }),每 10 秒遍历所有连接,对time() - $connection->last_time > 30的连接执行$connection->close() -
onClose回调里必须同步删除 Redis 中该连接对应的所有状态:HDEL room:123 player_fd_456、ZREM leaderboard:123 player_456等
漏掉任意一环,“玩家已离线但服务器还认他在场”就会成为常态——这是线上最隐蔽也最难排查的问题之一。










