Beego
小案例
package main
import "github.com/beego/beego/v2/server/web"
func main() {
web.BConfig.CopyRequestBody = true
c := &UserController{}
web.Router("/user", c, "get:GetUser")
web.Router("/user", c, "post:CreateUser")
web.Run(":8080")
}
type UserController struct {
web.Controller
}
func (c *UserController) GetUser() {
c.Ctx.WriteString("hello, world")
}
func (c *UserController) CreateUser() {
u := &User{}
err := c.Ctx.BindJSON(u)
if err != nil {
c.Ctx.WriteString(err.Error())
return
}
c.Ctx.JSONResp(u)
}
type User struct {
Name string
}
Controller 抽象
Beego 是基于 MVC (Model-View-Controller) 的,所以它定义了一个核心接口 ControllerInterface。ControllerInterface 定义了一个控制器必须要解决什么问题。
同时 ControllerInterface 的默认实现 Controller 提供了实现自定义控制器的各种辅助方法,所以在 Beego 里面, 一般都是组合 Controller 来实现自己的 Controller。
// ControllerInterface is an interface to uniform all controller handler.
type ControllerInterface interface {
Init(ct *context.Context, controllerName, actionName string, app interface{})
Prepare()
Get()
Post()
Delete()
Put()
Head()
Patch()
Options()
Trace()
Finish()
Render() error
XSRFToken() string
CheckXSRFCookie() bool
HandlerFunc(fn string) bool
URLMapping()
}
注意到用户虽然被要求组合 Controller,但是路由注册和服务器启动是通过另外一套机制来完成的。
func main() {
web.BConfig.CopyRequestBody = true
c := &UserController{}
web.Router("/user", c, "get:GetUser")
web.Router("/user", c, "post:CreateUser")
web.Run(":8081")
}
HttpServer 和 ControllerRegister
ControllerInterface 可以看做核心接口,因为它直接体现了 Beego 的设计初衷:MVC 模式。同时它也是用户核心接入点。
但是如果从功能特性上来说,HttpServer 和 ControllerRegister 才是核心。
- HttpServer:代表一个
服务器
,大多数时候它就是一个进程。 - ControllerRegister:真正干活的人。注册路由,路由匹配和执行业务代码都是透过它来完成的。
// HttpServer defines beego application with a new PatternServeMux.
type HttpServer struct {
Handlers *ControllerRegister
Server *http.Server
Cfg *Config
LifeCycleCallbacks []LifeCycleCallback
}
// ControllerRegister containers registered router rules, controller handlers and filters.
type ControllerRegister struct {
...
}
Context 抽象
用户操作请求和响应是通过 Ctx 来达成的。它代表的是整个请求执行过程的上下文。
进一步,Beego 将 Context 细分了几个部分:
- Input:定义了很多和处理请求有关的方法
- Output:定义了很多和响应有关的方法
- Response:对 http.ResponseWriter 的二次封装
// Context Http request context struct including BeegoInput, BeegoOutput, http.Request and http.ResponseWriter.
// BeegoInput and BeegoOutput provides an api to operate request and response more easily.
type Context struct {
Input *BeegoInput
Output *BeegoOutput
Request *http.Request
ResponseWriter *Response
_xsrfToken string
}
func (c *UserController) CreateUser() {
u := &User{}
err := c.Ctx.BindJSON(u)
if err != nil {
c.Ctx.WriteString(err.Error())
return
}
_ = c.Ctx.JSONResp(u)
}
// Controller defines some basic http request handler operations, such as
// http context, template and view, session and xsrf.
type Controller struct {
// context data
Ctx *context.Context
Data map[interface{}]interface{}
...
}
路由树实现
Beego 的核心结构体是三个:
- ControllerRegister:类似于容器,放着所有的路由树
- 路由树是按照 HTTP method 来组织的, 例如 GET 方法会对应有一棵路由树
- Tree:它代表的就是路由树,在 Beego 里面,一棵路由树被看做是由子树组成的
- leafInfo:代表叶子节点
// ControllerRegister containers registered router rules, controller handlers and filters.
type ControllerRegister struct {
routers map[string]*Tree
enablePolicy bool
enableFilter bool
...
}
// Tree has three elements: FixRouter/wildcard/leaves
// fixRouter stores Fixed Router
// wildcard stores params
// leaves store the endpoint information
type Tree struct {
// prefix set for static router
prefix string
// search fix route first
fixrouters []*Tree
// if set, failure to match fixrouters search then search wildcard
wildcard *Tree
// if set, failure to match wildcard search
leaves []*leafInfo
}
type leafInfo struct {
// names of wildcards that lead to this leaf. eg, ["id" "name"] for the wildcard ":id" and ":name"
wildcards []string
// if the leaf is regexp
regexps *regexp.Regexp
runObject interface{}
}
Beego 的树定义,并没有采用 children 式的定义,而是采用递归式的定义,即一棵树是由根节点+子树
构成。
抽象总结
ControllerRegister 最为基础,它解决了路由注册和路由匹配这个基础问题。
Context 和 Controller 为用户提供了丰富 API,用于辅助构建系统。
HttpServer 作为服务器抽象,用于管理应用生命周期和资源隔离单位。
- ControllerRegister
- Context
- input
- output
- Controller
- HttpServer
- Context
Iris
小案例
package main
import (
"github.com/kataras/iris/v12"
)
func main() {
app := iris.New()
app.Get("/", func(ctx iris.Context) {
_, _ = ctx.HTML("Hello <strong>%s</strong>!", "World")
})
_ = app.Listen(":8080")
}
Application
Application 是 Iris 的核心抽象,它代表的是应用
。实际上这个语义更加接近 Beego 的 HttpServer和 Gin 的 Engine。
它提供了:
- 生命周期控制功能,如 Shutdown 等方法
- 注册路由的 API
路由相关
lris 的设计非常复杂。在 Beego 和 Gin 里面能够明显看到路由树的痕迹,但是在 lris 里面就很难看出来。
和处理路由相关的三个抽象:
- Route:直接代表了已经注册的路由。在 Beego 和 Gin 里面,对应的是路由树的节点
- APIBuilder:创建 Route 的 Builder 模式,Party 也是它创建的
- repository:存储了所有的 Routes,有点接近 Gin 的 methodTrees 的概念
总结:设计过于复杂,职责不清晰,不符合一般人的直觉,新人学习和维护门槛高。
// repository passed to all parties(subrouters), it's the object witch keeps
// all the routes.
type repository struct {
routes []*Route
pos map[string]int
}
// Route contains the information about a registered Route.
// If any of the following fields are changed then the
// caller should Refresh the router.
type Route struct {
Name string `json:"name"` // "userRoute"
Method string `json:"method"` // "GET"
Context 抽象
Context 也是代表上下文。
Context 本身也是提供了各种处理请求和响应的方法。
基本上和 Beego 和 Gin 的 Context 没啥区别。
比较有特色的是它的 Context 支持请求级别的添加 Handler,即 AddHandler 方法。
// AddHandler can add handler(s)
// to the current request in serve-time,
// these handlers are not persistenced to the router.
//
// Router is calling this function to add the route's handler.
// If AddHandler called then the handlers will be inserted
// to the end of the already-defined route's handler.
func (ctx *Context) AddHandler(handlers ...Handler) {
ctx.handlers = append(ctx.handlers, handlers...)
}
抽象总结
- Route Party APIBuilder
- Handler
- Context
- Application
Echo
小案例
package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
func main() {
// Echo instance
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Routes
e.GET("/", hello)
// Start server
e.Logger.Fatal(e.Start(":8080"))
}
// Handle
func hello(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
}
Echo
Echo 是它内部的一个结构体,类似于 Beego 的 HttpServer 和 Gin 的 Engine:
- 暴露了注册路由的方法,但是它并不是路由树的载体
- 生命周期管理:如 Shutdown 和 Start 等方法
在 Echo 里面有两个相似的字段:
- Route:这其实就是代表路由树
- Routers:这代表的是根据 Host 来进行分组组织,可以看做是近似于 namespace 之类的概念,既是一种组织方式,也是一种隔离机制
maxParam *int
router *Router
routers map[string]*Router
pool sync.Pool
Route 和 node
Router 代表的就是路由树,node 代表的是路由树上的节点。
node 里面有一个很有意思的设计:staticChildren、 paramChild 和 anyChild。利用这种设计可以轻松实现路由优先级和路由冲突检测。
它里面还有一个字段叫做 echo 维护的是使用 Route 的是 echo。这种设计形态在别的地方也能见到,比如说在 sql.Tx 里面维持了一个 sql.DB 的实例。
// Router is the registry of all registered routes for an `Echo` instance for
// request matching and URL path parameter parsing.
Router struct {
tree *node
routes map[string]*Route
echo *Echo
}
node struct {
kind kind
label byte
prefix string
parent *node
staticChildren children
originalPath string
methods *routeMethods
paramChild *node
anyChild *node
paramsCount int
// isLeaf indicates that node does not have child routes
Context
一个大而全的接口,定义了处理请求和响应的各种方法。
和 Beego、Gin、Iris 的 Context 没有什么区别。
核心抽象
- Route & node
- HandlerFunc
- Context
- Echo
框架对比
所以实际上我们要造一个 Web 框架,就是要建立我们自己的这几个抽象。
Web 框架小结
- Web 框架拿来做什么?处理 HTTP 请求,为用户提供便捷 API,为用户提供无侵入式的插件机制,提供如上传下载等默认功能
- 为什么都已经有了 http 包,还要开发 Web 框架?高级路由功能、封装 HTTP 上下文以提供简单 API、封装 Server 以提供生命周期控制、设计插件机制以提供无侵入式解决方案
- Web 框架的核心?路由树、上下文 Context、 Server(按照我理解的重要性排序)
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。