第二章-(前缀树路由)

在上一节中我们已经搭建起了自己的服务,并且可以在浏览器中看到服务运行的效果,接下来就要控制路由的访问了,之前我们使用一个map来存储路由,但是这样有一个问题,我们知道所有的服务路由不可能都为静态,一定会有类似hello/:username等类似的路由,我们称此类路由为动态路由,所谓动态路由,即一条路由规则可以匹配某一类型而非某一条固定的路由

动态路由有很多种实现方式,支持的规则、性能等有很大的差异。例如开源的路由实现gorouter支持在路由规则中嵌入正则表达式,例如/p/[0-9A-Za-z]+,即路径中的参数仅匹配数字和字母;另一个开源实现httprouter就不支持正则表达式。著名的Web开源框架gin 在早期的版本,并没有实现自己的路由,而是直接使用了httprouter,后来不知道什么原因,放弃了httprouter,自己实现了一个版本。

实现动态路由最常用的数据结构,被称为前缀树(Trie树)。看到名字你大概也能知道前缀树长啥样了:每一个节点的所有的子节点都拥有相同的前缀。

如果尚未接触过前缀树路由,在这里可以参考https://www.bilibili.com/video/BV1Az4y1S7c7?from=search&seid=13549176341384177198&spm_id_from=333.337.0.0的讲解

HTTP请求的路径恰好是由/分隔的多段构成的,因此,每一段可以作为前缀树的一个节点。我们通过树结构查询,如果中间某一层的节点都不满足条件,那么就说明没有匹配到的路由,查询结束。

接下来我们实现的动态路由具备以下两个功能。

  • 参数匹配:。例如 /p/:lang/doc,可以匹配 /p/c/doc 和 /p/go/doc
  • 通配*。例如 /static/*filepath,可以匹配/static/fav.ico,也可以匹配/static/js/jQuery.js,这种模式常用于静态服务器,能够递归地匹配子路径。

 

前缀树路由的实现:

前缀树结构:

type node struct {
pattern string // 待匹配路由,例如 /hello/:name
part string // 路由中的一部分,例如 :hello
children []*node // 子节点,例如 [doc, tutorial, intro] 在这里应当注意,前缀树通常根节点不保存任何数据,只是为了作为其它子节点的父节点
isWild bool // 是否精确匹配,part 含有 : 或 * 时为true
}
// 第一个匹配成功的节点,用于插入
func (n *node) matchChild(part string) *node {
for _, child := range n.children {
if child.part == part || child.isWild {
return child
}
}
return nil
}
// 所有匹配成功的节点,用于查找
func (n *node) matchChildren(part string) []*node {
nodes := make([]*node, 0)
for _, child := range n.children {
if child.part == part || child.isWild {
nodes = append(nodes, child)
}
}
return nodes
}
//递归插入,构建trie树
func (n *node)insert(pattern string,parts []string,height int){

if len(parts) == height{
n.pattern =pattern
return
}
part := parts[height]
child := n.matchChild(part)
if child == nil{
child = &node{
part: part,
IsPrecise: part[0]==':' || part[0]=='*',
}
n.children = append(n.children,child)
}
fmt.Println("子节点为:",n.children)
child.insert(pattern,parts,height+1)
}


func (n *node)search(parts []string,height int)*node{
if len(parts)==height || strings.HasPrefix(n.part, "*"){
if n.pattern==""{
return nil
}
return n
}

part := parts[height]
children := n.matchChildren(part)

for _,child :=range children{
result := child.search(parts, height+1)
if result != nil {
return result
}
}
return nil
}

func (n *node) travel(list *[]*node) {
if n.pattern != "" {
*list = append(*list, n)
}
for _, child := range n.children {
child.travel(list)
}
}

func (n *node) String() string {
return fmt.Sprintf("node{pattern=%s, part=%s, isWild=%t}", n.pattern, n.part, n.IsPrecise)
}
以上为trie.go中的代码文件,其主要目的在于构建我们所需要的前缀路由树,通过前缀路由树构建我们自己的路由导航。
于此同时,上一章中router.go文件也需要进行相应的变动,我们需要改变router结构体的定义形势


type Router struct {
roots map[string]*node
handlers map[string]HandlerFunc
}


//在这里,我们原先的写法是:handlers map[string]HandlerFunc
//对此将handler与root中间插入node,目的就是为了通过上一个文件中所构造的trie树对路由进行更多的操作
//如果不进行改动,那么我们对路由的操作将非常死板,只能预先定义好写死的路由
//但如果将路由和view中间分开加一层前缀路由树,那么我们可以动态的进行路由的添加,分组,删除等操作
//接下来对路由相应的操作和方法也进行同样的改动

func NewRouter() *Router {
return &Router{
roots:make(map[string]*node),
handlers:make(map[string]HandlerFunc),
}
}

func parsePattern(pattern string) []string {
vs := strings.Split(pattern, "/")

parts := make([]string, 0)
for _, item := range vs {
if item != "" {
parts = append(parts, item)
if item[0] == '*' {
break
}
}
}
return parts
}

func (r *router) addRoute(method string, pattern string, handler HandlerFunc) {
parts := parsePattern(pattern)

key := method + "-" + pattern
_, ok := r.roots[method]
if !ok {
r.roots[method] = &node{}
}
r.roots[method].insert(pattern, parts, 0)
r.handlers[key] = handler
}

func (r *router) getRoute(method string, path string) (*node, map[string]string) {
searchParts := parsePattern(path)
params := make(map[string]string)
root, ok := r.roots[method]

if !ok {
return nil, nil
}

n := root.search(searchParts, 0)

if n != nil {
parts := parsePattern(n.pattern)
for index, part := range parts {
if part[0] == ':' {
params[part[1:]] = searchParts[index]
}
if part[0] == '*' && len(part) > 1 {
params[part[1:]] = strings.Join(searchParts[index:], "/")
break
}
}
return n, params
}

return nil, nil
}


在 HandlerFunc 中,希望能够访问到解析的参数,因此,需要对 Context 对象增加一个属性和方法,来提供对路由参数的访问。我们将解析后的参数存储到Params中,通过c.Param("lang")的方式获取到对应的值。
type Context struct {

//请求和响应对象
Writer http.ResponseWriter
Req *http.Request

//请求路径和方法
Path string
Method string
//该参数目的是为了获取路由中的信息...
Params map[string]string
//状态码
StatusCode int
}

func (c *Context) Param(key string) string {
value, _ := c.Params[key]
return value
}
//至此,路由部分基础功能增加完善,通过测试来添加路由
package finto

import (
"fmt"
"reflect"
"testing"
)

func newTestRouter() *Router {
r := NewRouter()
r.AddRoute("GET", "/", nil)
r.AddRoute("GET", "/hello/:name", nil)
r.AddRoute("GET", "/hello/b/c", nil)
r.AddRoute("GET", "/hi/:name", nil)
r.AddRoute("GET", "/assets/*filepath", nil)
return r
}

//在main函数中进行测试

func main(){
fmt.Println("框架启动")
r:= finto.New()
r.Get("/", func(c *finto.Context) {
c.Html(http.StatusOK, "<h1>Hello Finto</h1>")
})

r.Get("/hello", func(c *finto.Context) {
c.Html(http.StatusOK, "<h1>Hello Hello</h1>")
})

r.Get("/hello/:name", func(c *finto.Context) {
c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
})

r.Get("/assets/*filepath", func(c *finto.Context) {
c.Json(http.StatusOK, finto.H{"filepath": c.Param("filepath")})
})


心得:
1.本章最晦涩难懂的地方在于前缀路由树的构建和遍历,应当对数据结构有深刻认识,前缀树是构建路由的主要和通用手段(大部分web框架都采用此种方式构建)
2.将路由中map[string]handler中的粘合方式改为map[string]*node,map[string]handler,相当于在原先的路由中间加了一层trie树结构的路由
3.context中需要获取路由中的信息,添加了param参数用来保存交互中的数据


posted @ 2021-11-16 15:36  fanyiaaa  阅读(347)  评论(0编辑  收藏  举报