httprouter解读
核心思想
- 与defaultServeMux的实现区别在于什么?采取特殊的数据结构作路由。
- defaultServeMux的实现采用什么样的数据结构?
- httprouter的实现采用什么样的数据结构?
- router的架构图是怎样的,与server.go有何异同
- 一些http相关的细节
- 其他细节
细读源码
- Router的 paramsPool?sync.Pool需要看看 done
- 修复重定向是代码大头,得细细解读重定向相关的协议内容 这部分先跳过吧,细节太多了
- http包的server.go里面req的context里面包含了params怎么取消的。done
“server.serve => conn.serve ==>defer cancleCtx/ w.cancelCtx() ”
因此, 最要紧的就是看tree.go里面定义的数据结构。曹大的图可以帮助理解。
tree
- addRoute方法为切入口
- 初次阅读有几个不明白的地方,还需要回顾router.go
- 具体的路由规则要烂熟,由此解读node的几个类型
- priority/wildchild/indices的意思和作用
- test文件的测试有没有没覆盖到的情况?
node 的结构
type node struct {
path string
indices string
wildChild bool
nType nodeType
priority uint32
children []*node
handle Handle
}
疑难字段名
-path
一截儿path,绝非全路径,这点可以确定。具体怎么表述?
- indices
// incrementChildPrio
if newPos != pos {
n.indices = n.indices[:newPos] +
n.indices[pos:pos+1] +
n.indices[newPos:pos] + n.indices[pos+1:]
}
可见每个node为chidren指定唯一的一字节字符,和排好序的chidren slice一一对应。问题是怎么指定的呢?
- wildchild
是什么?干嘛的?
- priority
干嘛的?
“加”的两个重头方法 addRoute 和 insertChild
解读
- 事实上,在router.go中,只用到了addRoute为我们的router添加路由以及对应的handle, insertChild为addRoute服务。
- 进一步的事实是,调用addRoute的总是根节点root.
- 我们根据以上事实,参照代码,在脑海中模拟运行。找到点感觉再更细致的分析(其实还有一个不错的方法是参照测试文件看看,找找感觉)。
// router.go Handle
if root == nil {
root = new(node)
r.trees[method] = root
// 其他逻辑
]}
// tree.go addRoute
fullpath := path
n.priority ++
//Empty tree
if len(n.path) == 0 && len(n.indices) == 0 {
n.insertChild(path, fullpath, handle)
n.nType == root // 常量: root
return
}
- insertChild 的逻辑是怎么样?
- 没有“:”“*”都好说,直接:
//insertChild
n.path = path
n.handle = handle
- 有呢?有点复杂。将情况简单化,如果之前是空树的话(现在要有第一个节点了),我们想想,有“:”“*”有什么说头?
- 问题一: 遇到无效的wildcard怎么处理?
- 问题二:可能要设置wildcard冲突,怎么设置呢?
- 问题一已经被枪毙了
if !valid {
panic("only one wildcard per path segment is allowed, has: '" + wildcard + "' in path '" + fullPath + "'")
}
- 另外还赠送了两种枪毙情况,“光杆司令” “鸠占鹊巢(初次插入时不会出现,但是很好理解,即冲突,后会提及此事)”
- 好了,开始好循环了吗?因为findWildCard只会找到离根部最近的那个。
- param,这个简单点,先从这儿开始吧。先贴源码。
if wildcard[0] == ':' { // param
if i > 0 {
// Insert prefix before the current wildcard
n.path = path[:i]
path = path[i:]
}
n.wildChild = true
child := &node{
nType: param,
path: wildcard,
}
n.children = []*node{child}
n = child
n.priority++
// If the path doesn't end with the wildcard, then there will be another non-wildcard subpath starting with '/'
if len(wildcard) < len(path) {
path = path[len(wildcard):]
child := &node{
priority: 1,
}
n.children = []*node{child}
n = child
continue
}
// Otherwise we're done. Insert the handle in the new leaf
n.handle = handle
return
}
- 其实很简单。当前节点n不是没children嘛(有chidren的之前已经被枪毙), 我param给你当吧。别高兴的太早,你得把我爹妈照顾好,于是有了:
if i > 0 {
n.path = path[:i]
path = path[i:]
}
- 需要注意的是这个path不是乱传的。path和fullpath和树和当前节点是有一致性的,在addRoute里会体现。我们可以猜想一下,n.path 和参数path的关系, n.path应该是参数path脱去通配内容之后的前缀,否则以上的语句就会无缘无故抹掉n.path的某种信息。设想path以":"开头的情况,也可以想通。这一点待验证。
- 我们当前关注的空树插节点不需要考虑这么深入,若“:”前有任何内容,直接给n即可,我们集中精力处理以":"开头的一段。path在这个过程中不断的“脱”。无论如何,现在的path以":"开头了。
n.wildchild = true
- 出现了,wildchild意思是,n的孩子是wildCard,并不是“野孩子”。当然wildchild也必须是独子,否则,会引起冲突,果然有点野。
child := &node{
nType: param,
path: wildcard,
}
n.children = []*node{child}
n = child
n.priority++
- 注意,上面的wildcard是不包含"/"的、以“:”开头的一段内容。如愿以偿的,这个野孩子当了n的孩子,不仅如此,还篡权了,成了新一代的n,接手以下的朝政工作。刚创建死后priority为0,现在为1。如果wildcard后面没有内容了,即没有“/”了,完事儿了,到头了:
n.handle = handle
return
- 后面还有内容的话,必须还没完事儿,做好重走循环的准备
if len(wildcard) < len(path) {
path = path[len(wildcard):]
child := &node{
priority: 1,
}
n.children = []*node{child}
n = child
continue
}
- path又开始“脱”了,path就像python(非彼python)一样一直蜕皮。这下path一定是以"/"开头的,这是从fildWildCard函数的实现中得出的结论。现在的n的类型是之前的param,形如“:xxx”的不含"/", 它创建了一个空孩子,并且这个空孩子当政成了n,重走循环。看我们之前推测的,n.path必须是参数path的前缀, 空串当然是path的前缀。
- 接下来是catchall情况,感觉应该和param雷同,但是又有点不一样。根据router.go开头的注释和测试用例,猜想一下:catchall一定是叶子节点,不以"/"结尾。
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 + "'")
}
- 也就是如果n.path非空,必须以"/"结尾。什么意思?什么node不以"/"结尾?比如param类型的, 比如catchall类型的,还比如想通配一个,“/file/prefix_*filepath” 统统不合法了。
- 但是path本身会不会是“/somesubpath/*cathall”类型的呢?这样接在param类型的node后面为什么是非法的?猜想:param类型的node不会直接调用insertChild方法!(等待验证)
- 这时候又来了下面一句:
i--
if path[i] != '/' {
panic("no / before catch-all in path '" + fullPath + "'")}
- 意思和上一条雷同。只不过非法情况来自
- i--? 如果i == 0 呢?由此可见,path不会以“*”开头(等待验证)
小结一下: 调用插入方法的node的path会是参数path的前缀,是空串最保险了。参数path不会以“*”开头, 以“:”开头是可能的。node不会是param类型(catch也不会)。
n.path = path[:i]
// First node: catchAll node with empty path
child := &node{
wildChild: true,
nType: catchAll,
}
n.children = []*node{child}
n.indices = string('/')
n = child
n.priority++
// Second node: node holding the variable
child = &node{
path: path[i:],
nType: catchAll,
handle: handle,
priority: 1,
}
n.children = []*node{child}
return
- path脱了后(还记得我们的前缀论吗),进行了一波骚操作,一连插入两个catchall类型的节点。不明白为什么,似乎是作了一个保护,最后一个节点才是真正的通配符。并且这里首次出现了indices.更懵圈了。
- 毕竟insertChild只是一个辅助方法,使用的情况很多很多限制。
回到addRoute吧。
// Find the longest common prefix.
//This also implies that the common prefix contains no ':' or '*'
//since the existing key can't contain those chars.
i := longestCommonPrefix(path, n.path)
再回顾一遍insertChild方法: 设想向空树插入:“/:name” , ":name", "/*catchall", 根结点不会带“:”“*”,要么空串, 要么"/"。注意插入“*catchall”非法(index out of range!!!)。可以理解他的注释。
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}
// []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
}
- 好的。i是path和n.path的最长公共前缀。最长公共前缀是要被剥离放在祖辈的。因为树生长的方向是向下,而越向下,生成的路径就会越长。这里相当于旧路径和新路径分叉了,自然要把前缀放在祖辈位置。
- 以上代码还有几个值得注意的细节。为什么接管n.path后半段的child的priority要-1?暂时还不清楚。
-n.indices的含义。从上可以瞥见一点端倪。记得我们说过,n.indices 和 n.children一一对应?这里n.indices就是这个child的开头字母,正如其名,起到的是索引的作用。
- n.wildchild是false,因为我们说了,最长前缀不含“:”"*". 有个特殊情况:path和n.path一样都是,:param 别的特殊情况也有,因此这个注释意思不明
- 还需要注意,n作为原n.path的子串,是没有设置handle的,你可以之后为这条路径设置。就直接走到该方法的最后:
if n.handle != nil {
panic("a handle is already registered for path '" + fullPath + "'")
}
n.handle = handle
- 我们继续往下走。先总览一下全貌。
if i < len(path) {
path = path[i:]
// 节点n的子节点是通配,杀伤力比较大,先掌权,后清洗
if n.wildchild {
// 1. 掌权
// 2.清洗,生存者可以进入下一轮。
// 思考:
// 1. 什么样的能逃过清洗?
// 2. 进入下一轮的初始状态是怎样的: 虚拟根结点
}
// 现在面临的情况:n的孩子节点没有param或catchall
//但是n本身可能是
idxc := path[0]
// 1. 一种简单的情况的处理 n是param,只有一个孩子
// 那必然是“/”开头的或空串,直接重走循环
// 2. n的某个孩子和path有公共前缀,提升priority并重走循环
// 3.其他情况插入新的子节点,使树增长
// 现在的情形是: n.path 和 path 绝对并没有公共前缀了
// 3.1 等待拼接的全新path不以":" "*" 打头,需要做一点工作
// 插入空节点,作用不明, 保护作用?
// 结合insertChild的情况,有个情况可以排除:
// 如果 idxc是“:” "*" 打头的,n必然没孩子
// 如果n是param, path是普通字母打头的呢?按理来说,这是个不合法现象。这在上面的判断(wildcard冲突判断中)已经枪毙。
// 创建孩子(空串),插入此节点。
n.insertChild(path, fullpPath, handle)
}
}
- 需要注意的是,如果节点是param类型,path必然是“:”和参数名,绝不含有“/”,也侧面说明两个param类型的节点不可能是父子。
- 如果是staitc类型,又没有“/”都是合法的,并且可以在任意位置(中间合法吗?),从上往下拼就可以了。
- continue walk 的情况:
- 公共前缀不为空或n.path为空串
- 若n为param(模拟根结点无法做到的), path一定是“:param” 或 “:param/” 或 “/”打头三种情况之一。
- param子节点若有一定是独生子!!
- param没孩子的话,要加一个空串“”
源码参考:
if i < len(path) {
path = path[i:]
if n.wildchild {
n = n.children[0]
n.prority ++
// Check if the wildcard mathes
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 != cathAll {
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 +
"'")
}
}
idxc := path[0]
if n.nType == param && idxc == "/" && len(n.children) == 1 {
n = n.children[0]
n.prority++
continue walk
}
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 += string([]byte{idxc})
child := &node{}
n.children = append(n.children, child)
n.incrementChildPrio(len(n.indices) - 1)
n = child
}
n.insertChild(path, fullpPath, handle)
}
“拿”的重要方法:getValue
作用猜想:根据树,匹配手头的path,把所有参数取出来,放在从sync.Pool里拿出临时对象[]Params, 不断把解析的param加入。 catchall怎么处理?拭目以待!
有疑问加站长微信联系(非本文作者)