この章について
Goではhttp.ListenAndServe
関数を呼ぶことで、httpサーバーを起動させることができます。
http.ListenAndServe(":8080", nil)
この章では、http.ListenAndServe
が呼ばれた裏側で、どのような処理が行われているのかについて解説します。
コードリーディング
Goの利点として「GoはGo自身で書かれているため、コードリーディングのハードルが低い」というのがあります。
そのため、net/http
パッケージに存在するhttp.ListenAndServe
関数の実装コードももちろんGoで行われています。
ここからは、http.ListenAndServe
関数の挙動を理解するために、net/http
パッケージのコードを実際に読んでいきたいと思います。
http.ListenAndServe
関数
1. 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()
}
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
Thehandler
is typicallynil
, in which case theDefaultServeMux
is used.
// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux
出典:pkg.go.dev - net/http#pkg-variables
http.Server
型のListenAndServe
メソッド
2. 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メソッドを呼ぶ
}
ここでやっていることは大きく2つです。
-
net.Listen
関数を使って、net.Listener
インターフェースln
を得る -
ln
を引数に使って、http.Server
型のServe
メソッドを呼ぶ
http.Server
型のServe
メソッド
3. 次に、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の実行
}
}
内部でやっているのは、以下の4つです。
- contextを作る
-
net.Listener
のメソッドAccept()
を呼んで、net.Conn
インターフェースrw
を得る -
net.Conn
からhttp.conn
型を作る - 新しいゴールーチン上で、
http.conn
型のserve
メソッドを実行する
この処理の中にはいくつか重要なポイントがありますので、ここからはそれを解説していきます。
net.Conn
インターフェースの入手
この時点で、http通信をするためのコネクションインターフェースnet.Conn
の入手が完了します。
net.Conn
の入手のために必要なステップは2つです。
- (
(srv *Server) ListenAndServe
メソッド内)net.Listen
関数からnet.Listener
インターフェースln
を得る - (
(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
メソッドが存在し、それらを実行することでネットワークからのリクエスト読み込み・レスポンス書き込みを行えるようになります。
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リクエストごとに新規ゴールーチンを立てた場合は、複数リクエストを並行に処理できるようになるためレスポンスタイムが向上します。
http.conn
型のserve
メソッド
4. 本題のhttp.ListenAndServe
関数の掘り下げに戻りましょう。
http.Server
型Serve
メソッド内で、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)
}
}
http.conn.serve
内部で行っているのは、大きく分けて以下の2つです。
-
http.conn
型のreadRequest
メソッドから、http.response
型を得る -
http.serverHandler
型のServerHTTP
メソッドを呼ぶ
これも一つずつ詳しく説明していきます。
http.conn.readRequest
メソッドによるhttp.response
型の入手
4-1. まず、readRequest
型のレシーバであるhttp.conn
型は、内部に先ほど入手したnet.Conn
を含んでいます。
// A conn represents the server side of an HTTP connection.
type conn struct {
server *Server
rwc net.Conn
// (以下略)
}
このnet.Conn
のRead
メソッドを駆使してリクエスト内容を読み込み、http.response
型を作成するのがreadRequest
メソッドの仕事です。
// A response represents the server side of an HTTP response.
type response struct {
conn *conn
req *Request // request for this response
// (以下略)
}
http.serverHandler.ServeHTTP
メソッドの呼び出し
4-2. リクエスト内容を得ることができたら、いよいよハンドリングに入っていきます。
http.conn
のフィールドに含まれていたhttp.Server
を、http.serverHandler
型インスタンスにラップした上でServeHTTP
メソッドを呼び出します。
type serverHandler struct {
srv *Server
}
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request)
また、ServeHTTP
メソッド呼び出しの際に渡している引数に注目すると、先ほど入手したhttp.response
型が使われていることも特筆に値するでしょう。
// 再掲
func (c *conn) serve(ctx context.Context) {
// (一部抜粋)
w, err := c.readRequest(ctx)
serverHandler{c.server}.ServeHTTP(w, w.req)
}
http.serverHandler
型のServerHTTP
メソッド
5. それでは、http.serverHandler.ServerHTTP
メソッドの中身を見ていきましょう。
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
// 一部抜粋
handler := sh.srv.Handler
handler.ServeHTTP(rw, req)
}
-
sh.srv.Handler
で、http.Handler
型インターフェースを得る - Handlerインターフェースのメソッド、
ServeHTTP
を呼ぶ
sh.srv.Handler
の取り出し
5-1. http.serverHandler
の中にはhttp.Server
が存在し、そしてhttp.Server
の中にはhttp.Handler
が存在します。
このハンドラを明示的に取り出しています。
type serverHandler struct {
srv *Server
}
type Server struct {
Handler Handler // これを取り出している
// (以下略)
}
http.Handler.ServeHTTP
メソッドの実行
5-2. 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
を満たす具体型は、パッケージ変数DefaultServeMux
のhttp.ServeMux
型となります。
次章予告
ここまではサーバーの起動作業、具体的には
-
net.Conn
を入手して、リクエストを受け取る体制を整える - ハンドラ関数の第二引数に渡す
http.response
型の用意 -
http.ListenAndServe
関数の第二引数(今回はnil
であるためDefaultServeMux
となる)で渡されたルーティングハンドラの起動
までを追っていきました。
次章では、この続きを追いやすくするために、ルーティングハンドラであるDefaultServeMux
そのものについて詳しく掘り下げていきます。