Build Web Application With Golang を試してやってみる 11

はじめに

前回の記事の続きです。
前回の記事はこちら

Goのhttpパッケージ詳細

前回は、Go がWebサーバを立てている裏で何をしているのかを見ていきました。
今回は、http パッケージの中身を見ていくようです。
ConnServerMux というのがコアの機能と書いてあります。

Connのgoroutine

goroutineを使うと、ノンブロッキングで効率的にレスポンスができるみたいです。
コードが書いてありますね、これは前回も出てきたものです、これでリクエストを待ち受けるようです。

クライアントの各リクエストはどれもConnを一つ作成しているのがわかるかと思います。
このConnには今回のリクエストの情報が保存されています。
これは目的のhandlerに渡され、このhandlerで目的のhandler情報を読み取ることができます。
このように各リクエストの独立性を保証します。

なるほど。
リクエストごとにConn を張っているようですね、これで独立性を担保しているようです。
(1つ1つにConn を作成すると、そうでない場合に比べリソースを喰いそうな気がしますがどうなんでしょうか。)

ServeMuxのカスタム定義

前の節でconn.serverについてご説明した際、拾は内部ではhttpパッケージのデフォルトのルートをコールしていました。

確かに!そうでした。

ルータを通して今回のリクエストのデータをバックエンドの処理関数に渡します。ではこのルータはどのように実現されているのでしょうか?

type ServeMux struct {
    mu sync.RWMutex   //ミューテックス、リクエストがマルチスレッド処理に及んだことでミューテックス機構が必要になります。
    m  map[string]muxEntry  // ルーティングルール、一つのstringがひとつのmuxエンティティに対応します。ここではstringは登録されるルーティングを表現しています。
    hosts bool // 任意のルールにhost情報が含まれているか
}
type muxEntry struct {
    explicit bool   // 精確にマッチするか否か
    h        Handler // このルーティング式はどのhandlerに対応するか
    pattern  string  //マッチング文字列
}
type Handler interface {
    ServeHTTP(ResponseWriter, *Request)  // ルーティング実現器
}

あーなんとなく構造が見えてきました。

Handlerはインターフェースですが、
前の節の中のsayhelloName関数では特にServeHTTPというインターフェースを実装してはいませんでした。どうして追加できるのでしょうか?

そうね!

もともとhttpパッケージの中ではHandlerFuncという型が定義されています。
私達が定義したsayhelloName関数はまさにこのHandlerFuncがコールされた結果であり、
この型はデフォルトでServeHTTPインターフェースを実装していることになります。

どれどれ、HandlerFunc を見てみます。

type HandlerFunc func(ResponseWriter, *Request)

// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}

見たことありますねぇ!

つまり、HandlerFunc(f)をコールして強制的にfをHandlerFunc型に型変換しているのです。
このようにしてfはServeHTTPメソッドを持つようになります。

ServeHTTP というのが実装されているわけですね。

ルータでは対応するルーティングルールを保存した後、具体的にはどのようにリクエストを振り分けているのでしょうか?以下のコードをご覧ください。

muxEntry 型として保存されてイますが、たしかにこれを実行するのはどうしているのでしょう。

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    if r.RequestURI == "*" {
        w.Header().Set("Connection", "close")
        w.WriteHeader(StatusBadRequest)
        return
    }
    h, _ := mux.Handler(r)
    h.ServeHTTP(w, r)
}

上に示す通りルータはリクエストを受け取った後、*であれば接続を切断し、
そうでなければmux.handler(r).ServeHTTP(w, r)をコールして対応する設定された処理Handlerを返し、h.ServeHTTP(w, r)を実行します。

ほうほう、パターンを検索して対応する関数をもらってきて実行するみたいですね。

つまり、目的のルーティングのhandlerのServerHTTPインターフェースへのコールです。ではmux.Handler(r)はどのように処理するのでしょうか?

更に中身ですね、パターンを検索する部分 のところですね。

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
    if r.Method != "CONNECT" {
        if p := cleanPath(r.URL.Path); p != r.URL.Path {
            _, pattern = mux.handler(r.Host, p)
            return RedirectHandler(p, StatusMovedPermanently), pattern
        }
    }    
    return mux.handler(r.Host, r.URL.Path)
}

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
}

長いですが、

もともとこれはユーザのリクエストしたURLとルータの中に保存されているmapに従ってマッチングしています。マッチングによって保存されているhandlerが返されるにあたり、このhandlerのServeHTTPインターフェースがコールされ、目的の関数を実行することができます。

    if mux.hosts {
        h, pattern = mux.match(host + path)
    }

ここの部分ですね。

上の紹介を通して、私達はルーティングの全体プロセスを理解しました。Goは実は外部で実装されたルータをサポートしています。ListenAndServeの第2引数が外部のルータを設定する為に用いられます。これはHandlerインターフェースのひとつで、外部ルータでHandlerインターフェースを実装し、そのServeHTTPにカスタム定義のルーティング機能を実装することができます。

多少なりとも理解できました、さてコードがあるので早速実行してみます。

実行してみます。
vs-code-golang-01

/ のパターンです。
vs-code-golang-02

Not Found のパターンです。 (ルートに当てはまらない場合)
vs-code-golang-03

長いですが実行プロセスを追っていきます。

Goのコードの実行プロセス
httpパッケージへの分析を通して、全体のコードの実行プロセスを整理してみましょう。
まずHttp.HandleFuncをコールします。
順序にしたがっていくつかの事を行います:
1 DefaultServeMuxHandlerFuncをコールする。
2 DefaultServeMuxHandleをコールする。
3 DefaultServeMuxmap[string]muxEntryで目的のhandlerとルーティングルールを追加する。
次にhttp.ListenAndServe(":9090", nil)をコールする。
順序にしたがっていくつかの事を行う:
1 Serverのエンティティ化
2 ServerListenAndServe()をコールする
3 net.Listen(“tcp”, addr)をコールし、ポートを監視する
4 forループを起動し、ループの中でリクエストをAcceptする
5 各リクエストに対してConnを一つエンティティ化し、このリクエストに対しgoroutineを一つ開いてgo c.serve()のサービスを行う。
6 各リクエストの内容を読み込むw, err := c.readRequest()
7 handlerが空でないか判断する。もしhandlerが設定されていなければ(この例ではhandlerは設定していません)、handlerDefaultServeMuxに設定されます。
8 handlerServeHttpをコールする
9 この例の中では、この後DefaultServeMux.ServeHttpの中に入ります
10 requestに従ってhandlerを選択し、このhandlerのServeHTTPに入ります
mux.handler(r).ServeHTTP(w, r)
11 handlerを選択します:
A ルータがこのrequestを満足したか判断します(ループによってServerMuxのmuxEntryを走査します。)
B もしルーティングされれば、このルーティングhandlerのServeHttpをコールします。
C ルーティングされなければ、NotFoundHandlerのServeHttpをコールします

A, B, C が キャプチャのパターンに対応していますね。
前章と合わせると中身がだいぶわかってきたような気がします。

おわりに

次回はまとめですが、こちらは飛ばして フォーム をやっていきます。
まとめ は裏で読んでおきます・・・!

Hugo で構築されています。
テーマ StackJimmy によって設計されています。