はじめに
こんにちは、サーバーサイドエンジニアのKidaです。
Goにはnet/http
というHTTP クライアントとサーバーの実装を提供してくれるパッケージがあります。
普段の開発でnet/http
パッケージを使っているのですがGoの経験がまだ浅いということもあり、仕様の理解が甘いなと感じていました。
そこで、本記事ではnet/http
パッケージが内部的にどんな処理をしているのか簡単に見ていき、理解を深めたいと思います!
簡単なサーバーを実際にたててみる
まずは、簡単なサーバーをたててみます。
package main import ( "fmt" "net/http" ) func main() { h1 := func(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, "Hello from h1!\n") } h2 := func(w http.ResponseWriter, _ *http.Request) { fmt.Fprint(w, "Hello from h2!\n") } http.HandleFunc("/", h1) http.HandleFunc("/h2", h2) if err := http.ListenAndServe(":8080", nil); err != nil { panic(err) } }
コードを走らせるとサーバーが立ち上がるので、下記のリクエストを送ってみると値が返ってくるのが確認できます。
$ curl http://localhost:8080 Hello from h1! $ curl http://localhost:8080/h2 Hello from h2!
上記のコードで重要なのは、主にHandleFuncとListenAndServeの二つになります。
それらがどういうものなのか見ていきましょう!
HandleFunc
まず、概要を公式ドキュメントで確認します。
func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) HandleFunc registers the handler function for the given pattern in the DefaultServeMux. The documentation for ServeMux explains how patterns are matched.
HandleFuncは与えられたパターンに対応するhandler関数をDefaultServeMuxに登録するもののようです。
上記のコードで言うと"/"などのpathが与えられたパターンでh1,h2などの関数がhandler関数ということになります。
では、DefaultServerMuxとはなんでしょうか?
Handler
DefaultServerMuxを説明する上でHandlerとは何かということを知る必要があるため、先にこちらの説明を少しだけします。
HandlerとはそれぞれのHTTPリクエストを捌くためのもので以下のinterfaceを満たす必要があります。
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
非常にシンプルなものでServeHTTP(ResponseWriter, *Request)メソッドを実装した型であればHandlerとして扱えるようです。
DefaultServeMux
DefaultServeMuxが定義されている箇所のソースコードを見てみましょう。
// DefaultServeMux is the default ServeMux used by Serve. var DefaultServeMux = &defaultServeMux var defaultServeMux ServeMux
https://cs.opensource.google/go/go/+/refs/tags/go1.18.1:src/net/http/server.go
DefaultServeMuxはデフォルトのServeMuxであり、ServeMuxとはHTTP requestのマルチプレクサーのことです。
つまり、DefaultServeMuxはpathに対応するHandlerにroutingを行うために必要なもののようです。
ここまでの情報で、HandleFuncが引数で受け取ったpatternにhandlerを登録しているものだと理解できました。
次に、ListenAndServeを見ていきます。
ListenAndServe
同様にまず、概要を確認します。
ListenAndServe listens on the TCP network address srv.Addr and then calls Serve to handle requests on incoming connections.
ListenAndServeは引数で受け取ったTCPネットワークのアドレスであるsrv.Addrで通信をリッスンし、来たコネクションに対してリクエストを捌くためにServeメソッドを呼ぶもののようです。
実際に、ソースコードを見ていきます。一部抜粋しコメントを入れています。
func ListenAndServe(addr string, handler Handler) error { server := &Server{Addr: addr, Handler: handler} return server.ListenAndServe() } // http.ListenAndServeでServer.ListenAndServeを呼んでいる部分のコード func (srv *Server) ListenAndServe() error { if srv.shuttingDown() { return ErrServerClosed } addr := srv.Addr // addrが空だったら:httpアドレスを指定 if addr == "" { addr = ":http" } // tcpコネクションのリスナーを引数で渡しているアドレスで作成 ln, err := net.Listen("tcp", addr) if err != nil { return err } return srv.Serve(ln) }
https://cs.opensource.google/go/go/+/refs/tags/go1.18.1:src/net/http/server.go
次に、Serveメソッドの中身を見ていきます。
一部抜粋
func (srv *Server) Serve(l net.Listener) error { baseCtx := context.Background() ctx := context.WithValue(baseCtx, ServerContextKey, srv) for { rw, err := l.Accept() connCtx := ctx c := srv.newConn(rw) go c.serve(connCtx) } }
https://cs.opensource.google/go/go/+/refs/tags/go1.18.1:src/net/http/server.go
まず、contextを作り、無限forループの中でnet.Conn型(コネクション) を待つためにAccept()を呼んでいます。
そして、newConn()リスナーから取得したnet.Conn型からhttp.conn型のstruct(新しいコネクション)が生成されます。
最後に、http.conn型のserveメソッドが新しいgoroutineで呼ばれています。
では、http.conn型のserveメソッドの中身を見ていきましょう。
一部抜粋
// Serve a new connection. func (c *conn) serve(ctx context.Context) { c.remoteAddr = c.rwc.RemoteAddr().String() ctx = context.WithValue(ctx, LocalAddrContextKey, c.rwc.LocalAddr()) // HTTP/1.x from here on. ctx, cancelCtx := context.WithCancel(ctx) for { w, err := c.readRequest(ctx) serverHandler{c.server}.ServeHTTP(w, w.req) w.cancelCtx() w.finishRequest() } }
readRequest()でリクエストをコネクションから読み込み、http.response型で受け取ります。
そして、serverHandlerのServeHTTPメソッドが呼ばれます。
一部抜粋
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { handler := sh.srv.Handler if handler == nil { handler = DefaultServeMux } handler.ServeHTTP(rw, req) }
ServeHTTPでは先ほど見たようにhandlerがnilだった場合はDefaultServeMuxが代入され、Handler interfaceのServeHTTPが呼ばれています。
まとめると、ListenAndServeでは以下のことをやっているようです。
- リスナーを作成し、コネクションを確立させリクエストを待ち受ける。
- 引数で受け取ったHandlerのServeHTTP()の呼び出し、nilだった場合はDefaultServerMuxのServeHTTP()を呼ぶ。
まとめ
この記事ではHandleFuncとListenAndServeの内部でどんなことが行われているかを簡単に見てきました。
思った以上に様々なことをやっており、まだまだ触れらていないことも多いですが、その一部だけでもしれて楽しかったです!