在上一篇文章中,咱们聊了聊在Golang中怎么实现一个Http服务器。可是在最后咱们能够发现,当然DefaultServeMux
能够作路由分发的功能,可是他的功能一样是不完善的。node
由DefaultServeMux
作路由分发,是不能实现RESTful
风格的API的,咱们没有办法定义请求所需的方法,也没有办法在API
路径中加入query
参数。其次,咱们也但愿可让路由查找的效率更高。git
因此在这篇文章中,咱们将分析httprouter
这个包,从源码的层面研究他是如何实现咱们上面提到的那些功能。而且,对于这个包中最重要的前缀树,本文将以图文结合的方式来解释。github
咱们一样以怎么使用做为开始,自顶向下的去研究httprouter
。咱们先来看看官方文档中的小例子:算法
package main
import (
"fmt"
"net/http"
"log"
"github.com/julienschmidt/httprouter"
)
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
fmt.Fprint(w, "Welcome!\n")
}
func Hello(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("/", Index)
router.GET("/hello/:name", Hello)
log.Fatal(http.ListenAndServe(":8080", router))
}
复制代码
其实咱们能够发现,这里的作法和使用Golang自带的net/http
包的作法是差很少的。都是先注册相应的URI和函数,换一句话来讲就是将路由和处理器相匹配。bash
在注册的时候,使用router.XXX
方法,来注册相对应的方法,好比GET
,POST
等等。服务器
注册完以后,使用http.ListenAndServe
开始监听。微信
至于为何,咱们会在后面的章节详细介绍,如今只须要先了解作法便可。app
咱们先来看看第一行代码,咱们定义并声明了一个Router
。下面来看看这个Router
的结构,这里把与本文无关的其余属性省略:函数
type Router struct {
//这是前缀树,记录了相应的路由
trees map[string]*node
//记录了参数的最大数目
maxParams uint16
}
复制代码
在建立了这个Router
的结构后,咱们就使用router.XXX
方法来注册路由了。继续看看路由是怎么注册的:post
func (r *Router) GET(path string, handle Handle) {
r.Handle(http.MethodGet, path, handle)
}
func (r *Router) POST(path string, handle Handle) {
r.Handle(http.MethodPost, path, handle)
}
...
复制代码
在这里还有一长串的方法,他们都是同样的,调用了
r.Handle(http.MethodPost, path, handle)
复制代码
这个方法。咱们再来看看:
func (r *Router) Handle(method, path string, handle Handle) {
...
if r.trees == nil {
r.trees = make(map[string]*node)
}
root := r.trees[method]
if root == nil {
root = new(node)
r.trees[method] = root
r.globalAllowed = r.allowed("*", "")
}
root.addRoute(path, handle)
...
}
复制代码
在这个方法里,一样省略了不少细节。咱们只关注一下与本文有关的。咱们能够看到,在这个方法中,若是tree
尚未初始化,则先初始化这颗前缀树。
而后咱们注意到,这颗树是一个map
结构。也就是说,一个方法,对应了一颗树。而后,对应这棵树,调用addRoute
方法,把URI
和对应的Handle
保存进去。
又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不只限于字符串),因此常常被搜索引擎系统用于文本词频统计。它的优势是:利用字符串的公共前缀来减小查询时间,最大限度地减小无谓的字符串比较,查询效率比哈希树高。
简单的来说,就是要查找什么,只要跟着这棵树的某一条路径找,就能够找获得。
好比在搜索引擎中,你输入了一个蔡:
他会有这些联想,也能够理解为是一个前缀树。
再举个例子:
在这颗GET
方法的前缀树中,包含了如下的路由:
说到这里你应该能够理解了,在构建这棵树的过程当中,任何两个节点,只要有了相同的前缀,相同的部分就会被合并成一个节点。
上面说的addRoute
方法,就是这颗前缀树的插入方法。假设如今数为空,在这里我打算以图解的方式来讲明这棵树的构建。
假设咱们须要插入的三个路由分别为:
(1)插入/hello/world
由于此时树为空,因此能够直接插入:
(2)插入/hello/china
此时,发现/hello/world
和/hello/china
有相同的前缀/hello/
。
那么要先将原来的/hello/world
结点,拆分出来,而后将要插入的结点/hello/china
,截去相同部分,做为/hello/world
的子节点。
(3)插入/hello/chinese
此时,咱们须要插入/hello/chinese
,可是发现,/hello/chinese
和结点/hello/
有公共的前缀/hello/
,因此咱们去查看/hello/
这个结点的子节点。
注意,在结点中有一个属性,叫indices
。它记录了这个结点的子节点的首字母,便于咱们查找。好比这个/hello/
结点,他的indices
值为wc
。而咱们要插入的结点是/hello/chinese
,除去公共前缀后,chinese
的第一个字母也是c
,因此咱们进入china
这个结点。
这时,有没有发现,状况回到了咱们一开始插入/hello/china
时候的局面。那个时候公共前缀是/hello/
,如今的公共前缀是chin
。
因此,咱们一样把chin
截出来,做为一个结点,将a
做为这个结点的子节点。而且,一样把ese
也做为子节点。
到这里,构建就已经结束了。咱们来总结一下算法。
具体带注释的代码将在本文最末尾给出,若是想要了解的更深能够自行查看。在这里先理解这个过程:
(1)若是树为空,则直接插入
(2)不然,查找当前的结点是否与要插入的URI
有公共前缀 (3)若是没有公共前缀,则直接插入 (4)若是有公共前缀,则判断是否须要分裂当前的结点
(5)若是须要分裂,则将公共部分做为父节点,其他的做为子节点
(6)若是不须要分裂,则寻找有无前缀相同的子节点
(7)若是有前缀相同的,则跳到(4)
(8)若是没有前缀相同的,直接插入
(9)在最后的结点,放入这条路由对应的Handle
可是到了这里,有同窗要问了:怎么这里的路由,不带参数的呀?
其实只要你理解了上面的过程,带参数也是同样的。逻辑是这样的:在每次插入以前,会扫描当前要插入的结点的path是否带有参数(即扫描有没有/
或者*
)。若是带有参数的话,将当前结点的wildChild
属性设置为true
,而后将参数部分,设置为一个新的子节点。
在讲完了路由的注册,咱们来聊聊路由的监听。
在上一篇文章的内容中,咱们有提到这个:
type serverHandler struct {
srv *Server
}
func (sh serverHandler) ServeHTTP(rw ResponseWriter, req *Request) {
handler := sh.srv.Handler
if handler == nil {
handler = DefaultServeMux
}
if req.RequestURI == "*" && req.Method == "OPTIONS" {
handler = globalOptionsHandler{}
}
handler.ServeHTTP(rw, req)
}
复制代码
当时咱们提到,若是咱们不传入任何的Handle
方法,Golang将使用默认的DefaultServeMux
方法来处理请求。而如今咱们传入了router
,因此将会使用router
来处理请求。
所以,router
也是实现了ServeHTTP
方法的。咱们来看看(一样省略了一些步骤):
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
...
path := req.URL.Path
if root := r.trees[req.Method]; root != nil {
if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil {
if ps != nil {
handle(w, req, *ps)
r.putParams(ps)
} else {
handle(w, req, nil)
}
return
}
}
...
// Handle 404
if r.NotFound != nil {
r.NotFound.ServeHTTP(w, req)
} else {
http.NotFound(w, req)
}
}
复制代码
在这里,咱们选择请求方法所对应的前缀树,调用了getValue
方法。
简单解释一下这个方法:在这个方法中会不断的去匹配当前路径与结点中的path
,直到找到最后找到这个路由对应的Handle
方法。
注意,在这期间,若是路由是RESTful风格的,在路由中含有参数,将会被保存在Param
中,这里的Param
结构以下:
type Param struct {
Key string
Value string
}
复制代码
若是未找到相对应的路由,则调用后面的404方法。
到了这一步,其实和之前的内容几乎同样了。
在获取了该路由对应的Handle
以后,调用这个函数。
惟一和以前使用net/http
包中的Handler
不同的是,这里的Handle
,封装了从API中获取的参数。
type Handle func(http.ResponseWriter, *http.Request, Params)
复制代码
谢谢你能看到这里~
至此,httprouter介绍完毕,最关键的也就是前缀树的构建了。在上面我用图文结合的方式,模拟了一次前缀树的构建过程,但愿可让你理解前缀树是怎么回事。固然,若是还有疑问,也能够留言或者在微信中与我交流~
固然,若是你不知足于此,能够看看后面的附录,有前缀树的全代码注释。
固然了,做者也是刚入门。因此,可能会有不少的疏漏。若是在阅读的过程当中,有哪些解释不到位,或者理解出现了误差,也请你留言指正。
再次感谢~
PS:若是有其余的问题,也能够在公众号找到做者。而且,全部文章第一时间会在公众号更新,欢迎来找做者玩~
type node struct {
path string //当前结点的URI
indices string //子结点的首字母
wildChild bool //子节点是否为参数结点
nType nodeType //结点类型
priority uint32 //权重
children []*node //子节点
handle Handle //处理器
}
复制代码
func (n *node) addRoute(path string, handle Handle) {
fullPath := path
n.priority++
// 若是这是个空树,那么直接插入
if len(n.path) == 0 && len(n.indices) == 0 {
//这个方法实际上是在n这个结点插入path,可是会处理参数
//详细实如今后文会给出
n.insertChild(path, fullPath, handle)
n.nType = root
return
}
//设置一个flag
walk:
for {
// 找到当前结点path和要插入的path中最长的前缀
// i为第一位不相同的下标
i := longestCommonPrefix(path, n.path)
// 此时相同的部分比这个结点记录的path短
// 也就是说须要把当前的结点分裂开
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,
}
// 将新的结点做为这个结点的子节点
n.children = []*node{&child}
// 把这个结点的首字母加入indices中
// 目的是查找更快
n.indices = string([]byte{n.path[i]})
n.path = path[:i]
n.handle = nil
n.wildChild = false
}
// 此时相同的部分只占了新URI的一部分
// 因此把path后面不相同的部分要设置成一个新的结点
if i < len(path) {
path = path[i:]
// 此时若是n的子节点是带参数的
if n.wildChild {
n = n.children[0]
n.priority++
// 判断是否会不合法
if len(path) >= len(n.path) && n.path == path[:len(n.path)] &&
n.nType != catchAll &&
(len(n.path) >= len(path) || path[len(n.path)] == '/') {
continue walk
} else {
pathSeg := path
if n.nType != catchAll {
pathSeg = strings.SplitN(pathSeg, "/", 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 +
"'")
}
}
// 把截取的path的第一位记录下来
idxc := path[0]
// 若是此时n的子节点是带参数的
if n.nType == param && idxc == '/' && len(n.children) == 1 {
n = n.children[0]
n.priority++
continue walk
}
// 这一步是检查拆分出的path,是否应该被合并入子节点中
// 具体例子可看上文中的图解
// 若是是这样的话,把这个子节点设置为n,而后开始一轮新的循环
for i, c := range []byte(n.indices) {
if c == idxc {
// 这一部分是为了把权重更高的首字符调整到前面
i = n.incrementChildPrio(i)
n = n.children[i]
continue walk
}
}
// 若是这个结点不用被合并
if idxc != ':' && idxc != '*' {
// 把这个结点的首字母也加入n的indices中
n.indices += string([]byte{idxc})
child := &node{}
n.children = append(n.children, child)
n.incrementChildPrio(len(n.indices) - 1)
// 新建一个结点
n = child
}
// 对这个结点进行插入操做
n.insertChild(path, fullPath, handle)
return
}
// 直接插入到当前的结点
if n.handle != nil {
panic("a handle is already registered for path '" + fullPath + "'")
}
n.handle = handle
return
}
}
复制代码
func (n *node) insertChild(path, fullPath string, handle Handle) {
for {
// 这个方法是用来找这个path是否含有参数的
wildcard, i, valid := findWildcard(path)
// 若是不含参数,直接跳出循环,看最后两行
if i < 0 {
break
}
// 条件校验
if !valid {
panic("only one wildcard per path segment is allowed, has: '" +
wildcard + "' in path '" + fullPath + "'")
}
// 一样判断是否合法
if len(wildcard) < 2 {
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
}
if len(n.children) > 0 {
panic("wildcard segment '" + wildcard +
"' conflicts with existing children in path '" + fullPath + "'")
}
// 若是参数的第一位是`:`,则说明这是一个参数类型
if wildcard[0] == ':' {
if i > 0 {
// 把当前的path设置为参数以前的那部分
n.path = path[:i]
// 准备把参数后面的部分做为一个新的结点
path = path[i:]
}
//而后把参数部分做为新的结点
n.wildChild = true
child := &node{
nType: param,
path: wildcard,
}
n.children = []*node{child}
n = child
n.priority++
// 这里的意思是,path在参数后面尚未结束
if len(wildcard) < len(path) {
// 把参数后面那部分再分出一个结点,continue继续处理
path = path[len(wildcard):]
child := &node{
priority: 1,
}
n.children = []*node{child}
n = child
continue
}
// 把处理器设置进去
n.handle = handle
return
} else { // 另一种状况
if i+len(wildcard) != len(path) {
panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'")
}
if len(n.path) > 0 && n.path[len(n.path)-1] == '/' {
panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'")
}
// 判断在这以前有没有一个/
i--
if path[i] != '/' {
panic("no / before catch-all in path '" + fullPath + "'")
}
n.path = path[:i]
// 设置一个catchAll类型的子节点
child := &node{
wildChild: true,
nType: catchAll,
}
n.children = []*node{child}
n.indices = string('/')
n = child
n.priority++
// 把后面的参数部分设置为新节点
child = &node{
path: path[i:],
nType: catchAll,
handle: handle,
priority: 1,
}
n.children = []*node{child}
return
}
}
// 对应最开头的部分,若是这个path里面没有参数,直接设置
n.path = path
n.handle = handle
}
复制代码
最关键的几个方法到这里就所有结束啦,先给看到这里的你鼓个掌!
这一部分理解会比较难,可能须要多看几遍。
若是仍是有难以理解的地方,欢迎留言交流,或者直接来公众号找我~