3.4 Goのhttpパッケージ詳細

前の節でGoが如何にWebの作業モードを実現するかフローをご紹介しました。この節では、httpパッケージを詳しく解剖していきます。これはどのように全体のプロセスを実現しているのでしょうか。

Goのhttpには2つのコアとなる機能があります:Conn、ServeMux

Connのgoroutine

我々が普段書くhttpサーバとは異なり、Goはマルチスレッドと高い性能を実現するため、goroutinesを使ってConnのイベント読み書きを処理します。これによって各リクエストは独立性を保持することができます。互いにブロックせず、効率よくネットワークイベントにレスポンスすることができます。これがGoに高い効率を保証します。

Goがクライアントのリクエストを待ち受けるには以下のように書きます:

c, err := srv.newConn(rw)
if err != nil {
    continue
}
go c.serve()

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

ServeMuxのカスタム定義

前の節でconn.serverについてご説明した際、拾は内部ではhttpパッケージのデフォルトのルートをコールしていました。ルータを通して今回のリクエストのデータをバックエンドの処理関数に渡します。ではこのルータはどのように実現されているのでしょうか?

構造は以下のとおりです:

type ServeMux struct {
    mu sync.RWMutex   //ミューテックス、リクエストがマルチスレッド処理に及んだことでミューテックス機構が必要になります。
    m  map[string]muxEntry  // ルーティングルール、一つのstringがひとつのmuxエンティティに対応します。ここではstringは登録されるルーティングを表現しています。
    hosts bool // 任意のルールにhost情報が含まれているか
}

以下でmuxEntryを見てみましょう

type muxEntry struct {
    explicit bool   // 精確にマッチするか否か
    h        Handler // このルーティング式はどのhandlerに対応するか
    pattern  string  //マッチング文字列
}

次にHandlerの定義を見てみましょう。

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)  // ルーティング実現器
}

Handlerはインターフェースですが、前の節の中のsayhelloName関数では特にServeHTTPというインターフェースを実装してはいませんでした。どうして追加できるのでしょうか?もともとhttpパッケージの中ではHandlerFuncという型が定義されています。私達が定義したsayhelloName関数はまさにこのHandlerFuncがコールされた結果であり、この型はデフォルトでServeHTTPインターフェースを実装していることになります。つまり、HandlerFunc(f)をコールして強制的にfをHandlerFunc型に型変換しているのです。このようにしてfはServeHTTPメソッドを持つようになります。

type HandlerFunc func(ResponseWriter, *Request)

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

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

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インターフェースがコールされ、目的の関数を実行することができます。

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

下のコードを通して、自分自身で簡単なルータを実装してみます。

package main

import (
    "fmt"
    "net/http"
)

type MyMux struct {
}

func (p *MyMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path == "/" {
        sayhelloName(w, r)
        return
    }
    http.NotFound(w, r)
    return
}

func sayhelloName(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Hello myroute!")
}

func main() {
    mux := &MyMux{}
    http.ListenAndServe(":9090", mux)
}

Goのコードの実行プロセス

httpパッケージへの分析を通して、全体のコードの実行プロセスを整理してみましょう。

  • まずHttp.HandleFuncをコールします。

    順序にしたがっていくつかの事を行います:

    1 DefaultServeMuxのHandlerFuncをコールする。

    2 DefaultServeMuxのHandleをコールする。

    3 DefaultServeMuxのmap[string]muxEntryで目的のhandlerとルーティングルールを追加する。

  • 次にhttp.ListenAndServe(":9090", nil)をコールする。

    順序にしたがっていくつかの事を行う:

    1 Serverのエンティティ化

    2 ServerのListenAndServe()をコールする

    3 net.Listen("tcp", addr)をコールし、ポートを監視する

    4 forループを起動し、ループの中でリクエストをAcceptする

    5 各リクエストに対してConnを一つエンティティ化し、このリクエストに対しgoroutineを一つ開いてgo c.serve()のサービスを行う。

    6 各リクエストの内容を読み込むw, err := c.readRequest()

    7 handlerが空でないか判断する。もしhandlerが設定されていなければ(この例ではhandlerは設定していません)、handlerはDefaultServeMuxに設定されます。

    8 handlerのServeHttpをコールする

    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をコールします

results matching ""

    No results matching ""