序言
一直想通过写一些技术博客的方式来提升自己,在拖延症的一次次犯病下,终于勇敢的迈出了第一步,开始码字写自己的第一个专栏,整个专栏是分享自己学习web框架原理的知识,会从0到1的搭建一个具备基本功能的web框架。
本专栏会参考一部分Go语言最流行的web框架Gin ,在每一个核心功能讲解我一般会分为三个部分
- 这个功能是为了解决什么问题
- Gin是怎么做的
- 我们是怎么实现的
在文章中也会提出很多问题,也是我自己在学习过程中遇到的问题,如果刚接触web框架原理的伙伴可以先思考一下再往下看。
开始
我们都知道,Web框架主要是用来处理 HTTP 请求的,那么一个合格的Web框架应该具备什么功能呢?可以自己回想一下,我们平时用过的Web框架都有什么样的功能,那些功能是我们觉得必备的?
我本人是Py转的Go,主要用过Django、Flask、Gin、Beego这四个Web框架,那么从我个人的使用感觉上,我觉得Web框架应该具备的一些功能是:
- 服务启动,监听端口
- 路由匹配功能
- 模板渲染功能
- 中间件功能(有的可能会叫插件功能)
- 对 cookies,headers 等处理机制
- 日志功能
- 文件处理和静态资源
我觉得上面列举的一些功能是比较常见的,一些流行的Web框架都有会支持上面这些功能,那么我们这个专栏就会带大家实现上面的这些功能,当然如果大家想增加一些其他的功能也是完全可以的,比如说可以支持鉴权、参数校验等等功能。
那么既然要设计一个我们自己的Web框架,首先我们来看一下Gin是怎么设计的
Gin核心抽象
IRoutes 接口
IRoutes 提供的是注册路由的抽象,Use方法提供了用户接入自定义逻辑的能力,就是我们常说的Middleware中间件机制或者说plugins 插件机制,还额外提供了静态文件的接口
Engine实现
Engine 是IRoutes 的实现,处理Http服务功能和逻辑处理功能都在这里面
- 实现了路由树功能,提供了注册和匹配路由的功能
- 本身可以作为一个Handler传递到http包,用于启动服务器
methodTrees 和 methodTree
methodTree 是指一颗路由树,而 methodTrees 指的就是森林,就是每一个HTTP方法都对应到一棵树,我们在前面Engine 的源码中也能看到初始化的时候也会make一个 methodTrees
HandlerFunc 和 HandlersChain
HandlerFunc 是抽象了处理逻辑,HandlersChain则是构造了责任链模式,会最终执行到封装了业务逻辑的HandleFunc
Context
Context 就是指请求的上下文,提供了丰富的API:
- 处理请求的API
- 处理响应的API
- 渲染页面的API
用一张图来表示Context的作用:
Gin的使用也很简单
func GetUser(ctx *gin.Context) {
panic("一些业务错误")
ctx.String(200, "hello, world")
}
func TestGin(t *testing.T) {
g := gin.Default()
// g 就是Engine
ctrl := &UserController{}
g.GET("/user/*", GetUser)
g.POST("/user/*", func(ctx *gin.Context) {
ctx.String(http.StatusOK, "hello %s", "world")
})
_ = g.Run(":8082")
}
上面就是我们Gin比较核心的抽象,如果有兴趣的朋友也可以去研究一下其他的Web框架,你就会发现,每一个Web框架都会有代表服务器的抽象(Engine)、代表路由的抽象(methodTree)、代表上下文的抽象(Context)、代表业务逻辑的抽象(HandleFunc),而我们造的自己的框架就是要建立这几个抽象。那么我们首先定义第一个代表服务器的抽象,我们这边称之为Server。
Server
Server从特性来说,至少需要提供三部分功能:
- 生命周期控制
- 路由注册接口
- 作为http包到Web框架的桥梁
所以我们定义一个Server的接口
type HandlerFunc func() // 避免编译不通过
type Server interface {
http.Handler
// Start 启动服务器
Start(addr string) error
// 注册路由 HandlerFunc 我们稍后再完善
addRoute(method string, path string, handler HandlerFunc)
}
注: http.Handler 也是一个接口,会有一个ServeHTTP 方法来处理http请求,如果不理解的可以先去学习一下Go语言的网络编程,就是net包和http包
接下来我们定义一个Http服务的实现,并实现Server接口
type HTTPServer struct {}
func (h *HTTPServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) {}
func (h *HTTPServer) Start(addr string) error {}
func (h *HTTPServer) addRoute(method string, path string, handler interface{}) {}
然后我们先实现这个服务的构造方法,这边我们采用Option 模式,我个人还是比较喜欢option 模式的,虽然现在不一定用得上,但是我们这样提前设计好会比较优雅(个人观点)
type Option func(server *HTTPServer)
func NewHttpServer(opts ...Option) *HTTPServer {
s := &HTTPServer{}
for _, opt := range opts {
opt(s)
}
return s
}
用户就可以通过NewHttpServer() 获得一个server的实例,然后调用Start()启动服务,ServeHTTP 方法来处理请求,那么我们接下来完善这两个方法
// Start 启动服务
func (h *HTTPServer) Start(addr string) error {
// ListenAndServe 需要的是端口号和实现了http.Handler 接口的实例,这里将自身传过去就行
return http.ListenAndServe(addr, h)
}
func (h *HTTPServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
writer.Write([]byte("simple! simple! simple!"))
}
到这里我们已经完成了server的启动服务了,那么我们测试一下
func TestHTTPServer_Start(t *testing.T) {
s := NewHttpServer()
err := s.Start(":8081")
if err != nil {
panic(err)
}
}
在浏览器上访问 localhost:8081 你会看到响应 simple! simple! simple!
具体代码实现看项目代码tag的v1版本 github.com/wufuliang/s…
好了,我们第一个server抽象以及完成了,是不是很简单!我刚学编程的时候就听别人说过,写一个项目就跟造火箭一样,看起来特别复杂,但是我们只有一个螺丝一个零件的拼接,慢慢就造好了,我们Web框架也一样,看起来很复杂(个人觉得)但是把整个框架拆分后,我们一个组件一个组件的完成,当最后完成的时候会发现Web框架也就这么回事。
接下来我们会来设计 Contex,大家可以思考一下几个问题:
- Context应该具备什么方法?
- Context应该有那些字段?
- Context是线程安全的吗?
声明:文章仅是学习心得和个人观点,绝对不是最优解,也欢迎大佬能提出更好的方案供大家学习交流