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

收藏待读

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

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 ,我們註冊的路徑函數信息都被存入 ServeMuxm 字段中,以便處理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請求的時候,會發現很多不足,比如:

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

那麼如何解決以上問題呢?一個辦法就是自己寫一個處理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.ParamsByName 獲取對應的值。

: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通過 HandlerHandlerFunc 兩個函數,提供了兼容 http.Handlerhttp.HandlerFunc 的完美支持。從以上源代碼中,我們可以看出,實現的方式也比較簡單,就是做了一個 http.Handlerhttprouter.Handle 的轉換,捨棄了通配符參數的支持。

Handler處理鏈

得益於 http.Handler 的模式,我們可以把不同的 http.Handler 組成一個處理鏈, httprouter.Router 也是實現了 http.Handler 的,所以它也可以作為 http.Handler 處理鏈的一部分,比如和 NegroniGorilla 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))
}

以上就是一個簡單的,針對不同域名,使用不同路由的例子,代碼中的注釋比較詳細了,這裡就不一一解釋了。這個例子中, HostSwitchhttprouter.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.FileServerfilepath 的路徑的內容顯示出來(如果路徑是個目錄則列出目錄文件;如果路徑是文件,則顯示內容)。

通過上面的源代碼,我們也可以知道, *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/ ,第一時間看後續精彩文章。覺得好的話,請猛擊文章右下角「好看」,感謝支持。

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

原文 : Go語言中文網

相關閱讀

免责声明:本文内容来源于studygolang.com,已注明原文出处和链接,文章观点不代表立场,如若侵犯到您的权益,或涉不实谣言,敬请向我们提出检举。