探秘Gin框架底层技术:高效处理HTTP请求的奥秘

Gin 是 Go 中一款优秀的 Web 框架,相对于 Go 本身提供的 net/http 标准库,Gin框架具有以下优点:

  • 更快速的开发:Gin 提供了许多实用的中间件和辅助函数,可以加速 Web 应用程序的开发过程。
  • 更强大的路由功能:Gin 支持更灵活的路由定义,包括参数化路由、路由组,使用压缩前缀树进行存储,使得管理复杂的路由更加容易。
  • 更丰富的中间件支持:Gin 内置了许多常用的中间件,如日志记录、错误恢复等,可以方便地扩展应用程序的功能。

快速入门

go 复制代码
func main() {
 r := gin.Default()
 r.GET("/hello", func(context *gin.Context) {
  context.JSON(200, gin.H{
   "message":"hello world",
  })
 })
 r.Run(":8090")
}

首先创建一个Gin 对象 r , 实际它本质是 Http Handler。

注册一个 HTTP 方法为 Get, path 为 /hello 的处理函数。

运行 Http 服务,端口号为 8090。

当在浏览器输入 http://127.0.0.1:8090/hello后, 会返回一个字符串。

代码分析

我们根据上述示例中的代码进行分析。

首先创建一个对象 Engine,它是 Gin 中的一个核心数据结构。

默认的 Engine 对象使用了 Logger 和 Recovery 中间件。

Logger 中间件主要是定义如何打印日志。

Recovery 中间件能够捕获 HTTP 请求处理过程中产生的所有 panic, 并返回 500 错误。

scss 复制代码
func Default() *Engine {
 engine := New()
 engine.Use(Logger(), Recovery())
 return engine
}

然后进入 New() 方法生成一个 Engine 对象。

Engine 对象中有三个比较重要的参数:RouterGrouppooltrees。刚开始学习时可以只关注这三个参数,然后随着学习的深入再探究其他参数的作用。

创建好 Engine 对象后,我们就可以为不同 HTTP 方法的不同 path 请求创建对应的 handler,当服务器接收到该请求后,我们就可以找到对应的 handler 处理请求。

以上面列举的 Get /hello 请求样例进行分析

方法入口为:

vbnet 复制代码
func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) IRoutes {
 return group.handle(http.MethodGet, relativePath, handlers)
}

其中:relativePath 值为 /hello, handlers 为:

javascript 复制代码
func(context *gin.Context) {
  context.JSON(200, gin.H{
   "message":"hello world",
  }

group.handle 方法中,主要执行了三个逻辑:

//1 中,进行 path 拼接,将 RouterGroup 中的 basePath 和方法传入的 relativePath 进行进行组装,获取一个完整的 path。

从中我们可以发现,各种 handlers 都是在对应 RouterGroup 基础上进行实现的。

可以将共享相同特性的路由抽象到一个路由组 RouterGroup 中,提高代码重用性和维护性。

同时,可以轻松地为特定的路由组添加中间件,以便在请求到达路由处理函数之前或之后执行特定操作,从而简化了中间件的管理和使用。

csharp 复制代码
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
 absolutePath := group.calculateAbsolutePath(relativePath)  //1
 handlers = group.combineHandlers(handlers) //2
 group.engine.addRoute(httpMethod, absolutePath, handlers) //3
 return group.returnObj()
}

//2 中,深拷贝 RouterGroup 中的 handlers 和注册传入的 handlers,组合成新的 handlers 数组。

//3 中,获取该 RouterGroup 所属的 Engine 对象,根据 HTTP方法类型,从方法路由树树组 trees 中找到对应的路由树,然后将该 path 的 handlers 注册到此路由树中。

下面就来到了重头戏部分,介绍是如何注册这些 handlers 到路由树的。

注册路由树

Gin 选择以压缩前缀树为基础实现路由树。

压缩前缀树又是以前缀树为基础。

前缀树有以下特点:

  • 根节点不包含字符,其他节点都包含一个字符。
  • 从根节点到某一个节点A,将对应路径的每个节点上的字符进行连接,即为该节点A对应的字符串。
  • 尽可能复用公共前缀,非必要不会分配新的节点。

压缩前缀树是前缀树的优化版本。当某个节点只有一个子节点时,就会向上移动,与父节点合并。

Gin 为什么要使用压缩前缀树来实现路由树呢?

  • 高效的路由匹配: 压缩前缀树可以实现高效的最长前缀匹配。当请求到达时,Gin 可以快速地从所有已注册的路由中找到最匹配的路由,以提高性能和响应速度。
  • 动态路由支持:压缩前缀树天然支持动态路由,这使得 Gin 能够轻松处理带有参数的路由,而不需要复杂的额外逻辑。
  • 灵活性和扩展性:通过使用压缩前缀树,Gin 能够更加灵活地管理路由规则。这种数据结构使得在添加、删除或者修改路由时更加高效,并且可以轻松地支持各种路由匹配规则。

介绍完一些前缀树知识点后,开始分析 Gin 是如何实现路由树的。

实现入口即为上面介绍的步骤3。

调用 group.engine.addRoute(httpMethod, absolutePath, handlers)n 方法将 path 注册到对应的路由树上。

假设存在路由: 1)/users,2) /user/:id,3)/user/detail, 4) /undo, 对应的路由树如下:

从9个方法路由树中根据根据方法名(比如:GET, POST)找到对应的路由树。

如果为空时,创建新的一个路由树并添加到切片中。

调用 addRoute 方法添加 path。

ini 复制代码
func (engine *Engine) addRoute(method, path string, handlers HandlersChain) {
 ......
 root := engine.trees.get(method)
 if root == nil {
  root = new(node)
  root.fullPath = "/"
  engine.trees = append(engine.trees, methodTree{method: method, root: root})
 }
 root.addRoute(path, handlers)
......

addRoute 方法较为复杂,存在多种情况,我们以上述的路由规则为例进行分析。

1) 当路由树为空路由树,首次添加 path 时, 假设 path 为 /users:

2)之前已经有路由树了,且存在公共前缀,公共部分比当前节点的 path 长度小。例如:存在节点 root, 该节点的 path 为 /users, 新添加 path 为 /undo, 它们的公共前缀为 /u, 添加过程如下:

3)另一种常用的就是动态路由,有通配符:,比如路由规则为 /users/:id,其中 :id 为参数部分,可以匹配任意字符串。这种类型的路由通常用于捕获 URL 中的变量,并将其作为输入参数传递给相应的处理函数。假设已有路由规则:/users, 要添加路由规则:/users/:id

实际还有其他情况,比如路由规则类似这种的,/user/*info,在这里就不赘述了,只介绍下常见的情况。

在添加路由时,Gin 做了一些优化,会根据子节点列表中每个节点的 priority 值进行排序,将 priority 值高的节点排序到排到前面,方便查找时提前加速查到对应的路由节点。

至此介绍完了注册路由树的大体过程,当有请求过来时,是怎么查找到对用的路由节点呢?

检索路由树

要能处理请求,首先需要启动服务。调用 Engine.Run() 方法进行启动。

scss 复制代码
func (engine *Engine) Run(addr ...string) (err error) {
 ........
 address := resolveAddress(addr)
 err = http.ListenAndServe(address, engine.Handler())
 return
}

通过代码我们可以发现,Gin 最终会调用 http 下的 ListenAndServe 方法进行处理,也进一步证明了 Gin 是在 http 包基础上的进一步开发。

Gin 主要是将 Engine 作为一个 Handler 注册到 http 的 Serve 中。

http 为什么会识别出 Engine 是它的一个 Handler 呢?

这是因为 Handler 是一个 interface, 它定义了一个方法 ServeHTTP

go 复制代码
type Handler interface {
 ServeHTTP(ResponseWriter, *Request)
}

Engine 实现了这个方法,所以就可以被看作 http 中的一个 Handler。

scss 复制代码
// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 // 从对象池中获取一个 Context
 c := engine.pool.Get().(*Context)
 // 重置 Context
 c.writermem.reset(w)
 c.Request = req
 c.reset()
 // 处理 http 请求
 engine.handleHTTPRequest(c)
 // 处理完后,再把 Context 方法放回到对象池中
 engine.pool.Put(c)
}

Engine 作为 http 的一个 Handler,每当 http 请求经过时,就会调用 ServeHTTP 方法进行处理。

  • 每次请求时,会先从对象池中获取一个 Context
  • 重置 Context
  • 调用 handleHTTPRequest 处理 http 请求
  • 处理完后把 Context 放回到对象池中

从中可以发现核心的处理逻辑为 handleHTTPRequest 方法。

检索路由节点的逻辑主要是在 root.getValue 中。

参考博客: mp.weixin.qq.com/s?__biz=Mzk...

相关推荐
一丝晨光9 小时前
Java、PHP、ASP、JSP、Kotlin、.NET、Go
java·kotlin·go·php·.net·jsp·asp
zaim117 小时前
计算机的错误计算(一百一十四)
java·c++·python·rust·go·c·多项式
百里守约学编程1 天前
70. 爬楼梯
算法·leetcode·go
a_ran2 天前
一些 Go Web 开发笔记
后端·golang·go·编程·web·网站
影灵衣丶2 天前
go进阶编程:设计模式之适配器模式
后端·go
StevenZeng学堂2 天前
【云原生安全篇】Cosign助力Harbor验证镜像实践
网络·安全·云原生·容器·kubernetes·云计算·go
qq_172805593 天前
GO Message Bus
开发语言·后端·golang·go
一丝晨光4 天前
void类型
java·开发语言·javascript·c++·c#·go·c
IT杨秀才4 天前
自己动手写了一个协程池
后端·程序员·go
狼爷4 天前
解开 Golang‘for range’的神秘面纱:易错点剖析与解读
go