HTTP 框架 — Hertz
前言
在这个以云原生为技术支撑的背景下,各个公司都开源了自己的微服务框架或者产品,如耳熟能详的 istio、envoy 、kratos、go-zero 等。
当然字节跳动也不会落下对微服务领域的探索与研究,这不前段时间就开源了 超大规模的企业级微服务 HTTP 框架 — Hertz
1 什么是 Hertz
官方 GitHub:https://github.com/cloudwego/hertz
官方文档:https://www.cloudwego.io/zh/docs/hertz/
Hertz 是一个超大规模的企业级微服务 HTTP 框架,具有高易用性、易扩展、低时延等特点。在设计之初参考了其他开源框架 fasthttp、gin、echo 的优势
1.1 架构特点
-
高易用性
在开发过程中,快速写出来正确的代码往往是更重要的。因此,在 Hertz 在迭代过程中,积极听取用户意见,持续打磨框架,希望为用户提供一个更好的使用体验,帮助用户更快的写出正确的代码。
-
高性能
Hertz 默认使用自研的高性能网络库 Netpoll,在一些特殊场景相较于 go net,Hertz 在 QPS、时延上均具有一定优势。关于性能数据,可参考下图 Echo 数据。
-
扩展性
Hertz 采用了分层设计,提供了较多的接口以及默认的扩展实现,用户也可以自行扩展。同时得益于框架的分层设计,框架的扩展性也会大很多。目前仅将稳定的能力开源给社区,更多的规划参考 RoadMap。
-
多协议支持
Hertz 框架原生提供 HTTP1.1、ALPN 协议支持。除此之外,由于分层设计,Hertz 甚至支持自定义构建协议解析逻辑,以满足协议层扩展的任意需求。
-
网络层切换能力
Hertz 实现了 Netpoll 和 Golang 原生网络库 间按需切换能力,用户可以针对不同的场景选择合适的网络库,同时也支持以插件的方式为 Hertz 扩展网络库实现。
2 Hertz 初体验
2.1 安装 Hertz
# 安装 HZ
$ go install github.com/cloudwego/hertz/cmd/hz@latest
# 创建存放源码目录
$ mkdir -p $(go env GOPATH)/src/github.com/cloudwego
$ cd $(go env GOPATH)/src/github.com/cloudwego
# 创建目录并创建一个新的 hz 项目
$ mkdir hertz_demo
$ cd hertz_demo/ && hz new
# 可以看到通过 hz new 初始化之后会生成对应的几个文件
$ ll
total 36
drwxr-xr-x 3 root root 4096 Jul 16 23:00 ./
drwxr-xr-x 3 root root 4096 Jul 16 23:00 ../
drwxr--r-- 4 root root 4096 Jul 16 23:00 biz/
-rwxr-xr-x 1 root root 351 Jul 16 23:00 .gitignore*
-rwxr-xr-x 1 root root 42 Jul 16 23:00 go.mod*
-rwxr-xr-x 1 root root 58 Jul 16 23:00 .hz*
-rwxr-xr-x 1 root root 173 Jul 16 23:00 main.go*
-rwxr-xr-x 1 root root 300 Jul 16 23:00 router_gen.go*
-rwxr-xr-x 1 root root 309 Jul 16 23:00 router.go*
# 解决依赖
$ go mod tidy
2.2 简单示例体验
这里我以官方文档中的代码示例为基础,然后再逐步刨析源码慢慢理解 Hertz 框架的特色以及使用
2.2.1 代码示例—hello
package main
import (
"context"
"fmt"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
)
func main() {
// 1.初始化 engine
h := server.Default()
// 2.注册路由
h.GET("/hello", func(ctx context.Context, c *app.RequestContext) {
c.String(200, "hello!!")
})
fmt.Println("http://10.0.0.134:8888/hello")
// 3.启动程序
h.Run()
}
其实从这个示例中可以看到其实代码结构和 gin 框架很像,无非都是以下几个步骤:
- 初始化一个 engine
- 注册一个路由
- 启动程序
2.2.2 Hertz 工作流程
- Engine 容器对象,整个框架的基础
- Engine.trees 负责存储路由和handle方法的映射,采用类似字典树的结构
- Engine.RouterGroup ,其中的Handlers存储着所有中间件
- Context 上下文对象,负责处理 请求和回应 ,其中的 handlers 是存储处理请求时中间件和处理方法的
2.2.3 hello 示例源码解读
2.2.3.1 server.Default()
那么我们看看这个官方 hello 示例中 Default()
的源码内容:
// server.Default():该函数其实就可以理解为一个构建函数,返回的就是一个 *Hertz 的初始化实列
func Default(opts ...config.Option) *Hertz {
// h := New()
h := New(opts...)
// h.Use() 注册中间件
h.Use(recovery.Recovery())
return h
}
在 Hertz
框架中很多的功能实现都是基于 Engine 结构体来实现
2.2.3.2 engine = New()
通过调用 New()
方法来实例化 Engine
结构体
-
初始化了 Engine ,因为
func Default() *Hertz {}
默认返回的就是一个engine
-
将
RouterGroup
的Handlers()
设置成nil
,basePath()
设置成/
-
为了使用方便,
RouteGroup
里面也有一个*Engine
, 这里将刚刚初始化的Engine
赋值给了RouterGroup
的Engine
指针 -
为了防止频繁的
context GC
造成效率的降低, 在Engine
里使用了sync.Pool
, 专门存储Hertz
的Context
那么我们接着看看返回的这个实例 *Hertz
究竟是什么内容
可以看到 Hertz 结构体里面其实调用的是一个 *route.Engine
结构体
// Hertz is the core struct of hertz.
type Hertz struct {
*route.Engine
}
// New creates a hertz instance without any default config.
func New(opts ...config.Option) *Hertz {
options := config.NewOptions(opts)
h := &Hertz{
Engine: route.NewEngine(options),
}
return h
}
route.NewEngin
中可以看到下面内容:
func NewEngine(opt *config.Options) *Engine {
// Engine 容器对象,整个框架的基础
engine := &Engine{ // 初始化语句
// 树结构,保存路由和处理方法的映射
trees: make(MethodTrees, 0, 9),
// handlers 全局中间件组件在注册路由时使用
RouterGroup: RouterGroup{ // Engine.RouterGroup,其中的 Handlers 存储了所有中间件
Handlers: nil,
basePath: "/",
root: true,
},
transport: defaultTransporter(opt),
tracerCtl: &internalStats.Controller{},
protocolServers: make(map[string]protocol.Server),
enableTrace: true,
options: opt,
}
engine.RouterGroup.engine = engine
traceLevel := initTrace(engine)
// prepare RequestContext pool
engine.ctxPool.New = func() interface{} {
ctx := engine.allocateContext()
if engine.enableTrace {
ti := traceinfo.NewTraceInfo()
ti.Stats().SetLevel(traceLevel)
ctx.SetTraceInfo(ti)
}
return ctx
}
// Init protocolSuite
engine.protocolSuite = suite.New()
return engine
}
为了使用方便 New()
函数默认返回 *Engine
指针,从而实现数据的初始化实例,然后再将路由保存到了 methodTrees
(访问路径的树形结构,比如 RUL
默认是 /
开始,而且在 /
下面有多级子目录的这么一个结构),其实这里的 methodTrees
就是用来实现匹配路由规则的,也就是说当用户访问到某一级路由之后从而实现对应的处理函数,这就是 methodTrees
的作用
2.2.3.3 h.Use() 注册中间件
Recovery()
中间件源码
func Recovery() app.HandlerFunc {
return func(c context.Context, ctx *app.RequestContext) {
defer func() {
if err := recover(); err != nil {
stack := stack(3)
hlog.CtxErrorf(c, "[Recovery] %s panic recovered:\n%s\n%s\n",
timeFormat(time.Now()), err, stack)
ctx.AbortWithStatus(consts.StatusInternalServerError)
}
}()
ctx.Next(c)
}
}
其实中间件就是当我们进行路由匹配之前我们需要提前执行的这么几个函数而已。
Hertz
框架可以看出和 gin 很像,我们可以首先从我们最常用的h := server.Default()
的Default
函数开始看- 它内部构造一个新的
engine
之后就通过Use()
函数注册了Recovery()
中间件 Use()
就是Hertz
的引入中间件的入口了.- 仔细分析这个函数, 不难发现
Use()
其实是在给RouteGroup
引入中间件的. - 具体是如何让中间件在
RouteGroup
上起到作用的, 等说到RouteGroup
再具体说.
func Default(opts ...config.Option) *Hertz {
h := New(opts...)
// 默认注册中间件
h.Use(recovery.Recovery())
return h
}
h.Use()
调用RouterGroup.Use
往RouterGroup.Handlers
写入记录
func (engine *Engine) Use(middleware ...app.HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...)
engine.rebuild404Handlers() // 注册 404 处理方法
engine.rebuild405Handlers() // 注册 405 处理方法
return engine
}
// 其中 Handlers 字段就是一组数组,用来存储中间件
func (group *RouterGroup) Use(middleware ...app.HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}
组成一条处理函数链
HandlersChain
- 也就是说,我们会将一个路由的中间件函数和处理函数结合到一起组成一条处理函数链
HandlersChain
- 而它的本质就是一个由
HandlerFunc
组成的切片
type HandlersChain []HandlerFunc
中间件执行
其中 c.Next()
就是很关键的一步,它的代码很简单
-
从下面的代码可以看到,这里通过索引遍历
HandlersChain
链条 -
从而实现依次调用该路由的每一个函数(中间件或处理请求的函数)
-
我们可以在中间件函数中通过再次调用
c.Next()
实现嵌套调用(func1中调用func2;func2中调用func3)
func (ctx *RequestContext) Next(c context.Context) {
ctx.index++
for ctx.index < int8(len(ctx.handlers)) {
ctx.handlers[ctx.index](c, ctx)
ctx.index++
}
}
2.2.3.4 注册路由
h.GET("/hello", func(ctx context.Context, c *app.RequestContext) {
c.String(200, "hello!!")
})
通过 Get 方法将路由和处理视图函数注册
// h.GET 源码
func (group *RouterGroup) GET(relativePath string, handlers ...app.HandlerFunc) IRoutes {
return group.handle(consts.MethodGet, relativePath, handlers)
}
func (group *RouterGroup) handle(httpMethod, relativePath string, handlers app.HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
// 将处理请求的函数与中间件函数结合
handlers = group.combineHandlers(handlers)
// 调用 addRoute 方法注册路由
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}
- addRoute构造路由树
- 这段代码就是利用method, path, 将handlers注册到engine的trees中.
- 注意这里为什么是HandlersChain呢, 可以简单说一下, 就是将中间件和处理函数都注册到 method, path 的 tree 中了
也就是当我们访问对应的 URL 的时候就会触发路由树并匹配对应的处理函数进行处理
2.2.3.5 h.Spin()
h.Spin()
源码:
通过官方的注释可以看到这个是一个用来直接运行服务的这么一个方法, 并且可以捕获状态。
但是从源码中其实最为核心的可以说是 h.Run()
方法
// Spin runs the server until catching os.Signal.
// SIGTERM triggers immediately close.
// SIGHUP|SIGINT triggers graceful shutdown.
func (h *Hertz) Spin() {
errCh := make(chan error)
// 开启多线程,所以为啥 Hertz 在对外宣称自己是一款能够承载高并发高负载的框架,我想通过这个细节也能看出来 Hertz 对传统框架的改进
go func() {
errCh <- h.Run()
}()
if err := waitSignal(errCh); err != nil {
hlog.Errorf("HERTZ: Receive close signal: error=%v", err)
if err := h.Engine.Close(); err != nil {
hlog.Errorf("HERTZ: Close error=%v", err)
}
return
}
hlog.Infof("HERTZ: Begin graceful shutdown, wait at most num=%d seconds...", h.GetOptions().ExitWaitTimeout/time.Second)
ctx, cancel := context.WithTimeout(context.Background(), h.GetOptions().ExitWaitTimeout)
defer cancel()
if err := h.Shutdown(ctx); err != nil {
hlog.Errorf("HERTZ: Shutdown error=%v", err)
}
}
h.Run()
源码:
- 通过调用 net/http 来启动服务,由于 engine 实现了 ServeHTTP 方法
- 只需要直接传 engine 对象就可以完成初始化并启动
func (engine *Engine) Run() (err error) {
if err = engine.Init(); err != nil {
return err
}
if !atomic.CompareAndSwapUint32(&engine.status, statusInitialized, statusRunning) {
return errAlreadyRunning
}
defer atomic.StoreUint32(&engine.status, statusClosed)
// 在golang中,你要构建一个web服务,必然要用到http.ListenAndServe
return engine.listenAndServe()
}