目录

Gin源码分析一:引擎 Engine

警告
本文最后更新于 2024-03-20,文中内容可能已过时。

HTTP 标准库 中解释了 go 的标准库是如何处理请求的,但是通过源码的分析,可以发现,标准库对于这部分的处理比较简单,例如对 url 中携带参数就不支持,面对这种情况,社区中出现了大量的框架,对原有的 http 进行补充。

大部分的 Go 的 HTTP 框架都是在重写路由部分,已实现更快的更准确的路由查找,减少路由解析过程中消耗的时间,来提高框架的处理速度。

先来回顾一下标准库的 HTTP 处理流程。

  1. 启动 HTTP 服务器:使用 http.ListenAndServe 或 http.ListenAndServeTLS 函数启动 HTTP 服务器。

  2. 处理请求:当服务器接收到 HTTP 请求时,它会使用与路径相对应的 http.Handler 实现处理请求。

  3. 调用处理程序:服务器会调用 ServeHTTP 方法,并将请求相关的信息作为参数传递给该方法。

  4. 路由匹配:在 ServeHTTP 方法中,通过比较请求的路径和已注册的路由,找到与请求匹配的路由。

  5. 调用处理函数:如果找到了匹配的路由,则调用与该路由相关的处理函数。

  6. 写入响应:处理函数通过 ResponseWriter 接口写入响应数据,以返回给客户端。

根据上述的处理方式,目前需要关注两个函数:ServeHTTPResponseWriter 。这是两个主要的处理方式。

http.ListenAndServehttp.ListenAndServeTLS 用于启动 HTTP 服务器;以及 http.ResponseWriterhttp.Request 分别用于写入响应和处理请求。

同时通过源码分析,如果在 Listen 的时候不传入 handler, 那么就会采用 http 默认的 Handler,也就是 ServeMux ,所以只要我们按照标准实现一个 Handler,那么就会替换原有的处理逻辑,而且 Handler 的实现也是非常简单,只要实现 ServeHTTP 这个接口即可。

采用 gin 实现一个最简单的 ping/pong 服务。这是官方 README 提供的例子。

Go

package main

import (
  "net/http"

  "github.com/gin-gonic/gin"
)

func main() {
  r := gin.Default()
  r.GET("/ping", func(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
      "message": "pong",
    })
  })
  r.Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}

例子中实现也是非常简单,先通过 gin.Default() 生成一个 engine。 之后添加相关的处理方式,最后用 r.Run() 启动。下面将会从最开始的方法开始剖析 gin 框架。

Default 是 gin 使用的第一个函数。这个函数看起来很简单,一共就五行代码。

Go

func Default() *Engine {
	debugPrintWARNINGDefault()
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}

其中主要通过 New() 函数初始化 Engine 结构体。在 gin 中,通过 Engine 这个结构体进行管理,这个结构体其实是实现了 ServeHTTP 这个方法。

在这里 Engine 的功能和标准库中的 ServeMux 的地位其实是一模一样的,那么他的主要功能也就是用来保存注册的 handler,等到使用的时候进行查找调用。那么就先看看路由和 handler 是如何注册的。

在上面的例子中, 路由和 handler 是通过 r.GET() 方法进行注册。从源码来看,可以发现不仅仅是 GET 方法,其他的请求方法也一样,都是直接调用了 group.handler 方法。

Go

func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
	// 计算绝对路径
	absolutePath := group.calculateAbsolutePath(relativePath)
	// 合并 handler
	handlers = group.combineHandlers(handlers)
	// 添加相关路由
	group.engine.addRoute(httpMethod, absolutePath, handlers)
	return group.returnObj()
}

从代码中可以看到这个方法,其实是比较简单,从代码上的命名来看,基本进行了以下操作:先进行绝对路径的计算,之后对 handler 进行合并,最后添加路由。

combineHandlers方法中,首先计算了当前handlers的长度,并判断是否超过了最大长度(当前最大长度为63)。如果超过最大长度,则会引发panic异常。在这里,源代码采用了两次复制(copy)操作。第一次是将group.Handlers中的数据复制到mergedHandlers中,第二次是将handlers的数据传入mergedHandlers中。

Go

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
	finalSize := len(group.Handlers) + len(handlers)
	// ...
	mergedHandlers := make(HandlersChain, finalSize)
	copy(mergedHandlers, group.Handlers)
	copy(mergedHandlers[len(group.Handlers):], handlers)
	// ...
}

这样 mergeHandler 中就存在两部分数据, handlers 可以通过源码看出来是在项目中注册的 handler,那么 group.Handlers 中又是什么呢?

其实就是在项目启动的时候注册的中间件。在 Engine 中的 Default 方法可以看到,engine.Use(Logger(), Recovery()) 项目在初始化的时候注册了两个中间件,而这个 Use 方法,其实就是将中间件添加到上面的 group.Handlers 中,这里不多赘述,只是简单的说明一下,具体的中间件的流程会在 中间件(Middleware) 章节讲述。

确切地说,mergeHandlers的目的是将中间件和用户定义的处理函数合并为一个处理函数切片,并按照一定的顺序注册到路由中。

路由添加方法也比较简单,核心代码一共就 6 行。

Go

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)

先通过请求方法获取树的根节点,如果根节点不存在就创建一个,最后添加相关的路由和 handlers。这里关于路由如何添加,路由数据结构在路由(Router)章节会进行讲解。

直到目前,路由的注册工作已经完成。

在之前详细的了解过 go 的 net/http 包中是如何启动一个 http 服务之后,其实现在回过头来看 gin,其实一切变得很简单。

在 gin 的 Run 方法中,主要通过标准库的 http.ListenAndServe 方法启动,而这个方法在 HTTP 标准库中有过详细的分析 ,剩下的方法流程和标准库中的流程基本一致,唯独不同的一点是将原有的默认 Handler 换成了 gin.Handler。

在之前说过,要想成为一个 Handler,只要实现 ServeHTTP 方法即可,而 gin 的 engine 就实现了这个方法。

根据之前对 http 了解的处理流程来看,在 gin 收到相关的请求,都会统一调用 ServeHTTP 方法,该方法会将接收到的参数等进行处理,例如寻找合适的处理器(Handler),最后返回统一的处理结果。

Go

// ServeHTTP conforms to the http.Handler interface.
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := engine.pool.Get().(*Context)
	c.writermem.reset(w)
	c.Request = req
	c.reset()
	engine.handleHTTPRequest(c)
	engine.pool.Put(c)
}

首先使用到的变量有 pool。这里的 pool 使用的 sync.Pool 这个类型,主要是用来重复使用的 Context。这里直接从 pool 中取出 Context,并对 Context 的一些参数进行设置,最后调用 engine.handleHTTPRequest 方法。

这也是目前常常使用

engine.handleHTTPRequest 这个方法主要处理用户的 HTTP 请求,确定该请求的处理方法。简单的来说就:首先,获取请求的 HTTP 方法(如 GET 或 POST)和 URL 路径,并在需要时解码路径。然后,它搜索匹配该请求的处理树。如果找到了一个匹配的节点,它会将处理程序分配给请求的上下文(c),并写入 HTTP 响应头。如果未找到匹配的节点,则会通过 serverError 写入 “405 Method Not Allowed” 或 “404 Not Found” 的错误响应。

这样基本就是一个简单的 http 请求的处理过程。在代码中可以看到很多事情其实是由上下文 (Context) 进行处理的。

相关内容