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) // 启动服务
}
其中,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的格式,这样请求就可以在不同的中间件函数和请求处理函数中流转,同时将所有的函数(中间件和请求处理函数)按调用顺序组织成数组,就可以实现按序调用。这是一种常用的请求编排的模式(另一种是通过装饰器实现)。
 

 

posted @ 2022-10-23 10:26  晨枫1  阅读(926)  评论(0编辑  收藏  举报