いろいろ備忘録日記

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

Goメモ-462 (golang.org/x/sys/unixを使ってソケット通信)(02-正規解放(Graceful Shutdown, Orderly Release))

関連記事

Flutterメモ-40 (dart pub unpack)(Dart 3.4で追加) - いろいろ備忘録日記

GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ

概要

以下、自分用のメモです。忘れないうちにメモメモ。。。

最近、ひょんなことで、golang.org/x/sys/unix を使ってソケット通信する処理をちょっと書いたので、ついでにここにメモ残しておこうと思いました。

syscall パッケージを使っても同じことが出来るのですが、少し呼び出すAPIが異なるかもしれません。

今回は、正規解放(Graceful Shutdown)のサンプル。

ソケットの正規解放って何?って方は、クライアント側のソースコードにコメントがありますので、良かったらご参考ください。

実務でも、ちゃんとこの正規解放していないコードって結構あったりします。通信処理は何が起こるか分からないものなので、出来るだけきっちり処理を書いた方が良いですね。一度動けばオッケイなサンプルなどで正規解放などは面倒なのでしなくても良いと思っています。

サンプル

サーバとクライアントを用意します。サンプルなので、1度のリクエスト・レスポンスで終わりです。

Client

//go:build linux

package main

import (
    "errors"
    "log"
    "net"

    "golang.org/x/sys/unix"
)

func init() {
    log.SetFlags(log.Lmicroseconds)
}

func main() {
    if err := run(); err != nil {
        log.Fatal(err)
    }
}

func run() error {
    var (
        sfd int
        err error
    )

    sfd, err = unix.Socket(unix.AF_INET, unix.SOCK_STREAM, unix.IPPROTO_TCP)
    if err != nil {
        return err
    }
    defer func() {
        log.Println("[CLIENT] ソケットクローズ")
        unix.Close(sfd)
    }()

    var (
        ip   = net.ParseIP("127.0.0.1")
        ipv4 [4]byte

        sAddr unix.Sockaddr
    )
    copy(ipv4[:], ip.To4())

    sAddr = &unix.SockaddrInet4{Port: 8888, Addr: ipv4}
    err = unix.Connect(sfd, sAddr)
    if err != nil {
        return err
    }

    log.Println("[CLIENT] Connect")

    //
    // Send
    //
    var (
        buf = make([]byte, 2048)
        msg = "helloworld"
    )
    copy(buf, []byte(msg))

    err = unix.Send(sfd, buf[:len(msg)], 0)
    if err != nil {
        return err
    }

    log.Printf("[CLIENT] SEND %s", msg)

    //
    // Recv
    //
    var (
        n int
    )
    clear(buf)

    n, err = unix.Read(sfd, buf)
    if err != nil {
        return err
    }

    log.Printf("[CLIENT] RECV %s", buf[:n])

    //
    // 正規解放 (Graceful Shutdown or Orderly Release)
    //
    // ソケットの正規解放とは、ソケット通信を適切に終了させ、リソースを解放するプロセスのことを指します。
    // これには通常、shutdownとcloseの2つの操作が含まれます。
    //
    // 1. Shutdown
    //   shutdownは通信相手に対して接続終了の意思を伝えます。
    //   例えば、SHUT_WRを使用すると、相手側にEOF(End of File)を送信します。
    //
    // 2. close
    //   closeはソケットのファイルディスクリプタを閉じ、関連するリソースを解放します。
    //   最後の参照が閉じられたときにのみ、ネットワークの端点を完全に解放します。
    //
    // 正規解放の手順
    //   1. shutdown(SHUT_WR) の呼び出し。これにより相手に送信停止を通知する。
    //   2. 必要に応じて、残りのデータを受信する。
    //   3. 最後に close を呼び出して、ソケットのリソースを完全に解放する。
    //
    // 正規解放を行うことで、ネットワーク通信を適切に終了し、リソースを効率的に管理することができます。
    // 特に信頼性の高い通信が必要な場合や、大規模なシステムでリソース管理が重要な場合に、この方法は有効です。
    //

    // 1. shutdown(SHUT_WR) の呼び出し。これにより相手に送信停止を通知する。
    //    つまり、相手側にEOFが送信される。「もうデータは送りません」という意思表示。
    err = unix.Shutdown(sfd, unix.SHUT_WR)
    if err != nil {
        return err
    }

    log.Println("[CLIENT] shutdown(SHUT_WR)")

    // 2. 必要に応じて、残りのデータを受信する。
LOOP:
    for {
        clear(buf)

        n, err = unix.Read(sfd, buf)
        switch {
        case n == 0:
            log.Println("[CLIENT] 切断検知 (0 byte read)")
            break LOOP
        case err != nil:
            var sysErr unix.Errno
            if errors.As(err, &sysErr); sysErr == unix.ECONNRESET {
                log.Printf("[CLIENT] 切断検知 (%s)", sysErr)
                break LOOP
            }

            return err
        default:
            log.Printf("[CLIENT] RECV REMAIN [%s]", buf[:n])
        }
    }

    // 3. 最後に close を呼び出して、ソケットのリソースを完全に解放する。
    // これは上の defer で行われている。

    return nil
}

Server

//go:build linux

package main

import (
    "errors"
    "log"
    "net"

    "golang.org/x/sys/unix"
)

func init() {
    log.SetFlags(log.Lmicroseconds)
}

func main() {
    if err := run(); err != nil {
        log.Fatal(err)
    }
}

func run() error {
    //
    // Create
    //
    var (
        sfd int
        err error
    )

    sfd, err = unix.Socket(unix.AF_INET, unix.SOCK_STREAM, unix.IPPROTO_TCP)
    if err != nil {
        return err
    }
    defer func() {
        log.Println("[SERVER] サーバーソケットクローズ")
        unix.Close(sfd)
    }()

    //
    // SO_REUSEADDR
    //
    err = unix.SetsockoptInt(sfd, unix.SOL_SOCKET, unix.SO_REUSEADDR, 1)
    if err != nil {
        return err
    }

    //
    // Bind and Listen
    //
    var (
        ip   = net.ParseIP("127.0.0.1")
        ipv4 [4]byte

        sAddr   unix.Sockaddr
        backLog = 2
    )
    copy(ipv4[:], ip.To4())

    sAddr = &unix.SockaddrInet4{Port: 8888, Addr: ipv4}
    err = unix.Bind(sfd, sAddr)
    if err != nil {
        return err
    }

    err = unix.Listen(sfd, backLog)
    if err != nil {
        return err
    }

    //
    // Accept
    //
    var (
        cfd   int
        cAddr unix.Sockaddr
    )

    cfd, cAddr, err = unix.Accept(sfd)
    if err != nil {
        return err
    }
    defer func() {
        log.Println("[SERVER] パケット送受信用ソケットクローズ")
        unix.Close(cfd)
    }()

    cAddrInet4 := cAddr.(*unix.SockaddrInet4)
    log.Printf("[SERVER] Connect from %v:%v", cAddrInet4.Addr, cAddrInet4.Port)

    //
    // Recv
    //
    var (
        buf = make([]byte, 2048)
        n   int
    )

    n, err = unix.Read(cfd, buf)
    if err != nil {
        return err
    }

    log.Printf("[SERVER] RECV %s", string(buf[:n]))

    //
    // Send
    //
    var (
        msg = "HELLOWORLD "
    )

    for range 5 {
        clear(buf)
        copy(buf, []byte(msg))

        err = unix.Send(cfd, buf[:len(msg)], 0)
        if err != nil {
            return err
        }

        log.Printf("[SERVER] SEND %s", buf[:len(msg)])
    }

    // 1. shutdown(SHUT_WR) の呼び出し。これにより相手に送信停止を通知する。
    err = unix.Shutdown(cfd, unix.SHUT_WR)
    if err != nil {
        return err
    }

    log.Println("[SERVER] shutdown(SHUT_WR)")

    // 2. 必要に応じて、残りのデータを受信する。
LOOP:
    for {
        clear(buf)

        n, err = unix.Read(cfd, buf)
        switch {
        case n == 0:
            log.Println("[SERVER] 切断検知 (0 byte read)")
            break LOOP
        case err != nil:
            var sysErr unix.Errno
            if errors.As(err, &sysErr); sysErr == unix.ECONNRESET {
                log.Printf("[SERVER] 切断検知 (%s)", sysErr)
                break LOOP
            }

            return err
        default:
            log.Printf("[SERVER] RECV %s", buf[:n])
        }
    }

    // 3. 最後に close を呼び出して、ソケットのリソースを完全に解放する。
    // これは上の defer で行われている。

    return nil
}

Taskfile

実行用に以下のようなタスクファイルを用意

# https://taskfile.dev

version: '3'

tasks:
  default:
    cmds:
      - task: run
  fmt:
    cmds:
      - goimports -w .
  prepare:
    cmds:
      - mkdir -p bin
  build:
    deps: [ fmt ]
    cmds:
      - go build -o bin/server server/server.go
      - go build -o bin/client client/client.go
  run:
    deps: [ build ]
    cmds:
      - ./bin/server &
      - sleep 1
      - ./bin/client
      - sleep 1
      - pgrep server && pkill server
    ignore_error: true
  clean:
    cmds:
      - rm -rf ./bin

実行

task: [fmt] goimports -w .
task: [build] go build -o bin/server server/server.go
task: [build] go build -o bin/client client/client.go
task: [run] ./bin/server &
task: [run] sleep 1
task: [run] ./bin/client
17:17:14.152034 [CLIENT] Connect
17:17:14.152369 [CLIENT] SEND helloworld
17:17:14.152221 [SERVER] Connect from [127 0 0 1]:43708
17:17:14.152644 [SERVER] RECV helloworld
17:17:14.152686 [SERVER] SEND HELLOWORLD 
17:17:14.152689 [CLIENT] RECV HELLOWORLD 
17:17:14.152717 [SERVER] SEND HELLOWORLD 
17:17:14.152725 [SERVER] SEND HELLOWORLD 
17:17:14.152733 [SERVER] SEND HELLOWORLD 
17:17:14.152741 [SERVER] SEND HELLOWORLD 
17:17:14.152751 [CLIENT] shutdown(SHUT_WR)
17:17:14.152771 [CLIENT] RECV REMAIN [HELLOWORLD HELLOWORLD HELLOWORLD HELLOWORLD ]
17:17:14.152775 [SERVER] shutdown(SHUT_WR)
17:17:14.152784 [CLIENT] 切断検知 (0 byte read)
17:17:14.152787 [SERVER] 切断検知 (0 byte read)
17:17:14.152788 [CLIENT] ソケットクローズ
17:17:14.152796 [SERVER] パケット送受信用ソケットクローズ
17:17:14.152818 [SERVER] サーバーソケットクローズ
task: [run] sleep 1
task: [run] pgrep server && pkill server

参考情報

Goメモ-428 (flaggyメモ)(01) - いろいろ備忘録日記

Goメモ-429 (flaggyメモ)(02) - いろいろ備忘録日記

Goメモ-430 (flaggyメモ)(03) - いろいろ備忘録日記

Goのおすすめ書籍


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

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