関連記事
GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ
概要
以下、自分用のメモです。使いたいときに忘れているので、ここにメモメモ。。。
Goで通信処理を実装する際、netパッケージを利用することでソケットの存在が抽象化され、とても使いやすいです。
ですが、たまに直接ソケットレベルの操作を行いたいときもあったりします。今回は selectシステムコールを呼び出すサンプル。
golang.org/sys/unix を使って、select(2)を呼び出して、対象となるソケットファイルディスクリプタが読み取り可能な状態であるかを確認して処理するサンプルです。
syscallでも同様のことが出来ますが、golang.org/sys/unixのほうが FD_SET用の構造体などが定義されており扱いやすいです。
サンプル
fd.go
golang.org/sys/unix 経由でselect(2)を呼び出す処理
package main import ( "errors" "fmt" "time" "golang.org/x/sys/unix" ) // SocketFd は、ソケットファイルディスクリプタを表します。 type SocketFd int // Readable は、select(2)を呼び出し読み込み可能かどうかを判定します。 func (me SocketFd) Readable(sec, usec time.Duration) (bool, error) { fd := int(me) if fd < 0 || fd >= unix.FD_SETSIZE { return false, fmt.Errorf("invalid file descriptor: out of range %d (FD_SETSIZE = %d)", fd, unix.FD_SETSIZE) } rfds := &unix.FdSet{} rfds.Zero() rfds.Set(fd) timeout := &unix.Timeval{ Sec: int64(sec.Seconds()), Usec: usec.Microseconds(), } n, err := unix.Select(fd+1, rfds, nil, nil, timeout) if err != nil { if errors.Is(err, unix.EINTR) { return false, nil } return false, err } return n > 0 && rfds.IsSet(fd), nil }
main.go
package main import ( "errors" "flag" "io" "log" "net" "time" ) type ( Args struct { IsServer bool } ) var ( args Args ) func init() { flag.BoolVar(&args.IsServer, "server", false, "server mode") } func main() { log.SetFlags(log.Lmicroseconds) flag.Parse() if err := run(); err != nil { panic(err) } } func run() error { var err error switch args.IsServer { case true: err = runServer() default: err = runClient() } if err != nil { return err } return nil } func runServer() error { ln, err := net.Listen("tcp", ":8888") if err != nil { return err } defer ln.Close() errCh := make(chan error) defer close(errCh) for { select { case e := <-errCh: return e default: } err = ln.(*net.TCPListener).SetDeadline(time.Now().Add(1 * time.Second)) if err != nil { return err } conn, err := ln.Accept() if err != nil { if netErr, ok := err.(net.Error); ok && netErr.Timeout() { continue } return err } go func(conn net.Conn) { defer func() { conn.Close() log.Println("[S] close") }() time.Sleep(100 * time.Millisecond) { log.Println("[S] send data") if _, err := conn.Write([]byte("hello")); err != nil { errCh <- err } buf := make([]byte, 1) for { _, err := conn.Read(buf) if err != nil { if errors.Is(err, io.EOF) { log.Println("[S] disconnect") break } errCh <- err } } } }(conn) } } func runClient() error { conn, err := net.Dial("tcp", ":8888") if err != nil { return err } defer func() { conn.Close() log.Println("[C] close") }() // // select(2) を使って読み込み可能かを判定 // select(2)に指定するFDは、net.TCPConn.File() から取得する // tcpConn, _ := conn.(*net.TCPConn) file, err := tcpConn.File() if err != nil { return err } defer file.Close() var ( fd = SocketFd(file.Fd()) buf = make([]byte, 10) sec = 0 * time.Second usec = 10 * time.Millisecond ) for { readable, err := fd.Readable(sec, usec) if err != nil { return err } if !readable { log.Printf("[C] select(2) -- not readable(fd=%d)", int(fd)) continue } clear(buf) n, err := conn.Read(buf) if err != nil { if errors.Is(err, io.EOF) { log.Println("[C] disconnect") break } return err } log.Printf("[C] recv %s", buf[:n]) err = conn.(*net.TCPConn).CloseWrite() if err != nil { return err } log.Println("[C] shutdown(SHUT_WR)") break } return nil }
Taskfile.yml
# https://taskfile.dev version: '3' tasks: default: cmds: - task: build - task: run build: cmds: - go build -o app . run: cmds: - ./app -server & - sleep 0.5 - ./app - sleep 0.5 - pkill app ignore_error: true
実行
サーバはクライアントが接続してきたら、意図的に100ms待機し、その後にデータを送りつけます。
クライアント側は、接続したら10ms周期でselect(2)を呼び出し、読み取り可能状態であるかを確認します。
$ task task: [build] go build -o app . task: [run] ./app -server & task: [run] sleep 0.5 task: [run] ./app 10:01:00.206680 [C] select(2) -- not readable(fd=6) 10:01:00.216917 [C] select(2) -- not readable(fd=6) 10:01:00.227016 [C] select(2) -- not readable(fd=6) 10:01:00.237166 [C] select(2) -- not readable(fd=6) 10:01:00.247262 [C] select(2) -- not readable(fd=6) 10:01:00.257365 [C] select(2) -- not readable(fd=6) 10:01:00.267527 [C] select(2) -- not readable(fd=6) 10:01:00.277664 [C] select(2) -- not readable(fd=6) 10:01:00.287777 [C] select(2) -- not readable(fd=6) 10:01:00.296891 [S] send data 10:01:00.297322 [C] recv hello 10:01:00.297369 [C] shutdown(SHUT_WR) 10:01:00.297405 [C] close 10:01:00.297458 [S] disconnect 10:01:00.297523 [S] close task: [run] sleep 0.5 task: [run] pkill app
参考情報
- man select (2): 同期 I/O の多重化
- unix package - golang.org/x/sys/unix - Go Packages
- syscall package - syscall - Go Packages
Goのおすすめ書籍
過去の記事については、以下のページからご参照下さい。
サンプルコードは、以下の場所で公開しています。