经典库httprouter源码阅读
官方:https://github.com/julienschmidt/httprouter文档:https://godoc.org/github.com/julienschmidt/httprouter简单描述,httprouter是一个golang实现的路由组件。httprouter使用一个前缀树来维护映射的父子关系,通过前缀树快速路由。同时其里面的HttpRouter结构体实现了golang
为什么总是传入 Write、Router和 Param是什么?
func CreateUser(w http.ResponseWriter, r *http.Request, p httprouter.Params) {
io.WriteString(w, "Create User Handler")
}
这三个参数是用于处理HTTP请求和响应的。在Web应用程序中,HTTP请求通常包含有关客户端(例如浏览器)正在请求的资源以及其他相关信息。HTTP响应是服务器返回给客户端的内容,包括HTML,JSON,XML或其他类型的数据。
具体来说,这三个参数的含义如下:
w http.ResponseWriter
用于向客户端发送HTTP响应。您可以使用它来设置响应头、状态码和响应正文。r *http.Request
包含客户端的HTTP请求信息,包括请求方法、请求头、请求体、URL参数等等。您可以使用它来获取客户端发送的数据或信息。p httprouter.Params
包含从URL路径中提取的参数。在路由处理程序中,您可以使用这些参数来执行特定的操作,例如从数据库中检索数据或调用其他API。
因此,这三个参数是处理HTTP请求和响应所必需的。在这个示例中,函数 CreateUser 可能是一个HTTP路由处理程序,通过这些参数来接收HTTP请求并生成HTTP响应。
简介
官方:https://github.com/julienschmidt/httprouter
文档:https://godoc.org/github.com/julienschmidt/httprouter
HttpRouter is a lightweight high performance HTTP request router (also called multiplexer or just mux for short) for Go. In contrast to the default mux of Go’s net/http package, this router supports variables in the routing pattern and matches against the request method. It also scales better. The router is optimized for high performance and a small memory footprint. It scales well even with very long paths and a large number of routes. A compressing dynamic trie (radix tree) structure is used for efficient matching.
简单描述,httprouter是一个golang实现的路由组件。httprouter使用一个前缀树来维护映射的父子关系,通过前缀树快速路由。同时其里面的HttpRouter结构体实现了golang的net.http.server的Handler接口,可以作为httpHandle发布。golang以性能出名的gin使用的也就是httprouter来做路由处理。
在go web开发中,很多时候会选用一款web框架,随着项目功能增加,接口也越来越多,一款好的路由框架能很方便的帮助我们管理与维护繁多的接口地址。
特性
- 基于基数树实现的高性能路由框架
- 仅支持精确匹配
- 不必关心 URL 结尾的斜线
- 路径自动校正,例如在 url 路径当中有
../
,//
的时候 - 可以在 URL 当中设置参数,例如
/user/:id
- 零内存分配
- 不存在服务器崩溃,可以通过设置
panic handler
使服务器从 panic 当中恢复 - 适合 API 构建
依赖
require github.com/julienschmidt/httprouter latest
使用Demo
package httpRouterDemo
import (
"net/http"
"github.com/julienschmidt/httprouter"
)
func Index(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
w.Write([]byte("hello world"))
}
func main() {
router := httprouter.New()
router.GET("/", Index)
http.ListenAndServe(":80", router)
}
源码分析
解决两个问题,就基本明白了这个路由框架
- 路由是是如何注册?如何保存的?
- 当请求到来之后,路由是如何匹配,如何查找的?
还是从一个Hello World
讲起
func main() {
r := httprouter.New()
r.GET("/:name", func(writer http.ResponseWriter, request *http.Request, params httprouter.Params) {
fmt.Fprintf(writer, "hello, %s!\n", params.ByName("name"))
})
http.ListenAndServe(":8080",r)
}
httprouter.New()
初始化了一个 Router,下面直接看一下 Router 的结构
Router
在 Router 的源码当中有十分详尽的注释,这里按照我个人的理解注释一下
// Router实现了Http.Handler接口,用于注册分发路由
type Router struct {
// trees 是一个基数树集合,每一个HTTP方法对应一棵单独的路由树
// node是基数树的根节点
trees map[string]*node
// 用于开启上文提到的自动处理URL尾部斜杆的特性
// 这个值为true时,如果/foo/没有被匹配到,会尝试匹配/foo
RedirectTrailingSlash bool
// 用于开启上文提到的路由校正的特性
// 这个值为true时,会对../和//这种路径进行校正
RedirectFixedPath bool
// 这个值为true时,如果当前方法的路由没有被匹配到,会尝试匹配其他方法的路由,
// 如果匹配到了则返回405,如果没有,就交给NotFound Handler处理
HandleMethodNotAllowed bool
// 这个值为true时,将开启OPTIONS自动匹配,注意: 手动匹配优先级更高
HandleOPTIONS bool
// 没有匹配到相应路由的时候会调用这个方法
// 如果没有注册这个方法会返回 NotFound
NotFound http.Handler
// 没有匹配到相应路由并且HandleMethodNotAllowed为true时会调用这个方法
MethodNotAllowed http.Handler
// 用于从panic当中恢复
// 需要返回500错误,并且渲染相应的错误页面
PanicHandler func(http.ResponseWriter, *http.Request, interface{})
}
初始化 Router 之后看看路由是如何保存并且注册的
路由是如何保存的?
这里以官方 Readme 当中的例子说明:
如果注册了以下路由
r.GET("/", f1)
r.GET("/search/", f2)
r.GET("/support/", f3)
r.GET("/blog/", f4)
r.GET("/blog/:post/", f5)
r.GET("/about_us/", f6)
r.GET("/about_us/team/", f7)
r.GET("/contact/", f8)
那么这些路由会如下方所示,以一颗树的形式保存,并且这些路由的公共前缀会被抽离并且变为上一层节点
Priority 表示加上自身一共有多少个节点
Path 表示路径
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>
r.Handle
r.Get
, r.Post
等方法实质都是通过调用 r.Handle 实现的
func (r *Router) Handle(method, path string, handle Handle) {
// 路径注册必须从/开始,否则直接报错
if path[0] != '/' {
panic("path must begin with '/' in path '" + path + "'")
}
// 路由树map不存在需要新建
if r.trees == nil {
r.trees = make(map[string]*node)
}
// 获取当前方法所对应树的根节点,不存在则新建一个
root := r.trees[method]
if root == nil {
root = new(node)
r.trees[method] = root
}
// 向路由树当中添加一条一条路由
root.addRoute(path, handle)
}
node
路由是注册到一颗路由树当中的,先看看节点的源码,再来分析,是如何添加路由的
type node struct {
// 当前节点的路径
path string
// 是否为参数节点,参数节点用:name表示
wildChild bool
// 当前节点类型, 一共有4种
// static: 静态节点,默认类型
// root: 根节点
// param: 其他节点
// catchAll: 带有*的节点,这里*的作用和正则当中的*一样
nType nodeType
// 当前路径上最大参数的个数,不能超过255
maxParams uint8
// 代表分支的首字母
// 上面的例子,当前节点为s
// 那么indices = eu
// ├s nil
// |├earch\ *<2>
// |└upport\ *<3>
indices string
// 孩子节点
children []*node
// 注册的路由
handle Handle
// 权重,表示当前节点加上所有子节点的数目
priority uint32
}
流程
httprouter使用了前缀树来存储路由和其对应的处理函数。
httprouter为每个http方法均创建了一颗前缀树。
type node struct {
path string // 路径
wildChild bool // 路径是否有通配符(:或*)
nType nodeType // 节点类型,static(默认),root,param,catchAll中的一种,对应值为0,1,2,3
maxParams uint8 // 当前节点及其子节点最大param数量
priority uint32 // 优先级
indices string // children的索引(孩子节点的第一个字母)
children []*node // 孩子节点
handle Handle // 路由处理函数
}
假如我们有下面这几个路径(GET方法):
/search/
/support
/blog/:post/
/blog/:post/:name
/files/*filepath
则创建的前缀树如下:
/
├─ blog/
│ └─ :post
│ └─ /
│ └─ :name
├─ files
│ └─ ""
│ └─ /*filepath
└─ s
├─ earch
└─ upport
插入代码为func (n *node) addRoute(path string, handle Handle)。
插入即为前缀树的插入操作,从根节点开始,查找树是否有相同前缀,对无共同前缀的部分进行插入操作,在这个操作中,原来的节点非共同前缀部分会作为子节点。
要注意的是,插入分为有参数(有:或*符号)和无参数两类
1.path为最大能利用的公共前缀,当为叶子节点时,则为除去路径前缀的剩余部分(无参数)
2.如果有参数的路径,在上一步会进行额外操作,把参数部分裂开作为一个单独节点。如:/blog/:name/:id/,即使其为叶子节点,会把:name和:id作为单独节点
3.如果有通配符(*),则只能为最后一个节点。
查找
查找代码func (n *node) getValue(path string)
查找时同样分为有参数和无参数两类
1.对于无参数的路径,直接根据node.path匹配,根据node.indices进入下一节点顺着树查找即可。
2.对于有参数的路径,区分参数(:符号)和通配符(*),拿到路径对应的参数,也比较简单。
总结
1.httprouter使用前缀树来复用空间
2.代码简洁易懂
3.支持路径传参和通配
4.有优先级,保证匹配的确定性
纵览
New方法实际就是生成一个HttpRouter对象 接下来看看注册Get映射的实现
func (r *Router) GET(path string, handle Handle) {
r.Handle(http.MethodGet, path, handle)
}
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]
if root == nil {
root = new(node)
r.trees[method] = root
r.globalAllowed = r.allowed("*", "")
}
root.addRoute(path, handle)
}
func (n *node) addRoute(path string, handle Handle) {
fullPath := path
n.priority++
numParams := countParams(path)
// non-empty tree
if len(n.path) > 0 || len(n.children) > 0 {
walk:
for {
// Update maxParams of the current node
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++
}
// 出现部分前缀匹配,最极端情况/根路径
//创建一个以匹配部分为映射路径的空节点顶替当前匹配节点n,当前匹配节点n作为其子节点
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,
}
// 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]})
n.path = path[:i]
n.handle = nil
n.wildChild = false
}
//到这里可以确认,n绝对可以成为注册映射的祖先节点
//找到最匹配的祖先节点作为自己的父节点
if i < len(path) {
path = path[i:]
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
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
var pathSeg string
if n.nType == catchAll {
pathSeg = path
} else {
pathSeg = strings.SplitN(path, "/", 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 +
"'")
}
}
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
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
if c != ':' && c != '*' {
// []byte for proper unicode char conversion, see #65
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) { // Make node a (in-path) leaf
if n.handle != nil {
panic("a handle is already registered for path '" + fullPath + "'")
}
n.handle = handle
}
return
}
} else {
//树是空的,直接作为root
n.insertChild(numParams, path, fullPath, handle)
n.nType = root
}
}
然后查看其处理请求的方式,也就是对前缀树进行查询
func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) {
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请求
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)
}
}
func (n *node) getValue(path string) (handle Handle, p Params, tsr bool) {
walk: // outer loop for walking the tree
for {
//查询前缀树节点
if len(path) > len(n.path) {
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
if !n.wildChild {
c := path[0]
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 {
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
p[i].Key = n.path[1:]
p[i].Value = path[:end]
// we need to go deeper!
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
}
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")
}
}
} else if path == n.path {
// We should have reached the node containing the handle.
// Check if this node has a handle registered.
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
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
}
}
参考资料
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)