httprouter框架简析(一)

简述

关于go的web开发框架,主要是两大类。一类以beego为主,功能全,配置多,大而全;一类是以gin为主的,基于httprouter进行开发,轻而快。这个无所谓好坏,全看个人选择。今天主要说httprouter是如何实现。

  • httprouter支持参数类型的URL(/user/:id )也支持* 的通配符URL(/static/*file)。

  • httprouter使用radix tree(前缀树)对请求的URL进行管理,最差时间复杂度为O(n),n为URL长度。

  • httprouter对每种请求方法都会建立一个tree进行管理。例如所有的GET请求会都用一个tree,所有的POST用一个tree,所有的PUT一个tree......

  • 当同时使用GET方式添加路由/ff/:id/ff/kk 时,会抛出panic: 'kk' in new path '/ff/kk' conflicts with existing wildcard ':id' in existing prefix '/ff/:id' 。是因为在GET的tree上面,前缀都是/ff的情况下,参数类型的:id和具体的路由字段kk 冲突。

关于httprouter目录

项目总共三个文件router.go、tree.go、path.go,其中router.go是主文件,路由的添加、异常路由的处理等基本设置在此完成,调用httprouter.New() 返回一个Router结构的指针。tree.go负责前缀树的操作,包括将路由添加到前缀树中,从树中查询到URL对应的handler。path.go主要包含CleanPath方法,返回一个符合规范的URL。

关于router.go 中的 Router结构

type Router struct {
  //前缀树的结构
  trees map[string]*node
  paramsPool sync.Pool
  maxParams  uint16
  //如果无法匹配到当前路由,但是存在带有(不带)尾部斜杠的路由时,是否自动重定向
  //例如,如果请求了/foo/,但是只存在/foo的路由,那么将301重定向到 /foo
  RedirectTrailingSlash bool
  //是否修正路径,主要依靠path.go 的 CleanPath 方法
  RedirectFixedPath bool
  //如果无法匹配到当前路由, 那么检查是否有其他方法能否拼配到当前的路由
  HandleMethodNotAllowed bool
  //是否允许路由自动拼配到options
  HandleOPTIONS bool
  //全局的options操作,可以设置CORS时调用
  GlobalOPTIONS http.Handler
  globalAllowed string
  //当没有匹配到路由的时候, 执行这个handler. 如果没有配置,那么返回NotFound
  NotFound http.Handler
  //当没有匹配到路由并且HandleMethodNotAllowed=true的时候,这个函数被使用
  MethodNotAllowed http.Handler
  PanicHandler func(http.ResponseWriter, *http.Request, interface{})
}

这其中最重要的就是trees节点,路由的添加、查找都是通过这个操作完成。

关于node的结构

type node struct {
    //当前node 的路径
    path      string
    // children对应的索引,保存的是分裂分支的第一个字符
    indices   string
    //判断子节点是否为通配节点,(包括:param、catchAll)
    wildChild bool
    //节点类型。
    //root: 第一个插入的节点
    //catchAll: 有* 匹配的节点
    //param:参数节点 :id
    //static: 静态节点,前几个剩下的都是静态节点
    nType     nodeType
    // 优先级
    priority  uint32
    //子节点
    children  []*node
    //当前节点的处理函数
    handle    Handle
}

node示意图

例如GET请求,/search/:id/info/status/:id/info/user/getall

httprouter.png

关于addRoute


当调用GET添加路由时,走的是Router的Handle方法。同样,post、put、delete等也是一样

而Handle最后走向addRoute。

addRoute 的基本思路就是先寻找路由的最长公共前缀,找到对应的节点位置,然后调用insertChild,插入到对应的node中。

  1. tree是一个空树时,那么当前URL直接插入path,nType为root
  2. tree不为空时,如果匹配的最长前缀小于当前节点path的长度,那么说明当前节点需要进行调整。最长公共前缀作为节点的path,原先节点成为最长公共前缀的子节点。
  3. tree不为空时,如果匹配的最长前缀小于传入的path的长度,说明需要在当前节点下,添加一个新的子节点。当然这里可能会有循环递归。
  4. tree不为空时,如果匹配的最长前缀等于path,那么就是在此节点直接添加handler操作。

代码说明

// addRoute adds a node with the given handle to the path.
// Not concurrency-safe!
func (n *node) addRoute(path string, handle Handle) {
    fullPath := path
    n.priority++

    // Empty tree
    if len(n.path) == 0 && len(n.indices) == 0 {
        n.insertChild(path, fullPath, handle)
        n.nType = root
        return
    }

walk:
    for {
        // 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)

        // Split edge--分割 边际
        // 如果最长前缀的长度 < 节点长度
        if i < len(n.path) {
            //那么当前节点的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
        }

        // Make new node a child of this node    ----为当前节点创建一个新的子节点
        //如果最长前缀 < 路径的长度  
        //除了tree为空的情况外,只有在这个判断下才会走 insertChild ,创建新节点
        if i < len(path) {
            path = path[i:]

            //如果子节点是参数节点
            if n.wildChild {
                n = n.children[0]
                n.priority++
                
                // 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
                    // 通配符冲突
                    pathSeg := path
                    if n.nType != catchAll {
                        // SplitN 根据/分割字符串,最多2个
                        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]

            // '/' after param          --
            if n.nType == param && idxc == '/' && len(n.children) == 1 {
                n = n.children[0]
                n.priority++
                continue walk
            }

            // Check if a child with the next path byte exists
            //判断当前节点的 indices 索引中是否存在当前path的首字母
            for i, c := range []byte(n.indices) {
                if c == idxc {
                    i = n.incrementChildPrio(i)
                    n = n.children[i]
                    continue walk
                }
            }

            // Otherwise insert it   ---否则直接插入
            if idxc != ':' && idxc != '*' {
                // []byte for proper unicode char conversion, see #65
                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
        }

        // Otherwise add handle to current node
        //如果最长前缀 == 传入的path,就直接在这个节点添加handle
        if n.handle != nil {
            panic("a handle is already registered for path '" + fullPath + "'")
        }
        n.handle = handle
        return
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,546评论 6 507
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,224评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,911评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,737评论 1 294
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,753评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,598评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,338评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,249评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,696评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,888评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,013评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,731评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,348评论 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,929评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,048评论 1 270
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,203评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,960评论 2 355

推荐阅读更多精彩内容