関連記事
GitHub - devlights/blog-summary: ブログ「いろいろ備忘録日記」のまとめ
概要
以下、自分用のメモです。ちょっと試してみたので、ついでにここにメモメモ。。。
ひょんなことで、C言語でファイルディスクリプタパッシングをする処理をちょっと作ってたのですが、Goでも同じこと出来るかな?って思って少し試してみました。
sys/unix を使って実装。
ファイルディスクリプタパッシングって?
Unix/Linux系で通信処理とかを作っていない人にはちょっと馴染みがない概念と思います。
ファイルディスクリプタパッシング(FD passing)とは、あるプロセスから別のプロセスへ、既に開かれたファイルディスクリプタを転送する技術です。Unixシステムでは、ファイル、ソケット、パイプなどのリソースは、プロセス内でファイルディスクリプタという整数値で表現されています。
通常、各プロセスは独自のファイルディスクリプタテーブルを持ちますが、UnixドメインソケットのSCM_RIGHTS機能を使用することで、あるプロセスのファイルディスクリプタを別のプロセスに転送し、そのプロセスからも同じリソースにアクセスできるようにすることができます。
これを使うと、例えば ソケット を持っているプロセスから、全然関係ないプロセスにそのソケットを転送して、そっちで通信処理をやってもらうってことが出来るようになります。
言語を問わない機能なので、C言語で実装されたTCPサーバからGoで実装されたプロセスへディスクリプタパッシングして、Go側で通信処理を継続させるということも出来ます。
サンプル
fdpassing/fd.go
ファイルディスクリプタパッシングを行う処理です。
package fdpassing import ( "fmt" "net" "golang.org/x/sys/unix" ) // Fd はUnixドメインソケットを使用してファイルディスクリプタをパッシングするための構造体です。 // Unixドメインソケット接続をラップし、ファイルディスクリプタの送受信機能を提供します。 type Fd struct { conn *net.UnixConn } // NewFd は与えられたUnixドメインソケット接続から新しいFdインスタンスを作成します。 // このコネクションを通じてファイルディスクリプタの送受信が可能になります。 // // パラメータ: // - conn: ファイルディスクリプタの送受信に使用するUnixドメインソケット接続 // // 戻り値: // - 初期化されたFdインスタンスへのポインタ func NewFd(conn *net.UnixConn) *Fd { fd := new(Fd) fd.conn = conn return fd } // Send はファイルディスクリプタをUnixドメインソケット経由で送信します。 // SCM_RIGHTS機能を使用して、プロセス間でファイルディスクリプタを転送します。 // // パラメータ: // - fd: 送信するファイルディスクリプタ // // 戻り値: // - エラーが発生した場合はエラー、成功した場合はnil func (me *Fd) Send(fd int) error { var ( dummy = make([]byte, 1) rights = unix.UnixRights(fd) err error ) _, _, err = me.conn.WriteMsgUnix(dummy, rights, nil) if err != nil { return err } return nil } // Recv はUnixドメインソケット経由でファイルディスクリプタを受信します。 // 送信側から送られたSCM_RIGHTS制御メッセージを解析し、ファイルディスクリプタを取得します。 // // 戻り値: // - 受信したファイルディスクリプタ // - エラーが発生した場合はエラー、成功した場合はnil // エラーの場合、ファイルディスクリプタは-1が返されます func (me *Fd) Recv() (int, error) { var ( dummy = make([]byte, 1) oob = make([]byte, unix.CmsgSpace(4)) flags int err error ) _, _, flags, _, err = me.conn.ReadMsgUnix(dummy, oob) if err != nil { return -1, err } if flags&unix.MSG_TRUNC != 0 { return -1, fmt.Errorf("control message is truncated") } var ( msgs []unix.SocketControlMessage ) msgs, err = unix.ParseSocketControlMessage(oob) if err != nil { return -1, err } if len(msgs) != 1 { return -1, fmt.Errorf("want: 1 control message; got: %d", len(msgs)) } var ( fds []int ) fds, err = unix.ParseUnixRights(&msgs[0]) if err != nil { return -1, err } if len(fds) != 1 { return -1, fmt.Errorf("want: 1 fd; got: %d", len(fds)) } return fds[0], nil }
tcpserver/main.go
クライアントからの接続を受け付けて、Acceptしたソケットをファイルディスクリプタパッシングで転送します。
package main import ( "errors" "log" "net" "time" "github.com/devlights/fdpassing" ) func main() { log.SetFlags(log.Lmicroseconds) if err := run(); err != nil { panic(err) } } func run() error { var ( udsConn net.Conn err error ) for range 3 { udsConn, err = net.DialTimeout("unix", "@tcp_fd_passing", 1*time.Second) if err != nil { var netErr net.Error if errors.As(err, &netErr); netErr.Timeout() { continue } return err } break } defer udsConn.Close() log.Println("[TCP-S] connect uds-server") ln, err := net.Listen("tcp", ":8888") if err != nil { return err } defer ln.Close() log.Println("[TCP-S] tcp-listen on :8888") for { errCh := make(chan error, 1) func() { conn, err := ln.Accept() if err != nil { errCh <- err return } defer func() { conn.Close() log.Println("[TCP-S] close") }() log.Println("[TCP-S] accept client") var ( unixConn = udsConn.(*net.UnixConn) tcpConn = conn.(*net.TCPConn) file, _ = tcpConn.File() fd = fdpassing.NewFd(unixConn) ) err = fd.Send(int(file.Fd())) if err != nil { errCh <- err return } log.Printf("[TCP-S] passing fd=%d to uds-server", file.Fd()) errCh <- nil }() err = <-errCh if err != nil { return err } } }
udsserver/main.go
UNIXドメインソケットサーバを起動して、ファイルディスクリプタパッシングされたFDを受信する側です。
package main import ( "errors" "fmt" "io" "log" "net" "os" "github.com/devlights/fdpassing" ) func main() { log.SetFlags(log.Lmicroseconds) if err := run(); err != nil { panic(err) } } func run() error { ln, err := net.Listen("unix", "@tcp_fd_passing") if err != nil { return err } defer ln.Close() log.Println("[UDS-S] uds-server listening on") udsConn, err := ln.Accept() if err != nil { return err } defer udsConn.Close() log.Printf("[UDS-S] %v", udsConn.RemoteAddr()) unixConn, ok := udsConn.(*net.UnixConn) if !ok { return fmt.Errorf("not net.UnixConn") } fd, err := fdpassing.NewFd(unixConn).Recv() if err != nil { return err } log.Printf("[UDS-S] recv fd=%d", fd) file := os.NewFile(uintptr(fd), "client-socket") if file == nil { return fmt.Errorf("os.NewFile() failed") } defer file.Close() conn, err := net.FileConn(file) if err != nil { return fmt.Errorf("net.FileConn() failed") } defer func() { conn.Close() log.Println("[UDS-S] close") }() buf := []byte("hello") _, err = conn.Write(buf) if err != nil { return err } log.Printf("[UDS-S] send (%s)", buf) buf = make([]byte, 5) n, err := conn.Read(buf) if err != nil { switch { case errors.Is(err, io.EOF): log.Println("[UDS-S] disconnect") default: return err } } log.Printf("[UDS-S] recv (%s)", buf[:n]) tcpConn, _ := conn.(*net.TCPConn) tcpConn.CloseWrite() log.Println("[UDS-S] shutdown(SHUT_WR)") return nil }
tcpclient/main.go
TCPクライアントとしてサーバに接続して通信処理を行います。
package main import ( "bytes" "errors" "io" "log" "net" "time" ) func main() { log.SetFlags(log.Lmicroseconds) if err := run(); err != nil { panic(err) } } func run() error { conn, err := net.Dial("tcp", ":8888") if err != nil { return err } defer func() { conn.Close() log.Println("[TCP-C] close") }() log.Println("[TCP-C] connect tcp-server") buf := make([]byte, 5) n, err := conn.Read(buf) if err != nil { switch { case errors.Is(err, io.EOF): return nil default: return err } } msg := buf[:n] log.Printf("[TCP-C] recv (%s)", msg) msg = bytes.ToUpper(msg) _, err = conn.Write(msg) if err != nil { return err } log.Printf("[TCP-C] send (%s)", msg) buf = make([]byte, 1) for { conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond)) _, err = conn.Read(buf) if err != nil { if errors.Is(err, io.EOF) { log.Println("[TCP-C] disconnect") break } return err } } return nil }
Taskfile.yml
# https://taskfile.dev version: '3' tasks: default: cmds: - task: build - task: run build: cmds: - go build -o tcp-client tcpclient/main.go - go build -o tcp-server tcpserver/main.go - go build -o uds-server udsserver/main.go run: cmds: - ./uds-server & - sleep 1 - ./tcp-server & - sleep 1 - ./tcp-client - pkill tcp-server - pkill uds-server ignore_error: true
実行
ちゃんと、TCPサーバ側で持ってるFDが転送されて、FD受信したプロセスで通信処理が継続されていますね。
$ task task: [build] go build -o tcp-client tcpclient/main.go task: [build] go build -o tcp-server tcpserver/main.go task: [build] go build -o uds-server udsserver/main.go task: [run] ./uds-server & task: [run] sleep 1 05:12:51.462037 [UDS-S] uds-server listening on task: [run] ./tcp-server & task: [run] sleep 1 05:12:52.475893 [UDS-S] @ 05:12:52.475846 [TCP-S] connect uds-server 05:12:52.476473 [TCP-S] tcp-listen on :8888 task: [run] ./tcp-client 05:12:53.489672 [TCP-S] accept client 05:12:53.489720 [TCP-S] passing fd=8 to uds-server 05:12:53.489742 [TCP-S] close 05:12:53.489650 [TCP-C] connect tcp-server 05:12:53.489781 [UDS-S] recv fd=7 05:12:53.489868 [UDS-S] send (hello) 05:12:53.489894 [TCP-C] recv (hello) 05:12:53.489945 [TCP-C] send (HELLO) 05:12:53.489955 [UDS-S] recv (HELLO) 05:12:53.489978 [UDS-S] shutdown(SHUT_WR) 05:12:53.489984 [TCP-C] disconnect 05:12:53.489990 [UDS-S] close 05:12:53.490041 [TCP-C] close task: [run] pkill tcp-server task: [run] pkill uds-server
参考情報
GitHub - devlights/fdpassing: A Go library for file descriptor passing
Goのおすすめ書籍
過去の記事については、以下のページからご参照下さい。
サンプルコードは、以下の場所で公開しています。