// +---------------------------------------------------------------------- namespace addon\mqttbroker\service; use Workerman\Mqtt\Client; /** * 外部 Broker 转发器(规则引擎的投递通道) * * 将本地匹配到的消息转发到外部 MQTT Broker(如 EMQX),实现"桥接到 EMQX"。 * 每个目标 Broker 维护一个异步常驻连接(基于 Workerman\Mqtt\Client,复用 Broker 的事件循环), * 连接未就绪时消息进入待发缓冲(上限 200,避免无限堆积),连接成功后冲刷。 * * 依赖 workerman/mqtt(composer require workerman/mqtt)。未安装时 isAvailable()=false, * 规则引擎自动跳过转发,不影响 Broker 其它功能。 */ class Forwarder { /** @var array key => ['client'=>Client|null,'connected'=>bool,'pending'=>callable[]] */ protected $clients = []; public function isAvailable(): bool { return class_exists(Client::class); } /** * 按规则转发一条消息到外部 Broker */ public function forward(array $rule, string $topic, string $msg, int $qos): void { if (!$this->isAvailable()) { return; } $key = md5(($rule['target_broker'] ?? '') . '|' . ($rule['target_clientid'] ?? '') . '|' . ($rule['target_username'] ?? '')); if (!isset($this->clients[$key]) || $this->clients[$key]['client'] === null) { $this->connect($key, $rule); } $entry = &$this->clients[$key]; $target = $this->buildTargetTopic((string) ($rule['target_topic'] ?? ''), $topic); $pubQos = min($qos, (int) ($rule['target_qos'] ?? 0)); $send = function () use ($entry, $target, $msg, $pubQos) { try { $entry['client']->publish($target, $msg, ['qos' => $pubQos]); } catch (\Throwable $e) { } }; if ($entry['connected']) { $send(); } elseif (count($entry['pending']) < 200) { $entry['pending'][] = $send; } } /** * 建立到目标 Broker 的异步连接(懒连接) */ protected function connect(string $key, array $rule): void { $this->clients[$key] = ['client' => null, 'connected' => false, 'pending' => []]; try { $uri = $this->buildUri((string) ($rule['target_broker'] ?? '')); $opts = []; if (!empty($rule['target_clientid'])) { $opts['clientId'] = $rule['target_clientid']; } if (!empty($rule['target_username'])) { $opts['username'] = $rule['target_username']; $opts['password'] = $rule['target_password'] ?? ''; } $client = new Client($uri, $opts); $self = $this; $client->onConnect = function () use ($key, $self) { $self->clients[$key]['connected'] = true; foreach ($self->clients[$key]['pending'] as $cb) { try { $cb(); } catch (\Throwable $e) { } } $self->clients[$key]['pending'] = []; }; $client->onError = function () use ($key, $self) { $self->clients[$key]['connected'] = false; }; $client->onClose = function () use ($key, $self) { $self->clients[$key]['connected'] = false; }; $client->connect(); $this->clients[$key]['client'] = $client; } catch (\Throwable $e) { $this->clients[$key] = ['client' => null, 'connected' => false, 'pending' => []]; } } protected function buildUri(string $broker): string { $broker = trim($broker); if ($broker === '' || strpos($broker, '://') !== false) { return $broker === '' ? 'mqtt://127.0.0.1:1883' : $broker; } return 'mqtt://' . $broker; } /** * 目标主题模板:空或 ${topic} 表示沿用原主题,否则替换变量 */ protected function buildTargetTopic(string $tpl, string $srcTopic): string { if ($tpl === '' || $tpl === '${topic}') { return $srcTopic; } return str_replace('${topic}', $srcTopic, $tpl); } }