前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Web框架的设计方案和Go源码实现

Web框架的设计方案和Go源码实现

作者头像
后端云
发布2022-11-25 19:04:48
3570
发布2022-11-25 19:04:48
举报
文章被收录于专栏:后端云

为何要用web框架

最有名的web框架莫过于Java的SpringBoot,Go的Gin。本篇以Go语言为例。其他语言或其他主流的web框架基于的设计方案基本都遵循这个。

其实不用web框架也可以进行web后端开发,比如下面的最简单的例子:

代码语言:javascript
复制
package main

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

func main() {
    // http.HandleFunc 实现了路由和Handler的映射
	http.HandleFunc("/", indexHandler)
	http.HandleFunc("/hello", helloHandler)
    // http.ListenAndServe 启动一个http服务,第一个参数是ip和端口号,第二个参数是http包里的Handler接口
	log.Fatal(http.ListenAndServe(":9999", nil))
}

// handler echoes r.URL.Path
func indexHandler(w http.ResponseWriter, req *http.Request) {
	fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
}

// handler echoes r.URL.Header
func helloHandler(w http.ResponseWriter, req *http.Request) {
	for k, v := range req.Header {
		fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
	}
}

测试得到下面的结果:

代码语言:javascript
复制
$ curl http://localhost:9999/
URL.Path = "/"
$ curl http://localhost:9999/hello
Header["Accept"] = ["*/*"]
Header["User-Agent"] = ["curl/7.54.0"]

那么为何要用web框架,或者说现在的主流web后端开发都要选定一个框架,然后再开发,就是为了提高效率,共通的业务以外的逻辑都由框架实现了,有了框架,开发只需要专注业务逻辑。

那么设计web框架的目的就很明确了,解决非业务的共通需求。那么有哪些此类的需求呢?就是框架要解决的问题。

  1. 注册路由和路由发现
  2. 快速路由算法(是框架理解上最复杂的地方,参考之前的两篇文章 http前缀树路由算法和Go源码分析 http基数树路由算法和Go源码分析,本篇略过。路由的性能非常重要,是框架间竞争的主要指标)
  3. 上下文Context
  4. 分组路由
  5. 中间件(比如日志中间件,校验中间件)
  6. 模板Template(现在开发都是前后端分离,模板很少实际开发中使用,所以这部分本篇略过)
  7. 错误恢复

所以一个好的web框架的核心在于:决定性能的路由算法。社区活跃度。特色功能。易用度。等等。

掌握的这些问题的解决方案,就可以自己设计web框架,或者在现有框架的基础上定制框架。下面逐一介绍:

注册路由和路由发现

在拥有框架之前,是通过http.HandleFunc关联URL和处理函数handler,再调用http.ListenAndServe(“:9999”, nil),http.ListenAndServe第二个参数留空。

第二个参数是一个Handler接口,需要实现方法 ServeHTTP,第二个参数也是基于net/http标准库实现Web框架的入口。

http.Handler接口的源码:

代码语言:javascript
复制
package http

type Handler interface {
    ServeHTTP(w ResponseWriter, r *Request)
}

func ListenAndServe(address string, h Handler) error

除了调用http.HandleFunc,也可以通过下面的方式实现相同的目的。通过下面的Engine结构体来实现接口方法ServeHTTP,再将Engine传入http.ListenAndServe的第二个参数。

代码语言:javascript
复制
// Engine is the uni handler for all requests
type Engine struct{}

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	switch req.URL.Path {
	case "/":
		fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
	case "/hello":
		for k, v := range req.Header {
			fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
		}
	default:
		fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
	}
}

func main() {
	engine := new(Engine)
	log.Fatal(http.ListenAndServe(":9999", engine))
}

只要对上面的代码做一些代码结构拆分,并在Engine结构体中创建一个map用于保存路由和Handler的映射。之后的调用就出现了我们用主流web框架的雏形调用样式。

代码语言:javascript
复制
func main() {
	r := gee.New()
	r.GET("/", func(w http.ResponseWriter, req *http.Request) {
		fmt.Fprintf(w, "URL.Path = %q\n", req.URL.Path)
	})

	r.GET("/hello", func(w http.ResponseWriter, req *http.Request) {
		for k, v := range req.Header {
			fmt.Fprintf(w, "Header[%q] = %q\n", k, v)
		}
	})

	r.Run(":9999")
}
代码语言:javascript
复制
package gee

import (
	"fmt"
	"net/http"
)

// HandlerFunc defines the request handler used by gee
type HandlerFunc func(http.ResponseWriter, *http.Request)

// Engine implement the interface of ServeHTTP
type Engine struct {
	router map[string]HandlerFunc
}

// New is the constructor of gee.Engine
func New() *Engine {
	return &Engine{router: make(map[string]HandlerFunc)}
}

func (engine *Engine) addRoute(method string, pattern string, handler HandlerFunc) {
	key := method + "-" + pattern
	engine.router[key] = handler
}

// GET defines the method to add GET request
func (engine *Engine) GET(pattern string, handler HandlerFunc) {
	engine.addRoute("GET", pattern, handler)
}

// POST defines the method to add POST request
func (engine *Engine) POST(pattern string, handler HandlerFunc) {
	engine.addRoute("POST", pattern, handler)
}

// Run defines the method to start a http server
func (engine *Engine) Run(addr string) (err error) {
	return http.ListenAndServe(addr, engine)
}

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	key := req.Method + "-" + req.URL.Path
	if handler, ok := engine.router[key]; ok {
		handler(w, req)
	} else {
		fmt.Fprintf(w, "404 NOT FOUND: %s\n", req.URL)
	}
}

上下文Context

为何要有上下文:

  1. 对Web服务来说,无非是根据请求*http.Request,构造响应http.ResponseWriter。要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。现在前后端分离的web开发,返回的结构体往往是json数据类型,所以要对返回体作json数据格式的封装。
  2. 提供和当前请求强相关的信息的存放位置。比如:解析动态路由/hello/:name,参数:name的值。中间件。Context 就像一次会话的百宝箱,可以找到任何东西。

代码实现上:

将router map[string]HandlerFunc的HandlerFunc,从type HandlerFunc func(http.ResponseWriter, *http.Request)切换成type HandlerFunc func(*Context)

对框架的调用也从r.GET("/hello", func(w http.ResponseWriter, req *http.Request)变成r.GET("/hello", func(c *gee.Context)

创建Context结构体,保存上下文(Context目前只包含了http.ResponseWriter和*http.Request,另外提供了对 Method 和 Path 这两个常用属性的直接访问。):

代码语言:javascript
复制
type Context struct {
	// origin objects
	Writer http.ResponseWriter
	Req    *http.Request
	// request info
	Path   string
	Method string
	// response info
	StatusCode int
}

提供了访问Query和PostForm参数的方法:

代码语言:javascript
复制
func (c *Context) PostForm(key string) string {
	return c.Req.FormValue(key)
}

func (c *Context) Query(key string) string {
	return c.Req.URL.Query().Get(key)
}

提供修改返回的状态码和头的方法:

代码语言:javascript
复制
func (c *Context) Status(code int) {
	c.StatusCode = code
	c.Writer.WriteHeader(code)
}

func (c *Context) SetHeader(key string, value string) {
	c.Writer.Header().Set(key, value)
}

提供了快速构造String/Data/JSON/HTML响应的方法:

代码语言:javascript
复制
func (c *Context) String(code int, format string, values ...interface{}) {
	c.SetHeader("Content-Type", "text/plain")
	c.Status(code)
	c.Writer.Write([]byte(fmt.Sprintf(format, values...)))
}

func (c *Context) JSON(code int, obj interface{}) {
	c.SetHeader("Content-Type", "application/json")
	c.Status(code)
	encoder := json.NewEncoder(c.Writer)
	if err := encoder.Encode(obj); err != nil {
		http.Error(c.Writer, err.Error(), 500)
	}
}

func (c *Context) Data(code int, data []byte) {
	c.Status(code)
	c.Writer.Write(data)
}

func (c *Context) HTML(code int, html string) {
	c.SetHeader("Content-Type", "text/html")
	c.Status(code)
	c.Writer.Write([]byte(html))
}

分组路由

web框架一般都提供分组路由和多层分组嵌套功能。分组路由的好处有:

  • 提取共通的部分作为分组,可以减少框架使用者URL的输入长度。
  • 真实的业务场景中,往往某一组路由需要相似的处理。可以按分组配置中间件。

分组路由和嵌套分组的代码实现:

代码语言:javascript
复制
// Engine implement the interface of ServeHTTP
type (
	RouterGroup struct {
		prefix      string
		middlewares []HandlerFunc // support middleware
		parent      *RouterGroup  // support nesting
		engine      *Engine       // all groups share a Engine instance
	}

	Engine struct {
		*RouterGroup
		router *router
		groups []*RouterGroup // store all groups
	}
)

// New is the constructor of gee.Engine
func New() *Engine {
	engine := &Engine{router: newRouter()}
	engine.RouterGroup = &RouterGroup{engine: engine}
	engine.groups = []*RouterGroup{engine.RouterGroup}
	return engine
}

// Group is defined to create a new RouterGroup
// remember all groups share the same Engine instance
func (group *RouterGroup) Group(prefix string) *RouterGroup {
	engine := group.engine
	newGroup := &RouterGroup{
		prefix: group.prefix + prefix,
		parent: group,
		engine: engine,
	}
	engine.groups = append(engine.groups, newGroup)
	return newGroup
}

再addRoute, GET, POST原来放在Engine结构体的方法,现在放到RouterGroup结构体上

框架调用方式现在变为:

代码语言:javascript
复制
func main() {
	r := gee.New()
	r.GET("/index", func(c *gee.Context) {
		c.HTML(http.StatusOK, "<h1>Index Page</h1>")
	})
	v1 := r.Group("/v1")
	{
		v1.GET("/", func(c *gee.Context) {
			c.HTML(http.StatusOK, "<h1>Hello Gee</h1>")
		})

		v1.GET("/hello", func(c *gee.Context) {
			// expect /hello?name=geektutu
			c.String(http.StatusOK, "hello %s, you're at %s\n", c.Query("name"), c.Path)
		})
	}
	v2 := r.Group("/v2")
	{
		v2.GET("/hello/:name", func(c *gee.Context) {
			// expect /hello/geektutu
			c.String(http.StatusOK, "hello %s, you're at %s\n", c.Param("name"), c.Path)
		})
	}
	r.Run(":9999")
}

中间件

中间件(middlewares),简单说,就是非业务的技术类组件。一般中间件加在某一个路由分组上或者总的分组上,即应用在代码的RouterGroup上,中间件可以给框架提供无限的扩展能力。例如/admin的分组,可以应用鉴权中间件;/分组应用日志中间件。

中间件的设计思路:

没有中间件的框架设计是这样的,当接收到请求后,匹配路由,该请求的所有信息都保存在Context中。中间件也不例外,接收到请求后,应查找所有应作用于该路由的中间件,保存在Context中,依次进行调用。为什么依次调用后,还需要在Context中保存呢?因为在设计中,中间件不仅作用在处理流程前,也可以作用在处理流程后,即在用户业务的 Handler 处理完毕后,还可以执行剩下的操作。

具体的中间件框架的代码设计如下:

一部分是对Context的设计,增加中间件相关代码:

代码语言:javascript
复制
type Context struct {
    ...
	// 新增middleware相关的两个参数
	handlers []HandlerFunc
	index    int
}

func newContext(w http.ResponseWriter, req *http.Request) *Context {
	return &Context{
        ...
        // index是含有所有中间件和用户handler的数组下标,初始值为-1,因为首次调用Next()会第一句代码会+1
		index:  -1,
	}
}

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

一部分是对handle以及调用handle的ServeHTTP(实现net/http标准库接口方法),增加中间件相关代码:

代码语言:javascript
复制
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	var middlewares []HandlerFunc
	for _, group := range engine.groups {
		if strings.HasPrefix(req.URL.Path, group.prefix) {
			middlewares = append(middlewares, group.middlewares...)
		}
	}
	c := newContext(w, req)
	c.handlers = middlewares
	engine.router.handle(c)
}

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
		c.handlers = append(c.handlers, r.handlers[key])
	} else {
		c.handlers = append(c.handlers, func(c *Context) {
			c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
		})
	}
	c.Next()
}

这两部分要结合起来看,Context结构体新增middleware相关的两个参数,handler数组和handler数组下标index,index初始值为-1,因为首次调用Next()会第一句代码会+1,即首次会执行注册的第一个中间件。

首次Next()调用是由ServeHTTP方法调用,下次调用Next(),是由框架的使用者在编写业务所需的中间件的代码中调用。

用户路由对应的handler不需要写Next(),因为是该handler是handler数组最后一个。

比如:

代码语言:javascript
复制
//用户编写的中间件A
func A(c *Context) {
    part1
    c.Next()
    part2
}
//用户编写的中间件B
func B(c *Context) {
    part3
    c.Next()
    part4
}

执行的顺序是:part1 -> part3 -> Handler -> part 4 -> part2

错误恢复

对一个 Web 框架而言,错误处理机制是非常必要的。可能是框架本身没有完备的测试,导致在某些情况下出现空指针异常等情况。也有可能用户不正确的参数,触发了某些异常,例如数组越界,空指针等。如果因为这些原因导致系统宕机,必然是不可接受的。

代码实现:

是通过编写错误恢复中间件,并将该中间件注册到Engine上实现的。

错误恢复中间件:

代码语言:javascript
复制
// print stack trace for debug
func trace(message string) string {
	var pcs [32]uintptr
	n := runtime.Callers(3, pcs[:]) // skip first 3 caller

	var str strings.Builder
	str.WriteString(message + "\nTraceback:")
	for _, pc := range pcs[:n] {
		fn := runtime.FuncForPC(pc)
		file, line := fn.FileLine(pc)
		str.WriteString(fmt.Sprintf("\n\t%s:%d", file, line))
	}
	return str.String()
}

func Recovery() HandlerFunc {
	return func(c *Context) {
		defer func() {
			if err := recover(); err != nil {
				message := fmt.Sprintf("%s", err)
				log.Printf("%s\n\n", trace(message))
				c.Fail(http.StatusInternalServerError, "Internal Server Error")
			}
		}()

		c.Next()
	}
}
代码语言:javascript
复制
// New is the constructor of gee.Engine
func New() *Engine {
	engine := &Engine{router: newRouter()}
	engine.RouterGroup = &RouterGroup{engine: engine}
	engine.groups = []*RouterGroup{engine.RouterGroup}
	return engine
}

// Default use Logger() & Recovery middlewares
func Default() *Engine {
	engine := New()
	engine.Use(Logger(), Recovery())
	return engine
}

调用New()是没有日志中间件和错误恢复中间件的,只有调用Default()才有这两个中间件,且加在了Engine,即加在全局上。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-10-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 后端云 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为何要用web框架
  • 注册路由和路由发现
  • 上下文Context
  • 分组路由
  • 中间件
  • 错误恢复
相关产品与服务
消息队列 TDMQ
消息队列 TDMQ (Tencent Distributed Message Queue)是腾讯基于 Apache Pulsar 自研的一个云原生消息中间件系列,其中包含兼容Pulsar、RabbitMQ、RocketMQ 等协议的消息队列子产品,得益于其底层计算与存储分离的架构,TDMQ 具备良好的弹性伸缩以及故障恢复能力。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档