PART1. 学习路线
PART2. Web框架的核心
2.1 Web框架的构成
在框架对比的时候,我们注意到对于一个Web框架来说,至少要提供三个抽象:
2.2 Server
从前面框架对比来看,对于一个Web框架来说,我们首先要有一个整体代表服务器的抽象 ,也就是Server.
Server从特性上来说,至少要提供三部分功能:
生命周期控制 :即启动、关闭.如果在后期,还要考虑增加生命周期回调特性
如果在你的代码中,没有这个代表服务器的抽象 (换言之,你的代码里没有定义服务器这个概念,或者说没有用于表示服务器的类),那么很多功能你无法实现.例如上边说过的生命周期控制,你的代码里都没有服务器这个"实体"(这里的实体概念上有点像CRUD中的业务实体),自然也就没有控制这个实体"成往怀灭"的能力的.
同样的,路由注册功能没有Server这个抽象也无法实现.因为不同的路由可能要注册到不同的Server上.回忆一下之前讲过的多端口进程的例子.
PART3. http.Handler接口
那么问题来了,怎么接入go原生的net/http
包?
假设我们现在已经定义了一个Server结构体(当然我们现在还不知道它应该有什么成员属性和成员方法),那我们该如何让它和原生的net/http
包交互?
先来看看原生的net/http
包如何启动一个Server:
Copy package server
import (
"net/http"
"testing"
)
func TestServer (t * testing . T ) {
http. ListenAndServe ( ":8085" , nil )
}
这样就已经启动了一个HTTP服务器.那么答案就很明确了:用我们的Server替换掉代码中的nil,那我们的Server就和原生的 net/http
包交互了 .而我们要做的就是在请求与http包之间,做一个WEB框架.
PART4. Server Interface的定义
4.1 我们需要什么?
Copy func ListenAndServe (addr string , handler Handler ) error {
server := & Server {Addr: addr, Handler: handler}
return server. ListenAndServe ()
}
从http.ListenAndServe()
方法可以看出,我们需要实现一个Handler类型:
Copy type Handler interface {
ServeHTTP ( ResponseWriter , * Request )
}
4.2 构建Server Interface的方案:组合http.Handler
4.2.1 组合http.Handler
通常而言,设计一个稍微复杂的系统,总是需要先使用接口来定义行为,而非直接写实现类的.
工程结构:
Copy (base) root@yuanhong 01-composingHandlerIntoServer % tree ./
./
├── server.go
└── server_test.go
0 directories, 2 files
Copy package composingHandlerIntoServer
import "net/http"
type Server interface {
// Handler 组合http.Handler接口
http . Handler
}
server_test.go
:使用Server接口
Copy package composingHandlerIntoServer
import (
"net/http"
"testing"
)
func TestServer (t * testing . T ) {
var s Server
http. ListenAndServe ( ":8085" , s)
http. ListenAndServeTLS ( ":443" , "" , "" , s)
}
4.2.2 组合http.Handler
方案的优缺点
这样设计的优点:
用户在使用的时候只需要调用http.ListenAndServe
就可以
这样设计的缺点:
难以控制生命周期,并且很难在控制生命周期的时候增加回调支持
比如在端口监听之后,服务启动之前,想要做一些操作,这样的设计是不支持的.因为从端口启动开始,后续的所有工作都是http包来完成的.我们肯定不能去魔改http包
缺乏控制力:如果将来希望支持优雅退出的功能,将难以支持
只能通过http.Server.Shutdown()
/http.Server.Close()
等http包里的方法去实现,没办法自己实现.因为这样的设计即使你给自定义Server接口的实现类设计了Shutdown()
方法,也调用不到.本质上还是因为从端口启动开始,后续的所有工作都是http包来完成的 ,我们无法干涉这其中的步骤
4.3 构建Server Interface的方案:组合http.Handler
并增加Start()
方法
4.3.1 增加Start()
方法
在这个方案中,我们希望使用者调用我们的Server.Start()
方法来启动HTTP服务器.
工程结构如下:
Copy (base) root@yuanhong 02-composingHandlerAndAddStart % tree ./
./
├── httpServer.go
├── httpServer_test.go
└── serverInterface.go
0 directories, 3 files
serverInterface.go
:定义Server接口
Copy package composingHandlerAndAddStart
import "net/http"
type Server interface {
http . Handler
Start (addr string ) error
}
httpServer.go
:Server接口的HTTP实现
Copy package composingHandlerAndAddStart
import (
"net"
"net/http"
)
type HTTPServer struct {
}
func (s * HTTPServer ) ServeHTTP (w http . ResponseWriter , r * http . Request ) {
// TODO implement me
panic ( "implement me" )
}
func (s * HTTPServer ) Start (addr string ) error {
l, err := net. Listen ( "tcp" , addr)
if err != nil {
return err
}
// 在监听端口之后,启动服务之前做一些操作
// 例如在微服务框架中,启动服务之前需要注册服务
return http. Serve (l, s)
}
可以看到,这样的设计就支持注册after start回调了.大白话来说就是:相比于直接调用http.ListenAndServe()
的方案,这种方案拆解了监听和启动服务这2个步骤,使得在这2个步骤之间可以做一些操作了.
httpServer_test.go
:使用HTTP Server
Copy package composingHandlerAndAddStart
import "testing"
func TestServer (t * testing . T ) {
s := & HTTPServer {}
s. Start ( ":8084" )
}
4.3.2 支持HTTPS
可以使用装饰器模式,基于HTTPServer
结构体进行再封装:
工程结构如下:
Copy (base) root@yuanhong 02-composingHandlerAndAddStart % tree ./
./
├── httpServer.go
├── httpServer_test.go
├── httpsServer.go
└── serverInterface.go
0 directories, 4 files
httpsServer.go
:Server接口的HTTPS实现
Copy package composingHandlerAndAddStart
type HTTPSServer struct {
HTTPServer
}
这个不是现在的重点,只是提一下可以这么去设计.
4.3.3 在Start()
方法内部创建一个原生的http.Server
对象
Copy (base) root@yuanhong 02-composingHandlerAndAddStart % tree ./
./
├── createHttpServerInStart.go
├── httpServer.go
├── httpServer_test.go
├── httpsServer.go
└── serverInterface.go
0 directories, 5 files
createHttpServerInStart.go
:Server接口的HTTP实现
Copy package composingHandlerAndAddStart
import (
"net"
"net/http"
)
type HTTPServerWithOriginHttpServer struct {
}
func (s * HTTPServerWithOriginHttpServer ) ServeHTTP (w http . ResponseWriter , r * http . Request ) {
// TODO implement me
panic ( "implement me" )
}
func (s * HTTPServerWithOriginHttpServer ) Start (addr string ) error {
originServer := http . Server {
Addr: addr,
Handler: s,
}
l, err := net. Listen ( "tcp" , addr)
if err != nil {
return err
}
return originServer. Serve (l)
}
4.3.4 组合http.Handler
并增加Start()
方法的优缺点
这样设计的优点:
Server
接口的实现既可以当成普通的http.Handler
接口的实现来使用,又可以作为一个独立的实体,拥有自己的管理生命周期的能力
这样设计的缺点:
如果用户不希望通过调用http.ListenAndServeTLS()
的方式来实现HTTPS支持,那么Server
接口需要提供HTTPS的支持(也就是HTTPS的实现)
PART5. ServeHTTP的实现
ServeHTTP()
方法是处理请求的入口.这里说的"入口"指的是你设计的WEB框架的入口.在这个方法中需要完成3个操作:
PART6. 注册路由API设计
上文说过,ServeHTTP()
方法作为WEB框架的入口,需要完成3个操作:
路由匹配 :首先需要把路由注册到Server上,才能考虑匹配的过程
首先需要在接口中定义路由注册的行为.参考GIN的IRoutes接口/Iris的APIBuilder结构体/Echo的Echo结构体,他们都是把注册路由的行为定义在接口上的.
在之前的课程分析GIN框架时说过,实际上Engine结构体的GET()
/POST()
等HTTP动词方法,本质上调用的都是Handle()
方法.所以注册路由的方法分为2类:
针对任意方法的 :如Gin和Iris的Handle()
方法、Echo的Add()
方法
针对不同HTTP方法的 :如GET()
、POST()
、DELETE()
这一类方法基本上都是委托给前一类方法
所以实际上核心方法只需要有一个,例如Handle()
方法.其它的方法都建立在这上面.
PART7. AddRoute方法
在本例中,我们将上文的Handle()
方法命名为AddRoute()
方法.从命名上来讲,Handle()
表示"处理"的含义,而实际上这个方法要完成的操作是注册路由 ,而非处理,因此用AddRoute()
更合适.
7.1 定义注册路由行为
Copy (base) root@yuanhong 03-serveHTTP % tree ./
./
├── handleFunc.go // 定义业务逻辑函数类型
├── httpServer.go // 定义HTTP服务器
└── serverInterface.go // 定义HTTP服务器接口
0 directories, 3 files
Copy package serveHTTP
// HandleFunc 定义业务逻辑函数类型
// Tips: 该类型应与http.HandlerFunc类型一致 此处只是暂时定义一下这个类型
type HandleFunc func ()
注:此处将其命名为HandleFunc
而非HandlerFunc
,是因为handle
是动词而handler
是名词,后边跟的Func
很明显是名词.使用动名词的组合更加符合GO的命名风格.
serverInterface.go
:定义HTTP服务器接口
Copy package serveHTTP
import "net/http"
// Server WEB服务器接口
type Server interface {
// Handler 组合http.Handler接口
http . Handler
// Start 启动WEB服务器
Start (addr string ) error
// AddRoute 注册路由
AddRoute (method string , path string , handleFunc HandleFunc )
}
Copy package serveHTTP
import (
"net"
"net/http"
)
// 为确保HTTPServer结构体为Server接口的实现而定义的变量
var _ Server = & HTTPServer {}
// HTTPServer HTTP服务器
type HTTPServer struct {
}
// ServeHTTP WEB框架入口
func (s * HTTPServer ) ServeHTTP (w http . ResponseWriter , r * http . Request ) {
// TODO implement me
panic ( "implement me" )
}
// Start 启动WEB服务器
func (s * HTTPServer ) Start (addr string ) error {
l, err := net. Listen ( "tcp" , addr)
if err != nil {
return err
}
// 在监听端口之后,启动服务之前做一些操作
// 例如在微服务框架中,启动服务之前需要注册服务
return http. Serve (l, s)
}
// AddRoute 注册路由
func (s * HTTPServer ) AddRoute (method string , path string , handleFunc HandleFunc ) {
// TODO: implement me
panic ( "implement me" )
}
7.2 定义Context
其实相比7.1小节,就是增加了一个Context类型的定义,并修改了HandleFunc类型的入参
Copy (base) root@yuanhong 03 - serveHTTP % tree . /
. /
├── context. go // 定义HandleFunc的上下文
├── handleFunc. go
├── httpServer. go
└── serverInterface. go
0 directories, 4 files
context.go
:定义HandleFunc的上下文
Copy package serveHTTP
// Context HandleFunc的上下文
type Context struct {
}
Copy package serveHTTP
// HandleFunc 定义业务逻辑函数类型
// Tips: 该类型应与http.HandlerFunc类型一致 此处只是暂时定义一下这个类型
type HandleFunc func (ctx Context )
其他2个文件没有变化.
7.3 思考:是否需要AddRoutes()
方法?
7.3.1 如果需要AddRoutes()方法,该如何实现?
来看GIN的Engine.Handle()
方法:
Copy func (group * RouterGroup ) Handle (httpMethod, relativePath string , handlers ... HandlerFunc ) IRoutes {
if matched := regEnLetter. MatchString (httpMethod); ! matched {
panic ( "http method " + httpMethod + " is not valid" )
}
return group. handle (httpMethod, relativePath, handlers)
}
可以看到该方法是支持1次注册多个逻辑处理函数到路由上的 .如果我们要是也支持这个功能,该如何实现?
Copy (base) root@yuanhong 04-addRoutes % tree ./
./
├── context.go
├── handleFunc.go
├── httpServer.go
└── serverInterface.go
0 directories, 4 files
Copy package addRoutes
import "net/http"
// Server WEB服务器接口
type Server interface {
// Handler 组合http.Handler接口
http . Handler
// Start 启动WEB服务器
Start (addr string ) error
// AddRoute 注册路由
AddRoute (method string , path string , handleFunc HandleFunc )
// AddRoutes 支持1个路由对应多个处理函数的注册路由
AddRoutes (method string , path string , handles ... HandleFunc )
}
Copy package addRoutes
import (
"net"
"net/http"
)
// 为确保HTTPServer结构体为Server接口的实现而定义的变量
var _ Server = & HTTPServer {}
// HTTPServer HTTP服务器
type HTTPServer struct {
}
// ServeHTTP WEB框架入口
func (s * HTTPServer ) ServeHTTP (w http . ResponseWriter , r * http . Request ) {
// TODO implement me
panic ( "implement me" )
}
// Start 启动WEB服务器
func (s * HTTPServer ) Start (addr string ) error {
l, err := net. Listen ( "tcp" , addr)
if err != nil {
return err
}
// 在监听端口之后,启动服务之前做一些操作
// 例如在微服务框架中,启动服务之前需要注册服务
return http. Serve (l, s)
}
// AddRoute 注册路由
func (s * HTTPServer ) AddRoute (method string , path string , handleFunc HandleFunc ) {
// TODO: implement me
panic ( "implement me" )
}
// AddRoutes 支持1个路由对应多个处理函数的注册路由
func (s * HTTPServer ) AddRoutes (method string , path string , handles ... HandleFunc ) {
// TODO: implement me
panic ( "implement me" )
}
7.3.2 从使用的角度思考是否需要AddRoutes()方法
写个单元测试,演示一下如何使用AddRoutes()
方法:
Copy (base) root@yuanhong 04-addRoutes % tree ./
./
├── context.go
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go // 演示如何调用Server实现的单测
└── serverInterface.go
0 directories, 5 files
httpServer_test.go
:演示如何调用Server实现的单测
Copy package addRoutes
import (
"fmt"
"net/http"
"testing"
)
func TestServer (t * testing . T ) {
s := & HTTPServer {}
// 注册多个处理函数到路由
handleFoo := func (ctx Context ) { fmt. Println ( "处理第1件事" ) }
handleBar := func (ctx Context ) { fmt. Println ( "处理第2件事" ) }
s. AddRoutes (http.MethodGet, "/getUser" , handleFoo, handleBar)
_ = s. Start ( ":8080" )
}
其他文件无变化.
7.3.3 尝试使用AddRoute()方法实现同等效果
完全可以将7.3.2示例中的handleFoo
和handleBar
封装成1个函数,然后调用AddRoute()
方法,其实是可以达到同等效果的.
Copy (base) root@yuanhong 03-serveHTTP % tree ./
./
├── context.go
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go // 演示如何调用Server实现的单测
└── serverInterface.go
0 directories, 5 files
httpServer_test.go
:演示如何调用Server实现的单测
Copy package serveHTTP
import (
"fmt"
"net/http"
"testing"
)
func TestServer (t * testing . T ) {
s := & HTTPServer {}
// 注册1个处理函数到路由
handleFoo := func (ctx Context ) { fmt. Println ( "处理第1件事" ) }
handleBar := func (ctx Context ) { fmt. Println ( "处理第2件事" ) }
// 将2个处理函数封装为1个处理函数
handleAssemble := func (ctx Context ) {
handleFoo (ctx)
handleBar (ctx)
}
s. AddRoute (http.MethodGet, "/getUser" , handleAssemble)
_ = s. Start ( ":8080" )
}
也就是说,让使用者自行将多个处理函数封装为1个,然后交给WEB框架注册路由 .
7.3.4 结论
不需要支持AddRoutes()
方法.
理由:把"将多个函数组合成1个函数"的职责交由使用者去实现即可
而且,如果需要支持AddRoutes()
方法,还会带来一些其他问题:
如果允许注册多个函数,那么在实现的时候就要考虑,若其中一个HandleFunc执行失败了,是否还允许继续执行后续的HandleFunc?反之,如果其中一个HandleFunc要中断后续执行,该怎么中断?
中断后续执行:例如共有5个HandleFunc,但执行完第2个之后,业务逻辑上判断后续的HandleFunc不需要执行了,即为中断后续执行
站在使用者的视角上来看,由于AddRoutes()
方法中的handles
是不定长参数,因此使用时可能出现一个函数都不传的情况.这种情况在编译期间不会被发现
另外,Echo框架虽然不支持传入多个注册函数的功能,但它保留了传入的HandleFunc为nil的可能性.实际上Echo是将中间件注册逻辑和路由注册逻辑合并在了一起 :
Copy // GET registers a new GET route for a path with matching handler in the router
// with optional route-level middleware.
func (e * Echo ) GET (path string , h HandlerFunc , m ... MiddlewareFunc ) * Route {
return e. Add (http.MethodGet, path, h, m ... )
}
7.4 AddRoute方法的衍生方法
针对不同HTTP方法的注册API,都可以委托给 AddRoute()
方法 .这种设计思路很常用.
在我们的设计中,我们不认为针对HTTP动词的路由注册方法是框架的核心方法 .保持接口简洁,但这并不意味着实现不能复杂 .
因此,我们将GET()
/POST()
等方法实现在HTTPServer
结构体上.
Copy (base) root@yuanhong 03-serveHTTP % tree ./
./
├── context.go
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
└── serverInterface.go
0 directories, 5 files
Copy package serveHTTP
import (
"net"
"net/http"
)
// 为确保HTTPServer结构体为Server接口的实现而定义的变量
var _ Server = & HTTPServer {}
// HTTPServer HTTP服务器
type HTTPServer struct {
}
// ServeHTTP WEB框架入口
func (s * HTTPServer ) ServeHTTP (w http . ResponseWriter , r * http . Request ) {
// TODO implement me
panic ( "implement me" )
}
// Start 启动WEB服务器
func (s * HTTPServer ) Start (addr string ) error {
l, err := net. Listen ( "tcp" , addr)
if err != nil {
return err
}
// 在监听端口之后,启动服务之前做一些操作
// 例如在微服务框架中,启动服务之前需要注册服务
return http. Serve (l, s)
}
// AddRoute 注册路由
func (s * HTTPServer ) AddRoute (method string , path string , handleFunc HandleFunc ) {
// TODO: implement me
panic ( "implement me" )
}
// GET 注册GET请求路由
func (s * HTTPServer ) GET (path string , handleFunc HandleFunc ) {
s. AddRoute (http.MethodGet, path, handleFunc)
}
// POST 注册POST请求路由
func (s * HTTPServer ) POST (path string , handleFunc HandleFunc ) {
s. AddRoute (http.MethodPost, path, handleFunc)
}
其他文件代码无变化.
这样设计的话,就需要注意在使用时,初始化HTTPServer结构体时,不能将其类型声明为Server接口:
Copy package serveHTTP
import (
"fmt"
"net/http"
"testing"
)
func TestServer (t * testing . T ) {
// Tips: 初始化HTTPServer结构体时,不能将其类型声明为Server接口的实现,因为一些方法被我们定义在了
// Tips: 实现上,而不是在接口上,所以不能将其类型声明为Server接口的实现.即:不能写成如下形式:
// var s Server = &HTTPServer{}
s := & HTTPServer {}
// 注册1个处理函数到路由
handleFoo := func (ctx Context ) { fmt. Println ( "处理第1件事" ) }
handleBar := func (ctx Context ) { fmt. Println ( "处理第2件事" ) }
// 将2个处理函数封装为1个处理函数
handleAssemble := func (ctx Context ) {
handleFoo (ctx)
handleBar (ctx)
}
s. AddRoute (http.MethodGet, "/getUser" , handleAssemble)
_ = s. Start ( ":8080" )
}
AddRoute()
方法最终会和路由树交互,我们后面再考虑.
如上图示,保持核心API的方法较少,而衍生API的方法较多 ,是我们这个框架的核心设计思想.因此在设计的时候,需要区分核心API和衍生API.
PART8. ServeHTTP()
方法
ServeHTTP()
方法是作为http
包与WEB框架的关联点,需要在ServeHTTP()
方法内部完成如下操作:
说句再直白一点的话:请求进来了就执行ServeHTTP()
方法
8.1 定义Context
那么问题来了,一个HandleFunc的上下文是什么?首先至少肯定有请求 和响应 .因此我们的Context就需要能够将请求 和响应 "带"到具体的HandleFunc中去.
Copy (base) root@yuanhong 03-serveHTTP % tree ./
./
├── context.go
├── handleFunc.go
├── httpServer.go
├── httpServer_test.go
└── serverInterface.go
0 directories, 5 files
Copy package serveHTTP
import "net/http"
// Context HandleFunc的上下文
type Context struct {
// Req 请求
Req * http . Request
// Resp 响应
Resp http . ResponseWriter
}
httpServer.go
:修改了ServeHTTP()
方法的代码.增加了构建上下文的部分.
Copy package serveHTTP
import (
"net"
"net/http"
)
// 为确保HTTPServer结构体为Server接口的实现而定义的变量
var _ Server = & HTTPServer {}
// HTTPServer HTTP服务器
type HTTPServer struct {
}
// ServeHTTP WEB框架入口
func (s * HTTPServer ) ServeHTTP (w http . ResponseWriter , r * http . Request ) {
// 构建上下文
ctx := & Context {
Req: r,
Resp: w,
}
// 查找路由树并执行命中的业务逻辑
s. serve (ctx)
// TODO implement me
panic ( "implement me" )
}
// serve 查找路由树并执行命中的业务逻辑
func (s * HTTPServer ) serve (ctx * Context ) {
// TODO implement me
panic ( "implement me" )
}
// Start 启动WEB服务器
func (s * HTTPServer ) Start (addr string ) error {
l, err := net. Listen ( "tcp" , addr)
if err != nil {
return err
}
// 在监听端口之后,启动服务之前做一些操作
// 例如在微服务框架中,启动服务之前需要注册服务
return http. Serve (l, s)
}
// AddRoute 注册路由
func (s * HTTPServer ) AddRoute (method string , path string , handleFunc HandleFunc ) {
// TODO: implement me
panic ( "implement me" )
}
// GET 注册GET请求路由
func (s * HTTPServer ) GET (path string , handleFunc HandleFunc ) {
s. AddRoute (http.MethodGet, path, handleFunc)
}
// POST 注册POST请求路由
func (s * HTTPServer ) POST (path string , handleFunc HandleFunc ) {
s. AddRoute (http.MethodPost, path, handleFunc)
}
PART9. 面试要点
9.1 HTTP服务器的生命周期?
一般来说就是启动、运行和关闭.在这三个阶段的前后都可以插入生命周期回调.一般来说,面试生命周期,多半都是为了后边问生命周期回调.例如说怎么做WEB服务的服务发现?就是利用生命周期回调的启动后回调,将WEB服务注册到服务中心.
9.2 HTTP Server的功能?
记住在不同的框架里面有不同的叫法.比如说在Gin里面叫做Engine,它们的基本功能都是提供路由注册、生命周期控制以及作为与http包结合的桥梁.
附录
问题1. 生命周期回调究竟是个啥?