关于WEB框架
由于现在编程的语言变成go了,所以拆轮子系列,拆的轮子也是go方面的了,其实也不要紧,因为处理的思路是和语言无关的。gin是go的轻量级的web框架,轻量级意味着仅仅提供web框架应有的基础功能。我觉得看源码最好就是要有目标,看gin这个web框架,我的目标是:
- gin这个web框架是怎么实现web框架应有的基础功能的
- 代码上实现上有什么值得学习的地方。
一次请求处理的大体流程
如何找到入口
要知道一次请求处理的大体流程,只要找到web框架的入口即可。先看看gin文档当中最简单的demo。Run方法十分耀眼,点击去可以看到关键的http.ListenAndServe,这意味着Engine这个结构体,实现了ServeHTTP这个接口。入口就是Engine实现的ServeHTTP接口。
//我是最简单的demo
func main() {
r := gin.Default()
r.GET("/ping", func(c *gin.Context) {
c.JSON(200, gin.H{
"message": "pong",
})
c.Redirect(http.StatusMovedPermanently, "https://github.com/gin-gonic/gin")
})
r.Run() // listen and serve on 0.0.0.0:8080
}
//我是Run方法
func (engine *Engine) Run(addr ...string) (err error) {
defer func() { debugPrintError(err) }()
address := resolveAddress(addr)
debugPrint("Listening and serving HTTP on %s\n", address)
err = http.ListenAndServe(address, engine)
return
}
ServeHTTP
大体流程就如注释那样,那么的简单。这里值得关注的是,Context这个上下文对象是在对象池里面取出来的,而不是每次都生成,提高效率。可以看到,真正的核心处理流程是在handleHTTPRequest方法当中。
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// 从上下文对象池中获取一个上下文对象
c := engine.pool.Get().(*Context)
// 初始化上下文对象,因为从对象池取出来的数据,有脏数据,故要初始化。
c.writermem.reset(w)
c.Request = req
c.reset()
//处理web请求
engine.handleHTTPRequest(c)
//将Context对象扔回对象池了
engine.pool.Put(c)
}
handleHTTPRequest
下面的代码省略了很多和核心逻辑无关的代码,核心逻辑很简单:更具请求方法和请求的URI找到处理函数们,然后调用。为什么是处理函数们,而不是我们写的处理函数?因为这里包括了中间层的处理函数。
func (engine *Engine) handleHTTPRequest(context *Context) {
httpMethod := context.Request.Method
var path string
var unescape bool
// 省略......
// tree是个数组,里面保存着对应的请求方式的,URI与处理函数的树。
// 之所以用数组是因为,在个数少的时候,数组查询比字典要快
t := engine.trees
for i, tl := 0, len(t); i < tl; i++ {
if t[i].method == httpMethod {
root := t[i].root
// 找到路由对应的处理函数们
handlers, params, tsr := root.getValue(path, context.Params, unescape)
// 调用处理函数们
if handlers != nil {
context.handlers = handlers
context.Params = params
context.Next()
context.writermem.WriteHeaderNow()
return
}
// 省略......
break
}
}
// 省略......
}
值得欣赏学习的地方
路由处理
关键需求
先抛开gin框架不说,路由处理的关键需求有哪些?个人认为有以下两点
- 高效的URI对应的处理函数的查找
- 灵活的路由组合
gin的处理
核心思路
- 每一个路由对应的都有一个独立的处理函数数组
- 中间件与处理函数是一致的
- 利用树提供高效的URI对应的处理函数数组的查找
有趣的地方
RouterGroup对路由的处理
灵活的路由组合是通过将每一个URI都应用着一个独立的处理函数数组来实现的。对于路由组合的操作抽象出了RouterGroup结构体来应对。它的主要作用是:
- 将路由与相关的处理函数关联起来
- 提供了路由组的功能,这个是由于关联前缀的方式实现的
- 提供了中间件自由组合的功能:1. 总的中间件 2. 路由组的中间件 3.处理函数的中间件
路由组和处理函数都可以添加中间件这比DJango那种只有总的中间件要灵活得多。
中间件的处理
中间件在请求的时候需要处理,在返回时也可能需要做处理。如下图(图是django的)。
问题来了在gin中间件就是一个处理函数,怎么实现返回时的处理呢。仔细观察,上面图的调用,就是后进先出,是的每错答案就是:利用函数调用栈后进先出的特点,巧妙的完成中间件在自定义处理函数完成的后处理的操作。django它的处理方式是定义个类,请求处理前的处理的定义一个方法,请求处理后的处理定义一个方法。gin的方式更灵活,但django的方式更加清晰。
//调用处理函数数组
func (c *Context) Next() {
c.index++
s := int8(len(c.handlers))
for ; c.index < s; c.index++ {
c.handlers[c.index](c)
}
}
// 中间件例子
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
t := time.Now()
// before request
c.Set("example", "12345")
c.Next()
// 返回后的处理
latency := time.Since(t)
log.Print("latency: ", latency)
status := c.Writer.Status()
log.Println("status: ", status)
}
}
func main() {
r := gin.New()
r.Use(Logger())
r.GET("/test", func(c *gin.Context) {
example := c.MustGet("example").(string)
// it would print: "12345"
log.Println("example", example)
})
// Listen and serve on 0.0.0.0:8080
r.Run(":8081")
}
请求内容的处理与返回内容的处理
需求
- 获取路径当中的参数
- 获取请求参数
- 获取请求内容
- 将处理好的结果返回
Gin框架的实现思路
自己包装一层除了能提供体验一致的处理方法之外,如果对官方实现的不爽,可以替换掉,甚至可以加一层缓存处理(其实没必要,因为正常的使用,仅仅只会处理一次就够了)。
- 如果官方的http库能提供的,则在官方的http库只上包装一层,提供体验一致的接口。
- 官方http库不能提供的,则自己实现
关键结构体
type Context struct {
writermem responseWriter
Request *http.Request
// 传递接口,使用各个处理函数,更加灵活,降低耦合
Writer ResponseWriter
Params Params // 路径当中的参数
handlers HandlersChain // 处理函数数组
index int8 // 目前在运行着第几个处理函数
engine *Engine
Keys map[string]interface{} // 各个中间件添加的key value
Errors errorMsgs
Accepted []string
}
值得学习的点
在数量少的情况下用数组查找值,比用字典查找值要快
在上面对Context结构体的注释当中,可以知道Params其实是个数组。本质上可以说是key值的对应,为啥不用字典呢,而是用数组呢? 实际的场景,获取路径参数的参数个数不会很多,如果用字典性能反而不如数组高。因为字典要找到对应的值,大体的流程:对key进行hash —> 通过某算法找到对应偏移的位置(有好几种算法,有兴趣的可以去查查看) —> 取值。一套流程下来,数组在量少的情况下,已经遍历完了。
router.GET("user/:name/*action", func(c *gin.Context) {
name := c.Param("name")
action := c.Param("action")
message := name + " is222 " + action
c.String(http.StatusOK, message)
})
func (ps Params) Get(name string) (string, bool) {
for _, entry := range ps {
if entry.Key == name {
return entry.Value, true
}
}
return "", false
}
通过接口处理有所同,有所不同的场景
获取请求内容
对于获取请求内容这个需求面临着的场景。对于go这种静态语言来说,如果要对请求内容进行处理,就需要对内容进行反序列化到某个结构体当中,然而请求内容的形式多种多样,例如:JSON,XML,ProtoBuf等等。因此这里可以总结出下面的非功能性需求。
- 不同的内容需要不同的反序列化机制
- 允许用户自己实现反序列化机制
共同点都是对内容做处理,不同点是对内容的处理方式不一样,很容易让人想到多态这概念,异种求同。多态的核心就是接口,这时候需要抽象出一个接口。
type Binding interface {
Name() string
Bind(*http.Request, interface{}) error
}
将处理好的内容返回
请求内容多种多样,返回的内容也是一样的。例如:返回JSON,返回XML,返回HTML,返回302等等。这里可以总结出以下非功能性需求。
- 不同类型的返回内容需要不同的序列化机制
- 允许用户实现自己的序列化机制
和上面的一致的,因此这里也抽象出一个接口。
type Render interface {
Render(http.ResponseWriter) error
WriteContentType(w http.ResponseWriter)
}
接口定义好之后需要思考如何使用接口
思考如何优雅的使用这些接口
对于获取请求内容,在模型绑定当中,有以下的场景
- 绑定失败是用户自己处理还是框架统一进行处理
- 用户需是否需要关心请求的内容选择不同的绑定器
在gin框架的对于这些场景给出的答案是:提供不同的方法,满足以上的需求。这里的关键点还是在于使用场景是怎样的。
// 自动更加请求头选择不同的绑定器对象进行处理
func (c *Context) Bind(obj interface{}) error {
b := binding.Default(c.Request.Method, c.ContentType())
return c.MustBindWith(obj, b)
}
// 绑定失败后,框架会进行统一的处理
func (c *Context) MustBindWith(obj interface{}, b binding.Binding) (err error) {
if err = c.ShouldBindWith(obj, b); err != nil {
c.AbortWithError(400, err).SetType(ErrorTypeBind)
}
return
}
// 用户可以自行选择绑定器,自行对出错处理。自行选择绑定器,这也意味着用户可以自己实现绑定器。
// 例如:嫌弃默认的json处理是用官方的json处理包,嫌弃它慢,可以自己实现Binding接口
func (c *Context) ShouldBindWith(obj interface{}, b binding.Binding) error {
return b.Bind(c.Request, obj)
}
对于实现的结构体构造不一致的处理
将处理好的内容返回,实现的类构造参数都是不一致的。例如:对于文本的处理和对于json的处理。面对这种场景祭出的武器是:封装多一层,用于构造出相对于的处理对象。
//对于String的处理
type String struct {
Format string
Data []interface{}
}
//对于String处理封装多的一层
func (c *Context) String(code int, format string, values ...interface{}) {
c.Render(code, render.String{Format: format, Data: values})
}
//对于json的处理
JSON struct {
Data interface{}
}
//对于json的处理封装多的一层
func (c *Context) JSON(code int, obj interface{}) {
c.Render(code, render.JSON{Data: obj})
}
//核心的一致的处理
func (c *Context) Render(code int, r render.Render) {
c.Status(code)
if !bodyAllowedForStatus(code) {
r.WriteContentType(c.Writer)
c.Writer.WriteHeaderNow()
return
}
if err := r.Render(c.Writer); err != nil {
panic(err)
}
}
总结
这个看代码的过程是在有目标之后,按照官方文档的例子,一步一步的看的。然后再慢慢欣赏,这框架对于一些web框架常见的场景,它是怎么处理。这框架的代码量很少,而且写得十分的优雅,非常值得一看。