Golangのhttptest.NewServer()の実装を調べてみた
httptest.NewServer()
とは
1. 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
を返却する
serveFlag
はgo 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できるような設定を見つけたので、この内容についても調べてみようと思います。
Discussion