MQTT และ IoT
UAT 105 เทคโนโลยีดิจิทัลและเครือข่าย
บทเรียน
เมื่อเรียนจบโมดูลนี้ ผู้เรียนจะสามารถ
- อธิบายสถาปัตยกรรม IoT ตั้งแต่อุปกรณ์ เกตเวย์ ถึงคลาวด์
- ออกแบบโครงสร้าง topic และใช้ wildcard + และ
- เลือกระดับ QoS ข้อความค้าง (retained) และ Last Will ให้เหมาะกับข้อมูลแต่ละชนิด
- ตั้งค่า Mosquitto อย่างปลอดภัย และประเมินอายุข้อมูลเมื่อคิวสะสม
ทำไมต้องรู้
ภารกิจเฝ้าระวังน้ำท่วมอาจมีโดรนสามลำ เซนเซอร์ระดับน้ำสิบจุด สถานีภาคพื้น แดชบอร์ดของศูนย์บัญชาการ และฐานข้อมูล ถ้าให้ทุกอุปกรณ์เชื่อมกันเองแบบจุดต่อจุด จำนวนการเชื่อมต่อจะเพิ่มเร็วมาก และเพิ่มอุปกรณ์ใหม่แต่ละครั้งต้องแก้ทุกเครื่อง MQTT แก้ปัญหานี้ด้วยตัวกลางที่รับและกระจายข้อความ UAT 314 โมดูล 1 แนะนำ MQTT ไว้แล้ว โมดูลนี้จะลงรายละเอียดจนออกแบบและตั้งค่าระบบได้จริง
สถาปัตยกรรม IoT
อินเทอร์เน็ตของสรรพสิ่ง (Internet of Things, IoT) คือระบบที่อุปกรณ์จำนวนมากวัดค่า ส่งข้อมูล และรับคำสั่งผ่านเครือข่าย โดยทั่วไปแบ่งเป็นสี่ชั้น
อุปกรณ์ปลายทางมักมีพลังงานและกำลังประมวลผลจำกัด เกตเวย์ รวบรวมข้อมูลจากอุปกรณ์ที่ใช้วิทยุระยะไกลกำลังต่ำ เช่น LoRaWAN แล้วแปลงส่งต่อเข้าเครือข่าย IP ตัวอย่างในคลังความรู้โดรนคือ ChirpStack ซึ่งเป็นเซิร์ฟเวอร์เครือข่าย LoRaWAN ที่ส่งข้อมูลทุกข้อความของอุปกรณ์ออกเป็น JSON ผ่าน MQTT ใน topic เช่น application/APPLICATION_ID/device/DEV_EUI/event/up
Publish/subscribe และ topic
ใน MQTT ผู้ส่ง (publisher) ส่งข้อความไปยัง topic ที่ตัวกลางชื่อ โบรกเกอร์ (broker) ส่วนผู้รับ (subscriber) บอกโบรกเกอร์ว่าสนใจ topic ใด โบรกเกอร์กระจายข้อความให้ผู้รับที่ตรงเงื่อนไข ผู้ส่งกับผู้รับไม่ต้องรู้จักกัน ไม่ต้องออนไลน์พร้อมกัน และเพิ่มผู้รับใหม่ได้โดยไม่แก้ผู้ส่ง
topic เป็นข้อความหลายระดับคั่นด้วย / คล้ายเส้นทางไฟล์ ผู้รับใช้ wildcard ได้สองแบบ ตามมาตรฐาน MQTT 5.0
+แทน หนึ่งระดับพอดี เช่นdrone/+/batteryตรงกับdrone/3/batteryแต่ไม่ตรงกับdrone/3/gps#แทน ทุกระดับที่เหลือ ต้องอยู่ท้ายสุดเท่านั้น และตรงกับระดับแม่ด้วย เช่นdrone/#ตรงกับทั้งdrone/3/gps/fixและdrone
def topic_matches(topic_filter, topic):
f_parts, t_parts = topic_filter.split("/"), topic.split("/")
for i, f in enumerate(f_parts):
if f == "#":
return True
if i >= len(t_parts):
return False
if f != "+" and f != t_parts[i]:
return False
return len(f_parts) == len(t_parts)
cases = [("drone/+/battery", "drone/3/battery"), ("drone/+/battery", "drone/3/gps"), ("drone/#", "drone/3/gps/fix"),
("drone/+", "drone/3/battery"), ("drone/#", "drone"), ("fleet/+/+/alt", "fleet/a/7/alt")]
for f, t in cases:
print(f"{f:<16} {t:<18} {topic_matches(f, t)}")
drone/+/battery drone/3/battery True
drone/+/battery drone/3/gps False
drone/# drone/3/gps/fix True
drone/+ drone/3/battery False
drone/# drone True
fleet/+/+/alt fleet/a/7/alt True
หลักออกแบบ topic
เรียงจากกว้างไปแคบ เช่น ภารกิจ/โดรน/ชนิดข้อมูล ใช้ตัวอักษรภาษาอังกฤษตัวเล็กไม่มีช่องว่าง แยก topic ของข้อมูลวัดกับคำสั่งออกจากกัน เช่น drone/3/telemetry กับ drone/3/cmd เพื่อกำหนดสิทธิ์แยกกันได้ และอย่าใส่ข้อมูลส่วนบุคคลหรือรหัสลับไว้ในชื่อ topic
QoS: รับประกันการส่งสามระดับ
MQTT กำหนด คุณภาพบริการ (Quality of Service, QoS) ต่อข้อความ
| QoS | ความหมายตามมาตรฐาน | ข้อความที่แลก | เหมาะกับ |
|---|---|---|---|
| 0 | ส่งไม่เกินหนึ่งครั้ง (at most once) อาจหาย | PUBLISH | telemetry ความถี่สูงที่ข้อมูลใหม่มาแทน |
| 1 | ส่งอย่างน้อยหนึ่งครั้ง (at least once) อาจซ้ำ | PUBLISH, PUBACK | สถานะและการแจ้งเตือน |
| 2 | ส่งครั้งเดียวพอดี (exactly once) | PUBLISH, PUBREC, PUBREL, PUBCOMP | คำสั่งที่ห้ามซ้ำและห้ามหาย |
QoS สูงขึ้นแลกกับข้อความและเวลาที่มากขึ้น ใน QoS 1 ถ้า PUBACK หายระหว่างทาง ผู้ส่งจะส่ง PUBLISH ซ้ำ ผู้รับจึงได้ข้อความเดียวกันสองครั้ง โปรแกรมผู้รับต้องเตรียมรับมือ
ตัวอย่างที่ 1 คำสั่งซ้ำจาก QoS 1
จำลองคำสั่ง RTL หมายเลข 7 ที่ PUBACK ครั้งแรกหาย แล้วให้ผู้รับตัดข้อความซ้ำด้วยหมายเลขข้อความ
received = []
def deliver_qos1(msg_id, payload, puback_lost):
attempts = 0
while True:
attempts += 1
received.append((msg_id, payload))
if not puback_lost or attempts > 1:
return attempts
print("attempts:", deliver_qos1(7, "RTL", puback_lost=True), "received:", received)
seen, actions = set(), []
for msg_id, payload in received:
if msg_id in seen:
continue
seen.add(msg_id)
actions.append(payload)
print("actions:", actions)
attempts: 2 received: [(7, 'RTL'), (7, 'RTL')]
actions: ['RTL']
การออกแบบให้ทำคำสั่งซ้ำแล้วได้ผลเหมือนทำครั้งเดียว (idempotent) เช่น “ตั้งโหมดเป็น RTL” ปลอดภัยกว่าคำสั่งแบบสะสม เช่น “ไต่ขึ้นอีก 10 m” ที่ถ้าทำซ้ำจะไต่ขึ้น 20 m
ข้อความค้างและ Last Will
- ข้อความค้าง (retained message) ถ้าส่งพร้อมธง RETAIN โบรกเกอร์จะเก็บข้อความล่าสุดของ topic นั้นไว้ แทนที่ข้อความค้างเดิม ผู้รับที่สมัครทีหลังจะได้ค่าล่าสุดทันทีโดยไม่ต้องรอรอบถัดไป เหมาะกับสถานะ เช่น
onlineหรือค่าตั้งของภารกิจ - Last Will อุปกรณ์ฝากข้อความไว้กับโบรกเกอร์ตอนเชื่อมต่อ ถ้าการเชื่อมต่อขาดแบบผิดปกติ โบรกเกอร์จะส่งข้อความนั้นแทน เช่น
drone/1/status = offlineศูนย์บัญชาการจึงรู้ว่าโดรนหลุดการเชื่อมต่อ
class MiniBroker:
def __init__(self):
self.subs = []
self.retained = {}
def subscribe(self, name, topic_filter):
self.subs.append((topic_filter, name))
for topic, payload in self.retained.items():
if topic_matches(topic_filter, topic):
print(f" {name} <- {topic} = {payload} (retained)")
def publish(self, topic, payload, retain=False):
if retain:
self.retained[topic] = payload
for topic_filter, name in self.subs:
if topic_matches(topic_filter, topic):
print(f" {name} <- {topic} = {payload}")
broker = MiniBroker()
broker.subscribe("gcs", "drone/+/battery")
broker.publish("drone/1/status", "online", retain=True)
broker.publish("drone/1/battery", "76%")
print("dashboard joins late:")
broker.subscribe("dashboard", "drone/#")
print("drone 1 link drops, broker sends its will:")
broker.publish("drone/1/status", "offline", retain=True)
gcs <- drone/1/battery = 76%
dashboard joins late:
dashboard <- drone/1/status = online (retained)
drone 1 link drops, broker sends its will:
dashboard <- drone/1/status = offline
แดชบอร์ดที่เข้ามาทีหลังได้สถานะ online ทันทีเพราะเป็นข้อความค้าง แต่ไม่ได้ค่าแบตเตอรี่ 76% เพราะข้อความนั้นไม่ได้ตั้ง RETAIN โบรกเกอร์จำลองตัวนี้เขียนเพื่อให้เห็นหลักการเท่านั้น
MQTT 5.0 (มาตรฐาน OASIS ปี 2019) เพิ่มความสามารถสำคัญจาก 3.1.1 เช่น reason code ที่บอกเหตุผลเมื่อทำงานไม่สำเร็จ session expiry ที่กำหนดว่าโบรกเกอร์จำการสมัครของผู้รับไว้นานเท่าใด และ message expiry interval ที่ทิ้งข้อความที่เก่าเกินกำหนด ซึ่งช่วยไม่ให้ส่งคำสั่งที่หมดอายุไปแล้ว
ความสดของข้อมูลและคิว
ข้อความส่งถึงไม่ได้แปลว่าข้อมูลยังใช้ได้ อายุข้อมูล คือเวลาตั้งแต่วัดจนถึงตอนใช้ ถ้าข้อความเข้าคิวเร็วกว่าที่ผู้รับประมวลผลได้ () คิวจะยาวขึ้นเรื่อย ๆ และข้อความใหม่ต้องรอนานขึ้น สูตรนี้มาจากหน่วยความรู้ MQTT และ Mosquitto ของคลังความรู้โดรน
ตัวอย่างที่ 2 แดชบอร์ดประมวลผลไม่ทัน
โดรนหลายลำส่งรวม 50 ข้อความต่อวินาที แต่แดชบอร์ดประมวลผลได้ 40 ข้อความต่อวินาที เริ่มจากคิวว่าง
lam, mu, q0 = 50, 40, 0
for t in (10, 60, 300):
q = max(0, q0 + (lam - mu) * t)
print(f"after {t:>3} s queue {q:>4} messages a new message waits about {q / mu:.1f} s")
after 10 s queue 100 messages a new message waits about 2.5 s
after 60 s queue 600 messages a new message waits about 15.0 s
after 300 s queue 3000 messages a new message waits about 75.0 s
หลัง 5 นาที ข้อความใหม่ต้องรอประมาณ 75 วินาที ตำแหน่งบนแดชบอร์ดจึงเก่าไปกว่าหนึ่งนาทีทั้งที่ไม่มีข้อความหายเลย ทางแก้คือลดอัตราส่ง รวมข้อความ เพิ่มกำลังประมวลผล หรือใช้ message expiry ทิ้งข้อความเก่า และควรแสดงอายุข้อมูลบนหน้าจอเสมอ
Mosquitto และความปลอดภัย
Eclipse Mosquitto เป็นโบรกเกอร์ MQTT โอเพนซอร์สที่ใช้แพร่หลาย รุ่นล่าสุดขณะเขียนคือ 2.1.2 (กุมภาพันธ์ 2026) มีเครื่องมือ mosquitto_pub และ mosquitto_sub สำหรับทดสอบ IANA กำหนดพอร์ต 1883 สำหรับ MQTT ข้อความธรรมดา และ 8883 สำหรับ MQTT บน TLS
ตั้งแต่ Mosquitto 2.0 ถ้าไม่ได้กำหนด listener โบรกเกอร์จะรับเฉพาะการเชื่อมต่อจากเครื่องตัวเอง (loopback) และเมื่อกำหนด listener แล้ว allow_anonymous มีค่าเริ่มต้นเป็น false ค่าเริ่มต้นนี้ป้องกันการเปิดโบรกเกอร์สู่เครือข่ายโดยไม่ตั้งใจ การใช้งานจริงควรเพิ่มอีกสามชั้น
- TLS เข้ารหัสข้อมูลระหว่างทางผ่านพอร์ต 8883 กันการดักฟัง
- บัญชีแยกต่ออุปกรณ์ โดรนแต่ละลำมีชื่อผู้ใช้และรหัสผ่านหรือใบรับรองของตัวเอง
- สิทธิ์ราย topic (ACL) โดรน 1 publish ได้เฉพาะ
drone/1/#จึงปลอมตัวส่งข้อมูลในนามโดรน 2 ไม่ได้
ตัวอย่างไคลเอนต์ด้วย paho-mqtt รุ่น 2 ซึ่งต้องระบุ CallbackAPIVersion (โค้ดนี้ต้องมีโบรกเกอร์จริง จึงไม่ถูกรันในการตรวจอัตโนมัติ)
import os
import paho.mqtt.client as mqtt
def on_connect(client, userdata, flags, reason_code, properties):
print("connected:", reason_code)
client.subscribe("drone/+/battery", qos=1)
def on_message(client, userdata, msg):
print(msg.topic, msg.payload.decode())
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="gcs-01", protocol=mqtt.MQTTv5)
client.username_pw_set("gcs-01", os.environ["MQTT_PASSWORD"])
client.tls_set(ca_certs="ca.crt")
client.will_set("gcs/gcs-01/status", "offline", qos=1, retain=True)
client.on_connect = on_connect
client.on_message = on_message
client.connect("broker.field.lan", 8883)
client.loop_forever()
รหัสผ่านอ่านจากตัวแปรสภาพแวดล้อม ไม่เขียนลงในโค้ด ตามหลักที่เรียนใน UAT 104 โมดูล 5
ปฏิบัติการประจำโมดูล
ปฏิบัติการ: ระบบรายงานแบตเตอรี่ฝูงโดรน
- ติดตั้ง Mosquitto บนเครื่องของตน เปิด
mosquitto_sub -t "drone/#" -vในหน้าต่างหนึ่ง แล้วส่งข้อความด้วยmosquitto_pubจากอีกหน้าต่าง ทดลอง+และ# - เขียนโปรแกรม paho-mqtt จำลองโดรนสามลำส่งค่าแบตเตอรี่ทุก 1 วินาทีด้วย QoS 0 และสถานะ
onlineแบบ retained พร้อม Last Willoffline - เขียนผู้รับที่แสดงค่าแบตเตอรี่ล่าสุดพร้อมอายุข้อมูล แล้วปิดโปรแกรมโดรนหนึ่งลำแบบกะทันหันเพื่อดูว่า Last Will ทำงานหรือไม่
- เปิด TLS และ ACL ตามหน่วยความรู้ MQTT และ Mosquitto แล้วทดสอบว่าโดรน 1 ส่งข้อความใน topic ของโดรน 2 ไม่ได้
ข้อผิดพลาดที่พบบ่อย
ระวัง
- เปิดโบรกเกอร์พอร์ต 1883 สู่เครือข่ายโดยไม่มีรหัสผ่าน ใครก็ดักฟังและส่งคำสั่งปลอมได้
- ใช้ QoS 2 กับทุกข้อความ เพิ่มข้อความสี่เท่าโดยไม่จำเป็นกับ telemetry ที่ข้อมูลใหม่มาแทน
- ไม่รับมือข้อความซ้ำจาก QoS 1 คำสั่งแบบสะสมจะถูกทำซ้ำ
- ตั้ง retained กับ telemetry ที่เปลี่ยนเร็ว ผู้รับใหม่อาจได้ค่าที่เก่ามากโดยไม่รู้ตัว
- ดูแค่ว่าข้อความมาถึง ไม่ตรวจอายุข้อมูล จึงไม่รู้ว่าคิวสะสมจนข้อมูลเก่า
สรุป
- IoT แบ่งเป็นอุปกรณ์ เกตเวย์ โบรกเกอร์และคลาวด์ และแอปพลิเคชัน MQTT เชื่อมทุกชั้นด้วย publish/subscribe
+แทนหนึ่งระดับ#แทนทุกระดับที่เหลือและต้องอยู่ท้ายสุด- QoS 0 อาจหาย QoS 1 อาจซ้ำ QoS 2 ครั้งเดียวพอดีแต่ช้าที่สุด ข้อความค้างให้ค่าล่าสุดแก่ผู้รับใหม่ และ Last Will แจ้งเมื่ออุปกรณ์หลุด
- ตั้ง Mosquitto ด้วย TLS บัญชีแยก และ ACL และตรวจอายุข้อมูลเสมอ เพราะคิวที่สะสมทำให้ข้อมูลเก่าได้โดยไม่มีข้อความหาย
แบบฝึกตรวจความเข้าใจ
site/+/drone/#ตรงกับsite/a/drone/2/gpsหรือไม่drone/+ตรงกับdrone/2/batteryหรือไม่ เพราะอะไร- คำสั่ง “ลงจอด” ควรใช้ QoS ระดับใด และควรออกแบบคำสั่งอย่างไร
- ข้อความเข้า 30 ข้อความต่อวินาที ประมวลผลได้ 25 ข้อความต่อวินาที เริ่มจากคิวว่าง หลัง 2 นาทีคิวยาวเท่าใด และข้อความใหม่รอประมาณกี่วินาที
- ทำไมจึงไม่ควรเปิดโบรกเกอร์ที่พอร์ต 1883 บนเครือข่ายที่คนอื่นเข้าได้
เฉลย
- ตรง เพราะ
+แทนaและ#แทน2/gps - ไม่ตรง เพราะ
+แทนได้เพียงหนึ่งระดับ แต่2/batteryมีสองระดับ - ใช้ QoS 1 หรือ 2 เพื่อไม่ให้หาย และออกแบบให้ idempotent คือทำซ้ำแล้วผลเหมือนเดิม
- ข้อความ และรอประมาณ วินาที
- พอร์ต 1883 ส่งข้อความธรรมดาไม่เข้ารหัส ผู้ที่อยู่ในเครือข่ายเดียวกันดักฟังได้ และถ้าไม่มีการยืนยันตัวตนก็ส่งคำสั่งปลอมได้
สรุปสูตรสำคัญ
| อายุข้อมูล ณ ผู้ใช้ | |
| ความยาวคิวเมื่อข้อความเข้าเร็วกว่าประมวลผล | |
| เวลารอของข้อความใหม่ท้ายคิว |
แหล่งอ้างอิงหลัก
- OASIS. (2019). MQTT version 5.0 (OASIS Standard, 7 March 2019). link
- OASIS. (2014). MQTT version 3.1.1 (OASIS Standard, 29 October 2014). link
- Eclipse Foundation. Eclipse Mosquitto: An open source MQTT broker. link
- Eclipse Foundation. Migrating from 1.x to 2.0. Eclipse Mosquitto documentation. link
- Eclipse Foundation. paho-mqtt: MQTT version 5.0/3.1.1 client class (Python package). link
- ChirpStack. MQTT integration. ChirpStack documentation. link
- Kurose, J. F., & Ross, K. W. (2025). Computer networking: A top-down approach (9th ed.). Pearson. link
- Fagan, M., Megas, K., Cuthill, B., Marron, J., & Hoehn, B. (2026). Foundational cybersecurity activities for IoT product manufacturers (NIST IR 8259 Rev. 1). National Institute of Standards and Technology. link
อ่านเพิ่มเติม
ศึกษาหน่วยความรู้ที่กำหนดล่วงหน้า ดูสื่อประกอบ และทำ quiz ประจำโมดูล
เนื้อหาเจาะลึก: MQTT และ Eclipse Mosquitto สำหรับระบบโดรนและ IoT
คลาวด์และบริการข้อมูล IoT
เนื้อหาเจาะลึก: การประสานฝูงโดรน (Swarm Drone) ผ่านโครงข่ายสื่อสาร LoRa Mesh Network
ในชั้นเรียน / ภาคสนาม
ปฏิบัติการในห้องแล็บหรือภาคสนามตามใบงาน พร้อม checklist ความปลอดภัย
หลักฐานการเรียนรู้: ใบงานที่ผ่านการตรวจและผล quiz