聊一聊Go网络框架Gin系列之(一)--中间件如何定制通用拦截器

·  阅读 1144
聊一聊Go网络框架Gin系列之(一)--中间件如何定制通用拦截器

前言

网关中间件是事件流处理执行通道,它是请求流转控件,又叫middleware,常常用于作为请求头/请求体校验、缓存、限流等核心组件的关卡"安检"。

模块梳理

抛砖引玉,我们先来比对Go标准库server与Gin框架启动流程,再从gin开始切入。

Go标准库Server

我们知道,Go标准库启动HTTP监听函数需要经过下面几个步骤:

  • 流程概述

注册Handler → ListenAndServer → for轮询Accept → 执行处理 → 调用内部实现体的ServerHTTP()函数

func main() {
    hf := func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello there, from %q", html.EscapeString(r.URL.Path))
    }

    http.HandleFunc("/bar", hf)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

下面我们进入源码,看下其中做了什么:
首先,HandleFunc()底层封装了一个叫做ServeMux的结构体,内部有一个核心是muxEntry作为handler与url请求路径的映射。

type ServeMux struct {
    mu    sync.RWMutex
    m     map[string]muxEntry // Handler注册字典用于返回对应的处理函数
    ...
}
// handler与定位url的映射
type muxEntry struct {
    h       Handler
    pattern string
}

当程序启动,上面的main函数相对于把hf和"/bar"这个路径绑在一起。

接着,就是多路复用的简单实现,我们知道在Go里创建协程的成本其实不高,而且通常一个请求周期不会持续太久。
源码中可以看到,Go每次接受一个HTTP连接本质上就是new一个Go协程进行处理,整个流程图如下: HTTP request flow

Gin做了什么封装

OK,知道了主要流程之后,其实Go里面所有的HTTP server实现结构,本质上就是

  1. 利用url定位到已注册的逻辑函数
  2. 取出handler()函数进行调用。

Gin框架其实也是这个主要过程,优化点在于在Gin里面urlhandler fun的绑定关系是在一颗前缀树上面,完全参照了HttpRouter
有兴趣可以参考我在掘金之前的一篇博客梳理: 聊一聊Gin Web框架之前,看一眼httprouter


中间件入门----gin常见案例

后续我们先来实现几个比较简单的中间件模板:

Header拦截器

我们设计一个Header必须要有我们期望值的请求入口,如果丢失所需Header则不进行后续调用,中断http请求。

用法模板:

func init() {
    // 设置gin启动默认端口
    if err := os.Setenv("PORT", "8099"); err != nil {
        panic(err)
    }
}

var (
    H_KEY    = "h-key"  //业务header key
)

// 声明一个pingpong的简单handler,作为请求接受行为
func helloFunc(c *gin.Context) {
    const TAG = "PingPong"
    c.JSON(comdel.Success, comdel.SimpleResponse(200, TAG))
    return
}

func main() {
    // 创建gin实例
    r := gin.Default()
    // 校验header
    r.POST("/hello-with-header", middle.HeaderCheck(H_KEY), helloFunc)
    e := r.Run()
    fmt.Printf("Server stop with err: %v\n", e)
}

上面注册了/hello-with-headermiddle.HeaderCheck(H_KEY)的路由关系,相当于把函数HeaderCheck作为helloFunc()的上游拦截器。

接着看下中间件的具体代码:

func HeaderCheck(key string) func(c *gin.Context) {
    return func(c *gin.Context) {
        // 获取header的值
        kh := c.GetHeader(key)
        if kh == "" {
            // header缺失
            c.JSON(http.StatusOK, &comdel.Response{Code: comdel.Unknown, Msg: "lacking necessary header"})
            // 请求不合法,终止当前流程
            c.Abort()
            return
        }
        // 请求正常,执行链往下调用
        c.Next()
    }
}

演示结果: 可以看到Header缺少我们预期要的key,所以被中间件拦截了。 header拦截

所以理论上你可以在中间件做任何你想要校验的逻辑,我们再来整理一个body校验中间件,功能是:
接受任意目标类型的结构体,在HTTP请求进来时候对其与目标类型进行校验,利用gin框架的ShouldBindBodyWith()来定位当前结构体是否合法。

结构体校验(反射、运行时检测)

前置知识:我们都知道目前作为静态语言,Go的泛型还不是很成熟,所以程序在运行时如果要表示传入任意类型,需要利用反射特性,对运行时未知参数进行类型判断

  1. interface{}来进行兼容入参类型。
  2. 如果传入类型非nil,则对通用interface{}进行参数类型还原
  3. 还原为原结构传入gin内置校验函数ShouldBindBodyWith()

现在编写一个运行时body结构体检测,传入预期目标类型的结构,并对字段进行校验

// 构造返回错误格式相应
func NewBindFailedResponse(tag string) *Response {
    return &Response{Code: WrongArgs, Msg: "wrong argument", Tag: tag}
}

// reqVal表示具体struct类型
func ReqCheck(reqVal interface{}) func(ctx *gin.Context) {
    var reqType reflect.Type = nil
    if reqVal != nil {
        // 从interface{}还原,提取原值类型
        value := reflect.Indirect(reflect.ValueOf(reqVal))
        // 运行时: 拿到校验体原始类型
        reqType = value.Type()
    }

    return func(c *gin.Context) {
        tag := c.Request.RequestURI

        var req interface{} = nil
        if reqType != nil {
            // 原始类型
            req = reflect.New(reqType).Interface()
            // 原始类型校验
            if err := c.ShouldBindBodyWith(req, binding.JSON); err != nil {
                // 结构体绑定出错
                c.JSON(http.StatusOK, NewBindFailedResponse(tag))
                // 终止执行链
                c.Abort()
                return
            }
        }
        // 无需校验, 执行链往下
        c.Next()
    }
}

程序示例:

// 声明请求参数结构,id必传
type PingReq struct {
    Name string `json:"name"`
    // tag required表示id字段必传
    Id   string `json:"id" binding:"required"`
}

/*
    在路由/hello-with-req helloFunc()处理逻辑之前,
    注入ReqCheck检测请求体拦截模块
*/
//...
r.POST("/hello-with-req", middle.ReqCheck(bizmod.PingReq{}), helloFunc)
//...

请求示例:
传入约定合法参数:
请求合法

传入约定违规缺失id参数:
请求缺失必选字段 可以看到,程序两个分支都符合我们的预期,所以,后续可以用middle.ReqCheck(tar $Type)这个中间件来拦截请求,让程序通用地对$Type在运行时才根据传入$Type进行类型校验,减轻下游业务函数的重复代码。

后续核心业务只需要交给helloFunc去执行就好了,相当于把进出火车站的乘客隐患在安检处统一处理排除,上了火车的一定是安检通过的乘客。

总结

回到前面的描述,理论上中间件特别适合做通用模块的接入,如签名校验/网关限流/日志上报/缓存等等。

这个系列后续有机会将会展开更具体的业务场景的实现举例与分析。

分类:
后端
标签:
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改