【零】 web框架概述以及Server定义

153 阅读6分钟

序言

一直想通过写一些技术博客的方式来提升自己,在拖延症的一次次犯病下,终于勇敢的迈出了第一步,开始码字写自己的第一个专栏,整个专栏是分享自己学习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 接口

image-20221007190318330.png

IRoutes 提供的是注册路由的抽象,Use方法提供了用户接入自定义逻辑的能力,就是我们常说的Middleware中间件机制或者说plugins 插件机制,还额外提供了静态文件的接口

Engine实现

image-20221007191723301.png EngineIRoutes 的实现,处理Http服务功能和逻辑处理功能都在这里面

  • 实现了路由树功能,提供了注册和匹配路由的功能
  • 本身可以作为一个Handler传递到http包,用于启动服务器

methodTrees 和 methodTree

image-20221010184308203.png

methodTree 是指一颗路由树,而 methodTrees 指的就是森林,就是每一个HTTP方法都对应到一棵树,我们在前面Engine 的源码中也能看到初始化的时候也会make一个 methodTrees

HandlerFunc 和 HandlersChain

handlerFunc.png

HandlerFunc 是抽象了处理逻辑,HandlersChain则是构造了责任链模式,会最终执行到封装了业务逻辑的HandleFunc

handlerChild.png

Context

context.png

Context 就是指请求的上下文,提供了丰富的API:

  • 处理请求的API
  • 处理响应的API
  • 渲染页面的API

用一张图来表示Context的作用:

context_img.png

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是线程安全的吗?

声明:文章仅是学习心得和个人观点,绝对不是最优解,也欢迎大佬能提出更好的方案供大家学习交流