Flask'ın kendi event loop'u yoktur ve her isteği bir thread'de işler; bu yüzden Flask'ta MQTT, web sunucusunun yanında bir MQTT istemcisi çalıştırmak demektir: arka planda bir ağ thread'i mesajları alır, istek handler'ları da aynı istemci üzerinden publish eder. Bunu flask-mqtt eklentisiyle ya da doğrudan paho-mqtt 2.x ile yapabilirsiniz. İkisi de çalışır ve ikisi de aynı thread ve çoklu süreç tuzaklarını paylaşır — bu rehberin asıl konusu da bunlar.
Her iki seçeneği kuracak, "her mesaj iki kez geliyor" sorununu çözecek ve MQTT verilerini Server-Sent Events ile tarayıcıya aktaran küçük bir canlı dashboard ile bitireceğiz.
Seçenek 1: flask-mqtt
flask-mqtt, paho-mqtt'yi sarar ve ayarlarını app.config'den okur:
pip install flask flask-mqttimport os
from flask import Flask
from flask_mqtt import Mqtt
app = Flask(__name__)
app.config["MQTT_BROKER_URL"] = "broker.connectmq.com"
app.config["MQTT_BROKER_PORT"] = 1883
app.config["MQTT_USERNAME"] = os.getenv("MQTT_USER", "")
app.config["MQTT_PASSWORD"] = os.getenv("MQTT_PASS", "")
app.config["MQTT_KEEPALIVE"] = 30
app.config["MQTT_TLS_ENABLED"] = False
mqtt = Mqtt(app)
@mqtt.on_connect()
def handle_connect(client, userdata, flags, rc):
mqtt.subscribe("testmqtt/flask/#")
@mqtt.on_message()
def handle_message(client, userdata, message):
print(message.topic, message.payload.decode())
@app.post("/ping")
def ping():
mqtt.publish("testmqtt/flask/ping", "pong")
return {"ok": True}Kurulumu hızlıdır, ama iki şeyi aklınızda tutun. Eklentinin bakımı zayıftır ve eski sürümleri paho-mqtt 1.x callback API'sine göre yazılmıştır — paho-mqtt 2.0 kurulduktan sonra callback imzası hataları alırsanız daha yeni bir flask-mqtt sürümü arayın ya da paho-mqtt<2 ile sabitleyin. Ayrıca Mqtt(app) çalıştığı anda bağlanır; yani uygulamayı import eden her süreç bir bağlantı açar (aşağıda ayrıntısı var).
Seçenek 2: doğrudan paho-mqtt 2.x
paho'yu doğrudan kullanmak birkaç satır daha ister; karşılığında güncel API'yi, otomatik yeniden bağlanmayı ve tam kontrolü alırsınız. İstemciyi kendi modülüne koyun:
import os
import queue
import threading
import uuid
import paho.mqtt.client as mqtt
TELEMETRY = "testmqtt/flask/+/telemetry"
class MqttService:
def __init__(self, host="broker.connectmq.com", port=1883):
self.host, self.port = host, port
self.latest: dict[str, str] = {}
self.listeners: list[queue.Queue] = []
self.lock = threading.Lock()
self.client = mqtt.Client(
mqtt.CallbackAPIVersion.VERSION2,
client_id=f"testmqtt-flask-{uuid.uuid4().hex[:8]}", # süreç başına benzersiz
)
if os.getenv("MQTT_USER"):
self.client.username_pw_set(os.environ["MQTT_USER"], os.getenv("MQTT_PASS"))
self.client.reconnect_delay_set(min_delay=1, max_delay=30)
self.client.on_connect = self._on_connect
self.client.on_message = self._on_message
def start(self):
self.client.connect_async(self.host, self.port, keepalive=30)
self.client.loop_start() # ağ thread'i, kendiliğinden yeniden bağlanır
def _on_connect(self, client, userdata, flags, reason_code, properties):
if reason_code.is_failure:
print("MQTT connect failed:", reason_code)
return
client.subscribe(TELEMETRY, qos=1) # her yeniden bağlanmadan sonra tekrar çalışır
def _on_message(self, client, userdata, msg):
item = {"topic": msg.topic, "payload": msg.payload.decode(errors="replace")}
with self.lock:
self.latest[msg.topic] = item["payload"]
listeners = list(self.listeners)
for q in listeners:
try:
q.put_nowait(item)
except queue.Full:
pass # yavaş tarayıcı: at, ağ thread'ini asla bloklama
def join(self) -> queue.Queue:
q: queue.Queue = queue.Queue(maxsize=100)
with self.lock:
self.listeners.append(q)
return q
def leave(self, q: queue.Queue):
with self.lock:
self.listeners.remove(q)
def publish(self, topic: str, payload: str, qos: int = 1) -> bool:
if not self.client.is_connected():
return False
return self.client.publish(topic, payload, qos=qos).rc == mqtt.MQTT_ERR_SUCCESSconnect_async() ile loop_start(), broker erişilemez olsa bile Flask'ın başlamasını sağlar; paho, reconnect_delay_set() ile belirlenen backoff ile arka planda denemeye devam eder. on_connect içinde abone olmak, aboneliği her yeniden bağlanmadan sonra geri yükler. paho API'sinin temelleri için Python'da MQTT yazısına bakın.
Thread kuralları
Bu kurulumda üç thread MQTT verisine dokunur; birkaç kural her şeyi güvende tutar:
| Kural | Neden |
|---|---|
on_message'ı kısa tutun | paho'nun tek ağ thread'inde çalışır. Yavaş bir handler tüm mesajları geciktirir ve keep-alive ping'lerini geciktirebilir. |
| Paylaşılan durumu bir lock ile koruyun | İstek thread'leri latest'i okurken ağ thread'i ona yazar. |
İstek handler'larından publish() sorun değil | paho'nun publish()'i thread-safe'tir; önce is_connected()'ı kontrol edin, değilse 503 dönün. |
| Callback'lerde app context yok | current_app, g ve Flask-SQLAlchemy oturumları callback içinde with app.app_context(): gerektirir. |
wait_for_publish()'i asla timeout'suz beklemeyin | Bağlantısı kopmuş bir broker istek thread'ini sonsuza dek askıda bırakır. |
Ağır işler — veritabanı yazımları, diğer servislere HTTP çağrıları — on_message'a değil, bir worker thread'in ya da Celery veya RQ gibi bir görev kuyruğunun tükettiği bir kuyruğa aittir.
"Her mesaj iki kez" sorunu
Flask'ta en yaygın MQTT hatası: mesajlar iki kez işlenir ya da log, istemcinin bir döngü içinde bağlanıp koptuğunu gösterir. Nedenler ve çözümler:
- Debug reloader.
app.run(debug=True)modülünüzü hem izleyici süreçte hem de isteklere hizmet eden süreçte çalıştırır; iki MQTT istemciniz olur.use_reloader=Falsekullanın ya da MQTT'yi yalnızca hizmet veren süreçte başlatın:if os.environ.get("WERKZEUG_RUN_MAIN") == "true" or not app.debug:. - Birden fazla Gunicorn worker'ı.
-w 4dört istemci demektir. Dashboard için sorun değil (her worker'ın kendi tarayıcıları için tüm mesajlara ihtiyacı var), ama yalnızca bir kez olması gereken her şey — bir veritabanı insert'i, bir alarm — shared subscription kullanmalıdır; böylece broker her mesajı tek bir worker'a verir. Bkz. shared subscription'lar. - Sabit client ID. Tüm bu süreçler aynı client ID'yi kullanırsa broker her yeni bağlantıda eskisini düşürür ve süreçler birbirini sonsuza dek atar. Rastgele sonekler bunu önler — bkz. yinelenen client ID.
gunicorn --preload. Uygulama fork'tan önce master'da import edilir, ama paho thread'ifork()sonrasında hayatta kalmaz; worker'lar ağ döngüsü olmayan bir istemci nesnesiyle kalır. Preload kullanmayın ya da istemciyi birpost_forkhook'unda başlatın.
Basit bir canlı dashboard
Şimdi servisi Flask'a bağlayın: bir JSON anlık görüntüsü, bir komut endpoint'i, bir SSE akışı ve küçük bir HTML sayfası.
import json
import queue
import re
from flask import Flask, Response, abort, jsonify, request
from mqtt_service import MqttService
app = Flask(__name__)
mqtt_service = MqttService()
mqtt_service.start()
DEVICE_ID = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
PAGE = """<!doctype html><title>MQTT dashboard</title>
<h1>Canlı telemetri</h1><table id="t"></table>
<script>
const rows = {};
new EventSource("/stream").onmessage = (e) => {
const { topic, payload } = JSON.parse(e.data);
let row = rows[topic];
if (!row) {
row = rows[topic] = document.getElementById("t").insertRow();
row.insertCell().textContent = topic;
row.insertCell();
}
row.cells[1].textContent = payload;
};
</script>"""
@app.get("/")
def index():
return PAGE
@app.get("/api/latest")
def latest():
with mqtt_service.lock:
return jsonify(mqtt_service.latest)
@app.post("/api/devices/<device_id>/command")
def command(device_id):
if not DEVICE_ID.match(device_id):
abort(400) # doğrulanmamış girdiyi asla topic'e koymayın
body = json.dumps(request.get_json(force=True))
if not mqtt_service.publish(f"testmqtt/flask/{device_id}/command", body):
abort(503)
return jsonify(queued=True), 202
@app.get("/stream")
def stream():
q = mqtt_service.join()
def events():
try:
while True:
try:
yield f"data: {json.dumps(q.get(timeout=15))}\n\n"
except queue.Empty:
yield ": keep-alive\n\n" # proxy'lerin akışı kapatmasını önler
finally:
mqtt_service.leave(q)
return Response(events(), mimetype="text/event-stream", headers={"Cache-Control": "no-cache"})
if __name__ == "__main__":
app.run(debug=True, use_reloader=False, threaded=True)Açık her dashboard bir SSE bağlantısı tutar ve thread'li sunucuda bu, tarayıcı başına bir thread demektir — birkaç izleyicili dahili bir dashboard için yeterli. Üretimde gunicorn -w 1 --threads 16 app:app çalıştırın (ya da yukarıdaki kurallara uyarak daha fazla worker). Çok sayıda eşzamanlı izleyici için asenkron bir framework daha uygundur; bkz. FastAPI ile MQTT ya da tarayıcıların canlı dashboard eğitiminde gösterildiği gibi doğrudan broker'a bağlanmasına izin verin.
Test edin
python app.pykomutunu çalıştırın vehttp://localhost:5000adresini açın.- Online MQTT istemcisinde
broker.connectmq.com'a bağlanın vetestmqtt/flask/greenhouse/telemetrytopic'ine{"temp": 23.1}publish edin. Dashboard'da hemen bir satır görünmelidir. - Online istemcide
testmqtt/flask/#'e abone olun ve bir komut gönderin:
curl -X POST localhost:5000/api/devices/greenhouse/command \
-H 'content-type: application/json' -d '{"fan": "on"}'curl localhost:5000/api/latestkomutunun topic başına son değeri döndürdüğünü kontrol edin.- Broker'ı durdurun ya da ağı kesin: komut endpoint'i
503dönmeli, paho yeniden bağlandığında dashboard kendiliğinden devam etmelidir.
Flask dashboard'unuza test telemetrisi gönderin
Projeniz Flask yerine Django kullanıyorsa aynı kalıplar, uzun ömürlü subscriber olarak bir management command ile geçerlidir — bkz. Django ile MQTT.
Flask'ta MQTT, onu gerçekte olduğu şey olarak ele aldığınızda iyi çalışır: arka plan thread'inde, süreç başına bir tane uzun ömürlü istemci. Onu bir kez başlatın, callback'leri kısa ve thread-safe tutun, her sürece benzersiz bir client ID verin ve daha fazla worker eklemeden önce tüm döngüyü — cihazdan tarayıcıya ve tarayıcıdan cihaza — test edin.
Sıkça sorulan sorular
Flask uygulamam neden her MQTT mesajını iki kez alıyor?
Genellikle iki MQTT istemcisi çalışıyordur: debug reloader kodunuzu iki süreçte çalıştırır ya da Gunicorn birden fazla worker başlatmıştır. Reloader'ı kapatın (use_reloader=False) veya MQTT'yi yalnızca isteklere hizmet eden süreçte başlatın ve her sürece benzersiz bir client ID verin.
flask-mqtt mi kullanmalıyım, doğrudan paho-mqtt mi?
flask-mqtt, ayarlarını app.config'den okuyan ve decorator'lar sunan ince bir sarmalayıcıdır. Küçük uygulamalar için pratiktir ama bakımı zayıftır; paho-mqtt 2.x ile uyumluluğunu kontrol edin. Doğrudan paho-mqtt kullanmak tam kontrol ve güncel callback API'sini sağlar.
MQTT callback'i içinde Flask-SQLAlchemy kullanabilir miyim?
Evet, ancak MQTT callback'leri herhangi bir isteğin dışında, paho'nun ağ thread'inde çalışır; dolayısıyla uygulama bağlamı (app context) yoktur. Veritabanı kodunu with app.app_context(): içine alın ve işi kısa tutun ya da bir kuyruğa devredin.


