Hertz Engine 的核心功能解析

server.HertzHertz 的核心类型,它由 route.Engine 以及 signalWaiter 组成,Hertz 服务器的启动、路由注册、中间件注册以及退出等重要方法均包含在 server.Hertz 中。

以下是 server.Hertz 的定义:

// Hertz is the core struct of hertz.
type Hertz struct {
    *route.Engine
    // 用于接收信号以实现优雅退出
    signalWaiter func(err chan error) error
}

route.Engineserver.Hertz 的重要组成部分,Engine 的定义位于 Engine

配置

配置项 默认值 说明
WithTransport network.NewTransporter 更换底层 transport
WithHostPorts :8888 指定监听的地址和端口
WithKeepAliveTimeout 1min tcp 长连接保活时间,一般情况下不用修改,更应该关注 idleTimeout
WithReadTimeout 3min 底层读取数据超时时间
WithIdleTimeout 3min 长连接请求链接空闲超时时间
WithMaxRequestBodySize 4 _ 1024 _ 1024 配置最大的请求体大小
WithRedirectTrailingSlash true 自动根据末尾的 / 转发,例如:如果 router 只有 /foo/,那么 /foo 会重定向到 /foo/ ;如果只有 /foo,那么 /foo/ 会重定向到 /foo
WithRemoveExtraSlash false RemoveExtraSlash 当有额外的 / 时也可以当作参数。如:user/:name,如果开启该选项 user//xiaoming 也可匹配上参数
WithUnescapePathValues true 如果开启,请求路径会被自动转义(eg. ‘%2F’ -> ‘/’)。如果 UseRawPath 为 false(默认情况),则 UnescapePathValues 实际上为 true,因为 .URI().Path() 将被使用,它已经是转义后的。设置该参数为 false,需要配合 WithUseRawPath(true)
WithUseRawPath false 如果开启,会使用原始 path 进行路由匹配
WithHandleMethodNotAllowed false 如果开启,当当前路径不能被匹配上时,server 会去检查其他方法是否注册了当前路径的路由,如果存在则会响应"Method Not Allowed",并返回状态码 405; 如果没有,则会用 NotFound 的 handler 进行处理
WithDisablePreParseMultipartForm false 如果开启,则不会预处理 multipart form。可以通过 ctx.Request.Body() 获取到 body 后由用户处理
WithStreamBody false 如果开启,则会使用流式处理 body
WithNetwork “tcp” 设置网络协议,可选:tcp,udp,unix(unix domain socket),默认为 tcp
WithExitWaitTime 5s 设置优雅退出时间。Server 会停止建立新的连接,并对关闭后的每一个请求设置 Connection: Close 的 header,当到达设定的时间关闭 Server。当所有连接已经关闭时,Server 可以提前关闭
WithTLS nil 配置 server tls 能力,详情可见 TLS
WithListenConfig nil 设置监听器配置,可用于设置是否允许 reuse port 等
WithALPN false 是否开启 ALPN
WithTracer []interface{}{} 注入 tracer 实现,如不注入 Tracer 实现,默认关闭
WithTraceLevel LevelDetailed 设置 trace level
WithWriteTimeout 无限长 写入数据超时时间
WithRedirectFixedPath false 如果开启,当当前请求路径不能匹配上时,server 会尝试修复请求路径并重新进行匹配,如果成功匹配并且为 GET 请求则会返回状态码 301 进行重定向,其他请求方式返回 308 进行重定向
WithBasePath / 设置基本路径,前缀和后缀必须为 /
WithMaxKeepBodySize 4 _ 1024 _ 1024 设置回收时保留的请求体和响应体的最大大小。单位:字节
WithGetOnly false 如果开启则只接受 GET 请求
WithKeepAlive true 如果开启则使用 HTTP 长连接
WithAltTransport network.NewTransporter 设置备用 transport
WithH2C false 设置是否开启 H2C
WithReadBufferSize 4 * 1024 设置读缓冲区大小,同时限制 HTTP header 大小
WithRegistry registry.NoopRegistry, nil 设置注册中心配置,服务注册信息
WithAutoReloadRender false, 0 设置自动重载渲染配置
WithDisablePrintRoute false 设置是否禁用 debugPrintRoute
WithOnAccept nil 设置在 netpoll 中当一个连接被接受但不能接收数据时的回调函数,在 go net 中在转换 TLS 连接之前被调用
WithOnConnect nil 设置 onConnect 函数。它可以接收来自 netpoll 连接的数据。在 go net 中,它将在转换 TLS 连接后被调用
WithDisableHeaderNamesNormalizing false 设置是否禁用 Request 和 Response Header 名字的规范化 (首字母和破折号后第一个字母大写)

Server Connection 数量限制:

  • 如果是使用标准网络库,无此限制
  • 如果是使用 netpoll,最大连接数为 10000 (这个是 netpoll 底层使用的 gopool )控制的,修改方式也很简单,调用 gopool 提供的函数即可:gopool.SetCap(xxx)(main.go 中调用一次即可)。
    package main
    
    import (
        "context"
        "github.com/bytedance/gopkg/util/gopool"
        "github.com/cloudwego/hertz/pkg/app"
        "github.com/cloudwego/hertz/pkg/app/server"
        "net/http"
    )
    
    func main() {
        // 设置新的最大连接数,例如设置为 20000
        newCap := 20000
        gopool.SetCap(int32(newCap))
    
        // 创建一个 Hertz 服务器实例
        h := server.Default()
    
        // 定义路由
        h.GET(
      	  "/", func(c context.Context, ctx *app.RequestContext) {
      		  ctx.String(http.StatusOK, "Hello, Hertz!")
      	  },
        )
    
        // 启动服务器
        h.Spin()
    }
    

Server 侧的配置项均在初始化 Server 时采用 server.WithXXX 的方式,如:

package main

import (
	"context"
	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/server"
	"net/http"
	"time"
)

func main() {
	// 创建一个 Hertz 服务器实例
	h := server.Default(
		// 设置监听端口
		server.WithHostPorts(":8080"),
		// 设置读取超时时间
		server.WithReadTimeout(time.Minute*5),
		// 设置写入超时时间
		server.WithBasePath("/api"),
	)
	// 定义路由
	h.GET(
		"/", func(c context.Context, ctx *app.RequestContext) {
			ctx.String(http.StatusOK, "Hello, Hertz!")
		},
	)

	// 启动服务器
	h.Spin()
}

初始化服务

有两种方式:

  1. func Default(opts ...config.Option) *Hertz
  2. func New(opts ...config.Option) *Hertz

Default(推荐)

Default 用于初始化服务,默认使用了 Recovery 中间件以保证服务在运行时不会因为 panic 导致服务崩溃。

函数签名:

func Default(opts ...config.Option) *Hertz

示例代码:

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

New

New 用于初始化服务,没有使用默认的 Recovery 中间件。

函数签名:

func New(opts ...config.Option) *Hertz

示例代码:

func main() {
    h := server.New()
    h.Spin()
}

测试

要测试这一点的区别,可以通过如下代码运行对比

Default

package main

import (
	"context"
	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/server"
	"net/http"
	"time"
)

func main() {
	// 创建一个 Hertz 服务器实例
	h := server.Default()
	// 定义路由
	h.GET(
		"/test_panic", func(c context.Context, ctx *app.RequestContext) {
			panic("panic")
			ctx.String(http.StatusOK, "Hello, Hertz!")
		},
	)
	h.GET(
		"/test_normal", func(c context.Context, ctx *app.RequestContext) {
			ctx.String(http.StatusOK, "Hello, Hertz!")
		},
	)

	// 启动服务器
	h.Spin()
}

New

package main

import (
	"context"
	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/server"
	"net/http"
	"time"
)

func main() {
	// 创建一个 Hertz 服务器实例
	h := server.New()
	// 定义路由
	h.GET(
		"/test_panic", func(c context.Context, ctx *app.RequestContext) {
			panic("panic")
			ctx.String(http.StatusOK, "Hello, Hertz!")
		},
	)
	h.GET(
		"/test_normal", func(c context.Context, ctx *app.RequestContext) {
			ctx.String(http.StatusOK, "Hello, Hertz!")
		},
	)

	// 启动服务器
	h.Spin()
}

分别运行代码,然后分别访问

  • 127.0.0.1:8888/test_panic
  • 127.0.0.1:8888/test_normal

可以看到

NEW Default
127.0.0.1:8888/test_panic返回 无返回 500状态码
服务端 服务报错,退出运行 服务正常,打印报错日志,不退出
再访问 127.0.0.1:8888/test_normal 无法访问 还可以正常访问

服务运行与退出

func (h *Hertz) Spin()
func (engine *Engine) Run() (err error)
func (h *Hertz) SetCustomSignalWaiter(f func(err chan error) error)

Spin(推荐)

Spin 函数用于运行 Hertz 服务器,接收到退出信号后可退出服务。

该函数支持服务的优雅退出,优雅退出的详细内容请看 优雅退出

在使用 服务注册发现 的功能时,Spin 会在服务启动时将服务注册进入注册中心,并使用 signalWaiter 监测服务异常。

函数签名:

func (h *Hertz) Spin()

示例代码:

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

Run

Run 函数用于运行 Hertz 服务器,接收到退出信号后可退出服务。

该函数不支持服务的优雅退出,除非有特殊需求,不然一般使用 Spin 函数用于运行服务。

函数签名:

func (engine *Engine) Run() (err error)

示例代码:

func main() {
    h := server.Default()
    if err := h.Run(); err != nil {
        // ...
    	panic(err)
    }
}

SetCustomSignalWaiter

SetCustomSignalWaiter 函数用于自定义服务器接收信号后的处理函数,若没有设置自定义函数,Hertz 使用 waitSignal 函数作为信号处理的默认实现方式,详细内容请看 优雅退出

函数签名:

func (h *Hertz) SetCustomSignalWaiter(f func(err chan error) error)

示例代码:

func main() {
	h := server.New()
	h.SetCustomSignalWaiter(func(err chan error) error {
		return nil
	})
	h.Spin()
}

Spin与Run的区别

Spin的退出

2025/02/23 11:50:01.612320 engine.go:397: [Info] HERTZ: Using network library=standard
2025/02/23 11:50:01.612834 transport.go:65: [Info] HERTZ: HTTP server listening on address=[::]:8080

# 下面就是优雅退出的部分
2025/02/23 11:50:12.437447 hertz.go:113: [Info] HERTZ: Received signal: interrupt
2025/02/23 11:50:12.437447 hertz.go:77: [Info] HERTZ: Begin graceful shutdown, wait at most num=5 seconds...
2025/02/23 11:50:12.438197 transport.go:71: [Error] HERTZ: Error=accept tcp [::]:8080: use of closed network connection
2025/02/23 11:50:17.439975 engine.go:311: [Info] HERTZ: Execute OnShutdownHooks timeout: error=context deadline exceeded

Run的退出

2025/02/23 11:49:41.349221 engine.go:397: [Info] HERTZ: Using network library=standard
2025/02/23 11:49:41.350245 transport.go:65: [Info] HERTZ: HTTP server listening on address=[::]:8080
exit status 0xc000013a

中间件的使用

中间件的使用

func (engine *Engine) Use(middleware ...app.HandlerFunc) IRoutes

Use 函数用于将中间件注册进入路由。

Hertz 支持用户自定义中间件,Hertz 已经实现了一些常用的中间件,详情见 hertz-contrib

Hertz 支持的中间件的使用方法包括全局注册路由组级别和单一路由级别的注册,详情见 服务端中间件

Use 函数中 middleware 的形参必须为 app.HandlerFunc 的 http 处理函数:

type HandlerFunc func (ctx context.Context, c *app.RequestContext)

只要符合这个定义的方法,都可以成为中间件

例如

func main() {
    h := server.New()
    // 将内置的 Recovery 中间件注册进入路由
    h.Use(recovery.Recovery())
    // 使用自定义的中间件
    h.Use(exampleMiddleware())
}

func exampleMiddleware() app.HandlerFunc {
    return func(ctx context.Context, c *app.RequestContext) {
        // 在 Next 中的函数执行之前打印日志
        hlog.Info("print before...")
        // 使用 Next 使得路由匹配的函数执行
        c.Next(ctx)
        // 在 Next 中的函数执行之后打印日志
        hlog.Info("print after...")
    }
}

gin中的访问日志

[GIN] 2025/02/23 - 10:58:31 | 200 |            0s |       127.0.0.1 | GET      "/api"
[GIN] 2025/02/23 - 10:58:33 | 200 |            0s |       127.0.0.1 | GET      "/api"
[GIN] 2025/02/23 - 10:58:34 | 200 |            0s |       127.0.0.1 | GET      "/api"
[GIN] 2025/02/23 - 10:58:34 | 200 |            0s |       127.0.0.1 | GET      "/api"

虽然Hertz中没有,但是我们可以通过中间件来达到目的

package main

import (
	"context"
	"fmt"
	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/server"
	"net/http"
	"time"
)

// AccessLogMiddleware 自定义访问日志中间件
func AccessLogMiddleware() app.HandlerFunc {
	return func(c context.Context, ctx *app.RequestContext) {
		// 记录请求开始时间
		start := time.Now()
		// 继续处理请求
		ctx.Next(c)
		// 计算请求处理时间
		elapsed := time.Since(start)
		// 获取客户端 IP 地址
		clientIP := ctx.ClientIP()
		// 获取请求方法
		method := string(ctx.Method())
		// 获取请求路径
		path := string(ctx.Path())
		// 获取响应状态码
		statusCode := ctx.Response.StatusCode()
		// 输出类似 Gin 风格的访问日志
		// 格式化日期
		now := time.Now().Format("2006/01/02 - 15:04:05")
		// 输出类似 Gin 风格的访问日志
		fmt.Printf(
			"[Hertz] %s | %d | %13v | %15s | %-7s %s\n",
			now, statusCode, elapsed, clientIP, method, path,
		)
	}
}
func main() {
	// 创建一个 Hertz 服务器实例
	h := server.Default(
		// 设置监听端口
		server.WithHostPorts(":8080"),
	)
	// 使用自定义的访问日志中间件
	h.Use(AccessLogMiddleware())
	// 定义路由
	h.GET(
		"/", func(c context.Context, ctx *app.RequestContext) {
			ctx.String(http.StatusOK, "Hello, Hertz!")
		},
	)

	// 启动服务器
	h.Spin()
}

产生的日志

[Hertz] 2025/02/23 - 13:10:44 | 404 |            0s |       127.0.0.1 | GET     /api/aa
[Hertz] 2025/02/23 - 13:10:45 | 404 |            0s |       127.0.0.1 | GET     /api/aa
[Hertz] 2025/02/23 - 13:10:45 | 404 |            0s |       127.0.0.1 | GET     /api/aa

流式处理

Hertz 支持 Server 的流式处理,包括流式读和流式写。

注意:由于 netpoll 和 go net 触发模式不同,netpoll 流式为“伪”流式(由于 LT 触发,会由网络库将数据读取到网络库的 buffer 中),在大包的场景下(如:上传文件等)可能会有内存问题,推荐使用 go net。

流式读

Hertz Server 支持流式读取请求内容。

示例代码:

func main() {
	h := server.Default(server.WithHostPorts("127.0.0.1:8080"), server.WithStreamBody(true), server.WithTransport(standard.NewTransporter))

	h.POST("/bodyStream", handler)

	h.Spin()
}

func handler(ctx context.Context, c *app.RequestContext) {
	// Acquire body streaming
	bodyStream := c.RequestBodyStream()
	// Read half of body bytes
	p := make([]byte, c.Request.Header.ContentLength()/2)
	r, err := bodyStream.Read(p)
	if err != nil {
		panic(err)
	}
	left, _ := ioutil.ReadAll(bodyStream)
	c.String(consts.StatusOK, "bytes streaming_read: %d\nbytes left: %d\n", r, len(left))
}

流式写

Hertz Server 支持流式写入响应。

提供了两种方式:

  1. 用户在 handler 中通过 ctx.SetBodyStream 函数传入一个 io.Reader,然后按与示例代码(利用 channel 控制数据分块及读写顺序)类似的方式分块读写数据。注意,数据需异步写入。

    若用户事先知道传输数据的总长度,可以在 ctx.SetBodyStream 函数中传入该长度进行流式写,示例代码如 /streamWrite1

    若用户事先不知道传输数据的总长度,可以在 ctx.SetBodyStream 函数中传入 -1 以 Transfer-Encoding: chunked 的方式进行流式写,示例代码如 /streamWrite2

    示例代码:

    func main() {
        h := server.Default(server.WithHostPorts("127.0.0.1:8080"), server.WithStreamBody(true), server.WithTransport(standard.NewTransporter))
    
        h.GET("/streamWrite1", func(ctx context.Context, c *app.RequestContext) {
            rw := newChunkReader()
            line := []byte("line\r\n")
            c.SetBodyStream(rw, 500*len(line))
    
            go func() {
                for i := 1; i <= 500; i++ {
                    // For each streaming_write, the upload_file prints
                    rw.Write(line)
                    fmt.Println(i)
                    time.Sleep(10 * time.Millisecond)
                }
                rw.Close()
            }()
    
            go func() {
                <-c..Finished()
                fmt.Println("request process end")
            }()
        })
    
        h.GET("/streamWrite2", func(ctx context.Context, c *app.RequestContext) {
            rw := newChunkReader()
            // Content-Length may be negative:
            // -1 means Transfer-Encoding: chunked.
            c.SetBodyStream(rw, -1)
    
            go func() {
                for i := 1; i < 1000; i++ {
                    // For each streaming_write, the upload_file prints
                    rw.Write([]byte(fmt.Sprintf("===%d===\n", i)))
                    fmt.Println(i)
                    time.Sleep(100 * time.Millisecond)
                }
                rw.Close()
            }()
    
            go func() {
                <-c..Finished()
                fmt.Println("request process end")
            }()
        })
    
        h.Spin()
    }
    
    type ChunkReader struct {
        rw  bytes.Buffer
        w2r chan struct{}
        r2w chan struct{}
    }
    
    func newChunkReader() *ChunkReader {
        var rw bytes.Buffer
        w2r := make(chan struct{})
        r2w := make(chan struct{})
        cr := &ChunkReader{rw, w2r, r2w}
        return cr
    }
    
    var closeOnce = new(sync.Once)
    
    func (cr *ChunkReader) Read(p []byte) (n int, err error) {
        for {
            _, ok := <-cr.w2r
            if !ok {
                closeOnce.Do(func() {
                    close(cr.r2w)
                })
                n, err = cr.rw.Read(p)
                return
            }
    
            n, err = cr.rw.Read(p)
    
            cr.r2w <- struct{}{}
    
            if n == 0 {
                continue
            }
            return
        }
    }
    
    func (cr *ChunkReader) Write(p []byte) (n int, err error) {
        n, err = cr.rw.Write(p)
        cr.w2r <- struct{}{}
        <-cr.r2w
        return
    }
    
    func (cr *ChunkReader) Close() {
        close(cr.w2r)
    }
    
  2. 用户可以在 handler 中使用 pkg/protocol/http1/resp/writer 下提供的 NewChunkedBodyWriter 方法劫持 response 的 writer,然后使用 ctx.Write 函数将分块数据写入 Body 并将分块数据使用 ctx.Flush 函数立即发送给客户端。

    示例代码:

    h.GET("/flush/chunk", func(ctx context.Context, c *app.RequestContext) {
    	// Hijack the writer of response
    	c.Response.HijackWriter(resp.NewChunkedBodyWriter(&c.Response, c.GetWriter()))
    
    	for i := 0; i < 10; i++ {
    		c.Write([]byte(fmt.Sprintf("chunk %d: %s", i, strings.Repeat("hi~", i)))) // nolint: errcheck
    		c.Flush()                                                                 // nolint: errcheck
    		time.Sleep(200 * time.Millisecond)
    	}
    })
    

这两种方式的区别:第一种在执行完 handler 逻辑后再将数据按分块发送给客户端,第二种在 handler 逻辑中就可以将分块数据发送出去。

更多示例代码可参考 example

Panic 处理函数

用于设置当程序发生 panic 时的处理函数,默认为 nil

注意:如果同时设置了 PanicHandlerRecovery 中间件,则 Recovery 中间件会覆盖 PanicHandler 的处理逻辑。

示例代码:

package main

import (
	"context"
	"github.com/cloudwego/hertz/pkg/app"
	"github.com/cloudwego/hertz/pkg/app/server"
	"github.com/cloudwego/hertz/pkg/common/utils"
	"github.com/cloudwego/hertz/pkg/protocol/consts"
)

func main() {
	// 创建一个 Hertz 服务器实例
	h := server.New(
		// 设置监听端口
		server.WithHostPorts(":8080"),
	)
    
    
	// 在 panic 时,会触发 PanicHandler 中的函数,返回 500 状态码并携带错误信息
	h.PanicHandler = func(ctx context.Context, c *app.RequestContext) {
		c.JSON(
			consts.StatusInternalServerError, utils.H{
				"message": "panic",
			},
		)
	}
	// 定义路由
	h.GET(
		"/", func(c context.Context, ctx *app.RequestContext) {
			panic("panic")
		},
	)
	// 启动服务器
	h.Spin()
}

posted @ 2025-02-24 16:46  厚礼蝎  阅读(111)  评论(0)    收藏  举报