Hertz Engine 的核心功能解析
server.Hertz 是 Hertz 的核心类型,它由 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.Engine 为 server.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()
}
初始化服务
有两种方式:
func Default(opts ...config.Option) *Hertzfunc 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 支持流式写入响应。
提供了两种方式:
-
用户在 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) } -
用户可以在 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。
注意:如果同时设置了
PanicHandler和Recovery中间件,则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()
}
本文来自博客园,作者:厚礼蝎,转载请注明原文链接:https://www.cnblogs.com/guangdelw/p/18734434

浙公网安备 33010602011771号