LoginSignup
227
154

More than 5 years have passed since last update.

【Go】net/httpパッケージを読んでhttp.HandleFuncが実行される仕組み

Last updated at Posted at 2018-09-14

読者対象

  • 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を見ても個別のメソッドの説明はあったものの、それらの関係を構造化したものがなかったのでまとめた。
スクリーンショット 2018-09-14 23.36.36.png

よく出てくる型(前提)

まず、説明するにあたってよく出てくる二つの型について理解しておく。

1. Handlerインターフェース

リクエストに応じる呼び出されるハンドラーが満たすべきインターフェース。
この後で追っていくが、登録されたハンドラーが実行される時は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

最初のコードでは、最後に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で登録されたDefaultServeMuxServeHTTPを呼び出している。

// 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を作り、DefaultServeMuxServeHTTPを呼び出している。

http.ServeMux構造体

その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.ServeHTTP

そして、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))
  })
  ...
}

http.HandleFunc

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) }

所感

  • どのように動いているか想像して普段の実装ができそう
  • インターフェースに依存した実装なので読みやすかった
227
154
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
227
154