简介
之前我介绍了 cloudwego/hertz 是如何将路由存储到 Radix Tree 中的, 没有观看过的小伙伴可以先观看一下, 今天我将通过一个简单的例子梳理一下, hertz 是如何将 url 交由对应 handler 进行处理的。
在官方 blog 的介绍中, 介绍了 hertz 不仅支持静态路由、参数路由的注册, 还支持按优先级 匹配,支持路由回溯, 支持尾斜线重定向。 这些特性我将会在后续一一介绍。
一个简单的例子
在这个简单的例子中, 我注册了两个 GET router 进入 hertz, 其中涵盖了 static, param, all 这三种类型的路由节点
go
func main() {
h := server.New()
h.GET("/hello/world", func(c context.Context, ctx *app.RequestContext) {
param := ctx.Param("param")
ctx.JSON(200, utils.H{
"msg": param,
})
})
h.GET("/hello/:param/*any", func(c context.Context, ctx *app.RequestContext) {
param := ctx.Param("param")
a := ctx.Param("any")
ctx.JSON(200, utils.H{
"msg": param,
"any": a,
})
})
h.Spin()
}
以下是这个 GET 路由树的大致结构, 记住这张图, 后续的所有分析都是基于这个路由
于此同时我再对启动的服务器发送 GET http://localhost:8888/hello/world/test
的请求, hertz 会如何处理它呢?
从哪里开始处理 URL ?
由于今天介绍的重点是如何搜寻到每个 url 对应的处理器, 所有我们可以先从 route.ServeHTTP
入手
其中通过 tree.find
处理后, 如果对应的节点有对应处理的 handlers, 就会执行其中的逻辑, 并结束 ServeHTTP
。
所以可以说这个 find
是今天的需要介绍的核心方法了
与此同时注意 43 行的处理逻辑, 它就是支持尾斜线重定向的关键了
go
// ServeHTTP makes the router implement the Handler interface.
func (engine *Engine) ServeHTTP(c context.Context, ctx *app.RequestContext) {
// ...
// 获取 request method, rawPath
rPath := string(ctx.Request.URI().Path())
httpMethod := bytesconv.B2s(ctx.Request.Header.Method())
unescape := false
// path 是否需要转义
if engine.options.UseRawPath {
rPath = string(ctx.Request.URI().PathOriginal())
unescape = engine.options.UnescapePathValues
}
// 是否移除多余的斜杠
if engine.options.RemoveExtraSlash {
rPath = utils.CleanPath(rPath)
}
// ...
// Find root of the tree for the given HTTP method
methodTrees := engine.trees
paramsPointer := &ctx.Params
for _, tree := range methodTrees {
// 找到对应的 method Tree
if tree.method != httpMethod {
continue
}
// Find route in tree
value := tree.find(rPath, paramsPointer, unescape)
// 有对应的 handler 就进行处理
if value.handlers != nil {
ctx.SetHandlers(value.handlers)
ctx.SetFullPath(value.fullPath)
ctx.Next(c)
return
}
if httpMethod != consts.MethodConnect && rPath != "/" {
// 需要 RedirectTrailingSlash
if value.tsr && engine.options.RedirectTrailingSlash {
redirectTrailingSlash(ctx)
return
}
if engine.options.RedirectFixedPath && redirectFixedPath(ctx, tree.root, engine.options.RedirectFixedPath) {
return
}
}
break
}
if engine.options.HandleMethodNotAllowed {
for _, tree := range engine.trees {
if tree.method == httpMethod {
continue
}
if value := tree.find(rPath, paramsPointer, unescape); value.handlers != nil {
ctx.SetHandlers(engine.allNoMethod)
serveError(c, ctx, consts.StatusMethodNotAllowed, default405Body)
return
}
}
}
ctx.SetHandlers(engine.allNoRoute)
serveError(c, ctx, consts.StatusNotFound, default404Body)
}
find 的处理逻辑解析
相信我, 这个 find 可以说是逻辑非常复杂, 所以我会逐步放出代码并一步步的进行解析
于此同时我在解析开始前贴上 find 的完整代码, 初次阅读时可以先不去阅读它, 可以在后续慢慢的上来对照。
go
// ========= package param
// Param is a single URL parameter, consisting of a key and a value.
type Param struct {
Key string
Value string
}
// Params is a Param-slice, as returned by the router.
// The slice is ordered, the first URL parameter is also the first slice value.
// It is therefore safe to read values by the index.
type Params []Param
// ============= package route
type (
node struct {
// kind 为节点类型
// 有 static, param, all 三种类型
kind kind
label byte
prefix string
parent *node
// 子节点数组
children children
// original path
ppath string
// param names
pnames []string
// 函数处理链
handlers app.HandlersChain
paramChild *node
anyChild *node
// isLeaf indicates that node does not have child routes
isLeaf bool
}
kind uint8
children []*node
)
const (
// static kind
skind kind = iota
// param kind
pkind
// all kind
akind
paramLabel = byte(':')
anyLabel = byte('*')
slash = "/"
nilString = ""
)
type nodeValue struct {
handlers app.HandlersChain
tsr bool
fullPath string
}
func (r *router) find(path string, paramsPointer *param.Params, unescape bool) (res nodeValue) {
var (
currentNode = r.root // current node
search = path // current path
searchIndex = 0
paramIndex int
)
// backtrackToNextNodeKind 用于回溯到决策路径上的下一个节点类型
backtrackToNextNodeKind := func(fromKind kind) (alternativeNodeKind kind, valid bool) {
// 回溯到决策路径上的上一个节点,以继续搜索可能的匹配
previous := currentNode
currentNode = previous.parent
valid = currentNode != nil
// Next node type by priority
if previous.kind == akind {
alternativeNodeKind = skind
} else {
alternativeNodeKind = previous.kind + 1
}
// valid 为 false 表示在决策路径上没有其他可能的节点类型
if fromKind == skind {
// when backtracking is done from static kind block we did not change search so nothing to restore
return
}
// restore search to value it was before we move to current node we are backtracking from.
if previous.kind == skind {
searchIndex -= len(previous.prefix)
} else {
paramIndex--
// for param/any node.prefix value is always `:` so we can not deduce searchIndex from that and must use pValue
// for that index as it would also contain part of path we cut off before moving into node we are backtracking from
searchIndex -= len((*paramsPointer)[paramIndex].Value)
(*paramsPointer) = (*paramsPointer)[:paramIndex]
}
search = path[searchIndex:]
return
}
// search order: static > param > any
for {
if currentNode.kind == skind {
// 存在相同的前缀
if len(search) >= len(currentNode.prefix) && currentNode.prefix == search[:len(currentNode.prefix)] {
// Continue search
search = search[len(currentNode.prefix):]
searchIndex = searchIndex + len(currentNode.prefix)
} else {
// 拥有相同的前缀, 但存在 TrailingSlash
if (len(currentNode.prefix) == len(search)+1) &&
(currentNode.prefix[len(search)]) == '/' &&
currentNode.prefix[:len(search)] == search &&
(currentNode.handlers != nil || currentNode.anyChild != nil) {
res.tsr = true
}
// No matching prefix, let's backtrack to the first possible alternative node of the decision path
ak, ok := backtrackToNextNodeKind(skind)
if !ok {
return // No other possibilities on the decision path
} else if ak == pkind {
goto Param
} else {
// Not found (this should never be possible for static node we are looking currently)
break
}
}
}
if search == nilString && len(currentNode.handlers) != 0 {
res.handlers = currentNode.handlers
break
}
// Static node
if search != nilString {
// If it can execute that logic, there is handler registered on the current node and search is `/`.
if search == "/" && currentNode.handlers != nil {
res.tsr = true
}
// Go deeper
if child := currentNode.findChild(search[0]); child != nil {
currentNode = child
continue
}
}
if search == nilString {
if cd := currentNode.findChild('/'); cd != nil && (cd.handlers != nil || cd.anyChild != nil) {
res.tsr = true
}
}
Param:
// Param node
if child := currentNode.paramChild; search != nilString && child != nil {
currentNode = child
i := strings.Index(search, slash)
if i == -1 {
i = len(search)
}
*paramsPointer = (*paramsPointer)[:(paramIndex + 1)]
// 取出 param 的值
val := search[:i]
// 如果需要对参数值进行反转义, 则执行反转义操作
if unescape {
if v, err := url.QueryUnescape(search[:i]); err == nil {
val = v
}
}
(*paramsPointer)[paramIndex].Value = val
paramIndex++
// 更新`search`和搜索索引
search = search[i:]
searchIndex = searchIndex + i
if search == nilString {
if cd := currentNode.findChild('/'); cd != nil && (cd.handlers != nil || cd.anyChild != nil) {
res.tsr = true
}
}
continue
}
Any:
// Any node
if child := currentNode.anyChild; child != nil {
// If any node is found, use remaining path for paramValues
currentNode = child
*paramsPointer = (*paramsPointer)[:(paramIndex + 1)]
index := len(currentNode.pnames) - 1
val := search
if unescape {
if v, err := url.QueryUnescape(search); err == nil {
val = v
}
}
(*paramsPointer)[index].Value = val
// update indexes/search in case we need to backtrack when no handler match is found
paramIndex++
searchIndex += len(search)
search = nilString
res.handlers = currentNode.handlers
break
}
// Let's backtrack to the first possible alternative node of the decision path
ak, ok := backtrackToNextNodeKind(akind)
if !ok {
break // No other possibilities on the decision path
} else if ak == pkind {
goto Param
} else if ak == akind {
goto Any
} else {
// Not found
break
}
}
if currentNode != nil {
res.fullPath = currentNode.ppath
// save params' key
for i, name := range currentNode.pnames {
(*paramsPointer)[i].Key = name
}
}
return
}
func (n *node) findChild(l byte) *node {
for _, c := range n.children {
if c.label == l {
return c
}
}
return nil
}
提前声明的变量
在 find
的开头, 提前的声明了一些变量, 其中 searchIndex
和 paramIndex
是用于截取路径和参数用的。
以之前的示例为背景, r.root
即 currentNode 的节点很明显是根节点
这时这四个变量的情况如下
Param name | Value |
---|---|
currentNode | {prefix: /hello/, kind: skind} |
search | /hello/world/test |
searchIndex | 0 |
paramIndex | 0 |
ini
var (
currentNode = r.root // current node
search = path // current path
searchIndex = 0
paramIndex int
)
不断深入的搜索
这里我们先跳过 backtrackToNextNodeKind
函数, 它是用于探索节点回溯可能性的函数, 我们在遇到使用它的逻辑后再进行分析。
此时 currentNode.kind 的值为 skind, 即静态节点, 这在之前的图中是有体现的。
拥有相同的前缀
在这里,
scss
if len(search) >= len(currentNode.prefix) &¤tNode.prefix == search[:len(currentNode.prefix)]
对应的情况是在当前节点的前缀与 search
的前缀匹配且 search
长度大于或等于当前节点前缀的情况。
具体来说,这个条件检查两个条件:
len(search) >= len(currentNode.prefix)
:确保search
的长度大于或等于当前节点前缀的长度。这是为了避免search
过短而无法匹配当前节点前缀的情况。currentNode.prefix == search[:len(currentNode.prefix)]
:检查当前节点的前缀与search
的前缀是否完全匹配。它使用切片操作search[:len(currentNode.prefix)]
获取search
的前缀部分,并与当前节点的前缀进行比较。
如果这两个条件都满足,则表示当前节点的前缀与 search
的前缀匹配,并且 search
的长度足够长以继续进行匹配
scss
if currentNode.kind == skind {
// 存在相同的前缀
if len(search) >= len(currentNode.prefix) && currentNode.prefix == search[:len(currentNode.prefix)] {
// Continue search
search = search[len(currentNode.prefix):]
searchIndex = searchIndex + len(currentNode.prefix)
}
// ...
简单来说, 此时 search 为 /hello/world/test
, currentNode.prefix 为 /hello/
, 在这种情况下它们拥有相同的前缀。 为了继续搜索这时就要去除共同的前缀, 更新 searchIndex
此时变量的情况如下
Param name | Value |
---|---|
currentNode | {prefix: /hello/, kind: skind} |
search | world/test |
searchIndex | 7 |
paramIndex | 0 |
下面的代码其中第 10 行的判断, 用于确定是否需要尾斜线重定向, 需要就标记 tsr (TrailingSlashRedirect) 为 true
我发起的请求中不需要 tsr, 这个值将一直为 false
在经过 currentNode.findChild 后, 很明显 child 不可能为 nil, 程序于是深入一步继续搜索。
此时变量的情况如下
Param name | Value |
---|---|
currentNode | {prefix: world, kind: skind} |
search | world/test |
searchIndex | 7 |
paramIndex | 0 |
go
// 判断是否搜索完毕
if search == nilString && len(currentNode.handlers) != 0 {
res.handlers = currentNode.handlers
break
}
// Static node
if search != nilString {
// If it can execute that logic, there is handler registered on the current node and search is `/`.
if search == "/" && currentNode.handlers != nil {
res.tsr = true
}
// Go deeper
if child := currentNode.findChild(search[0]); child != nil {
currentNode = child
continue
}
}
func (n *node) findChild(l byte) *node {
for _, c := range n.children {
if c.label == l {
return c
}
}
return nil
}
搜索的优先级
此时 search 和 currentNode.prefix 还是拥有相同前缀 world
, 和之前相比, 这个节点没有 children 了 (此时搜索进入图例左下角的情况), 之后它还会经过 Param 和 Any 的判断(因为搜索顺序: 静态节点 > 参数节点 > 通配符节点), 但是都不符合。
Param name | Value |
---|---|
currentNode | {prefix: world, kind: skind} |
search | /test |
searchIndex | 12 |
paramIndex | 0 |
go
Param:
// Param node
if child := currentNode.paramChild; search != nilString && child != nil {
// ...
}
Any:
// Any node
if child := currentNode.anyChild; child != nil {
// ...
}
// Let's backtrack to the first possible alternative node of the decision path
ak, ok := backtrackToNextNodeKind(akind)
if !ok {
break // No other possibilities on the decision path
} else if ak == pkind {
goto Param
} else if ak == akind {
goto Any
} else {
// Not found
break
}
让我们开始回溯吧!
第一次的回溯
这时 fromKind 为 akind
, 通过简单的思考我们可以确定 vaild 为 true, alternativeNodeKind 为 1 即 (pkind)
进入这里的原因因为 search 已经走到尽头都没匹配上, 那么就得将 search
和 searchIndex
回溯一下
此时变量的情况如下
Param name | Value |
---|---|
currentNode | {prefix: /hello/, kind: skind} |
search | world/test |
searchIndex | 7 |
paramIndex | 0 |
go
backtrackToNextNodeKind := func(fromKind kind) (alternativeNodeKind kind, valid bool) {
// 回溯到决策路径上的上一个节点,以继续搜索可能的匹配
previous := currentNode
currentNode = previous.parent
valid = currentNode != nil
// Next node type by priority
if previous.kind == akind {
alternativeNodeKind = skind
} else {
alternativeNodeKind = previous.kind + 1
}
// valid 为 false 表示在决策路径上没有其他可能的节点类型
if fromKind == skind {
// 当从静态节点的回溯完成时,搜索路径没有变化,因此无需恢复g to restore
return
}
// restore search to value it was before we move to current node we are backtracking from.
if previous.kind == skind {
searchIndex -= len(previous.prefix)
} else {
paramIndex--
// for param/any node.prefix value is always `:` so we can not deduce searchIndex from that and must use pValue
// for that index as it would also contain part of path we cut off before moving into node we are backtracking from
searchIndex -= len((*paramsPointer)[paramIndex].Value)
(*paramsPointer) = (*paramsPointer)[:paramIndex]
}
search = path[searchIndex:]
return
}
这时 ak 为 pkind, 于是进入 Param 的情况
go
ak, ok := backtrackToNextNodeKind(akind)
if !ok {
break // No other possibilities on the decision path
} else if ak == pkind {
goto Param
} else if ak == akind {
goto Any
} else {
// Not found
break
}
Param 怎么进行解析 ?
具体分析逻辑在注释之中, 在经过这段处理后, 此时变量的情况如下
Param name | Value |
---|---|
currentNode | {prefix: ":", kind: pkind, } |
search | /test |
searchIndex | 12 |
param | 1 |
ini
Param:
// Param node
if child := currentNode.paramChild; search != nilString && child != nil {
currentNode = child
// 确认参数的位置
i := strings.Index(search, slash)
if i == -1 {
i = len(search)
}
// 添加参数的空间
*paramsPointer = (*paramsPointer)[:(paramIndex + 1)]
// 取出 param 的值
val := search[:i]
// 如果需要对参数值进行反转义, 则执行反转义操作
if unescape {
if v, err := url.QueryUnescape(search[:i]); err == nil {
val = v
}
}
// 添加参数的 Value
(*paramsPointer)[paramIndex].Value = val
// 更新参数信息
paramIndex++
// 更新搜索路径和搜索索引, 继续搜索
search = search[i:]
searchIndex = searchIndex + i
if search == nilString {
if cd := currentNode.findChild('/'); cd != nil && (cd.handlers != nil || cd.anyChild != nil) {
res.tsr = true
}
}
continue
}
此时的 currentNode 转到了图例的右侧, 拥有了 children. 但它是 pkind, 且没有对应的 handler 对 /hello/:param
这个 path, 于是就再次 go deeper, 此时变量的情况如下
Param name | Value |
---|---|
currentNode | {prefix: "/", kind: skind, } |
search | /test |
searchIndex | 12 |
param | 1 |
这时 current.prefix 和 search 拥有共同前缀 /
, 再次更新, 这个节点只有一个 anyChild, 于是开始解析 Any
Param name | Value |
---|---|
currentNode | {prefix: "/", kind: skind, } |
search | test |
searchIndex | 13 |
param | 1 |
Any 怎么进行解析
虽然乍一看好像和 Param 的逻辑差不多, 但是 Any 和 Param 相比有一个特殊的点.
即 Any 参数必须在路径的最后才可以使用, 于是最后直接将 search 清空附上对应 handlers
go
Any:
// Any node
if child := currentNode.anyChild; child != nil {
// If any node is found, use remaining path for paramValues
currentNode = child
*paramsPointer = (*paramsPointer)[:(paramIndex + 1)]
index := len(currentNode.pnames) - 1
val := search
if unescape {
if v, err := url.QueryUnescape(search); err == nil {
val = v
}
}
(*paramsPointer)[index].Value = val
// update indexes/search in case we need to backtrack when no handler match is found
paramIndex++
searchIndex += len(search)
search = nilString
res.handlers = currentNode.handlers
break
}
此时变量的情况如下
Param name | Value |
---|---|
currentNode | {prefix: "*", kind: akind, } |
search | "" |
searchIndex | 17 |
param | 2 |
处理路径完毕如何返回 ?
在这里会将节点的完整路径进行赋值, 并将之前没有添加的 Key 补上, 再进行返回
go
type nodeValue struct {
handlers app.HandlersChain
tsr bool
fullPath string
}
if currentNode != nil {
res.fullPath = currentNode.ppath
// save params' key
for i, name := range currentNode.pnames {
(*paramsPointer)[i].Key = name
}
}
return
没有提到的情况
backtrackToNextNodeKind
有的时候回溯只是修改 search, 但事实上有时候还要将参数回溯
scss
if previous.kind == skind {
searchIndex -= len(previous.prefix)
} else {
paramIndex--
// for param/any node.prefix value is always `:` so we can not deduce searchIndex from that and must use pValue
// for that index as it would also contain part of path we cut off before moving into node we are backtracking from
searchIndex -= len((*paramsPointer)[paramIndex].Value)
(*paramsPointer) = (*paramsPointer)[:paramIndex]
}
不存在相同前缀的情况
此时当前节点的前缀与搜索路径的前缀完全匹配,但 搜索路径的长度比当前节点的前缀多出一个斜杠。
这通常用于处理类似于路由重定向或类似的特殊情况,其中搜索路径的长度比当前节点的前缀短一个字符,但仍然需要处理该路径
go
if len(search) >= len(currentNode.prefix) && currentNode.prefix == search[:len(currentNode.prefix)] {
// Continue search
// ...
} else {
// 拥有相同的前缀, 但存在 TrailingSlash
if (len(currentNode.prefix) == len(search)+1) &&
(currentNode.prefix[len(search)]) == '/' &&
currentNode.prefix[:len(search)] == search &&
(currentNode.handlers != nil || currentNode.anyChild != nil) {
res.tsr = true
}
// 没有匹配的前缀,让我们回溯到决策路径上的第一个可能的替代节点
ak, ok := backtrackToNextNodeKind(skind)
if !ok {
return // No other possibilities on the decision path
} else if ak == pkind {
goto Param
} else {
// Not found (this should never be possible for static node we are looking currently)
break
}
}
Handler 怎么处理请求
在完美的情况下, 假定 value.handlers != nil, 这时 Next 方法会执行对应的所有 handler, 最终结束
scss
value := tree.find(rPath, paramsPointer, unescape)
// 有对应的 handler 就进行处理
if value.handlers != nil {
ctx.SetHandlers(value.handlers)
ctx.SetFullPath(value.fullPath)
ctx.Next(c)
return
}
// ======== package app
func (ctx *RequestContext) Next(c context.Context) {
ctx.index++
for ctx.index < int8(len(ctx.handlers)) {
ctx.handlers[ctx.index](c, ctx)
ctx.index++
}
}
结尾
这篇文章完稿之际其实距离上篇文章并不久, 但是世事难料, 编程将和我告一段落了.
希望我的文章可以帮助到需要它的人, 再见!