前回、固定ヘッダーを表す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
オプションをつけてるとコードの解析もしてくれる。ただし、起動に時間がかかる...。
もう一方の go doc
(goコマンド+docオプション)ですが、こちらはコマンドラインでGo Docが確認できる。
結構色々あるみたい。こちらの記事が参考になる。
おしまい
FixedHeader
に ReminingLength
フィールドを追加しました。しかし、 ToFixedHeader
に渡す []byte
のチェックをしてないので1バイトの配列やnilを渡すとpanicしてしまいます。次回はここのエラーハンドリングから考えることにします。
今回の学び
- MQTTの固定ヘッダーのRemining Length
- go vet
- gofmt
- go doc