9.18. MQTT をバイト単位で見る

この時点で、カメラはオープンなインターネット上の実サービスと通信するために必要なすべての要素を備えています。すなわち TCP ソケット、それをラップする TLS、相手に名前を付ける DNS、そして接続が開いている間に同じスクリプトが他の作業を行えるようにする asyncio です。MQTT は、これらすべてをまとめて、実際に出荷される製品が使うものへと仕立て上げる最初のワイヤープロトコルです。

このページではプロトコルそのもの、つまりワイヤー上のフォーマット、各参加者が果たす役割、そして設計上のトレードオフを扱います。これらを十分に正直に説明することで、付属の mqtt クライアントが、すでに分かっていることを当然のように包んだものに見え、信仰の飛躍のようには感じられないようにすることが狙いです。

9.18.1. パブリッシュ/サブスクライブ対リクエスト/レスポンス

HTTP は、ほとんどのカメラプロジェクトが最初に手を伸ばすプロトコルですが、これは リクエスト/レスポンス 型です。クライアントが特定のサーバーに特定のリソースを要求し、サーバーが応答します。すべてのやり取りは一対一で、両端が事前に互いのアドレスを知っています。

MQTT は パブリッシュ/サブスクライブ 型です。クライアントは中間に位置する第三者である ブローカー に接続します。パブリッシャー は、誰が聞いているかを知ることも気にすることもなく、名前の付いた トピック にメッセージを送ります。サブスクライバー は、どのトピックが欲しいかをブローカーに伝え、それ以降そのトピックに発行されたすべてのメッセージを受信します。ブローカーはファンアウトの役割を担います。yard-cam/motion への 1 回のパブリッシュは、その購読者がゼロ台でも 1 台でも 50 台でも、yard-cam/motion を購読しているすべてのデバイスに届きます。

このモデルの変化から、次の 3 つのことが導かれます。

  • 疎結合。 パブリッシャーはサブスクライバーが存在することを知る必要がありません。サブスクライバーはパブリッシャーに気づかれることなく現れたり消えたりできます。2 つ目のダッシュボードを追加するのは、新しいダッシュボード側のコード 1 行で済み、カメラは変更されません。

  • ファンアウト。 ブローカーがすべての複製を処理するため、カメラは何台のデバイスが読み取るかに関係なくパケットを 1 つ送ります。これこそが MQTT が作られた目的のユースケースです。

  • 非対称性。 ブローカーは今や必須のインフラとなり、これがなければプロトコルは機能しません。家庭向けプロジェクトでは、通常これは無料の公開ブローカー(test.mosquitto.orgbroker.hivemq.com)か、自分で運用する小さなものになります。

1 台のカメラがブローカー上の yard-cam/motion トピックにパブリッシュし、 2 つのブラウザダッシュボードと 1 つのクラウドアーカイバーが それぞれ同じメッセージを受信する様子。

9.18.2. トピック

トピックはスラッシュ区切りの文字列です。慣例として、左側が最も一般的、右側が最も具体的になります:

yard-cam/motion
yard-cam/temperature
workshop-cam/motion
workshop-cam/temperature/sensor-3

サブスクリプション では 2 つのワイルドカードが使えます(パブリッシュでは使えません)。

  • + は 1 レベルにマッチします。+/motion はすべてのカメラの motion トピックを購読し、yard-cam/+ はすべての yard-cam サブトピックを購読します。

  • # は末尾の 1 レベル以上にマッチします。yard-cam/#yard-cam/motionyard-cam/temperatureyard-cam/temperature/sensor-3、そして yard-cam/ の下にあるその他すべてを購読します。これはサブスクリプションの末尾に現れる必要があります。

トピック文字列は大文字と小文字を区別します。仕様により、先頭の $ はブローカー内部のトピック($SYS/...)を示し、パブリッシャーはそこに書き込むべきではありません。

9.18.3. パケットフォーマット

MQTT は TCP 上で動作します。すべての制御パケットは 1 バイトの 固定ヘッダー で始まり、続いて可変長の Remaining Length フィールド、その後にパケットタイプ固有の 可変ヘッダー、そして ペイロード が続きます。同じ外側のフォーマットが、CONNECT、PUBLISH、SUBSCRIBE、PUBACK、DISCONNECT などすべてのコマンドをカバーします。だからこそ MQTT クライアントは数百行で書けるのです。

MQTT PUBLISH パケットのバイトレイアウト。固定ヘッダーの タイプおよびフラグバイト、可変長の Remaining Length フィールド、トピック名、オプションのパケット識別子、 そしてペイロードバイトを示しています。

固定ヘッダーは 1 バイトです。

  • ビット 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 未満の長さは 1 バイトに収まり、より大きなペイロードはより多くのバイトを使います。エンコード可能な最大長は 256 MiB です。

PUBLISH の場合、可変ヘッダーはトピック名(2 バイトの長さ、続いて UTF-8 バイト)で、その後に QoS が 1 または 2 のときにのみ存在する 2 バイトの パケット識別子 が続きます。残りのバイトはペイロードで、プロトコルからは不透明なバイト列として扱われます。

a/b への ok の最小限の QoS-0 PUBLISH は次のようになります:

30 07 00 03 'a' '/' 'b' 'o' 'k'
  • 30 -- PUBLISH、フラグはすべてゼロ。

  • 07 -- 7 バイトが続く。

  • 00 03 -- トピック長 3。

  • 'a' '/' 'b' -- トピック。

  • 'o' 'k' -- ペイロード。

ワイヤー上では 9 バイトで、メッセージはブローカー上の a/b のすべてのサブスクライバーに届きます。

9.18.4. QoS レベル

Quality-of-Service(サービス品質)は、ブローカー(およびクライアント)が配信を保証するためにどれだけ努力するかを制御します。3 つのレベルがあります。

QoS 0 -- 高々 1 回。 送りっぱなしです。PUBLISH パケットは送信され、確認されることはありません。TCP が配信すればブローカーは転送します。送信途中で接続が切れれば、メッセージは失われます。ほとんどのセンサーテレメトリは QoS 0 で問題ありません。30 秒ごとに発信されるストリームの中で温度測定値が 1 つ欠けても問題にはなりません。

QoS 1 -- 少なくとも 1 回。 パブリッシャーはパケット識別子を含め、PUBACK を待ちます。タイムアウトまでに PUBACK が届かなければ、パブリッシャーは DUP フラグを立てて再送します。ブローカーは同じレベルのサブスクライバーに同じメッセージを 2 回配信することになる場合があり、サブスクライバーは重複を処理する用意がなければなりません。

QoS 2 -- ちょうど 1 回。 4 ステップのハンドシェイク(PUBREC / PUBREL / PUBCOMP)によって、再接続をまたいでもメッセージがちょうど 1 回だけ届くことを保証します。ラウンドトリップ数とブローカーの状態の点で高コストです。これを必要とするカメラアプリはほとんどありません。

付属の mqtt クライアントは QoS 0 と QoS 1 を実装しています。QoS 2 を要求すると例外が発生します。センサー測定値を報告するカメラの場合、ほぼ常に QoS 0 が正しい選択です。

9.18.5. 保持メッセージと最終遺言

ブローカーがあなたのトピックについて何を記憶するかを変える 2 つの機能は、知っておく価値があります。

RETAIN。 PUBLISH に RETAIN フラグが立っている場合、ブローカーはそのメッセージを保存し、将来の サブスクライバーが購読した瞬間にそれを転送します。これが MQTT による「現在の値は何か?」の扱い方です。センサーは最新の測定値を保持付きで発行し、10 分後に購読したダッシュボードでも、次の発行を待つことなく直近の値を受け取れます。同じトピックで再発行すると保持された値が上書きされ、空のペイロードを発行するとそれがクリアされます。

最終遺言。 クライアントは接続時にブローカーに「最終遺言(last will and testament)」を渡すことができます。これはトピック、ペイロード、QoS、保持フラグから成ります。そのクライアントが 正常でない形で 切断された場合、つまり TCP RESET、電源喪失、DISCONNECT パケットなしのネットワーク断などが起きた場合、ブローカーはそのクライアントに代わって遺言を発行します。サブスクライバーはそれを、カメラがオフラインになったという通知として受け取ります。カメラ自身は決して遺言を送りません。送るのはブローカーです。なぜなら、その時点ではカメラはもういないからです。

9.18.6. キープアライブと再接続

CONNECT は キープアライブ 間隔を秒単位で運びます。クライアントがその時間だけ無音であれば、ブローカーはそれを死んだものとみなします。これを防ぐため、クライアントは定期的に PINGREQ(1 バイト: 0xC0)を送り、PINGRESP(0xD0)を受け取ります。これはプロトコルが運べる最小かつ最も安価なハートビートです。ほとんどのカメラアプリはキープアライブを 30 秒または 60 秒に設定します。

TCP 接続が切れると、両側がそれに気づいて一から再接続します。接続時にクライアントが 永続セッション を使っていない限り、切断前に行ったサブスクリプションは失われます。単純なカメラアプリでは、再接続時に再購読するパターンの方が短く、同じくらい有効です。

これだけ分かれば、MQTT 仕様を読んだり、socket.socket 上で自前のクライアントを手作りしたりできます。mqtt の付属クライアントはまさにそれを行い、加えてアプリケーションコード向けの理にかなった API を提供します。