之前在Gin中已经说到, Gin比Martini的效率高好多耶, 究其原因是因为使用了httprouter这个路由框架, httprouter的git地址是: httprouter源码. 今天稍微看了下httprouter的 实现原理, 其实就是使用了一个radix tree(前缀树)来管理请求的URL, 下面具体看看httprouter原理.

###1. httprouter基本结构

httprouter中, 对于每种方法都有一颗tree来管理, 例如所有的GET方法对应的请求会有一颗tree管理, 所有的POST同样如此. OK, 那首先看一下 这个router结构体长啥样:

type Router struct {
// 这个radix tree是最重要的结构
// 按照method将所有的方法分开, 然后每个method下面都是一个radix tree
trees map[string]*node // Enables automatic redirection if the current route can't be matched but a
// handler for the path with (without) the trailing slash exists.
// For example if /foo/ is requested but a route only exists for /foo, the
// client is redirected to /foo with http status code 301 for GET requests
// and 307 for all other request methods.
// 当/foo/没有匹配到的时候, 是否允许重定向到/foo路径
RedirectTrailingSlash bool // If enabled, the router tries to fix the current request path, if no
// handle is registered for it.
// First superfluous path elements like ../ or // are removed.
// Afterwards the router does a case-insensitive lookup of the cleaned path.
// If a handle can be found for this route, the router makes a redirection
// to the corrected path with status code 301 for GET requests and 307 for
// all other request methods.
// For example /FOO and /..//Foo could be redirected to /foo.
// RedirectTrailingSlash is independent of this option.
// 是否允许修正路径
RedirectFixedPath bool // If enabled, the router checks if another method is allowed for the
// current route, if the current request can not be routed.
// If this is the case, the request is answered with 'Method Not Allowed'
// and HTTP status code 405.
// If no other Method is allowed, the request is delegated to the NotFound
// handler.
// 如果当前无法匹配, 那么检查是否有其他方法能match当前的路由
HandleMethodNotAllowed bool // If enabled, the router automatically replies to OPTIONS requests.
// Custom OPTIONS handlers take priority over automatic replies.
// 是否允许路由自动匹配options, 注意: 手动匹配的option优先级高于自动匹配
HandleOPTIONS bool // Configurable http.Handler which is called when no matching route is
// found. If it is not set, http.NotFound is used.
// 当no match的时候, 执行这个handler. 如果没有配置,那么返回NoFound
NotFound http.Handler // Configurable http.Handler which is called when a request
// cannot be routed and HandleMethodNotAllowed is true.
// If it is not set, http.Error with http.StatusMethodNotAllowed is used.
// The "Allow" header with allowed request methods is set before the handler
// is called.
// 当no natch并且HandleMethodNotAllowed=true的时候,这个函数被使用
MethodNotAllowed http.Handler // Function to handle panics recovered from http handlers.
// It should be used to generate a error page and return the http error code
// 500 (Internal Server Error).
// The handler can be used to keep your server from crashing because of
// unrecovered panics.
// panic函数
PanicHandler func(http.ResponseWriter, *http.Request, interface{})
}

上面的结构中, trees map[string]*node代表的一个森林, 里面有一颗GET tree, POST tree… 
对应到每棵tree上的结构, 其实就是前缀树结构, 从github上盗了一张图:

假设上图是一颗GET tree, 那么其实是注册了下面这些GET方法:

GET("/search/", func1)
GET("/support/", func2)
GET("/blog/:post/", func3)
GET("/about-us/", func4)
GET("/about-us/team/", func5)
GET("/contact/", func6)

注意看到, tree的组成是根据前缀来划分的, 例如search和support存在共同前缀s, 所以将s作为单独的parent节点. 但是注意这个s节点是没有handle的. 对应/about-us/和/about-us/team/, 前者是后者的parent, 但是前者也是有 handle的, 这一点还是有点区别的. 
总体来说, 创建节点和查询都是按照tree的层层查找来进行处理的. 下面顺便解释一下tree node的结构:

type node struct {
// 保存这个节点上的URL路径
// 例如上图中的search和support, 共同的parent节点的path="s"
// 后面两个节点的path分别是"earch"和"upport"
path string
// 判断当前节点路径是不是参数节点, 例如上图的:post部分就是wildChild节点
wildChild bool
// 节点类型包括static, root, param, catchAll
// static: 静态节点, 例如上面分裂出来作为parent的s
// root: 如果插入的节点是第一个, 那么是root节点
// catchAll: 有*匹配的节点
// param: 除上面外的节点
nType nodeType
// 记录路径上最大参数个数
maxParams uint8
// 和children[]对应, 保存的是分裂的分支的第一个字符
// 例如search和support, 那么s节点的indices对应的"eu"
// 代表有两个分支, 分支的首字母分别是e和u
indices string
// 保存孩子节点
children []*node
// 当前节点的处理函数
handle Handle
// 优先级, 看起来没什么卵用的样子@_@
priority uint32
}

###2. 建树过程

建树过程主要涉及到两个函数: addRoute和insertChild, 下面主要看看这两个函数: 
首先是addRoute函数:

// addRoute adds a node with the given handle to the path.
// Not concurrency-safe!
// 向tree中增加节点
func (n *node) addRoute(path string, handle Handle) {
fullPath := path
n.priority++
numParams := countParams(path) // non-empty tree
// 如果之前这个Method tree中已经存在节点了
if len(n.path) > 0 || len(n.children) > 0 {
walk:
for {
// Update maxParams of the current node
// 更新当前node的最大参数个数
if numParams > n.maxParams {
n.maxParams = numParams
} // 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 := 0
max := min(len(path), len(n.path))
// 匹配相同的字符
for i < max && path[i] == n.path[i] {
i++
} // Split edge
// 说明前面有一段是匹配的, 例如之前为:/search,现在来了一个/support
// 那么会将/s拿出来作为parent节点, 将child节点变成earch和upport
if i < len(n.path) {
// 将原本路径的i后半部分作为前半部分的child节点
child := node{
path: n.path[i:],
wildChild: n.wildChild,
nType: static,
indices: n.indices,
children: n.children,
handle: n.handle,
priority: n.priority - 1,
} // Update maxParams (max of all children)
// 更新最大参数个数
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]})
// 路径变成前i半部分path
n.path = path[:i]
n.handle = nil
n.wildChild = false
} // Make new node a child of this node
// 同时, 将新来的这个节点插入新的parent节点中当做孩子节点
if i < len(path) {
// i的后半部分作为路径, 即上面例子support中的upport
path = path[i:] // 如果n是参数节点(包含:或者*)
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
// 例如: /blog/:ppp 和 /blog/:ppppppp, 需要检查更长的通配符
if len(path) >= len(n.path) && n.path == path[:len(n.path)] {
// check for longer wildcard, e.g. :name and :names
if len(n.path) >= len(path) || path[len(n.path)] == '/' {
continue walk
}
} panic("path segment '" + path +
"' conflicts with existing wildcard '" + n.path +
"' in path '" + fullPath + "'")
} 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
// 检查路径是否已经存在, 例如search和support第一个字符相同
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
// new一个node
if c != ':' && c != '*' {
// []byte for proper unicode char conversion, see #65
// 记录第一个字符,并放在indices中
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 // 说明是相同的路径,仅仅需要将handle替换就OK
// 如果是nil那么说明取消这个handle, 不是空不允许
} 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)
// 节点的种类是root
n.nType = root
}
}

上面函数的目的是找到插入节点的位置, 需要主要如果存在common前缀, 那么需要将节点进行分裂, 然后再插入child节点. 再看一些insertChild函数:

// 插入节点函数
// @1: 参数个数
// @2: 输入路径
// @3: 完整路径
// @4: 路径关联函数
func (n *node) insertChild(numParams uint8, path, fullPath string, handle Handle) {
var offset int // already handled bytes of the path // find prefix until first wildcard (beginning with ':'' or '*'')
// 找到前缀, 直到遇到第一个wildcard匹配的参数
for i, max := 0, len(path); numParams > 0; i++ {
c := path[i]
if c != ':' && c != '*' {
continue
} // find wildcard end (either '/' or path end)
end := i + 1
// 下面判断:或者*之后不能再有*或者:, 这样是属于参数错误
// 除非到了下一个/XXX
for end < max && path[end] != '/' {
switch path[end] {
// the wildcard name must not contain ':' and '*'
case ':', '*':
panic("only one wildcard per path segment is allowed, has: '" +
path[i:] + "' in path '" + fullPath + "'")
default:
end++
}
} // check if this Node existing children which would be
// unreachable if we insert the wildcard here
if len(n.children) > 0 {
panic("wildcard route '" + path[i:end] +
"' conflicts with existing children in path '" + fullPath + "'")
} // check if the wildcard has a name
// 下面的判断说明只有:或者*,没有name,这也是不合法的
if end-i < 2 {
panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
} // 如果是':',那么匹配一个参数
if c == ':' { // param
// split path at the beginning of the wildcard
// 节点path是参数前面那么一段, offset代表已经处理了多少path中的字符
if i > 0 {
n.path = path[offset:i]
offset = i
}
// 构造一个child
child := &node{
nType: param,
maxParams: numParams,
}
n.children = []*node{child}
n.wildChild = true
// 下次的循环就是这个新的child节点了
n = child
// 最长匹配, 所以下面节点的优先级++
n.priority++
numParams-- // if the path doesn't end with the wildcard, then there
// will be another non-wildcard subpath starting with '/'
if end < max {
n.path = path[offset:end]
offset = end child := &node{
maxParams: numParams,
priority: 1,
}
n.children = []*node{child}
n = child
} } else { // catchAll
// *匹配所有参数
if end != max || numParams > 1 {
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 + "'")
} // currently fixed width 1 for '/'
i--
if path[i] != '/' {
panic("no / before catch-all in path '" + fullPath + "'")
} n.path = path[offset:i] // first node: catchAll node with empty path
child := &node{
wildChild: true,
nType: catchAll,
maxParams: 1,
}
n.children = []*node{child}
n.indices = string(path[i])
n = child
n.priority++ // second node: node holding the variable
child = &node{
path: path[i:],
nType: catchAll,
maxParams: 1,
handle: handle,
priority: 1,
}
n.children = []*node{child} return
}
} // insert remaining path part and handle to the leaf
n.path = path[offset:]
n.handle = handle
}

insertChild函数是根据path本身进行分割, 将’/’分开的部分分别作为节点保存, 形成一棵树结构. 注意参数匹配中的’:’和’*‘的区别, 前者是匹配一个字段, 后者是匹配后面所有的路径. 具体的细节, 请查看代码中的注释.

###3. 查找path过程

这个过程其实就是匹配每个child的path, walk知道path最后.

// Returns the handle registered with the given path (key). The values of
// wildcards are saved to a map.
// If no handle can be found, a TSR (trailing slash redirect) recommendation is
// made if a handle exists with an extra (without the) trailing slash for the
// given path.
func (n *node) getValue(path string) (handle Handle, p Params, tsr bool) {
walk: // outer loop for walking the tree
for {
// 意思是如果还没有走到路径end
if len(path) > len(n.path) {
// 前面一段必须和当前节点的path一样才OK
if path[:len(n.path)] == n.path {
path = path[len(n.path):]
// If this node does not have a wildcard (param or catchAll)
// child, we can just look up the next child node and continue
// to walk down the tree
// 如果不是参数节点, 那么根据分支walk到下一个节点就OK
if !n.wildChild {
c := path[0]
// 找到分支的第一个字符=>找到child
for i := 0; i < len(n.indices); i++ {
if c == n.indices[i] {
n = n.children[i]
continue walk
}
} // Nothing found.
// We can recommend to redirect to the same URL without a
// trailing slash if a leaf exists for that path.
tsr = (path == "/" && n.handle != nil)
return } // handle wildcard child
// 下面处理通配符参数节点
n = n.children[0]
switch n.nType {
// 如果是普通':'节点, 那么找到/或者path end, 获得参数
case param:
// find param end (either '/' or path end)
end := 0
for end < len(path) && path[end] != '/' {
end++
}
// 获取参数
// save param value
if p == nil {
// lazy allocation
p = make(Params, 0, n.maxParams)
}
i := len(p)
p = p[:i+1] // expand slice within preallocated capacity
// 获取key和value
p[i].Key = n.path[1:]
p[i].Value = path[:end] // we need to go deeper!
// 如果参数还没处理完, 继续walk
if end < len(path) {
if len(n.children) > 0 {
path = path[end:]
n = n.children[0]
continue walk
} // ... but we can't
tsr = (len(path) == end+1)
return
}
// 否则获得handle返回就OK
if handle = n.handle; handle != nil {
return
} else if len(n.children) == 1 {
// No handle found. Check if a handle for this path + a
// trailing slash exists for TSR recommendation
n = n.children[0]
tsr = (n.path == "/" && n.handle != nil)
} return case catchAll:
// save param value
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[2:]
p[i].Value = path handle = n.handle
return default:
panic("invalid node type")
}
}
// 走到路径end
} else if path == n.path {
// We should have reached the node containing the handle.
// Check if this node has a handle registered.
// 判断这个路径节点是都存在handle, 如果存在, 那么就可以直接返回了.
if handle = n.handle; handle != nil {
return
}
// 下面判断是不是需要进入重定向
if path == "/" && n.wildChild && n.nType != root {
tsr = true
return
} // No handle found. Check if a handle for this path + a
// trailing slash exists for trailing slash recommendation
// 判断path+'/'是否存在handle
for i := 0; i < len(n.indices); i++ {
if n.indices[i] == '/' {
n = n.children[i]
tsr = (len(n.path) == 1 && n.handle != nil) ||
(n.nType == catchAll && n.children[0].handle != nil)
return
}
} return
} // Nothing found. We can recommend to redirect to the same URL with an
// extra trailing slash if a leaf exists for that path
tsr = (path == "/") ||
(len(n.path) == len(path)+1 && n.path[len(path)] == '/' &&
path == n.path[:len(n.path)-1] && n.handle != nil)
return
}
}

  

httprouter框架 (Gin使用的路由框架)的更多相关文章

  1. golangWEB框架gin学习之路由群组

    原文地址:http://www.niu12.com/article/42 package main import ( "github.com/gin-gonic/gin" &quo ...

  2. Gin框架系列02:路由与参数

    回顾 上一节我们用Gin框架快速搭建了一个GET请求的接口,今天来学习路由和参数的获取. 请求动词 熟悉RESTful的同学应该知道,RESTful是网络应用程序的一种设计风格和开发方式,每一个URI ...

  3. Golang 微框架 Gin 简介

    框架一直是敏捷开发中的利器,能让开发者很快的上手并做出应用,甚至有的时候,脱离了框架,一些开发者都不会写程序了.成长总不会一蹴而就,从写出程序获取成就感,再到精通框架,快速构造应用,当这些方面都得心应 ...

  4. Go语言web框架 gin

    Go语言web框架 GIN gin是go语言环境下的一个web框架, 它类似于Martini, 官方声称它比Martini有更好的性能, 比Martini快40倍, Ohhhh….看着不错的样子, 所 ...

  5. go框架gin的使用

    我们在用http的时候一般都会用一些web框架来进行开发,gin就是这样的一个框架,它有哪些特点呢 一:gin特点 1.性能优秀2.基于官方的net/http的有限封装3.方便 灵活的中间件4.数据绑 ...

  6. gin框架教程一: go框架gin的基本使用

    我们在用http的时候一般都会用一些web框架来进行开发,gin就是这样的一个框架,它有哪些特点呢 一:gin特点 1.性能优秀2.基于官方的net/http的有限封装3.方便 灵活的中间件4.数据绑 ...

  7. 发现了一个关于 gin 1.3.0 框架的 bug

    gin 1.3.0 框架 http 响应数据错乱问题排查 问题概述 客户端同时发起多个http请求,gin接受到请求后,其中一个接口响应内容为空,另外一个接口响应内容包含接口1,接口2的响应内容,导致 ...

  8. 01 . Go之从零实现Web框架(框架雏形, 上下文Context,路由)

    设计一个框架 大部分时候,我们需要实现一个 Web 应用,第一反应是应该使用哪个框架.不同的框架设计理念和提供的功能有很大的差别.比如 Python 语言的 django和flask,前者大而全,后者 ...

  9. JAVA 中一个非常轻量级只有 200k 左右的 RESTful 路由框架

    ICEREST 是一个非常轻量级只有 200k 左右的 RESTful 路由框架,通过 ICEREST 你可以处理 url 的解析,数据的封装, Json 的输出,和传统的方法融合,请求的参数便是方法 ...

随机推荐

  1. codeforces 817 D. Imbalanced Array(单调栈+思维)

    题目链接:http://codeforces.com/contest/817/problem/D 题意:给你n个数a[1..n]定义连续子段imbalance值为最大值和最小值的差,要你求这个数组的i ...

  2. codeforces E. Trains and Statistic(线段树+dp)

    题目链接:http://codeforces.com/contest/675/problem/E 题意:你可以从第 i 个车站到 [i + 1, a[i]] 之间的车站花一张票.p[i][j]表示从 ...

  3. yzoj P1122 阶乘 题解

    T组数据,给出N,求出N!最右边非零的数. 对于30%的数据,N <= 30,T<=10. 对于全部的数据,N <= 10^2009,T<=30. 一道数学题 解析 N!/(1 ...

  4. Promise then中回调为什么是异步执行?Promise执行机制问题

    今天发现一个问题,看下方代码 let p = new Promise(function(resolve, reject) { resolve() console.log('); }); p.then( ...

  5. 059 Python计算生态概览

    目录 一.概要 二.导学 三.实践能力 一.概要 从数据处理到人工智能 实例15-霍兰德人格分析雷达图 从Web解析到网络空间 从人机交互到艺术设计 实例16-玫瑰花绘制 二.导学 纵览Python计 ...

  6. TypeScript模块系统、命名空间、声明合并

    命名空间 命名空间能有效避免全局污染.在ES6引入模块之后,命名空间就较少被提及了.如果使用了全局的类库,命名空间仍是一个好的解决方案. namespace Shape{ const pi = Mat ...

  7. J2EE简单的分页器

    J2EE项目特别是后台管理,或者一部分前台展示数据比较多,通常需要分页把展示折叠在数据区内. 一般有几种方式来实现分页, 一是官方分页插件,二是自己写,三是网上找(类似于第一种) 这里就介绍第二种, ...

  8. charles 黑名单

    本文参考:charles 黑名单 charles 黑名单 功能:阻止对匹配HOST的请求:可以直接把请求丢掉,也可以直接返回403状态码: 我一般用黑名单工具来block一些软件的自动上传功能 黑名单 ...

  9. Tomcat乱码

    日志乱码修改logging.properties文件中encoding=UTF-8 vim logging.properties encoding=utf-8 wq!保持退出,重启tomcat 乱码O ...

  10. IDEA微服务项目的application.yml没有绿色叶子的解决办法

    1.今天在写微服务项目的时候成功入坑,那么问题是啥呢?接下来和我一起走入bug的世界吧,让我们看看究竟是怎么回事. *问题描述 1.application.yml是灰色的小格子 2.实在难看 *需要解 ...