GatewayWorker是PHP生态中实现APP消息推送最稳定可控的方案,核心是通过长连接通道秒级触达,需客户端绑定业务ID、服务端用Gateway::bindUid关联用户,并配合ACK机制、离线兜底和分组部署保障可靠性。

Workerman 实现 APP 消息推送通知,核心不是“发 HTTP 请求”,而是维持一条服务端可控的长连接通道。只要连接活着,就能秒级触达——这比轮询、APNs 透传再走业务逻辑快得多,也比依赖第三方推送 SDK 更可控。
关键结论:用 GatewayWorker 架构 + 客户端主动绑定业务 ID,是当前 PHP 生态下最稳定、可运维的 APP 推送方案。别硬套单 Worker 的 WebSocket 实现,它扛不住多端登录、断线重连、集群扩容。
为什么不能只用 Worker 直接监听 WSS?
单 Worker 实例看似简单,但遇到真实 APP 场景立刻崩:
- APP 后台被杀后冷启动,连接断开,
connection_id彻底丢失,服务端无法关联“这个连接对应哪个用户” - 用户在手机 A 登录,又在手机 B 登录,两个连接都挂着,推送会发两遍(或覆盖不及时)
- 单进程无法水平扩展,10 万连接就卡死,加
$worker->count > 1后连接散落在不同进程,sendToUid()根本找不到目标 - 没有心跳保活、断线自动清理、连接数统计等生产级能力,上线等于裸奔
GatewayWorker 中如何绑定 APP 用户 ID?
绑定不是靠 Cookie 或 Session,而是客户端首次连接后,主动发一条 JSON 指令告诉服务端“我是用户 10086”:
{"type":"bind","client_id":"10086","device_token":"xxx"}
服务端在 Events::onMessage() 里处理:
- 校验
client_id是否合法(比如是否为数字、长度、是否已存在风控标记) - 调用
Gateway::bindUid($connection->id, $client_id),把临时connection_id和业务 ID 绑定 - 同一
client_id多次绑定时,旧连接会自动被踢掉(避免多端消息错乱) - 务必在
onClose()里调用Gateway::unbindUid($client_id),否则内存泄漏
APP 端怎么确保收到推送不丢?
纯靠 WebSocket send() 不够,必须加三层保障:
-
服务端写日志:每次
Gateway::sendToUid()成功后,记录uid、msg_id、时间戳到 Redis 或 DB,供失败重推 -
APP 端 ACK 机制:收到消息后主动回传
{"type":"ack","msg_id":"abc123"},服务端删掉待确认队列 -
离线消息兜底:若
Gateway::isOnline($uid) === false,把消息存进 Redis List(如offline:10086),APP 重连后拉取并标记已读
注意:Gateway::sendToUid() 返回 true 只代表“投递到网关进程”,不代表 APP 已接收。网络抖动、APP 进入后台、系统省电策略都可能让消息滞留在 TCP 缓冲区。
推送性能瓶颈在哪?怎么压测验证?
真正卡点从来不是 PHP 代码,而是:
-
Gateway 进程和 BusinessWorker 进程之间的 IPC 延迟:用
Gateway::sendToUid()本质是发消息给 Gateway 进程,再由它转发。单机 5 万连接时,IPC 队列积压会导致延迟突增 -
客户端心跳间隔设置不合理:APP 在 iOS 后台默认 30 秒断连,Android 各厂商差异大,建议心跳设为 25 秒,并在
onClose()里打日志看断连原因 -
没做连接分组:所有用户塞一个 Gateway,不如按地区/业务线拆成多个
Gateway实例(如gateway-sh、gateway-sz),用 Nginx 做路由
压测时别只跑 ab 或 wrk,要用真实 APP SDK 模拟 1000+ 并发长连接,观察 Gateway::getAllClientCount() 和 Gateway::getClientCountByUid() 的返回值是否一致——不一致说明映射表已脏。











