frankenphp内置mercure不支持指定用户推送,仅支持基于uri topic的广播;需通过专属topic设计(如/users/456)与jwt订阅权限控制实现逻辑上的定向推送。

FrankenPHP 本身不支持“向指定用户推送”这个语义——它内置的 Mercure Hub 是 topic-based 的广播系统,不是 connection-aware 的点对点通道。 你没法直接调用某个函数传个 $user_id 就把消息塞进他浏览器。真要实现“只推给 A 不推给 B”,必须靠你在 topic 设计或 JWT 权限上做隔离,而不是依赖 Hub 自带功能。
为什么 Mercure 天然不支持“指定用户”
Mercure 的协议模型是:客户端订阅一个或多个 URI(topic),服务端往 topic 发布,所有订阅者都收到。它没有连接 ID、没有 session 绑定、也不维护客户端列表。FrankenPHP 内置的 Hub 完全遵循这一规范,没加任何“定向推送”扩展。
常见误解是以为 mercure_publish() 或 POST 到 /.well-known/mercure 时能带个 to_user_id 字段——不行。Hub 只认 mercure.publish 声明里的 topic URL,其他字段全被忽略。
- Topic 必须是合法 URI(如
https://example.com/users/123),且字节级严格匹配 - 订阅者能否收到,只取决于他是否订阅了该 topic,以及他的 JWT 是否包含对应
subscribe权限 - 没有“排除某几个订阅者”的机制,也没有“按 metadata 过滤”的能力
用 topic + subscriber JWT 实现逻辑上的“指定用户”
真正可行的做法,是让每个用户只订阅属于自己的 topic,并在生成 subscriber token 时精确控制权限。这不是 Hub 的功能,而是你应用层的约定。
例如:用户 ID 是 456,你前端订阅时用:
const es = new EventSource('https://localhost:8080/.well-known/mercure?topic=https://example.com/users/456');
同时,PHP 后端生成 subscriber JWT 时,payload 必须显式声明只允许订阅这个 topic:
$payload = [
'mercure' => [
'subscribe' => ['https://example.com/users/456']
]
];
关键点:
- Caddyfile 中的
subscriber_jwt密钥必须和 PHP 签名时用的完全一致(包括换行、空格) - 不能用
*或通配符;Mercure 不支持https://example.com/users/* - 如果用户还订阅了公共 topic(如
https://example.com/notifications),要把它也列进subscribe数组里 - 前端请求必须带
Authorization: Bearer <token></token>,否则 401
发布时只推给特定用户:构造专属 topic 并调用 mercure_publish()
当你想通知用户 456 时,PHP 不是“找连接发消息”,而是往 https://example.com/users/456 这个 topic 发布一条消息。只有订阅了它的客户端(且 JWT 权限正确)才会收到。
最小发布代码示例(无 SDK,纯 curl):
$topic = 'https://example.com/users/456';
$publisherToken = openssl_sign(json_encode(['mercure' => ['publish' => [$topic]]]), $key, 'sha256');
$ch = curl_init('http://host.docker.internal:8080/.well-known/mercure');
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['data' => json_encode(['msg' => 'Hello user 456']), 'type' => 'application/json', 'id' => uniqid(), 'retry' => 10000]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Authorization: Bearer ' . base64_encode($publisherToken),
'Content-Type: application/json',
'Accept: text/event-stream'
]);
curl_exec($ch);
注意:
-
$key是 Caddyfile 中publisher_jwt配置的密钥,不是 subscriber 密钥 - topic URL 必须和前端订阅时用的**完全一致**(协议、域名、路径、尾部斜杠)
- 若用 Docker,
localhost在 PHP 容器内不可达,必须用host.docker.internal或宿主机 IP - 不要试图在 topic 里拼 query 参数(如
?user_id=456),Mercure 不解析 query,只比对完整 URI
Worker 模式下自己管理连接?别踩内存泄漏坑
如果你放弃 Mercure,改用 FrankenPHP 的 Worker + WebSocket 手动维护连接池,理论上可以做到真正的“指定用户”(比如查 $worker->uidConnections[$uid])。但这是高风险路径:
- Worker 进程默认不共享内存,多进程部署时,
$uidConnections只在当前进程有效;跨进程推送需额外引入 Redis Channel 或消息队列 - 连接假死难检测:没有心跳机制时,
$connection->send()可能静默失败,$uidConnections数组会越积越大 - PHP 的
gc_collect_cycles()不会自动清理断开的连接,必须在onClose回调里手动unset() - 单进程
$worker->count = 1是硬性要求;设成 >1 会导致$uidConnections键冲突或丢失
真正需要点对点、高可靠、带状态的推送,Mercure 不是设计目标——这时候该考虑专门的实时服务(如 Centrifugo)或降级到 WebSocket + 自研路由,而不是强行在 FrankenPHP Worker 里堆逻辑。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











