いろいろ備忘録日記

主に .NET とか Go とか Flutter とか Python絡みのメモを公開しています。

Goメモ-563 (net.Connとselectシステムコール)

関連記事

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

参考情報

Goのおすすめ書籍


過去の記事については、以下のページからご参照下さい。

サンプルコードは、以下の場所で公開しています。