MQTTサーバー実装でGoを学ぶ - その1

2018年はGoとMQTTデビューをしたので、学んだことの振り返りも兼ねて、GoでMQTTサーバーを実装してみます、という日記です。

今回の学ぶこと。

  • mosquitto_pubした時の通信
  • MQTTの固定ヘッダーの構造
  • Goのテスト
  • Goのビット演算

MQTTの仕様を確認

実装してみるのは、MQTTのv3.1.1。

MQTT Version 3.1.1

MQTTの通信でやりとりされるパケットを、MQTT Control Packetと呼ぶ。MQTT Control Packetにはいくつか種類があり、後述する固定ヘッダーの MQTT Control Packet type で判別できる。

MQTT Control Packetは以下の3つの部分で構成される。

  • 固定ヘッダー
    • 2バイト
    • 必須
  • 可変ヘッダー
    • MQTT Control Packet typeによって異なる
  • ペイロード
    • MQTT Control Packet typeによって異なる

MQTTのパケットを確認

MosquittoというMQTTサーバーを使ってどういうパケットがやりとりされてるか確認してみる。

インストール。

$ brew install mosquitto

サーバーを起動。

$ mosquitto -v
   1535406563: mosquitto version 1.5.1 starting
   1535406563: Using default config.
   1535406563: Opening ipv6 listen socket on port 1883.
   1535406563: Opening ipv4 listen socket on port 1883.

Wiresharkでパケットキャプチャする。

Mosquittoをインストールすると、MQTTクライアントとして使えるコマンドもついて来るので、Publishしてみる。

$ mosquitto_pub -t hoge -m "Hello"

Wiresharkで確認。以下のように通信が行われていることが分かる。

  1. クライアント → Connect Command → サーバー
  2. クライアント ← Connect Ack ← サーバー
  3. クライアント → Publish Message → サーバー
  4. クライアント → Disconnect Req → サーバー

f:id:bati11:20181011195454p:plain

自分で実装したMQTTサーバーとMosquittoクライアントで上の通信ができるようにしよう!

固定ヘッダーの実装

まずは固定ヘッダーから。

下調べ

MQTT Control Packetの固定ヘッダーを実装する。

固定ヘッダーの仕様を調べる。

  • 2byte
  • 全ての種類のControl Packetにある
  • 以下のようなフォーマットになってる
    Bit 7 6 5 4 3 2 1 0
    byte1 MQTT Control Packet type Flags specific to each MQTT Control Packet type
    byte2... Remaining Length

WiresharkでConnect Commandを詳しく見てみる。

16進数とASCIIで表現されたバイト列は以下。最初の2バイト 10 23 が固定ヘッダー。

0000   10 23 00 04 4d 51 54 54 04 02 00 3c 00 17 6d 6f   .#..MQTT...<..mo
0010   73 71 70 75 62 7c 37 31 33 33 35 2d 6f 2d 31 30   sqpub|11111-a-22
0020   38 39 33 2d 6d                                    222-b

Wiresharkが上のバイト列をMQTTとして解釈して構造化してくれたのが以下。最初の1バイト目が Header Flagsで、2バイト目がMsg Len。

MQ Telemetry Transport Protocol, Connect Command
    Header Flags: 0x10, Message Type: Connect Command
        0001 .... = Message Type: Connect Command (1)
        .... 0000 = Reserved: 0
    Msg Len: 35
    Protocol Name Length: 4
    Protocol Name: MQTT
    Version: MQTT v3.1.1 (4)
    Connect Flags: 0x02, QoS Level: At most once delivery (Fire and Forget), Clean Session Flag
        0... .... = User Name Flag: Not set
        .0.. .... = Password Flag: Not set
        ..0. .... = Will Retain: Not set
        ...0 0... = QoS Level: At most once delivery (Fire and Forget) (0)
        .... .0.. = Will Flag: Not set
        .... ..1. = Clean Session Flag: Set
        .... ...0 = (Reserved): Not set
    Keep Alive: 60
    Client ID Length: 23
    Client ID: mosqpub|11111-a-22222-b

FixedHeaderの実装とGoのテスト

固定ヘッダーを表す FixedHeader というstructを実装する。MQTT Control Packetの種類を表す PacketType フィールドを持たせる。

Goで数値を表す型はいくつかある。PacketTypeは0から15のいずれかなので、1番小さいサイズで良いので byte 型を使うことにする( byteuint8 のalias)。

// fixed_header.go
package packet

type FixedHeader struct {
    PacketType byte
}

固定ヘッダーは2バイト。 []byte を引数で受け取って FixedHeader を返す ToFixedHeader 関数を作る。

// fixed_header.go
package packet

type FixedHeader {
    PacketType byte
}

func ToFixedHeader(bs []byte) FixedHeader {
    return FixedHeader{}
}

テストを書いてみる。この記事がすごく参考になります。

  • Goのtestを理解する in 2018 #go - My External Storage
    • Goのテストコードは同じディレクトリに _test というサフィックスをつけた .go ファイルに書く
      • $ go test の時だけビルド対象となる
    • テストは Test というプレフィックスをつけた関数名で書く。引数は *testing.T
    • パッケージはテスト対象と同じパッケージでもいいけど、 _test というサフィックスをつけたパッケージにしておく
      • 別パッケージにすることで、テスト対象のプライベートな変数や関数に依存したテストを書くのを防げる
// fixed_header_test.go
package packet_test

func TestPacketType(t *testing.T) {}

GoだとTable Driven Testという書き方で書くといい。Table Driven Testで FixedHeader のテストを書く。

ちなみに、JetBrains製品使ってる場合はテスト対象の関数内にカーソルがある状態で ⌘+Shift+T 、VSCodeなら Go: Generate Unit Tests For Function するとTable Driven Testな雛形のテストを生成してくれて便利。

package packet_test

import (
    "github.com/bati11/oreno-mqtt/study/packet"
    "reflect"
    "testing"
)

func TestToFixedHeader(t *testing.T) {
    type args struct {
        bs []byte
    }
    tests := []struct {
        name string
        args args
        want packet.FixedHeader
    }{
        {
            "Reserved",
            args{[]byte{0x00, 0x00}},
            packet.FixedHeader{0},
        },
        {
            "CONNECT",
            args{[]byte{0x10, 0x00}},
            packet.FixedHeader{1},
        },
        {
            "CONNACK",
            args{[]byte{0x20, 0x00}},
            packet.FixedHeader{2},
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            if got := packet.ToFixedHeader(tt.args.bs); !reflect.DeepEqual(got, tt.want) {
                t.Errorf("ToFixedHeader() = %v, want %v", got, tt.want)
            }
        })
    }
}

テストを実行すると失敗する。実行の仕方は色々ある。

$ go test ./packet/fixed_header_test.go 
--- FAIL: TestToFixedHeader (0.00s)
    --- FAIL: TestToFixedHeader/CONNECT (0.00s)
        fixed_header_test.go:37: ToFixedHeader() = {0}, want {1}
    --- FAIL: TestToFixedHeader/CONNACK (0.00s)
        fixed_header_test.go:37: ToFixedHeader() = {0}, want {2}
FAIL
FAIL    command-line-arguments  0.018s

ビット演算

Goの演算子について。

MQTTの仕様を見ると、固定ヘッダーの1バイト目の上位4ビットがMQTT Control Packet type。 ToFixedHeader を書き換える。

func ToFixedHeader(bs []byte) FixedHeader {
    b := bs[0]
    packetType := b >> 4
    return FixedHeader{packetType}
}
$ go test ./packet/fixed_header_test.go 
ok      command-line-arguments  1.241s

テストが通った!

ちなみに、 -v をオプションでつけると成功した場合もログが出力される。

$ go test -v ./packet/fixed_header_test.go 
=== RUN   TestToFixedHeader
=== RUN   TestToFixedHeader/CONNECT
=== RUN   TestToFixedHeader/CONNACK
--- PASS: TestToFixedHeader (0.00s)
    --- PASS: TestToFixedHeader/CONNECT (0.00s)
    --- PASS: TestToFixedHeader/CONNACK (0.00s)
PASS
ok    command-line-arguments  0.018s

おしまい

次はMQTT固定ヘッダーの続きから。

今回の学び