在 HTTP 标准库 中解释了 go 的标准库是如何处理请求的,但是通过源码的分析,可以发现,标准库对于这部分的处理比较简单,例如对 url 中携带参数就不支持,面对这种情况,社区中出现了大量的框架,对原有的 http 进行补充。
大部分的 Go 的 HTTP 框架都是在重写路由部分,已实现更快的更准确的路由查找,减少路由解析过程中消耗的时间,来提高框架的处理速度。
先来回顾一下标准库的 HTTP 处理流程。
启动 HTTP 服务器:使用 http.ListenAndServe 或 http.ListenAndServeTLS 函数启动 HTTP 服务器。
处理请求:当服务器接收到 HTTP 请求时,它会使用与路径相对应的 http.Handler 实现处理请求。
调用处理程序:服务器会调用 ServeHTTP
方法,并将请求相关的信息作为参数传递给该方法。
路由匹配:在 ServeHTTP
方法中,通过比较请求的路径和已注册的路由,找到与请求匹配的路由。
调用处理函数:如果找到了匹配的路由,则调用与该路由相关的处理函数。
写入响应:处理函数通过 ResponseWriter
接口写入响应数据,以返回给客户端。
根据上述的处理方式,目前需要关注两个函数:ServeHTTP
和 ResponseWriter
。这是两个主要的处理方式。
http.ListenAndServe
和 http.ListenAndServeTLS
用于启动 HTTP 服务器;以及 http.ResponseWriter
和 http.Request
分别用于写入响应和处理请求。
同时通过源码分析,如果在 Listen 的时候不传入 handler, 那么就会采用 http 默认的 Handler,也就是 ServeMux
,所以只要我们按照标准实现一个 Handler,那么就会替换原有的处理逻辑,而且 Handler 的实现也是非常简单,只要实现 ServeHTTP
这个接口即可。
采用 gin 实现一个最简单的 ping/pong 服务。这是官方 README 提供的例子。
|
|
例子中实现也是非常简单,先通过 gin.Default()
生成一个 engine。 之后添加相关的处理方式,最后用 r.Run()
启动。下面将会从最开始的方法开始剖析 gin 框架。
Default
是 gin 使用的第一个函数。这个函数看起来很简单,一共就五行代码。
|
|
其中主要通过 New()
函数初始化 Engine
结构体。在 gin 中,通过 Engine
这个结构体进行管理,这个结构体其实是实现了 ServeHTTP
这个方法。
在这里 Engine
的功能和标准库中的 ServeMux
的地位其实是一模一样的,那么他的主要功能也就是用来保存注册的 handler,等到使用的时候进行查找调用。那么就先看看路由和 handler 是如何注册的。
在上面的例子中, 路由和 handler 是通过 r.GET()
方法进行注册。从源码来看,可以发现不仅仅是 GET
方法,其他的请求方法也一样,都是直接调用了 group.handler
方法。
|
|
从代码中可以看到这个方法,其实是比较简单,从代码上的命名来看,基本进行了以下操作:先进行绝对路径的计算,之后对 handler 进行合并,最后添加路由。
在combineHandlers
方法中,首先计算了当前handlers
的长度,并判断是否超过了最大长度(当前最大长度为63)。如果超过最大长度,则会引发panic异常。在这里,源代码采用了两次复制(copy)操作。第一次是将group.Handlers
中的数据复制到mergedHandlers
中,第二次是将handlers
的数据传入mergedHandlers
中。
|
|
这样 mergeHandler
中就存在两部分数据, handlers
可以通过源码看出来是在项目中注册的 handler,那么 group.Handlers
中又是什么呢?
其实就是在项目启动的时候注册的中间件。在 Engine 中的 Default
方法可以看到,engine.Use(Logger(), Recovery())
项目在初始化的时候注册了两个中间件,而这个 Use
方法,其实就是将中间件添加到上面的 group.Handlers
中,这里不多赘述,只是简单的说明一下,具体的中间件的流程会在 中间件(Middleware) 章节讲述。
确切地说,mergeHandlers
的目的是将中间件和用户定义的处理函数合并为一个处理函数切片,并按照一定的顺序注册到路由中。
路由添加方法也比较简单,核心代码一共就 6 行。
|
|
先通过请求方法获取树的根节点,如果根节点不存在就创建一个,最后添加相关的路由和 handlers。这里关于路由如何添加,路由数据结构在路由(Router)章节会进行讲解。
直到目前,路由的注册工作已经完成。
在之前详细的了解过 go 的 net/http 包中是如何启动一个 http 服务之后,其实现在回过头来看 gin,其实一切变得很简单。
在 gin 的 Run 方法中,主要通过标准库的 http.ListenAndServe
方法启动,而这个方法在 HTTP 标准库中有过详细的分析 ,剩下的方法流程和标准库中的流程基本一致,唯独不同的一点是将原有的默认 Handler 换成了 gin.Handler。
在之前说过,要想成为一个 Handler,只要实现 ServeHTTP
方法即可,而 gin 的 engine
就实现了这个方法。
根据之前对 http 了解的处理流程来看,在 gin 收到相关的请求,都会统一调用 ServeHTTP
方法,该方法会将接收到的参数等进行处理,例如寻找合适的处理器(Handler),最后返回统一的处理结果。
|
|
首先使用到的变量有 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) 进行处理的。