Golang構建HTTP服務(二)--- Handler,ServeMux與中間件

Golang標準庫http包提供了基礎的http服務,這個服務又基於Handler接口和ServeMux結構的做Mutilpexer。實際上,go的作者設計Handler這樣的接口,不僅提供了默認的ServeMux對象,開發者也可以自定義ServeMux對象。

本質上ServeMux只是一個路由管理器,而它本身也實現了Handler接口的ServeHTTP方法。因此圍繞Handler接口的方法ServeHTTP,可以輕鬆的寫出go中的中間件。

在go的http路由原理討論中,追本溯源還是討論Handler接口和ServeMux結構。下面就基於這兩個對象開始更多關於go中http的故事吧。

介紹http庫源碼的時候,創建http服務的代碼很簡單,實際上代碼隱藏了很多細節,纔有了後來的流程介紹。本文的目的主要是把這些細節暴露,從更底層的方式開始,一步步隱藏細節,完成樣例代碼的一樣的邏輯。瞭解更多http包的原理之後,才能基於此構建中間件。

自定義的Handler

標準庫http提供了Handler接口,用於開發者實現自己的handler。只要實現接口的ServeHTTP方法即可。

關於約定名詞 handler函數handler處理器handler,請參考http原理與源碼筆記中的定義。不然對下文的描述將會很困惑。

type textHandler struct {
    responseText string
}

func (th *textHandler) ServeHTTP(w http.ResponseWriter, r *http.Request){
    fmt.Fprintf(w, th.responseText)
}

type indexHandler struct {}

func (ih *indexHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html")

    html := `<doctype html>
        <html>
        <head>
          <title>Hello World</title>
        </head>
        <body>
        <p>
          <a href="/welcome">Welcome</a> |  <a href="/message">Message</a>
        </p>
        </body>
</html>`
    fmt.Fprintln(w, html)
}

func main() {
    mux := http.NewServeMux()

    mux.Handle("/", &indexHandler{})

    thWelcome := &textHandler{"TextHandler !"}
    mux.Handle("/text",thWelcome)

    http.ListenAndServe(":8000", mux)
}

上面自定義了兩個handler結構,都實現了ServeHTTP方法。我們知道,NewServeMux可以創建一個ServeMux實例,ServeMux同時也實現了ServeHTTP方法,因此代碼中的mux也是一種handler。把它當成參數傳給http.ListenAndServe方法,後者會把mux傳給Server實例。因爲指定了handler,因此整個http服務就不再是DefaultServeMux,而是mux,無論是在註冊路由還是提供請求服務的時候。

有一點值得注意,這裏並沒有使用HandleFunc註冊路由,而是直接使用了mux註冊路由。當沒有指定mux的時候,系統需要創建一個默認的defaultServeMux,此時我們已經有了mux,因此不再需要http.HandleFucn方法了,直接使用mux的Handle方法註冊即可。

此外,Handle第二個參數是一個handler(處理器),並不是HandleFunc的一個handler函數,其原因也是因爲mux.Handle本質上就需要綁定url的pattern模式和handler(處理器)即可。既然indexHandler是handle(處理器),當然就能作爲參數,一切請求的處理過程,都交給器實現的接口方法ServeHTTP就行了。這個過程有點饒,如果不甚瞭解,建議先閱讀http原理與源碼筆記瞭解註冊路由的本質。下圖

handleFunc-handle.jpeg

左邊的12兩步只是爲了創建一個ServeMux實例,然後調用實例的Handle方法,右邊的直接就調用了mux實例的Handle方法。

創建handler處理器

上面費勁口舌羅嗦,不就是1,2,3與3的差別麼,並且1,2的兩步操作,封裝程度更高,開發者只需要寫函數即可,不用再定義結構。代碼更簡潔,因此,下面將直接創建handler函數,調用go的方法將函數轉變成handler(處理器)。

func text(w http.ResponseWriter, r *http.Request){
    fmt.Fprintln(w, "hello world")
}

func index(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html")

    html := `<doctype html>
        <html>
        <head>
          <title>Hello World</title>
        </head>
        <body>
        <p>
          <a href="/welcome">Welcome</a> |  <a href="/message">Message</a>
        </p>
        </body>
</html>`
    fmt.Fprintln(w, html)
}

func main() {
    mux := http.NewServeMux()
    mux.Handle("/", http.HandlerFunc(index))
    mux.HandleFunc("/text", text)
    http.ListenAndServe(":8000", mux)
}

代碼中使用了http.HandlerFunc方法直接將一個handler函數轉變成實現了handler(處理器)。等價與圖中的3的步驟。

mux.HandleFunc("/text", text)就更進一步,與圖中的2步驟一致,與defaultServemux.HandleFunc(pattern, function)的用法一樣。

使用默認的DefaultServeMux

經過了上面兩個過程的轉化,隱藏了更多的細節,代碼與defaultServeMux的方式越來越像。下面再去掉自定義的ServeMux,只需要修改main函數的邏輯如下:

func main() {
    http.Handle("/", http.HandlerFunc(index))
    http.HandleFunc("/text", text)
    http.ListenAndServe(":8000", nil)
}

上述的代碼就和前文的例子一樣,當代碼中不顯示的創建serveMux對象,http包就默認創建一個DefaultServeMux對象用來做路由管理器mutilplexer。

自定義Server

默認的DefaultServeMux創建的判斷來自server對象,如果server對象不提供handler,纔會使用默認的serveMux對象。既然ServeMux可以自定義,那麼Server對象一樣可以。

使用http.Server 即可創建自定義的server對象:

func main(){
    http.HandleFunc("/", index)

    server := &http.Server{
        Addr: ":8000",
        ReadTimeout: 60 * time.Second,
        WriteTimeout: 60 * time.Second,
    }
    server.ListenAndServe()
}

自定義的serverMux對象也可以傳到server對象中。

func main() {

    mux := http.NewServeMux()
    mux.HandleFunc("/", index)

    server := &http.Server{
        Addr: ":8000",
        ReadTimeout: 60 * time.Second,
        WriteTimeout: 60 * time.Second,
        Handler: mux,
    }
    server.ListenAndServe()
}

可見go中的路由和處理函數之間關係非常密切,同時又很靈活。通過巧妙的使用Handler接口,可以設計出優雅的中間件程序。

中間件Middleware

所謂中間件,就是連接上下級不同功能的函數或者軟件,通常進行一些包裹函數的行爲,爲被包裹函數提供添加一些功能或行爲。前文的HandleFunc就能把簽名爲 func(w http.ResponseWriter, r *http.Reqeust)的函數包裹成handler。這個函數也算是中間件。

這裏我們以HTTP請求的中間件爲例子,提供一個log中間件,能夠打印出每一個請求的log。

go的http中間件很簡單,只要實現一個函數簽名爲func(http.Handler) http.Handler的函數即可。http.Handler是一個接口,接口方法我們熟悉的爲serveHTTP。返回也是一個handler。因爲go中的函數也可以當成變量傳遞或者或者返回,因此也可以在中間件函數中傳遞定義好的函數,只要這個函數是一個handler即可,即實現或者被handlerFunc包裹成爲handler處理器。

func middlewareHandler(next http.Handler) http.Handler{
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request){
        // 執行handler之前的邏輯
        next.ServeHTTP(w, r)
        // 執行完畢handler後的邏輯
    })
}

這種方式在Elixir的Plug框架中很流行,思想偏向於函數式範式。熟悉python的朋友一定也想到了裝飾器。閒話少說,來看看go是如何實現的吧:


func loggingHandler(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        log.Printf("Started %s %s", r.Method, r.URL.Path)
        next.ServeHTTP(w, r)
        log.Printf("Comleted %s in %v", r.URL.Path, time.Since(start))
    })
}

func main() {
    http.Handle("/", loggingHandler(http.HandlerFunc(index)))
    http.ListenAndServe(":8000", nil)
}

loggingHandler即是一箇中間件函數,將請求的和完成的時間處理。可以看見請求或go的輸出:

2016/12/04 21:18:13 Started GET /
2016/12/04 21:18:13 Comleted / in 13.365µs
2016/12/04 21:18:20 Started GET /
2016/12/04 21:18:20 Comleted / in 17.541µs

既然中間件是一種函數,並且簽名都是一樣,那麼很容易就聯想到函數一層包一層的中間件。再添加一個函數,然後修改main函數:


func hook(next http.Handler) http.Handler{
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        log.Println("before hook")
        next.ServeHTTP(w, r)
        log.Println("after hook")

    })
}

func main() {
    http.Handle("/", hook(loggingHandler(http.HandlerFunc(index))))
    http.ListenAndServe(":8000", nil)
}

在loggingHandler再包了一層hook,可以看到輸出爲:

2016/12/04 21:26:30 before hook
2016/12/04 21:26:30 Started GET /
2016/12/04 21:26:30 Comleted / in 14.016µs
2016/12/04 21:26:30 after hook

函數調用形成了一條鏈,可以是在這條鏈上做很多事情。當然go的寫法上,比起elixir的|>的符號,優雅性略差。

總結

通過對http包的源碼學習,我們瞭解了Handler接口和ServeMux結構。並且知道如何配合他們實現go的中間件函數。當然,對於幾個約定名詞,handler函數,handler處理器和handler對象的理解,是掌握它們關係的關鍵因素,而handler處理器和handler對象的關係,恰恰又是go接口使用的經典例子,讓go具有一些動態類型的特性。

瞭解了http服務如何構建之後,處理請求和返回響應就是下一個故事。而實現處理邏輯恰恰在我們一直在強調的ServeHTTP接口方法中。



作者:人世間
鏈接:https://www.jianshu.com/p/16210100d43d
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章