いろいろ備忘録日記

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

Goメモ-290 (ソケットに SO_REUSEPORT を設定してListenerを起動)(net.ListenConfig)

概要

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

ソケットに SO_REUSEPORT を設定して、同一のEndPointに対して複数のリスナーをbindできるようにするサンプルです。

これをするためには、bind前にソケットオプションを指定する必要があります。

Go 1.11 から、net.ListenConfig という構造体が追加されていて、この子を使うと bind 前の状態のソケットを触ることが出来ます。ただ、ドキュメント見ても正直使い方が良く分からない感じなので、メモ代わりにリポジトリ作っておきました。

github.com

以下は、上のリポジトリの内容の抜粋です。

準備

Linuxで確認しますので、以下を go get しておきます。(ソケットオプションの定数などが定義されている)

$ go get golang.org/x/sys/unix@latest

ソース

サーバ側

net.ListenConfigControl フィールドに setSockOpt という関数ポインタを設定しています。

この中でソケットオプションを設定しています。通信部分は適当です。自分の番号をストリームに書いてブチッとソケットを切断しちゃってます。

// This is example to Use SO_REUSEPORT by Golang.
//
// net.ListenConfig の Control フィールドに指定する func には
// 「OS側にバインドされる前」のコネクションが渡される。
//
// なので、バインド前に設定しないと行けないソケットオプションなどは
// このタイミングで指定することが出来る。
//
// # REFERENCES:
//     - https://christina04.hatenablog.com/entry/go-so-reuseport
//     - https://pkg.go.dev/net@go1.20#ListenConfig

package main

import (
    "context"
    "fmt"
    "net"
    "os"
    "os/signal"
    "syscall"

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

func main() {
    if err := run(); err != nil {
        panic(err)
    }
}

func run() error {
    var (
        listenCtl = net.ListenConfig{
            Control: setSockOpt,
        }
        listener net.Listener
        sigCh    = make(chan os.Signal, 1)
        myNo     = os.Args[1]
        err      error
    )

    listener, err = listenCtl.Listen(context.Background(), "tcp4", ":9999")
    if err != nil {
        return err
    }

    signal.Notify(sigCh, os.Interrupt)
    go func() {
        defer listener.Close()
        <-sigCh
        fmt.Println("shutdown..." + myNo)
    }()

    for {
        var (
            conn    net.Conn
            connErr error
        )

        conn, connErr = listener.Accept()
        if connErr != nil {
            return err
        }

        go func(c net.Conn) {
            defer c.Close()
            c.Write([]byte(myNo))
        }(conn)
    }
}

func setSockOpt(network string, address string, rc syscall.RawConn) error {
    var (
        sockOpErr error
        sockOpFn  = func(fd uintptr) {
            sockOpErr = unix.SetsockoptInt(int(fd), unix.SOL_SOCKET, unix.SO_REUSEPORT, 1)
        }
        err error
    )

    err = rc.Control(sockOpFn)
    if err != nil {
        return err
    }

    if sockOpErr != nil {
        return sockOpErr
    }

    return nil
}

クライアント

こっちは普通の処理です。

package main

import (
    "fmt"
    "net"
)

func main() {
    if err := run(); err != nil {
        panic(err)
    }
}

func run() error {
    var (
        conn net.Conn
        buf  = make([]byte, 6)
        err  error
    )

    conn, err = net.Dial("tcp4", ":9999")
    if err != nil {
        return err
    }
    defer conn.Close()

    conn.Read(buf)

    fmt.Printf("RESPONSE FROM: %s\n", string(buf))

    return nil
}

タスクファイル

version: '3'

tasks:
  build:
    - task: clean
    - go build -o bin/server cmd/server/main.go
    - go build -o bin/client cmd/client/main.go
  server:
    cmds:
      - cmd: bin/server {{.CLI_ARGS}}
        ignore_error: true
  client:
    cmds:
      - cmd: for i in {1..10}; do bin/client; done
        ignore_error: true
  ss:
    cmds:
      - cmd: ss -atn | grep -F "0.0.0.0:9999"
        ignore_error: true
  close:
    cmds:
      - pkill server
  clean:
    cmds:
      - rm -rf bin

ビルド

$ task build
task: [clean] rm -rf bin
task: [build] go build -o bin/server cmd/server/main.go
task: [build] go build -o bin/client cmd/client/main.go

実行

ターミナルを一つ開いて、以下を実行します。

$ task server -- 1

これで 9999 番ポート にリスナーが一つひも付きます。

通常は、もう一つ同じポートに紐づけるのは無理なのですが、SO_REUSEPORT を設定しているので複数行けるようになっています。

もうひとつ、ターミナルを開いて、以下を実行します。

$ task server -- 2

エラーにならずに起動してくれます。

もうひとつターミナルを開いて、LISTEN状態を見てみます。

$ task ss
task: [ss] ss -atn | grep -F "0.0.0.0:9999"
LISTEN    0      128                0.0.0.0:9999                  0.0.0.0:*
LISTEN    0      128                0.0.0.0:9999                  0.0.0.0:*

ちゃんと2ついますね。

てことで、クライアント側を複数起動してみて、どのようになるかを見てみます。

$ task client
task: [client] for i in {1..10}; do bin/client; done
RESPONSE FROM: 2
RESPONSE FROM: 1
RESPONSE FROM: 1
RESPONSE FROM: 1
RESPONSE FROM: 2
RESPONSE FROM: 1
RESPONSE FROM: 1
RESPONSE FROM: 2
RESPONSE FROM: 1
RESPONSE FROM: 1

2つのサーバから応答が返ってきてますね。偏りなどについてはランダム性があるのか無いのかは良くわからないですが、均等とかでは無さそう。

Graceful に Restart を行いたい際などに使えます。

参考情報

kuniyu.jp

ja.tak-cslab.org

hana-shin.hatenablog.com

nigaky.hatenablog.com

ryuichi1208.hateblo.jp

learn.microsoft.com

Goのおすすめ書籍

Go言語による並行処理

Go言語による並行処理

Amazon


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

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