MQTTサーバーを実装しながらGoを学ぶ - その2 go vet, gofmt, go doc
前回、固定ヘッダーを表すstructの実装に着手しました。
// fixed_header.go package packet type FixedHeader { PacketType byte } func ToFixedHeader(bs []byte) FixedHeader { b := bs[0] packetType := b >> 4 return FixedHeader{packetType} }
ここからの続きです。Goでの開発で便利そうなgo vet, gofmtといったコマンドも試してみます。
目次。
MQTT固定ヘッダー
MQTTの固定ヘッダーは以下のようなフォーマットになってた。
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 |
MQTT Control Packet type
を実装したので、次は Flags specific to each MQTT Control Packet type
に着手する。その後、 Remaining Length
。
Flags
ここはサクッと。
fixed_header_test.go
want packet.FixedHeader }{ { - "Reserved", - args{[]byte{0x00, 0x00}}, - packet.FixedHeader{0}, + "Reserved Dup:0 QoS:00 Retain:0", + args{[]byte{0x00, 0x00}}, // 0000 0 00 0 + packet.FixedHeader{PacketType: 0, Dup: 0, QoS1: 0, QoS2: 0, Retain: 0}, }, { - "CONNECT", - args{[]byte{0x10, 0x00}}, - packet.FixedHeader{1}, + "CONNECT Dup:1 QoS:01 Retain:1", + args{[]byte{0x1B, 0x00}}, // 0001 1 01 1 + packet.FixedHeader{PacketType: 1, Dup: 1, QoS1: 0, QoS2: 1, Retain: 1}, }, { - "CONNACK", - args{[]byte{0x20, 0x00}}, - packet.FixedHeader{2}, + "CONNACK Dup:0 QoS:10 Retain:1", + args{[]byte{0x24, 0x00}}, // 0002 0 10 0 + packet.FixedHeader{PacketType: 2, Dup: 0, QoS1: 1, QoS2: 0, Retain: 0}, }, } for _, tt := range tests {
fixed_header.go
type FixedHeader struct { PacketType byte + Dup byte + QoS1 byte + QoS2 byte + Retain byte } func ToFixedHeader(bs []byte) FixedHeader { b := bs[0] packetType := b >> 4 - return FixedHeader{packetType} + dup := refbit(bs[0], 3) + qos1 := refbit(bs[0], 2) + qos2 := refbit(bs[0], 1) + retain := refbit(bs[0], 0) + return FixedHeader{ + PacketType: packetType, + Dup: dup, + QoS1: qos1, + QoS2: qos2, + Retain: retain, + } +} + +func refbit(b byte, n uint) byte { + return (b >> n) & 1 }
Remining Length
次は、固定ヘッダーの2バイト目が表しているRemining Length。Remining Lengthは、固定ヘッダーに続く「可変ヘッダー」「ペイロード」のサイズが合計で何バイトなのかを示す。
ドキュメントにencodeとdecodeのアルゴリズムが書いてある。
ドキュメントを参考にテストコードを修正。
fixed_header_test.go
want packet.FixedHeader }{ { - "Reserved Dup:0 QoS:00 Retain:0", - args{[]byte{0x00, 0x00}}, // 0000 0 00 0 - packet.FixedHeader{PacketType: 0, Dup: 0, QoS1: 0, QoS2: 0, Retain: 0}, + "Reserved Dup:0 QoS:00 Retain:0 RemainingLength:0", + args{[]byte{ + 0x00, // 0000 0 00 0 + 0x00, // 0 + }}, + packet.FixedHeader{PacketType: 0, Dup: 0, QoS1: 0, QoS2: 0, Retain: 0, RemainingLength: 0}, }, { - "CONNECT Dup:1 QoS:01 Retain:1", - args{[]byte{0x1B, 0x00}}, // 0001 1 01 1 - packet.FixedHeader{PacketType: 1, Dup: 1, QoS1: 0, QoS2: 1, Retain: 1}, + "CONNECT Dup:1 QoS:01 Retain:1 RemainingLength:127", + args{[]byte{ + 0x1B, // 0001 1 01 1 + 0x7F, // 127 + }}, + packet.FixedHeader{PacketType: 1, Dup: 1, QoS1: 0, QoS2: 1, Retain: 1, RemainingLength: 127}, }, { - "CONNACK Dup:0 QoS:10 Retain:1", - args{[]byte{0x24, 0x00}}, // 0002 0 10 0 - packet.FixedHeader{PacketType: 2, Dup: 0, QoS1: 1, QoS2: 0, Retain: 0}, + "CONNACK Dup:0 QoS:10 Retain:1 RemainingLength:128", + args{[]byte{ + 0x24, // 0002 0 10 0 + 0x80, 0x01, //128 + }}, + packet.FixedHeader{PacketType: 2, Dup: 0, QoS1: 1, QoS2: 0, Retain: 0, RemainingLength: 128}, }, } for _, tt := range tests {
コンパイルが通るように修正。
fixed_header.go
package packet type FixedHeader struct { - PacketType byte - Dup byte - QoS1 byte - QoS2 byte - Retain byte + PacketType byte + Dup byte + QoS1 byte + QoS2 byte + Retain byte + RemainingLength uint } func ToFixedHeader(bs []byte) FixedHeader {
テスト実行。
$ go test ./packet/fixed_header_test.go --- FAIL: TestToFixedHeader (0.00s) --- FAIL: TestToFixedHeader/CONNECT_Dup:1_QoS:01_Retain:1_RemainingLength:127 (0.00s) fixed_header_test.go:47: ToFixedHeader() = {1 1 0 1 1 0}, want {1 1 0 1 1 127} --- FAIL: TestToFixedHeader/CONNACK_Dup:0_QoS:10_Retain:1_RemainingLength:128 (0.00s) fixed_header_test.go:47: ToFixedHeader() = {2 0 1 0 0 0}, want {2 0 1 0 0 128} FAIL FAIL command-line-arguments 0.019s
まだ実装してないので失敗。ドキュメントを参考にdecodeする処理を追加。
fixed_header.go
qos1 := refbit(bs[0], 2) qos2 := refbit(bs[0], 1) retain := refbit(bs[0], 0) + remainingLength := decodeRemainingLength(bs[1:]) return FixedHeader{ - PacketType: packetType, - Dup: dup, - QoS1: qos1, - QoS2: qos2, - Retain: retain, + PacketType: packetType, + Dup: dup, + QoS1: qos1, + QoS2: qos2, + Retain: retain, + RemainingLength: remainingLength, } } func refbit(b byte, n uint) byte { return (b >> n) & 1 } + +func decodeRemainingLength(bs []byte) uint { + multiplier := uint(1) + var value uint + i := uint(0) + for ; i < 8; i++ { + b := bs[i] + digit := b + value = value + uint(digit&127)*multiplier + multiplier = multiplier * 128 + if (digit & 128) == 0 { + break + } + } + return value +}
テスト実行。
$ go test ./packet/fixed_header_test.go ok command-line-arguments 2.739s
OKOK。
テストケースの name
を書くのが面倒なのでリファクタリング。
fmt.Sprintf
を使って、 want
で指定してるstructをテストケースの name
の代わりに使う。
diff --git a/study/packet/fixed_header_test.go b/study/packet/fixed_header_test.go index 2670823..ed086a2 100644 --- a/study/packet/fixed_header_test.go +++ b/study/packet/fixed_header_test.go @@ -1,6 +1,7 @@ package packet_test import ( + "fmt" "reflect" "testing" @@ -12,12 +13,10 @@ func TestToFixedHeader(t *testing.T) { bs []byte } tests := []struct { - name string args args want packet.FixedHeader }{ { - "Reserved Dup:0 QoS:00 Retain:0 RemainingLength:0", args{[]byte{ 0x00, // 0000 0 00 0 0x00, // 0 @@ -25,7 +24,6 @@ func TestToFixedHeader(t *testing.T) { packet.FixedHeader{PacketType: 0, Dup: 0, QoS1: 0, QoS2: 0, Retain: 0, RemainingLength: 0}, }, { - "CONNECT Dup:1 QoS:01 Retain:1 RemainingLength:127", args{[]byte{ 0x1B, // 0001 1 01 1 0x7F, // 127 @@ -33,7 +31,6 @@ func TestToFixedHeader(t *testing.T) { packet.FixedHeader{PacketType: 1, Dup: 1, QoS1: 0, QoS2: 1, Retain: 1, RemainingLength: 127}, }, { - "CONNACK Dup:0 QoS:10 Retain:1 RemainingLength:128", args{[]byte{ 0x24, // 0002 0 10 0 0x80, 0x01, //128 @@ -42,7 +39,7 @@ func TestToFixedHeader(t *testing.T) { }, } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + t.Run(fmt.Sprintf("%#v", tt.args.bs), 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) }
以下のようにテスト名に []byte{0x23,_0x80,_0x1}
というような感じで出力される。
$ go test -v ./packet/fixed_header_test.go === RUN TestToFixedHeader === RUN TestToFixedHeader/[]byte{0x0,_0x0} === RUN TestToFixedHeader/[]byte{0x1b,_0x7f} === RUN TestToFixedHeader/[]byte{0x24,_0x80,_0x1} --- PASS: TestToFixedHeader (0.00s) --- PASS: TestToFixedHeader/[]byte{0x0,_0x0} (0.00s) --- PASS: TestToFixedHeader/[]byte{0x1b,_0x7f} (0.00s) --- PASS: TestToFixedHeader/[]byte{0x24,_0x80,_0x1} (0.00s) PASS ok command-line-arguments 1.299s
go vet
fmt.Sprintf
はすごく便利なのだけど、例えば fmt.Sprinf("x is %v")
というように第2引数を指定し忘れていたとしてもコンパイルエラーにならず、ミスに気がつきにくい。
go vet
というコマンドを使うと、よくあるミスを指摘してくれる。
さっきのテストコードでわざと間違えてみる。
}, } for _, tt := range tests { - t.Run(fmt.Sprintf("%#v", tt.args.bs), func(t *testing.T) { + t.Run(fmt.Sprintf("%#v"), 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 vet ./packet/ # github.com/bati11/oreno-mqtt/packet_test packet/fixed_header_test.go:42: Sprintf format %#v reads arg #1, but call has 0 args
これは助かる!他にどんなチェックをしてくれるのかは以下のページを参照。
実はGo1.10からは go test
実行時に、vetの項目のうちのいくつかをチェックしてくれるようになったらしい。
試しに fmt.Sprintf
の第2引数を渡してない状態で go test
してみる。
$ go test ./packet/ # github.com/bati11/oreno-mqtt/packet_test packet/fixed_header_test.go:42: Sprintf format %#v reads arg #1, but call has 0 args FAIL github.com/bati11/oreno-mqtt/study/packet [build failed]
良い。
gofmt
お次はフォーマッター。
gofmt
コマンドとは別に go fmt
というのもある。何か歴史的な経緯があるのだろうか。ちなみに gofmt -l -w
と go fmt
の結果が同じになった。
$ gofmt --help ... -l list files whose formatting differs from gofmt's ... -w write result to (source) file instead of stdout
gofmt
は -d
オプションで実際にファイルをフォーマットせずに差分の出力だけすることができるのでCIでも使い勝手良さそう。
go doc
お次はドキュメンテーション。2通りある。
godoc
コマンドgo doc
というようにgoコマンドにdocを指定する方法の
まずは godoc
から。以下のように実行する。
$ godoc -http=:6060
ブラウザで http://localhost:6060
にアクセス。するといつものGoの画面が。
いつものと違って「Packages」自分が作ったパッケージが紛れてます。自分が作ったパッケージの他に、ローカルPCのGOPATHにあるパッケージも載ってます。
ドキュメントの書き方は以下の記事が参考になりそう。
起動時に $ godoc -http=:6060 -analysis=pointer -analysis=type
というように analysis
オプションをつけてるとコードの解析もしてくれる。ただし、起動に時間がかかる...。
- https://qiita.com/lufia/items/97acb391c26f967048f1#%E9%9D%99%E7%9A%84%E8%A7%A3%E6%9E%90
- Static analysis features of godoc - The Go Programming Language
もう一方の go doc
(goコマンド+docオプション)ですが、こちらはコマンドラインでGo Docが確認できる。
結構色々あるみたい。こちらの記事が参考になる。
おしまい
FixedHeader
に ReminingLength
フィールドを追加しました。しかし、 ToFixedHeader
に渡す []byte
のチェックをしてないので1バイトの配列やnilを渡すとpanicしてしまいます。次回はここのエラーハンドリングから考えることにします。
今回の学び
- MQTTの固定ヘッダーのRemining Length
- go vet
- gofmt
- go doc
MQTTサーバーを実装しながらGoを学ぶ - その1 Table Driven Test
2018年はGoとMQTTデビューをしたので、学んだことの振り返りも兼ねて、GoでMQTTサーバーを実装してみます、という日記です。
使用するGoのバージョンは1.10.7です。
MQTTとは以下のようばPubSubを実現するプロトコルです。詳細は実装しながら確認していきます。以下の図だと「Temperature Sensor」がPublisherとなるクライアント、「Computer」と「Mobile Device」がSubscriberとなるクライアントです。真ん中の「MQTT Broker」がサーバーでこれを実装していきます。
引用元:API Builder and MQTT for IoT - Part 1
目次。
MQTTの仕様を確認
実装してみるのは、MQTTのv3.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で確認。以下のように通信が行われていることが分かる。
クライアント → Connect Command → サーバー
クライアント ← Connect Ack ← サーバー
クライアント → Publish Message → サーバー
クライアント → Disconnect Req → サーバー
自分で実装した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の実装
固定ヘッダーを表す FixedHeader
というstructを実装する。MQTT Control Packetの種類を表す PacketType
フィールドを持たせる。
Goで数値を表す型はいくつかある。PacketTypeは0から15のいずれかなので、1番小さいサイズで良いので byte
型を使うことにする( byte
は uint8
の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でテスト - Table Driven Test
テストを書いてみる。この記事がすごく参考になります。
- Goのtestを理解する in 2018 #go - My External Storage
- Goのテストコードは同じディレクトリに
_test
というサフィックスをつけた.go
ファイルに書く$ go test
の時だけビルド対象となる
- テストは
Test
というプレフィックスをつけた関数名で書く。引数は*testing.T
- パッケージはテスト対象と同じパッケージでもいいけど、
_test
というサフィックスをつけたパッケージにしておく- 別パッケージにすることで、テスト対象のプライベートな変数や関数に依存したテストを書くのを防げる
- Goのテストコードは同じディレクトリに
// 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の演算子について。
- The Go Programming Language Specification - The Go Programming Language
- [Go] 算術シフトと論理シフト|tabatak|note
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固定ヘッダーの続きから。
今回の学び
- mosquitto_pubした時の通信
- MQTTの固定ヘッダーの構造
- Goのテスト
- Goのビット演算
Visual Studio Codeのコマンドをvscodevimでキーバインドする
VS CodeにGoのプラグインを追加するとテストの実行を支援してくれる機能が色々入ります。カーソルがあたってるテストケースを実行したり、テストファイルとテスト対象ファイルを切り替えたり。例えば、コマンドパレットで Go Test Function At Cursor
というコマンドを実行すると、カーソルがあたってるテストケースを実行できます。
でも、コマンドパレット開いて入力するの面倒くさい!もっと気軽に実行したい!!
というわけで、できるようにしてみます。
VS Codeのショートカットの設定をしてもいいのだけど、vscodevimを使ってるのでそちらでキーバインディングの設定をしたい。コマンドパレットを開いて Go Test Function At Cursor
を実行する代わりに、 \gt
と入力するだけで実行できるようにする。
以下のメニューを開く。
vscodevimのキーバインドで必要なコマンドID?を以下のように「Copy Command」で取得する。
setting.jsonに以下を記述。
"vim.normalModeKeyBindingsNonRecursive": [ { "before": ["<leader>", "g", "t"], "after": [], "commands": [ "go.test.cursor" // 「Copy Command」で取得したコマンドID ] }, ]
これでコマンドパレットを使わずに、 \gt
と入力するだけでカーソルがあたってるテストケースを実行することができた!直前に実行テストを実行するコマンドやテストファイルとテスト対象ファイルを切り替えるコマンドも簡単に実行できると捗りそうですね!