on
GoのHTTPサーバーにおけるURLとハンドラの紐付けについて考える
Tags:
#GoGoのHTTPサーバーにおけるURLとハンドラの紐付けについて疑問に思ったので調べてみました。Goでは比較的簡単にHTTPサーバーのコードを書くことができます。簡単に書けるために裏でどのような処理が行われているかわからないこともあります。 今回はHTTPサーバーの実装でどのようにURLとハンドラが紐づけれれているかについて調べました。
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)
}
要約
- http.DefaultServeMuxというパッケージ変数があり、DefaultServeMuxはキーにURLパス、バリューにHandlerをとるmapをフィールドとして持っている
- http.HandleFuncでDefaultServeMuxのフィールドのmapにパスとそのハンドラを追加する
- http.ListenAndServeでサーバーを起動する
- http.ListenAndServeの第二引数がnilで実行された時はHTTPリクエストに対してDefaultServeMuxのServeHTTPメソッドを呼び出す
- DefaultServeMuxのServeHTTPメソッドではmapを検索し、HTTPリクエストのパスに対応するHandlerがあれば、HandlerのServeHTTPメソッドを呼び出す
HTTPサーバーの中身をみていく
Goのnet/http
パッケージを活用すると簡単にHTTPサーバーを構築することができます。
簡単なHTTPサーバーのコードは以下のような感じになります。
package main
import (
"fmt"
"html"
"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)
}
このHTTPサーバーでは/barのURLにリクエストがきたらHello "/bar"
という文字列を返すという単純なものです。
http.HabndleFuncでURLのパスとハンドラになる関数のセットを登録します。
実際にcurlで叩いてみると以下のようにリクエストが返ってきました。
$ curl localhost:8080/bar
Hello "/bar"
どのようにこのハンドラが呼び出されているのかさらに調べてみます。
先ほどの例では最終行で、ListenAndServe関数を呼び出していました。 この関数は中でhttp.Server構造体を構築し、そのServer構造体のListenAndServeメソッドを呼び出します。
// ListenAndServe listens on the TCP network address addr and then calls
// Serve with handler to handle requests on incoming connections.
// Accepted connections are configured to enable TCP keep-alives.
//
// The handler is typically nil, in which case the DefaultServeMux is used.
//
// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
server := &Server{Addr: addr, Handler: handler}
return server.ListenAndServe()
}
通常であれば、ListenAndServe関数は第二引数にHandler型の引数をとり、Server構造体のhandlerにセットしています。 Handlerは以下のようなインターフェース型です。
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
ServeHTTPのメソッドを持つものであればこのHandlerインターフェースを満たします。 リクエストがあった際にServer構造体のHandlerのServeHTTPを呼び出します。
試しに以下のコードを試してみます。
package main
import (
"fmt"
"net/http"
)
type MyInt int
func (m *MyInt) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, %d", *m)
}
func main() {
myInt := MyInt(100)
http.ListenAndServe(":8080", &myInt)
}
このコードでは最初にint型に対して、MyIntという名前をつけて、MyIntがHandlerインターフェースを満たすようにServeHTTPメソッドを定義します。 そして、main関数内でMyIntを初期化し、ListenAndServe関数に初期化したmyIntを渡します。
以下はこの関数を実行し、HTTPサーバーに対して、curlコマンドでリクエストを投げてみます。
$ curl localhost:8080/
Hello 100
$ curl localhost:8080/hoge
Hello 100
$ curl localhost:8080/bar
Hello 100
リクエストを投げてみると、期待通りHello 100の文字列が返ってきます。 リクエストを受け取るとServer構造体のHandlerフィールドのServeHTTPを呼び出すことが確認できます。
しかしながら、どのURLに対してリクエストを送っても、Hello 100が返ってきます。 では最初の本題に戻ります。どの部分でURLパスとハンドラの紐付けを行なっているのでしょうか。
ヒントはListenAndServe関数の第二引数をnilで呼び出したときににあります。 最初の例ようにListenAndServe関数の第二引数にnilを指定するとどうなるのでしょうか。
ListenAndServe関数を辿っていくと、以下の関数に行き着きます。 (http.ListenAndServe -> Server.ListenAndServe -> Server.Serve -> Conn.serve -> serverHandler.ServeHTTPという順で辿る)
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
関数内1行目handler := sh.srv.Handler
はListenAndServe関数で作成したServer構造体のフィールドHandlerを表しています。
最初の例のようにhttp.ListenAndServeの第二引数にnilを指定するとこの変数handlerはnilになり、関数の中でhandler = DefaultServeMux
が呼ばれます。
そのhandlerのServeHTTPコマンドが最終的に呼び出されます。
DefaultServeMux
はnet/httpパッケージ内のパッケージ変数として定義されています。
var DefaultServeMux = &defaultServeMux
var defaultServeMux ServeMux
DefaultServeMux
変数の正体はServeMux構造体のポインタです。
type ServeMux struct {
mu sync.RWMutex
m map[string]muxEntry
es []muxEntry // slice of entries sorted from longest to shortest.
hosts bool // whether any patterns contain hostnames
}
type muxEntry struct {
h Handler
pattern string
}
中略
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
if r.RequestURI == "*" {
if r.ProtoAtLeast(1, 1) {
w.Header().Set("Connection", "close")
}
w.WriteHeader(StatusBadRequest)
return
}
h, _ := mux.Handler(r)
h.ServeHTTP(w, r)
}
DefaultServeMuxのServeHTTP関数では最終的にHandlerメソッドを呼び出して、その返り値に対してServeHTTPメソッドが呼び出されています。
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
// CONNECT requests are not canonicalized.
if r.Method == "CONNECT" {
// If r.URL.Path is /tree and its handler is not registered,
// the /tree -> /tree/ redirect applies to CONNECT requests
// but the path canonicalization does not.
if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
}
return mux.handler(r.Host, r.URL.Path)
}
// All other requests have any port stripped and path cleaned
// before passing to mux.handler.
host := stripHostPort(r.Host)
path := cleanPath(r.URL.Path)
// If the given path is /tree and its handler is not registered,
// redirect for /tree/.
if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
}
if path != r.URL.Path {
_, pattern = mux.handler(host, path)
url := *r.URL
url.Path = path
return RedirectHandler(url.String(), StatusMovedPermanently), pattern
}
return mux.handler(host, r.URL.Path)
}
// 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
}
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
}
// Check for longest valid match. mux.es contains all patterns
// that end in / sorted from longest to shortest.
for _, e := range mux.es {
if strings.HasPrefix(path, e.pattern) {
return e.h, e.pattern
}
}
return nil, ""
}
辿っていくとmux.m[path]の関数を返していることがわかります。 DefaultServeMuxの中にはmap型があり、URLにマッチするHandlerをmapから取り出して、その関数のServeHTTP関数を呼び出していることになります。
このmap型はどのタイミングで初期化され、Handlerが登録されるのでしょうか。 その答えはhttp.HandleFuncにあります。
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)
}
http.HandleFuncの中身は以下のようになっています。
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
DefaultServeMux.HandleFunc(pattern, handler)
}
DefaultServeMuxのHandleFuncメソッドが呼び出されています。
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
mux.Handle(pattern, HandlerFunc(handler))
のHandlerFunc(handler)
ではhandlerをHandlerFunc型へと変換しています。。
var v float64 = 1.1
intValue := int(v)
type HandlerFunc func(ResponseWriter, *Request)
// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
HandlerFunc型はServeHTTPメソッドを持ちHandlerインターフェースを満たしています。
HandlerFunc型に型変換することによってHandlerインターフェースを満たすことになります。
mux.Handle(pattern, HandlerFunc(handler))
のなかでは以下の操作を行なっています。
func (mux *ServeMux) Handle(pattern string, handler Handler) {
mux.mu.Lock()
defer mux.mu.Unlock()
if pattern == "" {
panic("http: invalid pattern")
}
if handler == nil {
panic("http: nil handler")
}
if _, exist := mux.m[pattern]; exist {
panic("http: multiple registrations for " + pattern)
}
if mux.m == nil {
mux.m = make(map[string]muxEntry)
}
e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
if pattern[len(pattern)-1] == '/' {
mux.es = appendSorted(mux.es, e)
}
if pattern[0] != '/' {
mux.hosts = true
}
}
e := muxEntry{h: handler, pattern: pattern}
mux.m[pattern] = e
まとめると以下のようになります。
まとめ
- http.DefaultServeMuxというパッケージ変数があり、DefaultServeMuxはキーにURLパス、バリューにHandlerをとるmapをフィールドとして持っている
- http.HandleFuncでDefaultServeMuxのフィールドのmapにパスとそのハンドラを設定する
- http.ListenAndServeでサーバーを起動する
- http.ListenAndServeの第二引数がnilで実行された時はHTTPリクエストに対してDefaultServeMuxのServeHTTPメソッドを呼び出す
- DefaultServeMuxのServeHTTPメソッドではmapを検索し、HTTPリクエストのパスに対応するHandlerがあれば、HandlerのServeHTTPメソッドを呼び出す
長くなりましたが、自分の備忘録として残しておきます。