前言
本篇为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) // 启动服务 }其中,http.HandleFunc()用于注册路由。所谓路由,就是给定一个URL地址,要找到处理该URL的函数,自然最理想的结构就是map,map的key保存URL地址,value保存处理URL的响应函数(handler),这样就能根据URL,快速定位函数并执行调用。http.HandleFunc()将这个路由map保存到了默认的服务端实例DefaultServeMux(ServeMux结构体对象)上,ServeMux的结构如下:
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 }http.HandleFunc()具体功能就是将用户定义的Handler函数保存到DefaultServeMux.m中,原理较为简单,在此不再赘述。
1.2 请求处理
路由注册完毕,并开启了服务端的服务,接下来就是处理请求,此处的请求包含两种:连接请求(由DefaultServeMux处理)和连接之后的正常服务请求(由建立连接后开启的新goroutine去处理)。http.ListenAndServe()的作用就是监听服务端端口,每当有新的连接时,开启一个goroutine去处理连接,大致流程如下:
func ListenAndServe() { ln := net.Listen() // 阻塞 for { conn := ln.Accept() // 建立TCP连接 go conn.serve// 开启新的goroutine去处理请求 } }每个新的goroutine用来处理连接后的正常服务请求,大致的流程就是根据request.URL.path,找到对应的Handler处理函数,并调用。大致流程如下:
func (c *conn) serve() { for { // conn.Server中有对DefaultServeMux的引用 conn.Server.ServeHTTP() // 调用DefaultServeMux的ServeHTTP } }// DefaultServeMux的ServeHTTP 的作用就是请求分发,找到其路由map中的处理函数Handler并调用
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) }ServeHTTP()的第一个参数ResonseWriter是一个输出流,负责将输出response结构体写入传输流;第二个参数是输入request结构体,对一个HTTP请求的核心参数进行了包装。Request的核心参数如下:
type Request struct { ... Method string // 请求方法,即post, get, delete等 URL *url.URL // 请求url Header Header // 请求头 Body io.ReadCloser // 请求体 Form url.Values // 经过解析的相关参数 PostForm url.Values ... }http.HandleFunc注册了一个ServeMux对象,ServeMux实现了ServeHTTP()方法,主要功能就是根据url找到处理请求的具体Handler,并调用对应的Handler函数。同时,注册在路由上的每个处理函数也都实现了ServeHTTP()方法,负责处理具体请求。 综上,net/http处理请求的完整流程如下:
图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() }整体流程和net/http类似,都是初始化服务端,注册路由,启动服务。那么gin的每一步具体是如何现实的呢?
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 }http.ListenAndServe有两个参数,第一个参数是服务端地址(ip:port),第二个参数为http.Handler实例。http.Handler是一个接口,包含一个ServeHTTP方法,因此只要gin.Engine实现了ServeHTTP,并作为ListenAndServe的第二个参数传入,就能替代http的默认server DefaultServeMux,然后在gin.Engine的ServeHTTP方法中按照httprouter的方式实现路由匹配,就能将请求通过url传递到对应的处理函数上。源码中engine.Handler()方法会返回一个gin定义的server类型,该server的ServeHTTP方法通过httprouter进行路由匹配,下发请求到处理函数。
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传递的参数 ... }HandlerFunc的作用就跟net/http的Handler一样,作为统一的请求处理函数,其函数签名如下:
type HandlerFunc func(*Context)当一个请求到来时,将请求封装成context结构体,然后将所有的中间件函数和请求处理函数都定义成HandlerFunc的格式,这样请求就可以在不同的中间件函数和请求处理函数中流转,同时将所有的函数(中间件和请求处理函数)按调用顺序组织成数组,就可以实现按序调用。这是一种常用的请求编排的模式(另一种是通过装饰器实现)。
标签:http,请求,request,流转,Handler,gin,处理函数,路由 From: https://www.cnblogs.com/yuanwebpage/p/16818047.html