Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

Goのnet/httpパッケージを理解する

はじめに

こんにちは、サーバーサイドエンジニアの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!

上記のコードで重要なのは、主にHandleFuncListenAndServeの二つになります。

それらがどういうものなのか見ていきましょう!

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.

pkg.go.dev

HandleFuncは与えられたパターンに対応するhandler関数をDefaultServeMuxに登録するもののようです。

上記のコードで言うと"/"などのpathが与えられたパターンでh1,h2などの関数がhandler関数ということになります。

では、DefaultServerMuxとはなんでしょうか?

Handler

DefaultServerMuxを説明する上でHandlerとは何かということを知る必要があるため、先にこちらの説明を少しだけします。

HandlerとはそれぞれのHTTPリクエストを捌くためのもので以下のinterfaceを満たす必要があります。

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

pkg.go.dev

非常にシンプルなもので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の内部でどんなことが行われているかを簡単に見てきました。

思った以上に様々なことをやっており、まだまだ触れらていないことも多いですが、その一部だけでもしれて楽しかったです!