聊一聊Gin Web框架以前,看一眼httprouter

HTTP Router

前言: Gin的詞源是金酒, 又稱琴酒, 是來自荷蘭的一種烈性酒。node

在Go中,有一個常常說起的web框架,就是gin web,具有高性能,可靈活定製化的特色,既然它是如此被看好,在深刻了解它以前,不妨先看下他是基於什麼實現的。git

飲酒思源:httprouter

根據Git做者描述,Gin的高性能得益於一個叫httprouter的框架生成的,順着源頭看,咱們先針對httprouter, 從HTTP路由開始,做爲Gin框架的引入篇。github

Router路由

在扎堆深刻以前, 先梳理一下路由的概念:
路由: 大概意思是經過轉發數據包實現互聯,好比生活中常見的物理路由器,是指內網與外網之間信息流的分配。
同理,軟件層面也有路由的概念,通常暴露在業務層之上,用於轉發請求到合適的邏輯處理器。golang

The router matches incoming requests by the request method and the path.web

程序的應用上,常見的如對外服務器把外部請求打到Nginx網關,再路由(轉發)到內部服務或者內部服務的「控制層」,如Java的springMVC,Go的原生router等對不一樣請求轉發到不一樣業務層。
或者再具體化點說,好比不一樣參數調用同名方法,如Java的重載,也能夠理解爲程序根據參數的不一樣路由到相應不一樣的方法。spring

httprouter

功能現象:

Git的README文檔上,httprouter開門見山的展現了它的一個常見功能,
啓動一個HTTP服務器,而且監聽8080端口,對請求執行參數解析,僅僅幾行代碼,當我第一次見到這種實現時候,確實以爲go這種實現至關優雅。shell

router.GET("/", Index)
//傳入參數name
router.GET("/hello/:name", Hello)

func Hello(w http.ResponseWriter, r *http.Request) {
    //經過http.Request結構的上下文能夠拿到請求url所帶的參數
    params := httprouter.ParamsFromContext(r.Context())
    fmt.Fprintf(w, "hello, %s!\n", params.ByName("name"))
}

//啓動監聽
http.ListenAndServe(":8080", router)

複製代碼

接口實現

在觀察瞭如何創建一個監聽程序以後,挖掘這種優雅是如何封裝實現以前,咱們要先了解,在原生Go中,每一個Router路由結構都實現了http.Handler接口,Handler只有一個方法體,就是ServerHTTP,它只有一個功能,就是處理請求,作出響應。bash

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}
複製代碼

題外話,Go中比較傾向於KISS或者單一職責,把每一個接口的功能都單一化,有須要再進行組合,用組合代替繼承,後續會把它看成一個編碼規範來看。服務器

net\http\server

// The HandlerFunc type is an adapter to allow the use of
// ordinary functions as HTTP handlers. If f is a function
// with the appropriate signature, HandlerFunc(f) is a
// Handler that calls f.
type HandlerFunc func(ResponseWriter, *Request) // ServeHTTP calls f(w, r). func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
    f(w, r)
}
複製代碼

能夠看到,在Go原生庫中,ServeHTTP()實現體HandlerFunc就是個func函數類型,具體實現又是直接套用HandlerFunc進行處理,我沒有在套娃哈,是否是有種「我實現我本身」的趕腳。app

All in all, 咱們先拋開第三方庫的封裝,複習一下標準庫,假如咱們想用原生http\server包搭建一個HTTP處理邏輯,咱們通常能夠
方式1:

  1. 定義一個參數列表是(ResponseWriter, *Request)的函數
  2. 將其註冊到http.Server做爲其Handler成員
  3. 調用ListenAndServerhttp.Server進行監聽

方式2:

  1. 定義一個結構,而且實現接口ServeHTTP(w http.ResponseWriter, req *http.Request)
  2. 將其註冊到http.Server做爲其Handler成員
  3. 調用ListenAndServerhttp.Server進行監聽

示例以下:

//方式1
func SelfHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprint(w, "<h1>Hello!</h1>")
}

//方式2
type HelloHandler struct {
}
//HelloHandler實現ServeHTTP()接口
func (* HelloHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    fmt.Fprint(w, "<h1>Hello!</h1>")
}

s := &http.Server{
    Addr:           ":8080",
    //方式1
    Handler:        SelfHandler,
    //方式2
    //Handler: &HelloHandler{},
    ReadTimeout:    10 * time.Second,
    WriteTimeout:   10 * time.Second,
    MaxHeaderBytes: 1 << 20,
}
s.ListenAndServe()
複製代碼

拋磚引玉:
以上就是Go標準庫實現http服務的經常使用用法,如今進行拓展,假如咱們須要經過url去獲取參數,如Get請求,localhost:8080/abc/1 Q: 咱們如何拿到abc或者1呢?
A: 其實有個相對粗暴的方法,就是硬解:

  • 利用net/urlParse()函數把8080後面的那一段提取出來
  • 使用stings.split(ul, "/")
  • 利用下標進行參數範圍

示例以下

func TestStartHelloWithHttp(t *testing.T)  {
    //fmt.Println(path.Base(ul))
    
    ul := `https://localhost:8080/pixeldin/123`
    parse, e := url.Parse(ul)
    if e != nil {
    	log.Fatalf("%v", e)
    }
    //fmt.Println(parse.Path) // "/pixeldin/123"
    name := GetParamFromUrl(parse.Path, 1)
    id := GetParamFromUrl(parse.Path, 2)
    fmt.Println("name: " + name + ", id: " + id)	
}

//指定下標返回相對url的值
func GetParamFromUrl(base string, index int) (ps string) {
    kv := strings.Split(base, "/")
    assert(index < len(kv), errors.New("index out of range."))
    return kv[index]
}

func assert(ok bool, err error)  {
    if !ok {
    	panic(err)
    }
}
複製代碼

輸出:

name: pixeldin, id: 123
複製代碼

這種辦法給人感受至關暴力,並且須要記住每一個參數的位置和對應的值,而且多個url不能統一管理起來,每次尋址都是遍歷。儘管Go標準庫也提供了一些通用函數,好比下面這個栗子:
GET方式的url: https://localhost:8080/?key=hello,
能夠經過*http.Request來獲取,這種請求方式是在url中聲明鍵值對,而後後臺根據請求key進行提取。

//摘取自:https://golangcode.com/get-a-url-parameter-from-a-request/
func handler(w http.ResponseWriter, r *http.Request) {
    
    keys, ok := r.URL.Query()["key"]
    
    if !ok || len(keys[0]) < 1 {
        log.Println("Url Param 'key' is missing")
        return
    }
    
    // Query()["key"] will return an array of items, 
    // we only want the single item.
    key := keys[0]
    
    log.Println("Url Param 'key' is: " + string(key))
}
複製代碼

可是,曾經滄海難爲水。相信你們更喜歡開篇列舉的那個例子,包括如今咱們習慣的幾個主流的框架,都傾向於利用url的位置去尋參,固然httprouter的優點確定不止在這裏,這裏只是做爲一個瞭解httprouter的切入點。

router.GET("/hello/:name", Hello)
router.GET("/hello/*name", HelloWorld)
複製代碼

到這裏先止住,後續咱們來追蹤它們封裝以後的底層實現以及是如何規劃url參數的。


httprouter對ServerHTTP()的實現

前面提到,全部路由結構都實現了http.Handler接口的ServeHTTP()方法,咱們來看下httprouter基於它的實現方式。

julienschmidt\httprouter

httprouter中,ServeHTTP() 的實現結構就叫*Router,它內部封裝了用於檢索url的tree結構,幾個經常使用的布爾選項,還有幾個也是基於http.Handler實現的默認處理器,它的實現以下:

// ServeHTTP makes the router implement the http.Handler interface.
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    if r.PanicHandler != nil {
    	defer r.recv(w, req)
    }
    
    path := req.URL.Path
    
    if root := r.trees[req.Method]; root != nil {
        //getValue()返回處理方法與參數列表
    	if handle, ps, tsr := root.getValue(path); handle != nil {
    	    //匹配執行
    		handle(w, req, ps)
    		return
    	} else if req.Method != http.MethodConnect && path != "/" {
    		//...
    	}
    }
    
    if req.Method == http.MethodOptions && r.HandleOPTIONS {
    	// Handle OPTIONS requests
    	//...
    } else if r.HandleMethodNotAllowed { // Handle 405
    	//執行默認處理器...
    }
    
    // Handle 404
    if r.NotFound != nil {
    	r.NotFound.ServeHTTP(w, req)
    } else {
    	http.NotFound(w, req)
    }
}
複製代碼

到這裏能夠大體猜想它把處理method注入到內部trees結構,利用傳入url在trees進行匹配查找,對執行鏈進行相應執行。 能夠猜想這個Router.trees包含了handle和相應的參數,接着咱們進入它的路由索引功能,來看下它是怎麼實現++url匹配++與++參數解析++的。

結構梳理:
這個trees存在tree.go源文件中,實際上是個map鍵值對,
key是HTTP methods(如GET/HEAD/POST/PUT等),method就是當前method與方法綁定上的節點

我在源碼補充些註釋,相信你們容易看懂。

// Handle registers a new request handle with the given path and method.
//
// For GET, POST, PUT, PATCH and DELETE requests the respective shortcut
// functions can be used.
// ...
func (r *Router) Handle(method, path string, handle Handle) {
    if len(path) < 1 || path[0] != '/' {
    	panic("path must begin with '/' in path '" + path + "'")
    }
    
    //首次註冊url,初始化trees
    if r.trees == nil {
    	r.trees = make(map[string]*node)
    }
    
    //綁定http methods根節點,method能夠是GET/POST/PUT等
    root := r.trees[method]
    if root == nil {
    	root = new(node)
    	r.trees[method] = root
    
    	r.globalAllowed = r.allowed("*", "")
    }
    
    //對http methods方法樹的路徑劃分
    root.addRoute(path, handle)
}
複製代碼

Router.treesmap鍵值對的value是一個node結構體,每一個HTTP METHOD 都是一個root節點,最主要的path分配是在這些節點的addRoute() 函數,
簡單理解的話, 最終那些前綴一致的路徑會被綁定到這個樹的同一個分支方向上,直接提升了索引的效率。

下面我先列舉出node幾個比較重要的成員:

type node struct {
    path      string
    //標識 path是否後續有':', 用於參數判斷
    wildChild bool
    /* 當前節點的類型,默認是0, (root/param/catchAll)分別標識(根/有參數/全路徑)*/
    nType     nodeType
    maxParams uint8
    //當前節點優先級, 掛在上面的子節點越多,優先級越高
    priority  uint32
    indices   string
    //知足前綴的子節點,能夠延申
    children  []*node
    //與當前節點綁定的處理邏輯塊
    handle    Handle
}
複製代碼

其中子節點越多,或者說綁定handle方法越多的根節點,priority優先級越高,做者有意識的對每次註冊完成進行優先級排序。 引用做者的批註:

This helps in two ways:

  • Nodes which are part of the most routing paths are evaluated first. This helps to make as much routes as possible to be reachable as fast as possible.
  • It is some sort of cost compensation. The longest reachable path (highest cost) can always be evaluated first. The following scheme visualizes the tree structure. Nodes are evaluated from top to bottom and from left to right.

優先級高的節點有利於handle的快速定位,相信比較好理解,現實中人流量密集處每每就是十字路口,相似交通樞紐。基於前綴樹的匹配,讓尋址從密集處開始,有助於提升效率。

由淺入深:
咱們先給路由器router註冊幾個做者提供的GET處理邏輯,而後開始調試,看下這個trees成員隨着url新增有什麼變化,

router.Handle("GET", "/user/ab/", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    //do nothing, just add path+handler
})

router.Handle("GET", "/user/abc/", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    //do nothing, just add path+handler
})

router.Handle(http.MethodGet, "/user/query/:name", func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
    routed = true
    want := httprouter.Params{httprouter.Param{"name", "gopher"}}
    if !reflect.DeepEqual(ps, want) {
    	t.Fatalf("wrong wildcard values: want %v, got %v", want, ps)
    }
})
複製代碼

上述操做把【RESTful的GET方法url路徑匿名函數handler】做爲router.Handler()的參數,router.Handler()的操做上面咱們已經簡單的分析過了,主要節點劃分在其中的addRoute()函數裏面,下面咱們簡單過一下它的邏輯

// 將當前url與處理邏輯放在當前節點
func (n *node) addRoute(path string, handle Handle) {
	fullPath := path
	n.priority++
	//提取當前url參數個數
	numParams := countParams(path)

	// 若是當前節點已經存在註冊鏈路
	if len(n.path) > 0 || len(n.children) > 0 {
	walk:
		for {
			// 更新最大參數個數
			if numParams > n.maxParams {
				n.maxParams = numParams
			}

			// 判斷待註冊url是否與已有url有重合,提取重合的最長下標
			// This also implies that the common prefix contains no ':' or '*'
			// since the existing key can't contain those chars.
			i := 0
			max := min(len(path), len(n.path))
			for i < max && path[i] == n.path[i] {
				i++
			}

			/* 若是進來的url匹配長度大於於當前節點已有的url,則建立子節點 好比當前節點是/user/ab/ func1, 新進來一個/user/abc/ func2,則須要建立/user/ab的子節點/ 和 c/ 樹狀以下: |-/user/ab |--------|-/ func1 |--------|-c/ func2 以後若是再註冊一個/user/a/ 與func3 則最終樹會調整爲: 優先級3 |-/user/a 優先級2 |--------|-b 優先級1 |----------|-/ func1 優先級1 |----------|-c/ func2 優先級1 |--------|-/ func3 */
			if i < len(n.path) {
				child := node{
					path:      n.path[i:],
					wildChild: n.wildChild,
					nType:     static,
					indices:   n.indices,
					children:  n.children,
					handle:    n.handle,
					priority:  n.priority - 1,
				}

				// 遍歷子節點,取最高優先級做爲父節點優先級
				for i := range child.children {
					if child.children[i].maxParams > child.maxParams {
						child.maxParams = child.children[i].maxParams
					}
				}

				n.children = []*node{&child}
				// []byte for proper unicode char conversion, see #65
				n.indices = string([]byte{n.path[i]})
				n.path = path[:i]
				n.handle = nil
				n.wildChild = false
			}

			// Make new node a child of this node
			if i < len(path) {
				path = path[i:]

				if n.wildChild {
					n = n.children[0]
					n.priority++

					// Update maxParams of the child node
					if numParams > n.maxParams {
						n.maxParams = numParams
					}
					numParams--

					// Check if the wildcard matches
					if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
						// Adding a child to a catchAll is not possible
						n.nType != catchAll &&
						// Check for longer wildcard, e.g. :name and :names
						(len(n.path) >= len(path) || path[len(n.path)] == '/') {
						continue walk
					} else {
						// Wildcard conflict
						var pathSeg string
						if n.nType == catchAll {
							pathSeg = path
						} else {
							pathSeg = strings.SplitN(path, "/", 2)[0]
						}
						prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path
						panic("'" + pathSeg +
							"' in new path '" + fullPath +
							"' conflicts with existing wildcard '" + n.path +
							"' in existing prefix '" + prefix +
							"'")
					}
				}

				c := path[0]

				// slash after param
				if n.nType == param && c == '/' && len(n.children) == 1 {
					n = n.children[0]
					n.priority++
					continue walk
				}

				// Check if a child with the next path byte exists
				for i := 0; i < len(n.indices); i++ {
					if c == n.indices[i] {
					    //增長當前節點的優先級,而且作出位置調整
						i = n.incrementChildPrio(i)
						n = n.children[i]
						continue walk
					}
				}

				// Otherwise insert it
				if c != ':' && c != '*' {
					// []byte for proper unicode char conversion, see #65
					n.indices += string([]byte{c})
					child := &node{
						maxParams: numParams,
					}
					n.children = append(n.children, child)
					n.incrementChildPrio(len(n.indices) - 1)
					n = child
				}
				n.insertChild(numParams, path, fullPath, handle)
				return

			} else if i == len(path) { // Make node a (in-path) leaf
				if n.handle != nil {
					panic("a handle is already registered for path '" + fullPath + "'")
				}
				n.handle = handle
			}
			return
		}
	} else { // Empty tree
		n.insertChild(numParams, path, fullPath, handle)
		n.nType = root
	}
}
複製代碼

以上的大體思路是,把各個handle func()註冊到一棵url前綴樹上面,根據url前綴相同的匹配度進行分支,以提升路由效率。

參數查找:
接下來咱們看下httprouter是怎麼將參數param封裝在上下文裏面的:
不難猜想分支劃分的時候是經過判斷關鍵字「:」來提取預接收的參數,這些參數是存字符串(字典)鍵值對,底層存儲在一個Param結構體:

type Param struct {
	Key   string
	Value string
}
複製代碼

關於上下文的概念在其餘語言也挺常見,如Java Spring框架中的application-context,用來貫穿程序生命週期,用於管理一些全局屬性。
Go的上下文也在不一樣框架有多種實現,這裏咱們先初步瞭解Go程序最頂級的上下文是background(),是全部子上下文的來源,相似於Linux系統的init()進程。

先舉個栗子,簡單列舉在Gocontext傳參的用法:

func TestContext(t *testing.T) {
	// 獲取頂級上下文
	ctx := context.Background()
	// 在上下文寫入string值, 注意須要返回新的value上下文
	valueCtx := context.WithValue(ctx, "hello", "pixel")
	value := valueCtx.Value("hello")
	if value != nil {
	    /* 已知寫入值是string,因此咱們也能夠直接進行類型斷言 好比: p, _ := ctx.Value(ParamsKey).(Params) 這個下劃線實際上是go斷言返回的bool值 */
		fmt.Printf("Params type: %v, value: %v.\n", reflect.TypeOf(value), value)
	}
}
複製代碼

輸出:

Params type: string, value: pixel.
複製代碼

httprouter中,封裝在http.request中的上下文實際上是個valueCtx,類型和咱們上面的栗子中valueCtx是同樣的,框架中提供了一個從上下文獲取Params鍵值對的方法,

func ParamsFromContext(ctx context.Context) Params {
	p, _ := ctx.Value(ParamsKey).(Params)
	return p
}
複製代碼

利用返回的Params就能夠根據key獲取咱們的目標值了,

params.ByName(key)
複製代碼

通過追尋,Params是來自一個叫getValue(path string) (handle Handle, p Params, tsr bool)的函數,還記得上面列舉的*Router路由實現的ServeHTTP()接口嗎?

//ServeHTTP() 函數的一部分
if root := r.trees[req.Method]; root != nil {
    /** getValue(),返回處理方法與參數列表 **/
	if handle, ps, tsr := root.getValue(path); handle != nil {
	    //匹配執行, 這裏的handle就是上面的匿名函數func
		handle(w, req, ps)
		return
	} else if req.Method != http.MethodConnect && path != "/" {
		//...
	}
}
複製代碼

ServeHTTP()其中有個getValue函數,它的返回值有兩個重要成員:當前路由的處理邏輯和url參數列表,因此在路由註冊的時候咱們須要把params做爲入參傳進去。 像這樣子:

router.Handle(http.MethodGet, "/user/query/:name", 
//匿名函數func,這裏的ps參數就是在ServeHttp()的時候幫你提取的
func(w http.ResponseWriter, r *http.Request, ps httprouter.Params) {
	fmt.Println(params.ByName("name"))
})
複製代碼

getValue()在對當前節點n進行類型判斷,若是是‘param’類型(在addRoute的時候已經根據url進行分類),則填充待返回參數Params。

//-----------go
//...github.com/julienschmidt/httprouter@v1.3.0/tree.go:367
switch n.nType {
case param:
	// find param end (either '/' or path end)
	end := 0
	for end < len(path) && path[end] != '/' {
		end++
	}
	
    //遇到節點參數
	if p == nil {
		// lazy allocation
		p = make(Params, 0, n.maxParams)
	}
	i := len(p)
	p = p[:i+1] // expand slice within preallocated capacity
	p[i].Key = n.path[1:]
	p[i].Value = path[:end]
//...
複製代碼

流程梳理:
So far,咱們再次概括一下httprouter的路由過程:

  1. 初始化建立路由器router
  2. 註冊簽名爲:type Handle func(http.ResponseWriter, *http.Request, Params)的函數到該router
  3. 調用HTTP通用接口ServeHTTP(),用於提取當前url預期的參數而且供業務層使用

以上這是這兩天對httprouter的瞭解,順便對go的Http有了進一步的認知,後續將嘗試進入gin中,看下Gin是基於httprouter作了什麼拓展以及熟悉常見用法。

參考連接:

julienschmidt/httprouter
github.com/julienschmi…

相關文章
相關標籤/搜索