Go語言經典庫使用分析(七)| 高性能可擴展 HTTP 路由 httprouter
- 2020 年 2 月 10 日
- 筆記
Go語言(golang)的一個很大的優勢,就是很容易的開發出網路後台服務,而且性能快,效率高。在開發後端HTTP網路應用服務的時候,我們需要處理很多HTTP的請求訪問,比如常見的API服務,我們就要處理很多HTTP請求,然後把處理的資訊返回給使用者。對於這類需求,Golang提供了內置的net/http包幫我們來處理這些HTTP請求,讓我們可以比較方便的開發一個HTTP服務。
net/http
func main() { http.HandleFunc("/",Index) log.Fatal(http.ListenAndServe(":8080", nil)) } func Index(w http.ResponseWriter, r *http.Request){ fmt.Fprint(w,"Blog:www.flysnow.orgnwechat:flysnow_org") } 這是net/http包中一個經典的HTTP服務實現,我們運行後打開http://localhost:8080,就可以看到如下資訊:
Blog:www.flysnow.org wechat:flysnow_org
顯示的關鍵就是我們http.HandleFunc函數,我們通過該函數註冊了對路徑/的處理函數Index,所有才會看到上面的顯示資訊。那麼這個http.HandleFunc他是如何註冊Index函數的呢?下面看看這個函數的源程式碼。
// DefaultServeMux is the default ServeMux used by Serve. var DefaultServeMux = &defaultServeMux var defaultServeMux ServeMux func HandleFunc(pattern string, handler func(ResponseWriter, *Request)) { DefaultServeMux.HandleFunc(pattern, handler) } type ServeMux struct { mu sync.RWMutex m map[string]muxEntry hosts bool // whether any patterns contain hostnames } 看以上的源程式碼,是存在一個默認的DefaultServeMux路由的,這個DefaultServeMux類型是ServeMux,我們註冊的路徑函數資訊都被存入ServeMux的m欄位中,以便處理HTTP請求的時候使用。
DefaultServeMux.HandleFunc函數最終會調用ServeMux.Handle函數。
func (mux *ServeMux) Handle(pattern string, handler Handler) { //省略加鎖和判斷程式碼 if mux.m == nil { mux.m = make(map[string]muxEntry) } //把我們註冊的路徑和相應的處理函數存入了m欄位中 mux.m[pattern] = muxEntry{h: handler, pattern: pattern} if pattern[0] != '/' { mux.hosts = true } } 這下應該明白了,註冊的路徑和相應的處理函數都存入了m欄位中。
既然註冊存入了相應的資訊,那麼在處理HTTP請求的時候,就可以使用了。Go語言的net/http更底層細節就不詳細分析了,我們只要知道處理HTTP請求的時候,會調用Handler介面的ServeHTTP方法,而ServeMux正好實現了Handler。
func (mux *ServeMux) ServeHTTP(w ResponseWriter, r *Request) { //省略一些無關程式碼 h, _ := mux.Handler(r) h.ServeHTTP(w, r) } 上面程式碼中的mux.Handler會獲取到我們註冊的Index函數,然後執行它,具體mux.Handler的詳細實現不再分析了,大家可以自己看下源程式碼。
現在我們可以總結下net/http包對HTTP請求的處理。
HTTP請求->ServeHTTP函數->ServeMux的Handler方法->Index函數
這就是整個一條請求處理鏈,現在我們明白了net/http里對HTTP請求的原理。
net/http 的不足
我們自己在使用內置的net/http的默認路徑處理HTTP請求的時候,會發現很多不足,比如:
- 不能單獨的對請求方法(POST,GET等)註冊特定的處理函數
- 不支援Path變數參數
- 不能自動對Path進行校準
- 性能一般
- 擴展性不足
- ……
那麼如何解決以上問題呢?一個辦法就是自己寫一個處理HTTP請求的路由,因為從上面的源程式碼我們知道,net/http用的是默認的路徑。
//這個是我們啟動HTTP服務的函數,最後一個handler參數是nil http.ListenAndServe(":8080", nil) func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) { handler := sh.srv.Handler //這個判斷成立,因為我們傳遞的是nil if handler == nil { handler = DefaultServeMux } //省略了一些程式碼 handler.ServeHTTP(rw, req) } 通過以上的程式碼分析,我們自己在通過http.ListenAndServe函數啟動一個HTTP服務的時候,最後一個handler的值是nil,所以上面的nil判斷成立,使用的就是默認的路由DefaultServeMux。
現在我們就知道如何使用自己定義的路由了,那就是給http.ListenAndServe的最後一個參數handler傳一個自定義的路由,比如:
type CustomMux struct { } func (cm *CustomMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w,"Blog:www.flysnow.orgnwechat:flysnow_org") } func main() { log.Fatal(http.ListenAndServe(":8080", &CustomMux{})) } 這個自定義的CustomMux就是我們的路由,它顯示了和使用net/http演示的例子一樣的功能。
現在我們改變下程式碼,只有GET方法才會顯示以上資訊。
func (cm *CustomMux) ServeHTTP(w http.ResponseWriter, r *http.Request) { if r.Method == "GET" { fmt.Fprint(w,"Blog:www.flysnow.orgnwechat:flysnow_org") } else { fmt.Fprint(w,"bad http method request") } } 只需要改變下ServeHTTP方法的處理邏輯即可,現在我們可以換不同的請求方法試試,就會顯示不同的內容。
這個就是自定義,我們可以通過擴展ServeHTTP方法的實現來添加我們想要的任何功能,包括上面章節列出來的net/http的不足都可以解決,不過我們無需這麼麻煩,因為開源大牛已經幫我們做了這些事情,它就是 github.com/julienschmidt/httprouter
httprouter
httprouter 是一個高性能、可擴展的HTTP路由,上面我們列舉的net/http默認路由的不足,都被httprouter 實現,我們先用一個例子,認識下 httprouter 這個強大的 HTTP 路由。
package main import ( "fmt" "github.com/julienschmidt/httprouter" "net/http" "log" ) func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { fmt.Fprintf(w, "Blog:%s nWechat:%s","www.flysnow.org","flysnow_org") } func main() { router := httprouter.New() router.GET("/", Index) log.Fatal(http.ListenAndServe(":8080", router)) } 這個例子,實現了在GET請求/路徑時,會顯示如下資訊:
Blog:www.flysnow.org wechat:flysnow_org
在這個例子中,首先通過httprouter.New()生成了一個*Router路由指針,然後使用GET方法註冊一個適配/路徑的Index函數,最後*Router作為參數傳給ListenAndServe函數啟動HTTP服務即可。
其實不止是GET方法,httprouter 為所有的HTTP Method 提供了快捷的使用方式,只需要調用對應的方法即可。
func (r *Router) GET(path string, handle Handle) { r.Handle("GET", path, handle) } func (r *Router) HEAD(path string, handle Handle) { r.Handle("HEAD", path, handle) } func (r *Router) OPTIONS(path string, handle Handle) { r.Handle("OPTIONS", path, handle) } func (r *Router) POST(path string, handle Handle) { r.Handle("POST", path, handle) } func (r *Router) PUT(path string, handle Handle) { r.Handle("PUT", path, handle) } func (r *Router) PATCH(path string, handle Handle) { r.Handle("PATCH", path, handle) } func (r *Router) DELETE(path string, handle Handle) { r.Handle("DELETE", path, handle) } 以上這些方法都是 httprouter 支援的,我們可以非常靈活的根據需要,使用對應的方法,這樣就解決了net/http默認路由的問題。
httprouter 命名參數
現代的API,基本上都是Restful API,httprouter提供的命名參數的支援,可以很方便的幫助我們開發Restful API。比如我們設計的API/user/flysnow,這這樣一個URL,可以查看flysnow這個用戶的資訊,如果要查看其他用戶的,比如zhangsan,我們只需要訪問API/user/zhangsan即可。
現在我們可以發現,其實這是一種URL匹配模式,我們可以把它總結為/user/:name,這是一個通配符,看個例子。
func UserInfo(w http.ResponseWriter, r *http.Request, ps httprouter.Params) { fmt.Fprintf(w, "hello, %s!n", ps.ByName("name")) } func main() { router := httprouter.New() router.GET("/user/:name",UserInfo) log.Fatal(http.ListenAndServe(":8080", router)) } 當我們運行,在瀏覽器里輸入http://localhost:8080/user/flysnow時,就會顯示hello, flysnow!.
通過上面的程式碼示例,可以看到,路徑的參數是以:開頭的,後面緊跟著變數名,比如:name,然後在UserInfo這個處理函數中,通過httprouter.Params的ByName獲取對應的值。
:name這種匹配模式,是精準匹配的,同時只能匹配一個,比如:
Pattern: /user/:name /user/gordon 匹配 /user/you 匹配 /user/gordon/profile 不匹配 /user/ 不匹配
因為httprouter這個路由就是單一匹配的,所以當我們使用命名參數的時候,一定要注意,是否有其他註冊的路由和命名參數的路由,匹配同一個路徑,比如/user/new這個路由和/user/:name就是衝突的,不能同時註冊。
這裡稍微提下httprouter的另外一種通配符模式,就是把:換成*,也就是*name,這是一種匹配所有的模式,不常用,比如:
Pattern: /user/*name /user/gordon 匹配 /user/you 匹配 /user/gordon/profile 匹配 /user/ 匹配
因為是匹配所有的*模式,所以只要*前面的路徑匹配,就是匹配的,不管路徑多長,有幾層,都匹配。
httprouter兼容http.Handler
通過上面的例子,我們應該已經發現,GET方法的handle,並不是我們熟悉的http.Handler,它是httprouter自定義的,相比http.Handler多了一個通配符參數的支援。
type Handle func(http.ResponseWriter, *http.Request, Params)
自定義的Handle,唯一的目的就是支援通配符參數,如果你的HTTP服務里,有些路由沒有用到通配符參數,那麼可以使用原生的http.Handler,httprouter是兼容支援的,這也為我們從net/http的方式,升級為httprouter路由提供了方便,會高效很多。
func (r *Router) Handler(method, path string, handler http.Handler) { r.Handle(method, path, func(w http.ResponseWriter, req *http.Request, _ Params) { handler.ServeHTTP(w, req) }, ) } func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) { r.Handler(method, path, handler) } 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處理鏈的演示。
比如對多個不同的二級域名,進行不同的路由處理。
//一個新類型,用於存儲域名對應的路由 type HostSwitch map[string]http.Handler //實現http.Handler介面,進行不同域名的路由分發 func (hs HostSwitch) ServeHTTP(w http.ResponseWriter, r *http.Request) { //根據域名獲取對應的Handler路由,然後調用處理(分發機制) if handler := hs[r.Host]; handler != nil { handler.ServeHTTP(w, r) } else { http.Error(w, "Forbidden", 403) } } func main() { //聲明兩個路由 playRouter := httprouter.New() playRouter.GET("/", PlayIndex) toolRouter := httprouter.New() toolRouter.GET("/", ToolIndex) //分別用於處理不同的二級域名 hs := make(HostSwitch) hs["play.flysnow.org:12345"] = playRouter hs["tool.flysnow.org:12345"] = toolRouter //HostSwitch實現了http.Handler,所以可以直接用 log.Fatal(http.ListenAndServe(":12345", hs)) } 以上就是一個簡單的,針對不同域名,使用不同路由的例子,程式碼中的注釋比較詳細了,這裡就不一一解釋了。這個例子中,HostSwitch和httprouter.Router這兩個http.Handler就組成了一個http.Handler處理鏈。
httprouter 靜態文件服務
httprouter提供了很方便的靜態文件服務,可以把一個目錄託管在伺服器上,以供訪問。
router.ServeFiles("/static/*filepath",http.Dir("./")) 只需要這一句核心程式碼即可,這個就是把當前目錄託管在伺服器上,以供訪問,訪問路徑是/static。
使用ServeFiles需要注意的是,第一個參數路徑,必須要以/*filepath,因為要獲取我們要訪問的路徑資訊。
func (r *Router) ServeFiles(path string, root http.FileSystem) { if len(path) < 10 || path[len(path)-10:] != "/*filepath" { panic("path must end with /*filepath in path '" + path + "'") } fileServer := http.FileServer(root) r.GET(path, func(w http.ResponseWriter, req *http.Request, ps Params) { req.URL.Path = ps.ByName("filepath") fileServer.ServeHTTP(w, req) }) } 這是源程式碼實現,我們發現,最後還是一個GET請求服務,通過http.FileServer把filepath的路徑的內容顯示出來(如果路徑是個目錄則列出目錄文件;如果路徑是文件,則顯示內容)。
通過上面的源程式碼,我們也可以知道,*filepath這個通配符是為了獲取要放問的文件路徑,所以要符合預定,不然就會panic。
httprouter 異常捕獲
很少有路由支援這個功能的,httprouter允許使用者,設置PanicHandler用於處理HTTP請求中發生的panic。
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) { panic("故意拋出的異常") } func main() { router := httprouter.New() router.GET("/", Index) router.PanicHandler = func(w http.ResponseWriter, r *http.Request, v interface{}) { w.WriteHeader(http.StatusInternalServerError) fmt.Fprintf(w, "error:%s",v) } log.Fatal(http.ListenAndServe(":8080", router)) } 演示例子中,我們通過設置router.PanicHandler來處理髮生的panic,處理辦法是列印出來異常資訊。然後故意在Index函數中拋出一個painc,然後我們運行測試,會看到異常資訊。
這是一種非常好的方式,可以讓我們對painc進行統一處理,不至於因為漏掉的panic影響用戶使用。
小結
httprouter還有不少有用的小功能,比如對404進行處理,我們通過設置Router.NotFound來實現,我們看看Router這個結構體的配置,可以發現更多有用的功能。
type Router struct { //是否通過重定向,給路徑自定加斜杠 RedirectTrailingSlash bool //是否通過重定向,自動修復路徑,比如雙斜杠等自動修復為單斜杠 RedirectFixedPath bool //是否檢測當前請求的方法被允許 HandleMethodNotAllowed bool //是否自定答覆OPTION請求 HandleOPTIONS bool //404默認處理 NotFound http.Handler //不被允許的方法默認處理 MethodNotAllowed http.Handler //異常統一處理 PanicHandler func(http.ResponseWriter, *http.Request, interface{}) } 這些欄位都是導出的(export),我們可以直接設置,來達到我們的目的。
httprouter是一個高性能,低記憶體佔用的路由,它使用radix tree實現存儲和匹配查找,所以效率非常高,記憶體佔用也很低。關於radix tree大家可以查看相關的資料。
httprouter因為實現了http.Handler,所以可擴展性非常好,可以和其他庫、中間件結合使用,gin這個web框架就是使用的自定義的httprouter。
本文為原創文章,轉載註明出處,歡迎掃碼關注公眾號
flysnow_org或者網站asf http://www.flysnow.org/