関連記事
GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ
概要
以下、自分用のメモです。使いたいときに忘れているので、ここにメモメモ。。。
io.ReadFullという関数、皆さん使ってますか?
引数に指定したバッファ全部にデータが読み取られるまでブロッキングしてくれる関数です。通信処理と相性が良いです。
通信処理では、固定長ヘッダーと可変長データ部を持つ電文(古い人間なのでメッセージより電文の方がしっくり来ます)を処理したい場合
基本的な処理フローとして、まず最初にヘッダー部を読み取ることになります。大抵はその中にデータ長フィールドがある。
また、先頭Nバイトはシグネチャフィールドとなっている電文もあったりします。その場合、最初にその部分を読み取ったりします。
そういうときは、決まったサイズのデータをきっちり読み取る必要があります。読み取れなかったら、後続の処理に進むことが出来ない。
ですが、TCP/IPはストリーム型の通信プロトコルなので、常に決まった分量が受信出来るわけではありません。最初のrecvでは1バイト分しか読み取れないかもしれないし、一気に1024バイト読み取れるかもしれない。
上記のようなことがあるので、基本的に conn.Read() を行う部分は戻り値に「何バイト読み込めたのか」が返ってきますので、それを確認して必要な情報量を読み取れたかどうかをチェックすることになります。
こういうときに、io.ReadFullさんを利用するととても便利です。
データは受信出来ているけど、指定サイズ分読み取れなかった場合は io.ErrUnexpectedEOF をエラーとして返してくれます。
サンプル
面倒なので、1ファイルでサーバとクライアントを兼用しています。
main.go
package main import ( "errors" "flag" "io" "log" "net" "strings" "time" ) type ( Args struct { bufsize int length int timeout time.Duration } ) var ( args Args ) func init() { flag.IntVar(&args.bufsize, "bufsize", 4, "bufsize") flag.IntVar(&args.length, "length", 2, "length") flag.DurationVar(&args.timeout, "timeout", 1*time.Second, "timeout") } func main() { log.SetFlags(log.Lmicroseconds) flag.Parse() ln, err := net.Listen("tcp", ":8888") if err != nil { panic(err) } defer ln.Close() go func() { conn, err := ln.Accept() if err != nil { if errors.Is(err, net.ErrClosed) { return } panic(err) } defer conn.Close() buf := []byte(strings.Repeat("h", args.length)) _, err = conn.Write(buf) if err != nil { panic(err) } time.Sleep(500 * time.Millisecond) }() if err := run(); err != nil { panic(err) } } func run() error { conn, err := net.Dial("tcp", ":8888") if err != nil { return err } defer conn.Close() log.Println("[C] recv start") { buf := make([]byte, args.bufsize) for { clear(buf) err = conn.SetReadDeadline(time.Now().Add(args.timeout)) if err != nil { return err } n, err := io.ReadFull(conn, buf) if err != nil { switch { case errors.Is(err, io.EOF): log.Println("io.EOF") return nil case errors.Is(err, io.ErrUnexpectedEOF): log.Println("io.ErrUnexpectedEOF") return nil default: var netErr net.Error if errors.As(err, &netErr) && netErr.Timeout() { log.Println("netErr.Timeout()") return nil } } return err } log.Printf("[C] data=(%s)", buf[:n]) } } }
Taskfile.yml
# https://taskfile.dev version: '3' tasks: default: cmds: - task: build - task: run build: cmds: - go build -o app . run: cmds: - ./app -timeout 100ms - ./app -timeout 3s - ./app -length 10 -timeout 3s - ./app -bufsize 6 -length 12 -timeout 3s
実行
サーバはクライアントが接続してきたら、起動時引数のlengthバイト分のデータを送信します。
クライアント側は、起動時引数のbufsize分のバッファを作って、io.ReadFullをタイムアウトつきで呼び出し。
起動時引数の指定方法によって、io.EOF, io.ErrUnexpectedEOF, net.Error.Timeout() のパターンを見ることができます。
$ task task: [build] go build -o app . task: [run] ./app -timeout 100ms 07:54:29.313797 [C] recv start 07:54:29.414096 netErr.Timeout() task: [run] ./app -timeout 3s 07:54:29.422673 [C] recv start 07:54:29.923413 io.ErrUnexpectedEOF task: [run] ./app -length 10 -timeout 3s 07:54:29.930827 [C] recv start 07:54:29.931037 [C] data=(hhhh) 07:54:29.931064 [C] data=(hhhh) 07:54:30.431632 io.ErrUnexpectedEOF task: [run] ./app -bufsize 6 -length 12 -timeout 3s 07:54:30.439863 [C] recv start 07:54:30.439944 [C] data=(hhhhhh) 07:54:30.439959 [C] data=(hhhhhh) 07:54:30.940426 io.EOF
参考情報
Goのおすすめ書籍
過去の記事については、以下のページからご参照下さい。
サンプルコードは、以下の場所で公開しています。