前回、固定ヘッダーを表す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

