9.18. MQTT, בית אחר בית¶
בשלב זה יש למצלמה כל רכיב שהיא צריכה כדי לתקשר עם שירות אמיתי באינטרנט הפתוח: שקע TCP, שכבת TLS לעטיפתו, DNS לזיהוי הצד השני, ו-asyncio שמאפשר לאותו סקריפט לבצע עבודה אחרת בזמן שהחיבור פתוח. MQTT הוא פרוטוקול התקשורת הראשון שמאחד את כל אלה למשהו שמוצר ממשי בפועל אכן משתמש בו.
עמוד זה עוסק בפרוטוקול עצמו – הפורמט על גבי הקו, התפקידים שכל משתתף ממלא, והפשרות בעיצובו – בכנות מספקת כדי שלקוח ה-mqtt המצורף ייראה כעטיפה מובנת מאליה של מה שכבר ידוע, ולא כקפיצה לתוך הלא נודע.
9.18.1. Pub/sub לעומת בקשה/תגובה¶
HTTP – הפרוטוקול שאליו רוב פרויקטי המצלמה פונים תחילה – מבוסס על בקשה/תגובה. לקוח מבקש משרת מסוים משאב מסוים; השרת עונה. כל חליפין הוא אחד-לאחד, ושני הצדדים יודעים מראש את הכתובת של זה את זה.
MQTT מבוסס על פרסום/מנוי (publish/subscribe). לקוחות מתחברים לצד שלישי באמצע הנקרא broker. מפרסם (publisher) שולח הודעה לנושא (topic) בעל שם, מבלי לדעת או להתעניין מי מאזין. מנוי (subscriber) מודיע ל-broker אילו נושאים הוא רוצה ומקבל לאחר מכן כל הודעה שפורסמה לנושאים אלה. ה-broker הוא נקודת ההפצה: פרסום אחד אל yard-cam/motion מגיע לכל התקן שרשום כמנוי ל-yard-cam/motion, גם אם ישנם אפס, אחד או חמישים כאלה.
שלושה דברים נובעים משינוי מודל זה:
ניתוק תלות. מפרסמים אינם צריכים לדעת שקיימים מנויים. מנויים יכולים להופיע ולהיעלם מבלי שהמפרסם ישים לב. הוספת לוח מחוונים שני היא שורת קוד אחת בלוח המחוונים החדש; המצלמה אינה משתנה.
הפצה (fan-out). ה-broker מטפל בכל שכפול, כך שהמצלמה שולחת חבילה אחת ללא קשר למספר ההתקנים שקוראים אותה. זהו תרחיש השימוש שלמענו נבנה MQTT.
אסימטריה. ה-broker הוא כעת רכיב תשתית נדרש – בלעדיו הפרוטוקול אינו עובד. בפרויקטים ביתיים מדובר בדרך כלל ב-broker ציבורי חינמי (
test.mosquitto.org,broker.hivemq.com) או בקטן שאתם מפעילים בעצמכם.
9.18.2. נושאים (Topics)¶
נושאים הם מחרוזות מופרדות בלוכסנים. המוסכמה היא הכללי ביותר בצד שמאל, הספציפי ביותר בצד ימין:
yard-cam/motion
yard-cam/temperature
workshop-cam/motion
workshop-cam/temperature/sensor-3
שני תווים כלליים (wildcards) עובדים במנויים (לא בפרסומים):
+מתאים לרמה בודדת.+/motionרושם מנוי לנושא ה-motion של כל מצלמה;yard-cam/+רושם מנוי לכל תת-נושא של yard-cam.#מתאים לרמה אחת או יותר בסוף.yard-cam/#רושם מנוי ל-yard-cam/motion,yard-cam/temperature,yard-cam/temperature/sensor-3, וכל דבר אחר תחתyard-cam/. הוא חייב להופיע בסוף המנוי.
מחרוזות נושא רגישות לאותיות גדולות/קטנות. לפי המפרט, $ מוביל מסמן נושאים פנימיים של ה-broker ($SYS/...) שמפרסמים אינם אמורים לכתוב אליהם.
9.18.3. פורמט החבילה¶
MQTT פועל מעל TCP. כל חבילת בקרה מתחילה בכותרת קבועה בגודל בית אחד, ואחריה שדה Remaining Length באורך משתנה, לאחר מכן כותרת משתנה התלויה בסוג החבילה, ואז המטען (payload). אותו פורמט חיצוני מכסה כל פקודה – CONNECT, PUBLISH, SUBSCRIBE, PUBACK, DISCONNECT והשאר – ולכן ניתן לכתוב לקוח MQTT בכמה מאות שורות.
הכותרת הקבועה היא בית אחד:
ביטים 7..4 הם סוג חבילת הבקרה.
0x3הוא PUBLISH (ולכן הבית הראשון בדרך כלל מתחיל ב-0x3?).0x1הוא CONNECT,0x2הוא CONNACK,0x8הוא SUBSCRIBE,0xCהוא PINGREQ,0xEהוא DISCONNECT, וכן הלאה.ביטים 3..0 הם דגלים התלויים בסוג החבילה. עבור PUBLISH הדגלים מקודדים את דגל השידור החוזר DUP, את רמת ה-QoS (2 ביטים), ואת דגל ה-RETAIN.
ה-Remaining Length הוא מספר שלם באורך משתנה של 1 עד 4 בתים שסופר כל בית שאחריו. הביט העליון של כל בית הוא סמן המשכיות – 1 פירושו ”בית אורך נוסף בא בהמשך“, 0 פירושו ”זהו האחרון“. אורך מתחת ל-128 נכנס לבית אחד; מטענים גדולים יותר משתמשים ביותר. האורך המקסימלי המקודד הוא 256 MiB.
עבור PUBLISH הכותרת המשתנה היא שם הנושא – אורך בן 2 בתים, ואז בתי UTF-8 – ואחריה מזהה חבילה בן 2 בתים שקיים רק כאשר ה-QoS הוא 1 או 2. הבתים הנותרים הם המטען, המטופלים כבתים אטומים על ידי הפרוטוקול.
PUBLISH מינימלי ברמת QoS-0 של ok אל a/b הוא:
30 07 00 03 'a' '/' 'b' 'o' 'k'
30– PUBLISH, כל הדגלים אפס.07– 7 בתים באים בהמשך.00 03– אורך הנושא 3.'a' '/' 'b'– הנושא.'o' 'k'– המטען.
תשעה בתים על גבי הקו וההודעה נוחתת אצל כל מנוי ל-a/b על ה-broker.
9.18.4. רמות QoS¶
Quality-of-Service שולט בעוצמת המאמץ שה-broker (והלקוח) משקיעים כדי להבטיח את המסירה. שלוש הרמות:
QoS 0 – לכל היותר פעם אחת. שגר ושכח. חבילת ה-PUBLISH נשלחת ולעולם אינה מאושרת. אם TCP מוסר, ה-broker מעביר הלאה. אם החיבור נופל באמצע השליחה, ההודעה אבודה. רוב הטלמטריה של חיישנים מתאימה ל-QoS 0 – קריאת טמפרטורה בודדת שאבדה בזרם שפולט כל 30 שניות אינה משנה.
QoS 1 – לפחות פעם אחת. המפרסם כולל מזהה חבילה וממתין ל-PUBACK. אם לא מגיע PUBACK לפני פקיעת זמן, המפרסם משדר שוב כשדגל ה-DUP מוגדר. ה-broker עלול בסופו של דבר למסור את אותה הודעה פעמיים למנוי באותה רמה; המנוי צריך להיות מוכן לטפל בכפילויות.
QoS 2 – בדיוק פעם אחת. לחיצת יד בת ארבעה שלבים (PUBREC / PUBREL / PUBCOMP) מבטיחה שההודעה נוחתת בדיוק פעם אחת, גם לאורך התחברויות מחדש. יקרה במחזורי הלוך-ושוב ובמצב ה-broker. מעט אפליקציות מצלמה זקוקות לה.
לקוח ה-mqtt המצורף מממש QoS 0 ו-QoS 1; QoS 2 מעלה שגיאה אם תבקשו אותו. עבור מצלמה המדווחת קריאות חיישנים, QoS 0 הוא כמעט תמיד התשובה הנכונה.
9.18.5. הודעות נשמרות (retained) וצוואה אחרונה¶
שתי תכונות ראויות להכרה משום שהן משנות את מה שה-broker זוכר על הנושא שלכם.
RETAIN. אם ל-PUBLISH מוגדר דגל ה-RETAIN, ה-broker מאחסן את ההודעה ומעביר אותה לכל מנוי עתידי ברגע שהוא נרשם. כך MQTT מטפל ב“מהו הערך הנוכחי?“ – חיישן מפרסם את הקריאה האחרונה שלו עם retain, ולוח מחוונים שנרשם כמנוי עשר דקות מאוחר יותר עדיין מקבל את הערך העדכני ביותר במקום להמתין לפרסום הבא. פרסום מחדש עם אותו נושא דורס את הערך השמור; פרסום מטען ריק מנקה אותו.
צוואה אחרונה. כאשר לקוח מתחבר הוא יכול לתת ל-broker ”צוואה אחרונה“: נושא, מטען, QoS, ודגל retain. אם אותו לקוח מתנתק באופן לא נקי – TCP RESET, אובדן חשמל, נפילת רשת ללא חבילת DISCONNECT – ה-broker מפרסם את הצוואה בשם הלקוח. המנויים רואים זאת כהודעת המצלמה שהיא ירדה מהרשת. המצלמה עצמה לעולם אינה שולחת את הצוואה; ה-broker עושה זאת, כי עד אז המצלמה כבר אינה זמינה.
9.18.6. Keepalive והתחברות מחדש¶
CONNECT נושא מרווח keepalive בשניות. אם הלקוח שתק במשך זמן זה, ה-broker מחשיב אותו כמת. כדי למנוע זאת, הלקוח שולח מעת לעת PINGREQ (בית אחד: 0xC0) ומקבל בחזרה PINGRESP (0xD0) – הדופק הקטן והזול ביותר שהפרוטוקול יכול לשאת. רוב אפליקציות המצלמה מגדירות keepalive ל-30 או 60 שניות.
אם חיבור ה-TCP נופל, שני הצדדים שמים לב ומתחברים מחדש מאפס. מנויים שנעשו לפני הנפילה אובדים אלא אם הלקוח השתמש בהפעלה מתמשכת (persistent session) בעת ההתחברות; עבור אפליקציות מצלמה פשוטות, דפוס הרישום-מחדש-עם-התחברות-מחדש קצר יותר וטוב בדיוק באותה מידה.
די בכך כדי לקרוא את מפרט MQTT או לכתוב לקוח ביד מעל socket.socket. הלקוח המצורף ב-mqtt עושה בדיוק את זה, ובנוסף מספק API הגיוני לקוד היישום.