探秘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...

相关推荐
用户9230777112248 小时前
Gin教程 Golang+Gin框架入门实战教程 大地老师
gin
梁梁梁梁较瘦1 天前
边界检查消除(BCE,Bound Check Elimination)
go
梁梁梁梁较瘦1 天前
指针
go
梁梁梁梁较瘦1 天前
内存申请
go
半枫荷1 天前
七、Go语法基础(数组和切片)
go
梁梁梁梁较瘦2 天前
Go工具链
go
半枫荷2 天前
六、Go语法基础(条件控制和循环控制)
go
半枫荷3 天前
五、Go语法基础(输入和输出)
go
小王在努力看博客3 天前
CMS配合闲时同步队列,这……
go
驰羽4 天前
[GO]gin框架:ShouldBindJSON与其他常见绑定方法
开发语言·golang·gin