Bir MQTT sohbet uygulaması, publish/subscribe modelini gerçekten öğrenmenin en iyi yollarından biridir: odalar topic'lere, çevrimiçi durumu (presence) retained message'lara karşılık gelir ve çökme algılama Last Will ile bedavaya gelir. Bu eğitimde güvenli WebSocket üzerinden MQTT.js kullanarak broker.connectmq.com adresine bağlanan eksiksiz bir tarayıcı sohbetini tek bir HTML dosyasında oluşturacaksınız. Bu süreçte kendi mesajlarınızın size geri yansımaması için MQTT 5 No Local seçeneğini de kullanacaksınız.
MQTT Sohbet İçin Neden İyi Çalışır?
Sohbet bir mesajlaşma problemine benzer ve MQTT bir mesajlaşma protokolüdür; dolayısıyla uyum doğaldır. Bir sohbet uygulamasındaki her kavramın doğrudan bir MQTT karşılığı vardır:
| Sohbet özelliği | MQTT mekanizması |
|---|---|
| Sohbet odası | Bir topic, örneğin .../room/general/messages |
| Bir odaya katılmak | O topic'e abone olmak |
| Mesaj göndermek | O topic'e yayın yapmak |
| "Kim çevrimiçi" listesi | Bir presence topic'inde kullanıcı başına retained message |
| Kapanan sekmeyi veya kopan ağı algılamak | Broker tarafından yayınlanan Last Will and Testament |
| Kendi mesajınızı iki kez görmemek | MQTT 5 No Local abonelik seçeneği |
MQTT'nin size vermediği şey mesaj geçmişidir. Broker mesajları o anda abone olan kişilere teslim eder; bir veritabanı değildir. Bu bilinçli bir ödünleşimdir ve sonda geçmişin nasıl ekleneceğini tartışacağız.
Bu terimlerden herhangi biri sizin için yeniyse bir MQTT broker'ın ne yaptığına dair genel bakışımız ve MQTT 5 özellikleri rehberi iyi birer başlangıçtır.
MQTT Sohbet Uygulaması İçin Topic Tasarımı
İyi bir topic tasarımı kodu basit tutar. Testinizin public broker'daki diğer kişilerle çakışmaması için her topic rastgele bir önekle başlar ve ardından oda adı gelir:
connectmq/demo/<prefix>/room/<room>/messages chat messages (not retained)
connectmq/demo/<prefix>/room/<room>/presence/<clientId> presence (retained JSON)
Bir client .../messages ve .../presence/+ topic'lerine abone olur. Tek seviyeli wildcard + her kullanıcının presence topic'iyle eşleşir ve presence mesajları retained olduğundan yeni gelen biri odadaki herkesin güncel durumunu hemen alır. Bu, public broker'ın 8 topic seviyesi ve client başına 10 abonelik sınırlarının içinde kalır.
Eksiksiz Sohbet Sayfası
Bunu chat.html olarak kaydedin. İki tarayıcı penceresinde açın (veya dosyayı bir arkadaşınıza gönderin), farklı isimler ve aynı oda adını girin ve sohbete başlayın.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>MQTT Chat</title>
<script src="https://unpkg.com/mqtt@5.10.1/dist/mqtt.min.js"></script>
<style>
body { font-family: system-ui, sans-serif; max-width: 860px; margin: 1.5rem auto; padding: 0 1rem; }
.layout { display: grid; grid-template-columns: 1fr 200px; gap: 1rem; }
#messages { border: 1px solid #ccc; height: 360px; overflow-y: auto; padding: .5rem; }
#users { border: 1px solid #ccc; padding: .5rem; list-style: none; margin: 0; }
.me { color: #0a58ca; }
.meta { color: #777; font-size: .8rem; }
form { display: flex; gap: .5rem; margin-top: .5rem; }
form input[type=text] { flex: 1; }
input, button { font: inherit; padding: .4rem .6rem; }
</style>
</head>
<body>
<h1>MQTT Chat</h1>
<form id="joinForm">
<input type="text" id="name" placeholder="Your name" required maxlength="24">
<input type="text" id="room" value="general" required maxlength="24">
<button>Join</button>
</form>
<div class="layout">
<div>
<div id="messages"></div>
<form id="sendForm">
<input type="text" id="text" placeholder="Type a message" maxlength="500" autocomplete="off">
<button>Send</button>
</form>
</div>
<ul id="users"></ul>
</div>
<script>
// Bunu, sohbeti görmesi gereken herkesin paylaştığı kendi rastgele dizenizle değiştirin.
const PREFIX = "connectmq/demo/chat-k7f3q2";
const URL = "wss://broker.connectmq.com:8084/mqtt";
const clientId = "chat-" + Math.random().toString(16).slice(2, 10);
const messagesEl = document.getElementById("messages");
const usersEl = document.getElementById("users");
const users = new Map();
let client = null, me = null, topics = null;
function addMessage(from, text, ts, mine) {
const div = document.createElement("div");
const who = document.createElement("strong");
who.textContent = from + ": ";
if (mine) who.className = "me";
const body = document.createElement("span");
body.textContent = text; // asla innerHTML değil: payload'lar güvenilmezdir
const meta = document.createElement("span");
meta.className = "meta";
meta.textContent = " " + new Date(ts).toLocaleTimeString();
div.append(who, body, meta);
messagesEl.appendChild(div);
messagesEl.scrollTop = messagesEl.scrollHeight;
}
function renderUsers() {
usersEl.replaceChildren();
for (const [id, u] of users) {
if (u.status !== "online") continue;
const li = document.createElement("li");
li.textContent = u.name + (id === clientId ? " (you)" : "");
usersEl.appendChild(li);
}
}
function presence(status) {
return JSON.stringify({ name: me, status, ts: Date.now() });
}
document.getElementById("joinForm").onsubmit = (e) => {
e.preventDefault();
if (client) return;
me = document.getElementById("name").value.trim();
const room = document.getElementById("room").value.trim().replace(/[#+\/]/g, "");
const base = `${PREFIX}/room/${room}`;
topics = {
messages: `${base}/messages`,
presenceAll: `${base}/presence/+`,
presenceMe: `${base}/presence/${clientId}`
};
client = mqtt.connect(URL, {
clientId,
protocolVersion: 5,
clean: true,
keepalive: 20,
reconnectPeriod: 3000,
will: {
topic: topics.presenceMe,
payload: presence("offline"),
qos: 1,
retain: true
}
// Private broker: username: "YOUR_USERNAME", password: "YOUR_PASSWORD"
});
client.on("connect", () => {
client.subscribe({
[topics.messages]: { qos: 1, nl: true }, // No Local: kendi mesajlarımı atla
[topics.presenceAll]: { qos: 1 }
});
client.publish(topics.presenceMe, presence("online"), { qos: 1, retain: true });
addMessage("system", `Joined room "${room}" as ${me}`, Date.now(), false);
});
client.on("message", (topic, payload) => {
const presencePrefix = topics.presenceAll.slice(0, -1);
if (topic.startsWith(presencePrefix) && payload.length === 0) {
users.delete(topic.split("/").pop()); // retained presence temizlendi
return renderUsers();
}
let data;
try { data = JSON.parse(payload.toString()); } catch { return; }
if (topic === topics.messages) {
addMessage(String(data.from), String(data.text), data.ts, false);
} else if (topic.startsWith(presencePrefix)) {
const id = topic.split("/").pop();
users.set(id, { name: String(data.name), status: data.status });
renderUsers();
}
});
client.on("error", (err) => addMessage("system", "Error: " + err.message, Date.now(), false));
};
document.getElementById("sendForm").onsubmit = (e) => {
e.preventDefault();
const input = document.getElementById("text");
const text = input.value.trim();
if (!client || !client.connected || !text) return;
const msg = { from: me, text, ts: Date.now() };
client.publish(topics.messages, JSON.stringify(msg), { qos: 1 });
addMessage(me, text, msg.ts, true); // hemen yerel olarak göster
input.value = "";
};
window.addEventListener("beforeunload", () => {
if (client && client.connected) {
// Temiz çıkış: geride "offline" bırakmak yerine retained presence'ımı temizle.
client.publish(topics.presenceMe, "", { qos: 0, retain: true });
client.end(true);
}
});
</script>
</body>
</html>
Nasıl Çalışır?
Retained message'larla çevrimiçi durumu
Bir kullanıcı katıldığında sayfa, kendi presence topic'ine retain bayrağıyla {"status":"online"} yayınlar. Broker topic başına son retained mesajı saklar ve her yeni aboneye iletir. Böylece ikinci bir kullanıcı katılıp presence/+ filtresine abone olduğunda broker, zaten çevrimiçi olan her kullanıcı için bir retained mesajı hemen gönderir ve kimsenin kendini yeniden duyurmasına gerek kalmadan kullanıcı listesi dolar.
Last Will ile çökme algılama
Sekmeyi normal şekilde kapatmak, boş bir retained payload yayınlayan beforeunload handler'ını çalıştırır. Boş bir retained mesaj, broker'a o topic için saklanan mesajı silmesini söyler; böylece kullanıcı temiz bir şekilde listeden kaybolur.
Ancak sekmeler çöker, dizüstü bilgisayarlar uykuya geçer ve Wi-Fi kopar. İşte Last Will burada devreye girer. CONNECT sırasında client bir will mesajı kaydeder: retained bir "offline" presence. Broker, keepalive aralığının yaklaşık bir buçuk katı boyunca (burada yaklaşık 30 saniye) client'tan haber alamazsa will mesajını client adına yayınlar. Diğer tüm kullanıcılar bunu alır ve liste güncellenir. Kısa bir keepalive, birkaç ek ping paketi pahasına çevrimiçi durumunu daha doğru hale getirir.
No Local: kendi mesajlarınızın yankısı yok
Ek seçenekler olmadan, abone olduğu bir topic'e yayın yapan client kendi mesajını geri alır. Bir sohbette bu, yazdığınız her satırın iki kez görünmesi ya da göndericiye göre tekilleştirme yapmak zorunda kalmanız anlamına gelir. MQTT 5, MQTT.js'te nl: true olarak sunulan No Local abonelik seçeneğini ekler. Broker bu durumda bir mesajı, onu yayınlayan aynı client bağlantısına teslim etmez.
Sayfa, Send'e bastığınız anda mesajınızı yerel olarak gösterir; böylece arayüz yavaş bir ağda bile anlık hissettirir. No Local, protocolVersion: 5 gerektirir; MQTT 3.1.1'de bu seçenek yoktur ve bunun yerine mesaj handler'ında client ID'ye göre filtreleme yapmanız gerekir.
Güvenli gösterim
Her payload DOM'a textContent ile eklenir. Public bir broker'da önekinizi bilen herkes odanıza yayın yapabilir; bu yüzden her mesajı düşmanca girdi olarak ele alın. Burada innerHTML kullanmak sohbetinizi bir cross-site scripting oyun alanına çevirirdi.
Test Kontrol Listesi
chat.htmldosyasını iki pencerede açın, farklı isimlerle aynı odaya katılın ve iki ismin de her kullanıcı listesinde göründüğünü doğrulayın.- Her pencereden bir mesaj gönderin ve iki tarafta da tam olarak bir kez göründüğünü doğrulayın.
- Bir pencereyi normal şekilde kapatın ve ismin hemen kaybolduğunu doğrulayın.
- Bir makinede ağ bağlantısını kesin ve kullanıcısının yaklaşık 30 saniye sonra çevrimdışı göründüğünü doğrulayın.
- Ham trafiği bir terminalden izleyin:
mosquitto_sub -h broker.connectmq.com -p 1883 -i chat-watch-9c1e -t 'connectmq/demo/chat-k7f3q2/#' -v
Önce retained presence mesajlarını, ardından geldikçe sohbet mesajlarını görürsünüz.
Sınırlamalar ve Sohbeti Genişletme Yolları
Geçmiş. Sonradan katılanlar önceki mesajları görmez. Geçmiş eklemek için mesajları bir veritabanında saklayan küçük bir abone çalıştırın (örneğin MQTT'den SQLite'a eğitimindeki yaklaşımla) ve sayfanın katılırken çağırdığı bir okuma uç noktası sunun.
Hız sınırları. Public broker client başına saniyede 10 mesaja ve mesaj başına 64 KB'a izin verir. Birçok satıra bölünmüş yapıştırılmış metin gibi ani yükler bekliyorsanız sayfaya basit bir gönderim sınırlayıcı ekleyin.
Gizlilik. broker.connectmq.com adresinin public alanı herkes tarafından okunabilir. Bir demonun ötesindeki her şey için private bir ConnectMQ broker oluşturun, üretilen kimlik bilgilerini kullanın ve tarayıcı kodu kullanıcılarına açık olduğundan bu kimlik bilgilerinin nerede durduğunu düşünün. MQTT güvenliği için en iyi uygulamalar yazısı kimlik bilgisi stratejisini ele alır.
Framework'ler. Aynı mantık doğrudan bileşenlere aktarılabilir. React MQTT eğitimi ve Next.js MQTT modelleri rehberi, bir framework içinde client yaşam döngüsünün nasıl yönetileceğini gösterir; minimal tarayıcı MQTT client'ı ise WebSocket bağlantısını daha ayrıntılı açıklar. Diğer diller için MQTT client kütüphaneleri rehberine bakın.
Sohbetinizi Private Bir ConnectMQ Broker'da Barındırın
Sohbetiniz bir demonun ötesine geçtiğinde, barındırılan bir MQTT broker sizi kendi sunucunuzu çalıştırma ve yamalama zahmetinden kurtarır. ConnectMQ, retained message'lar, Last Will ve 8084 portunda güvenli WebSocket ile MQTT 5'i kutudan çıktığı haliyle destekler; portalının canlı topic gezgini de presence ve sohbet topic'lerini değiştikçe gösterir. Ücretsiz public MQTT broker ile başlayın, ardından kredi kartı gerektirmeyen Serverless planında kendi broker'ınızı ücretsiz oluşturun.
Sık Sorulan Sorular
MQTT bir sohbet uygulaması için iyi bir seçim mi?
Gerçek zamanlı teslimat, çevrimiçi durumu ve hafif client'lar için evet; özellikle sohbet IoT verileriyle yan yana yaşıyorsa. MQTT geçmişi saklamaz; bu yüzden kullanıcıların geriye kaydırması gerekiyorsa bir veritabanına yazan bir abone eklersiniz.
No Local seçeneği ne işe yarar?
No Local, broker'ın bir mesajı onu yayınlayan bağlantıya geri göndermesini engelleyen bir MQTT 5 abonelik seçeneğidir. MQTT.js'te abonelik seçeneklerinde nl: true ile etkinleştirirsiniz.
Sohbet, bir kullanıcının beklenmedik şekilde çevrimdışı olduğunu nasıl anlar?
Client bağlanırken bir Last Will mesajı kaydeder. Broker keepalive ping'lerini alamazsa bu will mesajını, yani retained bir "offline" presence'ı, abone olan herkese yayınlar.
Bu MQTT sohbet uygulamasını MQTT 3.1.1 ile kullanabilir miyim?
Büyük ölçüde evet. Retained presence ve Last Will aynı şekilde çalışır, ancak No Local yalnızca MQTT 5'te vardır; bu yüzden kendi mesajlarınızı mesaj handler'ında gönderici ID'sine göre filtrelemeniz gerekir.



