hertz 路由如何搜寻到对应处理器

简介

之前我介绍了 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 的开头, 提前的声明了一些变量, 其中 searchIndexparamIndex 是用于截取路径和参数用的。

以之前的示例为背景, 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) &&currentNode.prefix == search[:len(currentNode.prefix)]

对应的情况是在当前节点的前缀与 search 的前缀匹配且 search 长度大于或等于当前节点前缀的情况。

具体来说,这个条件检查两个条件:

  1. len(search) >= len(currentNode.prefix):确保 search 的长度大于或等于当前节点前缀的长度。这是为了避免 search 过短而无法匹配当前节点前缀的情况。
  2. 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 已经走到尽头都没匹配上, 那么就得将 searchsearchIndex 回溯一下

此时变量的情况如下

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++
    }
}

结尾

这篇文章完稿之际其实距离上篇文章并不久, 但是世事难料, 编程将和我告一段落了.

希望我的文章可以帮助到需要它的人, 再见!

相关推荐
Adolf_19931 小时前
Flask-JWT-Extended登录验证, 不用自定义
后端·python·flask
叫我:松哥1 小时前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
海里真的有鱼1 小时前
Spring Boot 项目中整合 RabbitMQ,使用死信队列(Dead Letter Exchange, DLX)实现延迟队列功能
开发语言·后端·rabbitmq
工业甲酰苯胺1 小时前
Spring Boot 整合 MyBatis 的详细步骤(两种方式)
spring boot·后端·mybatis
新知图书2 小时前
Rust编程的作用域与所有权
开发语言·后端·rust
wn5313 小时前
【Go - 类型断言】
服务器·开发语言·后端·golang
希冀1233 小时前
【操作系统】1.2操作系统的发展与分类
后端
GoppViper4 小时前
golang学习笔记29——golang 中如何将 GitHub 最新提交的版本设置为 v1.0.0
笔记·git·后端·学习·golang·github·源代码管理
爱上语文5 小时前
Springboot的三层架构
java·开发语言·spring boot·后端·spring
serve the people5 小时前
springboot 单独新建一个文件实时写数据,当文件大于100M时按照日期时间做文件名进行归档
java·spring boot·后端