聊一聊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…

相关文章
相关标签/搜索