一. 问题概述
- 了解go-zero底层也是基于net/http标准库实现http的,是怎么实现的,怎么触发到net/http的
- go-zero也是基于前缀树进行路由注册的,是怎么注册的,注册过程中有哪些注意点
- go-zero中支持中间件, 在服务启动时,中间件,路由是如何保存的,接收请求时是如何执行的
- 先看一下基础go-zero服务示例
package main
import (
"fmt"
"github.com/zeromicro/go-zero/rest/chain"
"github.com/zeromicro/go-zero/rest/httpx"
"github.com/zeromicro/go-zero/core/logx"
"github.com/zeromicro/go-zero/core/service"
"github.com/zeromicro/go-zero/rest"
"log"
"net/http"
)
func main() {
//1.创建服务句柄
//此处也可以替换为通过conf/MustLoad()加载yaml,通过rest/MustNewServer()创建服务
srv, err := rest.NewServer(rest.RestConf{
Port: 8080, // 侦听端口
ServiceConf: service.ServiceConf{
Log: logx.LogConf{Path: "./logs"}, // 日志路径
},
})
if err != nil {
log.Fatal(err)
}
defer srv.Stop()
//2.使用server上的Use()方法添加全局中间件
srv.Use(func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 在请求处理前执行一些逻辑
fmt.Println("before request")
// 调用下一个处理函数
next(w, r)
// 在请求处理后执行一些逻辑
fmt.Println("after request")
}
})
//2.注册路由
srv.AddRoutes([]rest.Route{
{
Method: http.MethodGet,
Path: "/user/info",
Handler: userInfo,
},
})
//===================下方是一些路由分组,中间件注册,拦截器注册的示例,不是真实代码会报错,使用时直接删除即可===============================
//3.路由分组示例只是示例会报错
srv.AddRoutes(
[]rest.Route{
{
Method: http.MethodPost,
Path: "/afsdfa",
Handler: thirdPayment.ThirdPaymentWxPayCallbackHandler(serverCtx),
},
},
//为一组路由开启jwt验证功能,并指定密钥
rest.WithJwt(serverCtx.Config.JwtAuth.AccessSecret),
//为一组路由添加一个公共的前缀
rest.WithPrefix("/afas/v1"),
)
srv.AddRoutes(
rest.WithMiddlewares(
[]rest.Middleware{
func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
}
},
func(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
}
},
//ToMiddleware()用于将一个接收和返回http.Handler的函数转换为一个Middleware类型的函数
rest.ToMiddleware(func(next http.Handler) http.Handler {
return next
}),
},
rest.Route{
Method: http.MethodGet,
Path: "/user/info2",
Handler: userInfo,
},
rest.Route{
Method: http.MethodGet,
Path: "/user/info3",
Handler: userInfo,
}),
rest.WithPrefix("/afas/v2"),
)
//====================================================================
srv.Start() // 启动服务
}
type User struct {
Name string `json:"name"`
Addr string `json:"addr"`
Level int `json:"level"`
}
func userInfo(w http.ResponseWriter, r *http.Request) {
var req struct {
UserId int64 `form:"user_id"` // 定义参数
}
if err := httpx.Parse(r, &req); err != nil { // 解析参数
httpx.Error(w, err)
return
}
users := map[int64]*User{
1: &User{"go-zero", "shanghai", 1},
2: &User{"go-queue", "beijing", 2},
}
httpx.WriteJson(w, http.StatusOK, users[req.UserId]) // 返回结果
}
二. 底层源码分析
涉及到的一些结构体简介
- 了解go-zero服务的启动与路由注册,首先要了解几个结构体比如: engine,patRouter,featuredRoutes,Route
- engine: 服务引擎,构建go-zero服务时首先要创建这个引擎
- 服务其中时会将服务的配置相关信息存储到engine的conf 属性中
- 在路由注册时会先将路由保存到engine的routes属性中,后续再通过这个属性获取所有路由构建前缀树
- 会将通过Server的Use()或rest下WithMiddleware()/WithMiddlewares()函数主动注册中间件存储到middlewares属性中
- 在后续处理时会获取middlewares属性中保存的中间件,engine的chain处理器链中
- …
type engine struct {
//RestConf结构体变量,内部存储了服务需要的配置信息,,比如监听地址,超时时间,鉴权等
conf RestConf
//一个featuredRoutes切片,用于存储rest服务的路由信息,每个路由包含了请求方法,路径,处理函数和特征值
routes []featuredRoutes
//表示rest服务的最大超时时间,它是根据所有路由的超时时间计算得到的
timeout time.Duration
//用于处理未授权的请求,比如返回401状态码或者重定向到登录页面
unauthorizedCallback handler.UnauthorizedCallback
//用于处理未签名的请求,比如返回403状态码或者提示用户签名
unsignedCallback handler.UnsignedCallback
//用于管理rest服务的中间件链,可以在请求处理前后执行一些逻辑,比如日志、监控、限流等
chain chain.Chain
//用于存储rest服务的中间件
middlewares []Middleware
//用于实现自适应限流功能,根据CPU负载和请求数动态调整限流阈值
shedder load.Shedder
//用于实现优先级限流功能,根据请求的优先级和请求数动态调整限流阈值
priorityShedder load.Shedder
//用于配置rest服务的TLS加密通信参数,比如证书、密钥等
tlsConfig *tls.Config
}
- patRouter: 通过NewServer()函数创建go-zero服务时内部会调用NewRouter()先初始化这个结构体变量,初始化内部的trees属性,这个trees属性就是多个以路由method为key的前缀树,patRouter是路由注册与请求执行的核心,该结构体实现了ServeHTTP()方法,会通过这个方法处理用户的请求
type patRouter struct {
//键是请求方法,值是一个search.Tree类型的对象
trees map[string]*search.Tree
//用于处理未找到匹配路由的请求,比如返回404状态码或者自定义的错误页面
notFound http.Handler
//用于处理不允许的请求方法,比如返回405状态码或者自定义的错误页面
notAllowed http.Handler
}
type Tree struct {
root *node
}
type node struct {
item any
children [2]map[string]*node
}
- featuredRoutes
type featuredRoutes struct {
//表示这组路由的超时时间,如果请求处理超过这个时间,会返回超时错误
timeout time.Duration
//表示这组路由是否具有优先级,如果为true,这组路由会使用优先级限流器进行限流,否则使用普通限流器
priority bool
//包含了jwt验证相关的配置信息,比如是否开启jwt验证、密钥等
jwt jwtSetting
//包含了签名验证相关的配置信息,比如是否开启签名验证、签名算法等
signature signatureSetting
//用于存储这组路由的具体信息,每个Route包含了请求方法、路径和处理函数
routes []Route
//表示这组路由允许的最大请求体大小,如果请求体超过这个大小,会返回错误
maxBytes int64
}
- Route
type Route struct {
//请求方法,比如GET、POST、PUT等
Method string
//请求路径,可以包含模式匹配的参数,比如/user/:id
Path string
//处理请求的函数,用于处理匹配到该路由的请求,并返回响应
Handler http.HandlerFunc
}
初始化
- 在编写go-zero服务时,可以编写yaml,通过conf/MustLoad()读取yaml配置,通过rest/MustNewServer()创建服务,也可以直接封装rest.RestConf配置变量,调用rest/NewServer()创建服务(实际MustLoad()内部也会调用这个NewServer()),查看NewServer()
- 首先调用调用newEngine()创建Engine服务引擎
- 调用NewRouter()函数,初始化patRouter,初始化patRouter中的trees路由前缀树
func NewServer(c RestConf, opts ...RunOption) (*Server, error) {
if err := c.SetUp(); err != nil {
return nil, err
}
server := &Server{
ngin: newEngine(c),
router: router.NewRouter(),
}
opts = append([]RunOption{WithNotFoundHandler(nil)}, opts...)
for _, opt := range opts {
opt(server)
}
return server, nil
}
中间件的预设置
- 有两种方式注册中间件:
- 通过Server的use方法注册全局中间件,
- 通过github.com\zeromicro\go-zero\rest\server.go中的WithMiddlewares/WithMiddleware()函数注册针对一组路由的中间件
- Server.Use()注册全局中间件,很简单,会调用engine的use()方法,将中间件函数保存到engine引擎的中间件切片属性middlewares中
func (s *Server) Use(middleware Middleware) {
//调用engine的user()方法
s.ngin.use(middleware)
}
func (ng *engine) use(middleware Middleware) {
ng.middlewares = append(ng.middlewares, middleware)
}
- 查看WithMiddlewares/WithMiddleware()函数针对一组路由注册中间件函数源码,最终会将中间件封装到每个路由的Handler属性中
//WithMiddlewares内部实际就是调用的WithMiddleware()只关注这一个即可
func WithMiddleware(middleware Middleware, rs ...Route) []Route {
routes := make([]Route, len(rs))
for i := range rs {
route := rs[i]
routes[i] = Route{
Method: route.Method,
Path: route.Path,
Handler: middleware(route.Handler),
}
}
return routes
}
- 另外rest下有一个WithChain()函数,直接将中间件添加到chain处理器链中(可以看为将中间件又封装了一层),如下,在创建Server时添加两个拦截器
server := MustNewServer(RestConf{}, rest.WithChain(chain.New(中间件函数1, 中间件函数2)))
路由注册与中间件的处理
- 在通过go-zero提供服务时,首先执行rest下的NewServer()函数,读取配置,创建服务端Server, 提供对外的接口时,需要将接口Method,接口函数,接口路径封装为Route结构体变量,调用Server的AddRoutes()方法将这个结构体变量添加到engine引擎的routes路由切片属性中
func (s *Server) AddRoutes(rs []Route, opts ...RouteOption) {
r := featuredRoutes{
routes: rs,
}
for _, opt := range opts {
opt(&r)
}
s.ngin.addRoutes(r)
}
func (ng *engine) addRoutes(r featuredRoutes) {
ng.routes = append(ng.routes, r)
}
- 路由添加完成后,会调用Server的Start()方法启动服务,查看源码.会调用engine的start()方法–>调用bindRoutes()
func (s *Server) Start() {
handleError(s.ngin.start(s.router))
}
// 定义一个 start 方法,接收一个 router 参数和可变数量的 StartOption 参数,返回一个 error 类型的值
func (ng *engine) start(router httpx.Router, opts ...StartOption) error {
// 调用 bindRoutes 方法,将路由绑定到 ng 上,如果出错则返回错误
if err := ng.bindRoutes(router); err != nil {
return err
}
// 将 ng.withTimeout 方法作为一个 StartOption 参数添加到 opts 切片的开头
opts = append([]StartOption{ng.withTimeout()}, opts...)
// 如果配置文件中没有证书文件和密钥文件,则调用 internal 包中的 StartHttp 方法,启动一个 http 服务
if len(ng.conf.CertFile) == 0 && len(ng.conf.KeyFile) == 0 {
return internal.StartHttp(ng.conf.Host, ng.conf.Port, router, opts...)
}
// 如果配置文件中有证书文件和密钥文件,则创建一个匿名函数,将 ng 的 tlsConfig 属性赋值给 svr 的 TLSConfig 属性,然后将这个匿名函数作为一个 StartOption 参数添加到 opts 切片的末尾
opts = append([]StartOption{
func(svr *http.Server) {
if ng.tlsConfig != nil {
svr.TLSConfig = ng.tlsConfig
}
},
}, opts...)
// 调用 internal 包中的 StartHttps 方法,启动一个 https 服务
return internal.StartHttps(ng.conf.Host, ng.conf.Port, ng.conf.CertFile,
ng.conf.KeyFile, router, opts...)
}
- 查看engine的bindRoutes()方法,获取engine的routes属性中保存的每一个路由,遍历调用bindFeaturedRoutes()–>调用bindRoute()方法在该方法中重点完成了:
- 获取engine的chain属性,如果为空调用buildChainWithNativeMiddlewares()新建一个chain链,获取配置信息,根据配置判断添加中间件,比如判断是否配置了追踪请求的Trace中间件, 记录请求日志的Log中间件,收集请求指标数据Prometheus中间件,限制最大并发连接数的MaxConns中间件,实现熔断机制Breaker中间件 ,实现流量控制Shedding中间件,设置请求超时时间的Timeout中间件,恢复异常请求的Recover中间件…如果有配置则将这些中间件添加到chain中
- 获取engine的middlewares中间件,遍历添加到chain中,也就是我们通过Server的Use()方法,或rest下WithMiddleware()/WithMiddlewares()函数主动注册中间件,将这些中间件添加到chain中
- 调用chain的ThenFunc()方法,遍历所有中间件,调用中间件的Handle()方法,将中间件包装为Handler形成中间件Handler链,并将接口的处理Handler添加到了链条的末尾,在执行时匹配到handler后会基于这个链条向下调用到最终的接口处理函数
- 调用Router的Handle()方法,将路由注册到路由树上,也就是前缀树的构建
func (ng *engine) bindRoutes(router httpx.Router) error {
//创建 metrics 对象,用于记录请求的统计信息
metrics := ng.createMetrics()
//遍历 engine.routes属性中的每一个路由
for _, fr := range ng.routes {
//调用 bindFeaturedRoutes方法进行绑定
if err := ng.bindFeaturedRoutes(router, fr, metrics); err != nil {
return err
}
}
return nil
}
func (ng *engine) bindFeaturedRoutes(router httpx.Router, fr featuredRoutes, metrics *stat.Metrics) error {
// 调用 signatureVerifier 方法,根据 fr.signature 的值创建一个签名验证器
verifier, err := ng.signatureVerifier(fr.signature)
if err != nil {
return err
}
//遍历 fr.routes 中的每一个路由
for _, route := range fr.routes {
//调用 bindRoute 方法进行绑定
if err := ng.bindRoute(fr, router, metrics, route, verifier); err != nil {
return err
}
}
return nil
}
//bindRoute()接收参数中: metrics是统计相关的,verifier 是一个签名验证器
//重点做了如下动作:
//1.获取engine的chain属性,如果为空调用buildChainWithNativeMiddlewares()根据配置判断添加中间件
//2.获取engine的middlewares中间件,遍历添加到chain中
//3.调用chain的ThenFunc()方法将路由接口添加到chain中
//4.调用Router的Handle()方法,将路由注册到路由树上,也就是前缀树的构建
func (ng *engine) bindRoute(fr featuredRoutes, router httpx.Router, metrics *stat.Metrics,
route Route, verifier func(chain.Chain) chain.Chain) error {
//1.获取 engine的chain属性(也就是通过rest下的withChain()注册的连接器又封装了一层的中间件)
chn := ng.chain
if chn == nil {
//如果为空调用 buildChainWithNativeMiddlewares()根据 fr, route, metrics 构建一个 chain 对象
chn = ng.buildChainWithNativeMiddlewares(fr, route, metrics)
}
//2.调用appendAuthHandler()根据 fr, chn, verifier 在 chain 对象后面追加一个认证处理器
chn = ng.appendAuthHandler(fr, chn, verifier)
//3.遍历 engine.middlewares属性中保存的每一个中间件
for _, middleware := range ng.middlewares {
//调用 convertMiddleware()函数,将中间件转换为 chain.Handler 类型,
//并追加到 chain 对象后面
chn = chn.Append(convertMiddleware(middleware))
}
//4.调用chain的ThenFunc()将route.Handler也就是接口函数作为最后一个处理器,添加到chain中
//该函数中会遍历c.middlewares中的每个中间件,将每个中间件包装为handle,形成一个handler链
//最后返回这个handler链的入口,也就是第一个中间件包装的handler对象
handle := chn.ThenFunc(route.Handler)
//调用 router 的 Handle 方法,将 route.Method, route.Path 和 handle 作为参数,注册到路由树上
return router.Handle(route.Method, route.Path, handle)
}
func (ng *engine) buildChainWithNativeMiddlewares(fr featuredRoutes, route Route,
metrics *stat.Metrics) chain.Chain {
//创建一个空的链式处理器
chn := chain.New()
//如果配置了Trace中间件,在链式处理器中添加TraceHandler,用于追踪请求的路径和忽略的路径
if ng.conf.Middlewares.Trace {
chn = chn.Append(handler.TraceHandler(ng.conf.Name,
route.Path,
handler.WithTraceIgnorePaths(ng.conf.TraceIgnorePaths)))
}
//如果配置了Log中间件,在链式处理器中添加getLogHandler,用于记录请求的日志
if ng.conf.Middlewares.Log {
chn = chn.Append(ng.getLogHandler())
}
//如果配置了Prometheus中间件,在链式处理器中添加PrometheusHandler,用于收集请求的指标数据
if ng.conf.Middlewares.Prometheus {
chn = chn.Append(handler.PrometheusHandler(route.Path, route.Method))
}
//如果配置了MaxConns中间件,在链式处理器中添加MaxConnsHandler,用于限制最大并发连接数
if ng.conf.Middlewares.MaxConns {
chn = chn.Append(handler.MaxConnsHandler(ng.conf.MaxConns))
}
// 如果配置了Breaker中间件,在链式处理器中添加BreakerHandler,用于实现熔断机制
if ng.conf.Middlewares.Breaker {
chn = chn.Append(handler.BreakerHandler(route.Method, route.Path, metrics))
}
//如果配置了Shedding中间件,在链式处理器中添加SheddingHandler,用于实现流量控制
if ng.conf.Middlewares.Shedding {
chn = chn.Append(handler.SheddingHandler(ng.getShedder(fr.priority), metrics))
}
//如果配置了Timeout中间件,在链式处理器中添加TimeoutHandler,用于设置请求的超时时间
if ng.conf.Middlewares.Timeout {
chn = chn.Append(handler.TimeoutHandler(ng.checkedTimeout(fr.timeout)))
}
//如果配置了Recover中间件,在链式处理器中添加RecoverHandler,用于恢复请求的异常
if ng.conf.Middlewares.Recover {
chn = chn.Append(handler.RecoverHandler)
}
//如果配置了Metrics中间件,在链式处理器中添加MetricHandler,用于记录请求的统计数据
if ng.conf.Middlewares.Metrics {
chn = chn.Append(handler.MetricHandler(metrics))
}
//如果配置了MaxBytes中间件,在链式处理器中添加MaxBytesHandler,用于限制请求的最大字节数
if ng.conf.Middlewares.MaxBytes {
chn = chn.Append(handler.MaxBytesHandler(ng.checkedMaxBytes(fr.maxBytes)))
}
//如果配置了Gunzip中间件,在链式处理器中添加GunzipHandler,用于解压缩请求的数据
if ng.conf.Middlewares.Gunzip {
chn = chn.Append(handler.GunzipHandler)
}
return chn
}
//添加一个授权验证的中间件
//入参verifier是一个函数类型,表示一个验证器,它接受一个链式处理器作为参数,并返回一个链式处理器作为结果
func (ng *engine) appendAuthHandler(fr featuredRoutes, chn chain.Chain,
verifier func(chain.Chain) chain.Chain) chain.Chain {
//1.判断是否启用了jwt
if fr.jwt.enabled {
if len(fr.jwt.prevSecret) == 0 {
//如果没有设置前一个密钥,就使用当前的密钥进行授权验证,并在验证失败时调用unauthorizedCallback函数
chn = chn.Append(handler.Authorize(fr.jwt.secret,
handler.WithUnauthorizedCallback(ng.unauthorizedCallback)))
} else {
//如果设置了前一个密钥,就使用当前的密钥和前一个密钥进行授权验证,并在验证失败时调用unauthorizedCallback函数
chn = chn.Append(handler.Authorize(fr.jwt.secret,
handler.WithPrevSecret(fr.jwt.prevSecret),
handler.WithUnauthorizedCallback(ng.unauthorizedCallback)))
}
}
//返回经过验证器处理后的链式处理器
return verifier(chn)
}
//这个方法在执行角度看还是比较重要的
//遍历所有中间件,调用中间件的Handle()方法,将中间件包装为一个新的handler对象,
//最终形成一个中间件的handler链,并将传入的h添加到了这个链条的末尾
func (c chain) Then(h http.Handler) http.Handler {
if h == nil {
h = http.DefaultServeMux
}
//遍历注册的所有中间件
for i := range c.middlewares {
//对每个中间件调用它的Handle(next http.HandlerFunc) http.HandlerFunc方法,
//传入当前的h作为参数,会返回一个新的handler对象,最终形成一个handler链
h = c.middlewares[len(c.middlewares)-1-i](h)
}
return h
}
- 查看patRouter的Handle()方法添加路由构建前缀树的源码,内部会
- patRouter是在NewServer()函数创建服务Server内部通过NewRouter()初始化的,内部有个trees属性,保存了以Method为维度的多个前缀树
- 在Handle()方法中会根据当前路由的Method判断是否存在该类型的前缀树,如果存在则调用Tree的Add()方法添加,如果不存在,则先调用调用一个NewTree()新建一个前缀树,然后调用Tree的Add()方法添加
- Tree的Add()方法中会调用一个add()函数,该函数中,会根据"/“截取路由path中的每一段,作为token标识判断当前前缀树中是否已经存在该节点,如果存在则递归调用根据”/"继续截取当当前节点作为子节点判断添加前缀树中,
- 如果不存在则调用newNode()函数新建node节点,.以当前路由截取到的token标识为key添加到前缀树中
func (pr *patRouter) Handle(method, reqPath string, handler http.Handler) error {
if !validMethod(method) {
return ErrInvalidMethod
}
if len(reqPath) == 0 || reqPath[0] != '/' {
return ErrInvalidPath
}
cleanPath := path.Clean(reqPath)
//根据method在patRouter的trees树中获取指定前缀树
tree, ok := pr.trees[method]
if ok {
//如果存在调用Tree的Add()方法注册路由
return tree.Add(cleanPath, handler)
}
//如果patRouter的trees中不存在当前method前缀树则创建
tree = search.NewTree()
pr.trees[method] = tree
//然后调用Tree的Add()方法注册路由
return tree.Add(cleanPath, handler)
}
func (t *Tree) Add(route string, item interface{}) error {
if len(route) == 0 || route[0] != slash {
return errNotFromRoot
}
if item == nil {
return errEmptyItem
}
//注册路由
err := add(t.root, route[1:], item)
//异常判断
switch err {
case errDupItem:
return duplicatedItem(route)
case errDupSlash:
return duplicatedSlash(route)
default:
return err
}
}
func add(nd *node, route string, item interface{}) error {
//如果路由为空,表示已经到达最后一个节点
if len(route) == 0 {
if nd.item != nil {
// 如果当前节点已经有了项,就返回errDupItem错误
return errDupItem
}
//否则,就把项赋值给当前节点,并返回nil
nd.item = item
return nil
}
//如果路由以斜杠开头,就返回errDupSlash错误
//slash是"/"
if route[0] == slash {
return errDupSlash
}
//遍历路由中的每个字符
for i := range route {
//如果不是斜杠,continue跳过
if route[i] != slash {
continue
}
//截取路由中第一个斜杠之前的部分作为token(也就是当前写过之前的)
token := route[:i]
//获取当前节点对应token的子节点集合(可能是静态子节点或者动态子节点)
//返回的是一个 map[string]*node 变量
children := nd.getChildren(token)
//如果子节点集合中存在当前token对应的子节点对象
if child, ok := children[token]; ok {
if child != nil {
//并且子节点对象不为nil,递归调用add函数,在子节点上继续添加剩余的路由并返回结果
return add(child, route[i+1:], item)
}
//子节点对象为nil,返回errInvalidState错误(这种情况不应该发生)
return errInvalidState
}
//如果子节点集合中不存在当前token对应的子节点对象,创建一个新的空节点对象作为子节点
child := newNode(nil)
//把新创建的子节点对象添加到子节点集合中
children[token] = child
//递归调用add函数,在新创建的子节点上继续添加剩余的路由
return add(child, route[i+1:], item)
}
//如果路由中没有斜杠了,表示已经到达最后一个token也就是路由的最后一个节点,获取当前节点对应token的子节点集合
children := nd.getChildren(route)
//如果子节点集合中存在token对应的子节点对象
if child, ok := children[route]; ok {
//如果子节点对象已经有了当前项,返回errDupItem错误
if child.item != nil {
return errDupItem
}
//没有,就把当前节点添加到子节点中
child.item = item
} else {
//如果子节点集合中不存在当前token对应的子节点对象
//创建一个新的节点对象作为子节点,然后添加到子节点集合中
children[route] = newNode(item)
}
return nil
}
启动服务到触发net/http
- 在调用Start()方法启动服务时,内部会有一个判断:如果配置文件中没有证书文件和密钥文件,调用 internal 包中的 StartHttp()方法,如果有则调用StartHttps()启动一个 http 服务
func (s *Server) Start() {
handleError(s.ngin.start(s.router))
}
// 定义一个 start 方法,接收一个 router 参数和可变数量的 StartOption 参数,返回一个 error 类型的值
func (ng *engine) start(router httpx.Router, opts ...StartOption) error {
// 调用 bindRoutes 方法,将路由绑定到 ng 上,如果出错则返回错误
if err := ng.bindRoutes(router); err != nil {
return err
}
// 将 ng.withTimeout 方法作为一个 StartOption 参数添加到 opts 切片的开头
opts = append([]StartOption{ng.withTimeout()}, opts...)
// 如果配置文件中没有证书文件和密钥文件,则调用 internal 包中的 StartHttp 方法,启动一个 http 服务
if len(ng.conf.CertFile) == 0 && len(ng.conf.KeyFile) == 0 {
return internal.StartHttp(ng.conf.Host, ng.conf.Port, router, opts...)
}
// 如果配置文件中有证书文件和密钥文件,则创建一个匿名函数,将 ng 的 tlsConfig 属性赋值给 svr 的 TLSConfig 属性,然后将这个匿名函数作为一个 StartOption 参数添加到 opts 切片的末尾
opts = append([]StartOption{
func(svr *http.Server) {
if ng.tlsConfig != nil {
svr.TLSConfig = ng.tlsConfig
}
},
}, opts...)
// 调用 internal 包中的 StartHttps 方法,启动一个 https 服务
return internal.StartHttps(ng.conf.Host, ng.conf.Port, ng.conf.CertFile,
ng.conf.KeyFile, router, opts...)
}
- 以StartHttp()为例,查看该函数,内部调用了net/http下的ListenAndServe()
func StartHttp(host string, port int, handler http.Handler, opts ...StartOption) error {
//start()源码在下方
return start(host, port, handler, func(svr *http.Server) error {
//封装了net/http下的ListenAndServe()
return svr.ListenAndServe()
}, opts...)
}
//函数类型的run入参就是net/http下的ListenAndServe()
func start(host string, port int, handler http.Handler, run func(svr *http.Server) error,
opts ...StartOption) (err error) {
//创建一个*http.Server类型的对象,并设置其地址和处理器属性
server := &http.Server{
Addr: fmt.Sprintf("%s:%d", host, port),
Handler: handler,
}
for _, opt := range opts {
opt(server)
}
//创建一个健康管理器对象,用于检测服务器的健康状态
healthManager := health.NewHealthManager(fmt.Sprintf("%s-%s:%d", probeNamePrefix, host, port))
//添加一个进程结束时的监听器函数,用于关闭服务器和标记健康状态为不可用
waitForCalled := proc.AddWrapUpListener(func() {
healthManager.MarkNotReady()
if e := server.Shutdown(context.Background()); e != nil {
logx.Error(e)
}
})
//使用延迟函数,在函数返回时执行以下逻辑
defer func() {
//如果返回的错误是http.ErrServerClosed,表示服务器已经关闭,就调用waitForCalled函数等待监听器函数执行完毕
if err == http.ErrServerClosed {
waitForCalled()
}
}()
//标记健康状态为可用
healthManager.MarkReady()
//添加健康管理器对象到健康检测模块中
health.AddProbe(healthManager)
//调用run函数,启动服务器,并返回结果
return run(server)
}
- 到这里就来到了go的net/http标准库,具体参考go 进阶 http标准库相关: 三. HttpServer 服务启动到Accept等待接收连接,简单复习一下net/http提供服务的流程:
- 在通过net/http编写服务端时, 首先调用NewServeMux()创建多路复用器,编写对外接收请求的接口函数也就是处理器,然后调用多路复用器上的HandleFunc()方法,将接口与接口路径进行绑定,注册路由, 最后调用ListenAndServe()函数在指定端口开启监听,启动服务
- ListenAndServe()方法内部重点调用了"net.Listen(“tcp”, addr)"多路复用相关初始化,初始化socket,端口连接绑定,开启监听,调用"srv.Serve(ln)”:等待接收客户端连接Accept(),与接收到连接后的处理流程
- 服务相关的我们先关注"srv.Serve(ln)",方法内通过for开启了一个死循环,在循环内部,调用Listener的Accept()方法,假设当前是TCP连接调用的就是TCPListener下的Accept(),监听客户端连接,当接收到客户端连接后,通过开启协程执行serve()方法处理请求,每一个连接开启一个goroutine来处理
接收请求的处理
- go-zero底层是基于net/http实现的,再看一下net/http接收请求时底层是如何执行的go 进阶 http标准库相关: 五. HttpServer 接收请求路由发现原理,
- 简单复习一下,基于net/http搭建服务时,底层会执行ListenAndServe()方法,最终会执行到Listener的Accept()方法,假设当前是TCP连接调用的就是TCPListener下的Accept(),阻塞监听客户端连接,当有接收到连接请求后Accept()方法返回,拿到一个新的net.Conn连接实例,然后开启协程调用Conn连接实例上的serve()方法处理客户端请求,查看这个serve()方法:
- 首先调用newBufioReader() 封装了一个bufio.Reader
- 开启了一个无限for循环,循环内
- 调用conn的readRequest(ctx)方法读取请求的内容,比如解析HTTP请求协议,读取请求头,请求参数,封装Request和response,在解析时会读取请求头的 Content-Length,不为 0会通过TCPConn.Read() 方法读取指定长度的数据并存入请求体中,如果 Content-Length 为 0 或者没有设置,则请求体为空
- 封装serverHandler调用serverHandler上的ServeHTTP(w, w.req)方法进行路由匹配,找到对应的处理函数,执行我们写的业务逻辑
- 调用response的finishRequest()方法进行最后处理工作,当底层 bufio.Writer 缓冲区的大小达到阈值或者Flush() 被显式调用时,就会将缓冲区内的数据写入到底层连接中,并触发 Conn 的 Write() 方法将数据发送到客户端,另外finishRequest()方法还会进行一些比如异常处理,资源回收,状态更新等操作
- 最后调用conn的setState()设置连接状态为StateIdle,方便后续重用连接
- 这里执行的ServeHTTP()就是匹配路由触发业务接口的函数,ServeHTTP是一个接口,绝大多数Web框架都是通过实现该接口,从而替换掉Golang默认的路由,这里执行的就是patRouter实现的ServeHTTP(),查看该函数源码
- 首先根据请求的method在trees中获取到指定前缀树,如果存在则根据请求的reqPath路径在前缀树中查找对应的handler对象,如果找到,则调用这个handler对象的ServeHTTP()方法
- 在服务启动时会将添加的中间件添加到chain处理器链中,然后遍历所有中间件转换为handler链,并将接口处理函数添加到handler链的末尾,这个过程是在chain类型的Then()方法中完成的。
- 在路由匹配时拿到第一个中间件handler开始执行,如果执行通过,中间件中会继续调用ServeHTTP(),也就是继续执行下一个中间件,一直执行到接口处理函数
func (pr *patRouter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
//对请求的路径进行清理.去除多余的斜杠和点
reqPath := path.Clean(r.URL.Path)
//先通过请求的method获取到指定的前缀树
if tree, ok := pr.trees[r.Method]; ok {
//如果前缀树存在,则开始匹配路由
if result, ok := tree.Search(reqPath); ok {
if len(result.Params) > 0 {
//如果结果对象中有参数,就把参数添加到请求的上下文中
r = pathvar.WithVars(r, result.Params)
}
//匹配到指定路由后转换为http.Handler类型,并调用它的ServeHTTP方法处理请求
result.Item.(http.Handler).ServeHTTP(w, r)
return
}
}
//如果未找到对应的前缀树调用pr.methodsAllowed方法,获取请求的路径允许的方法列表,并判断请求的方法是否在其中
allows, ok := pr.methodsAllowed(r.Method, reqPath)
if !ok { // 如果请求的方法不在允许的方法列表中
//调用pr.handleNotFound方法,处理未找到的情况,并返回
pr.handleNotFound(w, r)
return
}
//如果pr.notAllowed不为nil,表示有自定义的处理器对象用于处理不允许的方法的情况
if pr.notAllowed != nil {
//调用pr.notAllowed的ServeHTTP方法,处理请求,并返回
pr.notAllowed.ServeHTTP(w, r)
} else {
// 否则,就使用默认的处理逻辑
w.Header().Set(allowHeader, allows) //设置响应头中的Allow字段为允许的方法列表
w.WriteHeader(http.StatusMethodNotAllowed) //设置响应状态码为405 Method Not Allowed,并返回
}
}
三. 总结
- go-zero http服务是怎么启动的, 是怎么整合net/http的, 前缀树是怎么构建的,路由,中间件是怎么注册的,当接收到请求后是怎么匹配路由执行的,当问到这些问题都可以由rest下的NewServer()函数创建服务Server开始说,下面总结一下
- 了解go-zero http服务首先要了解几个结构体,比如engine服务引擎,首先要创建这个引擎,内部有几个比较重要的属性比如
- RestConf类型的conf属性: 存储了服务启动运行所需的配置信息
- featuredRoutes类型的切片routes属性,在执行AddRoutes()注册接口时会将将接口封装为Router,然后将Router通过AddRoutes()保存到这个切片属性中,服务启动时会通过这个切片获取到所有路由,构建前缀树进行路由注册
- Middleware类型切片的middlewares属性,会将通过Server的Use()方法注册的全局中间件保存到这个切片属性中,后续会通过这个属性获取到注册的中间件,加上局部中间件,接口handler转换为chain执行链,转换为Handler处理器链
- 还有一个比较重要的结构体patRouter, 内部存在一个trees属性,内部存储了以路由的method为key的前缀树,patRouter是路由注册与请求执行的核心,该结构体实现了ServeHTTP()方法,会通过这个方法处理用户的请求
- 说一下服务启动的执行过程,在构建go-zero http服务时,首先需要将配置设置到RestConf结构体变量上,通过执行rest下的NewServer()函数,读取配置创建服务端的Server,查看这个NewServer()源码
- 首先调用调用newEngine()创建Engine服务引擎
- 调用NewRouter()函数,初始化patRouter,初始化patRouter中的trees路由前缀树
- 将这两个属性封装到了一个Server结构体变量,并返回,然后通过Server结构体变量调用Use()方法注册中间件,调用AddRoutes()方法注册路由,调用Start()方法启动服务
- 中间件的注册: 在go-zero中可以通过Server的Use()方法注册全局中间件,可以通过WithMiddlewares/WithMiddleware()函数注册针对一组路由的局部中间件
- 通过Use()方法注册的中间件会保存到Engine的middlewares属性中
- 通过WithMiddlewares/WithMiddleware()函数注册的局部中间,查看源码发现,中间件函数会封装到Route的Handler中,跟随Route路由一块注册
- 另外go-zero的rest包下还有一个WithChain()也可以用来注册中举中间件,但是通过该函数注册的中间件会保存到Engine的chain属性中,是一个中间件链条
- 路由的保存: 当通过NewServer()拿到服务Server以后,调用Server的AddRoutes()方法,将对外的接口封装为Route进行路由后注册,在AddRoutes()方法中,将路由封装为featuredRoutes,保存到了engine的routes 属性中,这是保存,后续构建前缀树的逻辑在Server的Start()启动服务方法中
- 当拿到服务Server以后,查看Server的Start()启动服务方法,内部会调用engine的start()方法,内部重点执行了:
- bindRoutes(): 注册路由中间件,构建前缀树
- 判断如果没有配置证书,执行StartHttp()启动http服务,有配置执行StartHttps()启动https服务,触发到net/http
- bindRoutes()中间件,路由的注册与前缀树的构建,通过engine的routes属性获取到所有路由,遍历调用engine的bindFeaturedRoutes()方法开始注册路由,内部会调用engine的bindRoute(),在该方法中
- 首先获取engine的chain属性,如果为空,会调用buildChainWithNativeMiddlewares()在该方法中根据配置信息判断添加一下默认的中间件,比如判断是否配置了追踪请求的Trace中间件, 记录请求日志的Log中间件,收集请求指标数据Prometheus中间件,限制最大并发连接数的MaxConns中间件,实现熔断机制Breaker中间件 ,实现流量控制Shedding中间件,设置请求超时时间的Timeout中间件,恢复异常请求的Recover中间件…如果有配置则将这些中间件添加到chain中
- 遍历engine的middlewares也就是拿到全局中间件,将这些中间件也添加到到engine的chain属性中整个中间件链条封装完毕
- 比较重要的一个步骤,拿到路由接口的处理器Handler,执行chain的ThenFunc()方法,将保存了中间件链条的chain转换为Handler链,并将接口的处理器Handler添加到Handler链的末尾(在接收请求时根据路由匹配拿到指定的Handler后会基于这个链条向下调用到最终的接口处理函数)
- 调用Router的Handle()方法,将路由注册到路由树上,也就是前缀树的构建,实际执行的是patRouter的Handle(),方法中:
- 根据当前路由的Method判断是否存在该类型的前缀树,如果存在则调用Tree的Add()方法添加,如果不存在,则先调用调用一个NewTree()新建一个前缀树,然后调用Tree的Add()方法添加
- Tree的Add()方法中会调用一个add()函数,该函数中,会根据"/“截取路由path中的每一段,作为token标识判断当前前缀树中是否已经存在该节点,如果存在则递归调用根据”/"继续截取当当前节点作为子节点判断添加前缀树中,
- 如果不存在则调用newNode()函数新建node节点,.以当前路由截取到的token标识为key添加到前缀树中
- Server的Start()方法服务的启动, 查看源码内部会根据是否配置了证书选择调用StartHttp()/StartHttps()启动http或https服务,以http为例,查看StartHttp()源码内最终封装调用了net/http标准库中的ListenAndServe()
- 了解go-zero http服务怎么接收请求执行的,要先了解net/http是怎么请求,路由匹配的,在net/http处理请求时会调用路由的ServeHTTP()方法,这里调用的就是patRouter上的这个方法,查看源码:
- 首先根据请求的method在trees中获取到指定前缀树,如果存在则根据请求的reqPath路径在前缀树中查找对应的handler对象,如果找到,则调用这个handler对象的ServeHTTP()方法
- 在服务启动时会将添加的中间件添加到chain处理器链中,然后遍历所有中间件转换为handler链,并将接口处理函数添加到handler链的末尾,这个过程是在chain类型的Then()方法中完成的。
- 在路由匹配时拿到第一个中间件handler开始执行,如果执行通过,中间件中会继续调用ServeHTTP(),也就是继续执行下一个中间件,一直执行到接口处理函数