必须确保每条弹幕精准推送到当前房间内所有在线连接,且不发错房间、不漏人、不断连后仍能正常广播;需通过URL参数或首条消息提取room_id并挂载到连接对象,用共享存储(如Redis)维护房间成员列表,广播时跳过发送者自身,并在onClose中及时清理失效连接。

要让观众在观看直播时发送的弹幕实时出现在所有人的屏幕上,必须确保每条弹幕精准推送到当前房间内所有在线连接,且不发错房间、不漏人、不断连后仍能正常广播。
建立 WebSocket 连接并提取房间 ID
前端连接时必须携带 room_id 参数,否则后端无法识别用户归属,后续广播将全部失效。
方法一:通过 URL 查询参数传入
前端使用 new WebSocket("ws://your-domain.com:2346?room_id=1001") 建立连接。
方法二:首次消息中携带 room_id
连接建立后立即发送 JSON 消息:{"type":"join","room_id":"1001"},后端在 onMessage 中解析并挂载。
后端必须在 onConnect 或首次 onMessage 中完成挂载:$connection->room_id = $roomId; —— 【这是后续广播的前提,漏掉则所有弹幕发到空房间】
按房间隔离广播弹幕
第一步:初始化房间连接池
在 Worker 启动前定义全局数组 $rooms = [];,用于暂存各房间连接列表。
第二步:连接加入房间
当新连接携带有效 room_id 时,执行:
if (!isset($rooms[$roomId])) { $rooms[$roomId] = []; }<br>$rooms[$roomId][] = $connection;
第三步:收到弹幕后只广播给同房间其他成员
在 onMessage 中判断 type === "danmaku",然后遍历 $rooms[$connection->room_id],对每个 $conn 调用 $conn->send($data),但必须跳过自己:if ($conn !== $connection) { $conn->send($data); }
这一步不能直接 $connection->send(),否则发弹幕的人自己会看到两次——一次是自己发的,一次是被广播回来的。
解决多进程下房间状态不一致问题
Workerman 默认开启多进程(如 $worker->count = 4),但 PHP 数组 $rooms 是进程私有内存,A 进程添加的连接在 B 进程里完全不可见,导致广播漏人。
方法一:改用 Swoole\Table(需已安装 Swoole)
声明共享表:
$table = new \Swoole\Table(1024);<br>$table->column('connections', \Swoole\Table::TYPE_LIST, 1024);<br>$table->create();写入时用
$table->set($room_id, ['connections' => [...]]),读取时用 $table->get($room_id)。
方法二:接入 Redis 管理房间成员(推荐)
连接加入房间时:$redis->sAdd("room:{$roomId}:members", $connection->id);
弹幕到来时:
$members = $redis->sMembers("room:{$roomId}:members");<br>foreach ($members as $cid) { /* 通过 cid 查找 connection 并 send */ }【注意:Redis 的 set 成员是字符串,connection 对象无法序列化存储,实际需配合 Gateway 或自建连接映射表】
清理断开连接,防止 ghost 弹幕
用户关闭页面或网络中断时,Workerman 会触发 onClose 回调,但不会自动从房间列表中移除该连接。
必须在 onClose 中主动清理:
若用数组方案:
if (isset($rooms[$connection->room_id])) {<br> $rooms[$connection->room_id] = array_filter($rooms[$connection->room_id], function($c) use ($connection) { return $c !== $connection; });<br>}
若用 Redis 方案:$redis->sRem("room:{$connection->room_id}:members", $connection->id);
不做这一步,下次有人发弹幕,系统仍会尝试向已失效的连接发送数据,大量 write error 日志堆积,且弹幕看似“发出去了”实则无人接收。
前端必须传结构化弹幕字段
后端不做任何文本清洗、截断或格式转换,只做透传。弹幕一致性完全依赖前端提交的标准化字段:
text:UTF-8 字符串,前端须 .trim().slice(0,30);
color:十六进制颜色值,如 "#ff4757";
size:整数,建议 16~32;
mode:1(滚动)、4(底部)、5(顶部);
timestamp:毫秒级时间戳,服务端原样返回,前端靠它计算入场偏移。
后端收到后不做校验直接广播,【一旦前端漏传 mode 或 timestamp,Canvas 渲染层将无法正确调度弹幕位置和速度】。

















