前言
hertz是公司开源的http框架,相比gin框架全面优化了协议、路由等核心模块的性能,如今也已经在cloudwego上开源(github.com/cloudwego),…
hertz的学习文档主要重点在于整体架构以及性能优化点上,而对于源码的解读文档目前较少,本文将聚焦hertz处理一次http请求的源码,分析hertz的协议层读写流程及路由树匹配流程。
启动流程简介
func main() {
h := server.Default()
h.StaticFS("/", &app.FS{Root: "./", GenerateIndexPages: true})
h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
})
......
h.Spin()
}
首先通过调用server.Default()方法创建一个新的Hertz结构体,结构体的代码注释为:
// Hertz is the core struct of hertz.
由此可见,该结构体就是整个hertz框架的核心,网络连接、路由、中间件等多种功能都是依据Hertz结构体展开的,下面看一下Hertz结构体:
type Hertz struct {
*route.Engine
signalWaiter func(err chan error) error
}
type Engine struct {
......
// engine name
Name string
serverName atomic.Value
// Options for route and protocol server
options *config.Options
// route
RouterGroup
trees MethodTrees
......
// RequestContext pool
ctxPool sync.Pool
......
}
有gin框架基础的同学这里可以很明显地关注到三个模块:RouterGroup、trees、ctxPool,与gin中的核心数据结构完全吻合,即路由组、方法路由树、Context对象内存池,可以大胆猜测他们的核心功能大差不差。
第五行h.GET()方法即注册路由,这块儿主要是维护了压缩前缀树(radix tree)对路由进行注册,后面路由匹配时再将路由对应的节点挂载的handler拿出来并执行。
本文的核心在于hertz的http请求处理流程,我们从h.Spin()方法进行切入:
// Spin runs the server until catching os.Signal or error returned by h.Run().
func (h *Hertz) Spin() {
errCh := make(chan error)
h.initOnRunHooks(errCh)
go func() {
errCh <- h.Run()
}()
signalWaiter := waitSignal
......
if err := signalWaiter(errCh); err != nil {
......
return
}
......
}
func (engine *Engine) Run() (err error) {
......
return engine.listenAndServe()
}
func (engine *Engine) listenAndServe() error {
......
return engine.transport.ListenAndServe(engine.onData)
}
第24行底层使用网络库netpoll,构建EventLoop并启动服务监听,并在接收客户端请求时,执行engine.onData所指代的方法。至此,http服务端启动完毕。
源码解读—请求处理流程
一次http请求的处理流程
(*Engine).onData方法包含了hertz的读、处理、写流程。在完成服务监听后,收到客户端请求就会执行onData来进行相应的流转,这里简化一下,只看HTTP1.1版本协议对应的处理流程:
func (engine *Engine) onData(c context.Context, conn interface{}) (err error) {
switch conn := conn.(type) {
case network.Conn:
err = engine.Serve(c, conn)
case network.StreamConn:
err = engine.ServeStream(c, conn)
}
return
}
func (engine *Engine) Serve(c context.Context, conn network.Conn) (err error) {
defer func() {
errProcess(conn, err)
}()
......
// HTTP1 path
err = engine.protocolServers[suite.HTTP1].Serve(c, conn)
return
}
err = engine.protocolServers[suite.HTTP1].Serve(c, conn) 这行代码会调用到pkg.protocol.http1.Server.Service()方法,这块儿代码比较多,可总结为以下流程:
上述流程不包含Trace上报,步骤大致可概括为:
-
从内存池中获取RequestContext(简称ctx),这时的ctx一定是初始化过的,可直接拿来使用
-
对当前ctx进行基本属性参数赋值,并将抓取到的连接network.Conn赋值给ctx
-
拉起for true循环
-
判断参数connRequestNum,在每次循环该参数都会+1
-
若connRequestNum>1,则说明本连接为长连接,使用的底层网络库netpoll有个特性,即长连接需要保证两次数据报接受间隔在idleTimeout之内,故将当前读取超时时间设置为idleTimeout,并调用zeroCopyReader.Peek(),若未在idleTimeout之内peek到4个字节的数据,则抛出idlTimeoutError,退出本次ctx处理;若在idleTimeout之内peek到数据,则继续往下走
-
若connRequestNum<=1,则说明为首次循环,直接往下走
-
-
响应头基本属性信息设置
-
读请求头信息。若失败,则根据error决策是否抛出,并终止流程
-
读取请求体信息。若失败,则根据error决策是否抛出,并终止流程
-
检查请求头是否为"Expect:100-continue"的POST请求
-
是,则先发送"HTTP/1.1 100 Continue\r\n\r\n"到对等端,告知client端是否同意接收POST数据,若同意,然后再去读取客户端后续发送的请求数据
-
否,则此时本次所有请求数据已经读取至当前ctx.Request缓存中
-
-
调用Engine.ServeHTTP,处理接收到的请求
-
写ctx.Response,并将返回数据发送至对等端
-
收尾工作,如ctx初始化并放回context内存池、释放未释放的zeroCopyReader等
-
hertz处理核心方法 — Engine.ServeHTTP
Engine.ServeHTTP方法核心为路由寻址。在读取到请求信息后,hertz进行前缀树的路由匹配,并使用目标路径节点的handlerChain对http的处理进行增强封装。主要流程为:
路由核心数据结构 — 路由树
路由寻址的核心在于路由树路径匹配方法(*router).find,即寻找当前http请求地址所映射到的handlerChain。在梳理其核心流程前,首先看下路由树的核心结构——压缩前缀树:
type MethodTrees []*router
type router struct {
method string
root *node
hasTsrHandler map[string]bool
}
type (
node struct {
kind kind // 树节点的类型:static/param/any
label byte // 标签 这里一般指代的是前缀prefix第一位 如param节点的label是":" any节点的label是"*"
prefix string // 当前节点前缀
parent *node // 父节点 用以实现寻址回溯
children children // 子节点
// original path
ppath string // 当前节点绝对路径
// param names
pnames []string // 参数名 比如当前节点前缀为:id,则pnames为id
handlers app.HandlersChain // 当前节点关联的handlerChain
paramChild *node // 参数子节点
anyChild *node // 通配符子节点
// isLeaf indicates that node does not have child routes
isLeaf bool // 是否为叶子节点(gin中标识为是否为根节点)
}
kind uint8
children []*node
)
压缩前缀树(radix tree)是对前缀树(trie tree)的改良版本,改良点主要在于,如果树子节点是其父节点的唯一子节点,那么将该对父子节点进行合并,如下图所示:
路由树寻址流程可概括为下:
需要注意以下几点
-
树节点有三种类型,按匹配优先级排序为:static>param>any。
-
回溯方法返回的并不是真实的节点类型,而是回溯完毕之后需要使用什么逻辑进行处理。比如说,我们按照优先级static>param>any,static节点没有匹配到,进行回溯找到其父节点,这时虽然其static子节点没有成功匹配,但是可能还有param/any子节点,还是可以进行匹配的,所以此时就需要指定param/any的匹配逻辑去执行。
-
回溯方法会reset掉本次寻址已经加上的paramIdx、searchIdx等下标数据,并会回退掉search。paramIdx主要用于在对应下标下存储paramValue,而searchIdx主要用做回溯之后的search回退。
-
打标重定向后并不会强制结束流程,但后续匹配会失败,在外层做重定向处理。
路由树匹配方法到这里已经整明白了,至于路由树的注册流程本文就不再赘述了
总结
本文从源码角度总结了hertz的http处理流程以及路由树寻址流程,算是整体串了一下hertz的核心源码流程,供对http框架有兴趣的朋友们一同学习交流,更加深入的底层能力介绍以及性能优化实战可参考官方文档。