いろいろ備忘録日記

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

Goメモ-559 (UNIXドメインソケットの抽象名前空間)(syscall版)

関連記事

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

概要

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

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

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

以下の特徴を持ちます。

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

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

今回は syscall 版。

サンプル

main.go

package main

import (
    "bytes"
    "errors"
    "flag"
    "log"
    "os"
    "os/signal"
    "syscall"
)

type (
    Args struct {
        IsServer bool
    }
)

const (
    serverAddr = "@go_unix_domain_socket_test"
    bufSize    = len("hello")
    backLog    = 1
)

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 {
    log.SetFlags(log.Lmicroseconds)

    var err error
    switch args.IsServer {
    case true:
        err = runServer()
    default:
        err = runClient()
    }

    if err != nil {
        return err
    }

    return nil
}

func runServer() error {
    fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
    if err != nil {
        return err
    }
    defer syscall.Close(fd)

    addr := &syscall.SockaddrUnix{
        Name: "\x00" + serverAddr[1:], // 抽象名前空間のソケットは、パス名の先頭にNULバイト(\0)を付けることで識別される
    }

    err = syscall.Bind(fd, addr)
    if err != nil {
        return err
    }

    err = syscall.Listen(fd, backLog)
    if err != nil {
        return err
    }

    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
    go func(sigCh <-chan os.Signal) {
        <-sigCh
        syscall.Close(fd)
        os.Exit(0)
    }(sigCh)

    cfd, caddr, err := syscall.Accept(fd)
    if err != nil {
        return err
    }
    defer syscall.Close(cfd)

    unixAddr, ok := caddr.(*syscall.SockaddrUnix)
    if ok {
        // 「Connect from @」と出力されるが、これは正常な挙動。
        //   1.Unixドメインソケットの抽象名前空間では、クライアント側が明示的にバインドしていない場合、カーネルが自動的にアドレスを割り当てる
        //   2.この自動割り当てされたアドレスは通常表示されず、単に@または空の文字列として見えることがある.
        log.Printf("[S] Connect from %s", unixAddr.Name)
    }

    buf := make([]byte, bufSize)
    for {
        clear(buf)

        n, err := syscall.Read(cfd, buf)
        if err != nil {
            if errors.Is(err, syscall.EINTR) {
                continue
            }

            return err
        }

        if n == 0 {
            log.Println("[S] disconnect")
            break
        }

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

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

    return nil
}

func runClient() error {
    fd, err := syscall.Socket(syscall.AF_UNIX, syscall.SOCK_STREAM, 0)
    if err != nil {
        return err
    }
    defer syscall.Close(fd)

    addr := &syscall.SockaddrUnix{Name: "\x00" + serverAddr[1:]}
    err = syscall.Connect(fd, addr)
    if err != nil {
        return err
    }

    sigCh := make(chan os.Signal, 1)
    signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
    go func(sigCh <-chan os.Signal) {
        <-sigCh
        syscall.Close(fd)
        os.Exit(0)
    }(sigCh)

    buf := []byte("hello")
    _, err = syscall.Write(fd, buf)
    if err != nil {
        return err
    }
    log.Printf("[C] Send (%s)", string(buf))

    buf = make([]byte, bufSize)
    n, err := syscall.Read(fd, buf)
    if err != nil {
        if errors.Is(err, syscall.EINTR) {
            return nil
        }

        return err
    }

    if n == 0 {
        log.Println("[C] disconnect")
        return nil
    }
    log.Printf("[C] Recv (%s)", string(buf[:n]))

    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
08:23:30.473291 [S] Connect from @
08:23:30.473439 [S] Recv (hello)
08:23:30.473446 [S] Send (HELLO)
08:23:30.473435 [C] Send (hello)
08:23:30.473604 [C] Recv (HELLO)
08:23:30.473618 [S] disconnect
task: [run] pkill -f './app -server' || true

参考情報

Goのおすすめ書籍


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

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