Workerman实现 WSS 基于客户端 ID 的精准推送,核心思路是:在服务端建立「客户端ID ↔ 连接实例」的映射关系,推送时通过 ID 找到对应的连接,再发送消息。下面结合 Workerman(你当前使用的框架)给出完整、可落地的实现方案:
1. 核心原理(先理解逻辑)
客户端连接WSS
服务端分配唯一connection_id
客户端主动发送自定义ID
服务端建立ID-connection映射
需要精准推送
根据自定义ID查映射表
找到对应connection实例
调用send方法推送消息
关键是:Workerman 每个连接自带唯一 connection_id,但这个 ID 是临时的(断开重连会变),所以需要绑定你业务侧的自定义客户端ID(比如用户ID、设备ID)。
2. 完整实现代码(Workerman 端)
以下是 GatewayWorker 架构下的核心代码(适配你的 8282 端口 WSS 服务):
步骤1:修改 Events.php(核心逻辑)
php
<?php
/**
* GatewayWorker的事件处理类
*/
class Events
{
// 存储「客户端ID → 连接connection」的映射(全局数组)
public static $client_map = [];
/**
* 当客户端连接上来时触发
* @param $connection 连接实例
*/
public static function onConnect($connection)
{
// 1. 连接成功时,Workerman自动生成唯一connection_id(临时)
echo "客户端连接成功,临时ID:{$connection->id}\n";
}
/**
* 当客户端发送消息时触发(核心:绑定自定义ID)
* @param $connection 连接实例
* @param $data 客户端发送的消息(JSON格式)
*/
public static function onMessage($connection, $data)
{
$data = json_decode($data, true);
if (!$data) {
$connection->send(json_encode(['code' => 400, 'msg' => '消息格式错误']));
return;
}
// 场景1:客户端绑定自定义ID(比如用户ID)
if ($data['type'] === 'bind') {
$client_id = $data['client_id']; // 客户端传的业务ID(如用户ID 1001)
// ① 移除旧映射(防止同一ID多端登录覆盖)
if (isset(self::$client_map[$client_id])) {
unset(self::$client_map[$client_id]);
}
// ② 绑定新映射(自定义ID ↔ 连接实例)
self::$client_map[$client_id] = $connection;
// ③ 给connection绑定反向属性(方便后续操作)
$connection->client_id = $client_id;
// 回复客户端绑定成功
$connection->send(json_encode(['code' => 200, 'msg' => 'ID绑定成功']));
return;
}
// 场景2:精准推送指令(比如管理员后台触发)
if ($data['type'] === 'push' && $data['target_id'] && $data['msg']) {
$target_id = $data['target_id']; // 要推送的目标客户端ID
$msg = $data['msg']; // 推送的内容
// ① 检查目标ID是否存在映射
if (!isset(self::$client_map[$target_id])) {
$connection->send(json_encode(['code' => 404, 'msg' => '目标客户端未连接']));
return;
}
// ② 精准推送:找到目标连接,发送消息
$target_conn = self::$client_map[$target_id];
$target_conn->send(json_encode([
'type' => 'push',
'from' => $connection->client_id ?? 'admin',
'msg' => $msg
]));
// ③ 回复推送方成功
$connection->send(json_encode(['code' => 200, 'msg' => '精准推送成功']));
return;
}
// 其他消息处理...
$connection->send(json_encode(['code' => 200, 'msg' => '收到消息:'.$data['msg']]));
}
/**
* 当客户端断开连接时触发(清理映射,避免内存泄漏)
* @param $connection 连接实例
*/
public static function onClose($connection)
{
// 移除该连接对应的ID映射
if (isset($connection->client_id) && isset(self::$client_map[$connection->client_id])) {
unset(self::$client_map[$connection->client_id]);
echo "客户端 {$connection->client_id} 断开连接,已清理映射\n";
}
echo "临时ID {$connection->id} 断开连接\n";
}
}
步骤2:客户端对接(浏览器/JS 示例)
javascript
// 连接WSS服务(用域名,避免证书报错)
const ws = new WebSocket('wss://你的域名:8282');
// 1. 连接成功后,绑定自定义ID(比如用户ID 1001)
ws.onopen = function() {
console.log('连接成功');
// 发送绑定指令
ws.send(JSON.stringify({
type: 'bind',
client_id: '1001' // 业务侧的唯一ID(用户ID/设备ID)
}));
};
// 2. 接收服务端推送的消息
ws.onmessage = function(e) {
const res = JSON.parse(e.data);
console.log('收到消息:', res);
// 处理精准推送的消息
if (res.type === 'push') {
alert(`收到精准推送:${res.msg}`);
}
};
// 3. 主动触发精准推送(比如给ID 1002推送消息)
function pushToClient() {
ws.send(JSON.stringify({
type: 'push',
target_id: '1002', // 要推送的目标ID
msg: '你好,这是给你的精准推送!'
}));
}
3. 进阶优化(生产环境必做)
(1)映射表持久化/分布式
上面的 $client_map 是内存数组,仅适用于单服务器;如果是多服务器集群,需要用 Redis 存储映射关系:
php
// 绑定ID时存Redis
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$redis->set('client:'.$client_id, $connection->id); // 存connection_id
$redis->set('connection:'.$connection->id, $client_id); // 反向映射
// 推送时查Redis
$target_conn_id = $redis->get('client:'.$target_id);
// 通过Gateway类获取对应连接
Gateway::sendToClient($target_conn_id, $msg);
(2)连接异常处理
- 给映射表加过期时间,避免无效连接残留;
- 推送前检查连接是否存活(
$connection->getStatus())。
(3)GatewayWorker 专用方法
如果用的是 GatewayWorker 完整架构,推荐用官方封装的方法:
php
// 绑定ID
Gateway::bindUid($connection->id, $client_id);
// 根据ID推送
Gateway::sendToUid($client_id, $msg);
// 解绑ID
Gateway::unbindUid($connection->id, $client_id);
4. 测试验证步骤
- 客户端A连接,绑定ID
1001; - 客户端B连接,绑定ID
1002; - 客户端A调用
pushToClient()给1002推送消息; - 客户端B能收到精准推送,客户端A收到"推送成功"回复 → 验证完成。
回顾
- 精准推送的核心是建立「业务客户端ID ↔ Workerman连接实例」的映射关系;
- 客户端先主动绑定自定义ID,服务端存储映射,推送时通过ID查连接并发送消息;
- 单服务器用内存数组即可,集群环境需用Redis存储映射,结合GatewayWorker的
sendToUid方法更高效; - 断开连接时必须清理映射,避免内存泄漏和无效推送。
如果需要适配多服务器集群、或结合PHP后台(非客户端触发)实现服务端主动推送,我可以补充对应的代码示例。