Broker testleri

MQTT 5 Özelliklerini Test Etme: User Properties, Expiry, Topic Alias

MQTT.js ve paho-mqtt ile MQTT 5 özelliklerinin uygulamalı testleri: user properties, mesaj ve oturum süresi dolumu, topic alias, request/response ve no-local.

Güncellendi · 10 dk okuma

MQTT 5 Özelliklerini Test Etme: User Properties, Expiry, Topic Alias

MQTT 5, uygulamaların davranışını değiştiren özellikler getirdi: user properties içinde meta veri, süresi dolan mesajlar, topic alias'lar sayesinde daha kısa paketler, ömrü olan oturumlar. Her biri hem istemci kütüphanesinin hem de broker'ın onu doğru uygulamasına bağlıdır; bu yüzden onlara güvenmeden önce MQTT 5 özelliklerini uçtan uca test edin.

Bu yazı MQTT.js 5 (Node.js) ve paho-mqtt 2.x (Python) ile küçük, çalıştırılabilir testler sunuyor. Her test publish eder, gelenleri gözler ve assertion ile doğrular; yani dizüstü bilgisayarınızda olduğu gibi CI'da da çalışabilir.

Neyi test ediyoruz, "geçti" ne demek

ÖzellikTestGeçme koşulu
User propertiesAnahtar/değer çiftleriyle publishSubscriber aynı çiftleri alır
Content type, correlation dataİkisi de ayarlı olarak publishSubscriber ikisini de değişmeden alır
Message expiry (retained)3 sn expiry'li retained mesaj, 5 sn sonra subscribeHiçbir şey teslim edilmez
Message expiry (kuyruk)Çevrimdışı bir oturum için mesajları kuyruğa alSüresi dolmayan mesaj azalmış bir aralıkla gelir; süresi dolan atılır
Session expiryclean_start=False ile yeniden bağlansession present true olur ve kuyruktaki mesajlar gelir
Topic aliasBir alias kaydet, ardından boş topic ile publish etSubscriber tam topic adını görür
No localAynı bağlantıda nl ile subscribe ve publishİstemci kendi mesajını almaz

Önce broker'ın MQTT 5 bağlantılarını kabul ettiğini doğrulayın ve CONNACK sınırlarını (topic alias maximum, maximum QoS) broker özellik yoklaması ile kontrol edin. Broker TopicAliasMaximum 0 duyurduğu için başarısız olan bir test bir hata değil, bir yapılandırma bulgusudur.

Hazırlık

İki kütüphaneyi de kurun
npm install mqtt@5
pip install "paho-mqtt>=2.0"

Tüm örnekler broker.connectmq.com:1883 ve testmqtt/mqtt5/ altında rastgele bir topic öneki kullanır; böylece paralel çalışmalar birbirinin mesajlarını görmez. Broker'ınız gerektiriyorsa username ve password ($MQTT_USER / $MQTT_PASS) ekleyin.

MQTT.js: özellikler, expiry, alias ve no-local

Aşağıdaki script dört testi sırayla çalıştırır ve ilk hatada sıfırdan farklı bir çıkış koduyla sonlanır. nextMessage tek bir mesajı bekler ya da zaman aşımına uğrar; bu sayede "hiçbir şey gelmemeli" kontrolü "bu gelmeli" kadar kolaylaşır.

mqtt5.test.mjs
import mqtt from 'mqtt';
import assert from 'node:assert/strict';
import { randomUUID } from 'node:crypto';

const URL = 'mqtt://broker.connectmq.com:1883';
const opts = { protocolVersion: 5, username: process.env.MQTT_USER, password: process.env.MQTT_PASS };
const base = `testmqtt/mqtt5/${randomUUID().slice(0, 8)}`;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

const nextMessage = (client, timeoutMs = 5000) =>
  new Promise((resolve, reject) => {
    const timer = setTimeout(() => reject(new Error('timeout')), timeoutMs);
    client.once('message', (topic, payload, packet) => {
      clearTimeout(timer);
      resolve({ topic, payload, packet });
    });
  });

const sub = await mqtt.connectAsync(URL, opts);
const pub = await mqtt.connectAsync(URL, opts);

// 1. User properties, content type, correlation data
await sub.subscribeAsync(`${base}/props`, { qos: 1 });
let incoming = nextMessage(sub);
await pub.publishAsync(`${base}/props`, '{"temp":21.5}', {
  qos: 1,
  properties: {
    userProperties: { source: 'ci', 'trace-id': 'abc123' },
    contentType: 'application/json',
    responseTopic: `${base}/reply`,
    correlationData: Buffer.from('req-42'),
  },
});
const { packet } = await incoming;
assert.equal(packet.properties.userProperties.source, 'ci');
assert.equal(packet.properties.userProperties['trace-id'], 'abc123');
assert.equal(packet.properties.contentType, 'application/json');
assert.equal(packet.properties.correlationData.toString(), 'req-42');
console.log('PASS user properties / content type / correlation data');

// 2. Retained message expiry
await pub.publishAsync(`${base}/expiry`, 'short-lived', {
  qos: 1, retain: true, properties: { messageExpiryInterval: 3 },
});
await sleep(5000);
const late = await mqtt.connectAsync(URL, opts);
incoming = nextMessage(late, 3000);
await late.subscribeAsync(`${base}/expiry`, { qos: 1 });
await assert.rejects(incoming, /timeout/, 'expired retained message was delivered');
console.log('PASS retained message expired');

// 3. Topic alias: register alias 1, then publish with an empty topic
const aliasClient = mqtt.connect(URL, opts);
const connack = await new Promise((resolve, reject) => {
  aliasClient.once('connect', resolve);
  aliasClient.once('error', reject);
});
const aliasMax = connack.properties?.topicAliasMaximum ?? 0;
if (aliasMax === 0) {
  console.log('SKIP topic alias: broker advertises TopicAliasMaximum 0');
} else {
  const long = `${base}/alias/site-01/building-a/floor-3/sensor-temperature`;
  await sub.subscribeAsync(long, { qos: 1 });
  incoming = nextMessage(sub);
  await aliasClient.publishAsync(long, 'first', { qos: 1, properties: { topicAlias: 1 } });
  assert.equal((await incoming).topic, long);
  incoming = nextMessage(sub);
  await aliasClient.publishAsync('', 'second', { qos: 1, properties: { topicAlias: 1 } });
  assert.equal((await incoming).topic, long, 'broker did not resolve the alias');
  console.log(`PASS topic alias (broker allows ${aliasMax})`);
}

// 4. No local: a client must not receive its own publish
await pub.subscribeAsync(`${base}/nolocal`, { qos: 1, nl: true });
incoming = nextMessage(pub, 2000);
await pub.publishAsync(`${base}/nolocal`, 'echo?', { qos: 1 });
await assert.rejects(incoming, /timeout/, 'received own message despite no-local');
console.log('PASS no local');

await Promise.all([sub, pub, late, aliasClient].map((c) => c.endAsync()));

node mqtt5.test.mjs ile çalıştırın. Bilmeye değer birkaç ayrıntı:

  • User properties, MQTT.js'te düz bir nesnedir. Bir anahtar pakette birden çok kez geçiyorsa değeri bir diziye dönüşür — MQTT 5 yinelenen anahtarlara izin verir ve sıralarının korunması gerekir.
  • Expiry testinde sıra önemlidir. Mesaj dinleyicisini subscribe olmadan önce ekleyin; çünkü retained bir mesaj subscribeAsync tamamlanmadan gelebilir.
  • Aralık dışı topic alias. MQTT.js, broker'ın TopicAliasMaximum değerinin üzerindeki bir alias'ı göndermeyi reddeder ve bir hata yayar. Yine de böyle bir alias alan broker 0x94 Topic Alias invalid ile bağlantıyı keser. Ayrıntılar topic alias testinde.
  • No local bir subscription seçeneğidir ve abonelik başına ayarlanır. Diğer seçenekler (retain as published, retain handling) MQTT subscription seçenekleri yazısında açıklanıyor.

paho-mqtt: session expiry ve kuyruktaki mesajların süresi

En değerli MQTT 5 testi aynı zamanda en çok atlanan testtir: bir cihaz çevrimdışıyken mesajlara ne olur? Script kalıcı bir oturum oluşturur, bağlantıyı keser, biri 60 sn diğeri 2 sn expiry'li iki mesaj publish eder, bekler ve yeniden bağlanır.

session_expiry_test.py
import time, uuid
import paho.mqtt.client as mqtt
from paho.mqtt.packettypes import PacketTypes
from paho.mqtt.properties import Properties

HOST, PORT = "broker.connectmq.com", 1883
TOPIC = f"testmqtt/mqtt5/{uuid.uuid4().hex[:8]}/queued"
CLIENT_ID = f"mqtt5-session-{uuid.uuid4().hex[:8]}"
received = []

def on_connect(client, userdata, flags, reason_code, properties):
    print("connected:", reason_code, "| session present:", flags.session_present)

def on_message(client, userdata, msg):
    expiry = getattr(msg.properties, "MessageExpiryInterval", None)
    user = getattr(msg.properties, "UserProperty", None)
    print(f"got {msg.payload.decode()!r}  expiry left: {expiry}  user properties: {user}")
    received.append(msg.payload.decode())

def session_client():
    c = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=CLIENT_ID,
                    protocol=mqtt.MQTTv5)
    c.on_connect, c.on_message = on_connect, on_message
    props = Properties(PacketTypes.CONNECT)
    props.SessionExpiryInterval = 120   # keep the session 2 minutes after disconnect
    c.connect(HOST, PORT, clean_start=False, properties=props)
    c.loop_start()
    time.sleep(1)
    return c

# 1. Create the session and subscription, then go offline
s = session_client()
s.subscribe(TOPIC, qos=1)
time.sleep(1)
s.disconnect(); s.loop_stop()

# 2. Publish while the subscriber is offline
pub = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, protocol=mqtt.MQTTv5)
pub.connect(HOST, PORT); pub.loop_start()
for i, expiry in enumerate((60, 2)):
    props = Properties(PacketTypes.PUBLISH)
    props.MessageExpiryInterval = expiry
    props.UserProperty = ("test", f"msg-{i}")
    pub.publish(TOPIC, f"msg-{i}", qos=1, properties=props).wait_for_publish()
pub.disconnect(); pub.loop_stop()

time.sleep(5)  # longer than the 2 s expiry

# 3. Reconnect: msg-0 arrives with less than 60 s left, msg-1 has expired
s = session_client()
time.sleep(3)
s.disconnect(); s.loop_stop()
assert received == ["msg-0"], received
print("PASS session kept, unexpired message delivered, expired message dropped")

Beklenen çıktı: ikinci bağlantı session present: True yazdırır ve msg-0 yaklaşık 54–55 saniyelik bir expiry ile gelir. Broker orijinal değeri değil, kalan ömrü iletmelidir — 60 görüyorsanız broker değeri azaltmıyordur ve sonraki tüketiciler mesajın ne kadar eski olduğunu anlayamaz. session present False ise broker oturumu saklamamıştır: broker'ın maksimum session expiry ayarını ve client ID'nin değişip değişmediğini kontrol edin. Session expiry testi, bağlantı kesilirken expiry gibi uç durumları daha ayrıntılı ele alıyor.

Response topic ve correlation data ile request/response

MQTT 5 request/response'u standartlaştırır: isteği yapan bir response topic ve correlation data belirler; yanıtlayan, cevabını o topic'e publish eder ve correlation data'yı geri kopyalar. Yukarıdaki 1. test, broker'ın bu iki özelliği ilettiğini zaten doğrular. Gerçek bir yanıtlayıcıyı test etmek için response topic'e subscribe olun, bir istek gönderin ve yanıtın zaman aşımı süreniz içinde aynı correlation data'yı taşıdığını doğrulayın. Kalıp ve tam örnek MQTT request/response testinde.

Karma MQTT 3.1.1 ve MQTT 5 filolarını test etme

Çoğu filo yıllarca karma kalır. Özellik testini, subscriber 3.1.1 ile bağlıyken bir kez daha çalıştırın (MQTT.js'te protocolVersion: 4, paho'da protocol=mqtt.MQTTv311):

  • Mesaj yine gelmelidir.
  • packet.properties yoktur: 3.1.1 istemciler user properties, content type veya correlation data'yı asla görmez.
  • Message expiry çalışmaya devam eder, çünkü onu broker uygular.

Backend'iniz yönlendirme veya izleme (tracing) için user properties'e güveniyorsa, bu çalışma hangi cihazların payload'a geri dönmek zorunda kalacağını gösterir. Farkların tam listesi için MQTT 5 ve 3.1.1 karşılaştırması yazısına bakın.

MQTT 5 testlerini tarayıcıda izleyin

MQTT 5 özellikleri, publisher'ınız, broker ve subscriber'ınız arasındaki en zayıf uygulama kadar güvenilirdir. Birkaç yüz satırlık test — özelliklerin gidiş-dönüşü, expiry, alias'lar, oturumlar — farkları erkenden yakalar. Testleri CI'da tutun ve her broker veya kütüphane güncellemesinden sonra yeniden çalıştırın.

Sıkça sorulan sorular

Subscriber MQTT 3.1.1 kullanıyorsa MQTT 5 özellikleri çalışır mı?

Mesaj teslim edilir, ancak 3.1.1 bir subscriber özellikleri alamaz: user properties, content type, response topic ve correlation data o bağlantı için atılır. Message expiry ise broker tarafında yine uygulanır.

Retained mesajımın süresi neden dolmuyor?

MQTT 5 ile publish ettiğinizi (3.1.1 bağlantılarında özellikler yok sayılır), expiry interval'ın CONNECT'te değil PUBLISH'te ayarlandığını ve broker'ınızın retained mesajlar için süre dolumunu uyguladığını kontrol edin. Kısa bir aralık ve yeni bir subscriber ile test edin.

Topic alias'ları kendim mi yönetmeliyim?

Şart değil. MQTT.js alias'ları otomatik atayıp yeniden kullanabilir (autoAssignTopicAlias, autoUseTopicAlias) ve birçok kütüphane gelen alias'ları sizin yerinize çözer. Yalnızca bant genişliği üzerinde tam kontrole ihtiyacınız olduğunda elle yönetin.