いろいろ備忘録日記

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

Goメモ-561 (UNIXドメインソケットの抽象名前空間)(netパッケージのConn版)

関連記事

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

概要

以下、自分用のメモです。

UNIXドメインソケットにはLinux版のみの機能として「抽象名前空間」(Abstract Namespace)という概念があります。

通常、UNIXドメインソケットを利用する場合は実体となるファイルが必要となりますが、抽象名前空間を用いる場合はファイルを作成する必要はありません。

以下の特徴を持ちます。

  • ソケットのアドレス(名前)の先頭にNULバイト(\0)を付ける
  • ファイルシステム上にソケットファイルを作成しない
  • プロセス終了時に自動的にクリーンアップされる

知ってると結構便利な概念ですので、ついでなのでGoでサンプル作ってみました。syscallパッケージを使って低レイヤーな処理で扱うこともできますし、netパッケージからでも利用できます。

今回は net パッケージの Conn を利用した版。

サンプル

main.go

/*
UNIXドメインソケット 抽象名前空間のサンプル(netパッケージのConnを利用する版)
*/
package main

import (
    "bytes"
    "errors"
    "flag"
    "fmt"
    "io"
    "log"
    "net"
    "os"
    "os/signal"
    "time"
)

type (
    Args struct {
        IsServer bool
    }
)

const (
    // 抽象名前空間のソケットアドレス(@記号で始まる名前は\0に変換される)
    serverAddr = "@go_unix_domain_socket_test"
    bufSize    = len("hello")
)

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("unix", serverAddr)
    if err != nil {
        return err
    }
    defer ln.Close()

    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, os.Interrupt)
    go func(sigCh <-chan os.Signal) {
        <-sigCh
        log.Println("[S] Shutdown...")
        ln.Close()
        os.Exit(0)
    }(sigCh)

    buf := make([]byte, bufSize)
    for {
        conn, err := ln.Accept()
        if err != nil {
            if errors.Is(err, net.ErrClosed) {
                return nil
            }

            return err
        }

        // サンプルなので1接続で占有状態とする
        errCh := make(chan error)
        go func() {
            defer close(errCh)
            defer func() {
                conn.Close()
                log.Println("[S] close")
            }()

            clear(buf)
            n, err := conn.Read(buf)
            if n == 0 || errors.Is(err, io.EOF) {
                log.Println("[S] disconnect")
                return
            }

            if err != nil {
                errCh <- err
            }

            message := buf[:n]
            log.Printf("[S] Recv (%s)", message)

            message = bytes.ToUpper(buf[:n])
            _, err = conn.Write(message)
            if err != nil {
                errCh <- err
            }
            log.Printf("[S] Send (%s)", message)

            // FIN待機用のタイムアウト設定
            err = conn.SetReadDeadline(time.Now().Add(2 * time.Second))
            if err != nil {
                log.Printf("[S] SetReadDeadline error: %v", err)
                errCh <- err
                return
            }

            for {
                clear(buf)
                if n, err = conn.Read(buf); n == 0 || errors.Is(err, io.EOF) {
                    log.Println("[S] disconnect")
                    break
                }

                if err != nil {
                    if netErr, ok := err.(net.Error); ok && netErr.Timeout() {
                        log.Println("[S] timeout (client FIN)")
                        break
                    }

                    errCh <- err
                    return
                }
            }
        }()

        err = <-errCh
        if err != nil {
            return err
        }
    }
}

func runClient() error {
    conn, err := net.Dial("unix", serverAddr)
    if err != nil {
        return err
    }
    defer func() {
        conn.Close()
        log.Println("[C] close")
    }()

    buf := make([]byte, bufSize)
    copy(buf, []byte("hello"))

    _, err = conn.Write(buf)
    if err != nil {
        return err
    }
    log.Printf("[C] Send (%s)", buf)

    clear(buf)
    n, err := conn.Read(buf)
    if n == 0 || errors.Is(err, io.EOF) {
        log.Println("[S] disconnect")
        return nil
    }
    if err != nil {
        return err
    }

    message := string(buf[:n])
    log.Printf("[C] Recv (%s)", message)

    // Graceful shutdown
    {
        unixConn, ok := conn.(*net.UnixConn)
        if !ok {
            return fmt.Errorf("conn.(*net.UnixConn) failed")

        }

        err = unixConn.CloseWrite()
        if err != nil {
            return err
        }
        log.Println("[C] SEND FIN (shutdown(SHUT_WR))")

        for {
            clear(buf)
            if n, err = conn.Read(buf); n == 0 || errors.Is(err, io.EOF) {
                log.Println("[C] disconnect")
                break
            }
        }
    }

    return nil
}

Taskfile.yml

# https://taskfile.dev

version: '3'

vars:
  APP_NAME: app

tasks:
  default:
    cmds:
      - task: build
      - task: run
  build:
    cmds:
      - go build -o {{.APP_NAME}} main.go
  run:
    cmds:
      - ./{{.APP_NAME}} -server &
      - sleep 1
      - ./{{.APP_NAME}}
      - pkill -f './{{.APP_NAME}} -server' || true
  clean:
    cmds:
      - rm -f ./{{.APP_NAME}}

実行

$ task
task: [build] go build -o app main.go
task: [run] ./app -server &
task: [run] sleep 1
task: [run] ./app
02:42:06.032066 [C] Send (hello)
02:42:06.032128 [S] Recv (hello)
02:42:06.032345 [S] Send (HELLO)
02:42:06.032365 [C] Recv (HELLO)
02:42:06.032378 [C] SEND FIN (shutdown(SHUT_WR))
02:42:06.032382 [S] disconnect
02:42:06.032410 [S] close
02:42:06.032413 [C] disconnect
02:42:06.032436 [C] close
task: [run] pkill -INT -f './app -server'
02:42:06.043557 [S] Shutdown...

参考情報

Goのおすすめ書籍


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

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