当前位置: 首页 > article >正文

【Gin】| 框架源码解析 :路由详解

本文目录

  • 一、Gin框架路由详解
    • Radix Tree
  • 二、源码解析

本文为学习七米老师Gin框架源码解析文章教程的学习笔记。

一、Gin框架路由详解

Gin框架使用的是定制版的httprouter,路由原理是大量使用公共前缀的树结构,它基本上是一个紧凑的Trie tree(或者只是Radix Tree)。具有公共前缀的节点也共享一个公共父节点。

Radix Tree

基数树(Radix Tree)又称为PAT位树(Patricia Trie or crit bit tree),是一种更节省空间的前缀树(Trie Tree)。对于基数树的每个节点,如果该节点是唯一的子树的话,就和父节点合并。下图为一个基数树示例:

在这里插入图片描述
比如访问博客的时候,路径有很多参数和通配符,使用哈希map就不太方便,做一个网站路由不可能无限制的多,也是需要考量的,空间不会特别大。虽然没有哈希map那么快,但是用前缀树也够用了。

比如下面注册路由信息。

r := gin.Default()
r.GET("/", func1)
r.GET("/search/", func2)
r.GET("/support/", func3)
r.GET("/blog/", func4)
r.GET("/blog/:post/", func5)
r.GET("/about-us/", func6)
r.GET("/about-us/team/", func7)
r.GET("/contact/", func8)

那么就会得到一个GET方法对应的路由树,如下所示。
在这里插入图片描述
最右边那一列每个*<数字>表示Handle处理函数的内存地址(一个指针)。从根节点遍历到叶子节点我们就能得到完整的路由表

blog/:post其中:post只是实际文章名称的占位符(参数)。与hash-maps不同,这种树结构还允许我们使用像:post参数这种动态部分,因为我们实际上是根据路由模式进行匹配,而不仅仅是比较哈希值。

路由器为每个请求方法管理一棵单独的树。一方面,它比在每个节点中都保存一个method-> handle map更加节省空间,它还使我们甚至可以在开始在前缀树中查找之前大大减少路由问题。

也就是对于search来说,可能有get、post等请求,相比于单独对search开一个handle map来处理,从请求方法的角度来进行统一划分会更节省了空间,甚至可在查找之前减少路由问题。

为了获得更好的可伸缩性,每个树级别上的子节点都按Priority(优先级)排序,其中优先级(最左列)就是在子节点(子节点、子子节点等等)中注册的句柄的数量。这样做有两个好处:

1、首先优先匹配被大多数路由路径包含的节点。这样可以让尽可能多的路由快速被定位。

2、类似于成本补偿。最长的路径可以被优先匹配,补偿体现在最长的路径需要花费更长的时间来定位,如果最长路径的节点能被优先匹配(即每次拿子节点都命中),那么路由匹配所花的时间不一定比短路径的路由长。下面展示了节点(每个-可以看做一个节点)匹配的路径:从左到右,从上到下。
在这里插入图片描述

二、源码解析

先写一个gin的简单demo。

package main

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func main() {
	r := gin.Default()
	r.GET("/", func(c *gin.Context) {
		c.String(http.StatusOK, "ok")
	})
	r.Run()
}

点击Run进去看源码,可以看到调用了http也就是go的net/http网络组件进行开发。

在这里插入图片描述
来看看ListenAndServer这个函数,需要两个参数。

在这里插入图片描述
Handler类型要求必须实现ServerHTTP方法的接口。

在这里插入图片描述
而在gin中,被传进去了engine作为handler,也就是第二个参数,说明其一定实现了ServerHTTP方法。

在这里插入图片描述
这个Engine引擎实现了三个接口,如下所示。

在这里插入图片描述
定位到engine实现 ServeHTTP方法的地方,来看看。

在这里插入图片描述

c := engine.pool.Get().(*Context)

engine.pool是一个对象池(通常是一个sync.Pool实例),用于复用Context对象。Get()方法从对象池中获取一个对象。(Context)是类型断言,确保从池中获取的对象是Context类型。

通过对象池减少每次申请、垃圾回收的资源消耗。

同时取出来之后再做的初始化,也是一个比较重要的点。

首先sync.Pool 是线程安全的,但对池的操作(如 Put 和 Get)仍然会涉及锁机制。如果在归还对象时进行初始化,可能会增加锁的持有时间,从而降低并发性能。

sync.Pool 的设计目标是提供一个通用的对象复用机制,而不是管理对象的状态。也就是对象的状态让使用者来关心,归还对象时只涉及简单的 Put 操作,避免了在归还时进行复杂的初始化逻辑。

接着看handleHTTPRequest函数的源码。

在这里插入图片描述

首先是 请求路径处理与路由树匹配逻辑,在 handleHTTPRequest 函数的前半部分,代码主要负责处理 HTTP 请求的路径信息,并尝试在路由树中找到匹配的路由。首先,根据配置选择使用 Request.URL.RawPath 或 Request.URL.Path 作为路由匹配的基础路径,并决定是否对路径进行解码。如果启用了 RemoveExtraSlash,还会对路径进行清理,去除多余的斜杠。随后,代码遍历 engine.trees,这是一个按 HTTP 方法分类的路由树集合,寻找与当前请求方法匹配的路由树根节点。一旦找到对应的根节点,便调用 root.getValue 方法,尝试在路由树中匹配请求路径,并提取路径参数和对应的处理器。如果找到匹配的处理器,会将相关参数和处理器赋值给 Context,并继续后续处理;如果没有找到匹配的处理器,但满足特定条件(如路径尾部斜杠或路径拼写错误),则会尝试执行重定向操作。

进一步点进来看tengines的结构体,里边会找到一个IRouter。

在这里插入图片描述

var _ IRouter = (*Engine)(nil)

(*Engine)(nil):将 nil 转换为 *Engine 类型。这并不会分配内存,只是创建了一个 *Engine 类型的空值。var _ IRouter = (*Engine)(nil):将 (*Engine)(nil) 赋值给一个匿名变量 _,并声明它是一个 IRouter 类型。

如果 *Engine 没有实现 IRouter 接口,编译器会报错,提示 *Engine 不满足 IRouter 接口的定义。

在这里插入图片描述

在编写完一个结构体之后,通常会写一个匿名的变量,来等于一个空的结构体指针,是为了确保我们的结构体实现了指定的接口。

var _ IRouter = (*Engine)(nil) 是一种常见的类型断言写法,用于确保某个类型实现了某个接口。这种写法的作用是在编译时验证 *Engine 是否满足 IRouter 接口的定义。

如果 Engine 类型实现了 Handle、Group 以及其他所有方法,那么 *Engine 就实现了 IRouter 接口。

在这里插入图片描述
上面这种写法会在很多第三方库中学到。

接着继续看看trees的源码。

在这里插入图片描述
没有用map映射的方式去存请求方法和映射,好处就是节省内存。因为请求方法http1.1中一共就9个。只需要定义一个请求的切片即可。比map的内存会节省。

engine的new方法中,可以看到trees是一次性申请了9的容量。(可以一次性把切片容量申请到位,这样后面不需要动态扩容slice的容量。)

在这里插入图片描述
接着来看看node的详细字段。

在这里插入图片描述

type node struct {
	path      string         // 当前节点的路径片段,例如 "/user" 或 ":id"
	indices   string         
	// 路径片段的索引信息,用于快速匹配,和children字段对应, 保存的是分裂的分支的第一个字符, 例如search和support, 那么s节点的indices对应的"eu"
	wildChild bool           // 是否有子节点是通配符(如 `:param` 或 `*catchall`),也就是节点是否是参数节点
	nType     nodeType       // 节点类型(如静态节点、参数节点、通配符节点等)
	priority  uint32         // 节点的优先级,用于排序和匹配时的优先级判断
	children  []*node        // 子节点列表,最多包含一个 `:param` 风格的节点在数组末尾
	handlers  HandlersChain  // 与该节点关联的处理器链
	fullPath  string         // 完整的路径,从根节点到当前节点的路径字符串
}

接下来我们来看看怎么添加路由的。GET方法是属于RouterGroup的类型的,但是能被Eegine的r调用,说明肯定是在Engine中进行了嵌套调用。

在这里插入图片描述

来看看,确实看到对应的Engine中包含。
在这里插入图片描述

GET方法的两个参数,分别是string还有HandlerFunc类型的切片。

在这里插入图片描述

接着来看,group.handle()做了什么事情。
在这里插入图片描述

combineHandlers就是把我们注册的handlerfunc给拼接了一下。group本身的处理函数切片还有我们传过来的handlers拼接,组成一个处理函数的完整切片(调用链)。

combineHandlers 函数的作用是将 RouterGroup 的现有处理器链(group.Handlers)与传入的处理器链(handlers)合并。它首先计算合并后的处理器链的总长度,确保合并后的处理器数量不会超出限制(通过 assert1 断言)。接着,函数创建一个新的处理器链(mergedHandlers),并使用 copy 函数将 group.Handlers 和 handlers 的内容依次复制到新链中。最终返回合并后的处理器链。

在这里插入图片描述
接着我们回过头来看看addRoute函数的作用。

在这里插入图片描述

在这里插入图片描述
接着往下看可以发现 node 的addRoute函数,也就是构造路由树的方法。

核心的就是insertChild方法。

然后就是先判断是否是空树,如果不是,就继续walk下去。
在这里插入图片描述


http://www.kler.cn/a/556297.html

相关文章:

  • QT SQL框架及QSqlDatabase类
  • MATLAB | 设置滑动窗口计算栅格数据的CV变异系数
  • 装win10系统提示“windows无法安装到这个磁盘,选中的磁盘采用GPT分区形式”解决方法
  • 计算机视觉算法实战——图像风格迁移(主页有源码)
  • solidity之Foundry安装配置(一)
  • 利用Postman和Apipost进行WebSocket调试和文档设计
  • 国产开源PDF解析工具MinerU
  • 网络运维学习笔记 013网工初级(HCIA-Datacom与CCNA-EI)DHCP动态主机配置协议(此处只讲华为)
  • 【C语言】结构体内存对齐问题
  • 前端ES面试题及参考答案
  • Node.js 的 http 模块
  • Vue 3 和 Vite 从零开始搭建项目的详细步骤
  • 在 Flutter 中实现文件读写
  • LeetCode刷题---二分查找---441
  • Web Scraper,强大的浏览器爬虫插件!
  • 软件架构设计:架构风格
  • Python Cookbook-2.4 从文件中读取指定的行
  • 朴素贝叶斯法
  • AB-02 AUTOSAR builder创建工程
  • c#编程:学习Linq,重几个简单示例开始