为何要用web框架
最有名的web框架莫过于Java的SpringBoot,Go的Gin。本篇以Go语言为例。其他语言或其他主流的web框架基于的设计方案基本都遵循这个。
其实不用web框架也可以进行web后端开发,比如下面的最简单的例子:
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)
}
}
测试得到下面的结果:
$ curl http://localhost:9999/
URL.Path = "/"
$ curl http://localhost:9999/hello
Header["Accept"] = ["*/*"]
Header["User-Agent"] = ["curl/7.54.0"]
那么为何要用web框架,或者说现在的主流web后端开发都要选定一个框架,然后再开发,就是为了提高效率,共通的业务以外的逻辑都由框架实现了,有了框架,开发只需要专注业务逻辑。
那么设计web框架的目的就很明确了,解决非业务的共通需求。那么有哪些此类的需求呢?就是框架要解决的问题。
- 注册路由和路由发现
- 快速路由算法(是框架理解上最复杂的地方,参考之前的两篇文章 http前缀树路由算法和Go源码分析 http基数树路由算法和Go源码分析,本篇略过。路由的性能非常重要,是框架间竞争的主要指标)
- 上下文Context
- 分组路由
- 中间件(比如日志中间件,校验中间件)
- 模板Template(现在开发都是前后端分离,模板很少实际开发中使用,所以这部分本篇略过)
- 错误恢复
所以一个好的web框架的核心在于:决定性能的路由算法。社区活跃度。特色功能。易用度。等等。
掌握的这些问题的解决方案,就可以自己设计web框架,或者在现有框架的基础上定制框架。下面逐一介绍:
注册路由和路由发现
在拥有框架之前,是通过http.HandleFunc关联URL和处理函数handler,再调用http.ListenAndServe(“:9999”, nil),http.ListenAndServe第二个参数留空。
第二个参数是一个Handler接口,需要实现方法 ServeHTTP,第二个参数也是基于net/http标准库实现Web框架的入口。
http.Handler接口的源码:
package http
type Handler interface {
ServeHTTP(w ResponseWriter, r *Request)
}
func ListenAndServe(address string, h Handler) error
除了调用http.HandleFunc,也可以通过下面的方式实现相同的目的。通过下面的Engine结构体来实现接口方法ServeHTTP,再将Engine传入http.ListenAndServe的第二个参数。
// 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框架的雏形调用样式。
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")
}
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
为何要有上下文:
- 对Web服务来说,无非是根据请求*http.Request,构造响应http.ResponseWriter。要构造一个完整的响应,需要考虑消息头(Header)和消息体(Body),而 Header 包含了状态码(StatusCode),消息类型(ContentType)等几乎每次请求都需要设置的信息。因此,如果不进行有效的封装,那么框架的用户将需要写大量重复,繁杂的代码,而且容易出错。针对常用场景,能够高效地构造出 HTTP 响应是一个好的框架必须考虑的点。现在前后端分离的web开发,返回的结构体往往是json数据类型,所以要对返回体作json数据格式的封装。
- 提供和当前请求强相关的信息的存放位置。比如:解析动态路由/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 这两个常用属性的直接访问。):
type Context struct {
// origin objects
Writer http.ResponseWriter
Req *http.Request
// request info
Path string
Method string
// response info
StatusCode int
}
提供了访问Query和PostForm参数的方法:
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)
}
提供修改返回的状态码和头的方法:
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响应的方法:
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的输入长度。
- 真实的业务场景中,往往某一组路由需要相似的处理。可以按分组配置中间件。
分组路由和嵌套分组的代码实现:
// 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结构体上
框架调用方式现在变为:
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的设计,增加中间件相关代码:
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标准库接口方法),增加中间件相关代码:
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数组最后一个。
比如:
//用户编写的中间件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上实现的。
错误恢复中间件:
// 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()
}
}
// 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,即加在全局上。
转载请注明来源,欢迎指出任何有错误或不够清晰的表达。可以邮件至 backendcloud@gmail.com