Web Server 设计详解

Golang 1046 字

Web 核心 Server

在 《Golang 主流 Web 框架路由实现分析》中我们注意到对于一个 Web 框架来说,至少提供了三个抽象:

  • 代表服务器的吃哦续爱那个,我们这里称之为 Server
  • 代表上下文的抽象,我们这里称之为 Context
  • 路由树

从前面框架对比来看,对于一个 Web 框架来说,我们首先要有一个整体代表服务器的抽象,也就是 Server。

Server 从特性上来说,至少提供了三个功能:

  1. 生命周期管理:即启动、关闭。如果在后期,我们还要考虑增加生命周期回调特性
  2. 路由注册接口:提供路由注册功能
  3. 作为http包到 web 框架的桥梁

http.Handler接口

http 暴露了一个接口,Handler,它是我们引入自定义 Web 框架的相关连接点,自定义 Server 有两种实现方案:

方案 1. 只组合 http.Handler。

type Server interface {
    http.Handler
}

func TestServer(t * testing.T) {
    var s Server
    http.ListenAndServe(":8080", s)
    http.ListenAndServeTLS(":4000", "cretfile", "keyfile", s)
}

优点:

  1. 用户可在使用的饿时候只需要调用 Http.ListenAndServe 就可以
  2. 和 HTTPS 协议完全无缝衔接
  3. 极简设计

缺点

  1. 难以控制生命周期,并且在控制生命周期的时候增加回调支持
  2. 缺乏控制力:如果将来希望支持优雅退出的功能,将会难以支持

方案 2.组合 http.Handler 并增加 Start 方法

type Server interface {
    http.Handler
    Start(addr string) error
}

func TestServer(t * testing.T) {
    var s Server
    http.ListenAndServe(":8080", s)
    http.ListenAndServeTLS(":4000", "cretfile", "keyfile", s)

  s.Start(":8881")
}

优点:

  1. Server 既可以当成普通的 http.Handler 来使用,又可以作为一个独立的实体,拥有自己的管理生命周期能力
  2. 完全的控制

缺点:

  1. 如果用户不希望使用 ListenAndServeTLS,那么 Server 需要提供 HTTPS 的支持

这两个实现方案都直接耦合了 Go 自带的 http 包,如果我们希望切换为 fasthttp 或者类似的 http 包,则会非常困难。

HTTPServer 实现

// 确保 HTTPServer 实现了 Server 接口
var _ Server = &HTTPServer {}

type HTTPServer struct {

}

func (s * HttpServer) ServeHTTP(writer http.ResponseWriter, request *http.Request) {

}

func (s * HttpServer) Start (addr string) error {
        return http.ListenAndServe(addr, s)
}

func Serve(l net.Listener, handler Handler) error {
        srv := &Server{Handler: handler}
        return srv.Serve(l)
}

该实现直接使用 http.ListenAndServe 来启动,后续可以根据需要替换为:

  • 内部创建 http.Server 来启动
  • 使用 http.Serve 来启动,环球聚更大得我灵活性,比如讲端口监听和服务器启动分离等呢个

ServeHTTP 则是我们整个 Web 框架的核心入口,我们将在整个方法内部完成:

  • Context 构建
  • 路由匹配
  • 执行业务逻辑

注册路由

暂时我们可以考虑站在用户的角度,考虑如何注册路由,在《Golang 主流 Web 框架路由实现分析》文章中,详细介绍了Beego、Gin 等框架的路由注册实现,我们可以参考一下。

大致上分为两类方法:

  • 针对任意的方法的:如 Gin 和 Iris 的 Handle 方法,Echo 的 Add 方法。
  • 针对不同 HTTP 方法的:如 Get、Post、Delete,这类方法基本很伤都是委托给前一类方法

所以实际上,核心方法只需要一个,例如 Handle。其他的方法都建立在这上面,在这里我们使用 AddRoute 作为核心方法,其函数只要接受三个参数。

  • method:请求方法例如 Get、Put、Post
  • path:请求路径
  • handleFunc: 业务逻辑

type HandleFunc func()

type Server interface {
    http.Handler
    Start(addr string) error

    AddRouter(method string, path string, handleFunc HandleFunc)
}

这里我们需要思考一下:

  • AddRoute 方法只接受一个 HandleFunc,因为我希望它只注册业务逻辑。即便真有多个场景,用户可以自己组合成一个。
  • 如果允许注册多个,那么在实现的时候需要考虑,其中一个失败了,是否还允许继续执行下去;反过来,如果其中一个 HandleFunc 中药终端执行,怎么中断?
  • 这里我们采用新的名字 AddRoute,我认为更加贴合方法本意,Handle 看上去是处理什么东西,而是实质上这里只是注册路由。

其他框架都是允许注册多个的,但是其实用起来体验不会很好

  • Gin 和 Iris 最后一个是不定参数,那么完全可以一个都不传,如 PUT(“path”),这个在编译器无法发现
  • Echo 则是存在我希望 h 传入 nil 的可能。实际上 Echo 是讲中间件真粗额逻辑和路由逻辑合并在一起了。

回到我们设计的 AddRoute 的设计上,这种设计思路很常用,针对不同的 HTTP 方法的注册 API,我们都可以委托给 Handle 方法,也就是我们的 AddRoute。

func (s *HttpServer) Post(path string, handler handleFunc) {
    s.AddRoute(http.MethodPost, path, handler);
}

Context 设计

ServeHTTP 方法作为 http 包与 web 框架的关联点,需要在 ServeHTTP 内部,执行:

  • 构建起Web 框架的杀好难过i行啊问
  • 查找路由树,并执行命中路由的代码
maksim
Maksim(一笑,吡罗),PHPer,Goper
OωO
开启隐私评论,您的评论仅作者和评论双方可见