03.05 Go語言經典庫使用分析(七) 高性能可擴展 HTTP 路由 httprouter

Go語言(golang)的一個很大的優勢,就是很容易的開發出網絡後臺服務,而且性能快,效率高。在開發後端HTTP網絡應用服務的時候,我們需要處理很多HTTP的請求訪問,比如常見的API服務,我們就要處理很多HTTP請求,然後把處理的信息返回給使用者。對於這類需求,Golang提供了內置的net/http包幫我們來處理這些HTTP請求,讓我們可以比較方便的開發一個HTTP服務。


Go語言經典庫使用分析(七) 高性能可擴展 HTTP 路由 httprouter


net/http

<code>func main() {
\thttp.HandleFunc("/",Index)

\tlog.Fatal(http.ListenAndServe(":8080", nil))
}

func Index(w http.ResponseWriter, r *http.Request){
\tfmt.Fprint(w,"Blog:www.flysnow.org\\nwechat:flysnow_org")
}
/<code>

這是net/http包中一個經典的HTTP服務實現,我們運行後打開http://localhost:8080,就可以看到如下信息:

<code>Blog:www.flysnow.org
wechat:flysnow_org
/<code>

顯示的關鍵就是我們http.HandleFunc函數,我們通過該函數註冊了對路徑/的處理函數Index,所有才會看到上面的顯示信息。那麼這個http.HandleFunc他是如何註冊Index函數的呢?下面看看這個函數的源代碼。

<code>// DefaultServeMux is the default ServeMux used by Serve.
var DefaultServeMux = &defaultServeMux

var defaultServeMux ServeMux

func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
\tDefaultServeMux.HandleFunc(pattern, handler)
}

type ServeMux struct {
\tmu sync.RWMutex
\tm map[string]muxEntry
\thosts bool // whether any patterns contain hostnames
}

/<code>

看以上的源代碼,是存在一個默認的DefaultServeMux路由的,這個DefaultServeMux類型是ServeMux,我們註冊的路徑函數信息都被存入ServeMux的m字段中,以便處理HTTP請求的時候使用。

DefaultServeMux.HandleFunc函數最終會調用ServeMux.Handle函數。

<code>func (mux *ServeMux) Handle(pattern string, handler Handler) {
\t//省略加鎖和判斷代碼

\tif mux.m == nil {
\t\tmux.m = make(map[string]muxEntry)
\t}
\t//把我們註冊的路徑和相應的處理函數存入了m字段中
\tmux.m[pattern] = muxEntry{h: handler, pattern: pattern}

\tif pattern[0] != '/' {
\t\tmux.hosts = true
\t}
}
/<code>

這下應該明白了,註冊的路徑和相應的處理函數都存入了m字段中。


Go語言經典庫使用分析(七) 高性能可擴展 HTTP 路由 httprouter


既然註冊存入了相應的信息,那麼在處理HTTP請求的時候,就可以使用了。Go語言的net/http更底層細節就不詳細分析了,我們只要知道處理HTTP請求的時候,會調用Handler接口的ServeHTTP方法,而ServeMux正好實現了Handler。

<code>func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) {
\t//省略一些無關代碼
\t
\th, _ := mux.Handler(r)
\th.ServeHTTP(w, r)
}
/<code>

上面代碼中的mux.Handler會獲取到我們註冊的Index函數,然後執行它,具體mux.Handler的詳細實現不再分析了,大家可以自己看下源代碼。

現在我們可以總結下net/http包對HTTP請求的處理。

<code>HTTP請求->ServeHTTP函數->ServeMux的Handler方法->Index函數/<code>

這就是整個一條請求處理鏈,現在我們明白了net/http裡對HTTP請求的原理。

net/http 的不足

我們自己在使用內置的net/http的默認路徑處理HTTP請求的時候,會發現很多不足,比如:

  1. 不能單獨的對請求方法(POST,GET等)註冊特定的處理函數
  2. 不支持Path變量參數
  3. 不能自動對Path進行校準
  4. 性能一般
  5. 擴展性不足
  6. ……

那麼如何解決以上問題呢?一個辦法就是自己寫一個處理HTTP請求的路由,因為從上面的源代碼我們知道,net/http用的是默認的路徑。

<code>//這個是我們啟動HTTP服務的函數,最後一個handler參數是nil
http.ListenAndServe(":8080", nil)

func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
\thandler := sh.srv.Handler
\t
\t//這個判斷成立,因為我們傳遞的是nil
\tif handler == nil {
\t\thandler = DefaultServeMux
\t}
\t//省略了一些代碼
\thandler.ServeHTTP(rw, req)
}
/<code>

通過以上的代碼分析,我們自己在通過http.ListenAndServe函數啟動一個HTTP服務的時候,最後一個handler的值是nil,所以上面的nil判斷成立,使用的就是默認的路由DefaultServeMux。

現在我們就知道如何使用自己定義的路由了,那就是給http.ListenAndServe的最後一個參數handler傳一個自定義的路由,比如:

<code>type CustomMux struct {

}

func (cm *CustomMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
\tfmt.Fprint(w,"Blog:www.flysnow.org\\nwechat:flysnow_org")
}

func main() {
\tlog.Fatal(http.ListenAndServe(":8080", &CustomMux{}))
}
/<code>

這個自定義的CustomMux就是我們的路由,它顯示了和使用net/http演示的例子一樣的功能。

現在我們改變下代碼,只有GET方法才會顯示以上信息。

<code>func (cm *CustomMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
\tif r.Method == "GET" {
\t\tfmt.Fprint(w,"Blog:www.flysnow.org\\nwechat:flysnow_org")
\t} else {
\t\tfmt.Fprint(w,"bad http method request")
\t}
}
/<code>

只需要改變下ServeHTTP方法的處理邏輯即可,現在我們可以換不同的請求方法試試,就會顯示不同的內容。

這個就是自定義,我們可以通過擴展ServeHTTP方法的實現來添加我們想要的任何功能,包括上面章節列出來的net/http的不足都可以解決,不過我們無需這麼麻煩,因為開源大牛已經幫我們做了這些事情,它就是 github.com/julienschmidt/httprouter


Go語言經典庫使用分析(七) 高性能可擴展 HTTP 路由 httprouter


httprouter

httprouter 是一個高性能、可擴展的HTTP路由,上面我們列舉的net/http默認路由的不足,都被httprouter 實現,我們先用一個例子,認識下 httprouter 這個強大的 HTTP 路由。

<code>package main

import (

\t"fmt"
\t"github.com/julienschmidt/httprouter"
\t"net/http"
\t"log"
)

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
\tfmt.Fprintf(w, "Blog:%s \\nWechat:%s","www.flysnow.org","flysnow_org")
}
func main() {
\trouter := httprouter.New()
\trouter.GET("/", Index)

\tlog.Fatal(http.ListenAndServe(":8080", router))
}
/<code>

這個例子,實現了在GET請求/路徑時,會顯示如下信息:

<code>Blog:www.flysnow.org
wechat:flysnow_org/<code>

在這個例子中,首先通過httprouter.New()生成了一個*Router路由指針,然後使用GET方法註冊一個適配/路徑的Index函數,最後*Router作為參數傳給ListenAndServe函數啟動HTTP服務即可。

其實不止是GET方法,httprouter 為所有的HTTP Method 提供了快捷的使用方式,只需要調用對應的方法即可。

<code>func (r *Router) GET(path string, handle Handle) {
\tr.Handle("GET", path, handle)
}

func (r *Router) HEAD(path string, handle Handle) {
\tr.Handle("HEAD", path, handle)
}

func (r *Router) OPTIONS(path string, handle Handle) {
\tr.Handle("OPTIONS", path, handle)
}

func (r *Router) POST(path string, handle Handle) {

\tr.Handle("POST", path, handle)
}

func (r *Router) PUT(path string, handle Handle) {
\tr.Handle("PUT", path, handle)
}

func (r *Router) PATCH(path string, handle Handle) {
\tr.Handle("PATCH", path, handle)
}

func (r *Router) DELETE(path string, handle Handle) {
\tr.Handle("DELETE", path, handle)
}
/<code>

以上這些方法都是 httprouter 支持的,我們可以非常靈活的根據需要,使用對應的方法,這樣就解決了net/http默認路由的問題。

httprouter 命名參數

現代的API,基本上都是Restful API,httprouter提供的命名參數的支持,可以很方便的幫助我們開發Restful API。比如我們設計的API/user/flysnow,這這樣一個URL,可以查看flysnow這個用戶的信息,如果要查看其他用戶的,比如zhangsan,我們只需要訪問API/user/zhangsan即可。

現在我們可以發現,其實這是一種URL匹配模式,我們可以把它總結為/user/:name,這是一個通配符,看個例子。

<code>func UserInfo(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
\tfmt.Fprintf(w, "hello, %s!\\n", ps.ByName("name"))
}

func main() {
\trouter := httprouter.New()
\trouter.GET("/user/:name",UserInfo)

\tlog.Fatal(http.ListenAndServe(":8080", router))

}
/<code>

當我們運行,在瀏覽器裡輸入http://localhost:8080/user/flysnow時,就會顯示hello, flysnow!.

通過上面的代碼示例,可以看到,路徑的參數是以:開頭的,後面緊跟著變量名,比如:name,然後在UserInfo這個處理函數中,通過httprouter.Params的ByName獲取對應的值。

:name這種匹配模式,是精準匹配的,同時只能匹配一個,比如:

<code>Pattern: /user/:name

/user/gordon 匹配
/user/you 匹配
/user/gordon/profile 不匹配
/user/ 不匹配/<code>

因為httprouter這個路由就是單一匹配的,所以當我們使用命名參數的時候,一定要注意,是否有其他註冊的路由和命名參數的路由,匹配同一個路徑,比如/user/new這個路由和/user/:name就是衝突的,不能同時註冊。

這裡稍微提下httprouter的另外一種通配符模式,就是把:換成*,也就是*name,這是一種匹配所有的模式,不常用,比如:

<code>Pattern: /user/*name

/user/gordon 匹配
/user/you 匹配
/user/gordon/profile 匹配
/user/ 匹配/<code>

因為是匹配所有的*模式,所以只要*前面的路徑匹配,就是匹配的,不管路徑多長,有幾層,都匹配。

httprouter兼容http.Handler

通過上面的例子,我們應該已經發現,GET方法的handle,並不是我們熟悉的http.Handler,它是httprouter自定義的,相比http.Handler多了一個通配符參數的支持。

<code>type Handle func(http.ResponseWriter, *http.Request, Params)
/<code>

自定義的Handle,唯一的目的就是支持通配符參數,如果你的HTTP服務裡,有些路由沒有用到通配符參數,那麼可以使用原生的http.Handler,httprouter是兼容支持的,這也為我們從net/http的方式,升級為httprouter路由提供了方便,會高效很多。

<code>func (r *Router) Handler(method, path string, handler http.Handler) {
\tr.Handle(method, path,
\t\tfunc(w http.ResponseWriter, req *http.Request, _ Params) {
\t\t\thandler.ServeHTTP(w, req)
\t\t},
\t)
}

func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) {
\tr.Handler(method, path, handler)
}
/<code>

httprouter通過Handler和HandlerFunc兩個函數,提供了兼容http.Handler和http.HandlerFunc的完美支持。從以上源代碼中,我們可以看出,實現的方式也比較簡單,就是做了一個http.Handler到httprouter.Handle的轉換,捨棄了通配符參數的支持。

Handler處理鏈

得益於http.Handler的模式,我們可以把不同的http.Handler組成一個處理鏈,httprouter.Router也是實現了http.Handler的,所以它也可以作為http.Handler處理鏈的一部分,比如和Negroni、 Gorilla handlers這兩個庫配合使用,關於這兩個庫的介紹,可以參考我以前寫的文章。

Go語言經典庫使用分析(五)| Negroni 中間件(一) Go語言經典庫使用分析(三)| Gorilla Handlers 詳細介紹

這裡使用一個官方的例子,作為Handler處理鏈的演示。

比如對多個不同的二級域名,進行不同的路由處理。

<code>//一個新類型,用於存儲域名對應的路由
type HostSwitch map[string]http.Handler

//實現http.Handler接口,進行不同域名的路由分發
func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) {

//根據域名獲取對應的Handler路由,然後調用處理(分發機制)
\tif handler := hs[r.Host]; handler != nil {
\t\thandler.ServeHTTP(w, r)
\t} else {
\t\thttp.Error(w, "Forbidden", 403)
\t}
}

func main() {
//聲明兩個路由
\tplayRouter := httprouter.New()

\tplayRouter.GET("/", PlayIndex)
\t
\ttoolRouter := httprouter.New()
\ttoolRouter.GET("/", ToolIndex)

//分別用於處理不同的二級域名
\ths := make(HostSwitch)
\ths["play.flysnow.org:12345"] = playRouter
\ths["tool.flysnow.org:12345"] = toolRouter

//HostSwitch實現了http.Handler,所以可以直接用
\tlog.Fatal(http.ListenAndServe(":12345", hs))
}
/<code>

以上就是一個簡單的,針對不同域名,使用不同路由的例子,代碼中的註釋比較詳細了,這裡就不一一解釋了。這個例子中,HostSwitch和httprouter.Router這兩個http.Handler就組成了一個http.Handler處理鏈。

httprouter 靜態文件服務

httprouter提供了很方便的靜態文件服務,可以把一個目錄託管在服務器上,以供訪問。

<code>\trouter.ServeFiles("/static/*filepath",http.Dir("./"))
/<code>

只需要這一句核心代碼即可,這個就是把當前目錄託管在服務器上,以供訪問,訪問路徑是/static。

使用ServeFiles需要注意的是,第一個參數路徑,必須要以/*filepath,因為要獲取我們要訪問的路徑信息。

<code>func (r *Router) ServeFiles(path string, root http.FileSystem) { 

\tif len(path) < 10 || path[len(path)-10:] != "/*filepath" {
\t\tpanic("path must end with /*filepath in path '" + path + "'")
\t}

\tfileServer := http.FileServer(root)

\tr.GET(path, func(w http.ResponseWriter, req *http.Request, ps Params) {
\t\treq.URL.Path = ps.ByName("filepath")
\t\tfileServer.ServeHTTP(w, req)
\t})
}
/<code>

這是源代碼實現,我們發現,最後還是一個GET請求服務,通過http.FileServer把filepath的路徑的內容顯示出來(如果路徑是個目錄則列出目錄文件;如果路徑是文件,則顯示內容)。

通過上面的源代碼,我們也可以知道,*filepath這個通配符是為了獲取要放問的文件路徑,所以要符合預定,不然就會panic。

httprouter 異常捕獲

很少有路由支持這個功能的,httprouter允許使用者,設置PanicHandler用於處理HTTP請求中發生的panic。

<code>func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
\tpanic("故意拋出的異常")
}

func main() {
\trouter := httprouter.New()
\trouter.GET("/", Index)
\trouter.PanicHandler = func(w http.ResponseWriter, r *http.Request, v interface{}) {
\t\tw.WriteHeader(http.StatusInternalServerError)
\t\tfmt.Fprintf(w, "error:%s",v)
\t}

\tlog.Fatal(http.ListenAndServe(":8080", router))

}
/<code>

演示例子中,我們通過設置router.PanicHandler來處理發生的panic,處理辦法是打印出來異常信息。然後故意在Index函數中拋出一個painc,然後我們運行測試,會看到異常信息。

這是一種非常好的方式,可以讓我們對painc進行統一處理,不至於因為漏掉的panic影響用戶使用。

小結

httprouter還有不少有用的小功能,比如對404進行處理,我們通過設置Router.NotFound來實現,我們看看Router這個結構體的配置,可以發現更多有用的功能。

<code>type Router struct {
//是否通過重定向,給路徑自定加斜槓
\tRedirectTrailingSlash bool
//是否通過重定向,自動修復路徑,比如雙斜槓等自動修復為單斜槓
\tRedirectFixedPath bool
//是否檢測當前請求的方法被允許
\tHandleMethodNotAllowed bool
\t//是否自定答覆OPTION請求
\tHandleOPTIONS bool
//404默認處理
\tNotFound http.Handler
//不被允許的方法默認處理
\tMethodNotAllowed http.Handler
//異常統一處理
\tPanicHandler func(http.ResponseWriter, *http.Request, interface{})

}
/<code>

這些字段都是導出的(export),我們可以直接設置,來達到我們的目的。

httprouter是一個高性能,低內存佔用的路由,它使用radix tree實現存儲和匹配查找,所以效率非常高,內存佔用也很低。關於radix tree大家可以查看相關的資料。

httprouter因為實現了http.Handler,所以可擴展性非常好,可以和其他庫、中間件結合使用,gin這個web框架就是使用的自定義的httprouter。


分享到:


相關文章: