Chapter 03

httpサーバー起動の裏側

さき(H.Saki)
さき(H.Saki)
2021.09.19に更新

この章について

Goではhttp.ListenAndServe関数を呼ぶことで、httpサーバーを起動させることができます。

http.ListenAndServe(":8080", nil)

この章では、http.ListenAndServeが呼ばれた裏側で、どのような処理が行われているのかについて解説します。

コードリーディング

Goの利点として「GoはGo自身で書かれているため、コードリーディングのハードルが低い」というのがあります。
そのため、net/httpパッケージに存在するhttp.ListenAndServe関数の実装コードももちろんGoで行われています。

ここからは、http.ListenAndServe関数の挙動を理解するために、net/httpパッケージのコードを実際に読んでいきたいと思います。

1. http.ListenAndServe関数

http.ListenAndServe関数自体はとても単純な実装です。

// ListenAndServe always returns a non-nil error.
func ListenAndServe(addr string, handler Handler) error {
	server := &Server{Addr: addr, Handler: handler}
	return server.ListenAndServe()
}

出典:net/http/server.go

http.Server型を作成し、それのListenAndServeメソッドを呼んでいることがわかります。

このとき作られるhttp.Server型は、http.ListenAndServe関数の引数として渡された「サーバーアドレス」と「ルーティングハンドラ」を内部に持つことになります。

type Server struct {
	Addr string
	Handler Handler // handler to invoke, http.DefaultServeMux if nil
	// (以下略)
}

出典:pkg.go.dev - net/http#Server

サーバーアドレスとルーティングハンドラは、それぞれhttp.ListenAndServe関数の第一引数、第二引数で指定されたものが使用されます。
もしhttp.ListenAndServeの第二引数がnilだった場合は、net/httpパッケージ内でデフォルトで用意されているDefaultServeMuxが使用されます。

func ListenAndServe(addr string, handler Handler) error
The handler is typically nil, in which case the DefaultServeMux is used.

出典:pkg.go.dev - net/http#ListenAndServe

// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux

出典:pkg.go.dev - net/http#pkg-variables

2. http.Server型のListenAndServeメソッド

http.Server型のListenAndServeメソッドの中身は以下のようになっています。

func (srv *Server) ListenAndServe() error {
	if srv.shuttingDown() {
		return ErrServerClosed
	}
	addr := srv.Addr
	if addr == "" {
		addr = ":http"
	}
	ln, err := net.Listen("tcp", addr) // 1. net.Listenerを得る
	if err != nil {
		return err
	}
	return srv.Serve(ln) // 2. Serveメソッドを呼ぶ
}

出典:net/http/server.go

ここでやっていることは大きく2つです。

  1. net.Listen関数を使って、net.Listenerインターフェースlnを得る
  2. lnを引数に使って、http.Server型のServeメソッドを呼ぶ

3. http.Server型のServeメソッド

次に、http.Server型のListenAndServeメソッド中で呼ばれたServeメソッドを見てみましょう。

func (srv *Server) Serve(l net.Listener) error {
    // (一部抜粋)
	// 1. contextを作る
	baseCtx := context.Background()
	ctx := context.WithValue(baseCtx, ServerContextKey, srv)

	for {
		rw, err := l.Accept() // 2. ln.Acceptをしてnet.Connを得る

		connCtx := ctx
		c := srv.newConn(rw) // 3. http.conn型を作る
		go c.serve(connCtx) // 4. http.conn.serveの実行
	}
}

出典:net/http/server.go

内部でやっているのは、以下の4つです。

  1. contextを作る
  2. net.ListenerのメソッドAccept()を呼んで、net.Connインターフェースrwを得る
  3. net.Connからhttp.conn型を作る
  4. 新しいゴールーチン上で、http.conn型のserveメソッドを実行する

この処理の中にはいくつか重要なポイントがありますので、ここからはそれを解説していきます。

net.Connインターフェースの入手

この時点で、http通信をするためのコネクションインターフェースnet.Connの入手が完了します。

net.Connの入手のために必要なステップは2つです。

  1. ((srv *Server) ListenAndServeメソッド内) net.Listen関数からnet.Listenerインターフェースlnを得る
  2. ((srv *Server) Serveメソッド内) ln.Accept()メソッドを実行する
func (srv *Server) ListenAndServe() error {
	// (一部抜粋)
	ln, err := net.Listen("tcp", addr) // 1. net.Listenerを得る
	return srv.Serve(ln)
}

func (srv *Server) Serve(l net.Listener) error {
	// (一部抜粋)
	for {
		rw, err := l.Accept() // 2. ln.Acceptをしてnet.Connを得る
	}
}

net.ConnインターフェースにはRead,Writeメソッドが存在し、それらを実行することでネットワークからのリクエスト読み込み・レスポンス書き込みを行えるようになります。

net.Connを利用したネットワークI/Oの詳細については、拙著Goから学ぶI/O 第3章をご覧ください。

for無限ループによる処理の永続化

ln.Accept()メソッドによって得られたnet.Connは、一回の「リクエストーレスポンス」にしか使えません。
つまりこれは、「一つのnet.Connを使い回す形で、サーバーにくる複数のリクエストを捌くことはできない」ということです。

そのため、for無限ループを利用して「一つのリクエストごとに一つのnet.Connを作成するのを繰り返す」ことでサーバーを継続的に稼働させているのです。

func (srv *Server) Serve(l net.Listener) error {
	// (一部抜粋)
	for {
		rw, err := l.Accept()
		go c.serve(connCtx)
	}
}

新規ゴールーチン上でのhttp.conn.serveメソッド稼働

実際にリクエストをハンドルして、レスポンスを返す作業であるhttp.conn.serveメソッドは、http.ListenAndServe関数が動いているメインゴールーチン上ではなく、go文によって作成される新規ゴールーチン上にて実行されています。

// (再掲)
func (srv *Server) Serve(l net.Listener) error {
	for {
		go c.serve(connCtx) // 4. http.conn.serveの実行
	}
}

わざわざ新規ゴールーチンを立てるのは、リクエストの処理を並行に実施できるようにするためです。

メインゴールーチン上でリクエストを逐次的に処理してしまうと、一つ時間がかかるリクエストが来た場合に、その間にきた別のリクエストはその時間がかかっているリクエスト処理が終わるまで待たされることになってしまいます。
1リクエストごとに新規ゴールーチンを立てた場合は、複数リクエストを並行に処理できるようになるためレスポンスタイムが向上します。

4. http.conn型のserveメソッド

本題のhttp.ListenAndServe関数の掘り下げに戻りましょう。
http.ServerServeメソッド内で、http.conn.serveメソッドが呼ばれたところまで見てきました。

ここからはhttp.conn.serveメソッドを見ていきます。

func (c *conn) serve(ctx context.Context) {
    // 一部抜粋
	for {
		w, err := c.readRequest(ctx)
		serverHandler{c.server}.ServeHTTP(w, w.req)
	}
}

出典:net/http/server.go

http.conn.serve内部で行っているのは、大きく分けて以下の2つです。

  1. http.conn型のreadRequestメソッドから、http.response型を得る
  2. http.serverHandler型のServerHTTPメソッドを呼ぶ

これも一つずつ詳しく説明していきます。

4-1. http.conn.readRequestメソッドによるhttp.response型の入手

まず、readRequest型のレシーバであるhttp.conn型は、内部に先ほど入手したnet.Connを含んでいます。

// A conn represents the server side of an HTTP connection.
type conn struct {
    server *Server
    rwc net.Conn
    // (以下略)
}

出典:net/http/server.go

このnet.ConnReadメソッドを駆使してリクエスト内容を読み込み、http.response型を作成するのがreadRequestメソッドの仕事です。

// A response represents the server side of an HTTP response.
type response struct {
	conn	*conn
	req	*Request // request for this response
    // (以下略)
}

出典:net/http/server.go

4-2. http.serverHandler.ServeHTTPメソッドの呼び出し

リクエスト内容を得ることができたら、いよいよハンドリングに入っていきます。
http.connのフィールドに含まれていたhttp.Serverを、http.serverHandler型インスタンスにラップした上でServeHTTPメソッドを呼び出します。

type serverHandler struct {
	srv *Server
}

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request)

出典:net/http/server.go

また、ServeHTTPメソッド呼び出しの際に渡している引数に注目すると、先ほど入手したhttp.response型が使われていることも特筆に値するでしょう。

// 再掲
func (c *conn) serve(ctx context.Context) {
	// (一部抜粋)
	w, err := c.readRequest(ctx)
	serverHandler{c.server}.ServeHTTP(w, w.req)
}

http.Server型をわざわざhttp.serverHandler型にキャストすることによって、http.Handlerインターフェースを満たすようになります。

5. http.serverHandler型のServerHTTPメソッド

それでは、http.serverHandler.ServerHTTPメソッドの中身を見ていきましょう。

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
    // 一部抜粋
	handler := sh.srv.Handler
	handler.ServeHTTP(rw, req)
}

出典:net/http/server.go

  1. sh.srv.Handlerで、http.Handler型インターフェースを得る
  2. Handlerインターフェースのメソッド、ServeHTTPを呼ぶ

5-1. sh.srv.Handlerの取り出し

http.serverHandlerの中にはhttp.Serverが存在し、そしてhttp.Serverの中にはhttp.Handlerが存在します。
このハンドラを明示的に取り出しています。

type serverHandler struct {
	srv *Server
}
type Server struct {
	Handler Handler // これを取り出している
	// (以下略)
}

5-2. http.Handler.ServeHTTPメソッドの実行

http.Handler型というのはServeHTTPメソッドを持つインターフェースです。
上で取り出したhttp.Handlerに対して、このメソッドを呼び出しています。

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

出典:pkg.go.dev - net/http#Handler

しかし、このインターフェースを満たす具体型は一体何なのでしょうか。

実は今までのコードをよくよく見返してみると、sh.srv.Handlerで得られたhttp.Handlerは、http.ListenAndServe関数を呼んだときの第二引数であるということがわかります。

そのため、もしも

http.ListenAndServe(":8080", nil)

このようにサーバーを起動していた場合には、ここでのhttp.Handlerを満たす具体型は、パッケージ変数DefaultServeMuxhttp.ServeMux型となります。

次章予告

ここまではサーバーの起動作業、具体的には

  • net.Connを入手して、リクエストを受け取る体制を整える
  • ハンドラ関数の第二引数に渡すhttp.response型の用意
  • http.ListenAndServe関数の第二引数(今回はnilであるためDefaultServeMuxとなる)で渡されたルーティングハンドラの起動

までを追っていきました。

次章では、この続きを追いやすくするために、ルーティングハンドラであるDefaultServeMuxそのものについて詳しく掘り下げていきます。