1. 程式人生 > 其它 >Golang構建HTTP服務(二)--- Handler,ServeMux與中介軟體

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

左邊的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介面方法中。

接下來將會更詳細的討論請求和響應相關的函式物件。


關於作者

作者: 人世間 來源: 簡書