Contents

实现一个Web服务-(一)

熟悉 Golang 的话就会知道,用官方提供的 net/http 标准库搭建一个 Web Server,是一件非常简单的事。既然 Golang 利用其精简的语法实现了 Web Server,那么是有必要知其然和知其所以然。最近看到很多文章都介绍了如何实现一个 Web ,那么今天做一个总结。


不同的框架设计和功能有很大的差别。任何一个爆款后端编程语言都会有一个属于她的Web实现,比如Python的 django、flask。同样的,Golang 的新框架也层出不穷,比如 Beego,Gin,Iris ,还有字节跳动出品的 hertz 。下面看看怎么实现一个 Web Server。

由于代码比较多,没有将每一步的操作都保存下来,不过也有很多出色的教程,比如 Go语言动手写Web框架

最后的实现的git地址 : https://github.com/kiosk404/goweb/

如何实现一个 Web Server

Go 里可以使用 net/http 来创建一个 HTTP 服务快速实现一个简单的 Web Server。

1
2
3
4
5
6
7
// 创建一个bar路由和处理函数
http.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
})

// 监听8080端口
log.Fatal(http.ListenAndServe(":8080", nil))

上面的例子中 http.ListenAdnServe 启动了一个 Web Server,http.Handle 创建了一个路由并注册了路由处理的函数,访问 http://127.0.0.1:8080/bar,可以得到 Hello, “/bar” 内容。

在 net/http 里,可以看到主要有以下这些结构。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
go doc net/http | grep "^type"|grep struct
type Client struct{ ... }
type Cookie struct{ ... }
type ProtocolError struct{ ... }
type PushOptions struct{ ... }
type Request struct{ ... }
type Response struct{ ... }
type ServeMux struct{ ... }
type Server struct{ ... }
type Transport struct{ ... }
  • Client 负责构建 HTTP 客户端;
  • Server 负责构建 HTTP 服务端;
  • ServerMux 负责 HTTP 服务端路由;
  • Transport、Request、Response、Cookie 负责客户端和服务端传输对应的不同模块。

HTTP 基础

net/http.Server 这个结构体中最重要的是 Handler,当 ListenAndServe(addr string, handler Handler) 中的Handler 这个字段为空的时候,他默认会用 DefaultServerMux 这个路由器来填充。通过查看源码可以发现 Handler 是一个接口,需要实现方法 ServeHTTP, 也就是传入了任何实现了 ServerHTTP 接口的实例,就能处理一个传入的HTTP请求。

创建一个 framework 文件夹,新建 core.go, 在里面写入如下内容。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 框架核心结构
type Core struct {
}

// 初始化框架核心结构
func NewCore() *Core {
	return &Core{}
}

// 框架核心结构实现Handler接口
func (c *Core) ServeHTTP(response http.ResponseWriter, request *http.Request) {
	switch request.URL.Path {
	case "/":
		fmt.Fprintf(response, "URL.Path = %q\n", request.URL.Path)
	case "/hello":
		for k, v := range request.Header {
			fmt.Fprintf(response, "Header[%q] = %q\n", k, v)
		}
	default:
		fmt.Fprintf(response, "404 NOT FOUND: %s\n", request.URL)
	}
}

这下我们在 main.go 的里面调用做如下替换,即完成了一个最基础的 Web Server。但是这里的路由使用一个 switch 语句实现,还比较原始,接下来需要再对 路由 进行封装。

1
2
3
4
5
6
server := http.Server{
		Addr:              ":8080",
		Handler:           framework.NewCore(),
	}
// 监听8080端口
server.ListenAndServe()

不管怎么说,肯定是要对 Core 结构体进行改造了,我们将路由规则由 switch 匹配的模式改为 map 存储的格式,先实现一个静态路由匹配。实现最基本的功能。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// HandlerFunc defines the request handler used by gee
type HandlerFunc func(http.ResponseWriter, *http.Request)

// 框架核心结构
type Core struct {
	router map[string]HandlerFunc
}

func (c *Core) registerRouter(method, pattern string, handlerFunc HandlerFunc) {
	key := method + "-" + pattern
	c.router[key] = handlerFunc
}

func (c *Core) Get(url string, handler HandlerFunc) {
	c.registerRouter("GET", url, handler)
}

func (c *Core) Post(url string, handler HandlerFunc) {
	c.registerRouter("POST", url, handler)
}

// 初始化框架核心结构
func NewCore() *Core {
	return &Core{
		router: make(map[string]HandlerFunc),
	}
}

// 框架核心结构实现Handler接口
func (c *Core) ServeHTTP(response http.ResponseWriter, request *http.Request) {
	key := request.Method + "-" + request.URL.Path
	if handler, ok := c.router[key]; ok {
		handler(response, request)
	} else {
		fmt.Fprintf(response, "404 NOT FOUND: %s\n", request.URL)
	}
}

这下和就和标准的 HTTP Web框架比较像了,在 main 函数里进行路由注册。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func main() {
    core := framework.NewCore()
	core.Get("/foo", func(w http.ResponseWriter, req *http.Request) {
		fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
	})

	server := http.Server{
		Addr:              ":8080",
		Handler:           core,
	}
	// 监听8080端口
	server.ListenAndServe()
}

上下文

http 里的上下文不仅是为了封装 Request 和 Response,向外提供 JSON 或者 HTML 的返回,另外还肩负者请求的超时处理等工作。我们来分析一下 上下文 设计的必要性。

  1. beego 中的效果,我们还需要对一些模板、常见返回格式封装,比如我们在返回 json 时,需要对 Response Header 中的 Content-Type 字段进行设定,而一个优秀的 Web Context 就需要对其进行封装,避免大量重复、复杂的代码。比如一个返回码为 200 的JSON返回,经过封装如下即可。
1
2
3
4
c.JSON(http.StatusOK, gee.H{
    "username": c.PostForm("username"),
    "password": c.PostForm("password"),
})
  1. 官方库里可以看到其主要实现了 取消、超时、截止时间的Context。
1
2
3
4
go doc context | grep "^func" 
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

熟悉 beego 的同学可能都经常用创建 Context 的方式封装一个 Controller 。每个连接的 Context 都基于 baseContext 复制而来。 对应到代码中,就是在某个连接开启 Goroutine 的时候,为当前的连接创建了一个 connContext,这个 connContext 是基于 server 中的 Context 而来,而 server 中的 Context 的基础就是 baseContext。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13

type Server struct {
  ...

    // BaseContext 用来为整个链条创建初始化 Context
    // 如果没有设置的话,默认使用 context.Background()
  BaseContext func(net.Listener) context.Context{}

    // ConnContext 用来为每个连接封装 Context
    // 参数中的 context.Context 是从 BaseContext 继承来的
  ConnContext func(ctx context.Context, c net.Conn) context.Context{}
    ...
}
  1. 除此之外,一个框架肯定是需要支持中间件的,中间件产生的信息需要交给上下文去处理,Context 随着一个请求的出现而产生,请求的结束而销毁。

为了实现 Context 对现有 Core 的替换,我们新创建2个文件context.gocontroller.go 。其中 controller.go 用来将我们的 handler 封装过度到 context。

1
2
3
// controller.go

type ControllerHandler func(c *Context) error

context 里的内容比较多,主要做的事情就是将 ctx.request.Context() 暴露出来和对返回内容的封装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// context.go

type Context struct {
	request        *http.Request
	responseWriter http.ResponseWriter
	ctx            context.Context
	handler        ControllerHandler

	// 是否超时标记位
	hasTimeout bool
	// 写保护机制
	writerMux *sync.Mutex
}

func NewContext(r *http.Request, w http.ResponseWriter) *Context {
	return &Context{
		request:        r,
		responseWriter: w,
		ctx:            r.Context(),
		writerMux:      &sync.Mutex{},
	}
}

// #region base function

func (ctx *Context) WriterMux() *sync.Mutex {
	return ctx.writerMux
}

func (ctx *Context) GetRequest() *http.Request {
	return ctx.request
}

func (ctx *Context) GetResponse() http.ResponseWriter {
	return ctx.responseWriter
}

func (ctx *Context) SetHasTimeout() {
	ctx.hasTimeout = true
}

func (ctx *Context) HasTimeout() bool {
	return ctx.hasTimeout
}

// #endregion

func (ctx *Context) BaseContext() context.Context {
	return ctx.request.Context()
}

// #region implement context.Context
func (ctx *Context) Deadline() (deadline time.Time, ok bool) {
	return ctx.BaseContext().Deadline()
}

func (ctx *Context) Done() <-chan struct{} {
	return ctx.BaseContext().Done()
}

func (ctx *Context) Err() error {
	return ctx.BaseContext().Err()
}

func (ctx *Context) Value(key interface{}) interface{} {
	return ctx.BaseContext().Value(key)
}

// #endregion

// #region response

func (ctx *Context) Json(status int, obj interface{}) error {
	if ctx.HasTimeout() {
		return nil
	}
	ctx.responseWriter.Header().Set("Content-Type", "application/json")
	ctx.responseWriter.WriteHeader(status)
	byt, err := json.Marshal(obj)
	if err != nil {
		ctx.responseWriter.WriteHeader(500)
		return err
	}
	ctx.responseWriter.Write(byt)
	return nil
}

func (ctx *Context) HTML(status int, obj interface{}, template string) error {
	return nil
}

func (ctx *Context) Text(status int, obj string) error {
	ctx.responseWriter.Header().Set("Content-Type", "application/text")
	ctx.responseWriter.WriteHeader(status)
	_, err := ctx.responseWriter.Write([]byte(obj))
	return err
}

// #endregion

如此以来,将 core.go 的封装也改动一下 , 我们 Context 的改造也完毕了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 框架核心结构
type Core struct {
	router map[string]ControllerHandler
}

func (c *Core) Get(url string, handler ControllerHandler) {
	c.registerRouter("GET", url, handler)
}

func (c *Core) Post(url string, handler ControllerHandler) {
	c.registerRouter("POST", url, handler)
}

// 初始化框架核心结构
func NewCore() *Core {
	return &Core{
		router: make(map[string]ControllerHandler),
	}
}

// 框架核心结构实现Handler接口
func (c *Core) ServeHTTP(response http.ResponseWriter, request *http.Request) {
	ctx := NewContext(request, response)
	key := request.Method + "-" + request.URL.Path

	if handler, ok := c.router[key]; ok {
		handler(ctx)
	} else {
		ctx.Text(http.StatusNotFound, fmt.Sprintf("404 NOT FOUND: %s\n", request.URL.Path))
	}
}

路由

一个Web服务器框架的路由主要需要考虑 HTTP 方法匹配静态路由匹配批量通用前缀动态路由匹配

这里的路由就应该考虑一下复杂的逻辑了,比如动态匹配(Dynamic Route) 和 分组控制(Group Control)。

动态匹配 - (前缀树 Trie)

之前我们是以一个简单的 Map 结构存储了路由表,但是这就导致我们的路由只能是静态的。比如我们如何支持类似于 “/v1/hello/:name” 这样的路由呢?这种路由下 “/v1/hello/kiosk” 或者 “、"/v1/hello/bob” 都是可以的。

动态路由有很多实现方式,支持的规则、性能等有很大的差异,开源的实现有 "gorouter" 。支持包括正则表达式的匹配,另一个开源版本 httprouter 则不支持正则。

Trie 树具备参数匹配:例如 “/p/:lang/doc”, 可以匹配 “/p/c/doc” 和 “/p/go/doc”

Trie 实现

创建文件 trie.go , 实现树节点存储的信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 代表树结构
type Tree struct {
	root *node // 根节点
}

// 代表节点
type node struct {
	isLast  bool              // 该节点是否能成为一个独立的uri, 是否自身就是一个终极节点
	segment string            // uri中的字符串
	handler ControllerHandler // 控制器
	childs  []*node           // 子节点
}

与普通的树不同,为了实现动态路由,我们需要每一层都匹配,isWildSegment 判断是否范匹配到 “:xxx”。每次将匹配剩下的字符串拆成两份,如果拆成2份说明,没匹配完,拆成一份则表示匹配完了,现在这个 segment 是最后一个。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
// 判断路由是否已经在节点的所有子节点树中存在了
func (n *node) matchNode(uri string) *node {
    // 使用分隔符将uri切割为两个部分, 如 "/api/v1" 会被拆成 ""(/前面是空) 和 "api/v1"
	segments := strings.SplitN(uri, "/", 2)
	// 第一个部分用于匹配下一层子节点
	segment := segments[0] 
	segment = strings.ToUpper(segment)

	// 匹配符合的下一层子节点
	cnodes := n.filterChildNodes(segment)
	// 如果当前子节点没有一个符合,那么说明这个uri一定是之前不存在, 直接返回nil
	if cnodes == nil || len(cnodes) == 0 {
		return nil
	}

	// 如果只有一个segment,则是最后一个标记
	if len(segments) == 1 {
		// 如果segment已经是最后一个节点,判断这些cnode是否有isLast标志
		for _, tn := range cnodes {
			if tn.isLast {
				return tn
			}
		}

		// 都不是最后一个节点
		return nil
	}

	// 如果有2个segment, 递归每个子节点继续进行查找
	for _, tn := range cnodes {
		tnMatch := tn.matchNode(segments[1])
		if tnMatch != nil {
			return tnMatch
		}
	}
	return nil
}



// 过滤下一层满足segment规则的子节点
func (n *node) filterChildNodes(segment string) []*node {
	if len(n.childs) == 0 {
		return nil
	}

	// 如果segment是通配符,则所有下一层子节点都满足需求
	if isWildSegment(segment) {
		return n.childs
	}

	nodes := make([]*node, 0, len(n.childs))
	// 过滤所有的下一层子节点
	for _, cnode := range n.childs {
		if isWildSegment(cnode.segment) {
			// 如果下一层子节点有通配符,则满足需求
			nodes = append(nodes, cnode)
		} else if cnode.segment == segment {
			// 如果下一层子节点没有通配符,但是文本完全匹配,则满足需求
			nodes = append(nodes, cnode)
		}
	}

	return nodes
}

第10行在当前Node 节点遍历出所有符合的 node ,再到第30行递归接着遍历。直到遍历到最后segment split 结果为 1,返回最终的那个node。

除了查找之外,前缀树另外一个比较重要的部分是插入一个路由。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
func (tree *Tree) AddRouter(uri string, handler ControllerHandler) error {
	n := tree.root
	if n.matchNode(uri) != nil {
		return errors.New("route exist: " + uri)
	}

	segments := strings.Split(uri, "/")
	// 对每个segment
	for index, segment := range segments {

		// 最终进入Node segment的字段
		segment = strings.ToUpper(segment)
		isLast := index == len(segments)-1

		var objNode *node // 标记是否有合适的子节点

		childNodes := n.filterChildNodes(segment)
		// 如果有匹配的子节点
		if len(childNodes) > 0 {
			// 如果有segment相同的子节点,则选择这个子节点
			for _, cnode := range childNodes {
				if cnode.segment == segment {
					objNode = cnode
					break
				}
			}
		}

		if objNode == nil {
			// 创建一个当前node的节点
			cnode := newNode()
			cnode.segment = segment
			if isLast {
				cnode.isLast = true
				cnode.handler = handler
			}
			n.childs = append(n.childs, cnode)
			objNode = cnode
		}

		n = objNode
	}

	return nil
}

如此我们即实现了一个前缀树匹配路由。

分组控制

分组控制(Group Control)是 Web 框架应提供的基础功能之一。所谓分组,是指路由的分组。如果没有路由分组,我们需要针对每一个路由进行控制。但是真实的业务场景中,往往某一组路由需要相似的处理。例如:某一批的API均已 “/api/v1” 开头。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// IGroup 代表前缀分组
type IGroup interface {
	// 实现HttpMethod方法
	Get(string, ControllerHandler)
	Post(string, ControllerHandler)

	// 实现嵌套group
	Group(string) IGroup
}

// Group struct 实现了IGroup
type Group struct {
	core   *Core  // 指向core结构
	parent *Group //指向上一个Group,如果有的话
	prefix string // 这个group的通用前缀
}

// 初始化Group
func NewGroup(core *Core, prefix string) *Group {
	return &Group{
		core:   core,
		parent: nil,
		prefix: prefix,
	}
}

一个 Group 需要实现首先指向 core 的指针。另外还需要 父节点的分组(如果有的话), 另外就是最基础的当前这个通用前缀的名字了,另外他需要实现 Core 的 Get、Post 等方法。

生成一个 Group 的方法实现即

1
2
3
4
5
6
// 实现 Group 方法
func (g *Group) Group(uri string) IGroup {
	cgroup := NewGroup(g.core, uri)
	cgroup.parent = g
	return cgroup
}

如果想要在这个Group上加方法的话,则可以

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 实现Get方法
func (g *Group) Get(uri string, handler ControllerHandler) {
	uri = g.getAbsolutePrefix() + uri
	g.core.Get(uri, handler)
}

// 实现Post方法
func (g *Group) Post(uri string, handler ControllerHandler) {
	uri = g.getAbsolutePrefix() + uri
	g.core.Post(uri, handler)
}

// 获取当前group的绝对路径
func (g *Group) getAbsolutePrefix() string {
	if g.parent == nil {
		return g.prefix
	}
	return g.parent.getAbsolutePrefix() + g.prefix
}

如此一来,我们可以对 framework.Core 进行改造了。之前的静态 map 路由的方式可以改掉了,也不需要 key 由 “Method” 和 “URL” 拼接生成了,而是可以用 “Method” 当key,所有和路由相关的内容全部放入到 Trie Tree 里。core.go 改造如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 框架核心结构
type Core struct {
	router map[string]*Tree
}

func (c *Core) Get(url string, handler ControllerHandler) {
	if err := c.router["GET"].AddRouter(url, handler); err != nil {
		log.Fatal("add router error: ", err)
	}
}

func (c *Core) Post(url string, handler ControllerHandler) {
	if err := c.router["POST"].AddRouter(url, handler); err != nil {
		log.Fatal("add router error: ", err)
	}
}

// 初始化框架核心结构
func NewCore() *Core {
	router := map[string]*Tree{}
	router["GET"] = NewTree()
	router["POST"] = NewTree()
	return &Core{router: router}
}

// ==== http method wrap end

func (c *Core) Group(prefix string) IGroup {
	return NewGroup(c, prefix)
}

// 匹配路由,如果没有匹配到,返回nil
func (c *Core) FindRouteByRequest(request *http.Request) ControllerHandler {
	// uri 和 method 全部转换为大写,保证大小写不敏感
	uri := request.URL.Path
	method := request.Method
	upperMethod := strings.ToUpper(method)

	// 查找第一层map
	if methodHandlers, ok := c.router[upperMethod]; ok {
		return methodHandlers.FindHandler(uri)
	}
	return nil
}

// 框架核心结构实现Handler接口
func (c *Core) ServeHTTP(response http.ResponseWriter, request *http.Request) {
	ctx := NewContext(request, response)
	// 寻找路由
	router := c.FindRouteByRequest(request)
	if router == nil {
		// 如果没有找到,这里打印日志
		_ = ctx.Json(404, "not found")
		return
	}

	// 调用路由函数,如果返回err 代表存在内部错误,返回500状态码
	if err := router(ctx); err != nil {
		_ = ctx.Json(500, "inner error")
		return
	}
}

至此完整的代码可以参考: https://github.com/kiosk404/goweb/tree/web_router

中间件

中间件(middlewares),简单说,就是非业务的技术类组件。Web 框架本身不可能去理解所有的业务,因而不可能实现所有的功能。因此,框架需要有一个插口,允许用户自己定义功能,嵌入到框架中,仿佛这个功能是框架原生支持的一样。

之前其实就提到过了,中间件的实现离不开上面已经实现了的 Context 。我们处理的思路就是 PipeLine 改造中间件,使用一个数组链接起来,形成一个流水线,就完美的解决了这个问题。

请求流如下:

https://img1.kiosk007.top/static/images/blog/goweb_middleware.png

goweb_middleware

中间件的插入点是框架接收到请求初始化 Context 对象后,允许用户做一写自己的事情,如记录日志等,完成后,调用 (*Context).Next() 函数。

举个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func Logger() ControllerHandler {
    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))
	}
}

好了,开始对 Context 进行相关的改造:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Context代表当前请求上下文
type Context struct {
	request        *http.Request
	responseWriter http.ResponseWriter
	ctx            context.Context
	writerMux  *sync.Mutex

	// 当前请求的handler链条
	handlers []ControllerHandler
	index    int // 当前请求调用到调用链的哪个节点
}

// NewContext 初始化一个Context
func NewContext(r *http.Request, w http.ResponseWriter) *Context {
	return &Context{
		request:        r,
		responseWriter: w,
		ctx:            r.Context(),
		writerMux:      &sync.Mutex{},
		index:          -1,
	}
}

接下来就是最核心的函数 Next() ,在中间件中调用 Next 方法来执行下一个 Handler 内容。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 核心函数,调用context的下一个函数
func (ctx *Context) Next() error {
	ctx.index++
	if ctx.index <= len(ctx.handlers) {
		if err := ctx.handlers[ctx.index-1](ctx); err != nil {
			return err
		}
	}
	return nil
}

当然 core 也就需要相应的改造了,首先是前缀树路由,路由不再是一个 Handler ,而是一个 Handler 列表。另外我们还需要保存所有已经注册的中间件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 框架核心结构
type Core struct {
	router map[string]*Tree
	middlewares []ControllerHandler // 从core这边设置的中间件
}

// 注册中间件
func (c *Core) Use(middlewares ...ControllerHandler) {
	c.middlewares = append(c.middlewares, middlewares...)
}

相应的,如果添加 Method 方法,我们需要先将中间件加进去。

1
2
3
4
5
6
7
func (c *Core) Get(url string, handlers ...ControllerHandler) {
	// 将core的middleware 和 handlers结合起来
	allHandlers := append(c.middlewares, handlers...)
	if err := c.router["GET"].AddRouter(url, allHandlers); err != nil {
		log.Fatal("add router error: ", err)
	}
}

如此以来,ServeHTTP() 方法就需要小小的改造,封装好一个 Context ,从路由表里找到 handlers 列表,将 handlers 加入到 ctx 中依次遍历执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// 框架核心结构实现Handler接口
func (c *Core) ServeHTTP(response http.ResponseWriter, request *http.Request) {
	ctx := NewContext(request, response)
	// 寻找路由
	handlers := c.FindRouteByRequest(request)
	if handlers == nil {
		// 如果没有找到,这里打印日志
		_ = ctx.Json(404, "not found")
		return
	}

	ctx.SetHandlers(handlers)

	// 调用路由函数,如果返回err 代表存在内部错误,返回500状态码
	if err := ctx.Next(); err != nil {
		ctx.Json(500, "inner error")
		return
	}
}

下面写几个中间件,一个是计算一个请求的耗时,你个是设置请求的超时时间。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 超时的中间件
func Timeout(d time.Duration) framework.ControllerHandler {
	// 使用函数回调
	return func(c *framework.Context) error {
		finish := make(chan struct{}, 1)
		panicChan := make(chan interface{}, 1)
		// 执行业务逻辑前预操作:初始化超时context
		durationCtx, cancel := context.WithTimeout(c.BaseContext(), d)
		defer cancel()

		go func() {
			defer func() {
				if p := recover(); p != nil {
					panicChan <- p
				}
			}()
			// 使用next执行具体的业务逻辑
			c.Next()

			finish <- struct{}{}
		}()
		// 执行业务逻辑后操作
		select {
		case p := <-panicChan:
			c.Json(500, "server internal error")
			log.Println(p)
		case <-finish:
			fmt.Println("finish")
		case <-durationCtx.Done():
			fmt.Println("超时了")
			c.SetHasTimeout()
			c.Json(http.StatusBadGateway, "time out")
		}
		return nil
	}
}

统计时间的中间件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
// 统计运行时间的中间件
func Cost() framework.ControllerHandler {
	// 使用函数回调
	return func(c *framework.Context) error {
		// 记录开始时间
		start := time.Now()

		// 使用next执行具体的业务逻辑
		c.Next()

		// 记录结束时间
		end := time.Now()
		cost := end.Sub(start)
		fmt.Printf("api uri: %v, cost: %v \n", c.GetRequest().RequestURI, cost.Seconds())

		return nil
	}
}

相关的代码:https://github.com/kiosk404/goweb/tree/middleware

总结

至此,一个 WebServer 的 MVP版本就诞生了,其拥有 前缀树路由匹配、分组功能、加载中间件。其核心的功能都已经完成,其余的功能最多是丰富内容锦上添花。后面会介绍一个 WebServer 的更高阶内容。