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
| Özellik | Test | Geçme koşulu |
|---|---|---|
| User properties | Anahtar/değer çiftleriyle publish | Subscriber aynı çiftleri alır |
| Content type, correlation data | İkisi de ayarlı olarak publish | Subscriber ikisini de değişmeden alır |
| Message expiry (retained) | 3 sn expiry'li retained mesaj, 5 sn sonra subscribe | Hiçbir şey teslim edilmez |
| Message expiry (kuyruk) | Çevrimdışı bir oturum için mesajları kuyruğa al | Süresi dolmayan mesaj azalmış bir aralıkla gelir; süresi dolan atılır |
| Session expiry | clean_start=False ile yeniden bağlan | session present true olur ve kuyruktaki mesajlar gelir |
| Topic alias | Bir alias kaydet, ardından boş topic ile publish et | Subscriber tam topic adını görür |
| No local | Aynı 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
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.
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
subscribeAsynctamamlanmadan gelebilir. - Aralık dışı topic alias. MQTT.js, broker'ın
TopicAliasMaximumdeğerinin üzerindeki bir alias'ı göndermeyi reddeder ve bir hata yayar. Yine de böyle bir alias alan broker0x94 Topic Alias invalidile 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.
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.propertiesyoktur: 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.


