go 进阶 go-zero相关: 二. 服务启动与路由,中间件注册,请求接收底层原理

一. 问题概述

  1. 了解go-zero底层也是基于net/http标准库实现http的,是怎么实现的,怎么触发到net/http的
  2. go-zero也是基于前缀树进行路由注册的,是怎么注册的,注册过程中有哪些注意点
  3. go-zero中支持中间件, 在服务启动时,中间件,路由是如何保存的,接收请求时是如何执行的
  4. 先看一下基础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]) // 返回结果
}

二. 底层源码分析

涉及到的一些结构体简介

  1. 了解go-zero服务的启动与路由注册,首先要了解几个结构体比如: engine,patRouter,featuredRoutes,Route
  2. engine: 服务引擎,构建go-zero服务时首先要创建这个引擎
  1. 服务其中时会将服务的配置相关信息存储到engine的conf 属性中
  2. 在路由注册时会先将路由保存到engine的routes属性中,后续再通过这个属性获取所有路由构建前缀树
  3. 会将通过Server的Use()或rest下WithMiddleware()/WithMiddlewares()函数主动注册中间件存储到middlewares属性中
  4. 在后续处理时会获取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
}
  1. 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
}
  1. featuredRoutes
type featuredRoutes struct {
	//表示这组路由的超时时间,如果请求处理超过这个时间,会返回超时错误
	timeout   time.Duration
	//表示这组路由是否具有优先级,如果为true,这组路由会使用优先级限流器进行限流,否则使用普通限流器
	priority  bool
	//包含了jwt验证相关的配置信息,比如是否开启jwt验证、密钥等
	jwt       jwtSetting
	//包含了签名验证相关的配置信息,比如是否开启签名验证、签名算法等
	signature signatureSetting
	//用于存储这组路由的具体信息,每个Route包含了请求方法、路径和处理函数
	routes    []Route
	//表示这组路由允许的最大请求体大小,如果请求体超过这个大小,会返回错误
	maxBytes  int64
}
  1. Route
type Route struct {
	//请求方法,比如GET、POST、PUT等
	Method  string
	//请求路径,可以包含模式匹配的参数,比如/user/:id
	Path    string
	//处理请求的函数,用于处理匹配到该路由的请求,并返回响应
	Handler http.HandlerFunc
}

初始化

  1. 在编写go-zero服务时,可以编写yaml,通过conf/MustLoad()读取yaml配置,通过rest/MustNewServer()创建服务,也可以直接封装rest.RestConf配置变量,调用rest/NewServer()创建服务(实际MustLoad()内部也会调用这个NewServer()),查看NewServer()
  1. 首先调用调用newEngine()创建Engine服务引擎
  2. 调用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
}

中间件的预设置

  1. 有两种方式注册中间件:
  1. 通过Server的use方法注册全局中间件,
  2. 通过github.com\zeromicro\go-zero\rest\server.go中的WithMiddlewares/WithMiddleware()函数注册针对一组路由的中间件
  1. 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)
}
  1. 查看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
}
  1. 另外rest下有一个WithChain()函数,直接将中间件添加到chain处理器链中(可以看为将中间件又封装了一层),如下,在创建Server时添加两个拦截器
server := MustNewServer(RestConf{}, rest.WithChain(chain.New(中间件函数1, 中间件函数2)))

路由注册与中间件的处理

  1. 在通过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)
}
  1. 路由添加完成后,会调用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...)
}
  1. 查看engine的bindRoutes()方法,获取engine的routes属性中保存的每一个路由,遍历调用bindFeaturedRoutes()–>调用bindRoute()方法在该方法中重点完成了:
  1. 获取engine的chain属性,如果为空调用buildChainWithNativeMiddlewares()新建一个chain链,获取配置信息,根据配置判断添加中间件,比如判断是否配置了追踪请求的Trace中间件, 记录请求日志的Log中间件,收集请求指标数据Prometheus中间件,限制最大并发连接数的MaxConns中间件,实现熔断机制Breaker中间件 ,实现流量控制Shedding中间件,设置请求超时时间的Timeout中间件,恢复异常请求的Recover中间件…如果有配置则将这些中间件添加到chain中
  2. 获取engine的middlewares中间件,遍历添加到chain中,也就是我们通过Server的Use()方法,或rest下WithMiddleware()/WithMiddlewares()函数主动注册中间件,将这些中间件添加到chain中
  3. 调用chain的ThenFunc()方法,遍历所有中间件,调用中间件的Handle()方法,将中间件包装为Handler形成中间件Handler链,并将接口的处理Handler添加到了链条的末尾,在执行时匹配到handler后会基于这个链条向下调用到最终的接口处理函数
  4. 调用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
}
  1. 查看patRouter的Handle()方法添加路由构建前缀树的源码,内部会
  1. patRouter是在NewServer()函数创建服务Server内部通过NewRouter()初始化的,内部有个trees属性,保存了以Method为维度的多个前缀树
  2. 在Handle()方法中会根据当前路由的Method判断是否存在该类型的前缀树,如果存在则调用Tree的Add()方法添加,如果不存在,则先调用调用一个NewTree()新建一个前缀树,然后调用Tree的Add()方法添加
  3. Tree的Add()方法中会调用一个add()函数,该函数中,会根据"/“截取路由path中的每一段,作为token标识判断当前前缀树中是否已经存在该节点,如果存在则递归调用根据”/"继续截取当当前节点作为子节点判断添加前缀树中,
  4. 如果不存在则调用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

  1. 在调用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...)
}
  1. 以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)
}
  1. 到这里就来到了go的net/http标准库,具体参考go 进阶 http标准库相关: 三. HttpServer 服务启动到Accept等待接收连接,简单复习一下net/http提供服务的流程:
  1. 在通过net/http编写服务端时, 首先调用NewServeMux()创建多路复用器,编写对外接收请求的接口函数也就是处理器,然后调用多路复用器上的HandleFunc()方法,将接口与接口路径进行绑定,注册路由, 最后调用ListenAndServe()函数在指定端口开启监听,启动服务
  2. ListenAndServe()方法内部重点调用了"net.Listen(“tcp”, addr)"多路复用相关初始化,初始化socket,端口连接绑定,开启监听,调用"srv.Serve(ln)”:等待接收客户端连接Accept(),与接收到连接后的处理流程
  3. 服务相关的我们先关注"srv.Serve(ln)",方法内通过for开启了一个死循环,在循环内部,调用Listener的Accept()方法,假设当前是TCP连接调用的就是TCPListener下的Accept(),监听客户端连接,当接收到客户端连接后,通过开启协程执行serve()方法处理请求,每一个连接开启一个goroutine来处理

接收请求的处理

  1. go-zero底层是基于net/http实现的,再看一下net/http接收请求时底层是如何执行的go 进阶 http标准库相关: 五. HttpServer 接收请求路由发现原理,
  2. 简单复习一下,基于net/http搭建服务时,底层会执行ListenAndServe()方法,最终会执行到Listener的Accept()方法,假设当前是TCP连接调用的就是TCPListener下的Accept(),阻塞监听客户端连接,当有接收到连接请求后Accept()方法返回,拿到一个新的net.Conn连接实例,然后开启协程调用Conn连接实例上的serve()方法处理客户端请求,查看这个serve()方法:
  1. 首先调用newBufioReader() 封装了一个bufio.Reader
  2. 开启了一个无限for循环,循环内
  3. 调用conn的readRequest(ctx)方法读取请求的内容,比如解析HTTP请求协议,读取请求头,请求参数,封装Request和response,在解析时会读取请求头的 Content-Length,不为 0会通过TCPConn.Read() 方法读取指定长度的数据并存入请求体中,如果 Content-Length 为 0 或者没有设置,则请求体为空
  4. 封装serverHandler调用serverHandler上的ServeHTTP(w, w.req)方法进行路由匹配,找到对应的处理函数,执行我们写的业务逻辑
  5. 调用response的finishRequest()方法进行最后处理工作,当底层 bufio.Writer 缓冲区的大小达到阈值或者Flush() 被显式调用时,就会将缓冲区内的数据写入到底层连接中,并触发 Conn 的 Write() 方法将数据发送到客户端,另外finishRequest()方法还会进行一些比如异常处理,资源回收,状态更新等操作
  6. 最后调用conn的setState()设置连接状态为StateIdle,方便后续重用连接
  1. 这里执行的ServeHTTP()就是匹配路由触发业务接口的函数,ServeHTTP是一个接口,绝大多数Web框架都是通过实现该接口,从而替换掉Golang默认的路由,这里执行的就是patRouter实现的ServeHTTP(),查看该函数源码
  1. 首先根据请求的method在trees中获取到指定前缀树,如果存在则根据请求的reqPath路径在前缀树中查找对应的handler对象,如果找到,则调用这个handler对象的ServeHTTP()方法
  2. 在服务启动时会将添加的中间件添加到chain处理器链中,然后遍历所有中间件转换为handler链,并将接口处理函数添加到handler链的末尾,这个过程是在chain类型的Then()方法中完成的。
  3. 在路由匹配时拿到第一个中间件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,并返回
	}
}

三. 总结

  1. go-zero http服务是怎么启动的, 是怎么整合net/http的, 前缀树是怎么构建的,路由,中间件是怎么注册的,当接收到请求后是怎么匹配路由执行的,当问到这些问题都可以由rest下的NewServer()函数创建服务Server开始说,下面总结一下
  2. 了解go-zero http服务首先要了解几个结构体,比如engine服务引擎,首先要创建这个引擎,内部有几个比较重要的属性比如
  1. RestConf类型的conf属性: 存储了服务启动运行所需的配置信息
  2. featuredRoutes类型的切片routes属性,在执行AddRoutes()注册接口时会将将接口封装为Router,然后将Router通过AddRoutes()保存到这个切片属性中,服务启动时会通过这个切片获取到所有路由,构建前缀树进行路由注册
  3. Middleware类型切片的middlewares属性,会将通过Server的Use()方法注册的全局中间件保存到这个切片属性中,后续会通过这个属性获取到注册的中间件,加上局部中间件,接口handler转换为chain执行链,转换为Handler处理器链
  1. 还有一个比较重要的结构体patRouter, 内部存在一个trees属性,内部存储了以路由的method为key的前缀树,patRouter是路由注册与请求执行的核心,该结构体实现了ServeHTTP()方法,会通过这个方法处理用户的请求
  2. 说一下服务启动的执行过程,在构建go-zero http服务时,首先需要将配置设置到RestConf结构体变量上,通过执行rest下的NewServer()函数,读取配置创建服务端的Server,查看这个NewServer()源码
  1. 首先调用调用newEngine()创建Engine服务引擎
  2. 调用NewRouter()函数,初始化patRouter,初始化patRouter中的trees路由前缀树
  3. 将这两个属性封装到了一个Server结构体变量,并返回,然后通过Server结构体变量调用Use()方法注册中间件,调用AddRoutes()方法注册路由,调用Start()方法启动服务
  1. 中间件的注册: 在go-zero中可以通过Server的Use()方法注册全局中间件,可以通过WithMiddlewares/WithMiddleware()函数注册针对一组路由的局部中间件
  1. 通过Use()方法注册的中间件会保存到Engine的middlewares属性中
  2. 通过WithMiddlewares/WithMiddleware()函数注册的局部中间,查看源码发现,中间件函数会封装到Route的Handler中,跟随Route路由一块注册
  3. 另外go-zero的rest包下还有一个WithChain()也可以用来注册中举中间件,但是通过该函数注册的中间件会保存到Engine的chain属性中,是一个中间件链条
  1. 路由的保存: 当通过NewServer()拿到服务Server以后,调用Server的AddRoutes()方法,将对外的接口封装为Route进行路由后注册,在AddRoutes()方法中,将路由封装为featuredRoutes,保存到了engine的routes 属性中,这是保存,后续构建前缀树的逻辑在Server的Start()启动服务方法中
  2. 当拿到服务Server以后,查看Server的Start()启动服务方法,内部会调用engine的start()方法,内部重点执行了:
  1. bindRoutes(): 注册路由中间件,构建前缀树
  2. 判断如果没有配置证书,执行StartHttp()启动http服务,有配置执行StartHttps()启动https服务,触发到net/http
  1. bindRoutes()中间件,路由的注册与前缀树的构建,通过engine的routes属性获取到所有路由,遍历调用engine的bindFeaturedRoutes()方法开始注册路由,内部会调用engine的bindRoute(),在该方法中
  1. 首先获取engine的chain属性,如果为空,会调用buildChainWithNativeMiddlewares()在该方法中根据配置信息判断添加一下默认的中间件,比如判断是否配置了追踪请求的Trace中间件, 记录请求日志的Log中间件,收集请求指标数据Prometheus中间件,限制最大并发连接数的MaxConns中间件,实现熔断机制Breaker中间件 ,实现流量控制Shedding中间件,设置请求超时时间的Timeout中间件,恢复异常请求的Recover中间件…如果有配置则将这些中间件添加到chain中
  2. 遍历engine的middlewares也就是拿到全局中间件,将这些中间件也添加到到engine的chain属性中整个中间件链条封装完毕
  3. 比较重要的一个步骤,拿到路由接口的处理器Handler,执行chain的ThenFunc()方法,将保存了中间件链条的chain转换为Handler链,并将接口的处理器Handler添加到Handler链的末尾(在接收请求时根据路由匹配拿到指定的Handler后会基于这个链条向下调用到最终的接口处理函数)
  4. 调用Router的Handle()方法,将路由注册到路由树上,也就是前缀树的构建,实际执行的是patRouter的Handle(),方法中:
  5. 根据当前路由的Method判断是否存在该类型的前缀树,如果存在则调用Tree的Add()方法添加,如果不存在,则先调用调用一个NewTree()新建一个前缀树,然后调用Tree的Add()方法添加
  6. Tree的Add()方法中会调用一个add()函数,该函数中,会根据"/“截取路由path中的每一段,作为token标识判断当前前缀树中是否已经存在该节点,如果存在则递归调用根据”/"继续截取当当前节点作为子节点判断添加前缀树中,
  7. 如果不存在则调用newNode()函数新建node节点,.以当前路由截取到的token标识为key添加到前缀树中
  1. Server的Start()方法服务的启动, 查看源码内部会根据是否配置了证书选择调用StartHttp()/StartHttps()启动http或https服务,以http为例,查看StartHttp()源码内最终封装调用了net/http标准库中的ListenAndServe()
  2. 了解go-zero http服务怎么接收请求执行的,要先了解net/http是怎么请求,路由匹配的,在net/http处理请求时会调用路由的ServeHTTP()方法,这里调用的就是patRouter上的这个方法,查看源码:
  1. 首先根据请求的method在trees中获取到指定前缀树,如果存在则根据请求的reqPath路径在前缀树中查找对应的handler对象,如果找到,则调用这个handler对象的ServeHTTP()方法
  2. 在服务启动时会将添加的中间件添加到chain处理器链中,然后遍历所有中间件转换为handler链,并将接口处理函数添加到handler链的末尾,这个过程是在chain类型的Then()方法中完成的。
  3. 在路由匹配时拿到第一个中间件handler开始执行,如果执行通过,中间件中会继续调用ServeHTTP(),也就是继续执行下一个中间件,一直执行到接口处理函数
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
根据提供的引用内容,"container exited with a non-zero exit code 239. Error file: prelaunch.err" 是一个错误信息,表示容器以非零退出代码239退出,并且错误文件为prelaunch.err。这通常意味着在容器运行过程中发生了错误。 要解决这个问题,可以尝试以下几个步骤: 1. 检查错误文件:prelaunch.err是一个错误文件,其中可能包含有关容器退出的更多详细信息。你可以查看该文件以了解更多信息,例如具体的错误消息或异常堆栈跟踪。 2. 检查日志文件:除了prelaunch.err之外,还应该检查其他日志文件,例如应用程序的日志文件或容器管理器的日志文件。这些日志文件可能包含有关容器退出的更多信息,帮助你确定问题的根本原因。 3. 检查资源限制:容器退出的原因可能是由于资源限制导致的。你可以检查容器的资源配置,例如内存限制、CPU限制等,确保它们足够满足应用程序的需求。 4. 检查应用程序配置:容器退出的原因可能与应用程序的配置有关。你可以检查应用程序的配置文件,确保它们正确设置,并且没有任何错误或冲突。 5. 检查依赖项:容器退出的原因可能是由于缺少或错误的依赖项导致的。你可以检查应用程序的依赖项,确保它们正确安装并且与应用程序兼容。 6. 检查网络连接:容器退出的原因可能与网络连接有关。你可以检查网络连接是否正常,并确保容器可以访问所需的资源和服务。 请注意,以上步骤仅为一般性建议,具体解决方法可能因环境和具体情况而异。如果以上步骤无法解决问题,建议查阅相关文档或寻求专业支持以获取更详细的帮助。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值