目 录CONTENT

文章目录
Go

05.跟着Geek兔兔学习-Go语言动手写Web框架-中间件Middleware

Hello!你好!我是村望~!
2023-09-06 / 0 评论 / 0 点赞 / 233 阅读 / 2,350 字
温馨提示:
我不想探寻任何东西的意义,我只享受当下思考的快乐~

中间件Middleware

感谢Geek 兔的教程

示例代码,解释摘抄 https://geektutu.com/ + 结合自己的理解

中间件是什么

  • 设计并实现 Web 框架的中间件(Middlewares)机制。
  • 实现通用的Logger中间件,能够记录请求到响应所花费的时间

中间件(middlewares),简单说,就是非业务的技术类组件

Web 框架本身不可能去理解所有的业务,因而不可能实现所有的功能。

因此,框架需要有一个插口,允许用户自己定义功能,嵌入到框架中,仿佛这个功能是框架原生支持的一样。

因此,对中间件而言,需要考虑2个比较关键的点:

  • 插入点在哪?使用框架的人并不关心底层逻辑的具体实现,如果插入点太底层,中间件逻辑就会非常复杂。

    如果插入点离用户太近,那和用户直接定义一组函数,每次在 Handler 中手工调用没有多大的优势了。

  • 中间件的输入是什么?中间件的输入,决定了扩展能力。暴露的参数太少,用户发挥空间有限。

中间件设计

Gee 的中间件的定义与路由映射的 Handler 一致,处理的输入是Context对象。

插入点是框架接收到请求初始化Context对象后,允许用户使用自己定义的中间件做一些额外的处理,例如记录日志等,以及对Context进行二次加工。

另外通过调用(*Context).Next()函数,中间件可等待用户自己定义的 Handler处理结束后,再做一些额外的操作,例如计算本次处理所用时间等。

即 Gee 的中间件支持用户在请求被处理的前后,做一些额外的操作。

举个例子,我们希望最终能够支持如下定义的中间件,c.Next()表示等待执行其他的中间件或用户的Handler

func Logger() HandlerFunc {
	return func(c *Context) {
		// Start timer
		t := time.Now()
		// Process request
		c.Next()
		// Calculate resolution time
		log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t))
	}
}

另外,支持设置多个中间件,依次进行调用。

我们上一篇文章分组控制 Group Control中讲到,中间件是应用在RouterGroup上的,应用在最顶层的 Group,相当于作用于全局,所有的请求都会被中间件处理。

type RouterGroup struct {
	prefix      string
	middlewares []HandlerFunc 
	parent      *RouterGroup  
	engine      *EngineV2     
}

那为什么不作用在每一条路由规则上呢?

作用在某条路由规则,那还不如用户直接在 Handler 中调用直观。只作用在某条路由规则的功能通用性太差,不适合定义为中间件。

Context 设计

我们之前的框架设计是这样的,当接收到请求后,匹配路由,该请求的所有信息都保存在Context中。中间件也不例外,接收到请求后,应查找所有应作用于该路由的中间件,保存在Context中,依次进行调用。

为什么依次调用后,还需要在Context中保存呢?

因为在设计中,Context专注于请求和响应的信息,而RouterGroup专注于路由和中间件的管理,比如通过Group实例注册中间件!Context在多个中间件之间形成链条,而且Context在不同的请求中是独立的!

为此,我们给Context添加了2个参数,定义了Next方法:

type Context struct {
	// origin objects
	Writer http.ResponseWriter
	Req    *http.Request
	// request info
	Path   string
	Method string
	Params map[string]string
	// response info
	StatusCode int
	// middleware
	handlers []HandlerFunc
	index    int
}

func newContext(w http.ResponseWriter, req *http.Request) *Context {
	return &Context{
		Path:   req.URL.Path,
		Method: req.Method,
		Req:    req,
		Writer: w,
		index:  -1,
	}
}

Next方法

func (c *Context) Next() {
	c.index++
	s := len(c.handlers)
	for ; c.index < s; c.index++ {
		c.handlers[c.index](c)
	}
}

index是记录当前执行到第几个中间件,当在中间件中调用Next方法时,控制权交给了下一个中间件,直到调用到最后一个中间件,然后再从后往前,调用每个中间件在Next方法之后定义的部分。

如果我们将用户在映射路由时定义的Handler添加到c.handlers列表中,结果会怎么样呢?

func A(c *Context) {
    part1
    c.Next()
    part2
}

func B(c *Context) {
    part3
    c.Next()
    part4
}

假设我们应用了中间件 A 和 B,和路由映射的 Handler。

c.handlers是这样的[A, B, Handler],c.index初始化为-1。调用c.Next(),接下来的流程是这样的:

  • c.index++c.index变为 0
  • 0 < 3,调用 c.handlers[0],即 A()
  • 执行 part1,调用 c.Next() 记住此时A尚未结束!
  • c.index++c.index 变为 1
  • 1 < 3,调用 c.handlers[1],即 B()
  • 执行 part3,调用 c.Next() 记住此时B尚未结束!
  • c.index++c.index 变为 2
  • 2 < 3,调用 c.handlers[2],即Handler
  • Handler 调用完毕,返回到 B 中的 part4,继续执行 part4,此时B() 调用完毕!
  • part4 执行完毕,返回到 A 中的 part2,执行 part2,此时A() 调用完毕!
  • part2 执行完毕,结束。

一句话说清楚重点,最终的顺序是part1 -> part3 -> Handler -> part 4 -> part2。恰恰满足了我们对中间件的要求,接下来看调用部分的代码,就能全部串起来了。

代码实现

定义Use函数,将中间件应用到某个 Group 。还记得之前RouteGroup的实现留了middlewares的字段

type RouterGroup struct {
	prefix      string
	middlewares []HandlerFunc // support middleware
	parent      *RouterGroup  // support nesting
	engine      *EngineV2     // all groups share a EngineV2 instance
}

实现Use方法!

Use 方法将中间件注册到当前Group

func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
	group.middlewares = append(group.middlewares, middlewares...)
}

ServeHTTP 函数

也有变化,当我们接收到一个具体请求时,要判断该请求适用于哪些中间件

在这里我们简单通过 URL 的前缀来判断。得到中间件列表后,赋值给 c.handlers

比如发起的请求是 /api/v1/userList Group 前缀是 /api/v1 那么 strings.HasPrefix(req.URL.Path, group.prefix)就能找到 /api/v1 的group了,就能拿到注册在 /api/v1的中间件了!然后挂载到 Context

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	var middlewares []HandlerFunc
	for _, group := range engine.groups {
    // 那么就在这里能找到 这个group~
		if strings.HasPrefix(req.URL.Path, group.prefix) {
      // 然后把这个group的中间件,放到middlewares切片中,后面放到Context的handler中,统一去执行!因为本质上中间件也是handlerFunc
			middlewares = append(middlewares, group.middlewares...)
		}
	}                                 
	c := newContext(w, req)
	c.handlers = middlewares
	engine.router.handle(c)
}

handle 函数中,将从路由匹配得到的 Handler 添加到 c.handlers列表中,执行c.Next()

func (r *router) handle(c *Context) {
	n, params := r.getRoute(c.Method, c.Path)
	if n != nil {
		key := c.Method + "-" + n.pattern
		c.Params = params
    // 前面和之前都是一样的,拿着key匹配HandleFunc
    // 然后把非中间件的HandleFunc放在最后,最后执行的!
		c.handlers = append(c.handlers, r.handlers[key])
	} else {
    // 否则就自定义一个404的handlerFunc 丢进去
		c.handlers = append(c.handlers, func(c *Context) {
			c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
		})
	}
	c.Next()
}

使用 Demo

一些中间件函数

package gee

import (
	"fmt"
	"log"
	"time"
)

func Logger() HandlerFunc {
	return func(c *Context) {
		// Start timer
		t := time.Now()
		// Process request
		c.Next()
		// Calculate resolution time
		log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t))
	}
}



func PrintURL() HandlerFunc {
	return func(c *Context) {
		// Start timer
		t := time.Now()
		fmt.Printf("c.Req.URL: %v\n", c.Req.URL)
		// Process request
		c.Next()
		// Calculate resolution time
		log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t))
	}
}


func AdminOnly() HandlerFunc {
	return func(c *Context) {
		t := time.Now()
		fmt.Printf("尊敬的Admin大人 c.Req.URL: %v\n", c.Req.URL)
		// Process request
		c.Next()
		// Calculate resolution time
		log.Printf("🫡尊敬的Admin大人[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t))
	}
}

使用

package main

import (
	"fmt"
	"gee"
	"net/http"
)

func main() {
	r := gee.NewV2()
	tourister := r.Group("/tourister")
	tourister.Use(gee.Logger(), gee.PrintURL())
	touristerV1 := tourister.Group("/v1")
	touristerV1.GET("/post/:id", func(ctx *gee.Context) {
		ctx.String(http.StatusOK, fmt.Sprintf("游客阅读Post%s", ctx.Param("id")))
	})
	admin := r.Group("/admin")
	admin.Use(gee.AdminOnly())
	adminV1 := admin.Group("/v1")
	adminV1.GET("/post/:id", func(ctx *gee.Context) {
		ctx.String(http.StatusOK, fmt.Sprintf("Admin阅读Post%s", ctx.Param("id")))
	})
	r.Run(":9999")
}
❯ curl http://127.0.0.1:9999/admin/v1/post/1

Admin阅读Post1%  

❯ curl http://127.0.0.1:9999/tourister/v1/post/1
游客阅读Post1%    

Go console 输出

❯ go run main.go
2023/09/06 15:36:39 Route  GET - /tourister/v1/post/:id
2023/09/06 15:36:39 Route  GET - /admin/v1/post/:id

尊敬的Admin大人 c.Req.URL: /admin/v1/post/1
2023/09/06 15:36:48 🫡尊敬的Admin大人[200] /admin/v1/post/1 in 33.623µs

c.Req.URL: /tourister/v1/post/1
2023/09/06 15:36:54 [200] /tourister/v1/post/1 in 53.537µs
2023/09/06 15:36:54 [200] /tourister/v1/post/1 in 59.713µs
0

评论区