পাঠ ৪৭ · ৫৭-এর মধ্যে · মডিউল ১১

MQTT QoS ও বাস্তব-জীবনের প্যাটার্ন

MQTT Quality of Service & real-world patterns
১০ মিনিট পড়া মধ্যম · Intermediate Python সিমুলেশনসহ সম্পূর্ণ বাংলায়

এই পাঠে যা শিখবেন

  • QoS 0/1/2 ডেলিভারি সিমেন্টিক্সের মধ্যে সঠিক পার্থক্য এবং প্রতিটির বাস্তব ট্রেড-অফ
  • QoS 1-এর PUBLISH → PUBACK এক্সচেঞ্জ ও একটি হারিয়ে যাওয়া ACK-এর পর জেনুইন রিট্রান্সমিশন
  • রিটেইনড মেসেজ কীভাবে কাজ করে
  • লাস্ট-উইল-অ্যান্ড-টেস্টামেন্ট (LWT) কীভাবে একটি ডিভাইসের অপ্রত্যাশিত ডিসকানেক্ট শনাক্ত করতে ব্যবহৃত হয়

১ · তিনটি QoS লেভেল

L46-এ আমরা দেখেছি ব্রোকার কীভাবে টপিক ম্যাচ করে বার্তা রাউট করে — কিন্তু "রাউট করা" আর "নিশ্চিতভাবে পৌঁছানো" এক জিনিস নয়। নেটওয়ার্ক যেকোনো সময় একটি প্যাকেট হারাতে পারে (বিশেষত IoT-এর দুর্বল ওয়্যারলেস লিংকে — M10-এ দেখা "লসি লিংক" সমস্যা)। MQTT তাই তিনটি QoSQuality of Serviceএকটি বার্তা ঠিক কতটা নিশ্চিতভাবে ডেলিভার হবে তার গ্যারান্টি লেভেল — MQTT-তে ০, ১ ও ২ নামে তিনটি লেভেল আছে। লেভেল দেয় — প্রতিটি নির্ভরযোগ্যতা ও ওভারহেডের মধ্যে একটি ভিন্ন ট্রেড-অফ:

  • QoS 0 — At most once: পাবলিশার একবার পাঠায়, কোনো ACK ট্র্যাক করে না। সবচেয়ে কম ওভারহেড, কিন্তু নেটওয়ার্ক লস হলে বার্তা চিরতরে হারিয়ে যেতে পারে। ঘন ঘন পাঠানো নন-ক্রিটিক্যাল সেন্সর রিডিং-এর জন্য উপযুক্ত (একটি রিডিং মিস হলেও পরেরটি তো আসছেই)।
  • QoS 1 — At least once: পাবলিশার একটি PUBACK-এর অপেক্ষা করে; না পেলে একই প্যাকেট-আইডি দিয়ে পুনরায় পাঠায় (একটি DUP ফ্ল্যাগসহ)। এতে বার্তা হারায় না, কিন্তু ACK নিজেই হারিয়ে গেলে সাবস্ক্রাইবার একই বার্তা দুইবার পেতে পারে — তাই "কমপক্ষে একবার"।
  • QoS 2 — Exactly once: একটি ৪-ধাপের হ্যান্ডশেক (PUBLISH → PUBREC → PUBREL → PUBCOMP) ব্যবহার করে ডুপ্লিকেট প্রতিরোধ করে — কিন্তু সবচেয়ে বেশি নেটওয়ার্ক রাউন্ড-ট্রিপ ও ওভারহেড লাগে, তাই ব্যাটারি-চালিত সেন্সর নোডে কম ব্যবহৃত হয়।
ক্লায়েন্ট (Publisher) MQTT ব্রোকার ① PUBLISH(id=501, DUP=False) ✗ PUBACK(id=501) হারিয়ে গেছে (সিমুলেটেড) ② PUBLISH(id=501, DUP=True) — পুনরায় পাঠানো ③ PUBACK(id=501) ✓ — সফল
QoS 1-এ প্রথম PUBACK হারিয়ে গেলে ক্লায়েন্ট একই প্যাকেট-আইডি দিয়ে DUP ফ্ল্যাগসহ পুনরায় পাঠায় — সাবস্ক্রাইবার বার্তাটি ততক্ষণে ইতিমধ্যেই একবার পেয়ে থাকতে পারে, তাই ডুপ্লিকেট সম্ভব।

২ · ব্রোকার সম্প্রসারণ ও QoS সিমুলেশন

নিচে L46-এর MqttBroker ক্লাসটি পুনরায় তৈরি করে, তার ওপর QoS-নির্দিষ্ট মেসেজ-এক্সচেঞ্জ যোগ করা হয়েছে। প্রতিটি ধাপ একটি সত্যিকারের লগ এন্ট্রি হিসেবে প্রিন্ট হয় — বিশেষভাবে QoS 1-এ একটি হারিয়ে যাওয়া PUBACK সিমুলেট করে দেখানো হয়েছে কীভাবে জেনুইন রিট্রান্সমিশন কাউন্ট গণনা হয়।

Python
class MqttBroker:
    """L46-এর ব্রোকার -- এই কোড সেলে পুনরায় তৈরি করা হলো যাতে সেল স্বয়ংসম্পূর্ণ থাকে।"""

    def __init__(self):
        self.subscriptions = []
        self._next_packet_id = 0

    def subscribe(self, client_id, topic_filter):
        self.subscriptions.append((client_id, topic_filter))

    @staticmethod
    def topic_matches(topic_filter, topic):
        filter_levels = topic_filter.split("/")
        topic_levels = topic.split("/")
        i = 0
        while i < len(filter_levels):
            level = filter_levels[i]
            if level == "#":
                return True
            if i >= len(topic_levels):
                return False
            if level == "+" or level == topic_levels[i]:
                i += 1
                continue
            return False
        return i == len(topic_levels)

    def route(self, topic):
        return [cid for cid, tf in self.subscriptions if self.topic_matches(tf, topic)]

    def new_packet_id(self):
        self._next_packet_id += 1
        return self._next_packet_id


def publish_qos0(broker, topic, payload):
    log = [f"[QoS 0] ক্লায়েন্ট -> ব্রোকার: PUBLISH '{topic}' (কোনো ACK ট্র্যাকিং নেই)"]
    recipients = broker.route(topic)
    log.append(f"  ব্রোকার তাৎক্ষণিক ডেলিভার করলো -> {recipients}")
    return log


def publish_qos1(broker, topic, payload, simulate_lost_ack=False):
    pid = broker.new_packet_id()
    log = []
    deliveries = []
    attempt = 1
    acked = False
    while not acked:
        dup = attempt > 1
        log.append(f"[QoS 1] ক্লায়েন্ট -> ব্রোকার: PUBLISH(id={pid}, DUP={dup}) চেষ্টা #{attempt}")
        recipients = broker.route(topic)
        deliveries.append(recipients)
        log.append(f"  ব্রোকার সাবস্ক্রাইবারদের ডেলিভার করলো -> {recipients}")
        if simulate_lost_ack and attempt == 1:
            log.append(f"  ব্রোকার -> ক্লায়েন্ট: PUBACK(id={pid}) পাঠানো হলো, কিন্তু নেটওয়ার্কে হারিয়ে গেলো (সিমুলেটেড)")
            log.append("  ক্লায়েন্ট টাইম-আউটের পর ACK না পেয়ে পুনরায় পাঠাবে")
            attempt += 1
            continue
        log.append(f"  ব্রোকার -> ক্লায়েন্ট: PUBACK(id={pid}) প্রাপ্ত -> সফল")
        acked = True
    log.append(f"  মোট PUBLISH চেষ্টা: {attempt}, সাবস্ক্রাইবার বার্তাটি মোট {len(deliveries)} বার পেলো "
                f"(ডুপ্লিকেট ঝুঁকি QoS1-এর স্বভাবজাত বৈশিষ্ট্য)")
    return log


def publish_qos2(broker, topic, payload):
    pid = broker.new_packet_id()
    log = [
        f"[QoS 2] ক্লায়েন্ট -> ব্রোকার: PUBLISH(id={pid})",
        f"  ব্রোকার -> ক্লায়েন্ট: PUBREC(id={pid})  (গ্রহণ করা হয়েছে, কিন্তু এখনও ডেলিভার হয়নি)",
        f"  ক্লায়েন্ট -> ব্রোকার: PUBREL(id={pid})  (রিলিজ নিশ্চিতকরণ)",
    ]
    recipients = broker.route(topic)
    log.append(f"  ব্রোকার এখন প্রকৃত ডেলিভারি করলো -> {recipients}")
    log.append(f"  ব্রোকার -> ক্লায়েন্ট: PUBCOMP(id={pid})  (হ্যান্ডশেক সম্পূর্ণ, ঠিক একবার ডেলিভারি নিশ্চিত)")
    return log


broker = MqttBroker()
broker.subscribe("dashboard-A", "sensors/+/temperature")
broker.subscribe("logger-B", "sensors/#")

print("== QoS 0 ==")
for line in publish_qos0(broker, "sensors/room1/temperature", 24.5):
    print(line)

print("\n== QoS 1 (স্বাভাবিক, কোনো লস নেই) ==")
for line in publish_qos1(broker, "sensors/room1/temperature", 24.6, simulate_lost_ack=False):
    print(line)

print("\n== QoS 1 (প্রথম PUBACK হারিয়ে গেছে -> জেনুইন রিট্রান্সমিশন) ==")
for line in publish_qos1(broker, "sensors/room1/temperature", 24.7, simulate_lost_ack=True):
    print(line)

print("\n== QoS 2 (৪-ধাপের হ্যান্ডশেক) ==")
for line in publish_qos2(broker, "sensors/room1/temperature", 24.8):
    print(line)

    
QoS 1-এর হারিয়ে যাওয়া ACK কেসে লক্ষ্য করুন — deliveries লিস্টের দৈর্ঘ্য ২ হয় (একবার প্রথম চেষ্টায়, একবার রিট্রান্সমিশনে), যদিও PUBACK শেষ পর্যন্ত একবারই সফলভাবে ফিরে আসে। এটাই বাস্তবে দেখায় কেন QoS 1-কে "at least once" বলা হয়, "exactly once" নয় — সাবস্ক্রাইবার-সাইড কোডকে তাই প্রায়ই ডুপ্লিকেট বার্তা হ্যান্ডল করার জন্য প্রস্তুত থাকতে হয় (যেমন প্যাকেট-আইডি দেখে ডুপ্লিকেট বাদ দেওয়া)।

৩ · রিটেইনড মেসেজ

সাধারণত একজন নতুন সাবস্ক্রাইবার শুধু ভবিষ্যতের বার্তা পায় — সাবস্ক্রাইব করার আগের কোনো বার্তা সে মিস করে। কিন্তু পাবলিশার যদি retain=True ফ্ল্যাগসহ পাবলিশ করে, ব্রোকার সেই টপিকের শেষ মান সংরক্ষণ করে রাখে এবং পরে যেই সাবস্ক্রাইব করুক না কেন, তাকে সাথে সাথে সেই শেষ মান পাঠিয়ে দেয় — যেমন একটি থার্মোস্ট্যাটের "বর্তমান তাপমাত্রা" টপিক, যাতে নতুন কানেক্ট হওয়া ড্যাশবোর্ড খালি স্ক্রিন না দেখে অবিলম্বে সর্বশেষ মান দেখতে পায়।

৪ · লাস্ট-উইল-অ্যান্ড-টেস্টামেন্ট (LWT)

একটি ডিভাইস কানেক্ট করার সময় ব্রোকারকে একটি ঐচ্ছিক "উইল" — একটি টপিক ও পেলোড — নিবন্ধন করতে পারে। ডিভাইসটি যদি স্বাভাবিকভাবে (একটি পরিষ্কার DISCONNECT পাঠিয়ে) সংযোগ ছাড়ে, ব্রোকার উইলটি প্রকাশ করে না। কিন্তু ডিভাইস যদি অপ্রত্যাশিতভাবে সংযোগ হারায় (পাওয়ার লস, নেটওয়ার্ক ড্রপ) — ব্রোকার স্বয়ংক্রিয়ভাবে সেই উইল-বার্তা প্রকাশ করে দেয়, যা অন্যান্য ক্লায়েন্টদের "এই ডিভাইসটি অফলাইন হয়ে গেছে" জানাতে ব্যবহার করা যায়।

Python
class WillAwareBroker(MqttBroker):
    """LWT সাপোর্টসহ ব্রোকার -- ক্লায়েন্ট প্রতি একটি ঐচ্ছিক (topic, payload) উইল রাখে।"""

    def __init__(self):
        super().__init__()
        self.wills = {}

    def connect(self, client_id, will_topic=None, will_payload=None):
        if will_topic is not None:
            self.wills[client_id] = (will_topic, will_payload)

    def disconnect_ungracefully(self, client_id):
        log = [f"ক্লায়েন্ট '{client_id}' অপ্রত্যাশিতভাবে সংযোগ হারালো (পরিষ্কার DISCONNECT পাঠায়নি)"]
        if client_id in self.wills:
            topic, payload = self.wills[client_id]
            recipients = self.route(topic)
            log.append(f"  ব্রোকার স্বয়ংক্রিয়ভাবে LWT প্রকাশ করলো -> টপিক='{topic}', payload='{payload}'")
            log.append(f"  ডেলিভার্ড: {recipients}")
        else:
            log.append(f"  ক্লায়েন্ট '{client_id}'-এর কোনো নিবন্ধিত উইল নেই -> কিছু প্রকাশ হলো না")
        return log


will_broker = WillAwareBroker()
will_broker.subscribe("status-monitor", "devices/+/status")
will_broker.connect("sensor-node-7", will_topic="devices/sensor-node-7/status", will_payload="offline")

print("সংযোগের সময় নিবন্ধিত উইল:", will_broker.wills)
print()
for line in will_broker.disconnect_ungracefully("sensor-node-7"):
    print(line)

    
মূল কথা · Key takeaway

QoS 0/1/2 হলো নির্ভরযোগ্যতা-বনাম-ওভারহেড-এর একটি স্কেল — বেশিরভাগ IoT সেন্সর ডেটার জন্য QoS 0 বা 1 যথেষ্ট, QoS 2 সংরক্ষিত থাকে সত্যিই ক্রিটিক্যাল কমান্ডের জন্য। রিটেইনড মেসেজ "সর্বশেষ অবস্থা" দ্রুত পাওয়ার সমস্যা সমাধান করে, আর LWT ডিভাইস অফলাইন-শনাক্তকরণ সমাধান করে — দুটোই বাস্তব প্রোডাকশন MQTT সিস্টেমে অত্যন্ত সাধারণ প্যাটার্ন।

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিজে কিছুক্ষণ ভাবুন — তারপর "→ উত্তর" চাপুন।

প্র ০১ উপরের কোডে publish_qos1-এ যদি simulate_lost_ack=True করেও দ্বিতীয় চেষ্টাতেও ACK হারিয়ে যেত (কোডটি সেটি সিমুলেট না করলেও), attempt-এর মান শেষ পর্যন্ত কত হতো বলে আপনার ধারণা?

while not acked লুপটি যতক্ষণ না একটি সফল PUBACK আসে ততক্ষণ চলতেই থাকবে, তাই প্রতিটি অতিরিক্ত লস হওয়া ACK-এ attempt ১ করে বাড়বে। বাস্তব MQTT ক্লায়েন্ট লাইব্রেরিতে সাধারণত একটি সর্বোচ্চ রিট্রাই সংখ্যা বা টাইম-আউট পলিসি থাকে যাতে অসীম রিট্রাই না ঘটে — এই সরলীকৃত সিমুলেশনে সেই সীমা যোগ করা হয়নি, শুধু মূল রিট্রান্সমিশন লজিকটাই দেখানো হয়েছে।

প্র ০২ কেন একটি ব্যাটারি-চালিত সেন্সর নোড সাধারণত QoS 2 এড়িয়ে QoS 0 বা 1 ব্যবহার করে?

QoS 2-এর ৪-ধাপের হ্যান্ডশেক (PUBLISH/PUBREC/PUBREL/PUBCOMP) মানে প্রতিটি বার্তার জন্য বেশি রেডিও ট্রান্সমিশন — আর রেডিও ট্রান্সমিশন সাধারণত একটি ব্যাটারি-চালিত IoT ডিভাইসের সবচেয়ে বেশি পাওয়ার-ক্ষুধার্ত কাজ (M8-এর পাওয়ার বাজেটিং পাঠের সাথে সরাসরি সম্পর্কিত)। তাই বেশিরভাগ নন-ক্রিটিক্যাল সেন্সর রিডিং-এর জন্য QoS 0/1-এর কম ওভারহেড অনেক বেশি ব্যাটারি-বান্ধব।

প্র ০৩ রিটেইনড মেসেজ ও লাস্ট-উইল — এই দুটো ফিচারের উদ্দেশ্যের মূল পার্থক্য কী?

রিটেইনড মেসেজ ব্রোকারকে বলে "এই টপিকের সর্বশেষ মান মনে রাখো, নতুন সাবস্ক্রাইবারকে সাথে সাথে দাও" — এটি পাবলিশারের স্বাভাবিক কার্যক্রমের অংশ। লাস্ট-উইল একদম উল্টো পরিস্থিতির জন্য — ডিভাইস অস্বাভাবিকভাবে সংযোগ হারালে ব্রোকার নিজেই একটি প্রি-রেজিস্টার্ড বার্তা প্রকাশ করে, যা ডিভাইসটি নিজে পাঠাতে পারেনি বলেই দরকার হয়েছে।

অনুশীলন

  1. চিন্তা করুন: QoS 2-এর কোড সেলে PUBREC ও PUBCOMP-এর মধ্যে PUBREL ধাপটি না থাকলে (অর্থাৎ শুধু ৩-ধাপের হ্যান্ডশেক হলে), সেটি কি এখনও ডুপ্লিকেট-মুক্ত ডেলিভারি গ্যারান্টি দিতে পারবে বলে আপনার মনে হয়?

    না, নিশ্চিতভাবে বলা কঠিন — PUBREL ধাপটি ক্লায়েন্ট-সাইড থেকে ব্রোকারকে "আমি নিশ্চিত করছি, এবার প্রকৃত ডেলিভারি করো" বলার সুযোগ দেয়, যা মাঝপথে সংযোগ বিচ্ছিন্ন হলেও উভয় পক্ষকে তাদের নিজস্ব অবস্থা (state) নির্ভরযোগ্যভাবে পুনরুদ্ধার করতে সাহায্য করে। এই পূর্ণাঙ্গ ৪-ধাপের হ্যান্ডশেকই হলো MQTT স্পেসিফিকেশনের প্রকৃত ডিজাইন — এটি এত জটিল বলেই QoS 2-এর ওভারহেড QoS 1-এর চেয়ে বেশি।

  2. পরীক্ষা করুন: উপরের প্রথম কোড সেলে publish_qos1-এর কলে simulate_lost_ack=True-এর জায়গায় একটি নতুন প্যারামিটার কল্পনা করুন যেখানে প্রথম দুইটি ACK হারায় (কোডটি নিজে সম্পাদনা করে চেষ্টা করুন) — attempt-এর চূড়ান্ত মান কত হবে?

    যদি attempt == 1-এর বদলে attempt <= 2 চেক করা হয় (দুইটি ACK হারানো সিমুলেট করতে), লুপটি তিনবার চলবে (attempt 1, 2, 3) — তৃতীয়বারে ACK সফলভাবে আসবে। তাই চূড়ান্ত attempt মান হবে ৩, আর deliveries-এর দৈর্ঘ্যও ৩ হবে (সাবস্ক্রাইবার বার্তাটি তিনবার পাবে)।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

  • কোর্সের সম্পূর্ণ সিলেবাস দেখুন ৫৭টি পাঠ মাইক্রোপ্রসেসর আর্কিটেকচার, এমবেডেড C, GPIO, টাইমার/PWM/ADC, সিরিয়াল প্রোটোকল, RTOS, সেন্সর/অ্যাকচুয়েটর, IoT আর্কিটেকচার, ওয়্যারলেস প্রোটোকল, MQTT/CoAP ও IoT সিকিউরিটি — বাকি পাঠগুলো শীঘ্রই যুক্ত হবে।
  • L46 · MQTT প্রোটোকল আগের পাঠ টপিক হায়ারার্কি, ওয়াইল্ডকার্ড ম্যাচিং ও মূল MqttBroker ক্লাস — এই পাঠের ভিত্তি।
  • সব Courses দেখুন ABCL TECH C, C++, Python, Java, JavaScript, DSA, DBMS, Discrete Mathematics, System Design, Cybersecurity, Cloud Computing & DevOps, Computer Networks, Operating Systems, Computer Architecture, Design and Analysis of Algorithms ও আরও অনেক কোর্স — সব এক জায়গায়।
আগের পাঠ
MQTT প্রোটোকল