Golang ServeMux 是如何實現多路處理的

以前出於好奇看了一下 Golang net/http 包下的部分源碼,今天想仍是總結一下吧。因爲是第一次寫文章且抱着忐忑的心情發表,可能有些語義上的不清楚,諒解一下,或者提出修改的建議!git

簡介

net/http 包裏的 server.go 文件裏註釋寫着:ServeMux is an HTTP request multiplexer. 即 ServeMux 是一個 HTTP 請求的 "多路處理器",由於 ServeMux 實現的功能就是將收到的 HTTP 請求的 URL 與註冊的路由相匹配,選擇匹配度最高的路由的處理函數來處理該請求。github

最簡單的栗子:瀏覽器

mux := http.NewServeMux()
mux.HandleFunc("/a/b", ab)
mux.HandleFunc("/a", a)
http.ListenAndServe(":8000", mux)
複製代碼

每一個路由對應了一個處理函數。app

先來看看 NewServeMux 函數框架

func NewServeMux() *ServeMux { return new(ServeMux) }
複製代碼

咱們知道 new 函數會爲傳入的類型分配空間並返回指向該空間首地址的指針,因而咱們就獲取了一個 ServeMux 實例。函數

源碼分析

ServeMux 結構體

接下來就是 ServeMux 的結構源碼分析

type ServeMux struct {
    mu    sync.RWMutex
    m     map[string]muxEntry
    es    []muxEntry 
    hosts bool  // 標記路由中是否帶有主機名
}
複製代碼

其中 m 就是用來存儲路由與處理函數映射關係的 map,es 按照路由長度從大到小的存放處理函數 (後面會講爲何要這樣),但 ServeMux 爲了方便,map存放的值實際上是放有處理函數和路由路徑的 muxEntry 結構體:post

type muxEntry struct {
    h       Handler  // 處理函數
    pattern string   // 路由路徑
}
複製代碼

ServeMux 暴露的方法主要是下面 4 個:ui

func (mux *ServeMux) Handle(pattern string, handler Handler) func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) 複製代碼

Handle 方法

Handle 方法經過將傳入的路由和處理函數存入 ServeMux 的映射表 m 中來實現 "路由註冊(register)"url

源碼具體實現以下:

func (mux *ServeMux) Handle(pattern string, handler Handler) {
    mux.mu.Lock()
    defer mux.mu.Unlock()
    // 檢查路由路徑是否爲空
    if pattern == "" {
    	panic("http: invalid pattern")
    }
    // 檢查處理函數是否爲空
    if handler == nil {
    	panic("http: nil handler")
    }
    // 檢查該路由是否已經註冊過
    if _, exist := mux.m[pattern]; exist {
    	panic("http: multiple registrations for " + pattern)
    }
    // 若是尚未任何路由註冊,就爲 mux.m 分配空間
    if mux.m == nil {
    	mux.m = make(map[string]muxEntry)
    }
    // 實例化一個 muxEntry
    e := muxEntry{h: handler, pattern: pattern}
    // 將該路由與該 muxEntry 的實例存到 mux.m 中
    mux.m[pattern] = e
    // 若是該路由路徑以 "/" 結尾,就把該路由按照大到小的路徑長度插入到 mux.e 中
    if pattern[len(pattern)-1] == '/' {
    	mux.es = appendSorted(mux.es, e)
    }
    // 若是該路由路徑不以 "/" 開始,標記該 mux 中有路由的路徑帶有主機名
    if pattern[0] != '/' {
    	mux.hosts = true
    }
}
複製代碼

HandleFunc 方法

HandleFunc 方法接收一個具體的處理函數將其包裝成 Handler:

type HandlerFunc func(ResponseWriter, *Request) func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    if handler == nil {
    	panic("http: nil handler")
    }
    mux.Handle(pattern, HandlerFunc(handler))
}
複製代碼

其中 HandlerFunc(f) 起到的做用就是在 HandlerFunc 中執行 f

Handler 方法

Handler 方法從傳入的請求(Request)中拿到 URL 進行匹配,返回對應的處理函數和路由

在看 Handler 的實現前,先看看它調用的 handler 方法:

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

    // 若當前 mux 中註冊有帶主機名的路由,就用"主機名+路由路徑"去匹配
    // 也就是說帶主機名的路由優先於不帶的
    if mux.hosts {
    	h, pattern = mux.match(host + path)
    }
    // 因此若沒有匹配到,就直接把路由路徑拿去匹配
    if h == nil {
    	h, pattern = mux.match(path)
    }
    // 若都沒有匹配到,就默認返回 NotFoundHandler,該 Handler 會往
    // 響應裏寫上 "404 page not found"
    if h == nil {
    	h, pattern = NotFoundHandler(), ""
    }
    // 返回得到的 Handler 和路由路徑
    return
}
複製代碼

好了,如今是 Handler 方法

func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string) {
    // 去掉主機名上的端口號
    host := stripHostPort(r.Host)
    // 整理 URL,去掉 ".", ".."
    path := cleanPath(r.URL.Path)

    // redirectToPathSlash 在 mux.m 中查看 path+"/" 是否存在
    // 若是存在,RedirectHandler 就將該請求重定向到 path+"/"
    if u, ok := mux.redirectToPathSlash(host, path, r.URL); ok {
    	return RedirectHandler(u.String(), StatusMovedPermanently), u.Path
    }

    // 若是整理後的 URL 與請求中的路徑不同,先調用 handler 進行匹配
    // 在將請求裏的 URL 改爲整理後的 URL
    // 最後將該請求重定向到整理後的 URL
    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)
}
複製代碼

咱們有必要看看 match 方法是怎麼進行匹配的

func (mux *ServeMux) match(path string) (h Handler, pattern string) {
    // 若 mux.m 中已存在該路由映射,直接返回該路由的 Handler,和路徑
    v, ok := mux.m[path]
    if ok {
    	return v.h, v.pattern
    }

    // 找到路徑能最長匹配的路由。
    for _, e := range mux.es {
    	if strings.HasPrefix(path, e.pattern) {
    	    return e.h, e.pattern
    	}
    }
    return nil, ""
}
複製代碼

注意這裏是在 mux.es 中進行查找,而不是映射表 mux.m 中,而 mux.es 是存放全部以 "/" 結尾的路由路徑的切片。由於只會在以 "/" 結尾的路由路徑中才會出現須要選擇最長匹配方案

好比註冊的路由有

mux.HandleFunc("/a/b/", ab)
mux.HandleFunc("/a/", a)
複製代碼

那麼當一個請求的 URL 爲 /a/b/c 的時候,咱們但願是由 ab 來處理這個請求。

另外,爲了減小在 mux.es 中的查詢時間, mux.es 中元素是按照它們的長度由大到小順序存放的。

ServeHTTP 方法

咱們知道在 Go 中要實現一個處理請求的 handler 結構體須要讓該結構體現實 Handler 接口的 ServeHTTP 方法:

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

type myHandler struct {}

func (h *myHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("This message is from myHandler."))
}

func main() {
    http.Handle("/", &helloHandler{})  // 路由註冊
}
複製代碼

咱們已經經過 Handler 方法拿到了請求(Request)和它對應的處理函數(Handler)

咱們的 ServeMux 是一個結構體,它的 ServeHTTP 方法要作的就是將每一個請求派遣(dispatch)到它們對應的處理函數上。

func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
    // 若是請求路徑爲 "*",告訴瀏覽器該鏈接已關閉並返回狀態碼 400
    if r.RequestURI == "*" {
    	if r.ProtoAtLeast(1, 1) {
    	    w.Header().Set("Connection", "close")
    }
    	w.WriteHeader(StatusBadRequest)
    	return
    }
    // 調用 mux.Handler 方法獲取請求和它對應的處理函數
    h, _ := mux.Handler(r)
    // 將 ResponseWriter 和 *Request 類型的參數傳給處理函數
    h.ServeHTTP(w, r)
}
複製代碼

這樣,每收到一個請求就會調用對應的處理函數來處理該請求了。

關於 http.HandleFunc 方法

但咱們一般會看到,一些簡單的示例代碼是下面這樣寫的:

func helloHandler(w http.ResponseWriter, req *http.Request) {
    io.WriteString(w, "hello, world!\n")
}

func main() {
    http.HandleFunc("/", helloHandler)
    http.ListenAndServe(":8000", nil)
}
複製代碼

咱們能夠看一下 http.HandleFunc 方法作了些什麼:

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
    DefaultServeMux.HandleFunc(pattern, handler)
}
複製代碼

能夠看到該方法中使用了一個 DefaultServeMux 來註冊傳入的路由,繼續看:

var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux
複製代碼

能夠看到,http.HandleFunc 也是經過實例化一個全局的 ServeMux 來進行路由註冊的。

總結

咱們已經瞭解了 ServeMux 是怎麼實現多路處理了,簡單歸納一下。HandleHandleFunc 方法用來將路由路徑與處理函數的映射經過一個 map 記錄到當前的 mux 實例裏;Handler 方法將接收的請求中的 URL 預處理後拿去和記錄的映射匹配,若匹配到,就返回該路由的處理函數和路徑;ServeHTTP 方法將請求派遣給匹配到的處理函數處理。

可是 ServeMux 的多路處理實現並不支持請求方法判斷,也不能處理路由嵌套URL變量值提取的功能

因此最近在分析 ginkratos/blademaster 這樣的框架是如何實現這三個功能的,但願後面能有第二篇總結出現吧。。

其實寫到一半的時候發現掘金上已經有這部分的源碼分析了,因而就看了一下,和本身想的差很少,哈哈

相關文章
相關標籤/搜索