FrankenPHP 通过 Mercure Hub 实现广播:调用 frankenphp\publish() 向合法 URI topic 发布一次 JSON 数据,所有订阅该 topic 的客户端自动接收;前端需为每个 topic 单独创建 EventSource 实例,不可复用同一连接订阅多主题。

FrankenPHP 本身不提供“手动广播给多个用户”的 API,它的广播能力完全依赖 Mercure Hub 的发布-订阅机制:你向一个 topic 发布一次,所有订阅该 topic 的客户端自动收到——这才是真正的、无状态的广播。
用 frankenphp\publish() 向 topic 发布即完成广播
这是最常用、也最可靠的广播方式。只要前端订阅了同一个 topic 字符串,调用一次 publish() 就能触达全部在线订阅者。
-
topic必须是合法 URI(如"https://example.com/notifications"或简写"notifications/user-123"),不能含空格或非法字符 - 数据必须是 JSON-serializable:只支持
array、string、int、bool、null;传stdClass或资源会报错 - 不要在循环里反复 publish 同一消息——Mercure 不保证顺序,但重复 topic + 相同 payload 可能被 Hub 去重(取决于配置)
- 若需按用户分组广播(如“只推给 user-123 的所有标签页”),就用专属 topic,例如
"user/123",别用通配符或正则——Mercure 不支持 pattern matching 订阅
浏览器端订阅多个 topic 要开多个 EventSource 实例
Mercure 没有“单连接多 topic”模式。每个 EventSource 实例只绑定一个 topic 查询参数,想收多个主题就得建多个实例。
- 错误写法:
new EventSource('/mercure?topic=chat&topic=notify')—— URL 参数重复会被覆盖,实际只订阅最后一个 - 正确做法:为每个 topic 单独 new 一个,例如:
const es1 = new EventSource('/mercure?topic=chat/room-a');和const es2 = new EventSource('/mercure?topic=user/123'); - 注意内存泄漏:页面卸载前记得调用
es.close(),否则连接可能滞留(尤其在 SPA 中) - Chrome 对同一域名最多维持 6 个
EventSource连接,超出会排队或失败——这不是 bug,是浏览器限制
用 curl 并发模拟百级订阅者验证广播可靠性
浏览器标签页只能测到个位数并发,压测真实广播能力得靠命令行工具。
立即学习“PHP免费学习笔记(深入)”;
- 启动 30 个独立订阅进程(每条只等一条消息后退出):
for i in $(seq 1 30); do curl -N "https://localhost:8080/.well-known/mercure?topic=https://acme.com/broadcast" --max-time 15 & done -
-N关闭响应缓冲,--max-time 15防止因网络问题卡死 - 发布时观察各终端是否在 100ms 内几乎同时打印响应;若有大量超时,优先检查系统级限制:
ulimit -n(文件描述符)、GOMAXPROCS(Go 运行时线程数) - 别用
curl -s静默模式——你得亲眼看到每条响应是否到达,才能确认广播没丢
Worker 模式下自己维护连接池 ≠ 广播,且极易出错
如果你绕过 Mercure,用 Worker + raw socket 自建 WebSocket 推送,那“广播”就得你自己遍历连接列表并逐个 fwrite()。这条路不是不能走,但代价很高:
- 连接假死难检测:没有心跳机制时,客户端断网后 PHP 进程仍认为连接有效,导致消息堆积或发送失败
- 内存泄漏风险高:每个连接对应一个 resource,忘记
fclose()或未 unset,worker 进程内存会持续增长 - 无法水平扩展:FrankenPHP worker 是短生命周期模型,不适合长期持有数百 socket;Mercure Hub 才是为高并发广播设计的
- 若真要用,务必配合
stream_select()轮询可读性,并设stream_set_timeout()防卡死
真正需要关注的不是“怎么广播”,而是“topic 设计是否合理”和“前端是否稳定重连”。Mercure 的广播逻辑是确定性的,出问题基本都卡在订阅端或网络层。



















