Golang:HttpRouter 源码分析(六)

485 阅读6分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第8天,点击查看活动详情

前言

前情提要:

终于把专栏搞好啦,以后可以直接在专栏里看本系列的往期文章~

Golang:HttpRouter 源码分析

终于来到 HttpRouter “源码分析” 部分的收官之篇啦~ 本篇中,我们将把源码中剩下的所有内容全部讲完。当然,光看源码分析可能没啥感觉,再加上前面的文章很长,不知道看到这里的你是不是已经把前面的内容都忘光了呢?为了巩固我们学到的知识,并且提升对 HttpRouter 的运用能力,本系列还会再加更至少两篇的文章,分别来进行梳理总结,以及通过实际的案例来使用 HttpRouter 中的各项功能。

如果本篇文章对你有帮助的话,(或者觉得作者打这么多字挺辛苦的话),不要忘记点个赞哦,感谢支持~

CleanPath

在上一章中,我们讲了大小写非敏感的查找,它是路由中 RedirectFixedPath 功能的一部分。实际上,在进行查找前,它先将请求路径化为了最简形式,也就是调用了 path.go 中的 CleanPath 函数。让我们来看一下吧~

源码

讲解

CleanPath 实际上就是URL版本的 path.Clean,它会返回路径的最简洁形式。

它通过纯词法处理返回与传入路径 path 等价的最短路径名。它迭代应用下列规则,直到无法进行更进一步处理为止:

  • 用一个斜杠替换多个斜杠;

  • 删除每个 . 元素(当前目录);

  • 删除每个内部 .. 元素(父目录)以及前面的非 .. 元素;

  • 消除以根路径开始的 .. 元素:也就是说,在路径的开始处将 /.. 替换为 /

如果处理后的结果是空字符串,则返回根路径 /

下面来看看代码:

if p == "" {    // 空串为返回根路径 '/'
    return "/"
}
n := len(p)     // 长度
var buf []byte  // 结果
r := 1          // 记录路径中要进行处理的下一个索引
w := 1          // 记录结果中要写入的下一个索引
// 可以看做是两个指针

if p[0] != '/' { // 路径必须以 '/' 开始
    r = 0
    buf = make([]byte, n+1)
    buf[0] = '/'
}

trailing := n > 1 && p[n-1] == '/'  // 标记是否有结尾斜杠

向结果中添加字符是通过以下函数进行的,

// buf是结果缓存,s是原字符串,w是写入位置,c是要写入的字符
func bufApp(buf *[]byte, s string, w int, c byte) {
    if *buf == nil {  // buf 还未创建时
        if s[w] == c { // 如果写入字符与原字符串在该位置字符相同,则直接返回
            return
        }
        *buf = make([]byte, len(s)) // 否则创建buf
        copy(*buf, s[:w]) // 将应该写入的部分写入
    }
    (*buf)[w] = c  // 写入字符
}

这个函数对结果缓存的创建有一个延迟作用,只有当必要时,也就是路径需要被化简时,才会创建缓存,并且将写入指针前面的路径(未修改的路径)直接拷贝过来,然后向写入位置写入字符。之后,这个函数就只会执行最后一句,与正常写入一样了。

接下来是遍历路径,进行化简。相比于 path 包,这里因为没有 lazybuf 而稍显笨重,但循环是完全内联的,所以它没有昂贵的函数调用(除了make以外)。这里要解释一下,为什么它是内联的。

运行下面的命令可以看到类似的结果:

$ go build -gcflags="-m -m" .\path.go
# command-line-arguments
.\path.go:114:6: can inline bufApp with cost 30 as: func(*[]byte, string, int, byte) { if *buf == nil { if s[w] == c { return  }; *buf = make([]byte, len(s)); copy(*buf, s[:w]) }; (*buf)[w] = c }
.\path.go:21:6: cannot inline CleanPath: function too complex: cost 337 exceeds budget 80
.\path.go:88:11: inlining call to bufApp
.\path.go:94:11: inlining call to bufApp
.\path.go:103:9: inlining call to bufApp

这里实际上 go 的编译器优化,对于在 AST 中节点数小于 80 个的函数会自动进行内联优化。所以 bufApp 就被自动内联到循环中了。

for r < n {  // 从根/的下一个字符开始,每次判断是以到下一个/为止的片段进行的
    switch {
    case p[r] == '/':   //前面已经有一个/了,再有/就要消除
        r++  // r只负责标记下一个读到哪儿了,读完就前进
    case p[r] == '.' && r+1 == n:   // 以.结尾,因为本次就结束循环了,所以要通过循环外面的判断来添加尾斜杠
        trailing = true
        r++
    case p[r] == '.' && p[r+1] == '/':   // ./ 结构,直接跳过
        r += 2
    // ../或以..结尾的结构
    case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'):
        r += 3
        if w > 1 {   // 回溯到上一个/的位置
            w--
            if buf == nil {
                for w > 1 && p[w] != '/' {   // 还没有创建buf就从路径参数里回溯
                    w--
                }
            } else {
                for w > 1 && buf[w] != '/' {  // 否则从buf里回溯
                    w--
                }
            }
        }
    default:
            if w > 1 {   // 为前一个路径片段的结尾添加斜杠
                bufApp(&buf, p, w, '/')
                w++
            }
            for r < n && p[r] != '/' {   // 一直写到下一个斜杠(不包含)
                bufApp(&buf, p, w, p[r])
                w++
                r++
            }
    }
}

上面代码的主要逻辑是,不算根路径的 /,从后面开始按照 / 来划分每一个路径片段,一次处理一个片段,在之后的循环时如果跳到 default 了,则添加前一个片段的 / 并把从前一个片段处理完到这一次循环之间处理的片段添加到 buf 中(不包括斜杠)。

if trailing && w > 1 {
    bufApp(&buf, p, w, '/')
    w++
}
if buf == nil {
    return p[:w]
}
return string(buf[:w])

最后就是判断一下要不要加尾斜杠,如果没有修改(或者只删除了结尾的一部分),那么直接返回原路径的(或者它前面的部分)就可以了,否则返回缓存中的结果。

allowed

源码

讲解

allowed 源码比较容易理解,就不详细的讲了,在这里梳理一下流程:

  • 如果路径是 * ,说明是全局范围的查询:

    • 如果请求方法是空的,表示刷新全局对允许请求方法的缓存。

    • 如果不为空,返回全局对允许请求方法的缓存。

  • 如果不是全局查询:

    • 遍历所有请求方法的路由树,除了传入的请求方法(因为请求已经失败了)和 OPTIONS 方法以外,通过 getValue 看是不是能获取到 handle,可以就说明该方法允许请求。
  • 最后如果允许的请求方法不为空,向其中添加 OPTIONS 后,将请求方法按升序排序,转为以逗号分隔的字符串并返回。

其它函数

剩下的函数中有一些是为 net/http 包的一些请求处理函数写的适配器:

func (r *Router) Handler(method, path string, handler http.Handler) {
    r.Handle(method, path,
        func(w http.ResponseWriter, req *http.Request, p Params) {
            if len(p) > 0 {
                ctx := req.Context()
                ctx = context.WithValue(ctx, ParamsKey, p)
                req = req.WithContext(ctx)
            }
            handler.ServeHTTP(w, req)
        },
    )
}
func (r *Router) HandlerFunc(method, path string, handler http.HandlerFunc) {
    r.Handler(method, path, handler)
}

第一个是给 http.Handler 写的,第二个是给 http.HandlerFunc 写的,你可以用这两个函数快速重构用 http 包写的代码。

还有像 Lookup 函数可以手动查询某一路径,这主要是便于用来基于本包写一些高级的框架。

func (r *Router) Lookup(method, path string) (Handle, Params, bool) {
    if root := r.trees[method]; root != nil {
        return root.getValue(path)
    }
    return nil, nil, false
}

以及还有一些请求方法,之前提到了实际上和 GET 一样都是对 Handle 方法的调用。

HttpRouter 也提供了基本的静态文件访问服务,你注册的路径必须以 /*filepath 结尾。 如果想使用操作系统的文件系统实现,需要用 http.Dir

例如: router.ServeFiles("/src/*filepath", http.Dir("/var/www"))

func (r *Router) ServeFiles(path string, root http.FileSystem) {
    if len(path) < 10 || path[len(path)-10:] != "/*filepath" {
        panic("path must end with /*filepath in path '" + path + "'")
    }
    fileServer := http.FileServer(root)
    r.GET(path, func(w http.ResponseWriter, req *http.Request, ps Params) {
        req.URL.Path = ps.ByName("filepath")
        fileServer.ServeHTTP(w, req)
    })
}

总结

到这里,HttpRouter 的全部源码我们就分析完了,完结撒花 ✿✿ヽ(°▽°)ノ✿ !

最后,如果本篇文章对你有帮助的话,(或者觉得作者打这么多字挺辛苦的话),不要忘记点个赞哦,感谢支持~