Uygulamalı Eğitimler

php-mqtt/client ile PHP'de MQTT

php-mqtt/client ile PHP'de MQTT: kurulum, QoS 1 ile publish, subscriber döngüsü, TLS ve Last Will ekleme ve Laravel paketiyle kullanım adım adım anlatılıyor.

Güncellendi · 8 dk okuma

php-mqtt/client ile PHP'de MQTT

PHP'de MQTT kullanmanın standart yolu php-mqtt/client kütüphanesidir: Composer ile kurar, bir MqttClient oluşturur, connect() çağırır, ardından her yerden publish() ve uzun ömürlü bir CLI döngüsü içinde closure ile subscribe() yaparsınız. TCP veya TLS üzerinden MQTT 3.1 ve 3.1.1 konuşur, varsayılanların ötesinde PHP eklentisi gerektirmez ve resmi bir Laravel sarmalayıcısı vardır.

Bu eğitim publish işlemini (çoğu kişinin gözden kaçırdığı QoS 1 ayrıntısı dahil), sağlam bir subscriber'ı, kimlik bilgileriyle TLS'i, Last Will'i ve her parçanın nasıl test edileceğini ele alıyor.

Kurulum

php-mqtt/client, PHP 8 ve Composer gerektirir:

Kütüphaneyi kurun
composer require php-mqtt/client

PHP bir broker ile birbirinden çok farklı iki şekilde konuşur ve bunları karıştırmak sorunların çoğuna yol açar:

RolNerede çalışırÖmrüÖrnek
PublisherWeb isteği (PHP-FPM, Apache), cron, kuyruk job'uMilisaniyeler içinde bağlan, publish et, bağlantıyı kesKullanıcı bir düğmeye tıkladığında komut göndermek
SubscriberCLI süreci (php subscriber.php)Saatler veya günler, denetimliSensör ölçümlerini veritabanına kaydetmek

PHP-FPM worker'ları geri dönüştürülür ve istekler zaman aşımına uğrar; bu yüzden asla bir web isteği içinde aboneliği açık tutmaya çalışmayın.

Mesaj publish etme

publish.php
<?php
require __DIR__ . '/vendor/autoload.php';

use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\MqttClient;

$clientId = 'testmqtt-php-' . bin2hex(random_bytes(4));
$mqtt = new MqttClient('broker.connectmq.com', 1883, $clientId, MqttClient::MQTT_3_1_1);

$settings = (new ConnectionSettings)
    ->setConnectTimeout(5)
    ->setKeepAliveInterval(30);

$mqtt->connect($settings, true);          // true = clean session

$payload = json_encode(['temp' => 21.5, 'ts' => time()]);
$mqtt->publish('testmqtt/php/sensors/room1', $payload, MqttClient::QOS_AT_LEAST_ONCE);

$mqtt->loop(true, true);                  // wait for the PUBACK, then return
$mqtt->disconnect();
echo "published\n";

İki ayrıntı önemlidir:

  • Rastgele client ID. Her istek ayrı bir bağlantıdır. Eşzamanlı iki istek aynı sabit ID'yi kullansaydı broker ilkinin bağlantısını keserdi; bkz. yinelenen client ID. Client ID oluşturucu iyi formatlar gösterir.
  • QoS 1 veya 2 publish'ten sonra loop(true, true). publish() mesajı yalnızca kuyruğa ekler; onay döngü içinde işlenir. İkinci argüman, bekleyen bir şey kalmadığında loop()'un dönmesini sağlar. QoS 0'da doğrudan bağlantıyı kesebilirsiniz. MQTT QoS seviyeleri bu dengeyi açıklar.

İstek başına bir bağlantı açmak ara sıra gelen olaylar için sorun değildir. Bir sayfa her ziyarette publish ediyorsa veya mesajları toplu halde gönderiyorsanız, işi tek bir bağlantıyı açık tutan bir kuyruk worker'ına devredin; bu, Laravel'de MQTT yazısında gösterilen kalıptır.

Durum için retained mesajlar

publish()'in dördüncü argümanı retain bayrağıdır. Retained bir mesaj broker'da saklanır ve her yeni subscriber'a anında iletilir; bu da onu, bir PHP yönetim panelinden düzenlenen cihaz yapılandırması gibi "güncel durum" topic'leri için ideal kılar:

Retained durumu publish edin ve temizleyin
$config = ['interval' => 60, 'unit' => 'C'];
$mqtt->publish('testmqtt/php/devices/42/config', json_encode($config), MqttClient::QOS_AT_LEAST_ONCE, true);

// An empty retained payload deletes the stored message
$mqtt->publish('testmqtt/php/devices/42/config', '', MqttClient::QOS_AT_LEAST_ONCE, true);
$mqtt->loop(true, true);

Retain'i olaylar için değil, durum için kullanın: retained bir "düğmeye basıldı" mesajı, sonradan abone olan her istemciye yeniden iletilir. Payload'ları zaman damgalı kompakt JSON olarak tutun ki tüketiciler durumun ne kadar eski olduğunu anlayabilsin.

CLI worker'da subscribe

Bir subscriber her topic filtresi için bir closure kaydeder ve ardından loop(true) çağırır; bu çağrı siz kesene kadar bloklar ve mesajları dağıtır:

subscriber.php
<?php
require __DIR__ . '/vendor/autoload.php';

use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\MqttClient;

$mqtt = new MqttClient('broker.connectmq.com', 1883, 'testmqtt-php-worker', MqttClient::MQTT_3_1_1);
$mqtt->connect((new ConnectionSettings)->setKeepAliveInterval(30), true);

$mqtt->subscribe('testmqtt/php/sensors/+', function (string $topic, string $message, bool $retained) {
    $data = json_decode($message, true);
    if (!is_array($data)) {
        fwrite(STDERR, "invalid JSON on $topic\n");
        return;
    }
    printf("%s%s temp=%s\n", $topic, $retained ? ' (retained)' : '', $data['temp'] ?? '?');
}, MqttClient::QOS_AT_LEAST_ONCE);

// Stop cleanly on Ctrl+C or a SIGTERM from systemd / Docker
pcntl_async_signals(true);
pcntl_signal(SIGINT, fn () => $mqtt->interrupt());
pcntl_signal(SIGTERM, fn () => $mqtt->interrupt());

$mqtt->loop(true);
$mqtt->disconnect();

+ joker karakteri tek bir topic seviyesiyle eşleşir; yani bu filtre testmqtt/php/sensors/room1 alır ama testmqtt/php/sensors/room1/battery almaz. Dağıtımdan önce filtrelerinizi topic matcher ile kontrol edin.

Bir terminalde php subscriber.php, diğerinde php publish.php çalıştırın. Ölçümün milisaniyeler içinde ekrana yazıldığını görmelisiniz.

Bağlantı kopmalarını atlatmak

Haftalarca çalışan bir worker er ya da geç bağlantısını kaybeder: broker yeniden başlatmaları, NAT zaman aşımları, deploy'lar. Bu olduğunda kütüphane loop() içinden bir exception fırlatır. En basit sağlam kalıp, back-off ile yeniden bağlanan ve tekrar abone olan bir dış döngüdür:

worker.php
<?php
require __DIR__ . '/vendor/autoload.php';

use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\Exceptions\MqttClientException;
use PhpMqtt\Client\MqttClient;

$settings = (new ConnectionSettings)
    ->setKeepAliveInterval(30)
    ->setLastWillTopic('testmqtt/php/worker/status')
    ->setLastWillMessage('offline')
    ->setLastWillQualityOfService(MqttClient::QOS_AT_LEAST_ONCE)
    ->setRetainLastWill(true);

$delay = 1;
while (true) {
    try {
        $mqtt = new MqttClient('broker.connectmq.com', 1883, 'testmqtt-php-worker', MqttClient::MQTT_3_1_1);
        $mqtt->connect($settings, true);
        $mqtt->publish('testmqtt/php/worker/status', 'online', MqttClient::QOS_AT_LEAST_ONCE, true);
        $mqtt->subscribe('testmqtt/php/sensors/+', function (string $topic, string $message) {
            echo "$topic $message\n";
        }, MqttClient::QOS_AT_LEAST_ONCE);

        $delay = 1;                       // connected: reset back-off
        $mqtt->loop(true);
    } catch (MqttClientException $e) {
        fwrite(STDERR, 'MQTT error: ' . $e->getMessage() . "\n");
        sleep($delay);
        $delay = min($delay * 2, 30);
    }
}

Worker temiz bir bağlantı kesme olmadan ölürse Last Will offline (retained) publish eder; worker ise her bağlantıdan sonra bunu online ile ezer. Bu, diğer servislerin abone olabileceği ucuz bir sağlık göstergesidir. Ayrıntılar MQTT Last Will yazısında.

Burada clean session kullanmak bilinçli bir tercihtir: worker her bağlantıda yeniden abone olur, dolayısıyla broker'ın bir şey saklamasına bağımlı değildir. Worker çevrimdışıyken mesajların kuyrukta beklemesi gerekiyorsa sabit bir client ID ile connect($settings, false) kullanın ve bunu kendi özel broker'ınızda test edin; paylaşımlı broker.connectmq.com endpoint'i çevrimdışı oturum tutmaz. Bkz. kalıcı oturumlar.

php-mqtt/client ayrıca setMaxReconnectAttempts() ve setDelayBetweenReconnectAttempts() (milisaniye) ile birlikte setReconnectAutomatically(true) sunar; job'lar arasında bağlantıyı açık tutan publisher'lar için kullanışlıdır.

TLS ve kimlik doğrulama

Production broker'ları 8883 portunda TLS ve kimlik bilgileri ister. Yalnızca port ve ayarlar değişir:

tls.php
<?php
require __DIR__ . '/vendor/autoload.php';

use PhpMqtt\Client\ConnectionSettings;
use PhpMqtt\Client\MqttClient;

$settings = (new ConnectionSettings)
    ->setUsername(getenv('MQTT_USER'))
    ->setPassword(getenv('MQTT_PASS'))
    ->setUseTls(true)
    ->setTlsVerifyPeer(true)
    ->setTlsVerifyPeerName(true)
    ->setTlsSelfSignedAllowed(false);
    // ->setTlsCertificateAuthorityFile('/path/to/ca.pem') for a private CA

$mqtt = new MqttClient('broker.connectmq.com', 8883, 'testmqtt-php-' . bin2hex(random_bytes(4)), MqttClient::MQTT_3_1_1);
$mqtt->connect($settings, true);
$mqtt->publish('testmqtt/php/secure', 'hello over TLS', MqttClient::QOS_AT_LEAST_ONCE);
$mqtt->loop(true, true);
$mqtt->disconnect();

Açık bir CA dosyası verilmezse PHP'nin OpenSSL'i sistemin güven deposunu (veya php.ini içindeki openssl.cafile) kullanır. Peer doğrulamasını açık tutun; kapatmak gerçek sertifika sorunlarını gizler.

Laravel'de kullanım

Aynı geliştiriciler, bağlantı ayarlarını config/mqtt-client.php ve .env dosyasından okuyan ve bir facade sunan php-mqtt/laravel-client paketini de sürdürüyor:

Laravel paketini kurun
composer require php-mqtt/laravel-client
php artisan vendor:publish --provider="PhpMqtt\Client\MqttClientServiceProvider" --tag="config"
Uygulamanızın herhangi bir yerinde
use PhpMqtt\Client\Facades\MQTT;

MQTT::publish('testmqtt/php/laravel/hello', 'Hello from Laravel');

Abone olmak için bir Artisan komutu içinde MQTT::connection() çağırır ve loop(true)'yu orada çalıştırırsınız. Kalıbın tamamı (publish için kuyruklu job'lar, subscribe için denetimli bir komut ve testler) Laravel'de MQTT yazısında.

Sık karşılaşılan hatalar

BelirtiNedenÇözüm
"Establishing a connection to the MQTT broker failed" mesajlı ConnectingToBrokerFailedExceptionYanlış host veya port, firewall, şifresiz portta TLS (veya tersi)Port ile setUseTls() değerini eşleştirin: 1883 şifresiz, 8883 TLS
CONNECT sonrası bağlantı reddediliyorHatalı kimlik bilgileri veya yetkisizKullanıcı adı, parola ve ACL'leri kontrol edin
Worker birkaç saniyede bir bağlantıyı kaybediyorİki süreç aynı client ID'yi paylaşıyorSüreç başına benzersiz, istek başına rastgele ID'ler
QoS 1 mesajı kayıpBetik PUBACK'ten önce bağlantıyı kestidisconnect() öncesinde loop(true, true)
Call to undefined function pcntl_async_signals()pcntl kullanılamıyor (Windows veya web SAPI)Sinyal yönetimini yalnızca Unix'te CLI'da kullanın

Test edin

PHP kodunuzun publish ettiklerini bir terminal subscriber'ı veya online MQTT istemcisi ile izleyin:

PHP topic'lerini izleyin
mosquitto_sub -h broker.connectmq.com -p 1883 -t 'testmqtt/php/#' -v

Ardından subscriber'ınıza, JSON kontrolünün çalıştığını kanıtlamak için geçersiz olanı da dahil olmak üzere mesaj gönderin:

Worker'a test mesajları gönderin
mosquitto_pub -h broker.connectmq.com -p 1883 -t 'testmqtt/php/sensors/room1' -m '{"temp":22.1}' -q 1
mosquitto_pub -h broker.connectmq.com -p 1883 -t 'testmqtt/php/sensors/room1' -m 'not json' -q 1

Son olarak worker'ı kill -9 ile sonlandırın ve retained offline durumunun testmqtt/php/worker/status üzerinde göründüğünü doğrulayın. Daha fazla komut satırı tarifi için mosquitto_pub ve mosquitto_sub örnekleri yazısına bakın.

PHP topic'lerini online istemcide izleyin

PHP'nin süreç modeline saygı gösterdiğinizde php-mqtt/client ile PHP'de MQTT oldukça kolaydır: istekler içinden benzersiz client ID'lerle ve QoS 1 için loop(true, true) ile publish edin, yalnızca kendi kendine yeniden bağlanan denetimli CLI worker'larından abone olun ve canlıya çıkmadan önce TLS ile kimlik bilgilerini etkinleştirin.

Sıkça sorulan sorular

PHP, MQTT topic'lerine abone olabilir mi?

Evet, ama normal bir web isteği içinde değil. Bir subscriber'ın bağlı kalması gerekir; bu yüzden onu $mqtt->loop(true) çağıran, systemd, Supervisor veya Docker tarafından denetlenen uzun ömürlü bir CLI betiği (php subscriber.php) olarak çalıştırın. Web istekleri yalnızca publish etmelidir.

php-mqtt/client MQTT 5'i destekliyor mu?

Bu yazının yazıldığı sırada MQTT 3.1 ve 3.1.1'i uygular; bunları yaygın broker'ların hepsi kabul eder. Reason code'lar veya user property'ler gibi MQTT 5 özelliklerine ihtiyacınız varsa sistemin o kısmını başka bir dilde çözün ya da projenin daha yeni sürümlerine bakın.

PHP'den gönderdiğim QoS 1 mesajı neden hiç ulaşmıyor?

publish(), QoS 1 ve 2 mesajlarını yalnızca kuyruğa ekler; onay loop() içinde işlenir. Publish'ten sonra $mqtt->loop(true, true) çağırın ki istemci bağlantıyı kesmeden önce PUBACK'i beklesin; aksi halde betik daha önce sonlanabilir.