一、简介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 树,也叫字典树,前缀树,
-
基数树是空间优化的Trie,当某个节点的子节点唯一时,将子节点与该节点合并
-
// 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 }
-
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 { // 空树 // ... } }
-
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
函数会先查找路径中的参数捕获片段,来分割路径的节点,即最终节点关系为:调用函数的节点 -> 参数节点前的路径节点 -> 参数节点 -> 参数节点后的路径节点 -
启动服务
http.ListenAndServe(":8000", r)
,看出httprouter.Router
本质上就是重写的http.Handler
接口 -
同时解决路由注册和请求参数的解析。