目录
1、Engine
server.Hertz
是 Hertz
的核心类型,它由 route.Engine
以及 signalWaiter
组成,Hertz
服务器的启动、路由注册、中间件注册以及退出等重要方法均包含在 server.Hertz
中。以下是 server.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 连接后被调用
这个是Hertz框架的“引擎”,是Hertz框架的入口。引擎中则包含有所有的方法,如果想要使用这个框架就必须依靠这个引擎。
简单来说,就是我们通过Engine来开启和关闭网络服务。
那我们来看看怎么初始化吧!
初始化服务
func Default(opts ...config.Option) *Hertz
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()
}
平常我们的开发中一般会使用
Defalt
而不会直接使用New
,因为Defalt
会使用默认中间件Recovery。
服务运行与退出
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()
}
Use
Use
函数用于将中间件注册进入路由。
Hertz 支持用户自定义中间件,Hertz 已经实现了一些常用的中间件,详情见 hertz-contrib。
Hertz 支持的中间件的使用方法包括全局注册、路由组级别和单一路由级别的注册,详情见 服务端中间件。
Use
函数中 middleware
的形参必须为 app.HandlerFunc
的 http 处理函数:
type HandlerFunc func (ctx context.Context, c *app.RequestContext)
函数签名:
func (engine *Engine) Use(middleware ...app.HandlerFunc) IRoutes
示例代码:
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.Ingo("print after...")
}
}
补充:中间件
Hertz中间件的种类是多种多样的,简单分为两大类:
- 服务端中间件
- 客户端中间件
服务器中间件:
![]()
中间件可以在请求更深入地传递到业务逻辑之前或之后执行:
- 中间件可以在请求到达业务逻辑之前执行,比如执行身份认证和权限认证,当中间件只有初始化(pre-handle)相关逻辑,且没有和 real handler 在一个函数调用栈中的需求时,中间件中可以省略掉最后的
.Next
,如图1的中间件 B。- 中间件也可以在执行过业务逻辑之后执行,比如记录响应时间和从异常中恢复。如果在业务 handler 处理之后有其它处理逻辑( post-handle ),或对函数调用链(栈)有强需求,则必须显式调用
.Next
,如图1的中间件 C。
使用默认中间件
Hertz 框架已经预置了常用的 recover 中间件,使用
server.Default()
默认可以注册该中间件。
客户端中间件:
补充:Handler
Handler 相关操作
一个服务端中间件即为一个 Handler,Handler 相关操作见 Handler。
小demo:
package main
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/protocol/consts"
)
func main() {
h := server.Default()
h.GET("/hello", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "Hello hertz!")
})
h.Spin()
}
流式处理
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(c context.Context, ctx *app.RequestContext) {
rw := newChunkReader()
line := []byte("line\r\n")
ctx.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() {
<-ctx.Finished()
fmt.Println("request process end")
}()
})
h.GET("/streamWrite2", func(c context.Context, ctx *app.RequestContext) {
rw := newChunkReader()
// Content-Length may be negative:
// -1 means Transfer-Encoding: chunked.
ctx.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() {
<-ctx.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(c context.Context, ctx *app.RequestContext) {
// Hijack the writer of response
ctx.Response.HijackWriter(resp.NewChunkedBodyWriter(&ctx.Response, ctx.GetWriter()))
for i := 0; i < 10; i++ {
ctx.Write([]byte(fmt.Sprintf("chunk %d: %s", i, strings.Repeat("hi~", i)))) // nolint: errcheck
ctx.Flush() // nolint: errcheck
time.Sleep(200 * time.Millisecond)
}
})
这两种方式的区别:第一种在执行完 handler 逻辑后再将数据按分块发送给客户端,第二种在 handler 逻辑中就可以将分块数据发送出去。
钩子函数
钩子函数(Hooks)是一个通用的概念,表示某事件触发时所伴随的操作。
Hertz 提供了全局的 Hook 注入能力,用于在服务触发启动后和退出前注入自己的处理逻辑,详细信息可见 Hooks。
Panic 处理函数
用于设置当程序发生 panic 时的处理函数,默认为 nil
。
注意: 如果同时设置了
PanicHandler
和Recovery
中间件,则Recovery
中间件会覆盖PanicHandler
的处理逻辑。
示例代码:
func main() {
h := server.New()
// 在 panic 时,会触发 PanicHandler 中的函数,返回 500 状态码并携带错误信息
h.PanicHandler = func(c context.Context, ctx *app.RequestContext) {
ctx.JSON(500, utils.H{
"message": "panic",
})
}
h.GET("/hello", func(c context.Context, ctx *app.RequestContext) {
panic("panic")
})
h.Spin()
}
获取路由信息
func (engine *Engine) Routes() (routes RoutesInfo)
Routes
Routes
函数返回一个按 HTTP 方法划分的包含路由信息(HTTP 方法名,路由路径,请求处理函数名)的切片。
函数签名:
func (engine *Engine) Routes() (routes RoutesInfo)
示例代码:
func getHandler() app.HandlerFunc {
return func(c context.Context, ctx *app.RequestContext) {
ctx.String(consts.StatusOK, "get handler")
}
}
func postHandler() app.HandlerFunc {
return func(c context.Context, ctx *app.RequestContext) {
ctx.String(consts.StatusOK, "post handler")
}
}
func main() {
h := server.Default()
h.GET("/get", getHandler())
h.POST("/post", postHandler())
routesInfo := h.Routes()
fmt.Printf("%v\n", routesInfo)
// [{GET /get main.getHandler.func1 0xb2afa0} {POST /post main.postHandler.func1 0xb2b060}]
}
2、路由
Hertz 提供的路由功能。
Hertz 提供了
GET
、POST
、PUT
、DELETE
、ANY
等方法用于注册路由。
方法 介绍 Hertz.GET
用于注册 HTTP Method 为 GET 的方法 Hertz.POST
用于注册 HTTP Method 为 POST 的方法 Hertz.DELETE
用于注册 HTTP Method 为 DELETE 的方法 Hertz.PUT
用于注册 HTTP Method 为 PUT 的方法 Hertz.PATCH
用于注册 HTTP Method 为 PATCH 的方法 Hertz.HEAD
用于注册 HTTP Method 为 HEAD 的方法 Hertz.OPTIONS
用于注册 HTTP Method 为 OPTIONS 的方法 Hertz.Handle
这个方法支持用户手动传入 HTTP Method 用来注册方法,当用于注册普通的 HTTP Method 方法时和上述的方法作用是一致的,并且这个方法同时也支持用于注册自定义 HTTP Method 方法 Hertz.Any
用于注册所有 HTTP Method 方法
package main
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/protocol/consts"
)
func main(){
h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
h.StaticFS("/", &app.FS{Root: "./", GenerateIndexPages: true})
h.GET("/get", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "get")
})
h.POST("/post", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "post")
})
h.PUT("/put", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "put")
})
h.DELETE("/delete", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "delete")
})
h.PATCH("/patch", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "patch")
})
h.HEAD("/head", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "head")
})
h.OPTIONS("/options", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "options")
})
h.Any("/ping_any", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "any")
})
h.Handle("LOAD","/load", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "load")
})
h.Spin()
}
以上大概就是路由的各种用法。
我们还可以给路由分组。
路由组
Hertz 提供了路由组 ( Group
) 的能力,用于支持路由分组的功能,同时中间件也可以注册到路由组上。
示例代码:
package main
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/protocol/consts"
)
func main(){
h := server.Default(server.WithHostPorts("127.0.0.1:1111"))
v1 := h.Group("/v1")
v1.GET("/get", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "get")
})
v1.POST("/post", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "post")
})
v2 := h.Group("/v2")
v2.PUT("/put", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "put")
})
v2.DELETE("/delete", func(ctx context.Context, c *app.RequestContext) {
c.String(consts.StatusOK, "delete")
})
h.Spin()
}
在路由组中使用中间件
如下示例在路由组中使用 BasicAuth
中间件。
示例代码 1:
package main
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/middlewares/server/basic_auth"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/protocol/consts"
)
func main() {
h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
// use middleware
v1 := h.Group("/v1", basic_auth.BasicAuth(map[string]string{"test": "test"}))
v1.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
ctx.String(consts.StatusOK,"ping")
})
h.Spin()
}
示例代码 2:
package main
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/middlewares/server/basic_auth"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/protocol/consts"
)
func main() {
h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
v1 := h.Group("/v1")
// use `Use` method
v1.Use(basic_auth.BasicAuth(map[string]string{"test": "test"}))
v1.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
ctx.String(consts.StatusOK,"ping")
})
h.Spin()
}
路由类型
Hertz 支持丰富的路由类型用于实现复杂的功能,包括静态路由、参数路由 (命名参数、通配参数)。
路由的优先级:静态路由
> 命名参数路由
> 通配参数路由
刚刚所演示的都是静态路由,我们接下来看点别的。
命名参数路由
Hertz 支持使用 :name
这样的命名参数设置路由,并且命名参数只匹配单个路径段。
如果我们设置/user/:name
路由,匹配情况如下
路径 | 是否匹配 |
---|---|
/user/gordon | 匹配 |
/user/you | 匹配 |
/user/gordon/profile | 不匹配 |
/user/ | 不匹配 |
通过使用 RequestContext.Param
方法,我们可以获取路由中携带的参数。
示例代码:
package main
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/protocol/consts"
)
func main(){
h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
// This handler will match: "/hertz/version", but will not match : "/hertz/" or "/hertz"
h.GET("/hertz/:version", func(ctx context.Context, c *app.RequestContext) {
version := c.Param("version")
c.String(consts.StatusOK, "Hello %s", version)
})
h.Spin()
}
通配参数路由
Hertz 支持使用 *path
这样的通配参数设置路由,并且通配参数会匹配所有内容。
如果我们设置/src/*path
路由,匹配情况如下
路径 | 是否匹配 |
---|---|
/src/ | 匹配 |
/src/somefile.go | 匹配 |
/src/subdir/somefile.go | 匹配 |
通过使用 RequestContext.Param
方法,我们可以获取路由中携带的参数。
示例代码:
package main
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/protocol/consts"
)
func main(){
h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
// However, this one will match "/hertz/v1/" and "/hertz/v2/send"
h.GET("/hertz/:version/*action", func(ctx context.Context, c *app.RequestContext) {
version := c.Param("version")
action := c.Param("action")
message := version + " is " + action
c.String(consts.StatusOK, message)
})
h.Spin()
}
获取路由注册信息
Hertz 提供了 Routes
获取注册的路由信息供用户使用。
路由信息结构:
// RouteInfo represents a request route's specification which contains method and path and its handler.
type RouteInfo struct {
Method string // http method
Path string // url path
Handler string // handler name
HandlerFunc app.HandlerFunc
}
// RoutesInfo defines a RouteInfo array.
type RoutesInfo []RouteInfo
示例代码:
package main
import (
"context"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/common/hlog"
"github.com/cloudwego/hertz/pkg/common/utils"
"github.com/cloudwego/hertz/pkg/protocol/consts"
)
func main() {
h := server.Default()
h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
})
routeInfo := h.Routes()
hlog.Info(routeInfo)
h.Spin()
}
NoRoute 与 NoMethod 使用
Hertz 提供了 NoRoute
与 NoMethod
方法用于全局处理 HTTP 404 与 405 请求。 当使用 NoMethod
时需要与 WithHandleMethodNotAllowed
配合使用。
示例代码:
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() {
h := server.Default(server.WithHandleMethodNotAllowed(true))
h.POST("/ping", func(c context.Context, ctx *app.RequestContext) {
ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
})
// set NoRoute handler
h.NoRoute(func(c context.Context, ctx *app.RequestContext) {
ctx.String(consts.StatusOK, "no route")
})
// set NoMethod handler
h.NoMethod(func(c context.Context, ctx *app.RequestContext) {
ctx.String(consts.StatusOK, "no method")
})
h.Spin()
}
重定向尾斜杠
Hertz 在默认情况下会根据请求 path 末尾的 /
自动进行转发。如果 router 中只有 /foo/,那么请求 /foo 会被自动重定向到 /foo/;如果 router 中只有 /foo,那么 /foo/ 会被重定向到 /foo。
这样的请求除 GET
以外的请求方法都会触发 307 Temporary Redirect
状态码,而 GET
请求会触发 301 Moved Permanently
状态码。
可以在配置中取消,如下:
package main
import "github.com/cloudwego/hertz/pkg/app/server"
func main() {
h := server.New(server.WithRedirectTrailingSlash(false))
...
}
3、客户端
package main
import (
"context"
"fmt"
"github.com/cloudwego/hertz/pkg/app"
"github.com/cloudwego/hertz/pkg/app/client"
"github.com/cloudwego/hertz/pkg/app/server"
"github.com/cloudwego/hertz/pkg/protocol"
"github.com/cloudwego/hertz/pkg/protocol/consts"
)
func performRequest() {
c, _ := client.NewClient()
req, resp := protocol.AcquireRequest(), protocol.AcquireResponse()
req.SetRequestURI("http://localhost:8080/hello")
req.SetMethod("GET")
_ = c.Do(context.Background(), req, resp)
fmt.Printf("get response: %s\n", resp.Body()) // status == 200 resp.Body() == []byte("hello hertz")
}
func main() {
h := server.New(server.WithHostPorts(":8080"))
h.GET("/hello", func(c context.Context, ctx *app.RequestContext) {
ctx.JSON(consts.StatusOK, "hello hertz")
})
go performRequest()
h.Spin()
}
Client 配置
配置项 | 默认值 | 描述 |
---|---|---|
WithDialTimeout | 1s | 拨号超时时间 |
WithMaxConnsPerHost | 512 | 每个主机可能建立的最大连接数 |
WithMaxIdleConnDuration | 10s | 最大的空闲连接持续时间,空闲的连接在此持续时间后被关闭 |
WithMaxConnDuration | 0s | 最大的连接持续时间,keep-alive 连接在此持续时间后被关闭 |
WithMaxConnWaitTimeout | 0s | 等待空闲连接的最大时间 |
WithKeepAlive | true | 是否使用 keep-alive 连接,默认使用 |
WithClientReadTimeout | 0s | 完整读取响应(包括 body)的最大持续时间 |
WithTLSConfig | nil | 设置用于创建 tls 连接的 tlsConfig,具体配置信息请看 tls |
WithDialer | network.Dialer | 设置指定的拨号器 |
WithResponseBodyStream | false | 是否在流中读取 body,默认不在流中读取 |
WithDisableHeaderNamesNormalizing | false | 是否禁用头名称规范化,默认不禁用,如 cONTENT-lenGTH -> Content-Length |
WithName | "" | 用户代理头中使用的客户端名称 |
WithNoDefaultUserAgentHeader | false | 是否没有默认的 User-Agent 头,默认有 User-Agent 头 |
WithDisablePathNormalizing | false | 是否禁用路径规范化,默认规范路径,如 http://localhost:8080/hello/../ hello -> http://localhost:8080/hello |
WithRetryConfig | nil | HTTP 客户端的重试配置,重试配置详细说明请看 重试 |
WithWriteTimeout | 0s | HTTP 客户端的写入超时时间 |
WithConnStateObserve | nil, 5s | 设置观察和记录 HTTP 客户端的连接状态的函数以及观察执行间隔 |
WithDialFunc | network.Dialer | 设置 HTTP 客户端拨号器函数,会覆盖自定义拨号器 |
示例代码:
func main() {
observeInterval := 10 * time.Second
stateFunc := func(state config.HostClientState) {
fmt.Printf("state=%v\n", state.ConnPoolState().Addr)
}
var customDialFunc network.DialFunc = func(addr string) (network.Conn, error) {
return nil, nil
}
c, err := client.NewClient(
client.WithDialTimeout(1*time.Second),
client.WithMaxConnsPerHost(1024),
client.WithMaxIdleConnDuration(10*time.Second),
client.WithMaxConnDuration(10*time.Second),
client.WithMaxConnWaitTimeout(10*time.Second),
client.WithKeepAlive(true),
client.WithClientReadTimeout(10*time.Second),
client.WithDialer(standard.NewDialer()),
client.WithResponseBodyStream(true),
client.WithDisableHeaderNamesNormalizing(true),
client.WithName("my-client"),
client.WithNoDefaultUserAgentHeader(true),
client.WithDisablePathNormalizing(true),
client.WithRetryConfig(
retry.WithMaxAttemptTimes(3),
retry.WithInitDelay(1000),
retry.WithMaxDelay(10000),
retry.WithDelayPolicy(retry.DefaultDelayPolicy),
retry.WithMaxJitter(1000),
),
client.WithWriteTimeout(10*time.Second),
client.WithConnStateObserve(stateFunc, observeInterval),
client.WithDialFunc(customDialFunc, netpoll.NewDialer()),
)
if err != nil {
return
}
status, body, _ := c.Get(context.Background(), nil, "http://www.example.com")
fmt.Printf("status=%v body=%v\n", status, string(body))
}
Client Request 配置
配置项 | 默认值 | 描述 |
---|---|---|
WithDialTimeout | 0s | 拨号超时时间,该配置项的优先级高于 Client 配置,即会覆盖相应的 Client 配置项 |
WithReadTimeout | 0s | 完整读取响应(包括 body)的最大持续时间,该配置项的优先级高于 Client 配置,即会覆盖相应的 Client 配置项 |
WithWriteTimeout | 0s | HTTP 客户端的写入超时时间,该配置项的优先级高于 Client 配置,即会覆盖相应的 Client 配置项 |
WithRequestTimeout | 0s | 完整的 HTTP 请求的超时时间 |
WithTag | make(map[string]string) | 以 key-value 形式设置 tags 字段,配合服务发现使用,详情见 WithTag |
WithSD | false | 配合服务发现使用,传递 true 时,本次请求使用服务发现,详情见 WithSD |
示例代码:
func main() {
cli, err := client.NewClient()
if err != nil {
return
}
req, res := &protocol.Request{}, &protocol.Response{}
req.SetOptions(config.WithDialTimeout(1*time.Second),
config.WithReadTimeout(3*time.Second),
config.WithWriteTimeout(3*time.Second),
config.WithReadTimeout(5*time.Second),
config.WithSD(true),
config.WithTag("tag", "tag"))
req.SetMethod(consts.MethodGet)
req.SetRequestURI("http://www.example.com")
err = cli.Do(context.Background(), req, res)
fmt.Printf("resp = %v,err = %+v", string(res.Body()), err)
}
发送请求
func (c *Client) Do(ctx context.Context, req *protocol.Request, resp *protocol.Response) error
func (c *Client) DoRedirects(ctx context.Context, req *protocol.Request, resp *protocol.Response, maxRedirectsCount int) error
func (c *Client) Get(ctx context.Context, dst []byte, url string, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error)
func (c *Client) Post(ctx context.Context, dst []byte, url string, postArgs *protocol.Args, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error)
Do
Do 函数执行给定的 http 请求并填充给定的 http 响应。请求必须包含至少一个非零的 RequestURI,其中包含完整的 URL 或非零的 Host header + RequestURI。
该函数不会跟随重定向,请使用 Get 函数或 DoRedirects 函数或 Post 函数来跟随重定向。
如果 resp 为 nil,则会忽略响应。如果所有针对请求主机的 DefaultMaxConnsPerHost 连接都已忙,则会返回 ErrNoFreeConns
错误。在性能关键的代码中,建议通过 AcquireRequest 和 AcquireResponse 获取 req 和 resp。
函数签名:
func (c *Client) Do(ctx context.Context, req *protocol.Request, resp *protocol.Response) error
示例代码:
func main() {
// hertz server:http://localhost:8080/ping ctx.String(consts.StatusOK, "pong")
c, err := client.NewClient()
if err != nil {
return
}
req, res := &protocol.Request{}, &protocol.Response{}
req.SetMethod(consts.MethodGet)
req.SetRequestURI("http://localhost:8080/ping")
err = c.Do(context.Background(), req, res)
fmt.Printf("resp = %v,err = %+v", string(res.Body()), err)
// resp.Body() == []byte("pong") err == <nil>
}
DoRedirects
DoRedirects 函数执行给定的 http 请求并填充给定的 http 响应,遵循最多 maxRedirectsCount 次重定向。当重定向次数超过 maxRedirectsCount 时,将返回 ErrTooManyRedirects
错误。
函数签名:
func (c *Client) DoRedirects(ctx context.Context, req *protocol.Request, resp *protocol.Response, maxRedirectsCount int) error
示例代码:
func main() {
// hertz server
// http://localhost:8080/redirect ctx.Redirect(consts.StatusMovedPermanently, []byte("/redirect2"))
// http://localhost:8080/redirect2 ctx.Redirect(consts.StatusMovedPermanently, []byte("/redirect3"))
// http://localhost:8080/redirect3 ctx.String(consts.StatusOK, "pong")
c, err := client.NewClient()
if err != nil {
return
}
req, res := &protocol.Request{}, &protocol.Response{}
req.SetMethod(consts.MethodGet)
req.SetRequestURI("http://localhost:8080/redirect")
err = c.DoRedirects(context.Background(), req, res, 1)
fmt.Printf("resp = %v,err = %+v\n", string(res.Body()), err)
// res.Body() == []byte("") err.Error() == "too many redirects detected when doing the request"
err = c.DoRedirects(context.Background(), req, res, 2)
fmt.Printf("resp = %v,err = %+v\n", string(res.Body()), err)
// res.Body() == []byte("pong") err == <nil>
}
Get
Get 函数返回 URL 的状态码和响应体。如果 dst 太小,则将被响应体替换并返回,否则将分配一个新的切片。
该函数会自动跟随重定向。
函数签名:
func (c *Client) Get(ctx context.Context, dst []byte, url string, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error)
示例代码:
func main() {
// hertz server:http://localhost:8080/ping ctx.String(consts.StatusOK, "pong")
c, err := client.NewClient()
if err != nil {
return
}
status, body, err := c.Get(context.Background(), nil, "http://localhost:8080/ping")
fmt.Printf("status=%v body=%v err=%v\n", status, string(body), err)
// status == 200 res.Body() == []byte("pong") err == <nil>
}
Post
Post 函数使用给定的 POST 参数向指定的 URL 发送 POST 请求。如果 dst 太小,则将被响应体替换并返回,否则将分配一个新的切片。
该函数会自动跟随重定向。
如果 postArgs 为 nil,则发送空的 POST 请求体。
函数签名:
func (c *Client) Post(ctx context.Context, dst []byte, url string, postArgs *protocol.Args, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error)
示例代码:
func main() {
// hertz server:http://localhost:8080/hello ctx.String(consts.StatusOK, "hello %s", ctx.PostForm("name"))
c, err := client.NewClient()
if err != nil {
return
}
var postArgs protocol.Args
postArgs.Set("name", "cloudwego") // Set post args
status, body, err := c.Post(context.Background(), nil, "http://localhost:8080/hello", &postArgs)
fmt.Printf("status=%v body=%v err=%v\n", status, string(body), err)
// status == 200 res.Body() == []byte("hello cloudwego") err == <nil>
}
请求超时
注意:Do、DoRedirects、Get、Post 等请求函数可以通过 WithRequestTimeout 设置请求超时时间,DoTimeout 和 DoDeadline 函数通过传参的形式设置请求超时时间,两者都是修改
RequestOptions.requestTimeout
字段,所以在使用 DoTimeout 和 DoDeadline 函数时无需使用 WithRequestTimeout 函数,若同时使用了,请求超时时间以最后一次设置的为准。
func WithRequestTimeout(t time.Duration) RequestOption
func (c *Client) DoTimeout(ctx context.Context, req *protocol.Request, resp *protocol.Response, timeout time.Duration) error
func (c *Client) DoDeadline(ctx context.Context, req *protocol.Request, resp *protocol.Response, deadline time.Time) error
WithRequestTimeout
Do、DoRedirects、Get、Post 等请求函数虽然不能以传参的方式设置请求超时返回,但可以通过 Client Request 配置 中的 WithRequestTimeout
配置项来设置请求超时返回。
示例代码:
func main() {
c, err := client.NewClient()
if err != nil {
return
}
// Do
req, res := &protocol.Request{}, &protocol.Response{}
req.SetOptions(config.WithRequestTimeout(5 * time.Second))
req.SetMethod(consts.MethodGet)
req.SetRequestURI("http://localhost:8888/get")
err = c.Do(context.Background(), req, res)
// DoRedirects
err = c.DoRedirects(context.Background(), req, res, 5)
// Get
_, _, err = c.Get(context.Background(), nil, "http://localhost:8888/get", config.WithRequestTimeout(5*time.Second))
// Post
postArgs := &protocol.Args{}
_, _, err = c.Post(context.Background(), nil, "http://localhost:8888/post", postArgs, config.WithRequestTimeout(5*time.Second))
}
DoTimeout
DoTimeout 函数执行给定的请求并在给定的超时时间内等待响应。
该函数不会跟随重定向,请使用 Get 函数或 DoRedirects 函数或 Post 函数来跟随重定向。
如果 resp 为 nil,则会忽略响应。如果在给定的超时时间内未能收到响应,则会返回 errTimeout
错误。
函数签名:
func (c *Client) DoTimeout(ctx context.Context, req *protocol.Request, resp *protocol.Response, timeout time.Duration) error
示例代码:
func main() {
// hertz server:http://localhost:8080/ping ctx.String(consts.StatusOK, "pong") biz handler time: 1.5s
c, err := client.NewClient()
if err != nil {
return
}
req, res := &protocol.Request{}, &protocol.Response{}
req.SetMethod(consts.MethodGet)
req.SetRequestURI("http://localhost:8080/ping")
err = c.DoTimeout(context.Background(), req, res, time.Second*3)
fmt.Printf("resp = %v,err = %+v\n", string(res.Body()), err)
// res.Body() == []byte("pong") err == <nil>
err = c.DoTimeout(context.Background(), req, res, time.Second)
fmt.Printf("resp = %v,err = %+v\n", string(res.Body()), err)
// res.Body() == []byte("") err.Error() == "timeout"
}
DoDeadline
DoDeadline 执行给定的请求并等待响应,直至给定的最后期限。
该函数不会跟随重定向,请使用 Get 函数或 DoRedirects 函数或 Post 函数来跟随重定向。
如果 resp 为 nil,则会忽略响应。如果在给定的截止日期之前未能收到响应,则会返回 errTimeout
错误。
函数签名:
func (c *Client) DoDeadline(ctx context.Context, req *protocol.Request, resp *protocol.Response, deadline time.Time) error
示例代码:
func main() {
// hertz server:http://localhost:8080/ping ctx.String(consts.StatusOK, "pong") biz handler time: 1.5s
c, err := client.NewClient()
if err != nil {
return
}
req, res := &protocol.Request{}, &protocol.Response{}
req.SetMethod(consts.MethodGet)
req.SetRequestURI("http://localhost:8080/ping")
err = c.DoDeadline(context.Background(), req, res, time.Now().Add(3*time.Second))
fmt.Printf("resp = %v,err = %+v\n", string(res.Body()), err)
// res.Body() == []byte("pong") err == <nil>
err = c.DoDeadline(context.Background(), req, res, time.Now().Add(1*time.Second))
fmt.Printf("resp = %v,err = %+v\n", string(res.Body()), err)
// res.Body() == []byte("") err.Error() == "timeout"
}
请求重试
func (c *Client) SetRetryIfFunc(retryIf client.RetryIfFunc)
SetRetryIfFunc
SetRetryIfFunc
方法用于自定义配置重试发生的条件。(更多内容请参考 retry-条件配置)
函数签名:
func (c *Client) SetRetryIfFunc(retryIf client.RetryIfFunc)
示例代码:
func main() {
c, err := client.NewClient()
if err != nil {
return
}
var customRetryIfFunc = func(req *protocol.Request, resp *protocol.Response, err error) bool {
return true
}
c.SetRetryIfFunc(customRetryIfFunc)
status2, body2, _ := c.Get(context.Background(), nil, "http://www.example.com")
fmt.Printf("status=%v body=%v\n", status2, string(body2))
}
添加请求内容
Hertz 客户端可以在 HTTP 请求中添加 query
参数、www-url-encoded
、multipart/form-data
、json
等多种形式的请求内容。
示例代码:
func main() {
client, err := client.NewClient()
if err != nil {
return
}
req := &protocol.Request{}
res := &protocol.Response{}
// Use SetQueryString to set query parameters
req.Reset()
req.Header.SetMethod(consts.MethodPost)
req.SetRequestURI("http://127.0.0.1:8080/v1/bind")
req.SetQueryString("query=query&q=q1&q=q2&vd=1")
err = client.Do(context.Background(), req, res)
if err != nil {
return
}
// Send "www-url-encoded" request
req.Reset()
req.Header.SetMethod(consts.MethodPost)
req.SetRequestURI("http://127.0.0.1:8080/v1/bind?query=query&q=q1&q=q2&vd=1")
req.SetFormData(map[string]string{
"form": "test form",
})
err = client.Do(context.Background(), req, res)
if err != nil {
return
}
// Send "multipart/form-data" request
req.Reset()
req.Header.SetMethod(consts.MethodPost)
req.SetRequestURI("http://127.0.0.1:8080/v1/bind?query=query&q=q1&q=q2&vd=1")
req.SetMultipartFormData(map[string]string{
"form": "test form",
})
err = client.Do(context.Background(), req, res)
if err != nil {
return
}
// Send "Json" request
req.Reset()
req.Header.SetMethod(consts.MethodPost)
req.Header.SetContentTypeBytes([]byte("application/json"))
req.SetRequestURI("http://127.0.0.1:8080/v1/bind?query=query&q=q1&q=q2&vd=1")
data := struct {
Json string `json:"json"`
}{
"test json",
}
jsonByte, _ := json.Marshal(data)
req.SetBody(jsonByte)
err = client.Do(context.Background(), req, res)
if err != nil {
return
}
}
上传文件
Hertz 客户端支持向服务器上传文件。
示例代码:
func main() {
client, err := client.NewClient()
if err != nil {
return
}
req := &protocol.Request{}
res := &protocol.Response{}
req.SetMethod(consts.MethodPost)
req.SetRequestURI("http://127.0.0.1:8080/singleFile")
req.SetFile("file", "your file path")
err = client.Do(context.Background(), req, res)
if err != nil {
return
}
fmt.Println(err, string(res.Body()))
}
流式读响应内容
服务发现
Hertz 客户端支持通过服务发现寻找目标服务器。
Hertz 支持自定义服务发现模块,更多内容可参考 服务发现拓展。
Hertz 目前已接入的服务发现中心相关内容可参考 服务注册与发现。
TLS
Hertz 客户端默认使用的网络库 netpoll 不支持 TLS,如果要配置 TLS 访问 https 地址,应该使用标准库。
TLS 相关的配置信息可参考 tls。
正向代理
func (c *Client) SetProxy(p protocol.Proxy)
SetProxy
SetProxy 用来设置客户端代理。(更多内容请参考 正向代理)
注意:同一个客户端不能设置多个代理,如果需要使用另一个代理,请创建另一个客户端并为其设置代理。
示例代码:
func (c *Client) SetProxy(p protocol.Proxy)
函数签名:
func main() {
// Proxy address
proxyURL := "http://<__user_name__>:<__password__>@<__proxy_addr__>:<__proxy_port__>"
parsedProxyURL := protocol.ParseURI(proxyURL)
client, err := client.NewClient(client.WithDialer(standard.NewDialer()))
if err != nil {
return
}
client.SetProxy(protocol.ProxyURI(parsedProxyURL))
upstreamURL := "http://google.com"
_, body, _ := client.Get(context.Background(), nil, upstreamURL)
fmt.Println(string(body))
}
关闭空闲连接
func (c *Client) CloseIdleConnections()
CloseIdleConnections
CloseIdleConnections
方法用于关闭任何处于空闲状态的 keep-alive
连接。这些连接可能是之前的请求所建立的,但现在已经空闲了一段时间。该方法不会中断任何当前正在使用的连接。
函数签名:
func (c *Client) CloseIdleConnections()
示例代码:
func main() {
c, err := client.NewClient()
if err != nil {
return
}
status, body, _ := c.Get(context.Background(), nil, "http://www.example.com")
fmt.Printf("status=%v body=%v\n", status, string(body))
// close idle connections
c.CloseIdleConnections()
}
中间件
func (c *Client) Use(mws ...Middleware)
func (c *Client) UseAsLast(mw Middleware) error
func (c *Client) TakeOutLastMiddleware() Middleware
Use
使用 Use
方法对当前 client 增加一个中间件。(更多内容请参考 客户端中间件)
函数签名:
func (c *Client) Use(mws ...Middleware)
UseAsLast
UseAsLast
函数将中间件添加到客户端中间件链的最后。
如果客户端中间件链在之前已经设置了最后一个中间件,UseAsLast
函数将会返回 errorLastMiddlewareExist
错误。因此,为确保客户端中间件链的最后一个中间件为空,可以先使用 TakeOutLastMiddleware 函数清空客户端中间件链的最后一个中间件。
注意:
UseAsLast
函数将中间件设置在了c.lastMiddleware
中,而使用 Use 函数设置的中间件链存放在c.mws
中,两者相对独立,只是在执行客户端中间件链的最后才执行c.lastMiddleware
,因此UseAsLast
函数在 Use 函数之前或之后调用皆可。
函数签名:
func (c *Client) UseAsLast(mw Middleware) error
示例代码:
func main() {
client, err := client.NewClient()
if err != nil {
return
}
client.Use(MyMiddleware)
client.UseAsLast(LastMiddleware)
req := &protocol.Request{}
res := &protocol.Response{}
req.SetRequestURI("http://www.example.com")
err = client.Do(context.Background(), req, res)
if err != nil {
return
}
}
4、网络库
Hertz 默认集成了 Netpoll 和 Golang 原生网络库两个网络库,用户可以根据自己的场景选择合适的网络库以达到最佳性能。
使用方式
对于 Server 来说,默认使用 netpoll,可以通过配置项进行更改:
注意:netpoll 目前不支持 Windows,Windows 会通过条件编译将网络库自动切换为 go net。
server.New(server.WithTransport(standard.NewTransporter))
server.New(server.WithTransport(netpoll.NewTransporter))
对于 Client 来说,可以通过配置项进行更改:
client.NewClient(client.WithDialer(standard.NewDialer()))
client.NewClient(client.WithDialer(netpoll.NewDialer()))
网络库选择
- 如果有启动 TLS Server 的需求,请使用
go net
网络库。netpoll
正在实现对 TLS 的支持。 - 由于网络库触发模式的不同:
go net
为 ET 模型,netpoll
为 LT 模型,使得两个网络库的适用场景有一些不同。 在 ET 模型下,由框架处理 Read / Write 事件;在 LT 模型下,由网络库处理 Read / Write 事件。 使得在小包场景下,由于更优的调度策略使得 LT 性能更好;在大包场景下,由于读 / 写不受框架层控制,使得大量数据被读入内存而不能及时处理,可能会造成内存压力。
- 在较大 request size 下(request size > 1M),推荐使用 go net 网络库加流式。
- 在其他场景下,推荐使用 netpoll 网络库,会获得极致的性能。