gin框架(2)- request的流转
前言
本篇为gin框架的第二篇,主要讲述gin框架如何接收一次http请求,并执行对应的请求处理函数,即request请求在gin框架中的流转过程。gin框架的底层,仍然是采用go原生网络库net/http,遵循one connection per goroutine。因此在讲述gin的请求处理之前,我们得先了解net/http是如何处理http请求的。
1. net/http如何处理请求
1.1 路由注册
在go netpoll源码解析中,我们大致讲解了一下go的网络处理模型是one goroutine per connection,即每一个客户端连接开启一个goroutine去处理。net/http是应用层协议,底层依赖tcp进行连接和传输,网络模型跟go netpoll是一样的,也是one goroutine per connection。利用net/http处理http请求的代码如下:
func main() { http.HandleFunc("/hello", func(rw http.ResponseWriter, r *http.Request) { io.WriteString(rw, "hello world") }) // 注册路由 http.ListenAndServe("localhost:8000", nil) // 启动服务 }
type ServeMux struct { mu sync.RWMutex // 读写锁,用于并发访问路由map时加锁保护 m map[string]muxEntry // 保存路由的map es []muxEntry // slice of entries sorted from longest to shortest. 基于路径保存的handler列表 hosts bool // whether any patterns contain hostnames } type muxEntry struct { h Handler // Handler是go的标准网络处理函数,任何实现了ServeHTTP(ResponseWriter, *Request)的函数都可以作为Handler pattern string }
1.2 请求处理
路由注册完毕,并开启了服务端的服务,接下来就是处理请求,此处的请求包含两种:连接请求(由DefaultServeMux处理)和连接之后的正常服务请求(由建立连接后开启的新goroutine去处理)。http.ListenAndServe()的作用就是监听服务端端口,每当有新的连接时,开启一个goroutine去处理连接,大致流程如下:
func ListenAndServe() { ln := net.Listen() // 阻塞 for { conn := ln.Accept() // 建立TCP连接 go conn.serve// 开启新的goroutine去处理请求 } }
func (c *conn) serve() { for { // conn.Server中有对DefaultServeMux的引用 conn.Server.ServeHTTP() // 调用DefaultServeMux的ServeHTTP } }
1.3 请求传递
完成了路由注册,启动了服务端,每当有新的连接到来,就会开启一个goroutine对其进行处理。HTTP有不同的请求类型(POST,GET,DELETE等),不同的参数(比如URL路径参数,GET请求参数,POST body携带的参数),那么不同的请求和参数是怎么从输入传递到服务端,处理完成后再统一输出呢?想要通过路由统一分发,所有的请求必须有一个统一的结构(request),将所有参数包装在这个request中,然后有统一的处理函数,接收这个request作为参数,然后将处理结果包装成一个response,返回给网络进行传输。对于net/http而言,这个统一的函数叫做Handler,Handler在nt/http中是一个接口,定义如下:
type Handler interface { ServeHTTP(ResponseWriter, *Request) }
type Request struct { ... Method string // 请求方法,即post, get, delete等 URL *url.URL // 请求url Header Header // 请求头 Body io.ReadCloser // 请求体 Form url.Values // 经过解析的相关参数 PostForm url.Values ... }
图1 net/http请求处理的流程
2. gin如何处理请求
net/http已经让编写一个http服务端非常简单。我们可以实现自己的Server,并通过http.ListenAndServe(addr, Handler)的第二个参数传入我们的Server,就能使用自定义Server。gin的请求处理与此类似。先看一下启动一个gin服务的代码:
func main() { r := gin.Default() r.GET("/hello", func(c *gin.Context) { c.JSON(200, gin.H{ "message": "hello world", }) }) r.Run() }
2.1 路由注册和匹配
gin的路由注册采用的是httprouter。httprouter是基于radix tree实现的前缀匹配。关于radix tree,可以参考笔者之前的博客 gin框架(1)- 路由原理。gin启动服务端时,首先初始化了一个server实例(在gin中为gin.Engine类型),然后调用了gin.Engine.Run()方法开启服务。Run方法内部调用的是http.ListenAndServe()。源码如下:
func (engine *Engine) Run(addr ...string) (err error) { defer func() { debugPrintError(err) }() if engine.isUnsafeTrustedProxies() { debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" + "Please check https://pkg.go.dev/github.com/gin-gonic/gin#readme-don-t-trust-all-proxies for details.") } address := resolveAddress(addr) debugPrint("Listening and serving HTTP on %s\n", address) err = http.ListenAndServe(address, engine.Handler()) // Run方法调用的是http.ListenAndServe, 传入gin实现的server return }
2.2 请求传递
从1.3的分析可以看出,要实现请求的传递和处理,需要有统一的request封装和统一的函数接收封装的request,对应到gin中,分别是context结构体和HandlerFunc函数。context结构体作为request的统一封装,包含了一次http请求的主要参数,其主要参数如下:
type Context struct { ... Request *http.Request // request请求 Writer ResponseWriter // response Params Params // param parameters handlers HandlersChain // 该请求对应的所有处理函数 index int8 // HandlersChain是一个HandlerFunc数组,通过index控制访问到了哪一个HandlerFunc fullPath string // 请求的完整路径 Keys map[string]any // 用于保存需要在整个请求中在不同的middleware传递的参数 ... }
type HandlerFunc func(*Context)