一、前言

Hertz[həːts] 是一个 Golang 微服务 HTTP 框架,在设计之初参考了其他开源框架 fasthttpginecho 的优势, 并结合字节跳动内部的需求,使其具有高易用性、高性能、高扩展性等特点,目前在字节跳动内部已广泛使用。 如今越来越多的微服务选择使用 Golang,如果对微服务性能有要求,又希望框架能够充分满足内部的可定制化需求,Hertz 会是一个不错的选择。

对于源码该如何阅读,本身就值得思考。这篇文章我将以第一次阅读Hertz源码的视角,分享自己的思考过程,也借此梳理一下自己阅读源码的方法论。

接下来需要你对应打开Hertz的官方文档,以及在本地克隆Hertz的代码仓库,我们开始吧。

Hertz仓库地址:https://github.com/cloudwego/hertz

Hertz文档地址:https://www.cloudwego.io/zh/docs/hertz/getting-started/

二、架构设计

这是一张Hertz官方文档的架构设计图,图中的一个个组件对应hertz源码包内的一个个package文件夹,实现了对应的功能,如下:

三、快速开始

接下来按照文档的指示,通过hertz的命令行工具初始化一个最简单的hertz项目,先观其形,再会其意。

对应文档地址:https://www.cloudwego.io/zh/docs/hertz/getting-started/

# 安装hertz的命令行工具,用于生成hertz初始代码
go install github.com/cloudwego/hertz/cmd/hz@latest
# 通过hz工具生成代码,如果创建的项目不在GOPATH/src路径下,则需要额外声明-module参数
hz new -module hertz-study

此时按照文档指示,对项目进行编译运行可以访问这个HTTP服务了,它默认实现了一个/ping接口。

curl http://127.0.0.1:8888/ping
# 响应
{"message":"pong"}%

四、源码解析

server概览

首先看一下main.go函数,这是hertz服务的启动入口,大概可以猜测内容是:1. 初始化了一个默认的hz服务;2. 完成了一些注册工作;3. 启动hz服务(HTTP服务)。

func main() {
  h := server.Default()

  register(h)
  h.Spin()
}

回想刚刚这个 http://127.0.0.1:8888/ping 的接口服务,它所声明的IP和Port并未由你手动指定,并且/ping接口也不是你编写的,或许是这个server.Default()的作用。

反之我如果需要指定HTTP服务启动的各种定制化的配置,是否是给这个server.Default()传参数?又或者是换一个创建h的方法?

Default()

// Default creates a hertz instance with default middlewares.
func Default(opts ...config.Option) *Hertz {
  h := New(opts...)
  h.Use(recovery.Recovery())

  return h
}

查看Default()方法,发现确实可以传入参数(猜测就是可以自定义配置的内容),然后我们进一步分析New方法的内容,它接受了一个不定长度的Option数组为参。

// Option is the only struct that can be used to set Options.
type Option struct {
F func(o *Options)
}

// 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
}

接着我们再进入config.NewOptions方法观察这个Option切片将如何把我们自定义的内容应用到Hertz服务的初始化上去。

func NewOptions(opts []Option) *Options {
  options := &Options{
     KeepAliveTimeout: defaultKeepAliveTimeout,
     ReadTimeout: defaultReadTimeout,
     IdleTimeout: defaultReadTimeout,
     RedirectTrailingSlash: true,
     RedirectFixedPath: false,
     HandleMethodNotAllowed: false,
     UseRawPath: false,
     RemoveExtraSlash: false,
     UnescapePathValues: true,
     DisablePreParseMultipartForm: false,
     Network: defaultNetwork,
     Addr: defaultAddr,
     MaxRequestBodySize: defaultMaxRequestBodySize,
     MaxKeepBodySize: defaultMaxRequestBodySize,
     GetOnly: false,
     DisableKeepalive: false,
     StreamRequestBody: false,
     NoDefaultServerHeader: false,
     ExitWaitTimeout: defaultWaitExitTimeout,
     TLS: nil,
     ReadBufferSize: defaultReadBufferSize,
     ALPN: false,
     H2C: false,
     Tracers: []interface{}{},
     TraceLevel: new(interface{}),
     Registry: registry.NoopRegistry,
  }
  // 将自定义配置应用上去的方法
  options.Apply(opts)
  return options
}

func (o *Options) Apply(opts []Option) {
for _, op := range opts {
op.F(o)
}
}

通过观察config.NewOptions源码,它首先初始化了一个Options结构,这个结构存放了Hertz服务的各种初始化信息,此时的Options的各个属性都是默认固定的,直到调用了options.Apply(opts)方法,将自定义的配置应用上去。

并且应用上去的方式很特别,它将这个默认创建的Options结构的指针作为参数传递给每一个你声明的Option的F方法,通过F方法的调用去为Options结构赋值,因为是指针,自然能将所有的赋值应用到同一个Options上去。

而具体的Option的F方法如何定义,则可以灵活实现,这也是Hertz拥有良好扩展性的原因之一。

// Default creates a hertz instance with default middlewares.
func Default(opts ...config.Option) *Hertz {
 // h是*Hertz类型,是框架的核心结构
  h := New(opts...)
  h.Use(recovery.Recovery())

  return h
}

此时注意到还有一个h.Use(recovery.Recovery())方法,写法很像是gin框架的中间件使用方式。

// Recovery returns a middleware that recovers from any panic and writes a 500 if there was one.
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)
  }
}

通过阅读注释确实发现这是个中间件,用于从panic中recover。

register()

func main() {
  h := server.Default()

  register(h)
  h.Spin()
}

回到最初的main方法中,经过分析我们知道了Default方法大致完成了默认(自定义)Hertz结构的声明,下面看一下register函数的内容

// register registers all routers.
func register(r *server.Hertz) {

  router.GeneratedRegister(r)

  customizedRegister(r)
}

// GeneratedRegister registers routers generated by IDL.
func GeneratedRegister(r *server.Hertz) {
//INSERT_POINT: DO NOT DELETE THIS LINE!
}

// customizeRegister registers customize routers.
func customizedRegister(r *server.Hertz) {
r.GET("/ping", handler.Ping)

// your code ...
}

register(h)的工作是路由注册(也就是接口的声明),内部完成了两种类型的注册,GeneratedRegister()的注释指出这部分路由是由IDL生成的,关于IDL先卖个关子,你只要知道IDL描述了接口交互的结构。

customizedRegister()则是用于注册自定义的路由接口,并且初始化了一个你熟悉的/ping,当然也你可以在这里注册自己需要的路由,使用的方式也与gin很相似。

Spin()

最后分析一下main方法中的的第三部分,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 h.signalWaiter != nil {
     signalWaiter = h.signalWaiter
  }

  if err := signalWaiter(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)
  }
}

完成了一系列的初始化和声明操作之后,Spin()负责触发Hertz的运行,并且处理运行过程中的各种异常。其核心是errCh <- h.Run()

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)

  // trigger hooks if any
  ctx := context.Background()
  for i := range engine.OnRun {
     if err = engine.OnRun[i](ctx); err != nil {
        return err
    }
  }

  return engine.listenAndServe()
}

再看到末尾的engine.listenAndServe()方法,这是一个接口,查看其实现类,发现可以追溯到standard和netpoll两个包。

作为一个HTTP服务,最重要的就是提供网络通信交互能力,Hertz使用了可插拔的自研网络库netpoll负责网络通信,进一步优化了性能,这部分也将在后续的文章着重分析。

至此Hertz服务开始运行,你可以通过控制台请求:

curl http://127.0.0.1:8888/ping
{"message":"pong"}%

五、小结

使用hz工具生成最简易的Hertz代码后,本文粗浅地分析了main方法的内容,将其分为三个部分,服务配置声明Default()、路由注册register()、HTTP服务启动Spin()

虽然没有提及Hertz框架架构图当中的各种类型的package,但是其实处处有它们的身影,后续文章将以此文为基础,深入分析框架的各个功能组件,揭开Hertz的神秘面纱。

字节微服务HTTP框架Hertz使用与源码分析|拥抱开源的更多相关文章

  1. Hystrix微服务容错处理及回调方法源码分析

    前言 在 SpringCloud 微服务项目中,我们有了 Eureka 做服务的注册中心,进行服务的注册于发现和服务治理.使得我们可以摒弃硬编码式的 ip:端口 + 映射路径 来发送请求.我们有了 F ...

  2. 【集合框架】JDK1.8源码分析之HashMap(一) 转载

    [集合框架]JDK1.8源码分析之HashMap(一)   一.前言 在分析jdk1.8后的HashMap源码时,发现网上好多分析都是基于之前的jdk,而Java8的HashMap对之前做了较大的优化 ...

  3. 【集合框架】JDK1.8源码分析之ArrayList详解(一)

    [集合框架]JDK1.8源码分析之ArrayList详解(一) 一. 从ArrayList字表面推测 ArrayList类的命名是由Array和List单词组合而成,Array的中文意思是数组,Lis ...

  4. MyBatis框架的使用及源码分析(十一) StatementHandler

    我们回忆一下<MyBatis框架的使用及源码分析(十) CacheExecutor,SimpleExecutor,BatchExecutor ,ReuseExecutor> , 这4个Ex ...

  5. MyBatis框架的使用及源码分析(九) Executor

    从<MyBatis框架的使用及源码分析(八) MapperMethod>文中我们知道执行Mapper的每一个接口方法,最后调用的是MapperMethod.execute方法.而当执行Ma ...

  6. zookeeper服务发现实战及原理--spring-cloud-zookeeper源码分析

    1.为什么要服务发现? 服务实例的网络位置都是动态分配的.由于扩展.失败和升级,服务实例会经常动态改变,因此,客户端代码需要使用更加复杂的服务发现机制. 2.常见的服务发现开源组件 etcd—用于共享 ...

  7. 【集合框架】JDK1.8源码分析之Comparable && Comparator(九)

    一.前言 在Java集合框架里面,各种集合的操作很大程度上都离不开Comparable和Comparator,虽然它们与集合没有显示的关系,但是它们只有在集合里面的时候才能发挥最大的威力.下面是开始我 ...

  8. 【集合框架】JDK1.8源码分析之Collections && Arrays(十)

    一.前言 整个集合框架的常用类我们已经分析完成了,但是还有两个工具类我们还没有进行分析.可以说,这两个工具类对于我们操作集合时相当有用,下面进行分析. 二.Collections源码分析 2.1 类的 ...

  9. Java集合框架之接口Collection源码分析

    本文我们主要学习Java集合框架的根接口Collection,通过本文我们可以进一步了解Collection的属性及提供的方法.在介绍Collection接口之前我们不得不先学习一下Iterable, ...

随机推荐

  1. BUUCTF-一叶障目

    一叶障目 010editor打开没发现有什么异常,看分辨率尺寸觉得不对劲,修改了一下发现了flag 图片第二组前面四个是宽后面是高,修改第七位为05即可发现flag flag{66666}

  2. Vue回炉重造之如何使用props、emit实现自定义双向绑定

    下面我将使用Vue自带的属性实现简单的双向绑定. 下面的例子就是利用了父组件传给子组件(在子组件定义props属性,在父组件的子组件上绑定属性),子组件传给父组件(在子组件使用$emit()属性定义一 ...

  3. C#.NET笔试题-高级

    1.说说什么是架构模式. 1,分层. 2,分割. 分层是对网站进行横向的切分,那么分割就是对网站进行纵向的切分.将网站按照不同业务分割成小应用,可以有效控制网站的复杂程度. 3,分布式. 在大型网站中 ...

  4. React技巧之中断map循环

    正文从这开始~ 总览 在React中,中断map()循环: 在数组上调用slice()方法,来得到数组的一部分. 在部分数组上调用map()方法. 遍历部分数组. export default fun ...

  5. Tomcat深入浅出——Filter与Listener(五)

    一.Filter过滤器 1.1 Filter过滤器的使用 这是过滤器接口的方法 public interface Filter { default void init(FilterConfig fil ...

  6. 或许是 WebGIS 下一代的数据规范 - OGC API 系列

    目录 1. 前言 1.1. 经典的 OGC 标准回顾 1.2. 共同特点与时代变化 1.3. 免责声明 2. 什么是 OGC API 2.1. OGC API 是一个开放.动态的规范族 2.2. OG ...

  7. 【AcWing】周赛

    A.糖果 题目链接 链接 题目描述 给定三个正整数 a,b,c. 请计算 ⌊a+b+c2⌋,即 a,b,c 相加的和除以 2 再下取整的结果. 输入格式 第一行包含整数 T,表示共有 T 组测试数据. ...

  8. python 异常捕捉与异常处理

    简介 在实际开发中,为了防止异常界面直接被用户看到,往往我们会采用捕捉异常的方式来进一步处理异常. 异常捕捉 如下代码由于下标越界会导致异常 data = range(10) print(data[1 ...

  9. LGV 引理

    (其实是贺的:https://www.luogu.com.cn/paste/whl2joo4) 目录 LGV 引理 不相交路径计数 例题 Luogu6657. [模板]LGV 引理 CF348D Tu ...

  10. JS中的数据类型及转换

    js的六大类型 js中有六种数据类型,Boolean: 布尔类型 Number:数字(整数int,浮点数float ) String:字符串 Object:对象 (包含Array数组 ) 特殊数据类型 ...