middleware,常常用于作为请求头/请求体校验、缓存、限流等核心组件的关卡"安检"。

安检

前言

网关中间件是事件流处理执行通道,它是请求流转控件,又叫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去执行就好了,相当于把进出火车站的乘客隐患在安检处统一处理排除,上了火车的一定是安检通过的乘客。

总结

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

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