Unix domain socketをGoのWebアプリケーションで使用する方法です。tcpによるリッスンの情報はたくさんありますが、ソケットを介した方法があまりなかったので紹介します。
TL;DR
成果物は→kaneshin/playground/go/unixsocket
Unix domain socketの置き場
特に用意していない場合に、ソケットファイルの置き場を作成します。 /tmp
を経由するのはセキュリティの都合上よろしくないので/var/run
を経由するようにします。
/var/run
への追加はsystemd
の機能でテンポラリなディレクトリを作成するようにします。まずは/etc/tmpfiles.d
に設定ファイルを追加しておきます。
$ cat /etc/tmpfiles.d/gopher.conf d /var/run/gopher 0755 [UID] [GID] -
/var/run/gopher
というディレクトリを作成するようにしています。
※[UID], [GID]は各自で設定してください。
設定ファイルを登録して、再起動をかけます。
$ systemd-tmpfiles --create /etc/tmpfiles.d/gopher.conf $ systemctl daemon-reload
これでUnix domain socketの置く場所作成は完了です。
nginx configuration
ほぼ定型文です。今回は/var/run/gopher/go.sock
をリッスンします。
upstream backend { server unix:/var/run/gopher/go.sock; } server { listen 80; server_name go.example.com; root /var/www/html; location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_redirect off; proxy_pass http://backend/; } }
アプリケーションコード
GoでUnix domain socketをリッスンするのは非常に簡単です。
func main() { mux := http.NewServeMux() mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "<h1>It works!</h1>\n") }) mux.HandleFunc("/header", func(w http.ResponseWriter, r *http.Request) { b, err := json.MarshalIndent(r.Header, "", " ") if err != nil { fmt.Fprintf(w, err.Error()) } else { w.Header().Add("Content-Type", "application/json") fmt.Fprintf(w, "%v\n", string(b)) } })
net/http
が持つ ServerMux
にハンドラを登録したあと、ソケットを介してリッスンします。
listener, err := net.Listen("unix", sock) if err != nil { fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) } defer func() { if err := listener.Close(); err != nil { log.Println("Error:", err.Error()) } }() shutdown(listener) if err := http.Serve(listener, mux); err != nil { fmt.Fprintln(os.Stderr, err.Error()) os.Exit(1) } }
割り込みで終了のシグナルが来た場合にリスナーをクローズ (listener.Close()
) するようにしておきます。
func shutdown(listener net.Listener) { c := make(chan os.Signal, 2) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { s := <-c log.Println("Got signal:", s) if err := listener.Close(); err != nil { log.Println("Error:", err.Error()) } os.Exit(1) }() }
クローズしないとソケットファイルが生き残り続けます。
起動
go build
などでバイナリにして起動していただければそのまま動くと思います。
502 Bad Gateway
の文字が現れたときはエラーログを見てもらえればいいのですが、大抵はパーミッションによるエラーだと思います。
パーミッションの解決方法(例)
www-data
ユーザで実行することを仮定すると
$ cat /etc/tmpfiles.d/gopher.conf d /var/run/gopher 0755 www-data www-data -
としてsystemd
へ登録しなおし、その後にgo build
で作成したバイナリをwww-data
で起動してあげればそのまま動くはずです。
$ go build -o /tmp/bin $ sudo -u www-data /tmp/bin
おわりに
今回のコードは kaneshin/playground/go/unixsocket にあります。
毎回、「リッスン」を使用する度に思うのですが、「リッスン」は既に動詞なのに「リッスンする」という言葉を使用しないと日本語では伝わりにくいですよね。