概要
以下、自分用のメモです。忘れないうちにメモメモ。。。
ソケットに SO_REUSEPORT を設定して、同一のEndPointに対して複数のリスナーをbindできるようにするサンプルです。
これをするためには、bind前にソケットオプションを指定する必要があります。
Go 1.11 から、net.ListenConfig
という構造体が追加されていて、この子を使うと bind 前の状態のソケットを触ることが出来ます。ただ、ドキュメント見ても正直使い方が良く分からない感じなので、メモ代わりにリポジトリ作っておきました。
以下は、上のリポジトリの内容の抜粋です。
準備
Linuxで確認しますので、以下を go get しておきます。(ソケットオプションの定数などが定義されている)
$ go get golang.org/x/sys/unix@latest
ソース
サーバ側
net.ListenConfig
の Control
フィールドに 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 を行いたい際などに使えます。
参考情報
-
- 処理を書く際にとても参考になりました。勉強になりました。
-
- 各OS毎にとても詳しく説明されています。
Goのおすすめ書籍
過去の記事については、以下のページからご参照下さい。
サンプルコードは、以下の場所で公開しています。