golang gin 框架中的 httprouter 组件源码分析

一、简介HttpRouter源码解析

  • HttpRouter 是Go的一个轻量高性能的多路复用器 multiplexer

    HttpRouter 通过压缩Trie 字典树(也称为基数树 radix tree)来实现高效的路径匹配。

    HttpRouter 具有以下特性:

    • 仅提供精准匹配
    • 对结尾斜杠 / 自动重定向
    • 路径自动修正
    • 路由参数
    • 错误处理

二、案例

  • 使用httprouter 组件实现http web服务。
package main

import (
	"fmt"
	"github.com/julienschmidt/httprouter"
	"log"
	"net/http"
)

func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
	fmt.Fprint(w, "welcome!\n")
}

func Hello(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
	fmt.Fprintf(w, "hello,%s\n", params.ByName("name"))
}

func main() {
	r := httprouter.New()
	r.GET("/", Index)
	r.GET("/hello/:name", Hello)
	log.Fatal(http.ListenAndServe(":8000", r))
}

二、字典树

  • Trie 树,也叫字典树,前缀树,2022-07-27-22-31-42-image.png

  • 基数树是空间优化的Trie,当某个节点的子节点唯一时,将子节点与该节点合并

  • 2022-07-27-22-44-54-image.png

  • // router.go
    // 存储路由参数
    type Param struct {
        Key   string
        Value string
    }
    
    type Params []Param
    // 给net/http中的HandlerFunc加了路由参数
    type Handle func(http.ResponseWriter, *http.Request, Params)
    
    
    // router.go
    type Router struct {
        /**
         trees 是这个结构最核心的内容,是注册的路由
         按照 method (POST,GET ...) 方法分开,每一个方法对应一个基数树(radix tree)
        */
        trees map[string]*node
    
        // 对结尾斜杠自动重定向
        // 例如: 请求 /foo/ ,但只存在 /foo ,则重定向到 /foo,
        // 并对GET请求返回301,对其他请求返回307
        RedirectTrailingSlash bool
    
        // 自动尝试修复路径并重定向
        // 首先,移除像 ../ 或 // 的多余元素;然后做一次大小写不敏感的查找
        // 例如 /FOO 和 /..//Foo 可能被重定向到 /foo
        RedirectFixedPath bool
    
        // 检查请求方法是否被禁止
        // 当请求路径无法匹配时,检查当前路径是否有其他允许的请求方式,
        // 如果有返回405,否则返回404
        HandleMethodNotAllowed bool
    
        // 路由器自动回复OPTIONS请求
        // 自定义的 OPTIONS handlers 优先级更高
        HandleOPTIONS bool
    
        // 自动回复OPTIONS请求时调用的handler
        GlobalOPTIONS http.Handler
    
        // 缓存全局允许的请求方法
        globalAllowed string
    
        // 路径无法找到时调用的handler
        // 默认为 http.NotFound
        NotFound http.Handler
    
        // 请求方法被禁止时调用的handler
        // 默认为带 http.StatusMethodNotAllowed 的 http.Error
        MethodNotAllowed http.Handler
    
        // 服务器内部出现错误时调用的handler
        // 应该生成一个error页面,并返回500
        // 该handler使你的服务免于因为未发现的错误而崩溃
        PanicHandler func(http.ResponseWriter, *http.Request, interface{})
    }
    
    
    /**
    通过 New () 函数初始化一个 Router ,并且指定 Router 实现了 http.Handler 接口 ,如果 New () 没有实现 http.Handler 接口,在编译的时候就会报错了。这里只是为了验证一下, New () 函数的返回值并不需要,所以就把它赋值给 _ ,相当于是给丢弃了
    */
    var _ http.Handler = New()
    
    
    // New returns a new initialized Router.
    // Path auto-correction, including trailing slashes, is enabled by default.
    func New() *Router {
    	return &Router{
    		RedirectTrailingSlash:  true,
    		RedirectFixedPath:      true,
    		HandleMethodNotAllowed: true,
    		HandleOPTIONS:          true,
    	}
    }
    
    
    
    /**
    到这里,我们就可以看到了, Router 也是基于 http.Handler 做的实现,如果要实现 http.Handler 接口,就必须实现 ServeHTTP(w http.ResponseWriter, req *http.Request) 这个方法,下面就可以去追踪下 ServerHTTP 都做了什么
    */
    
    func (r *Router) recv(w http.ResponseWriter, req *http.Request) {
    	if rcv := recover(); rcv != nil {
    		r.PanicHandler(w, req, rcv)
    	}
    }
    
    
    // ServeHTTP makes the router implement the http.Handler interface.
    func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    
    /**
    在这里 做了 panic 的处理,可以保证业务出现问题的时候,不会导致服务器崩溃
    ,如果想在 panic 的时候自己处理只需要将 r.PanicHandler = func(http.ResponseWriter, *http.Request, interface{}) 
    重写, 添加上自己的处理逻辑即可。
    */
    	if r.PanicHandler != nil {
    		defer r.recv(w, req)
    	}
    
    	path := req.URL.Path
    
    	if root := r.trees[req.Method]; root != nil {
    		if handle, ps, tsr := root.getValue(path); handle != nil {
    			handle(w, req, ps)
    			return
    		} else if req.Method != http.MethodConnect && path != "/" {
    			code := 301 // Permanent redirect, request with GET method
    			if req.Method != http.MethodGet {
    				// Temporary redirect, request with same method
    				// As of Go 1.3, Go does not support status code 308.
    				code = 307
    			}
    
    			if tsr && r.RedirectTrailingSlash {
    				if len(path) > 1 && path[len(path)-1] == '/' {
    					req.URL.Path = path[:len(path)-1]
    				} else {
    					req.URL.Path = path + "/"
    				}
    				http.Redirect(w, req, req.URL.String(), code)
    				return
    			}
    
    			// Try to fix the request path
    			if r.RedirectFixedPath {
    				fixedPath, found := root.findCaseInsensitivePath(
    					CleanPath(path),
    					r.RedirectTrailingSlash,
    				)
    				if found {
    					req.URL.Path = string(fixedPath)
    					http.Redirect(w, req, req.URL.String(), code)
    					return
    				}
    			}
    		}
    	}
    
    	if req.Method == http.MethodOptions && r.HandleOPTIONS {
    		// Handle OPTIONS requests
    		if allow := r.allowed(path, http.MethodOptions); allow != "" {
    			w.Header().Set("Allow", allow)
    			if r.GlobalOPTIONS != nil {
    				r.GlobalOPTIONS.ServeHTTP(w, req)
    			}
    			return
    		}
    	} else if r.HandleMethodNotAllowed { // Handle 405
    		if allow := r.allowed(path, req.Method); allow != "" {
    			w.Header().Set("Allow", allow)
    			if r.MethodNotAllowed != nil {
    				r.MethodNotAllowed.ServeHTTP(w, req)
    			} else {
    				http.Error(w,
    					http.StatusText(http.StatusMethodNotAllowed),
    					http.StatusMethodNotAllowed,
    				)
    			}
    			return
    		}
    	}
    
    	// Handle 404
    	if r.NotFound != nil {
    		r.NotFound.ServeHTTP(w, req)
    	} else {
    		http.NotFound(w, req)
    	}
    }
    
    
    
    
  • //注册第一个请求 router.GET("/", Index) 
    
    // router.go
    func (r *Router) GET(path string, handle Handle) {
        r.Handle(http.MethodGet, path, handle)
    }
    
    // router.go
    func (r *Router) Handle(method, path string, handle Handle) {
        // 检查路径是否合法(存在且以/开头)
        if len(path) < 1 || path[0] != '/' {
            panic("path must begin with '/' in path '" + path + "'")
        }
        
        if r.trees == nil {
            r.trees = make(map[string]*node)
        }
    
        root := r.trees[method] // root是一个指针,对root操作就是对r.trees[method]操作
        if root == nil {
            root = new(node)
            r.trees[method] = root
    
            r.globalAllowed = r.allowed("*", "") // 全局添加路由允许的请求方法
        }
    
        root.addRoute(path, handle) // 向树中插入路径和handle
    }
    
    Handle 接收了一种新的请求方法时,创建该方法的根节点&将其添加进全局 globalAllowed ="GET,POST" 缓存中
    
    
  • // tree.go
    type nodeType uint8
    
    const (
        static nodeType = iota // default
        root
        param
        catchAll
    )
    // 树的节点
    type node struct {
        path      string    // 包含的路径片段
        wildChild bool      // 子节点是否为参数节点
        nType     nodeType  // 节点类型:静态(默认)、根、命名参数捕获、任意参数捕获
        maxParams uint8     // 最大参数个数
        priority  uint32    // 优先级
        indices   string    // 索引
        children  []*node   // 子节点
        handle    Handle    // 该节点所代表路径的handle
    }
    
    func (n *node) addRoute(path string, handle Handle) {
        fullPath := path
        n.priority++
        // 通过':'和'*'的个数得出参数个数,最大为255 
        numParams := countParams(path) 
        
        // 非空树
        if len(n.path) > 0 || len(n.children) > 0 {
            // ...
        } else { // 空树直接插入子节点
            n.insertChild(numParams, path, fullPath, handle)
            n.nType = root
        }
    }
    
    func (n *node) insertChild(numParams uint8, path, fullPath string, handle Handle) {
    // 已经处理完的path位置
        var offset int 
    
        // 遍历路径并搜索参数捕获片段
        for i, max := 0, len(path); numParams > 0; i++ {
            // ...
        }
    
        // 插入路径的剩余部分以及handle
        n.path = path[offset:]
        n.handle = handle
    }
    
    
    
  • Snipaste_2022-07-28_20-23-44.png

  • 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>
    
    // 这个图相当于注册了下面这几个路由
    GET("/search/", func1)
    GET("/support/", func2)
    GET("/blog/:post/", func3)
    GET("/about-us/", func4)
    GET("/about-us/team/", func5)
    GET("/contact/", func6)
    
  • type node struct {
        // 当前节点的 URL 路径
        // 如上面图中的例子的首先这里是一个 /
        // 然后 children 中会有 path 为 [s, blog ...] 等的节点 
        // 然后 s 还有 children node [earch,upport] 
        path      string
    
        // 判断当前节点路径是不是含有参数的节点, 上图中的 :post 的上级 blog 就是wildChild节点
        wildChild bool
    
        // 节点类型: static, root, param, catchAll
        // static: 静态节点, 如上图中的父节点 s (不包含 handler 的)
        // root: 如果插入的节点是第一个, 那么是root节点
        // catchAll: 有*匹配的节点
        // param: 参数节点,比如上图中的 :post 节点
        nType     nodeType
    
        // path 中的参数最大数量,最大只能保存 255 个
        maxParams uint8
    
        // 和下面的 children 对应,保留的子节点的第一个字符
        // 如上图中的 s 节点,这里保存的就是 eu (earch 和 upport)的首字母 
        indices   string
    
        // 当前节点的所有直接子节点
        children  []*node
    
        // 当前节点对应的 handler
        handle    Handle
    
        // 优先级,查找的时候会用到,表示当前节点加上所有子节点的数目
        priority  uint32
    }
    
  • 插入第二个请求 r.GET(“/hello/:name”, Hello)

  • func (n *node) addRoute(path string, handle Handle) {
        fullPath := path
        n.priority++
        numParams := countParams(path)
    
      
        if len(n.path) > 0 || len(n.children) > 0 {
        walk:
            for {
                // 更新最大参数个数
                if numParams > n.maxParams {
                    n.maxParams = numParams
                }
    
                // 查找最长公共前缀
                // 最长公共前缀中不会含有 ':' 或 '*'
                i := 0
                max := min(len(path), len(n.path))
                for i < max && path[i] == n.path[i] {
                    i++
                }
    
                // 新公共前缀比原公共前缀短,需要将当前的节点按公共前缀分成父子节点
                // 如: /hello & /hel/user 将 /hello 分成 /hel -> lo 的形式
                if i < len(n.path) {
                    // ...
                }
    
                // 创建此时节点 n 的新的子节点
                // 如: /hel & /hello
                if i < len(path) {
                    path = path[i:] // 刨去公共前缀后的部分
    
                    // 如果子节点是参数节点,检查是否发生冲突
                    if n.wildChild {
                        // ...
                    }
    
                    c := path[0] // 子路径的第一个字符
    
                    // 如果是参数节点后的斜杠
                    if n.nType == param && c == '/' && len(n.children) == 1 {
                        // ...
                    }
    
                    // 如果和子节点有公共前缀
                    for i := 0; i < len(n.indices); i++ {
                        // ...
                    }
    
                    // 否则直接插入
                    if c != ':' && c != '*' {
                        // 将子节点路径的第一个字符添加为索引
                        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
    
                } else if i == len(path) { // 插入的路径刚好是到此节点所表示的路径
                    // 如: /hello/user & /hello/city & /hello/
                    // ...
                }
                return
            }
        } else { // 空树
            // ...
        }
    }
    
    
  • Snipaste_2022-07-28_20-42-26.png

  • addRouter 负责找公共前缀

  • // tree.go
    func (n *node) insertChild(numParams uint8, path, fullPath string, handle Handle) {
        var offset int // 已经处理完的path位置
    
        // 遍历路径
        for i, max := 0, len(path); numParams > 0; i++ {
            // 查找通配符的起始
            c := path[i]
            if c != ':' && c != '*' {
                continue
            }
    
            // 查找通配符的结尾('/'或路径结尾)
            end := i + 1
            for end < max && path[end] != '/' {
                switch path[end] {
                // 每个片段只能有一个':'或'*'
                case ':', '*':
                    panic("only one wildcard per path segment is allowed, has: '" +
                        path[i:] + "' in path '" + fullPath + "'")
                default:
                    end++
                }
            }
    
            // 参数节点不能与其他节点共存于路径的同一位置中,会发生冲突
            // 例如:/hello/:name 和 /hello/user
            if len(n.children) > 0 {
                panic("wildcard route '" + path[i:end] +
                    "' conflicts with existing children in path '" + fullPath + "'")
            }
    
            // 检查参数是否有名字
            if end-i < 2 {
                panic("wildcards must be named with a non-empty name in path '" + fullPath + "'")
            }
    
            if c == ':' { // 命名参数捕获节点
                // 分割path从还未处理的部分的开始到通配符之前
                if i > 0 {
                    n.path = path[offset:i]
                    offset = i
                }
                // 创建参数节点
                child := &node{
                    nType:     param,
                    maxParams: numParams,
                }
                n.children = []*node{child}
                n.wildChild = true // 该节点的子节点为参数节点
                n = child
                n.priority++
                numParams--
    
                // 如果没有结束,说明后面还有路径,将参数节点补充完整,并创建新节点
                if end < max {
                    n.path = path[offset:end]
                    offset = end
    
                    child := &node{
                        maxParams: numParams,
                        priority:  1,
                    }
                    n.children = []*node{child}
                    n = child
                }
    
            } else { // 任意参数捕获节点
                // ...
        }
    
        // 插入路径的剩余部分以及handle
        n.path = path[offset:]
        n.handle = handle
    }
    
  • 参数节点比较特殊,它是其父节点的唯一子节点且不会和父节点合并,所以 insertChild 函数会先查找路径中的参数捕获片段,来分割路径的节点,即最终节点关系为:调用函数的节点 -> 参数节点前的路径节点 -> 参数节点 -> 参数节点后的路径节点

  • Snipaste_2022-07-28_21-47-24.png

  • 启动服务 http.ListenAndServe(":8000", r) ,看出 httprouter.Router 本质上就是重写的 http.Handler 接口

  • 同时解决路由注册和请求参数的解析。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小哥(xpc)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值