本篇是Mygin的第四篇

目的

  • 使用 Trie 树实现动态路由解析。
  • 参数绑定

前缀树

本篇比前几篇要复杂一点,原来的路由是用map实现,索引非常高效,但是有一个弊端,键值对的存储的方式,只能用来索引静态路由。遇到类似hello/:name这动态路由就无能为力了,实现动态路由最常用的数据结构,被称为前缀树。这种结构非常适用于路由匹配。比如我们定义了如下路由:

  • /a/b/c
  • /a/b
  • /a/c
  • /a/b/c/d
  • /a/:name/c
  • /a/:name/c/d
  • /a/b/:name/e

在前缀树中的结构体

HTTP请求的路径是由/分隔的字符串构成的,所以用/拆分URL字符串,得到不同的树节点,且有对应的层级关系。

代码实现

  • Mygin/tree.go 首先看tree中node结构定义
type node struct {
children []*node //子节点
part string //树节点
wildChild bool //是否是精确匹配
handlers HandlersChain //路由回调,实际的请求
nType nodeType //节点类型 默认static params
fullPath string //完整路径
}
  • Mygin/tree.go 具体实现
package mygin

import (
"strings"
) type nodeType uint8 // 路由的类型
const (
static nodeType = iota
root
param
catchAll
) // 不同的method 对应不同的节点树 定义
type methodTree struct {
method string
root *node
} // Param 参数的类型key=> value
type Param struct {
Key string
Value string
} // Params 切片
type Params []Param type methodTrees []methodTree type node struct {
children []*node
part string
wildChild bool
handlers HandlersChain
nType nodeType
fullPath string
} // Get 获取 参数中的值
func (ps Params) Get(name string) (string, bool) {
for _, entry := range ps {
if entry.Key == name {
return entry.Value, true
}
}
return "", false
} // ByName 通过ByName获取参数中的值 会忽略掉错误,默认返回 空字符串
func (ps Params) ByName(name string) (v string) {
v, _ = ps.Get(name)
return
} // 根据method获取root
func (trees methodTrees) get(method string) *node {
for _, tree := range trees {
if tree.method == method {
return tree.root
}
}
return nil
} // 添加路径时
func (n *node) addRoute(path string, handlers HandlersChain) { //根据请求路径按照'/'划分
parts := n.parseFullPath(path) //将节点插入路由后,返回最后一个节点
matchNode := n.insert(parts) //最后的节点,绑定执行链
matchNode.handlers = handlers //最后的节点,绑定完全的URL,后续param时有用
matchNode.fullPath = path } // 按照 "/" 拆分字符串
func (n *node) parseFullPath(fullPath string) []string {
splits := strings.Split(fullPath, "/")
parts := make([]string, 0)
for _, part := range splits {
if part != "" {
parts = append(parts, part)
if part == "*" {
break
}
}
}
return parts
} // 根据路径 生成节点树
func (n *node) insert(parts []string) *node {
part := parts[0]
//默认的字节类型为静态类型
nt := static
//根据前缀判断节点类型
switch part[0] {
case ':':
nt = param
case '*':
nt = catchAll
} //插入的节点查找
var matchNode *node
for _, childNode := range n.children {
if childNode.part == part {
matchNode = childNode
}
} //如果即将插入的节点没有找到,则新建一个
if matchNode == nil {
matchNode = &node{
part: part,
wildChild: part[0] == '*' || part[0] == ':',
nType: nt,
}
//新子节点追加到当前的子节点中
n.children = append(n.children, matchNode)
} //当最后插入的节点时,类型赋值,且返回最后的节点
if len(parts) == 1 {
matchNode.nType = nt
return matchNode
} //匹配下一部分
parts = parts[1:]
//子节点继续插入剩余字部分
return matchNode.insert(parts)
} // 根据路由 查询符合条件的节点
func (n *node) search(parts []string, searchNode *[]*node) {
part := parts[0] //a allChild := n.matchChild(part) //b c :name if len(parts) == 1 {
// 如果到达路径末尾,将所有匹配的节点加入结果
*searchNode = append(*searchNode, allChild...)
return
} parts = parts[1:] //b for _, n2 := range allChild {
// 递归查找下一部分
n2.search(parts, searchNode)
} } // 根据part 返回匹配成功的子节点
func (n *node) matchChild(part string) []*node { allChild := make([]*node, 0)
for _, child := range n.children {
if child.wildChild || child.part == part {
allChild = append(allChild, child)
}
} return allChild
}

上诉路由中,实现了插入insert和匹配search时的功能,插入时安装拆分后的子节点,递归查找每一层的节点,如果没有匹配到当前part的节点,则新建一个。查询功能,同样也是递归查询每一层的节点。

  • Mygin/router.go
package mygin

import (
"net/http"
) type Router struct {
trees methodTrees
} // 添加路由方法
func (r *Router) addRoute(method, path string, handlers HandlersChain) {
//根据method获取root
rootTree := r.trees.get(method) //如果root为空
if rootTree == nil {
//初始化一个root
rootTree = &node{part: "/", nType: root}
//将初始化后的root 加入tree树中
r.trees = append(r.trees, methodTree{method: method, root: rootTree}) } rootTree.addRoute(path, handlers) } // Get Get方法
func (r *Router) Get(path string, handlers ...HandlerFunc) {
r.addRoute(http.MethodGet, path, handlers)
} // Post Post方法
func (e *Engine) Post(path string, handlers ...HandlerFunc) {
e.addRoute(http.MethodPost, path, handlers)
}
  • router中修改不大

  • Mygin/engine.go

package mygin

import (
"net/http"
) // HandlerFunc 定义处理函数类型
type HandlerFunc func(*Context)
// HandlersChain 定义处理函数链类型
type HandlersChain []HandlerFunc // Engine 定义引擎结构,包含路由器
type Engine struct {
Router
} // ServeHTTP 实现http.Handler接口的方法,用于处理HTTP请求
func (e *Engine) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 获取对应HTTP方法的路由树的根节点
root := e.trees.get(r.Method)
// 解析请求路径
parts := root.parseFullPath(r.URL.Path) // 查找符合条件的节点
searchNode := make([]*node, 0)
root.search(parts, &searchNode) // 没有匹配到路由
if len(searchNode) == 0 {
w.Write([]byte("404 Not found!\n"))
return
} // 参数赋值
params := make([]Param, 0)
searchPath := root.parseFullPath(searchNode[0].fullPath)
for i, sp := range searchPath {
if sp[0] == ':' {
params = append(params, Param{
Key: sp[1:],
Value: parts[i],
})
}
} // 获取处理函数链
handlers := searchNode[0].handlers
if handlers == nil {
w.Write([]byte("404 Not found!\n"))
return
} // 执行处理函数链
for _, handler := range handlers {
handler(&Context{
Request: r,
Writer: w,
Params: params,
})
}
} // Default 返回一个默认的引擎实例
func Default() *Engine {
return &Engine{
Router: Router{
trees: make(methodTrees, 0, 9),
},
}
} // Run 启动HTTP服务器的方法
func (e *Engine) Run(addr string) error {
return http.ListenAndServe(addr, e)
}
package main

import (
"gophp/mygin"
) func main() {
// 创建一个默认的 mygin 实例
r := mygin.Default() // 定义路由处理函数
handleABC := func(context *mygin.Context) {
context.JSON(map[string]interface{}{
"path": context.Request.URL.Path,
})
} // 注册路由
r.Get("/a/b/c", handleABC)
r.Get("/a/b", handleABC)
r.Get("/a/c", handleABC) // 注册带参数的路由
r.Get("/a/:name/c", func(context *mygin.Context) {
name := context.Params.ByName("name")
path := "/a/" + name + "/c"
context.JSON(map[string]interface{}{
"path": path,
})
}) r.Get("/a/:name/c/d", func(context *mygin.Context) {
name := context.Params.ByName("name")
path := "/a/" + name + "/c/d"
context.JSON(map[string]interface{}{
"path": path,
})
}) r.Get("/a/b/:name/e", func(context *mygin.Context) {
name := context.Params.ByName("name")
path := "/a/b" + name + "/e"
context.JSON(map[string]interface{}{
"path": path,
})
}) r.Get("/a/b/c/d", handleABC) // 启动服务器并监听端口
r.Run(":8088")
}

测试

curl -i http://localhost:8088/a/b/c
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 23 Jan 2024 05:43:50 GMT
Content-Length: 18 {"path":"/a/b/c"}
➜ ~ curl -i http://localhost:8088/a/b
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 23 Jan 2024 05:43:53 GMT
Content-Length: 16 {"path":"/a/b"}
➜ ~ curl -i http://localhost:8088/a/c
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 23 Jan 2024 05:43:57 GMT
Content-Length: 16 {"path":"/a/c"}
➜ ~ curl -i http://localhost:8088/a/b/c/d
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 23 Jan 2024 05:44:05 GMT
Content-Length: 20 {"path":"/a/b/c/d"} ➜ ~ curl -i http://localhost:8088/a/scott/c
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 23 Jan 2024 05:45:16 GMT
Content-Length: 22 {"path":"/a/scott/c"}
➜ ~ curl -i http://localhost:8088/a/scott/c/d
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 23 Jan 2024 05:45:22 GMT
Content-Length: 24 {"path":"/a/scott/c/d"}
➜ ~ curl -i http://localhost:8088/a/b/scott/e
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 23 Jan 2024 05:45:32 GMT
Content-Length: 23 {"path":"/a/bscott/e"}

Mygin实现动态路由的更多相关文章

  1. AIX 环境下动态路由

    IBM AIX v5.3操作系统环境下动态路由配置如下: 1,用命令lssrc -S routed和lssrc -S gated分别检查routed和gated子系统是是活动状态.如果这两个子系统为活 ...

  2. asp.net MVC动态路由

    项目中遇到需要动态生成控制器和视图的. 于是就折腾半天,动态生成控制器文件和视图文件,但是动态生成控制器不编译是没法访问的. 找人研究后,得到要领: 1.放在App_Code文件夹内 2.不要命名空间 ...

  3. RIP、OSPF、BGP、动态路由选路协议、自治域AS

    相关学习资料 tcp-ip详解卷1:协议.pdf http://www.rfc-editor.org/rfc/rfc1058.txt http://www.rfc-editor.org/rfc/rfc ...

  4. Ngnix技术研究系列2-基于Redis实现动态路由

    上篇博文我们写了个引子: Ngnix技术研究系列1-通过应用场景看Nginx的反向代理 发现了新大陆,OpenResty OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台 ...

  5. 基于hi-nginx的web开发(python篇)——动态路由和请求方法

    hi.py的提供的路由装饰器接受两个参数,第一个参数指定动态路由的正则模式,第二个参数指定同意的http请求方法列表. 比如: @app.route(r"^/client/?$", ...

  6. vue+iview实现动态路由和权限验证

    github上关于vue动态添加路由的例子很多,本项目参考了部分项目后,在iview框架基础上完成了动态路由的动态添加和菜单刷新.为了帮助其他需要的朋友,现分享出实现逻辑,欢迎一起交流学习. Gith ...

  7. Cisco动态路由配置

    前言: 学完静态路由配置,该学动态路由.所以 学习完后来做终结. 准备: PC:192.168.1.10 R1:fa0/0 192.168.1.1 fa0/1 1.1.12.1 R2: fa0/0 1 ...

  8. Miox带你走进动态路由的世界——51信用卡前端团队

    写在前面: 有的时候再做大型项目的时候,确实会被复杂的路由逻辑所烦恼,会经常遇到权限问题,路由跳转回退逻辑问题.这几天在网上看到了51信用卡团队开源了一个Miox,可以有效的解决这些痛点,于是乎我就做 ...

  9. 从壹开始 [vueAdmin后台] 之三 || 动态路由配置 & 项目快速开发

    回顾 今天VS 2019正式发布,实验一波,你安装了么?Blog.Core 预计今天会升级到 Core 3.0 版本. 哈喽大家周三好!本来今天呢要写 Id4 了,但是写到了一半,突然有人问到了关于 ...

  10. 从壹开始前后端分离 [ vue + .netcore 补充教程 ] 三十║ Nuxt实战:动态路由+同构

    上期回顾 说接上文<二九║ Nuxt实战:异步实现数据双端渲染>,昨天咱们通过项目二的首页数据处理,简单了解到了 nuxt 异步数据获取的作用,以及亲身体验了几个重要文件夹的意义,整篇文章 ...

随机推荐

  1. vue 2实战系列 —— 复习Vue

    复习Vue 近期需要接手 vue 2的项目,许久未写,语法有些陌生.本篇将较全面复习 vue 2. Tip: 项目是基于 ant-design-vue-pro ant-design-vue-pro 由 ...

  2. Python——第五章:shutil模块

    复制文件 把dir1的文件a.txt 移动到dir2内 import shutil shutil.move("dir1/a.txt", "dir2") 复制两个 ...

  3. Python——第二章:字典的循环、嵌套、"解构"(解包)

    字典进阶操作 -- 循环和嵌套 字典的循环 我们先看直接打印字典的样子,会分别对每对key:value进行打印,并使用,分隔他们 dic = { "赵四": "特别能歪嘴 ...

  4. Spring Boot 导出EXCEL模板以及导入EXCEL数据(阿里Easy Excel实战)

    Spring Boot 导出EXCEL模板以及导入EXCEL数据(阿里Easy Excel实战) 导入pom依赖 编写导出模板 @ApiOperation("导出xxx模板") @ ...

  5. 15、Flutter 按钮组件

    按钮组件的属性 ButtonStylee里面的常用的参数 ElevatedButton ElevatedButton 即"凸起"按钮,它默认带有阴影和灰色背景.按下后,阴影会变大 ...

  6. 六步带你体验EDS交换数据全流程

    本期我们将走进XX医疗集团向某慢病院共享数据的场景,如何通过EDS完成数据交换,进而实现医疗数据的安全可控共享. 本文分享自华为云社区<[EDS从小白到专家]第1期-六步带你体验EDS交换数据全 ...

  7. 多模态AI开发套件HiLens Kit:超强算力彰显云上实力

    摘要:Huawei HiLens Kit是一款端云协同多模态AI开发套件,支持图像.视频.语音等多种数据分析与推理计算,可广泛用于智能监控.智能家庭.机器人.无人机.智慧工业.智慧门店等分析场景. 在 ...

  8. 火山引擎 DataLeap 构建Data Catalog系统的实践(二):技术与产品概览

    技术与产品概览 架构设计 元数据的接入 元数据接入支持T+1和近实时两种方式 上游系统:包括各类存储系统(比如Hive. Clickhouse等)和业务系统(比如数据开发平台.数据质量平台等) 中间层 ...

  9. 火山引擎 DataTester 揭秘:字节如何用 A/B 测试,解决增长问题的?

      更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 上线六年,字节跳动的短视频产品--抖音已成为许多人记录美好生活的平台.除了抖音,字节跳动旗下还同时运营着数十款 ...

  10. Solon Aop 特色开发(4)Bean 扫描的三种方式

    Solon,更小.更快.更自由!本系列专门介绍Solon Aop方面的特色: <Solon Aop 特色开发(1)注入或手动获取配置> <Solon Aop 特色开发(2)注入或手动 ...