😽

Golangのhttptest.NewServer()の実装を調べてみた

2023/04/16に公開

1. httptest.NewServer()とは

Golangで手軽にWebサーバーを立てることができる関数です。
例えば以下のように記述することで、指定したhandlerの処理を実装したWebサーバーを起動できます。

// このhandlerはrequestの内容を出力している
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	dump, err := httputil.DumpRequest(r, false)
	if err != nil {
		fmt.Fprintln(w, err)
	}
	fmt.Fprintln(w, string(dump))
})
ts := httptest.NewServer(handler)
defer ts.Close()

そしてこのWebサーバーにアクセスするためのURLはts.URLから文字列型で取得できます。
つまりこれをhttp.Get(ts.URL)のようにするだけで簡単にリクエストを送れます。

具体的にts.URLの値を見てみると127.0.0.1:35889となっていました(portの値は実行するごとに変化します)。
よくhttpのリクエストを送るときにも自動で空いているportが使われてたりするのですが、それをGoではどのように実装されているのでしょうか?

実装を見てみる

この関数が実装されているのはhttptestパッケージのserver.goです。
ソースコードはここから確認できます。

具体的にNewServer()の実装を見てみましょう。ソースコードは以下になります。

// NewServer starts and returns a new Server.
// The caller should call Close when finished, to shut it down.
func NewServer(handler http.Handler) *Server {
	ts := NewUnstartedServer(handler)
	ts.Start()
	return ts
}

まずNewUnstartedServer(handler)でサーバーの初期化を実施しています。
そして初期化したサーバーtsを開始して返却しています。

まずは初期化の処理を見てみます。

func NewUnstartedServer(handler http.Handler) *Server {
	return &Server{
		Listener: newLocalListener(),
		Config:   &http.Server{Handler: handler},
	}
}

これはhttptestパッケージ内のServer構造体のポインタ型に値を代入して返却しています。
ここで非公開のnewLocalListener()が呼ばれています。この処理の実装も見てみましょう。

func newLocalListener() net.Listener {
	if serveFlag != "" {
		l, err := net.Listen("tcp", serveFlag)
		if err != nil {
			panic(fmt.Sprintf("httptest: failed to listen on %v: %v", serveFlag, err))
		}
		return l
	}
	l, err := net.Listen("tcp", "127.0.0.1:0")
	if err != nil {
		if l, err = net.Listen("tcp6", "[::1]:0"); err != nil {
			panic(fmt.Sprintf("httptest: failed to listen on a port: %v", err))
		}
	}
	return l
}

上のコードの処理は以下のような処理になっています。

  • もしserveFlagが設定されていたらそのportを使ってnet.Listen()を実行して得たnet.Listenerを返却する
  • 値が設定されていない場合は127.0.0.1:0に対してnet.Listen()を実行して得たnet.Listenerを返却する

serveFlaggo testコマンドを実行するときのオプションにhttptest.serve=127.0.0.1:8080のように実行する値を具体的に指定したいときに使われます。
もし指定されていない場合は127.0.0.1:0に対してListenしているのですが、これは具体的なポートを指定していませんが、これによって動的に値を取得できているということになります。
これはどのような処理によって取得できているのでしょうか?
調べた結果わかったのはエファメラルポートを使用しているのではということです。

エファメラルポートの利用

今回はLinuxについてのみ調べました。
Linuxがportを割り当てる際にbindシステムコールを実行します。
このシステムコールが実行されるときにport番号が0だと、Linuxカーネルが開いているportを探して動的に割り当ててバインドしてくれます。
どのポートに割り当てられるかの範囲は/proc/sys/net/ipv4/ip_local_port_rangeに書かれています。

つまり先ほどのnewLocalListener()ではport0番を設定することでbindが動的に空いてるポートを設定してくれていることによってWebサーバーを空いたportに起動できるということでした。

まとめ

今回内部実装を見ることでなぜ動的に割り当てられているかがわかりました。
ただ具体的にどの行がbindを呼び出しているのかなどはわかっていないのでまた今後の課題にします。

追伸

そしてこの内容を調べている中でシステムコールを使ってWebサーバーをGraceful Restartできるような設定を見つけたので、この内容についても調べてみようと思います。

参考

UDPおよびTCPカーネル・パラメータの手動設定

Discussion