読者対象
- Goの基本的な文法を知っている
- リクエストがあると、関数が実行される仕組みを知りたい
- GoのWebApplicationFrameworkを使いこなしたい
なぜやるのか?
普段、Webアプリケーションを作っているとリクエストがあると対応するパスに応じて関数を実行させる必要がありますが、
WAFを使っていると内部でどのような処理がされてそうなっているのか知らずに動かせてしまいます。
しかし、正しくWebアプリケーションフレームワークを使うこなすには、内部の実装についての理解が必要なので、この機会にnet/httpパッケージのDocumentを見ながら、リクエストがあると関数が実行されるまでの仕組みを調べました。特に、http.Handle
とかhttp.HandleFunc
とか似たような名前があって一見こんがらがりますが、Documentに簡潔に説明があったのでそこまで苦労することなく調べられました。
何をやるのか?
以下のようなWebアプリケーションフレームワークを使わなかった場合に、実装される一般的なHTTPハンドラがどのように呼び出される調べた。
Webアプリケーションフレームワークを使った場合func(w http.ResponseWriter, req *http.Request)
のようなhttp.HandleFuncインターフェースすら実装する必要はないのだが、
結局内部的には、Goのnet/httpパッケージの仕組みを使ってこれらのHTTPハンドラを作っているにすぎない。
package main
import (
"net/http"
)
func main() {
// ①
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
// ②
http.ListenAndServe(":8080", nil)
}
概要
これが調べた全てと言って過言では無い。
Documentを見ても個別のメソッドの説明はあったものの、それらの関係を構造化したものがなかったのでまとめた。
よく出てくる型(前提)
まず、説明するにあたってよく出てくる二つの型について理解しておく。
リクエストに応じる呼び出されるハンドラーが満たすべきインターフェース。
この後で追っていくが、登録されたハンドラーが実行される時はServeHTTPメソッドが呼び出される。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
2. HandlerFunc関数型
func(ResponseWriter, *Request)
を別の型として定義したものであり、HTTPハンドラーのように使えるようにするための関数型。
また、この関数型はServeHTTPを持つ。
// https://golang.org/src/net/http/server.go?s=59707:59754#L1950
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
よくある使われ方としては、以下のような関数をHandlerFunc型にキャストすることで、HandlerFunc型のServeHTTPメソッドを付与することができる。
HandlerFunc(func(ResponseWriter, *http.Request))
これによって、上記のHandlerインターフェースを関数が満たすことができる。
リクエストされてからハンドラーが呼び出されるまで
最初のコードでは、最後にhttp.ListenAndServe
にポートと、nilを渡している。
package main
import (
"net/http"
)
func main() {
...
http.ListenAndServe(":8080", nil)
}
これは、アドレスとHandlerインターフェース型を構造体に持って、http.Server.ListenAndServe
を呼び出している。
これによって、HTTPサーバーを指定したアドレスでListenして、リクエストがあると登録したハンドラー(Handlerインターフェース型)を呼び出す。つまり、ServeHTTP
が呼び出される。
このハンドラーがnilの場合はDefaultServeMux
が使われる。DefaultServeMux
は後で詳しく見てみる。
ListenAndServe starts an HTTP server with a given address and handler. The handler is usually nil, which means to use DefaultServeMux. Handle and HandleFunc add handlers to DefaultServeMux:
// https://golang.org/src/net/http/server.go?s=93229:93284#L2992
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
そして、このhttp.Server.ListenAndServe
はその名前の通り指定ポートをリッスンして、リクエストがあるとhttp.Server.Serve
を呼び出す。
// https://golang.org/src/net/http/server.go?s=85391:85432#L2742
func (srv *Server) ListenAndServe() error {
...
addr := srv.Addr
...
ln, err := net.Listen("tcp", addr)
...
return srv.Serve(tcpKeepAliveListener{ln.(*net.TCPListener)})
}
http.Serve
では、指定ポートにConnection確立要求がくると呼び出されるhttp.Serve
は何をしているかと言うと、
それをAcceptし、リクエストに応じてハンドラー(Handlerインターフェース型を満たすのでServeHTTPを実装している)を呼び出すためのgoroutineを作る。
内部的には、http.conn.serve
を呼び出し、それが予めListenAndServe
で登録されたDefaultServeMux
のServeHTTP
を呼び出している。
// https://golang.org/src/net/http/server.go?s=87455:87501#L2795
func (srv *Server) Serve(l net.Listener) error {
....
baseCtx := context.Background() // base is always background, per Issue 16220
ctx := context.WithValue(baseCtx, ServerContextKey, srv)
for {
rw, e := l.Accept()
...
c := srv.newConn(rw)
c.setState(c.rwc, StateNew) // before Serve can return
go c.serve(ctx) // ここ
}
}
// src/net/http/server.go#L1739
// Serve a new connection.
func (c *conn) serve(ctx context.Context) {
...
for {
w, err := c.readRequest(ctx)
...
// HTTP cannot have multiple simultaneous active requests.[*]
// Until the server replies to this request, it can't read another,
// so we might as well run the handler in this goroutine.
// [*] Not strictly true: HTTP pipelining. We could let them all process
// in parallel even if their responses need to be serialized.
// But we're not going to implement HTTP pipelining because it
// was never deployed in the wild and the answer is HTTP/2.
serverHandler{c.server}.ServeHTTP(w, w.req)
...
}
}
まとめると、ListenAndServe
で指定ポートをbind, listen, acceptし、リクエストに応じてgoroutineを作り、DefaultServeMux
のServeHTTP
を呼び出している。
そのDefaultServeMux
とはServeMux
構造体のポインタが格納された変数のこと。
デフォルトで上記http.Serve
の作ったgoroutineから呼び出される。
Mux
とはマルチプレクサの略。マルチプレクサとは、二つ以上の入力を一つの信号として出力する機構のことらしい。
// src/net/http/server.go#L2164
// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
では、このServeMux構造体は何かと言うと、URLのパターンマッチをして、もっとも登録されたパスに近いハンドラーを呼び出す。
パスのパターンマッチには、様々なルールがあり、様々なURLのパターンとハンドラーの呼び出しロジックの定義を担っている様子。
例えば、長いパスの方が優先されて、それに対応するハンドラーが呼び出されたり、"/"で終わるサブツリーのパスの方が優先されたりなど。
そして、http.ServeMuxは、Handlerインターフェース(ServeHTTPを実装)を満たしている。
これが、パスにもっとも近いHandlerインターフェースを実装した関数を呼び出す。
// https://golang.org/src/net/http/server.go?s=71923:71983#L2342
// ServeHTTP dispatches the request to the handler whose
// pattern most closely matches the request URL.
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
...
// https://golang.org/pkg/net/http/#ServeMux.Handler
// http.ServeMux.Handlerによりリクエストに応じて登録されているハンドラーを選ぶ
h, _ := mux.Handler(r)
// そしてそのハンドラーのServeHTTPが呼び出される
// つまり、このハンドラーはHandlerインターフェースを満たしている
h.ServeHTTP(w, r)
}
パスにマッチした、Handlerに対してServeHTTPを呼び出している。
内部的には、http.ServeMux.matchを使ってパスからHandlerを見つけている。
// src/net/http/server.go##L2342
// handler is the main implementation of Handler.
// The path is known to be in canonical form, except for CONNECT methods.
func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
mux.mu.RLock()
defer mux.mu.RUnlock()
// Host-specific pattern takes precedence over generic ones
if mux.hosts {
h, pattern = mux.match(host + path)
}
if h == nil {
h, pattern = mux.match(path)
}
if h == nil {
h, pattern = NotFoundHandler(), ""
}
return
}
以下のように、http.ServeMux構造体に登録されているパスと、ハンドラーを返している。
// src/net/http/server.go##L2218
// Find a handler on a handler map given a path string.
// Most-specific (longest) pattern wins.
func (mux *ServeMux) match(path string) (h Handler, pattern string) {
// Check for exact match first.
v, ok := mux.m[path]
if ok {
return v.h, v.pattern
}
...
}
まとめると、http.ServeMux.Handler
を呼び出すし、登録されているハンドラーのServeHTTPメソッドを実行する。
ではそのハンドラーは、http.HandleFunc
からどうやって登録されていたのかを次に追ってみる。
ハンドラーを登録するまで
package main
import (
"net/http"
)
func main() {
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})
...
}
HandlerFunc型のハンドラーとパスをDefaultServeMux
に登録する。
これは内部的にhttp.ServeMux.HanlderFunc
を呼び出すショートカットにすぎない。
// https://golang.org/src/net/http/server.go?s=73427:73498#L2396
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
http.ServeMux.HandleFunc
受け取った関数がHandlerFunc型にキャストしてHandlerインターフェースを満たすようにして、
http.ServeMux.Handle
を呼び出す。
// https://golang.org/src/net/http/server.go?s=72834:72921#L2381
// HandleFunc registers the handler function for the given pattern.
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
// handlerをHandlerFunc関数型にキャストしてServeHTTPを付与している(Handlerインターフェースを満たしている)
mux.Handle(pattern, HandlerFunc(handler))
}
http.ServeMux.Handle
受け取ったHandlerをパスと対応させてmap[string]muxEntry型の値に登録する。
// https://golang.org/src/net/http/server.go?s=72291:72351#L2356
mux.m[pattern] = muxEntry{h: handler, pattern: pattern}
内部的には、map[string]muxEntry型の値にパス(string)とHandler型の関数をmuxEntry構造体でラップして対応づけている。
// https://golang.org/src/net/http/server.go?s=66347:66472#L2139
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
hosts bool // whether any patterns contain hostnames
}
type muxEntry struct {
h Handler
pattern string
}
http.Handle
ちなみに、http.Handle
と言うものもあるが、これもhttp.ServeMux.Handle
を内部的に呼び出すショートカットにすぎない。
Handler型のハンドラーとパスをDefaultServeMux
に登録する。
// https://golang.org/src/net/http/server.go?s=73173:73217#L2391
func Handle(pattern string, handler Handler) { DefaultServeMux.Handle(pattern, handler) }
所感
- どのように動いているか想像して普段の実装ができそう
- インターフェースに依存した実装なので読みやすかった