Httprouter介绍及源码阅读

注:本文已发布超过一年,请注意您所使用工具的相关版本是否适用

在上一篇文章当中阅读了 Go 语言的一个高性能的 Web 框架 Gin,Web 框架当中最重要的功能之一是路由,Gin 的路由就是由 httprouter 这个包实现的

地址

特性

  • 基于基数树实现的高性能路由框架
  • 仅支持精确匹配
  • 不必关心 URL 结尾的斜线
  • 路径自动校正,例如在 url 路径当中有../,//的时候
  • 可以在 URL 当中设置参数,例如/user/:id
  • 零内存分配
  • 不存在服务器崩溃,可以通过设置panic handler使服务器从 panic 当中恢复
  • 适合 API 构建

源码

两个问题

解决两个问题,就基本明白了这个路由框架

  • 路由是是如何注册?如何保存的?
  • 当请求到来之后,路由是如何匹配,如何查找的?

一个 Demo

还是从一个Hello World讲起

1
2
3
4
5
6
7
func main()  {
r := httprouter.New()
r.GET("/:name", func(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
fmt.Fprintf(writer, "hello, %s!\n", params.ByName("name"))
})
http.ListenAndServe(":8080",r)
}

httprouter.New()初始化了一个 Router,下面直接看一下 Router 的结构

Router

在 Router 的源码当中有十分详尽的注释,这里按照我个人的理解注释一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

// Router实现了Http.Handler接口,用于注册分发路由
type Router struct {
// trees 是一个基数树集合,每一个HTTP方法对应一棵单独的路由树
// node是基数树的根节点
trees map[string]*node

// 用于开启上文提到的自动处理URL尾部斜杆的特性
// 这个值为true时,如果/foo/没有被匹配到,会尝试匹配/foo
RedirectTrailingSlash bool

// 用于开启上文提到的路由校正的特性
// 这个值为true时,会对../和//这种路径进行校正
RedirectFixedPath bool

// 这个值为true时,如果当前方法的路由没有被匹配到,会尝试匹配其他方法的路由,
// 如果匹配到了则返回405,如果没有,就交给NotFound Handler处理
HandleMethodNotAllowed bool

// 这个值为true时,将开启OPTIONS自动匹配,注意: 手动匹配优先级更高
HandleOPTIONS bool

// 没有匹配到相应路由的时候会调用这个方法
// 如果没有注册这个方法会返回 NotFound
NotFound http.Handler

// 没有匹配到相应路由并且HandleMethodNotAllowed为true时会调用这个方法
MethodNotAllowed http.Handler

// 用于从panic当中恢复
// 需要返回500错误,并且渲染相应的错误页面
PanicHandler func(http.ResponseWriter, *http.Request, interface{})
}

初始化 Router 之后看看路由是如何保存并且注册的

路由是如何保存的?

这里以官方 Readme 当中的例子说明:
如果注册了以下路由

1
2
3
4
5
6
7
8
r.GET("/", f1)
r.GET("/search/", f2)
r.GET("/support/", f3)
r.GET("/blog/", f4)
r.GET("/blog/:post/", f5)
r.GET("/about_us/", f6)
r.GET("/about_us/team/", f7)
r.GET("/contact/", f8)

那么这些路由会如下方所示,以一颗树的形式保存,并且这些路由的公共前缀会被抽离并且变为上一层节点
Priority 表示加上自身一共有多少个节点
Path 表示路径
Handle 表示路由注册的方法

1
2
3
4
5
6
7
8
9
10
11
Priority   Path             Handle
9 \ *<1>
3 ├s nil
2 |├earch\ *<2>
1 |└upport\ *<3>
2 ├blog\ *<4>
1 | └:post nil
1 | └\ *<5>
2 ├about-us\ *<6>
1 | └team\ *<7>
1 └contact\ *<8>

r.Handle

r.Get, r.Post等方法实质都是通过调用 r.Handle 实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (r *Router) Handle(method, path string, handle Handle) {
// 路径注册必须从/开始,否则直接报错
if path[0] != '/' {
panic("path must begin with '/' in path '" + path + "'")
}

// 路由树map不存在需要新建
if r.trees == nil {
r.trees = make(map[string]*node)
}

// 获取当前方法所对应树的根节点,不存在则新建一个
root := r.trees[method]
if root == nil {
root = new(node)
r.trees[method] = root
}

// 向路由树当中添加一条一条路由
root.addRoute(path, handle)
}

node

路由是注册到一颗路由树当中的,先看看节点的源码,再来分析,是如何添加路由的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
type node struct {
// 当前节点的路径
path string

// 是否为参数节点,参数节点用:name表示
wildChild bool

// 当前节点类型, 一共有4种
// static: 静态节点,默认类型
// root: 根节点
// param: 其他节点
// catchAll: 带有*的节点,这里*的作用和正则当中的*一样
nType nodeType

// 当前路径上最大参数的个数,不能超过255
maxParams uint8

// 代表分支的首字母
// 上面的例子,当前节点为s
// 那么indices = eu
// ├s nil
// |├earch\ *<2>
// |└upport\ *<3>
indices string

// 孩子节点
children []*node

// 注册的路由
handle Handle

// 权重,表示当前节点加上所有子节点的数目
priority uint32
}

路由树是如何生成的?

未完待续

关注我获取更新

wechat
知乎
github

猜你喜欢


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议,转载请注明出处,禁止全文转载