Mygin实现动态路由
本篇是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实现动态路由的更多相关文章
- AIX 环境下动态路由
IBM AIX v5.3操作系统环境下动态路由配置如下: 1,用命令lssrc -S routed和lssrc -S gated分别检查routed和gated子系统是是活动状态.如果这两个子系统为活 ...
- asp.net MVC动态路由
项目中遇到需要动态生成控制器和视图的. 于是就折腾半天,动态生成控制器文件和视图文件,但是动态生成控制器不编译是没法访问的. 找人研究后,得到要领: 1.放在App_Code文件夹内 2.不要命名空间 ...
- RIP、OSPF、BGP、动态路由选路协议、自治域AS
相关学习资料 tcp-ip详解卷1:协议.pdf http://www.rfc-editor.org/rfc/rfc1058.txt http://www.rfc-editor.org/rfc/rfc ...
- Ngnix技术研究系列2-基于Redis实现动态路由
上篇博文我们写了个引子: Ngnix技术研究系列1-通过应用场景看Nginx的反向代理 发现了新大陆,OpenResty OpenResty 是一个基于 Nginx 与 Lua 的高性能 Web 平台 ...
- 基于hi-nginx的web开发(python篇)——动态路由和请求方法
hi.py的提供的路由装饰器接受两个参数,第一个参数指定动态路由的正则模式,第二个参数指定同意的http请求方法列表. 比如: @app.route(r"^/client/?$", ...
- vue+iview实现动态路由和权限验证
github上关于vue动态添加路由的例子很多,本项目参考了部分项目后,在iview框架基础上完成了动态路由的动态添加和菜单刷新.为了帮助其他需要的朋友,现分享出实现逻辑,欢迎一起交流学习. Gith ...
- Cisco动态路由配置
前言: 学完静态路由配置,该学动态路由.所以 学习完后来做终结. 准备: PC:192.168.1.10 R1:fa0/0 192.168.1.1 fa0/1 1.1.12.1 R2: fa0/0 1 ...
- Miox带你走进动态路由的世界——51信用卡前端团队
写在前面: 有的时候再做大型项目的时候,确实会被复杂的路由逻辑所烦恼,会经常遇到权限问题,路由跳转回退逻辑问题.这几天在网上看到了51信用卡团队开源了一个Miox,可以有效的解决这些痛点,于是乎我就做 ...
- 从壹开始 [vueAdmin后台] 之三 || 动态路由配置 & 项目快速开发
回顾 今天VS 2019正式发布,实验一波,你安装了么?Blog.Core 预计今天会升级到 Core 3.0 版本. 哈喽大家周三好!本来今天呢要写 Id4 了,但是写到了一半,突然有人问到了关于 ...
- 从壹开始前后端分离 [ vue + .netcore 补充教程 ] 三十║ Nuxt实战:动态路由+同构
上期回顾 说接上文<二九║ Nuxt实战:异步实现数据双端渲染>,昨天咱们通过项目二的首页数据处理,简单了解到了 nuxt 异步数据获取的作用,以及亲身体验了几个重要文件夹的意义,整篇文章 ...
随机推荐
- vue 2实战系列 —— 复习Vue
复习Vue 近期需要接手 vue 2的项目,许久未写,语法有些陌生.本篇将较全面复习 vue 2. Tip: 项目是基于 ant-design-vue-pro ant-design-vue-pro 由 ...
- Python——第五章:shutil模块
复制文件 把dir1的文件a.txt 移动到dir2内 import shutil shutil.move("dir1/a.txt", "dir2") 复制两个 ...
- Python——第二章:字典的循环、嵌套、"解构"(解包)
字典进阶操作 -- 循环和嵌套 字典的循环 我们先看直接打印字典的样子,会分别对每对key:value进行打印,并使用,分隔他们 dic = { "赵四": "特别能歪嘴 ...
- Spring Boot 导出EXCEL模板以及导入EXCEL数据(阿里Easy Excel实战)
Spring Boot 导出EXCEL模板以及导入EXCEL数据(阿里Easy Excel实战) 导入pom依赖 编写导出模板 @ApiOperation("导出xxx模板") @ ...
- 15、Flutter 按钮组件
按钮组件的属性 ButtonStylee里面的常用的参数 ElevatedButton ElevatedButton 即"凸起"按钮,它默认带有阴影和灰色背景.按下后,阴影会变大 ...
- 六步带你体验EDS交换数据全流程
本期我们将走进XX医疗集团向某慢病院共享数据的场景,如何通过EDS完成数据交换,进而实现医疗数据的安全可控共享. 本文分享自华为云社区<[EDS从小白到专家]第1期-六步带你体验EDS交换数据全 ...
- 多模态AI开发套件HiLens Kit:超强算力彰显云上实力
摘要:Huawei HiLens Kit是一款端云协同多模态AI开发套件,支持图像.视频.语音等多种数据分析与推理计算,可广泛用于智能监控.智能家庭.机器人.无人机.智慧工业.智慧门店等分析场景. 在 ...
- 火山引擎 DataLeap 构建Data Catalog系统的实践(二):技术与产品概览
技术与产品概览 架构设计 元数据的接入 元数据接入支持T+1和近实时两种方式 上游系统:包括各类存储系统(比如Hive. Clickhouse等)和业务系统(比如数据开发平台.数据质量平台等) 中间层 ...
- 火山引擎 DataTester 揭秘:字节如何用 A/B 测试,解决增长问题的?
更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 上线六年,字节跳动的短视频产品--抖音已成为许多人记录美好生活的平台.除了抖音,字节跳动旗下还同时运营着数十款 ...
- Solon Aop 特色开发(4)Bean 扫描的三种方式
Solon,更小.更快.更自由!本系列专门介绍Solon Aop方面的特色: <Solon Aop 特色开发(1)注入或手动获取配置> <Solon Aop 特色开发(2)注入或手动 ...