Web Server 设计详解
Golang 1046 字
Web 核心 Server
在 《Golang 主流 Web 框架路由实现分析》中我们注意到对于一个 Web 框架来说,至少提供了三个抽象:
- 代表服务器的吃哦续爱那个,我们这里称之为 Server
- 代表上下文的抽象,我们这里称之为 Context
- 路由树
从前面框架对比来看,对于一个 Web 框架来说,我们首先要有一个整体代表服务器的抽象,也就是 Server。
Server 从特性上来说,至少提供了三个功能:
- 生命周期管理:即启动、关闭。如果在后期,我们还要考虑增加生命周期回调特性
- 路由注册接口:提供路由注册功能
- 作为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)
}
优点:
- 用户可在使用的饿时候只需要调用 Http.ListenAndServe 就可以
- 和 HTTPS 协议完全无缝衔接
- 极简设计
缺点
- 难以控制生命周期,并且在控制生命周期的时候增加回调支持
- 缺乏控制力:如果将来希望支持优雅退出的功能,将会难以支持
方案 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")
}
优点:
- Server 既可以当成普通的 http.Handler 来使用,又可以作为一个独立的实体,拥有自己的管理生命周期能力
- 完全的控制
缺点:
- 如果用户不希望使用 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行啊问
- 查找路由树,并执行命中路由的代码