見出し画像

4連休でgoのウェブ関係を学ぼう(ServeMuxてなんぞや?)

前回はソースコードを追っていってなかなか骨の折れる作業でした。今回はGoのhttpパッケージの大事な概念であるServeMuxについて見ていきます。前回の最後の方でいきなりDefaultServeMuxなんて出てきてびっくりしたかもしれませんが今回そこを解決できればと思います。今回も例に漏れず参考サイトは以下になります。

あと必要に応じて公式のドキュメントやソースコードを引っ張ってきます。

ServeMuxを詳しく見てみる

// ServeMux is an HTTP request multiplexer.
// It matches the URL of each incoming request against a list of registered
// patterns and calls the handler for the pattern that
// most closely matches the URL.

type ServeMux struct {
   mu    sync.RWMutex
   m     map[string]muxEntry
   es    []muxEntry // slice of entries sorted from longest to shortest.
   hosts bool       // whether any patterns contain hostnames
}

type muxEntry struct {
	h       Handler
	pattern string
}​

Mutexが読み込み・書き込み両方に対してロックをかけるのに対してRWMutexは読み込み・書き込みのロックを分けることが出来る型です。ゴルーチンでそれぞれのリクエストの並行処理を行うので必要ですね。muxEntryはHandlerとマッチング文字列を持ちます。この文字列とリクエストのURLを比較して適当なhandlerに渡すわけですね。

ようやくするとServeMux は HTTP リクエストマルチプレクサです。
これは、登録されたパターンのリストと各受信リクエストの URL をマッチさせ、URL に最も近いパターンのハンドラを呼び出します。

Handlerを詳しく見てみる

以下にあるようにhandlerはインターフェースです。

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

しかしGoで簡易的なWebサーバを立てた時、ServeHTTPは実装しませんでした。ではどこで実現されていたかというとHandlerFuncです。

type HandlerFunc func(ResponseWriter, *Request)

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

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

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

mux.Handler(r)をもう少し詳しくみると以下の2つの関数で実際にリクエストのパスからマッチするHandlerを探して返しています。

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {

	if r.Method == "CONNECT" {
		if u, ok := mux.redirectToPathSlash(r.URL.Host, r.URL.Path, r.URL); ok {
			return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
		}
		return mux.handler(r.Host, r.URL.Path)
	}

	host := stripHostPort(r.Host)
	path := cleanPath(r.URL.Path)

	if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
		return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
	}
	if path != r.URL.Path {
		_, pattern = mux.handler(host, path)
		url := *r.URL
		url.Path = path
		return RedirectHandler(url.String(), StatusMovedPermanently), pattern
	}
	return mux.handler(host, r.URL.Path)
}

func (mux *ServeMux) handler(host, path string) (h Handler, pattern string) {
	mux.mu.RLock()
	defer mux.mu.RUnlock()

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

これがルーティングのプロセスになります。少し思い出して欲しいのですが、大元のListenAndServe(":9090", nil)ではnilを渡しているのでDefaultServeMuxつまり上で見てきたようなことが実行されます。ここを自分で定義したServeMuxに置き換えることでルータを実装することも出来ます。

簡単なルータを実装してみる

ルーティングを行っている大元はfunc (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request)ですのでここを書き換えると自分の自由にルータを実装することが出来ます。以下は一例です。

type MyMux struct {
}

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

func sayMyName(w http.ResponseWriter, r *http.Request) {
	r.ParseForm()
	fmt.Fprintf(w, "I'm bkc.")
}

func sayhelloName(w http.ResponseWriter, r *http.Request) {
	r.ParseForm() // オプションを解析
	fmt.Fprintf(w, "hello go server!")
}

func main() {
	mux := &MyMux{}
	err := http.ListenAndServe(":9090", mux)
	fmt.Println("Listen in 9090.")
	if err != nil {
		log.Fatal("Listen and server:", err)
	}
}

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

最後にまとめとして参考サイトの写しになりますがプロセスをまとめましょう。

前回で説明した分

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

これで4連休でGoのWeb関係を学ぼうはおしまいです。私の理解が甘く解りにくいところもあるかと思いますが、皆さんの理解の手助けになれば幸いです。参考サイトはこの後もフォームやデータベースの処理、セッションなどと続いていきます。興味がある方は続きもやってみてください。

この記事が気に入ったらサポートをしてみませんか?