diff --git a/content/zh/docs/hertz/consolidated.md b/content/zh/docs/hertz/consolidated.md
new file mode 100644
index 0000000000..93e4bbbd57
--- /dev/null
+++ b/content/zh/docs/hertz/consolidated.md
@@ -0,0 +1,22637 @@
+
+
+---
+title: 'Hertz'
+linkTitle: 'Hertz'
+weight: 2
+menu:
+ main:
+ weight: 2
+ parent: '文档'
+---
+
+
+---
+title: "常见问题"
+linkTitle: "常见问题"
+weight: 4
+keywords: ["内存使用率", "常见错误码", "上下文使用", "精度丢失问题"]
+description: "Hertz 常见问题解答。"
+
+---
+
+## 内存使用率高
+
+### 客户端不规范使用没有关连接
+
+如果 Client 侧发起大量连接而不关闭的话,极端情况下会有较大的资源浪费,随着时间的增长,可能会造成内存使用率高的问题。
+
+**解决办法**
+
+合理配置 `idleTimeout`,超时后 Hertz Server 会把连接关掉保证 Server 侧的稳定性。默认配置为3分钟。
+
+### 超大请求/响应
+
+1. 如果请求和响应非常大,并且没有使用一些其他发送模式如 stream、chunk 时,数据会全部进入内存,给内存造成较大压力。
+2. netpoll 网络库下的流式为假流式。由于 netpoll 使用 LT 触发模式,当数据到达时,会触发 netpoll
+ 读取数据;在接口设计上,也因此没有实现 `Reader` 接口。为了实现流式的能力,Hertz 将 netpoll 封装为
+ Reader,但其本身数据仍然不可控的进入了内存,所以在超大流式请求的情况下,可能会造成内存压力。
+
+**解决办法**
+
+超大请求的场景下,使用流式 + go net 的组合。
+
+## 常见错误码排查
+
+如果框架报以下的错误码,可以按照可能原因进行排查。如果出现非以下错误码,则不是框架打出来的,需要由使用方定位一下是否自行设置或者由某些中间件设置了错误码。
+
+### 404
+
+1. 访问到了错误的端口上了,常见访问到了 debug 端口
+ 1. 解决方案:区分框架服务的监听端口和 debug server 的监听端口,默认:8888
+2. 未匹配到路由
+ 1. 根据启动日志查看是否所有预期路由都正常注册
+ 2. 查看访问方法是否正确
+
+### 417
+
+server 在执行完自定义的 `ContinueHandler` 之后返回 `false`(server 主动拒绝掉 100 Continue 后续的 body)。
+
+### 500
+
+1. 中间件或者 `handlerFunc` 中抛 panic
+ 1. 解决方案:panic 栈信息定位具体问题
+2. fs 场景 path 携带 `/../`,可能出现访问预期之外的文件,server 端 app log
+ 中伴随错误日志:`cannot serve path with '/../' at position %d due to security reasons: %q`。
+ 1. 解决方案:检查是否存在非法请求
+
+## 上下文使用指南
+
+### 说明
+
+Hertz 在 `HandlerFunc` 设计上,同时提供了一个标准 `context.Context` 和一个请求上下文作为函数的入参。
+`handler/middleware` 函数签名为:
+
+```go
+type HandlerFunc func(c context.Context, ctx *RequestContext)
+```
+
+### 元数据存储方面
+
+两个上下文都有储值能力,使用时具体选择哪一个的简单依据:所储存值的生命周期和所选择的上下文要匹配。
+
+**具体细节**
+
+`ctx` 主要用来存储请求级别的变量,请求结束就回收了,特点是查询效率高(底层是 `map`),协程不安全,且未实现 `context.Context`
+接口。
+`c` 作为上下文在中间件 `/handler` 之间传递。拥有 `context.Context` 的所有语义,协程安全。所有需要 `context.Context`
+接口作为入参的地方,直接传递 `c` 即可。
+
+除此之外,如果面对一定要异步传递 `ctx` 的场景,hertz 也提供了 `ctx.Copy()` 接口,方便业务能够获取到一个协程安全的副本。
+
+## 精度丢失问题
+
+### 说明
+
+1. JavaScript 的数字类型一旦数字超过限值时将会丢失精度,进而导致前后端的值出现不一致。
+
+```javascript
+var s = '{"x":6855337641038665531}';
+var obj = JSON.parse(s);
+alert (obj.x);
+
+// Output 6855337641038666000
+```
+
+2. 在 JSON 的规范中,对于数字类型是不区分整形和浮点型的。 在使用 `json.Unmarshal` 进行 JSON
+ 的反序列化的时候,如果没有指定数据类型,使用 `interface{}` 作为接收变量,将默认采用 `float64`
+ 作为其数字的接受类型,当数字的精度超过float能够表示的精度范围时就会造成精度丢失的问题。
+
+### 解决办法
+
+1. 使用 json 标准包的 `string` tag。
+
+```go
+package main
+
+import (
+ "context"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/cloudwego/hertz/pkg/protocol/consts"
+)
+
+type User struct {
+ ID int `json:"id,string"`
+}
+
+func main() {
+ h := server.Default()
+
+ h.GET("/hello", func(ctx context.Context, c *app.RequestContext) {
+ var u User
+ u.ID = 6855337641038665531
+ c.JSON(consts.StatusOK, u)
+ })
+
+ h.Spin()
+}
+```
+
+2. 使用 `json.Number`
+
+```go
+package main
+
+import (
+ "context"
+ "encoding/json"
+
+ "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"
+)
+
+type User struct {
+ ID json.Number `json:"id"`
+}
+
+func main() {
+ h := server.Default()
+
+ h.GET("/hello", func(ctx context.Context, c *app.RequestContext) {
+ var u User
+ err := json.Unmarshal([]byte(`{"id":6855337641038665531}`), &u)
+ if err != nil {
+ panic(err)
+ }
+ c.JSON(consts.StatusOK, u)
+ })
+
+ h.Spin()
+}
+
+```
+
+
+
+---
+title: "快速开始"
+linkTitle: "快速开始"
+weight: 2
+keywords: ["Hertz", "开发环境", "快速上手", "代码生成工具"]
+description: "Hertz 开发环境准备、快速上手与代码生成工具 hz 基本使用。"
+---
+
+## 准备 Golang 开发环境
+
+1. 如果您之前未搭建 Golang 开发环境,可以参考 [Golang 安装](https://golang.org/doc/install)。
+2. 推荐使用最新版本的 Golang,或保证现有 Golang 版本 >= 1.15。小于 1.15 版本,可以自行尝试使用但不保障兼容性和稳定性。
+3. 确保打开 go mod 支持 (Golang >= 1.15 时,默认开启)。
+
+> 目前,Hertz 支持 Linux、macOS、Windows 系统。
+
+## 快速上手
+
+在完成环境准备后,可以按照如下操作快速启动 Hertz Server:
+
+1. 在当前目录下创建 hertz_demo 文件夹,进入该目录中。
+2. 创建 `main.go` 文件。
+3. 在 `main.go` 文件中添加以下代码。
+
+ ```go
+ 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()
+
+ h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
+ ctx.JSON(consts.StatusOK, utils.H{"message": "pong"})
+ })
+
+ h.Spin()
+ }
+ ```
+
+4. 生成 `go.mod` 文件。
+
+ ```bash
+ go mod init hertz_demo
+ ```
+
+5. 整理 & 拉取依赖。
+
+ ```bash
+ go mod tidy
+ ```
+
+6. 运行示例代码。
+
+ ```bash
+ go run hertz_demo
+ ```
+
+ 如果成功启动,你将看到以下信息:
+
+ ```bash
+ 2022/05/17 21:47:09.626332 engine.go:567: [Debug] HERTZ: Method=GET absolutePath=/ping --> handlerName=main.main.func1 (num=2 handlers)
+ 2022/05/17 21:47:09.629874 transport.go:84: [Info] HERTZ: HTTP server listening on address=[::]:8888
+ ```
+
+ 接下来,我们可以对接口进行测试:
+
+ ```bash
+ curl http://127.0.0.1:8888/ping
+ ```
+
+ 如果不出意外,我们可以看到类似如下输出:
+
+ ```bash
+ {"message":"pong"}
+ ```
+
+## 代码自动生成工具 hz
+
+hz 是 Hertz 框架提供的一个用于生成代码的命令行工具,可以用于生成 Hertz 项目的脚手架。
+
+### 安装命令行工具 hz
+
+首先,我们需要安装使用本示例所需要的命令行工具 hz:
+
+1. 确保 `GOPATH` 环境变量已经被正确地定义(例如 `export GOPATH=~/go`)并且将 `$GOPATH/bin` 添加到 `PATH`
+ 环境变量之中(例如 `export PATH=$GOPATH/bin:$PATH`);请勿将 `GOPATH` 设置为当前用户没有读写权限的目录。
+2. 安装 hz:`go install github.com/cloudwego/hertz/cmd/hz@latest`。
+
+更多 hz 使用方法可参考: [hz](https://www.cloudwego.io/zh/docs/hertz/tutorials/toolkit/)。
+
+### 确定代码放置位置
+
+1. 若将代码放置于 `$GOPATH/src` 下,需在 `$GOPATH/src` 下创建额外目录,进入该目录后再获取代码:
+
+ ```bash
+ mkdir -p $(go env GOPATH)/src/github.com/cloudwego
+ cd $(go env GOPATH)/src/github.com/cloudwego
+ ```
+
+2. 若将代码放置于 `GOPATH` 之外,可直接获取。
+
+### 生成/编写示例代码
+
+1. 在当前目录下创建 hertz_demo 文件夹,进入该目录中。
+2. 生成代码 `hz new`,若当前不在 `GOPATH`,需要添加 `-module` 或者 `-mod` flag
+ 指定一个自定义的模块名称。详细参考[这里](https://www.cloudwego.io/zh/docs/hertz/tutorials/toolkit/usage/)。
+3. 整理 & 拉取依赖。
+
+ ```bash
+ go mod init # 当前目录不在 GOPATH 下不需要 `go mod init` 这一步
+ go mod tidy
+ ```
+
+### 运行示例代码
+
+完成以上操作后,我们可以直接编译并启动 Server。
+
+```bash
+go build -o hertz_demo && ./hertz_demo
+```
+
+如果成功启动,你将看到以下信息:
+
+```bash
+2022/05/17 21:47:09.626332 engine.go:567: [Debug] HERTZ: Method=GET absolutePath=/ping --> handlerName=main.main.func1 (num=2 handlers)
+2022/05/17 21:47:09.629874 transport.go:84: [Info] HERTZ: HTTP server listening on address=[::]:8888
+```
+
+接下来,我们可以对接口进行测试:
+
+```bash
+curl http://127.0.0.1:8888/ping
+```
+
+如果不出意外,我们可以看到类似如下输出:
+
+```bash
+{"message":"pong"}
+```
+
+到现在,我们已经成功启动了 Hertz Server,并完成了一次调用。
+
+## 更多示例
+
+参考:[代码示例](/zh/docs/hertz/tutorials/example/)
+
+
+---
+title: "开发指南"
+linkTitle: "开发指南"
+weight: 3
+keywords: ["开发指南", "代码示例", "基本特性", "可观测性", "治理特性", "框架拓展", "hz 代码生成", "迁移到 Hertz"]
+description: "Hertz 开发指南,包括代码示例、基本特性、可观测性、治理特性、框架拓展、hz 代码生成、迁移到 Hertz。"
+
+---
+
+
+---
+title: "Sentinel"
+date: 2022-09-29
+weight: 2
+keywords: ["治理特性", "Sentinel"]
+description: "Hertz 提供了 hertz-contrib/opensergo, 以方便用户集成 sentinel-golang。"
+---
+
+Hertz 提供了 [hertz-contrib/opensergo](https://github.com/hertz-contrib/opensergo), 以方便用户集成 sentinel-golang。
+
+## 安装
+
+```bash
+go get github.com/hertz-contrib/opensergo
+```
+
+## 配置
+
+前置介绍:
+
+> **热点参数**限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。
+
+### sentinel-golang
+
+关于 sentinel-golang 的基本配置,详情参考 [文档](https://sentinelguard.io/zh-cn/docs/golang/quick-start.html)
+
+### 服务端
+
+#### SentinelServerMiddleware
+
+`SentinelServerMiddleware()` 返回 `app.HandlerFunc` 类型,用于将 sentinel-golang 集成进入 hertz server
+
+默认资源名称为 {method}:{path},如 "GET:/api/users/:id", 默认 block 时返回 429 状态码
+
+可以通过 `WithServerXxx()` 函数来进行自定义格式
+
+示例代码:
+
+```go
+package main
+
+// ...
+
+func main() {
+ h := server.Default(server.WithHostPorts(":8081"))
+ h.Use(adaptor.SentinelServerMiddleware())
+ // ...
+}
+```
+
+#### WithServerResourceExtractor
+
+`WithResourceExtractor` 为设置网络请求的自定义函数,通过自定义的资源名和 sentinel-golang 中的**热点参数**流控规则
+的 `Resource` 相匹配以达到自定义规则的目的
+
+示例代码:
+
+```go
+package main
+
+// ...
+
+func main() {
+ h := server.Default(server.WithHostPorts(":8081"))
+ h.Use(adaptor.SentinelServerMiddleware(
+ // customize resource extractor if required
+ // method_path by default
+ adaptor.WithServerResourceExtractor(func(c context.Context, ctx *app.RequestContext) string {
+ return "server_test"
+ }),
+ ))
+ // ...
+}
+```
+
+#### WithServerBlockFallback
+
+`WithServerBlockFallback` 为设置请求被阻断时的自定义回调函数,可以通过 `context.Context` 和 `app.RequestContext`
+分别来进行错误日志打印和自定义回调处理
+
+示例代码:
+
+```go
+package main
+
+// ...
+
+func main() {
+ h := server.Default(server.WithHostPorts(":8081"))
+ h.Use(adaptor.SentinelServerMiddleware(
+ // customize block fallback if required
+ // abort with status 429 by default
+ adaptor.WithServerBlockFallback(func(c context.Context, ctx *app.RequestContext) {
+ ctx.AbortWithStatusJSON(400, utils.H{
+ "err": "too many request; the quota used up",
+ "code": 10222,
+ })
+ }),
+ ))
+ // ...
+}
+```
+
+### 客户端
+
+#### SentinelClientMiddleware
+
+`SentinelClientMiddleware()` 返回一个 `client.Middleware` 类型,用于将 sentinel-golang 集成进入 hertz client
+
+默认的资源名格式为 {method}:{path}, 例如 "GET:/api/users", 默认 block 时返回 `blockError`
+
+可以通过 `WithClientXxx()` 函数来进行自定义格式
+
+示例代码:
+
+```go
+package main
+
+// ...
+
+func main() {
+ c, err := client.NewClient()
+ if err != nil {
+ log.Fatalf("Unexpected error: %+v", err)
+ return
+ }
+
+ c.Use(adaptor.SentinelClientMiddleware())
+}
+```
+
+#### WithClientResourceExtractor
+
+`WithClientResourceExtractor` 为设置网络请求的自定义函数,通过自定义的资源名和 sentinel-golang 中的 **热点参数** 流控规则
+的 `Resource` 相匹配以达到自定义规则的目的
+
+示例代码:
+
+```go
+package main
+
+// ...
+
+func main() {
+ c, err := client.NewClient()
+ if err != nil {
+ log.Fatalf("Unexpected error: %+v", err)
+ return
+ }
+
+ c.Use(adaptor.SentinelClientMiddleware(
+ // customize resource extractor if required
+ // method_path by default
+ adaptor.WithClientResourceExtractor(func(ctx context.Context, request *protocol.Request, response *protocol.Response) string {
+ return "client_test"
+ }),
+ ))
+}
+```
+
+#### WithClientBlockFallback
+
+`WithClientBlockFallback` 为设置请求被阻断时的自定义回调函数,可以通过 `context.Context`, `protocol.Request`
+, `protocol.Response` 来进行错误日志打印等功能,也可以通过自定义回调处理 `error` 来进行自定义错误处理。
+
+示例代码:
+
+```go
+package main
+
+// ...
+
+func main() {
+ c, err := client.NewClient()
+ if err != nil {
+ log.Fatalf("Unexpected error: %+v", err)
+ return
+ }
+
+ c.Use(adaptor.SentinelClientMiddleware(
+ // customize resource extractor if required
+ // method_path by default
+ adaptor.WithClientBlockFallback(func(ctx context.Context, req *protocol.Request, resp *protocol.Response, blockError error) error {
+ resp.SetStatusCode(http.StatusBadRequest)
+ resp.SetBody([]byte("request failed"))
+ return blockError
+ }),
+ ))
+}
+```
+
+## 完整示例代码
+
+完整用法示例详见 [example](https://github.com/cloudwego/hertz-examples/tree/main/sentinel/hertz)
+
+
+---
+title: "治理特性"
+linkTitle: "治理特性"
+weight: 4
+keywords: ["治理特性", "服务注册与发现", "Sentinel"]
+description: "Hertz 提供的治理特性。"
+
+---
+
+
+---
+title: "consul"
+date: 2023-04-22
+weight: 3
+keywords: ["服务注册与发现", "consul"]
+description: "Hertz 提供的服务注册与发现 consul 拓展。"
+---
+
+## 安装
+
+```go
+go get github.com/hertz-contrib/registry/consul
+```
+
+## 服务注册
+
+### Option
+
+Consul 拓展在服务注册部分中提供了 option 配置。
+
+#### WithCheck
+
+Consul 扩展提供了 `WithCheck` 用于帮助用户配置 Consul 中的 `AgentServiceCheck` 选项。若不使用,则默认设置 `check.Timeout`
+为 5 秒,`check.Internal` 为 5 秒,`check.DeregisterCriticalServiceAfter` 为 1 分钟。
+
+函数签名:
+
+```go
+func WithCheck(check *api.AgentServiceCheck) Option
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ consulClient, err := consulapi.NewClient(config)
+ // ...
+ check := &consulapi.AgentServiceCheck{
+ // ...
+ }
+ r := consul.NewConsulRegister(consulClient, consul.WithCheck(check))
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ // ...
+}
+```
+
+### NewConsulRegister
+
+`NewConsulRegister` 使用 consul 创建一个可配置客户端的服务注册中心,需要传入客户端,其中客户端使用 `NewClient`
+创建。可自定义服务注册中心配置。
+
+函数签名:
+
+```go
+func NewConsulRegister(consulClient *api.Client, opts ...Option) registry.Registry
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ consulClient, err := consulapi.NewClient(config)
+ // ...
+ r := consul.NewConsulRegister(consulClient)
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ // ...
+}
+```
+
+## 服务发现
+
+### NewConsulResolver
+
+`NewConsulResolver` 使用 consul 创建一个新的服务发现中心,需要传入客户端,其中客户端使用 `NewClient` 创建。可自定义服务发现中心配置。
+
+函数签名:
+
+```go
+func NewConsulResolver(consulClient *api.Client) discovery.Resolver
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ consulClient, err := consulapi.NewClient(consulConfig)
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ r := consul.NewConsulResolver(consulClient)
+
+ cli, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ cli.Use(sd.Discovery(r))
+}
+```
+
+## 使用示例
+
+### 服务端
+
+```go
+import (
+ "context"
+ "log"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/cloudwego/hertz/pkg/app/server/registry"
+ "github.com/cloudwego/hertz/pkg/common/utils"
+ "github.com/cloudwego/hertz/pkg/protocol/consts"
+ consulapi "github.com/hashicorp/consul/api"
+ "github.com/hertz-contrib/registry/consul"
+)
+
+
+func main() {
+ // build a consul client
+ config := consulapi.DefaultConfig()
+ config.Address = "127.0.0.1:8500"
+ consulClient, err := consulapi.NewClient(config)
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ // build a consul register with the consul client
+ r := consul.NewConsulRegister(consulClient)
+
+ // run Hertz with the consul register
+ addr := "127.0.0.1:8888"
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
+ ctx.JSON(consts.StatusOK, utils.H{"ping": "pong1"})
+ })
+ h.Spin()
+}
+```
+
+### 客户端
+
+```go
+import (
+ "log"
+
+ "github.com/cloudwego/hertz/pkg/app/client"
+ "github.com/cloudwego/hertz/pkg/app/middlewares/client/sd"
+ consulapi "github.com/hashicorp/consul/api"
+ "github.com/hertz-contrib/registry/consul"
+)
+
+func main() {
+ // build a consul client
+ consulConfig := consulapi.DefaultConfig()
+ consulConfig.Address = "127.0.0.1:8500"
+ consulClient, err := consulapi.NewClient(consulConfig)
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ // build a consul resolver with the consul client
+ r := consul.NewConsulResolver(consulClient)
+
+ // build a hertz client with the consul resolver
+ cli, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ cli.Use(sd.Discovery(r))
+}
+```
+
+## 配置
+
+可自定义 Consul 客户端以及服务端的配置,参考 [consul](https://github.com/hashicorp/consul) 配置。
+
+## 完整实例
+
+完整用法示例详见 [example](https://github.com/hertz-contrib/registry/tree/main/consul/example) 。
+
+
+---
+title: "nacos"
+date: 2023-04-22
+weight: 2
+keywords: ["服务注册与发现", "nacos"]
+description: "Hertz 提供的服务注册与发现 nacos 拓展。"
+---
+
+## 安装
+
+```go
+go get github.com/hertz-contrib/registry/nacos
+```
+
+## 服务注册
+
+### Option
+
+Nacos 拓展在服务注册部分中提供了 option 配置。
+
+#### WithRegistryCluster
+
+Nacos 扩展提供了 `WithRegistryCluster` 用于帮助用户配置自定义的集群。默认为“DEFAULT” 。
+
+函数签名:
+
+```go
+func WithRegistryCluster(cluster string) RegistryOption
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ r, err := nacos.NewDefaultNacosRegistry(
+ nacos.WithRegistryCluster("Cluster123"),
+ )
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ // ...
+}
+```
+
+#### WithRegistryGroup
+
+Nacos 扩展提供了 `WithRegistryGroup` 用于帮助用户配置自定义的集群。默认为 "DEFAULT_GROUP" 。
+
+函数签名:
+
+```go
+func WithRegistryGroup(group string) RegistryOption
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ r, err := nacos.NewDefaultNacosRegistry(
+ nacos.WithRegistryGroup("Group1"),
+ )
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ // ...
+}
+```
+
+### NewDefaultNacosRegistry
+
+`NewDefaultNacosRegistry` 使用 nacos 创建一个默认的服务注册中心。会调用 `NewDefaultNacosConfig` 读取环境变量来创建一个默认的
+nacos 客户端,并设置 RegionId 为 `cn-hangzhou`,且不会在启动时自动预加载服务实例信息到本地缓存。可自定义服务注册中心配置。
+
+环境变量:
+
+| 环境变量名 | 环境变量默认值 | 描述 |
+|------------|-----------|-----------------------|
+| serverAddr | 127.0.0.1 | nacos 服务器地址 |
+| serverPort | 8848 | nacos 服务器端口 |
+| namespace | | nacos 中的 namespace Id |
+
+函数签名:
+
+```go
+func NewDefaultNacosRegistry(opts ...RegistryOption) (registry.Registry, error)
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ r, err := nacos.NewDefaultNacosRegistry()
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ // ...
+}
+```
+
+### NewNacosRegistry
+
+`NewNacosRegistry`使用 nacos 创建一个可配置客户端的服务注册中心,需要传入自行配置的客户端。可自定义服务注册中心配置。
+
+函数签名:
+
+```go
+func NewNacosRegistry(client naming_client.INamingClient, opts ...RegistryOption) registry.Registry
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ cli, err := clients.NewNamingClient(
+ vo.NacosClientParam{
+ ClientConfig: &cc,
+ ServerConfigs: sc,
+ },
+ )
+ // ...
+ r := nacos.NewNacosRegistry(cli)
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }))
+ // ...
+}
+```
+
+## 服务发现
+
+### Option
+
+Nacos 拓展在服务发现部分中提供了 option 配置。
+
+#### WithResolverCluster
+
+Nacos 扩展提供了 `WithResolverCluster` 用于帮助用户配置自定义的集群。默认为“DEFAULT” 。
+
+函数签名:
+
+```go
+func WithResolverCluster(cluster string) ResolverOption
+```
+
+示例代码:
+
+```go
+func main() {
+ client, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ r, err := nacos.NewDefaultNacosResolver(
+ nacos.WithResolverCluster("Cluster123"),
+ )
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ client.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+#### WithResolverGroup
+
+Nacos 扩展提供了 `WithResolverGroup` 用于帮助用户配置自定义的集群。默认为 "DEFAULT_GROUP" 。
+
+函数签名:
+
+```go
+func WithResolverGroup(group string) ResolverOption
+```
+
+示例代码:
+
+```go
+func main() {
+ client, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ r, err := nacos.NewDefaultNacosResolver(
+ nacos.WithResolverGroup("Group1"),
+ )
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ client.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+### NewDefaultNacosResolver
+
+`NewDefaultNacosResolver` 使用 nacos 创建一个默认的服务发现中心。会调用 `NewDefaultNacosConfig` 读取环境变量来创建一个默认的
+nacos 客户端,并设置 RegionId 为 `cn-hangzhou`,且不会在启动时自动预加载服务实例信息到本地缓存。可自定义服务注册中心配置。
+
+环境变量:
+
+| 环境变量名 | 环境变量默认值 | 描述 |
+|------------|-----------|-----------------------|
+| serverAddr | 127.0.0.1 | nacos 服务器地址 |
+| serverPort | 8848 | nacos 服务器端口 |
+| namespace | | nacos 中的 namespace Id |
+
+函数签名:
+
+```go
+func NewDefaultNacosResolver(opts ...ResolverOption) (discovery.Resolver, error)
+```
+
+示例代码:
+
+```go
+func main() {
+ client, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ r, err := nacos.NewDefaultNacosResolver()
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ client.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+### NewNacosResolver
+
+`NewNacosResolver` 使用 nacos 创建一个可配置客户端的服务发现中心,需要传入自行配置的客户端。可自定义服务发现中心配置。
+
+函数签名:
+
+```go
+func NewNacosResolver(cli naming_client.INamingClient, opts ...ResolverOption) discovery.Resolver
+```
+
+示例代码:
+
+```go
+func main() {
+ cli, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ // ...
+ nacosCli, err := clients.NewNamingClient(
+ vo.NacosClientParam{
+ ClientConfig: &cc,
+ ServerConfigs: sc,
+ })
+ if err != nil {
+ panic(err)
+ }
+ r := nacos.NewNacosResolver(nacosCli)
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+## 使用示例
+
+### 服务端
+
+- 使用 `server.WithRegistry` 设置注册扩展以及注册信息。
+
+```go
+import (
+ "context"
+ "log"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/cloudwego/hertz/pkg/app/server/registry"
+ "github.com/cloudwego/hertz/pkg/common/utils"
+ "github.com/cloudwego/hertz/pkg/protocol/consts"
+ "github.com/hertz-contrib/registry/nacos"
+)
+
+func main() {
+ addr := "127.0.0.1:8888"
+ r, err := nacos.NewDefaultNacosRegistry()
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
+ ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
+ })
+ h.Spin()
+}
+```
+
+### 客户端
+
+- 使用内置的 `sd.Discovery` 中间件,支持传入自定义的服务发现扩展以及负载均衡扩展。
+- 使用服务发现时需要将 url 中的域名替换为服务名,并使用 `config.WithSD` 确定本次请求使用服务注册。
+
+```go
+import (
+ "context"
+ "log"
+
+ "github.com/cloudwego/hertz/pkg/app/client"
+ "github.com/cloudwego/hertz/pkg/app/middlewares/client/sd"
+ "github.com/cloudwego/hertz/pkg/common/config"
+ "github.com/cloudwego/hertz/pkg/common/hlog"
+ "github.com/hertz-contrib/registry/nacos"
+)
+
+func main() {
+ client, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ r, err := nacos.NewDefaultNacosResolver()
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ client.Use(sd.Discovery(r))
+ for i := 0; i < 10; i++ {
+ status, body, err := client.Get(context.Background(), nil, "http://hertz.test.demo/ping", config.WithSD(true))
+ if err != nil {
+ hlog.Fatal(err)
+ }
+ hlog.Infof("code=%d,body=%s\n", status, string(body))
+ }
+}
+```
+
+## 配置
+
+可自定义 Nacos 客户端以及服务端的配置,参考 [nacos-sdk-go](https://github.com/nacos-group/nacos-sdk-go) 配置。
+
+## 完整示例
+
+完整用法示例详见 [example](https://github.com/hertz-contrib/registry/tree/main/nacos/examples) 。
+
+
+---
+title: "etcd"
+date: 2023-04-22
+weight: 4
+keywords: ["服务注册与发现", "etcd"]
+description: "Hertz 提供的服务注册与发现 etcd 拓展。"
+---
+
+## 安装
+
+```go
+go get github.com/hertz-contrib/registry/etcd
+```
+
+## 服务注册
+
+### Option
+
+Etcd 拓展在服务注册部分中提供了 option 配置。
+
+#### WithTLSOpt
+
+Etcd 扩展提供了 `WithTLSOpt` 用于帮助用户配置 Etcd 中的`TLS`选项。
+
+函数签名:
+
+```go
+func WithTLSOpt(certFile, keyFile, caFile string) Option
+```
+
+示例代码:
+
+```go
+func main() {
+ r, err := etcd.NewEtcdRegistry([]string{"127.0.0.1:2379"},
+ etcd.WithTLSOpt(certFile, keyFile, caFile),
+ )
+ if err != nil {
+ panic(err)
+ }
+ // ...
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }))
+ // ...
+}
+```
+
+#### WithAuthOpt
+
+Etcd 扩展提供了`WithAuthOpt`用于帮助用户配置 Etcd 中的`Username`和`Password`选项。
+
+函数签名:
+
+```go
+func WithAuthOpt(username, password string) Option
+```
+
+示例代码:
+
+```go
+func main() {
+ r, err := etcd.NewEtcdRegistry([]string{"127.0.0.1:2379"},
+ etcd.WithAuthOpt("root","123456"),
+ )
+ if err != nil {
+ panic(err)
+ }
+ // ...
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }))
+ // ...
+}
+```
+
+### NewEtcdRegistry
+
+`NewEtcdRegistry` 使用 etcd 创建一个新的服务注册中心,需要传入端点值。可自定义服务注册中心配置。
+
+函数签名:
+
+```go
+func NewEtcdRegistry(endpoints []string, opts ...Option) (registry.Registry, error)
+```
+
+示例代码:
+
+```go
+func main() {
+ r, err := etcd.NewEtcdRegistry([]string{"127.0.0.1:2379"})
+ if err != nil {
+ panic(err)
+ }
+ // ...
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }))
+ // ...
+}
+```
+
+## 服务发现
+
+### Option
+
+Etcd 拓展在服务发现部分中提供了 option 配置。
+
+#### WithTLSOpt
+
+Etcd 扩展提供了 `WithTLSOpt` 用于帮助用户配置 Etcd 中的`TLS`选项。
+
+函数签名:
+
+```go
+func WithTLSOpt(certFile, keyFile, caFile string) Option
+```
+
+示例代码:
+
+```go
+func main() {
+ cli, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ r, err := etcd.NewEtcdResolver([]string{"127.0.0.1:2379"},
+ etcd.WithTLSOpt(certFile, keyFile, caFile),
+ )
+ if err != nil {
+ panic(err)
+ }
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+#### WithAuthOpt
+
+Etcd 扩展提供了`WithAuthOpt`用于帮助用户配置 Etcd 中的`Username`和`Password`选项。
+
+函数签名:
+
+```go
+func WithAuthOpt(username, password string) Option
+```
+
+示例代码:
+
+```
+func main() {
+ cli, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ r, err := etcd.NewEtcdResolver([]string{"127.0.0.1:2379"},
+ etcd.WithAuthOpt("root","123456"),
+ )
+ if err != nil {
+ panic(err)
+ }
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+### NewEtcdResolver
+
+`NewEtcdResolver` 使用 etcd 创建一个新的服务发现中心,需要传入端点值。可自定义服务发现中心配置。
+
+函数签名:
+
+```go
+func NewEtcdResolver(endpoints []string, opts ...Option) (discovery.Resolver, error)
+```
+
+示例代码:
+
+```go
+func main() {
+ cli, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ r, err := etcd.NewEtcdResolver([]string{"127.0.0.1:2379"})
+ if err != nil {
+ panic(err)
+ }
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+## 使用示例
+
+### 服务端
+
+```go
+import (
+ "context"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/cloudwego/hertz/pkg/app/server/registry"
+ "github.com/cloudwego/hertz/pkg/common/utils"
+ "github.com/cloudwego/hertz/pkg/protocol/consts"
+ "github.com/hertz-contrib/registry/etcd"
+)
+
+func main() {
+ r, err := etcd.NewEtcdRegistry([]string{"127.0.0.1:2379"})
+ if err != nil {
+ panic(err)
+ }
+ addr := "127.0.0.1:8888"
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }))
+ h.GET("/ping", func(_ context.Context, ctx *app.RequestContext) {
+ ctx.JSON(consts.StatusOK, utils.H{"ping": "pong2"})
+ })
+ h.Spin()
+}
+```
+
+### 客户端
+
+```go
+import (
+ "context"
+
+ "github.com/cloudwego/hertz/pkg/app/client"
+ "github.com/cloudwego/hertz/pkg/app/middlewares/client/sd"
+ "github.com/cloudwego/hertz/pkg/common/config"
+ "github.com/cloudwego/hertz/pkg/common/hlog"
+ "github.com/hertz-contrib/registry/etcd"
+)
+
+func main() {
+ cli, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ r, err := etcd.NewEtcdResolver([]string{"127.0.0.1:2379"})
+ if err != nil {
+ panic(err)
+ }
+ cli.Use(sd.Discovery(r))
+ for i := 0; i < 10; i++ {
+ status, body, err := cli.Get(context.Background(), nil, "http://hertz.test.demo/ping", config.WithSD(true))
+ if err != nil {
+ hlog.Fatal(err)
+ }
+ hlog.Infof("HERTZ: code=%d,body=%s", status, string(body))
+ }
+}
+```
+
+## 配置
+
+可自定义 Etcd 客户端以及服务端的配置,参考 [etcd-client](https://pkg.go.dev/go.etcd.io/etcd/client/v3) 配置。
+
+## 完整示例
+
+完整用法示例详见 [example](https://github.com/hertz-contrib/registry/tree/main/etcd/example) 。
+
+
+---
+title: "redis"
+date: 2023-04-22
+weight: 9
+keywords: ["服务注册与发现", "redis"]
+description: "Hertz 提供的服务注册与发现 redis 拓展。"
+---
+
+## 安装
+
+```go
+go get github.com/hertz-contrib/registry/redis
+```
+
+## 服务注册
+
+### Option
+
+Redis 拓展在服务注册部分中提供了 option 配置。
+
+#### WithPassword
+
+Redis 扩展提供了 `WithPassword` 配置 redis 的密码,此密码必须匹配服务器配置选项中指定的密码。默认为空。
+
+函数签名:
+
+```go
+func WithPassword(password string) Option
+```
+
+示例代码:
+
+```go
+func main() {
+ r := redis.NewRedisRegistry("127.0.0.1:6379", redis.WithPassword("123456"))
+ // ...
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ // ...
+}
+```
+
+#### WithDB
+
+Redis 扩展提供了 `WithDB` 配置连接到服务器后要选择的数据库。默认为 0。
+
+函数签名:
+
+```go
+func WithDB(db int) Option
+```
+
+示例代码:
+
+```go
+func main() {
+ r := redis.NewRedisRegistry("127.0.0.1:6379", redis.WithDB(1))
+ // ...
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ // ...
+}
+```
+
+#### WithTLSConfig
+
+Redis 扩展提供了 `WithTLSConfig` 配置 TLS 的配置项。
+
+函数签名:
+
+```go
+func WithTLSConfig(t *tls.Config) Option
+```
+
+示例代码:
+
+```go
+func main() {
+ r := redis.NewRedisRegistry("127.0.0.1:6379", redis.WithTLSConfig(&tls.Config{
+ // ...
+ }))
+ // ...
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ // ...
+}
+```
+
+#### WithDialer
+
+Redis 扩展提供了 `WithDialer` 配置 Dialer,Dialer 将会创建新的网络连接并优先于 Network 和 Addr 选项。
+
+函数签名:
+
+```go
+func WithDialer(dialer func(ctx context.Context, network, addr string) (net.Conn, error)) Option
+```
+
+示例代码:
+
+```go
+func main() {
+ r := redis.NewRedisRegistry("127.0.0.1:6379", redis.WithDialer(
+ // ...
+ ))
+ // ...
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ // ...
+}
+```
+
+#### WithReadTimeout
+
+Redis 扩展提供了 `WithReadTimeout` 配置读取 socket 超时的时间,默认为 3 秒。
+
+函数签名:
+
+```go
+func WithReadTimeout(t time.Duration) Option
+```
+
+示例代码:
+
+```go
+func main() {
+ r := redis.NewRedisRegistry("127.0.0.1:6379", redis.WithReadTimeout(5*time.Second))
+ // ...
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ // ...
+}
+```
+
+#### WithWriteTimeout
+
+Redis 扩展提供了 `WithWriteTimeout` 配置写入 socket 超时的时间,默认等同于 `ReadTimeout`。
+
+函数签名:
+
+```go
+func WithWriteTimeout(t time.Duration) Option
+```
+
+示例代码:
+
+```go
+func main() {
+ r := redis.NewRedisRegistry("127.0.0.1:6379", redis.WithWriteTimeout(5*time.Second))
+ // ...
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ // ...
+}
+```
+
+### NewRedisRegistry
+
+`NewRedisRegistry` 使用 redis 创建一个新的服务注册中心,需要传入目标地址。可自定义客户端配置并传入 `NewClient` 创建一个新的客户端。
+
+函数签名:
+
+```go
+func NewRedisRegistry(addr string, opts ...Option) registry.Registry
+```
+
+示例代码:
+
+```go
+func main() {
+ r := redis.NewRedisRegistry("127.0.0.1:6379")
+ // ...
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ // ...
+}
+```
+
+## 服务发现
+
+### Option
+
+Redis 拓展在服务发现部分中提供了 option 配置。
+
+#### WithPassword
+
+Redis 扩展提供了 `WithPassword` 配置 redis 的密码,此密码必须匹配服务器配置选项中指定的密码。默认为空。
+
+函数签名:
+
+```go
+func WithPassword(password string) Option
+```
+
+示例代码:
+
+```go
+func main() {
+ cli, err := client.NewClient()
+ // ...
+ r := redis.NewRedisResolver("127.0.0.1:6379", redis.WithPassword("123456"))
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+#### WithDB
+
+Redis 扩展提供了 `WithDB` 配置连接到服务器后要选择的数据库。默认为 0。
+
+函数签名:
+
+```go
+func WithDB(db int) Option
+```
+
+示例代码:
+
+```go
+func main() {
+ cli, err := client.NewClient()
+ // ...
+ r := redis.NewRedisResolver("127.0.0.1:6379", redis.WithDB(1))
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+#### WithTLSConfig
+
+Redis 扩展提供了 `WithTLSConfig` 配置 TLS 的配置项。
+
+函数签名:
+
+```go
+func WithTLSConfig(t *tls.Config) Option
+```
+
+示例代码:
+
+```go
+func main() {
+ cli, err := client.NewClient()
+ // ...
+ r := redis.NewRedisResolver("127.0.0.1:6379", redis.WithTLSConfig(&tls.Config{
+ // ...
+ }))
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+#### WithDialer
+
+Redis 扩展提供了 `WithDialer` 配置 Dialer,Dialer 将会创建新的网络连接并优先于 Network 和 Addr 选项。
+
+函数签名:
+
+```go
+func WithDialer(dialer func(ctx context.Context, network, addr string) (net.Conn, error)) Option
+```
+
+示例代码:
+
+```go
+func main() {
+ cli, err := client.NewClient()
+ // ...
+ r := redis.NewRedisRegistry("127.0.0.1:6379", redis.WithDialer(
+ // ...
+ ))
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+#### WithReadTimeout
+
+Redis 扩展提供了 `WithReadTimeout` 配置读取 socket 超时的时间,默认为 3 秒。
+
+函数签名:
+
+```go
+func WithReadTimeout(t time.Duration) Option
+```
+
+示例代码:
+
+```go
+func main() {
+ cli, err := client.NewClient()
+ // ...
+ r := redis.NewRedisRegistry("127.0.0.1:6379", redis.WithReadTimeout(5*time.Second))
+ // ...
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+#### WithWriteTimeout
+
+Redis 扩展提供了 `WithWriteTimeout` 配置写入 socket 超时的时间,默认等同于 `ReadTimeout`。
+
+函数签名:
+
+```go
+func WithWriteTimeout(t time.Duration) Option
+```
+
+示例代码:
+
+```go
+func main() {
+ cli, err := client.NewClient()
+ // ...
+ r := redis.NewRedisRegistry("127.0.0.1:6379", redis.WithWriteTimeout(5*time.Second))
+ // ...
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+### NewRedisResolver
+
+`NewRedisResolver` 使用 redis 创建一个新的服务发现中心,需要传入目标地址。可自定义客户端配置并传入 `NewClient` 创建一个新的客户端。
+
+函数签名:
+
+```go
+func NewRedisResolver(addr string, opts ...Option) discovery.Resolver
+```
+
+示例代码:
+
+```go
+func main() {
+ cli, err := client.NewClient()
+ // ...
+ r := redis.NewRedisResolver("127.0.0.1:6379")
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+## 使用示例
+
+### 服务端
+
+```go
+package main
+
+import (
+ "context"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/cloudwego/hertz/pkg/app/server/registry"
+ "github.com/cloudwego/hertz/pkg/common/utils"
+ "github.com/cloudwego/hertz/pkg/protocol/consts"
+ "github.com/hertz-contrib/registry/redis"
+)
+
+func main() {
+ r := redis.NewRedisRegistry("127.0.0.1:6379")
+ addr := "127.0.0.1:8888"
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ h.GET("/ping", func(_ context.Context, ctx *app.RequestContext) {
+ ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
+ })
+ h.Spin()
+}
+```
+
+### 客户端
+
+```go
+package main
+
+import (
+ "context"
+
+ "github.com/cloudwego/hertz/pkg/app/client"
+ "github.com/cloudwego/hertz/pkg/app/middlewares/client/sd"
+ "github.com/cloudwego/hertz/pkg/common/config"
+ "github.com/cloudwego/hertz/pkg/common/hlog"
+ "github.com/hertz-contrib/registry/redis"
+)
+
+func main() {
+ cli, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ r := redis.NewRedisResolver("127.0.0.1:6379")
+ cli.Use(sd.Discovery(r))
+ for i := 0; i < 10; i++ {
+ status, body, err := cli.Get(context.Background(), nil, "http://hertz.test.demo/ping", config.WithSD(true))
+ if err != nil {
+ hlog.Fatal(err)
+ }
+ hlog.Infof("HERTZ: code=%d,body=%s", status, string(body))
+ }
+}
+```
+
+## 配置
+
+可自定义 redis 客户端以及服务端的配置,参考 [go-redis](https://github.com/go-redis/redis) 配置。
+
+## 完整示例
+
+完整用法示例详见 [example](https://github.com/hertz-contrib/registry/tree/main/redis/example) 。
+
+
+---
+title: "eureka"
+date: 2023-04-22
+weight: 5
+keywords: ["服务注册与发现", "eureka"]
+description: "Hertz 提供的服务注册与发现 eureka 拓展。"
+---
+
+## 安装
+
+```go
+go get github.com/hertz-contrib/eureka
+```
+
+## 服务注册
+
+### NewEurekaRegistry
+
+`NewEurekaRegistry` 使用 eureka 创建一个新的服务注册中心,需要将服务 Url 通过一个字符串切片传入 `NewConn`,并同时传入心跳间隔时长。
+
+函数签名:
+
+```go
+func NewEurekaRegistry(servers []string, heatBeatInterval time.Duration) *eurekaRegistry
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ r := eureka.NewEurekaRegistry([]string{"http://127.0.0.1:8761/eureka"}, 40*time.Second)
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.discovery.eureka",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }))
+ //...
+}
+```
+
+### NewEurekaRegistryFromConfig
+
+`NewEurekaRegistryFromConfig` 使用 eureka 创建一个新的服务注册中心,需要传入配置并调用 `NewConnFromConfig`,也需要传入心跳间隔时长。
+
+函数签名:
+
+```go
+func NewEurekaRegistryFromConfig(config fargo.Config, heatBeatInterval time.Duration) *eurekaRegistry
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ config := fargo.Config{
+ // ...
+ }
+ r := eureka.NewEurekaRegistryFromConfig(config, 40*time.Second)
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.discovery.eureka",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }))
+ //...
+}
+```
+
+### NewEurekaRegistryFromConn
+
+`NewEurekaRegistryFromConn` 使用 eureka 创建一个新的服务注册中心,需要直接传入 conn,也需要传入心跳间隔时长。
+
+函数签名:
+
+```go
+func NewEurekaRegistryFromConn(conn fargo.EurekaConnection, heatBeatInterval time.Duration) *eurekaRegistry
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ conn := fargo.EurekaConnection{
+ // ...
+ }
+ r := eureka.NewEurekaRegistryFromConn(conn, 40*time.Second)
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.discovery.eureka",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }))
+ //...
+}
+```
+
+## 服务发现
+
+### NewEurekaResolver
+
+`NewEurekaResolver` 使用 eureka 创建一个新的服务发现中心,需要将服务 Url 通过一个字符串切片传入 `NewConn`。
+
+函数签名:
+
+```go
+func NewEurekaResolver(servers []string) *eurekaResolver
+```
+
+示例代码:
+
+```go
+func main() {
+ cli, err := client.NewClient()
+ if err != nil {
+ hlog.Fatal(err)
+ return
+ }
+ r := eureka.NewEurekaResolver([]string{"http://127.0.0.1:8761/eureka"})
+
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+### NewEurekaResolverFromConfig
+
+`NewEurekaResolverFromConfig` 使用 eureka 创建一个新的服务发现中心,需要传入配置并调用 `NewConnFromConfig`。
+
+函数签名:
+
+```go
+func NewEurekaResolverFromConfig(config fargo.Config) *eurekaResolver
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ config := fargo.Config{
+ // ...
+ }
+ cli, err := client.NewClient()
+ if err != nil {
+ hlog.Fatal(err)
+ return
+ }
+ r := eureka.NewEurekaResolverFromConfig(config)
+
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+### NewEurekaResolverFromConn
+
+`NewEurekaResolverFromConn` 使用 eureka 创建一个新的服务发现中心,需要直接传入 conn。
+
+函数签名:
+
+```go
+func NewEurekaResolverFromConn(conn fargo.EurekaConnection) *eurekaResolver
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ conn := fargo.EurekaConnection{
+ // ...
+ }
+ cli, err := client.NewClient()
+ if err != nil {
+ hlog.Fatal(err)
+ return
+ }
+ r := eureka.NewEurekaResolverFromConn(conn)
+
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+## 使用示例
+
+### 服务端
+
+```go
+import (
+ "context"
+ "time"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/cloudwego/hertz/pkg/app/server/registry"
+ "github.com/cloudwego/hertz/pkg/common/utils"
+ "github.com/cloudwego/hertz/pkg/protocol/consts"
+ "github.com/hertz-contrib/registry/eureka"
+)
+
+func main() {
+ addr := "127.0.0.1:8888"
+ r := eureka.NewEurekaRegistry([]string{"http://127.0.0.1:8761/eureka"}, 40*time.Second)
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.discovery.eureka",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }))
+ h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
+ ctx.JSON(consts.StatusOK, utils.H{"ping": "pong2"})
+ })
+ h.Spin()
+}
+```
+
+### 客户端
+
+```go
+import (
+ "context"
+
+ "github.com/cloudwego/hertz/pkg/app/client"
+ "github.com/cloudwego/hertz/pkg/app/middlewares/client/sd"
+ "github.com/cloudwego/hertz/pkg/common/config"
+ "github.com/cloudwego/hertz/pkg/common/hlog"
+ "github.com/hertz-contrib/registry/eureka"
+)
+
+func main() {
+ cli, err := client.NewClient()
+ if err != nil {
+ hlog.Fatal(err)
+ return
+ }
+ r := eureka.NewEurekaResolver([]string{"http://127.0.0.1:8761/eureka"})
+
+ cli.Use(sd.Discovery(r))
+ for i := 0; i < 10; i++ {
+ status, body, err := cli.Get(context.Background(), nil, "http://hertz.discovery.eureka/ping", config.WithSD(true))
+ if err != nil {
+ hlog.Fatal(err)
+ }
+ hlog.Infof("code=%d,body=%s", status, string(body))
+ }
+}
+```
+
+## 配置
+
+本项目使用 [fargo](https://github.com/hudl/fargo) 作为 eureka 客户端。您应该参考 [fargo](https://github.com/hudl/fargo)
+文档以了解高级配置。
+
+## 完整示例
+
+完整用法示例详见 [example](https://github.com/hertz-contrib/registry/tree/main/eureka/example) 。
+
+
+---
+title: "servicecomb"
+date: 2023-04-22
+weight: 7
+keywords: ["服务注册与发现", "servicecomb"]
+description: "Hertz 提供的服务注册与发现 servicecomb 拓展。"
+---
+
+## 安装
+
+```go
+go get github.com/hertz-contrib/registry/servicecomb
+```
+
+## 服务注册
+
+### Option
+
+Servicecomb 拓展在服务注册部分中提供了 option 配置。
+
+#### WithAppId
+
+Servicecomb 扩展提供了 `WithAppId` 用于帮助用户配置 Servicecomb 的 AppId。默认为“DEFAULT" 。
+
+函数签名:
+
+```go
+func WithAppId(appId string) RegistryOption
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ r, err := servicecomb.NewDefaultSCRegistry([]string{scAddr},
+ servicecomb.WithAppId("appID"),
+ )
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.servicecomb.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ // ...
+}
+```
+
+#### WithRegistryVersionRule
+
+Servicecomb 扩展提供了 `WithRegistryVersionRule` 用于帮助用户配置 Servicecomb 的版本要求。默认为 1.0.0。
+
+函数签名:
+
+```go
+func WithRegistryVersionRule(versionRule string) RegistryOption
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ r, err := servicecomb.NewDefaultSCRegistry([]string{scAddr},
+ servicecomb.WithRegistryVersionRule("1.1.0"),
+ )
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.servicecomb.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ // ...
+}
+```
+
+#### WithRegistryHostName
+
+Servicecomb 扩展提供了 `WithRegistryHostName` 用于帮助用户配置 Servicecomb 的主机名。默认为”DEFAULT" 。
+
+函数签名:
+
+```go
+func WithRegistryHostName(hostName string) RegistryOption
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ r, err := servicecomb.NewDefaultSCRegistry([]string{scAddr},
+ servicecomb.WithRegistryHostName("hostName"),
+ )
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.servicecomb.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ // ...
+}
+```
+
+#### WithRegistryHeartbeatInterval
+
+Servicecomb 扩展提供了 `WithRegistryHeartbeatInterval` 用于帮助用户配置发送心跳包的间隔时长。默认为 5。
+
+函数签名:
+
+```go
+func WithRegistryHeartbeatInterval(second int32) RegistryOption
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ r, err := servicecomb.NewDefaultSCRegistry([]string{scAddr},
+ servicecomb.WithRegistryHeartbeatInterval(10),
+ )
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.servicecomb.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ // ...
+}
+```
+
+### NewDefaultSCRegistry
+
+`NewDefaultSCRegistry` 使用 service-comb 创建一个默认服务注册中心,需要传入端点值。可自定义服务注册中心配置。
+
+函数签名:
+
+```go
+func NewDefaultSCRegistry(endPoints []string, opts ...RegistryOption) (registry.Registry, error)
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ r, err := servicecomb.NewDefaultSCRegistry([]string{scAddr})
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.servicecomb.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ // ...
+}
+```
+
+### NewSCRegistry
+
+`NewSCRegistry` 使用 service-comb 创建一个新的服务注册中心。需要传入自定义客户端。可自定义服务注册中心配置。
+
+函数签名:
+
+```go
+func NewSCRegistry(client *sc.Client, opts ...RegistryOption) registry.Registry
+```
+
+示例代码:
+
+```go
+func main() {
+ client := &sc.Client{
+ // ...
+ }
+ // ...
+ r, err := servicecomb.NewSCRegistry(client)
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.servicecomb.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+ // ...
+}
+```
+
+## 服务发现
+
+### Option
+
+Servicecomb 拓展在服务发现部分中提供了 option 配置。
+
+#### WithAppId
+
+Servicecomb 扩展提供了 `WithAppId` 用于帮助用户配置 Servicecomb 的 AppId。默认为“DEFAULT" 。
+
+函数签名:
+
+```go
+func WithResolverAppId(appId string) ResolverOption
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ r, err := servicecomb.NewDefaultSCRegistry([]string{scAddr},
+ servicecomb.WithAppId("appID"),
+ )
+ if err != nil {
+ panic(err)
+ }
+ cli, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+#### WithResolverVersionRule
+
+Servicecomb 扩展提供了 `WithResolverVersionRule` 用于帮助用户配置 Servicecomb 的版本要求。默认为 latest。
+
+函数签名:
+
+```go
+func WithResolverVersionRule(versionRule string) ResolverOption
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ r, err := servicecomb.NewDefaultSCRegistry([]string{scAddr},
+ servicecomb.WithResolverVersionRule("1.0.0"),
+ )
+ if err != nil {
+ panic(err)
+ }
+ cli, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+#### WithResolverConsumerId
+
+Servicecomb 扩展提供了 `WithResolverConsumerId` 用于帮助用户配置 Servicecomb 的 ConsumerId。默认为空。
+
+函数签名:
+
+```go
+func WithResolverConsumerId(consumerId string) ResolverOption
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ r, err := servicecomb.NewDefaultSCRegistry([]string{scAddr},
+ servicecomb.WithResolverConsumerId("1"),
+ )
+ if err != nil {
+ panic(err)
+ }
+ cli, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+### NewDefaultSCResolver
+
+`NewDefaultSCResolver` 使用 service-comb 创建一个默认服务发现中心,需要传入端点值。可自定义服务发现中心配置。
+
+函数签名:
+
+```go
+func NewDefaultSCResolver(endPoints []string, opts ...ResolverOption) (discovery.Resolver, error)
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ r, err := servicecomb.NewDefaultSCResolver([]string{scAddr})
+ if err != nil {
+ panic(err)
+ }
+ cli, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+### NewSCResolver
+
+`NewSCReslover` 使用 service-comb 创建一个新的服务发现中心。需要传入自定义客户端。可自定义服务发现中心配置。
+
+函数签名:
+
+```go
+func NewSCResolver(cli *sc.Client, opts ...ResolverOption) discovery.Resolver
+```
+
+示例代码:
+
+```go
+func main() {
+ client := &sc.Client{
+ // ...
+ }
+ // ...
+ r, err := servicecomb.NewSCResolver(client)
+ if err != nil {
+ panic(err)
+ }
+ cli, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+## 使用示例
+
+### 服务端
+
+```go
+import (
+ "context"
+ "log"
+ "sync"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/cloudwego/hertz/pkg/app/server/registry"
+ "github.com/cloudwego/hertz/pkg/common/utils"
+ "github.com/cloudwego/hertz/pkg/protocol/consts"
+ "github.com/hertz-contrib/registry/servicecomb"
+)
+
+func main() {
+ const scAddr = "127.0.0.1:30100"
+ const addr = "127.0.0.1:8701"
+ r, err := servicecomb.NewDefaultSCRegistry([]string{scAddr})
+ if err != nil {
+ log.Fatal(err)
+ return
+ }
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.servicecomb.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }),
+ )
+
+ h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
+ ctx.JSON(consts.StatusOK, utils.H{"ping": "pong1"})
+ })
+ h.Spin()
+}
+```
+
+### 客户端
+
+```go
+import (
+ "context"
+
+ "github.com/cloudwego/hertz/pkg/app/client"
+ "github.com/cloudwego/hertz/pkg/app/middlewares/client/sd"
+ "github.com/cloudwego/hertz/pkg/common/config"
+ "github.com/cloudwego/hertz/pkg/common/hlog"
+ "github.com/hertz-contrib/registry/servicecomb"
+)
+
+func main() {
+ const scAddr = "127.0.0.1:30100"
+ // build a servicecomb resolver
+ r, err := servicecomb.NewDefaultSCResolver([]string{scAddr})
+ if err != nil {
+ panic(err)
+ }
+ // build a hertz client with the servicecomb resolver
+ cli, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ cli.Use(sd.Discovery(r))
+ for i := 0; i < 10; i++ {
+ status, body, err := cli.Get(context.Background(), nil, "http://hertz.servicecomb.demo/ping", config.WithSD(true))
+ if err != nil {
+ hlog.Fatal(err)
+ }
+ hlog.Infof("code=%d,body=%s", status, string(body))
+ }
+}
+```
+
+## 配置
+
+可自定义 Servicecomb 客户端以及服务端的配置,参考 [go-chassis/sc-client](https://github.com/go-chassis/sc-client) 配置
+
+## 完整示例
+
+完整用法示例详见 [example](https://github.com/hertz-contrib/registry/tree/main/servicecomb/example) 。
+
+
+---
+title: "polaris"
+date: 2023-04-22
+weight: 6
+keywords: ["服务注册与发现", "polaris"]
+description: "Hertz 提供的服务注册与发现 polaris 拓展。"
+---
+
+## 安装
+
+```go
+go get github.com/hertz-contrib/registry/polaris
+```
+
+## 服务注册
+
+### NewPolarisRegistry
+
+`NewPolarisRegistry` 使用 polaris 创建一个新的服务注册中心,可传入配置文件并调用 `GetPolarisConfig`,若不传入则使用默认配置。
+
+函数签名:
+
+```go
+func NewPolarisRegistry(configFile ...string) (Registry, error)
+```
+
+示例代码:
+
+```go
+func main() {
+ r, err := polaris.NewPolarisRegistry(confPath)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ Info := ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", "127.0.0.1:8888"),
+ Tags: map[string]string{
+ "namespace": Namespace,
+ },
+ }
+ h := server.Default(server.WithRegistry(r, Info), server.WithExitWaitTime(10*time.Second))
+ // ...
+}
+```
+
+## 服务发现
+
+### NewPolarisResolver
+
+`NewPolarisResolver` 使用 polaris 创建一个新的服务发现中心,可传入配置文件并调用 `GetPolarisConfig`,若不传入则使用默认配置。
+
+函数签名:
+
+```go
+func NewPolarisResolver(configFile ...string) (Resolver, error)
+```
+
+示例代码:
+
+```go
+func main() {
+ r, err := polaris.NewPolarisResolver(confPath)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ client, err := hclient.NewClient()
+ client.Use(sd.Discovery(r))
+ //...
+}
+```
+
+## 使用示例
+
+### 服务端
+
+```go
+import (
+ "context"
+ "log"
+ "time"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/cloudwego/hertz/pkg/app/server/registry"
+ "github.com/cloudwego/hertz/pkg/common/utils"
+ "github.com/cloudwego/hertz/pkg/protocol/consts"
+ "github.com/hertz-contrib/registry/polaris"
+)
+
+const (
+ confPath = "polaris.yaml"
+ Namespace = "Polaris"
+ // At present,polaris server tag is v1.4.0,can't support auto create namespace,
+ // If you want to use a namespace other than default,Polaris ,before you register an instance,
+ // you should create the namespace at polaris console first.
+)
+
+func main() {
+ r, err := polaris.NewPolarisRegistry(confPath)
+
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ Info := ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", "127.0.0.1:8888"),
+ Tags: map[string]string{
+ "namespace": Namespace,
+ },
+ }
+ h := server.Default(server.WithRegistry(r, Info), server.WithExitWaitTime(10*time.Second))
+
+ h.GET("/hello", func(ctx context.Context, c *app.RequestContext) {
+ c.String(consts.StatusOK, "Hello,Hertz!")
+ })
+
+ h.Spin()
+}
+```
+
+### 客户端
+
+```go
+import (
+ "context"
+ "log"
+
+ hclient "github.com/cloudwego/hertz/pkg/app/client"
+ "github.com/cloudwego/hertz/pkg/app/middlewares/client/sd"
+ "github.com/cloudwego/hertz/pkg/common/config"
+ "github.com/cloudwego/hertz/pkg/common/hlog"
+ "github.com/hertz-contrib/registry/polaris"
+)
+
+const (
+ confPath = "polaris.yaml"
+ Namespace = "Polaris"
+ // At present,polaris server tag is v1.4.0,can't support auto create namespace,
+ // if you want to use a namespace other than default,Polaris ,before you register an instance,
+ // you should create the namespace at polaris console first.
+)
+
+func main() {
+ r, err := polaris.NewPolarisResolver(confPath)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ client, err := hclient.NewClient()
+ client.Use(sd.Discovery(r))
+
+ for i := 0; i < 10; i++ {
+ // config.WithTag sets the namespace tag for service discovery
+ status, body, err := client.Get(context.TODO(), nil, "http://hertz.test.demo/hello", config.WithSD(true), config.WithTag("namespace", Namespace))
+ if err != nil {
+ hlog.Fatal(err)
+ }
+ hlog.Infof("code=%d,body=%s\n", status, body)
+ }
+}
+```
+
+## 配置
+
+可自定义 polaris
+客户端以及服务端的配置,参考 [polaris-go](https://pkg.go.dev/github.com/polarismesh/polaris-go/api#section-readme) 配置。
+
+## 完整示例
+
+完整用法示例详见 [example](https://github.com/hertz-contrib/registry/tree/main/polaris/example) 。
+
+
+---
+title: "服务注册与发现"
+date: 2023-04-22
+weight: 1
+keywords: ["服务注册与发现", "nacos", "consul", "etcd", "eureka", "polaris", "servicecomb", "zookeeper", "redis"]
+description: "Hertz 提供的服务注册与发现拓展。"
+---
+
+目前在 Hertz 的开源版本支持的服务发现拓展都存放在 [registry](https://github.com/hertz-contrib/registry) 中,欢迎大家参与项目贡献与维护。
+
+到现在为止,支持的服务发现拓展有
+
+- [nacos](https://github.com/hertz-contrib/registry/tree/main/nacos)
+- [consul](https://github.com/hertz-contrib/registry/tree/main/consul)
+- [etcd](https://github.com/hertz-contrib/registry/tree/main/etcd)
+- [eureka](https://github.com/hertz-contrib/registry/tree/main/eureka)
+- [polaris](https://github.com/hertz-contrib/registry/tree/main/polaris)
+- [servicecomb](https://github.com/hertz-contrib/registry/tree/main/servicecomb)
+- [zookeeper](https://github.com/hertz-contrib/registry/tree/main/zookeeper)
+- [redis](https://github.com/hertz-contrib/registry/tree/main/redis)
+
+## 配置
+
+使用服务发现时会提供一些可选配置给用户。
+
+| 配置 | 描述 |
+|------------------------|----------------------------------|
+| WithSD | 配合服务发现使用,传递 `true` 时,本次请求使用服务发现。 |
+| WithTag | 配合服务发现使用,设置 Tag 信息。 |
+| WithCustomizedAddrs | 自定义目标实例地址。 |
+| WithLoadBalanceOptions | 配置负载均衡选项。 |
+
+### WithSD
+
+提供 `WithSD` 配置项,传入参数为 true 时,本次请求使用服务发现。使用服务发现请求时必须使用 `WithSD` 配置项。
+
+函数签名:
+
+```go
+func WithSD(b bool) RequestOption
+```
+
+示例代码:
+
+```go
+status, body, err := cli.Get(context.Background(), nil, "http://hertz.test.demo/ping", config.WithSD(true))
+```
+
+### WithTag
+
+提供 `WithTag` 配置项,使用此配置用于设置 Tag 信息。
+
+函数签名:
+
+```go
+func WithTag(k, v string) RequestOption
+```
+
+示例代码:
+
+```go
+status, body, err := cli.Get(context.Background(), nil, "http://hertz.test.demo/ping", config.WithTag("foo", "var"))
+```
+
+### WithCustomizedAddrs
+
+`WithCustomizedAddrs`配置项指定服务发现时的目标实例地址。它将会覆盖来自 `Resolver` 的结果。`Resolver` 是服务发现中心,用于服务发现。
+
+函数签名:
+
+```go
+func WithCustomizedAddrs(addrs ...string) ServiceDiscoveryOption
+```
+
+示例代码:
+
+```go
+cli.Use(sd.Discovery(r, sd.WithCustomizedAddrs("127.0.0.1:8088")))
+```
+
+### WithLoadBalanceOptions
+
+`WithLoadBalanceOptions`为客户端配置负载均衡实现和负载均衡参数。可以通过传递`loadbalance.Options`
+配置负载均衡参数,或者通过传递`loadbalance.DefaultOpts`使用默认负载均衡参数。若不使用此配置项,则客户端默认使用
+WeightedRandom 负载均衡实现并且使用默认负载均衡参数。
+
+可以设置的负载均衡参数:
+
+| 负载均衡参数名 | 负载均衡参数默认值 | 描述 |
+|-----------------|-----------|-----------|
+| RefreshInterval | 5秒 | 刷新服务端信息间隔 |
+| ExpireInterval | 15秒 | 服务端信息过期间隔 |
+
+函数签名:
+
+```go
+func WithLoadBalanceOptions(lb loadbalance.Loadbalancer, options loadbalance.Options) ServiceDiscoveryOption
+```
+
+示例代码:
+
+```go
+cli.Use(sd.Discovery(r, sd.WithLoadBalanceOptions(loadbalance.NewWeightedBalancer(), loadbalance.Options{
+ RefreshInterval: 5 * time.Second,
+ ExpireInterval: 15 * time.Second,
+})))
+```
+
+自定义负载均衡扩展详见[负载均衡扩展](https://www.cloudwego.io/zh/docs/hertz/tutorials/framework-exten/service_discovery/#负载均衡扩展)。
+
+
+---
+title: "zookeeper"
+date: 2023-04-22
+weight: 8
+keywords: ["服务注册与发现", "zookeeper"]
+description: "Hertz 提供的服务注册与发现 zookeeper 拓展。"
+---
+
+## 安装
+
+```go
+go get github.com/hertz-contrib/registry/zookeeper
+```
+
+## 服务注册
+
+### NewZookeeperRegistry
+
+`NewZookeeperRegistry` 使用 zookeeper 创建一个服务注册中心,需要将服务通过一个字符串切片与会话超时时间共同传入 `Connect`。
+
+函数签名:
+
+```go
+func NewZookeeperRegistry(servers []string, sessionTimeout time.Duration) (registry.Registry, error)
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ r, err := zookeeper.NewZookeeperRegistry([]string{"127.0.0.1:2181"}, 40*time.Second)
+ if err != nil {
+ panic(err)
+ }
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }))
+ // ...
+}
+```
+
+### NewZookeeperRegistryWithAuth
+
+`NewZookeeperRegistryWithAuth` 使用 zookeeper
+创建一个服务注册中心,需要将服务通过一个字符串切片与会话超时时间共同传入 `Connect`
+。除此之外还需要传入用户与密码来调用 `AddAuth`,用户与密码不能为空。
+
+函数签名:
+
+```go
+func NewZookeeperRegistryWithAuth(servers []string, sessionTimeout time.Duration, user, password string)
+```
+
+示例代码:
+
+```go
+func main() {
+ // ...
+ r, err := zookeeper.NewZookeeperRegistryWithAuth([]string{"127.0.0.1:2181"}, 20*time.Second, "hertzuser", "hertzpass")
+ if err != nil {
+ panic(err)
+ }
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }))
+ // ...
+}
+```
+
+## 服务发现
+
+### NewZookeeperResolver
+
+`NewZookeeperResolver` 使用 zookeeper 创建一个服务发现中心,需要将服务通过一个字符串切片与会话超时时间共同传入 `Connect`。
+
+函数签名:
+
+```go
+func NewZookeeperResolver(servers []string, sessionTimeout time.Duration) (discovery.Resolver, error)
+```
+
+示例代码:
+
+```go
+func main() {
+ cli, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ r, err := zookeeper.NewZookeeperResolver([]string{"127.0.0.1:2181"}, 40*time.Second)
+ if err != nil {
+ panic(err)
+ }
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+### NewZookeeperResolverWithAuth
+
+`NewZookeeperResolverWithAuth` 使用 zookeeper
+创建一个服务发现中心,需要将服务通过一个字符串切片与会话超时时间共同传入 `Connect`
+。除此之外还需要传入用户与密码来调用 `AddAuth`,用户与密码不能为空。
+
+函数签名:
+
+```go
+func NewZookeeperResolverWithAuth(servers []string, sessionTimeout time.Duration, user, password string)
+```
+
+示例代码:
+
+```go
+func main() {
+ cli, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ r, err := zookeeper.NewZookeeperResolverWithAuth([]string{"127.0.0.1:2181"}, 40*time.Second, "hertzuser", "hertzpass")
+ if err != nil {
+ panic(err)
+ }
+ cli.Use(sd.Discovery(r))
+ // ...
+}
+```
+
+## 使用示例
+
+### 服务端
+
+```go
+import (
+ "context"
+ "time"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/cloudwego/hertz/pkg/app/server/registry"
+ "github.com/cloudwego/hertz/pkg/common/utils"
+ "github.com/cloudwego/hertz/pkg/protocol/consts"
+ "github.com/hertz-contrib/registry/zookeeper"
+)
+
+func main() {
+ addr := "127.0.0.1:8888"
+ r, err := zookeeper.NewZookeeperRegistry([]string{"127.0.0.1:2181"}, 40*time.Second)
+ if err != nil {
+ panic(err)
+ }
+ h := server.Default(
+ server.WithHostPorts(addr),
+ server.WithRegistry(r, ®istry.Info{
+ ServiceName: "hertz.test.demo",
+ Addr: utils.NewNetAddr("tcp", addr),
+ Weight: 10,
+ Tags: nil,
+ }))
+ h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
+ ctx.JSON(consts.StatusOK, utils.H{"ping": "pong2"})
+ })
+ h.Spin()
+}
+```
+
+### 客户端
+
+```go
+import (
+ "context"
+ "time"
+
+ "github.com/cloudwego/hertz/pkg/app/client"
+ "github.com/cloudwego/hertz/pkg/app/middlewares/client/sd"
+ "github.com/cloudwego/hertz/pkg/common/config"
+ "github.com/cloudwego/hertz/pkg/common/hlog"
+ "github.com/hertz-contrib/registry/zookeeper"
+)
+
+func main() {
+ cli, err := client.NewClient()
+ if err != nil {
+ panic(err)
+ }
+ r, err := zookeeper.NewZookeeperResolver([]string{"127.0.0.1:2181"}, 40*time.Second)
+ if err != nil {
+ panic(err)
+ }
+ cli.Use(sd.Discovery(r))
+ for i := 0; i < 10; i++ {
+ status, body, err := cli.Get(context.Background(), nil, "http://hertz.test.demo/ping", config.WithSD(true))
+ if err != nil {
+ hlog.Fatal(err)
+ }
+ hlog.Infof("code=%d,body=%s", status, string(body))
+ }
+}
+```
+
+## 配置
+
+可自定义 zookeeper 客户端以及服务端的配置,参考 [go-zookeeper/zk](https://github.com/go-zookeeper/zk) 配置。
+
+## 完整示例
+
+完整用法示例详见 [example](https://github.com/hertz-contrib/registry/tree/main/zookeeper/example) 。
+
+
+---
+title: "绑定与校验"
+date: 2022-05-23
+weight: 8
+keywords: ["绑定与校验", "go-tagexpr", "tag", "参数绑定优先级"]
+description: "Hertz 支持的参数绑定与校验相关功能及用法。"
+
+---
+
+hertz 使用开源库 [go-tagexpr](https://github.com/bytedance/go-tagexpr) 进行参数的绑定及验证,下面分别介绍参数绑定和参数验证的用法。
+
+## 使用方法
+
+```go
+func main() {
+ r := server.New()
+
+ r.GET("/hello", func(c context.Context, ctx *app.RequestContext) {
+ // 参数绑定需要配合特定的 go tag 使用
+ type Test struct {
+ A string `query:"a" vd:"$!='Hertz'"`
+ }
+
+ // BindAndValidate
+ var req Test
+ err := ctx.BindAndValidate(&req)
+
+ ...
+
+ // Bind
+ req = Test{}
+ err = ctx.Bind(&req)
+
+ ...
+
+ // Validate,需要使用 "vd" tag
+ err = ctx.Validate(&req)
+
+ ...
+ })
+...
+}
+```
+
+## 支持的 tag 及参数绑定优先级
+
+### 支持的 tag
+
+不通过 IDL 生成代码时若字段不添加任何 tag 则会遍历所有 tag 并按照优先级绑定参数,添加 tag 则会根据对应的 tag 按照优先级去绑定参数。
+
+通过 IDL 生成代码时若不添加 [api注解](/zh/docs/hertz/tutorials/toolkit/annotation/#支持的-api-注解)
+则字段默认添加 `form`、`json`、`query` tag,添加 [api注解](/zh/docs/hertz/tutorials/toolkit/annotation/#支持的-api-注解)
+会为字段添加相应需求的 tag。
+
+| go tag | 说明 |
+|:---------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| path | 绑定 url 上的路径参数,相当于 hertz 路由{:param}或{*param}中拿到的参数。例如:如果定义的路由为:/v:version/example,可以把 path 的参数指定为路由参数:`path:"version"`,此时,url: http://127.0.0.1:8888/v1/example,可以绑定path参数"1" |
+| form | 绑定请求的 body 内容。content-type -> `multipart/form-data` 或 `application/x-www-form-urlencoded`,绑定 form 的 key-value |
+| query | 绑定请求的 query 参数 |
+| cookie | 绑定请求的 cookie 参数 |
+| header | 绑定请求的 header 参数 |
+| json | 绑定请求的 body 内容 content-type -> `application/json`,绑定 json 参数 |
+| raw_body | 绑定请求的原始 body(bytes),绑定的字段名不指定,也能绑定参数。(注:raw_body 绑定优先级最低,当指定多个 tag 时,一旦其他 tag 成功绑定参数,则不会绑定 body 内容。) |
+| vd | 参数校验,[校验语法](https://github.com/bytedance/go-tagexpr/tree/master/validator) |
+| default | 设置默认值 |
+
+### 参数绑定优先级
+
+```text
+path > form > query > cookie > header > json > raw_body
+```
+
+> 注:如果请求的 content-type 为 `application/json`,那么会在参数绑定前做一次 json unmarshal 处理作为兜底。
+
+### 必传参数
+
+通过在 tag 中添加 `required`,可以将参数标记为必传。当绑定失败时 `Bind` 和 `BindAndValidate` 将会返回错误。当多个 tag
+包含 `required` 时,将会按照优先级绑定。如果所有 tag 都没有绑定上,则会返回错误。
+
+``` go
+type TagRequiredReq struct {
+ // 当 JSON 中没有 hertz 字段时,会返回 required 错误:binding: expr_path=hertz, cause=missing required parameter
+ Hertz string `json:"hertz,required"`
+ // 当 query 和 JSON 中同时没有 kitex 字段时,会返回 required 错误:binding: expr_path=hertz, cause=missing required parameter"
+ Kitex string `query:"kitex,required" json:"kitex,required" `
+}
+```
+
+## 常见用法
+
+### 自定义 bind 和 validate 的 Error
+
+绑定参数发生错误和参数校验失败的时候,用户可以自定义的
+Error([demo](https://github.com/cloudwego/hertz-examples/tree/main/binding/custom_error) ),使用方法如下:
+
+```go
+import "github.com/cloudwego/hertz/pkg/app/server/binding"
+
+type BindError struct {
+ ErrType, FailField, Msg string
+}
+
+// Error implements error interface.
+func (e *BindError) Error() string {
+ if e.Msg != "" {
+ return e.ErrType + ": expr_path=" + e.FailField + ", cause=" + e.Msg
+ }
+ return e.ErrType + ": expr_path=" + e.FailField + ", cause=invalid"
+}
+
+type ValidateError struct {
+ ErrType, FailField, Msg string
+}
+
+// Error implements error interface.
+func (e *ValidateError) Error() string {
+ if e.Msg != "" {
+ return e.ErrType + ": expr_path=" + e.FailField + ", cause=" + e.Msg
+ }
+ return e.ErrType + ": expr_path=" + e.FailField + ", cause=invalid"
+}
+
+func init() {
+ CustomBindErrFunc := func(failField, msg string) error {
+ err := BindError{
+ ErrType: "bindErr",
+ FailField: "[bindFailField]: " + failField,
+ Msg: "[bindErrMsg]: " + msg,
+ }
+
+ return &err
+ }
+
+ CustomValidateErrFunc := func(failField, msg string) error {
+ err := ValidateError{
+ ErrType: "validateErr",
+ FailField: "[validateFailField]: " + failField,
+ Msg: "[validateErrMsg]: " + msg,
+ }
+
+ return &err
+ }
+
+ binding.SetErrorFactory(CustomBindErrFunc, CustomValidateErrFunc)
+}
+```
+
+### 自定义类型解析
+
+在参数绑定的时候,所有的 request 参数都是 `string` 或者 `[]string`;当有一些 field
+的类型为非基础类型或者无法直接通过 `string`
+转换,则可以自定义类型解析([demo](https://github.com/cloudwego/hertz-examples/tree/main/binding/custom_type_resolve)
+)。使用方法如下:
+
+```go
+import "github.com/cloudwego/hertz/pkg/app/server/binding"
+
+type Nested struct {
+ B string
+ C string
+}
+
+type TestBind struct {
+ A Nested `query:"a,required"`
+}
+
+func init() {
+ binding.MustRegTypeUnmarshal(reflect.TypeOf(Nested{}), func(v string, emptyAsZero bool) (reflect.Value, error) {
+ if v == "" && emptyAsZero {
+ return reflect.ValueOf(Nested{}), nil
+ }
+ val := Nested{
+ B: v[:5],
+ C: v[5:],
+ }
+ return reflect.ValueOf(val), nil
+ })
+}
+```
+
+### 自定义验证函数
+
+可以通过注册自定义验证函数,在'vd'
+注解中实现复杂的验证逻辑([demo](https://github.com/cloudwego/hertz-examples/tree/main/binding/custom_validate_func)
+),使用方法如下:
+
+```go
+import "github.com/cloudwego/hertz/pkg/app/server/binding"
+
+func init() {
+ binding.MustRegValidateFunc("test", func(args ...interface{}) error {
+ if len(args) != 1 {
+ return fmt.Errorf("the args must be one")
+ }
+ s, _ := args[0].(string)
+ if s == "123" {
+ return fmt.Errorf("the args can not be 123")
+ }
+ return nil
+ })
+}
+```
+
+### 配置 looseZero
+
+在一些场景下,前端有时候传来的信息只有 key 没有
+value,这会导致绑定数值类型的时候,会报错 `cause=parameter type does not match binding data`。
+这时需要配置 looseZero 模式([demo](https://github.com/cloudwego/hertz-examples/tree/main/binding/loose_zero) ),使用方法如下:
+
+```go
+import "github.com/cloudwego/hertz/pkg/app/server/binding"
+
+func init() {
+ // 默认 false,全局生效
+ binding.SetLooseZeroMode(true)
+}
+```
+
+### 配置其他 json unmarshal 库
+
+在绑定参数的时候,如果请求体为 json,会进行一次 json 的 unmarshal,如果用户需要使用特定的 json 库可以自己配置(hertz 默认使用开源
+json 库 [sonic](https://github.com/bytedance/sonic) )。使用方法如下:
+
+```go
+import "github.com/cloudwego/hertz/pkg/app/server/binding"
+
+func init() {
+ // 使用标准库
+ binding.UseStdJSONUnmarshaler()
+
+ // 使用 gjson
+ binding.UseGJSONUnmarshaler()
+
+ // 使用第三方 json unmarshal 方法
+ binding.UseThirdPartyJSONUnmarshaler()
+}
+```
+
+### 设置默认值
+
+参数支持 "default" tag 进行默认值的配置,使用方法如下:
+
+```go
+// 生成的代码
+type UserInfoResponse struct {
+ NickName string `default:"Hertz" json:"NickName" query:"nickname"`
+}
+```
+
+### 绑定文件
+
+参数绑定支持绑定文件,使用方法如下:
+
+```go
+// 需要请求的 content-type 为:multipart/form-data
+type FileParas struct {
+ F *multipart.FileHeader `form:"F1"`
+}
+
+h.POST("/upload", func(ctx context.Context, c *app.RequestContext) {
+ var req FileParas
+ err := binding.BindAndValidate(c, &req)
+})
+```
+
+## 常见问题分析
+
+**1. string 转 int 报错:json: cannot unmarshal string into Go struct field xxx of type intxx**
+
+原因:默认不支持 `string` 和 `int` 互转
+
+解决方法:
+
+- 建议使用标准包 json 的 `string` tag, 例如:
+
+ ```go
+ A int `json:"A, string"`
+ ```
+
+- 配置其他支持这种行为的 json 库
+
+
+---
+title: "重试"
+date: 2022-10-01
+weight: 13
+keywords: ["重试", "Client"]
+description: "Hertz 为用户提供的自定义重试逻辑。"
+
+---
+
+Hertz 为用户提供了自定义的重试逻辑,下面来看一下 Client 的 Retry 使用方法。**注意:Hertz 版本 >= v0.4.0**
+
+## Retry 次数及延迟策略配置
+
+首先创建 Client,使用配置项 `WithRetryConfig()` 来配置 Retry 相关逻辑(这一部分主要配置 Retry 的次数和延时部分)
+
+```go
+package main
+
+import (
+ "github.com/cloudwego/hertz/pkg/app/client"
+ "github.com/cloudwego/hertz/pkg/app/client/retry"
+)
+
+func main() {
+ cli, err := client.NewClient(
+ client.WithRetryConfig(
+ retry.WithXxx(), // 设置 Retry 配置的方式
+ ),
+ )
+}
+```
+
+| 配置名称 | 类型 | 介绍 |
+|:--------------------|:---------------------------------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| WithMaxAttemptTimes | uint | 用于设置最大尝试次数,默认 1 次(即只请求 1 次不重试) |
+| WithInitDelay | time.Duration | 用于设置初始延迟时间,默认 1ms |
+| WithMaxDelay | time.Duration | 用于设置最大延迟时间,默认 100ms |
+| WithMaxJitter | time.Duration | 用于设置最大扰动时间,需要配合 RandomDelayPolicy 使用,会生成不超过最大扰动时间的随机时间,默认 20ms |
+| WithDelayPolicy | type DelayPolicyFunc func(attempts uint, err error, retryConfig *Config) time.Duration | 用于设置延迟策略,可以使用以下四种的任意结合,FixedDelayPolicy, BackOffDelayPolicy, RandomDelayPolicy, DefaultDelayPolicy([详情见下一小节:**延迟策略**](#延迟策略))默认使用 DefaultDelayPolicy(即重试延迟为 0) |
+
+### 延迟策略
+
+`retry.WithDelayPolicy()` 使用方法
+
+```go
+cli, err := client.NewClient(
+ client.WithRetryConfig(
+ ...
+ retry.WithDelayPolicy(retry.CombineDelay(retry.FixedDelayPolicy, retry.BackOffDelayPolicy, retry.RandomDelayPolicy)),
+ ...
+ ),
+ )
+```
+
+| 函数名称 | 说明 |
+|:-------------------|:---------------------------------------------------------------------------------------------------------|
+| CombineDelay | 用于将下面四种策略进行任意组合,将所选策略计算出的值进行加和。当你只需要下面四种策略中的一种时,你可以选择使用 CombineDelay 或选择直接将任意一种策略传入 WithDelayPolicy 作为参数 |
+| FixedDelayPolicy | 用于设置固定延迟时间,使用 WithInitDelay 设置的值,来生成等值的延迟时间 |
+| BackOffDelayPolicy | 用于设置指数级延迟时间,使用 WithInitDelay 设置的值,根据当前是第几次重试,指数级生成延迟时间 |
+| RandomDelayPolicy | 用于设置随机延迟时间,使用 WithMaxJitter 设置的值,生成不超过该值的随机延迟时间 |
+| DefaultDelayPolicy | 用于设置默认延迟时间,返回 0,一般单独使用,和其他策略结合没有效果 |
+
+### 完整示例
+
+```Go
+package main
+
+import (
+ "github.com/cloudwego/hertz/pkg/app/client"
+ "github.com/cloudwego/hertz/pkg/app/client/retry"
+)
+func main() {
+
+ cli, err := client.NewClient(
+ client.WithRetryConfig(
+ retry.WithMaxAttemptTimes(3), // 最大的尝试次数,包括初始调用
+ retry.WithInitDelay(1*time.Millisecond), // 初始延迟
+ retry.WithMaxDelay(6*time.Millisecond), // 最大延迟,不管重试多少次,策略如何,都不会超过这个延迟
+ retry.WithMaxJitter(2*time.Millisecond), // 延时的最大扰动,结合 RandomDelayPolicy 才会有效果
+ /*
+ 配置延迟策略,你可以选择下面四种中的任意组合,最后的结果为每种延迟策略的加和
+ FixedDelayPolicy 使用 retry.WithInitDelay 所设置的值,
+ BackOffDelayPolicy 在 retry.WithInitDelay 所设置的值的基础上随着重试次数的增加,指数倍数增长,
+ RandomDelayPolicy 生成 [0,2*time.Millisecond)的随机数值,2*time.Millisecond 为 retry.WithMaxJitter 所设置的值,
+ DefaultDelayPolicy 生成 0 值,如果单独使用则立刻重试,
+ retry.CombineDelay() 将所设置的延迟策略所生成的值加和,最后结果即为当前次重试的延迟时间,
+ 第一次调用失败 -> 重试延迟:1 + 1<<1 + rand[0,2)ms -> 第二次调用失败 -> 重试延迟:min(1 + 1<<2 + rand[0,2) , 6)ms -> 第三次调用成功/失败
+ */
+ retry.WithDelayPolicy(retry.CombineDelay(retry.FixedDelayPolicy, retry.BackOffDelayPolicy, retry.RandomDelayPolicy)),
+ ),
+ )
+}
+```
+
+## Retry 条件配置
+
+如果你想要自定义配置重试发生的条件,你可以使用 `client.SetRetryIfFunc()` 配置,该函数的参数是一个函数,签名为:
+
+```go
+func(req *protocol.Request, resp *protocol.Response, err error) bool
+```
+
+相关参数包括 Hertz 请求中的 `req`、`resp` 和 `err` 字段,你可以通过这些参数,判断这个请求该不该重试。在如下例子中,当请求返回的状态码不是
+200 或者调用过程中 `err != nil` 时我们返回 true,即进行重试。
+
+```Go
+cli.SetRetryIfFunc(func(req *protocol.Request, resp *protocol.Response, err error) bool {
+ return resp.StatusCode() != 200 || err != nil
+})
+```
+
+需要注意的是,如果你没有设置 `client.SetRetryIfFunc()`。我们将会按照 Hertz
+默认的重试发生条件进行判断,即判断请求是否满足下面的 `DefaultRetryIf()`
+函数并且判断该调用是否是幂等调用(幂等调用:即 [pkg/protocol/http1/client.go::Do()](https://github.com/cloudwego/hertz/blob/develop/pkg/protocol/http1/client.go#L328 )
+和 [pkg/protocol/http1/client.go::doNonNilReqResp()](https://github.com/cloudwego/hertz/blob/develop/pkg/protocol/http1/client.go#L411)
+中 `canIdempotentRetry` 为 true 的 [情况](#table1))
+
+```Go
+// DefaultRetryIf Default retry condition, mainly used for idempotent requests.
+// If this cannot be satisfied, you can implement your own retry condition.
+func DefaultRetryIf(req *protocol.Request, resp *protocol.Response, err error) bool {
+ // cannot retry if the request body is not rewindable
+ if req.IsBodyStream() {
+ return false
+ }
+
+ if isIdempotent(req, resp, err) {
+ return true
+ }
+ // Retry non-idempotent requests if the server closes
+ // the connection before sending the response.
+ //
+ // This case is possible if the server closes the idle
+ // keep-alive connection on timeout.
+ //
+ // Apache and nginx usually do this.
+ if err == io.EOF {
+ return true
+ }
+
+ return false
+}
+func isIdempotent(req *protocol.Request, resp *protocol.Response, err error) bool {
+ return req.Header.IsGet() ||
+ req.Header.IsHead() ||
+ req.Header.IsPut() ||
+ req.Header.IsDelete() ||
+ req.Header.IsOptions() ||
+ req.Header.IsTrace()
+}
+```
+
+Table - 1 Hertz
+源码 [doNonNilReqResp()](https://github.com/cloudwego/hertz/blob/develop/pkg/protocol/http1/client.go#L411)
+中 `canIdempotentRetry` 为 true 的情况
+
+| doNonNilReqResp() 返回 true 的情况 |
+|-------------------------------------------------------------------------------------------------------------|
+| err = conn.SetWriteDeadline(currentTime.Add(c.WriteTimeout)) |
+| err = reqI.Write(req, zw) |
+| err = reqI.ProxyWrite(req, zw) |
+| err = zw.Flush() |
+| err = conn.SetReadTimeout(c.ReadTimeout) |
+| ( err = respI.ReadHeaderAndLimitBody() \|\| err = respI.ReadBodyStream() ) && (err != errs.ErrBodyTooLarge) |
+
+
+---
+title: "适配器"
+date: 2023-01-11
+weight: 16
+keywords: ["适配器", "http.Request", "http.ResponseWriter", "net/http"]
+description: "Hertz 提供获取 Go 标准库的 `http.Request` 和 `http.ResponseWriter` 的方式及其相关方法。"
+
+---
+
+Hertz 提供了获取 Go 标准库的 `http.Request` 和 `http.ResponseWriter` 的方式及其相关方法,以便于用户集成 `net/http` 进行开发。
+
+注意:这种适配性是以性能损耗为代价的。
+
+## 示例代码
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/cloudwego/hertz/pkg/common/adaptor"
+)
+
+func handler(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(200)
+
+ _, err := w.Write([]byte("Hello World"))
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+}
+
+func main() {
+ h := server.Default()
+
+ h.GET("/hello", func(ctx context.Context, c *app.RequestContext) {
+ req, err := adaptor.GetCompatRequest(&c.Request)
+ if err != nil {
+ fmt.Println(err)
+ return
+ }
+ // You may build more logic on req
+ fmt.Println(req.URL.String())
+
+ // caution: don't pass in c.GetResponse() as it return a copy of response
+ rw := adaptor.GetCompatResponseWriter(&c.Response)
+
+ handler(rw, req)
+ })
+
+ h.Spin()
+}
+```
+
+## http.Request
+
+| 函数 | 函数签名 | 介绍 |
+|--------------------|-----------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------------------------------------|
+| GetCompatRequest | `func GetCompatRequest(req *protocol.Request) (*http.Request, error)` | 通过 Hertz `protocol.Request` 构建并获取 Go 标准库 `http.Request` |
+| CopyToHertzRequest | `func CopyToHertzRequest(req *http.Request, hreq *protocol.Request)` | 拷贝 Go 标准库 `http.Request` 的 `URI`,`Host`,`Method`,`Protocol`,`Header` 到 Hertz `protocol.Request`,对于 `Body` 属性会以共享 `Reader` 的方式进行适配 |
+
+## http.ResponseWriter
+
+| 函数 / 结构体 | 函数签名 | 介绍 |
+|-------------------------|-----------------------------------------------------------------------------|-------------------------------------------------------------------------------------------|
+| GetCompatResponseWriter | `func GetCompatResponseWriter(resp *protocol.Response) http.ResponseWriter` | 通过 Hertz `protocol.Response` 构建并获取 Go 标准库 `http.ResponseWriter` |
+| compatResponse | / | `compatResponse` 结构体实现了 `http.ResponseWriter` 接口并对 `Header`,`Write`,`WriteHeader` 函数进行了适配 |
+
+## Handler
+
+Hertz 的 pprof 中间件提供了 Go 标准库 `http.Handler` 和 `http.HandlerFunc` 的适配方法,以便用户适配为
+Hertz `app.HandlerFunc` 进行开发。
+
+| 函数 | 函数签名 | 介绍 |
+|-------------------------|--------------------------------------------------------------------|-----------------------------------------------------------|
+| NewHertzHTTPHandlerFunc | `func NewHertzHTTPHandlerFunc(h http.HandlerFunc) app.HandlerFunc` | 用于将 Go 标准库 `http.HandlerFunc` 转换为 Hertz `app.HandlerFunc` |
+| NewHertzHTTPHandler | `func NewHertzHTTPHandler(h http.Handler) app.HandlerFunc` | 用于将 Go 标准库 `http.Handler` 转换为 Hertz `app.HandlerFunc` |
+
+参考 [hertz-example](https://github.com/cloudwego/hertz-examples/tree/main/adaptor)
+和 [pprof](https://github.com/hertz-contrib/pprof/tree/main/adaptor) 以获取更多示例
+
+
+---
+title: "Engine"
+date: 2023-08-18
+weight: 1
+description: >
+---
+
+
+`server.Hertz` 是 `Hertz` 的核心类型,它由 `route.Engine` 以及 `signalWaiter` 组成,`Hertz`
+服务器的启动、路由注册、中间件注册以及退出等重要方法均包含在 `server.Hertz` 中。以下是 `server.Hertz` 的定义:
+
+```go
+type Hertz struct {
+ *route.Engine
+ // 用于接收信号以实现优雅退出
+ signalWaiter func (err chan error) error
+}
+```
+
+`route.Engine` 为 `server.Hertz` 的重要组成部分,`Engine`
+的定义位于 [Engine](https://github.com/cloudwego/hertz/blob/main/pkg/route/engine.go)。
+
+## 配置
+
+| 配置项 | 默认值 | 说明 |
+|:---------------------------------|:---------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| 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](/zh/docs/hertz/tutorials/basic-feature/protocol/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 连接后被调用 |
+
+Server Connection 数量限制:
+
+* 如果是使用标准网络库,无此限制
+* 如果是使用 netpoll,最大连接数为 10000
+ (这个是 netpoll
+ 底层使用的 [gopool](https://github.com/bytedance/gopkg/blob/b9c1c36b51a6837cef4c2223e11522e3a647460c/util/gopool/gopool.go#L46)
+ )控制的,修改方式也很简单,调用 gopool 提供的函数即可:`gopool.SetCap(xxx)`(main.go 中调用一次即可)。
+
+Server 侧的配置项均在初始化 Server 时采用 `server.WithXXX` 的方式,如:
+
+```go
+func main() {
+ h := server.New(server.WithXXXX())
+ ...
+}
+```
+
+## 初始化服务
+
+```go
+func Default(opts ...config.Option) *Hertz
+func New(opts ...config.Option) *Hertz
+```
+
+### Default
+
+`Default` 用于初始化服务,默认使用了 `Recovery` 中间件以保证服务在运行时不会因为 `panic` 导致服务崩溃。
+
+函数签名:
+
+```go
+func Default(opts ...config.Option) *Hertz
+```
+
+示例代码:
+
+```go
+func main() {
+ h := server.Default()
+ h.Spin()
+}
+```
+
+### New
+
+`New` 用于初始化服务,没有使用默认的 `Recovery` 中间件。
+
+函数签名:
+
+```go
+func New(opts ...config.Option) *Hertz
+```
+
+示例代码:
+
+```go
+func main() {
+ h := server.New()
+ h.Spin()
+}
+```
+
+## 服务运行与退出
+
+```go
+func (h *Hertz) Spin()
+func (engine *Engine) Run() (err error)
+func (h *Hertz) SetCustomSignalWaiter(f func(err chan error) error)
+```
+
+### Spin
+
+`Spin` 函数用于运行 Hertz 服务器,接收到退出信号后可退出服务。
+
+该函数支持服务的优雅退出,优雅退出的详细内容请看 [优雅退出](https://www.cloudwego.io/zh/docs/hertz/tutorials/basic-feature/graceful-shutdown/)。
+
+在使用 [服务注册发现](https://www.cloudwego.io/zh/docs/hertz/tutorials/service-governance/service_discovery/)
+的功能时,`Spin` 会在服务启动时将服务注册进入注册中心,并使用 `signalWaiter` 监测服务异常。
+
+函数签名:
+
+```go
+func (h *Hertz) Spin()
+```
+
+示例代码:
+
+```go
+func main() {
+ h := server.Default()
+ h.Spin()
+}
+```
+
+### Run
+
+`Run` 函数用于运行 Hertz 服务器,接收到退出信号后可退出服务。
+
+该函数不支持服务的优雅退出,除非有**特殊**需求,不然一般使用 [Spin](#spin) 函数用于运行服务。
+
+函数签名:
+
+```go
+func (engine *Engine) Run() (err error)
+```
+
+示例代码:
+
+```go
+func main() {
+ h := server.Default()
+ if err := h.Run(); err != nil {
+ // ...
+ panic(err)
+ }
+}
+```
+
+### SetCustomSignalWaiter
+
+`SetCustomSignalWaiter` 函数用于自定义服务器接收信号后的处理函数,若没有设置自定义函数,Hertz 使用 `waitSignal`
+函数作为信号处理的默认实现方式,详细内容请看[优雅退出](https://www.cloudwego.io/zh/docs/hertz/tutorials/basic-feature/graceful-shutdown/)。
+
+函数签名:
+
+```go
+func (h *Hertz) SetCustomSignalWaiter(f func(err chan error) error)
+```
+
+示例代码:
+
+```go
+func main() {
+ h := server.New()
+ h.SetCustomSignalWaiter(func(err chan error) error {
+ return nil
+ })
+ h.Spin()
+}
+```
+
+## 中间件
+
+```go
+func (engine *Engine) Use(middleware ...app.HandlerFunc) IRoutes
+```
+
+### Use
+
+`Use` 函数用于将中间件注册进入路由。
+
+Hertz 支持用户自定义中间件,Hertz 已经实现了一些常用的中间件,详情见 [hertz-contrib](https://github.com/hertz-contrib)。
+
+Hertz 支持的中间件的使用方法包括**全局注册**、**路由组**级别和**单一路由**
+级别的注册,详情见 [服务端中间件](/zh/docs/hertz/tutorials/basic-feature/middleware/#服务端中间件)。
+
+`Use` 函数中 `middleware` 的形参必须为 `app.HandlerFunc` 的 http 处理函数:
+
+```go
+type HandlerFunc func (ctx context.Context, c *app.RequestContext)
+```
+
+函数签名:
+
+```go
+func (engine *Engine) Use(middleware ...app.HandlerFunc) IRoutes
+```
+
+示例代码:
+
+```go
+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 支持 Server 的流式处理,包括流式读和流式写。
+
+> 注意:由于 netpoll 和 go net 触发模式不同,netpoll 流式为 “伪” 流式(由于 LT 触发,会由网络库将数据读取到网络库的 buffer
+> 中),在大包的场景下(如:上传文件等)可能会有内存问题,推荐使用 go net。
+
+### 流式读
+
+Hertz Server 支持流式读取请求内容。
+
+示例代码:
+
+```go
+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`。
+
+ 示例代码:
+
+ ```go
+ 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)
+ }
+
+ ```
+
+2. 用户可以在 handler 中使用 `pkg/protocol/http1/resp/writer` 下提供的 `NewChunkedBodyWriter` 方法劫持 response 的
+ writer,然后使用 `ctx.Write` 函数将分块数据写入 Body 并将分块数据使用 `ctx.Flush` 函数立即发送给客户端。
+
+ 示例代码:
+
+ ```go
+ 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 逻辑中就可以将分块数据发送出去。
+**
+
+更多示例代码可参考 [example](/zh/docs/hertz/tutorials/example/#流式读写)。
+
+## 注册自定义协议
+
+```go
+func (engine *Engine) AddProtocol(protocol string, factory interface{})
+```
+
+详细信息可见 [注册自定义协议](/zh/docs/hertz/tutorials/framework-exten/protocol/#注册自定义协议-server-到-hertz-中)。
+
+## SetClientIPFunc
+
+该函数的参数 f 会被传递到 `RequestContext.SetClientIPFunc`
+函数中,作用及示例代码见 [SetClientIPFunc](/zh/docs/hertz/tutorials/basic-feature/context/request/#setclientipfunc)。
+
+函数签名:
+
+```go
+func (engine *Engine) SetClientIPFunc(f app.ClientIP)
+```
+
+## SetFormValueFunc
+
+该函数的参数 f 会被传递到 `RequestContext.SetFormValueFunc`
+函数中,作用及示例代码见 [SetFormValueFunc](/zh/docs/hertz/tutorials/basic-feature/context/request/#setformvaluefunc)。
+
+函数签名:
+
+```go
+func (engine *Engine) SetFormValueFunc(f app.FormValueFunc)
+```
+
+## 钩子函数
+
+钩子函数(Hooks)是一个通用的概念,表示某事件触发时所伴随的操作。
+
+Hertz 提供了全局的 Hook
+注入能力,用于在服务触发启动后和退出前注入自己的处理逻辑,详细信息可见 [Hooks](/zh/docs/hertz/tutorials/basic-feature/hooks/)。
+
+## Panic 处理函数
+
+用于设置当程序发生 panic 时的处理函数,默认为 `nil`。
+
+> 注意: 如果同时设置了 `PanicHandler` 和 `Recovery` 中间件,则 `Recovery` 中间件会覆盖 `PanicHandler` 的处理逻辑。
+
+示例代码:
+
+```go
+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()
+}
+```
+
+## ContinueHandler
+
+在接收到客户端发来的 Expect 100 Continue 头之后调用 ContinueHandler。使用 ContinueHandler,服务器可以决定是否读取可能很大的请求正文,默认情况下会读取。
+
+示例代码:
+
+```go
+h := server.Default()
+h.ContinueHandler = func(header *protocol.RequestHeader) bool {
+ return false
+}
+```
+
+## 渲染 template
+
+Hertz 提供了 `Delims`, `SetFuncMap`, `LoadHTMLGlob`, `LoadHTMLFiles` 等方法用于渲染 HTML
+或模板文件,详细内容可参考 [HTML](/zh/docs/hertz/tutorials/basic-feature/render/#html)。
+
+## NoRoute 与 NoMethod 使用
+
+Hertz 提供了 `NoRoute` 与 `NoMethod` 方法用于全局处理 HTTP 404 与 405
+请求,详细内容可参考 [NoRoute 与 NoMethod 使用](/zh/docs/hertz/tutorials/basic-feature/route/#noroute-与-nomethod-使用)。
+
+## 获取路由信息
+
+```go
+func (engine *Engine) Routes() (routes RoutesInfo)
+```
+
+### Routes
+
+`Routes` 函数返回一个按 HTTP 方法划分的包含路由信息(HTTP 方法名,路由路径,请求处理函数名)的切片。
+
+函数签名:
+
+```go
+func (engine *Engine) Routes() (routes RoutesInfo)
+```
+
+示例代码:
+
+```go
+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}]
+}
+```
+
+## 底层网络库
+
+```go
+func (engine *Engine) GetTransporterName() (tName string)
+func SetTransporter(transporter func (options *config.Options) network.Transporter)
+```
+
+### GetTransporterName
+
+获取当前使用的网络库名称,现在有原生的 `go net` 和 `netpoll` 两种。
+
+linux 默认使用 `netpoll`, windows 只能使用 `go net`。
+
+如果对如何使用对应的网络库有疑惑,请查看 [此处](https://www.cloudwego.io/zh/docs/hertz/tutorials/basic-feature/network-lib/)。
+
+函数签名:
+
+```go
+func (engine *Engine) GetTransporterName() (tName string)
+```
+
+示例代码:
+
+```go
+h := server.New()
+tName := h.GetTransporterName()
+```
+
+### SetTransporter
+
+`SetTransporter` 用于设置网络库。
+
+> 注意:`SetTransporter` 只设置 Engine 的全局默认值,所以在初始化 Engine 时使用 `WithTransporter`
+> 来设置网络库会覆盖掉 `SetTransporter` 的设置。
+
+函数签名:
+
+```go
+func SetTransporter(transporter func (options *config.Options) network.Transporter)
+```
+
+示例代码:
+
+```go
+route.SetTransporter(standard.NewTransporter)
+```
+
+## 链路追踪
+
+Hertz
+提供了链路追踪的能力,也支持用户自定义链路跟踪,详情可参考 [链路追踪](/zh/docs/hertz/tutorials/observability/tracing/)。
+
+## Hijack
+
+### NoHijackConnPool
+
+> Hertz 连接劫持时所使用的 hijack conn 是池化管理的,因此被劫持的连接在 websocket 中使用的时候,不支持异步操作。
+
+劫持的连接仅能被关闭一次,第二次关闭会导致空指针异常。
+
+NoHijackConnPool 将控制是否使用缓存池来获取/释放劫持连接。如果使用池,将提升内存资源分配的性能,但无法避免二次关闭连接导致的异常。
+
+如果很难保证 hijackConn 不会被反复关闭,可以将其设置为 true。
+
+示例代码:
+
+```go
+package main
+
+func main() {
+ // https://github.com/cloudwego/hertz/issues/121
+ h.NoHijackConnPool = true
+}
+```
+
+### HijackConnHandle
+
+设置 Hijack 连接处理函数。
+
+函数签名:
+
+```go
+func (engine *Engine) HijackConnHandle(c network.Conn, h app.HijackHandler)
+```
+
+
+---
+title: "网络库"
+date: 2022-05-20
+weight: 4
+keywords: ["netpoll", "go net"]
+description: "Hertz 默认集成了 Netpoll 和 Golang 原生网络库两个网络库,用户可以根据自己的场景选择合适的网络库以达到最佳性能。"
+
+---
+
+## 使用方式
+
+对于 Server 来说,默认使用 netpoll,可以通过配置项进行更改:
+
+> 注意:netpoll 目前不支持 Windows,Windows 会通过条件编译将网络库自动切换为 go net。
+
+```go
+server.New(server.WithTransport(standard.NewTransporter))
+server.New(server.WithTransport(netpoll.NewTransporter))
+```
+
+对于 Client 来说,可以通过配置项进行更改:
+
+```go
+client.NewClient(client.WithDialer(standard.NewDialer()))
+client.NewClient(client.WithDialer(netpoll.NewDialer()))
+```
+
+## 网络库选择
+
+1. 如果有启动 TLS Server 的需求,请使用 `go net` 网络库。`netpoll` 正在实现对 TLS 的支持。
+2. 由于网络库触发模式的不同:`go net` 为 ET 模型,`netpoll` 为 LT 模型,使得两个网络库的适用场景有一些不同。
+ 在 ET 模型下,由框架处理 Read / Write 事件;在 LT 模型下,由网络库处理 Read / Write 事件。
+ 使得在小包场景下,由于更优的调度策略使得 LT 性能更好;在大包场景下,由于读 / 写不受框架层控制,使得大量数据被读入内存而不能及时处理,可能会造成内存压力。
+
+- 在较大 request size 下(request size > 1M),推荐使用 go net 网络库加流式。
+- 在其他场景下,推荐使用 netpoll 网络库,会获得极致的性能。
+
+
+---
+title: "JSON Marshal 库"
+linkTitle: "JSON Marshal 库"
+date: 2023-08-03
+weight: 19
+keywords: ["JSON Marshal", "Sonic", "条件编译", "自定义 JSON Marshall 库"]
+description: "Hertz 使用的 JSON Marshal 库及自定义能力。"
+---
+
+Hertz 默认集成并使用 [Sonic](https://github.com/bytedance/sonic) 用于序列化 `ctx.JSON` 接口,以及反序列化 `binding`
+包中的请求。Sonic 是一款超高性能 golang json 库,详情参考 Sonic [README](https://github.com/bytedance/sonic) 。
+
+开启 Sonic 需要满足以下条件:
+
+- Go 1.16 以上
+- Linux / darwin OS / Windows
+- Amd64 CPU with AVX instruction set
+
+当上述条件不能满足时,Sonic 会自动 fallback 到 golang 的 encoding/json 库。
+
+## 自定义 JSON Marshall 库
+
+如果 Sonic 不能够满足您的需求,你可以使用以下方式自定义 json marshal 库的实现:
+
+```go
+import (
+ "encoding/json"
+
+ "github.com/bytedance/go-tagexpr/v2/binding"
+ "github.com/cloudwego/hertz/pkg/app/server/render"
+)
+func main() {
+ // Render
+ render.ResetJSONMarshal(json.Marshal)
+
+ // Binding
+ binding.ResetJSONUnmarshaler(json.Unmarshal)
+}
+```
+
+## 条件编译
+
+Hertz 支持条件编译来控制实际使用的 json 库,你可以通过 `-tags stdjson` 来选择使用标准库。
+
+```go
+go build -tags stdjson
+```
+
+## Sonic 相关问题
+
+若出现与 Sonic 相关的问题,可参考 Sonic [README](https://github.com/bytedance/sonic)
+或提 [issue](https://github.com/bytedance/sonic/issues) 解决。
+
+
+---
+title: "优雅退出"
+date: 2022-05-23
+weight: 11
+keywords: ["优雅退出"]
+description: "Hertz 停止服务时提供的优雅退出功能。"
+
+---
+
+Hertz 支持优雅退出,优雅退出过程如下:
+
+1. 设置 `engine` 状态为 `closed`
+2. 顺序非阻塞触发回调函数 `[]OnShutDown`(与标准包 net/http 一致),`Select` 等待回调函数执行完成或者超时返回
+3. `Select` 等待业务协程退出:
+ 1. 对于 netpoll 网络库,开启默认 1s(netpoll 中设置,暂时不可更改)的 `ticker`,定时查看 `active conn`(业务 handle
+ 退出且连接不处于阻塞读状态)是否为 0;对于 go net 网络库,则关闭监听,不对连接做处理。
+ 2. 等待超时时间为 `ExitWaitTime` 的 context 触发,默认 5s
+4. 注册中心注销对应服务
+5. 关闭网络库的信号监听
+6. 对处于关闭过程中的请求回包统一带上 `Connection:Close header`
+
+如需修改等待超时时间,可通过 `server.WithExitWaitTime()` 进行配置。
+
+如需注册退出 `hook` 函数,可通过获取到 `Engine` 后进行注册:
+
+```go
+h.Engine.OnShutdown = append(h.Engine.OnShutdown, shutDownFunc)
+```
+
+Hertz 使用 `waitSignal` 函数作为信号处理的默认实现方式,处理如下:
+
+- 当接收到 `SIGTERM` 系统信号时触发立即退出。
+- 当接收到 `SIGHUP|SIGINT` 系统信号时触发优雅退出。
+
+当信号处理的默认实现方式无法满足需求时,可通过 `SetCustomSignalWaiter` 来自定义信号处理方式。
+
+```go
+package main
+
+import (
+ "github.com/cloudwego/hertz/pkg/app/server"
+)
+
+func main() {
+ h := server.New()
+ h.SetCustomSignalWaiter(func(err chan error) error {
+ return nil
+ })
+ ...
+}
+
+```
+
+当自定义信号处理函数返回 `error` 时 Hertz 会立即退出,其他情况下则会优雅退出。
+
+
+---
+title: '渲染'
+date: 2023-06-01
+weight: 18
+keywords: ["渲染", "JSON", "Data", "HTML", "Protobuf", "Text", "XML", "自定义渲染"]
+description: "Hertz 提供的渲染能力。"
+---
+
+Hertz 支持对 JSON,HTML,Protobuf 等的渲染。
+
+## JSON
+
+### JSON
+
+Hertz 支持渲染 `JSON`。
+
+示例代码:
+
+```go
+func main() {
+ h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
+
+ // utils.H is a shortcut for map[string]interface{}
+ h.GET("/someJSON", func(ctx context.Context, c *app.RequestContext) {
+ c.JSON(consts.StatusOK, utils.H{"message": "hey", "status": consts.StatusOK})
+ })
+
+ h.Spin()
+}
+```
+
+你也可以使用一个结构体。
+
+示例代码:
+
+```go
+func main() {
+ h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
+
+ h.GET("/moreJSON", func(ctx context.Context, c *app.RequestContext) {
+ var msg struct {
+ Company string `json:"company"`
+ Location string
+ Number int
+ }
+ msg.Company = "company"
+ msg.Location = "location"
+ msg.Number = 123
+ // Note that msg.Company becomes "company" in the JSON
+ // Will output : {"company": "company", "Location": "location", "Number": 123}
+ c.JSON(consts.StatusOK, msg)
+ })
+
+ h.Spin()
+}
+```
+
+### PureJSON
+
+`JSON` 使用 Unicode 替换特殊的 HTML 字符,如果你想要按照字面意义编码这些字符,你可以使用 `PureJSON`。
+
+示例代码:
+
+```go
+func main() {
+ h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
+
+ h.GET("/pureJson", func(ctx context.Context, c *app.RequestContext) {
+ c.PureJSON(consts.StatusOK, utils.H{
+ "html": "
Hello World
",
+ })
+
+ h.Spin()
+}
+```
+
+### IndentedJSON
+
+`IndentedJSON` 将给定的结构序列化为优雅的 JSON (通过缩进 + 换行)。
+
+示例代码:
+
+```go
+func main() {
+ h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
+
+ h.GET("/indentedJSON", func(ctx context.Context, c *app.RequestContext) {
+ var msg struct {
+ Company string
+ Location string
+ Number int
+ }
+ msg.Company = "company"
+ msg.Location = "location"
+ msg.Number = 123
+
+ c.IndentedJSON(consts.StatusOK, msg)
+ /*
+ will output : {
+ "Company": "company",
+ "Location": "location",
+ "Number": 123
+ }
+ */
+
+ h.Spin()
+}
+```
+
+## Data
+
+`Data` 需要你自行设置 `Content-Type`,而且 `Data` 只接收 **[]byte**。
+
+示例代码:
+
+```go
+func main() {
+ h := server.Default(server.WithHostPorts("127.0.0.1:8080"))
+
+ h.GET("/someData", func(ctx context.Context, c *app.RequestContext) {
+ c.Data(consts.StatusOK, "text/plain; charset=utf-8", []byte("hello"))
+ })
+
+ h.Spin()
+}
+```
+
+## HTML
+
+### 加载模板文件
+
+Hertz 提供 `LoadHTMLGlob` 和 `LoadHTMLFiles` 来加载模板文件。
+
+示例代码:
+
+```go
+func main(){
+ h := server.Default(server.WithHostPorts(":8080"))
+
+ h.LoadHTMLGlob("render/html/*")
+ //h.LoadHTMLFiles("render/html/index.tmpl")
+
+ h.GET("/index", func(c context.Context, ctx *app.RequestContext) {
+ ctx.HTML(http.StatusOK, "index.tmpl", utils.H{
+ "title": "Main website",
+ })
+ })
+}
+```
+
+### 自定义分隔符
+
+Hertz 支持自定义分隔符。
+
+示例代码:
+
+```go
+ h := server.Default(server.WithHostPorts(":8080"))
+
+ h.Delims("{[{", "}]}")
+ //Left delimiter, defaults to {{.
+ //Right delimiter, defaults to }}.
+```
+
+### 自定义模板功能
+
+Hertz 支持自定义模板功能,示例代码如下。
+
+main.go:
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "html/template"
+ "net/http"
+ "time"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+)
+
+func formatAsDate(t time.Time) string {
+ year, month, day := t.Date()
+ return fmt.Sprintf("%d/%02d/%02d", year, month, day)
+}
+
+func main() {
+ h := server.Default(server.WithHostPorts(":8080"))
+
+ h.Delims("{[{", "}]}")
+
+ h.SetFuncMap(template.FuncMap{
+ "formatAsDate": formatAsDate,
+ })
+
+ h.LoadHTMLGlob("render/html/*")
+
+ h.GET("/raw", func(c context.Context, ctx *app.RequestContext) {
+ ctx.HTML(http.StatusOK, "template1.html", map[string]interface{}{
+ "now": time.Date(2017, 0o7, 0o1, 0, 0, 0, 0, time.UTC),
+ })
+ })
+
+ h.Spin()
+}
+```
+
+template1.html:
+
+```html
+Date: {[{.now | formatAsDate}]}
+```
+
+查看详细 [示例代码]([hertz-examples/render/html at main · cloudwego/hertz-examples · GitHub](https://github.com/cloudwego/hertz-examples/tree/main/render/html))。
+
+## Protobuf
+
+Hertz 支持渲染 `Protobuf`。
+
+示例代码:
+
+```go
+package main
+
+import (
+ "context"
+
+ "github.com/cloudwego/hertz-examples/render/protobuf/body"
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+)
+
+func main() {
+ h := server.Default(server.WithHostPorts(":8080"))
+
+ h.GET("/somePb", func(ctx context.Context, c *app.RequestContext) {
+ //The specific definition of protobuf is written in the "protobuf/body" file.
+ body := body.BodyStruct{
+ Body: []byte("Hello World"),
+ }
+ c.ProtoBuf(200, &body)
+ })
+
+ h.Spin()
+}
+```
+
+示例代码中的 `body.bodyStruct` 具体定义如下。
+
+```go
+type BodyStruct struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Body []byte `protobuf:"bytes,1,opt,name=body" json:"body,omitempty"`
+}
+```
+
+## Text
+
+Hertz 支持渲染 `string`,它需要你自行设置 `format`。
+
+示例代码:
+
+```go
+func main() {
+ h := server.Default(server.WithHostPorts(":8080"))
+
+ h.GET("someText", func(ctx context.Context, c *app.RequestContext) {
+ c.String(consts.StatusOK, "message", "hello,world")
+ })
+
+ h.Spin()
+}
+```
+
+## XML
+
+Hertz 支持渲染 `XML`。
+
+示例代码:
+
+```go
+func main() {
+ h := server.Default(server.WithHostPorts(":8080"))
+
+ h.GET("/someXML", func(ctx context.Context, c *app.RequestContext) {
+ c.XML(consts.StatusOK, "hello world")
+ })
+
+ h.Spin()
+}
+```
+
+## 自定义渲染
+
+Hertz 在 app 包内提供了 `Render` 方法。
+
+函数签名:
+
+```go
+func (ctx *RequestContext) Render(code int, r render.Render)
+```
+
+如果你想要进行自定义渲染,首先要自行实现 render 包内的 `Render` 接口。
+
+```go
+type Render interface {
+ // Render writes data with custom ContentType.
+ // Do not panic inside, RequestContext will handle it.
+ Render(resp *protocol.Response) error
+ // WriteContentType writes custom ContentType.
+ WriteContentType(resp *protocol.Response)
+}
+```
+
+以实现 `YAML` 渲染为例。
+
+示例代码:
+
+```go
+package main
+
+import (
+ "context"
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/cloudwego/hertz/pkg/protocol"
+ "github.com/cloudwego/hertz/pkg/protocol/consts"
+ "gopkg.in/yaml.v3"
+)
+
+func main() {
+ h := server.Default(server.WithHostPorts(":8080"))
+
+ h.GET("/someXML", func(ctx context.Context, c *app.RequestContext) {
+ c.Render(consts.StatusOK, YAML{Data: "hello,world"})
+ })
+
+ h.Spin()
+}
+
+type YAML struct {
+ Data interface{}
+}
+
+var yamlContentType = "application/yaml; charset=utf-8"
+
+func (r YAML) Render(resp *protocol.Response) error {
+ writeContentType(resp, yamlContentType)
+ yamlBytes, err := yaml.Marshal(r.Data)
+ if err != nil {
+ return err
+ }
+
+ resp.AppendBody(yamlBytes)
+
+ return nil
+}
+
+func (r YAML) WriteContentType(w *protocol.Response) {
+ writeContentType(w, yamlContentType)
+}
+
+func writeContentType(resp *protocol.Response, value string) {
+ resp.Header.SetContentType(value)
+}
+```
+
+## 完整示例
+
+完整用法示例详见 [example](https://github.com/cloudwego/hertz-examples/tree/main/render)。
+
+
+---
+title: "Hooks"
+date: 2022-10-16
+weight: 14
+keywords: ["Hooks", "StartHook", "ShutdownHook", "OnAccept", "OnConnect"]
+description: "Hertz 提供的钩子函数功能。"
+
+---
+
+**钩子函数**(Hooks)是一个通用的概念,表示某事件触发时所伴随的操作。
+
+Hertz 提供了全局的 Hook 注入能力,用于在服务**触发启动后**和**退出前**注入自己的处理逻辑。
+
+## StartHook
+
+`StartHook` 在 Hertz 当中表示服务**触发启动后**需调用的函数,使用 `CtxErrCallback` 类型表示。Hertz 使用 `OnRun`
+属性存储 `StartHook` 列表。
+
+```go
+// CtxErrCallback 参见下方其函数签名
+OnRun []CtxErrCallback
+```
+
+触发 Server 启动后,框架会按函数声明顺序**依次**调用所有的 `StartHook` 函数,完成调用之后,才会正式开始端口监听,如果发生错误,则立刻终止服务。
+
+函数签名:
+
+```go
+type CtxErrCallback func(ctx context.Context) error
+```
+
+示例代码:
+
+```go
+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.OnRun = append(h.OnRun, func(ctx context.Context) error {
+ hlog.Info("run the first start hook")
+ return nil
+ })
+ h.OnRun = append(h.OnRun, func(ctx context.Context) error {
+ hlog.Info("run the second start hook")
+ return nil
+ })
+ h.OnRun = append(h.OnRun, func(ctx context.Context) error {
+ hlog.Info("run the third start hook")
+ return nil
+ })
+
+ h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
+ ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
+ })
+
+ h.Spin()
+}
+```
+
+提示:启动服务,将在控制台**顺序**打印三个 `StartHook` 函数的日志。
+
+```shell
+main.go:17: [Info] run the first start hook
+main.go:21: [Info] run the second start hook
+main.go:25: [Info] run the third start hook
+```
+
+## ShutdownHook
+
+`ShutdownHook` 在 Hertz 当中表示服务**退出前**需调用的函数,使用 `CtxCallback` 类型表示。Hertz 使用 `OnShutdown`
+属性存储 `ShutdownHook` 列表。
+
+Server 退出前,框架会**并发地**调用所有声明的 `ShutdownHook` 函数,并且可以通过 `server.WithExitWaitTime`配置最大等待时长,默认为
+5 秒,如果超时,则立刻终止服务。
+
+`ShutdownHook` 的调用本质上是
+Hertz [优雅退出](https://www.cloudwego.io/zh/docs/hertz/tutorials/basic-feature/graceful-shutdown/) 的一环。
+
+函数签名:
+
+```go
+type CtxCallback func(ctx context.Context)
+```
+
+示例代码 1:
+
+```go
+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.OnShutdown = append(h.OnShutdown, func(ctx context.Context) {
+ hlog.Info("run the first shutdown hook")
+ })
+ h.OnShutdown = append(h.OnShutdown, func(ctx context.Context) {
+ hlog.Info("run the second shutdown hook")
+ })
+ h.OnShutdown = append(h.OnShutdown, func(ctx context.Context) {
+ hlog.Info("run the third shutdown hook")
+ })
+
+ h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
+ ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
+ })
+
+ h.Spin()
+}
+```
+
+提示:终止服务,将在控制台**乱序**打印三个 `ShutdownHook` 函数的日志。
+
+```shell
+hertz.go:77: [Info] HERTZ: Begin graceful shutdown, wait at most num=5 seconds...
+main.go:22: [Info] run the third shutdown hook
+main.go:16: [Info] run the first shutdown hook
+main.go:19: [Info] run the second shutdown hook
+engine.go:279: [Info] HERTZ: Execute OnShutdownHooks finish
+```
+
+示例代码 2:
+
+```go
+package main
+
+import (
+ "context"
+ "time"
+
+ "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(server.WithExitWaitTime(time.Second * 2))
+
+ h.OnShutdown = append(h.OnShutdown, func(ctx context.Context) {
+ hlog.Info("run shutdown hook")
+ time.Sleep(time.Second * 5)
+ })
+
+ h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
+ ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
+ })
+
+ h.Spin()
+}
+```
+
+提示:终止服务时,因为钩子函数执行时间超过 2 秒,打印超时日志。
+
+```shell
+hertz.go:77: [Info] HERTZ: Begin graceful shutdown, wait at most num=2 seconds...
+main.go:17: [Info] run shutdown hook
+engine.go:276: [Info] HERTZ: Execute OnShutdownHooks timeout: error=context deadline exceeded
+```
+
+## OnAccept
+
+`OnAccept` 是一个在连接建立后且被添加到 epoll 前调用的函数。
+
+```go
+OnAccept func(conn net.Conn) context.Context
+```
+
+示例代码:
+
+```go
+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"
+ "net"
+)
+
+func main() {
+
+ h := server.New(
+ server.WithOnAccept(func(conn net.Conn) context.Context {
+ hlog.Info("run the onAccept")
+ return context.Background()
+ }),
+ server.WithHostPorts("localhost:9230"))
+ h.GET("", func(c context.Context, ctx *app.RequestContext) {
+ hlog.Info("pong")
+ ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
+ })
+
+ h.Spin()
+
+}
+
+```
+
+提示:在发出请求后,将在控制台打印 `OnAccept` 函数的日志。
+
+```
+main.go:32: [Info] run the onAccept
+main.go:38: [Info] pong
+```
+
+## OnConnect
+
+`OnConnect` 是一个在其被添加到 epoll 后调用的函数。它和 `OnAccept` 的不同之处在于它可以获取数据但是 `OnAccept` 不可以。
+
+```go
+OnConnect func(ctx context.Context, conn network.Conn) context.Context
+```
+
+示例代码:
+
+```go
+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/network"
+ "github.com/cloudwego/hertz/pkg/protocol/consts"
+)
+
+func main() {
+
+ h := server.New(
+ server.WithHostPorts("localhost:9229"),
+ server.WithOnConnect(func(ctx context.Context, conn network.Conn) context.Context {
+ b, _ := conn.Peek(3)
+ hlog.Info("onconnect")
+ hlog.Info(b)
+ return ctx
+ }))
+ h.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
+ c.JSON(consts.StatusOK, utils.H{"ping": "pong"})
+ })
+ h.Spin()
+
+}
+```
+
+提示:在发出请求后,将在控制台打印 `OnConnect` 函数的日志。
+
+```
+main.go:19: [Info] onconnect
+main.go:20: [Info] [71 69 84]
+```
+
+
+---
+title: "错误处理"
+
+date: 2022-05-23
+weight: 10
+keywords: ["错误处理", "自定义错误"]
+description: "Hertz 提供的错误处理功能。"
+
+---
+
+## 错误
+
+在 Hertz 中,定义了如下的错误结构体:
+
+```go
+type Error struct {
+ Err error
+ Type ErrorType
+ Meta interface{}
+}
+```
+
+其中 `Err` 为标准错误,`Type` 为自定义错误类型,`Meta` 为错误元数据。
+
+### 错误类型
+
+为了更高效的处理错误,Hertz 针对错误类型做了如下预定义:
+
+```go
+// binding 过程的错误
+ErrorTypeBind ErrorType = 1 << iota
+// rendering 过程的错误
+ErrorTypeRender
+// Hertz 内部错误,业务无需感知
+ErrorTypePrivate
+// 相对于 Private 来说,需要外部感知的错误
+ErrorTypePublic
+// 其他错误
+ErrorTypeAny
+```
+
+建议按照错误类别定义相应的错误。
+
+### 自定义错误
+
+使用如下接口自定义错误:
+
+```go
+// shortcut for creating a public *Error from string
+func NewPublic(err string) *Error {
+ return New(errors.New(err), ErrorTypePublic, nil)
+}
+
+// shortcut for creating a private *Error from string
+func NewPrivate(err string) *Error {
+ return New(errors.New(err), ErrorTypePrivate, nil)
+}
+
+func New(err error, t ErrorType, meta interface{}) *Error {
+ return &Error{
+ Err: err,
+ Type: t,
+ Meta: meta,
+ }
+}
+
+func Newf(t ErrorType, meta interface{}, format string, v ...interface{}) *Error {
+ return New(fmt.Errorf(format, v...), t, meta)
+}
+
+func NewPublicf(format string, v ...interface{}) *Error {
+ return New(fmt.Errorf(format, v...), ErrorTypePublic, nil)
+}
+
+func NewPrivatef(format string, v ...interface{}) *Error {
+ return New(fmt.Errorf(format, v...), ErrorTypePrivate, nil)
+}
+
+```
+
+### 相关方法
+
+| 函数签名 | 描述 |
+|----------------------------------|-----------------------------------------|
+| SetType(flags ErrorType) *Error | 将 `Error` 的 `ErrorType` 设置为给定的 `flags` |
+| Error() string | 实现标准 `error` 接口 |
+| Unwrap() error | 抛出错误 |
+| SetMeta(data interface{}) *Error | 设置元数据 |
+| IsType(flags ErrorType) bool | 判断 `Error` 的 `ErrorType` 是否为给定的 `flags` |
+| JSON() interface{} | 将错误转换为 `json` 对象 |
+
+## ErrorChain
+
+除了针对错误定义的约定以外,框架同时提供
+ErrorChain(错误链)能力。顾名思义,能够方便业务将一次请求处理上所遇到的所有错误绑定到错误链上,可以方便后续(一般是在中间件中)对所有错误进行统一处理。
+
+### 相关方法
+
+| 函数签名 | 描述 |
+|----------------------------------|---------------------|
+| String() string | 返回一个可读性强的文本用于展示所有错误 |
+| Errors() []string | 将错误链转换为标准错误数组 |
+| ByType(typ ErrorType) ErrorChain | 按给定的错误类型返回对应的子错误链 |
+| Last() *Error | 返回最后(最新)的一个错误 |
+| JSON() interface{} | 将所有错误转换为 `json` 对象 |
+
+### 如何使用
+
+对应的 API 为:`RequestContext.Error(err)`,调用该 API 会将 err 绑到对应的请求上下文上之上。
+
+获取请求上下文已绑定的所有错误的方式:`RequestContext.Errors`。
+
+```go
+// 运行此代码并打开游览器访问 localhost:8080/error
+package main
+
+import (
+ "context"
+ "errors"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/cloudwego/hertz/pkg/protocol/consts"
+)
+
+func main() {
+ h := server.New(server.WithHostPorts(":8080"))
+
+ h.GET("/error", handle1, handle2, handle3)
+
+ h.Spin()
+}
+
+func handle1(_ context.Context, c *app.RequestContext) {
+ _ = c.Error(errors.New("first err"))
+}
+
+func handle2(_ context.Context, c *app.RequestContext) {
+ _ = c.Error(errors.New("second err"))
+}
+
+func handle3(_ context.Context, c *app.RequestContext) {
+ c.JSON(consts.StatusOK, c.Errors.Errors())
+}
+```
+
+
+---
+title: "正向代理和反向代理"
+date: 2022-09-08
+weight: 12
+keywords: ["正向代理", "反向代理"]
+description: "Hertz 提供的正向代理和反向代理能力。"
+
+---
+
+## 正向代理
+
+正向代理是一种特殊的网络服务,允许一个网络终端(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击。
+
+一个完整的代理请求过程为:客户端(Client)首先与代理服务器创建连接,接着根据代理服务器所使用的代理协议,请求对目标服务器创建连接、或者获得目标服务器的指定资源。
+
+### 安装
+
+hertz 内置了访问正向代理的功能
+
+### 定义
+
+```go
+// Proxy 结构体,根据 request 来选定访问的代理 uri
+type Proxy func(*protocol.Request) (*protocol.URI, error)
+
+// ProxyURI 用来生成只会返回固定代理 uri 的 Proxy
+func ProxyURI(fixedURI *protocol.URI) Proxy
+
+// SetProxy 用来设置 client 的 proxy,设置后 client 会与 proxy 建连发请求
+func (c *Client) SetProxy(p protocol.Proxy)
+```
+
+### 示例
+
+```go
+package main
+
+import (
+ "context"
+
+ "github.com/cloudwego/hertz/pkg/app/client"
+ "github.com/cloudwego/hertz/pkg/protocol"
+)
+
+func main() {
+ proxyURL := "http://<__user_name__>:<__password__>@<__proxy_addr__>:<__proxy_port__>"
+
+ // 将代理的 uri 转成 *protocol.URI 的形式
+ parsedProxyURL := protocol.ParseURI(proxyURL)
+
+ c, err := client.NewClient()
+ if err != nil {
+ return
+ }
+
+ // 设置代理
+ c.SetProxy(protocol.ProxyURI(parsedProxyURL))
+
+ upstreamURL := "http://google.com"
+ _, body, _ := c.Get(context.Background(), nil, upstreamURL)
+}
+```
+
+> 客户端默认不支持 TLS,如果要访问 https 地址,应该使用标准库
+
+```go
+c, err := client.NewClient(client.WithDialer(standard.NewDialer()))
+```
+
+> 如果报证书错误还需要跳过证书验证
+
+```go
+clientCfg := &tls.Config{
+ InsecureSkipVerify: true,
+}
+c, err := client.NewClient(client.WithTLSConfig(clientCfg), client.WithDialer(standard.NewDialer()))
+```
+
+## 反向代理
+
+反向代理在计算机网络中是代理服务器的一种。
+
+服务器根据客户端的请求,从其关系的一组或多组后端服务器(如 Web 服务器)上获取资源,然后再将这些资源返回给客户端,客户端只会得知反向代理的
+IP 地址,而不知道在代理服务器后面的服务器集群的存在。
+
+### 安装
+
+```bash
+go get github.com/hertz-contrib/reverseproxy
+```
+
+### 具体实现
+
+```go
+type ReverseProxy struct {
+ // 用于转发的客户端,可以通过 SetClient 方法对其进行配置
+ client *client.Client
+
+ // 设置反向代理的目标地址
+ target string
+
+ // 用于转换 request,可以通过 SetDirector 方法来自定义
+ // director 必须是将一个请求转换为一个新的请求的函数。
+ // 响应直接未经修改重定向返回给原始客户端
+ // 请求返回后 direcotr 不得访问
+ director func (*protocol.Request)
+
+ // modifyResponse 这是一个可选的函数,用于修改来自后端的响应
+ // 可以通过 SetModifyResponse 方法进行修改
+ // 如果后端返回任意响应,不管状态码是什么,这个方法将会被调用。
+ // 如果后端不可访问,errorHandler 方法会使用错误信息做入参被调用。
+ // 如果 modifyResponse 方法返回一个错误,errorHandler 方法将会使用错误做入参被调用。
+ // 如果 errorHandler 未设置,将使用默认实现。
+ modifyResponse func(*protocol.Response) error
+
+ // errorHandler 是一个可选的函数,用于处理到达后台的错误或来自 modifyResponse 的错误。
+ // 如果未进行设置,默认返回 StatusBadGateway (502)
+ errorHandler func(*app.RequestContext, error)
+}
+
+// NewSingleHostReverseProxy 返回一个新的反向代理来路由请求到指定后端。如果后端路径是”/base“请求路径是”/dir” ,目标路径将会是“/base/dir” 。
+// NewSingleHostReverseProxy 不会重写 Host 请求头。
+// 要想覆盖 Host 请求头,可以选择自定义 director
+func NewSingleHostReverseProxy(target string, opts ...config.Option) (*reverseProxy, error)
+```
+
+> - `NewSingleHostReverseProxy` 方法如果没有设置 `config.ClientOption` 将会使用默认的全局 `client.Client` 实例,
+ 如果设置了 `config.ClientOption` 将会初始化一个 `client.Client` 实例。
+ 如果你需要共享一个 `client.Client` 实例,可以使用 `ReverseProxy.SetClient` 来设置。
+> - 反向代理会重置响应头,如果在请求之前修改了响应头将不会生效。
+
+我们提供了 `SetXxx()` 函数用于设置私有属性
+
+| 方法 | 描述 |
+|---------------------|-------------------------------------|
+| `SetDirector` | 用于指定 protocol.Request |
+| `SetClient` | 用于指定转发的客户端 |
+| `SetModifyResponse` | 用于指定响应修改方法 |
+| `SetErrorHandler` | 用于指定处理到达后台的错误或来自 modifyResponse 的错误 |
+
+### 示例
+
+```go
+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/hertz-contrib/reverseproxy"
+)
+
+func main() {
+ h := server.Default(server.WithHostPorts("127.0.0.1:8000"))
+ // 设置目标地址
+ proxy, err := reverseproxy.NewSingleHostReverseProxy("http://127.0.0.1:8000/proxy")
+ if err != nil {
+ panic(err)
+ }
+ h.GET("/proxy/backend", func(cc context.Context, c *app.RequestContext) {
+ c.JSON(200, utils.H{
+ "msg": "proxy success!!",
+ })
+ })
+ // 设置代理
+ h.GET("/backend", proxy.ServeHTTP)
+ h.Spin()
+}
+```
+
+### FAQ
+
+#### 如何代理 HTTPS
+
+> Netpoll 不支持 TLS,Client 需要使用标准网络库.
+
+代理 HTTPS 需要在额外做一些配置.
+
+- `NewSingleHostReverseProxy` 方法中使用 `WithDialer` 传递 `standard.NewDialer()` 指定标准网络库。
+- 使用 `SetClient` 设置一个使用标准网络库的 Hertz Client。
+
+#### 如何配合中间件使用
+
+可以在 hertz handler 中也使用 `ReverseProxy.ServeHTTP` 来实现复杂的需求而不是直接将 `ReverseProxy.ServeHTTP` 注册到路由。
+
+**示例代码**
+
+```go
+package main
+
+import (
+ //...
+)
+
+func main() {
+ //...
+ r.Use(func(c context.Context, ctx *app.RequestContext) {
+ if ctx.Query("country") == "cn" {
+ proxy.ServeHTTP(c, ctx)
+ ctx.Response.Header.Set("key", "value")
+ ctx.Abort()
+ } else {
+ ctx.Next(c)
+ }
+ })
+ //...
+}
+```
+
+### 更多示例
+
+| 用途 | 示例代码 |
+|---------|-------------------------------------------------------------------------------------------|
+| 代理 tls | [code](https://github.com/cloudwego/hertz-examples/tree/main/reverseproxy/tls) |
+| 使用服务发现 | [code](https://github.com/cloudwego/hertz-examples/tree/main/reverseproxy/discovery) |
+| 配合中间件使用 | [code](https://github.com/cloudwego/hertz-examples/tree/main/reverseproxy/use_middleware) |
+
+更多使用方法可参考如下 [examples](https://github.com/cloudwego/hertz-examples/tree/main/reverseproxy)。
+
+
+---
+title: "常量"
+date: 2023-05-24
+weight: 17
+keywords: ["常量", "HTTP 请求方法", "HTTP 常用 MIME 类型", "HTTP 状态码", "HTTP 头信息", "HTTP 协议版本"]
+description: "Hertz 中定义的供用户使用的常量。"
+
+---
+
+
+在 Hertz
+中定义了一系列的常量以供用户使用,它们都位于 [github.com/cloudwego/hertz/pkg/protocol/consts](https://github.com/cloudwego/hertz/tree/develop/pkg/protocol/consts) 。
+
+### HTTP 请求方法
+
+```go
+// HTTP methods were copied from net/http.
+const (
+ MethodGet = "GET" // RFC 7231, 4.3.1
+ MethodHead = "HEAD" // RFC 7231, 4.3.2
+ MethodPost = "POST" // RFC 7231, 4.3.3
+ MethodPut = "PUT" // RFC 7231, 4.3.4
+ MethodPatch = "PATCH" // RFC 5789
+ MethodDelete = "DELETE" // RFC 7231, 4.3.5
+ MethodConnect = "CONNECT" // RFC 7231, 4.3.6
+ MethodOptions = "OPTIONS" // RFC 7231, 4.3.7
+ MethodTrace = "TRACE" // RFC 7231, 4.3.8
+)
+```
+
+### HTTP 常用 MIME 类型
+
+```go
+const (
+ // MIME text
+ MIMETextPlain = "text/plain"
+ MIMETextPlainUTF8 = "text/plain; charset=utf-8"
+ MIMETextPlainISO88591 = "text/plain; charset=iso-8859-1"
+ MIMETextPlainFormatFlowed = "text/plain; format=flowed"
+ MIMETextPlainDelSpaceYes = "text/plain; delsp=yes"
+ MiMETextPlainDelSpaceNo = "text/plain; delsp=no"
+ MIMETextHtml = "text/html"
+ MIMETextCss = "text/css"
+ MIMETextJavascript = "text/javascript"
+ // MIME application
+ MIMEApplicationOctetStream = "application/octet-stream"
+ MIMEApplicationFlash = "application/x-shockwave-flash"
+ MIMEApplicationHTMLForm = "application/x-www-form-urlencoded"
+ MIMEApplicationHTMLFormUTF8 = "application/x-www-form-urlencoded; charset=UTF-8"
+ MIMEApplicationTar = "application/x-tar"
+ MIMEApplicationGZip = "application/gzip"
+ MIMEApplicationXGZip = "application/x-gzip"
+ MIMEApplicationBZip2 = "application/bzip2"
+ MIMEApplicationXBZip2 = "application/x-bzip2"
+ MIMEApplicationShell = "application/x-sh"
+ MIMEApplicationDownload = "application/x-msdownload"
+ MIMEApplicationJSON = "application/json"
+ MIMEApplicationJSONUTF8 = "application/json; charset=utf-8"
+ MIMEApplicationXML = "application/xml"
+ MIMEApplicationXMLUTF8 = "application/xml; charset=utf-8"
+ MIMEApplicationZip = "application/zip"
+ MIMEApplicationPdf = "application/pdf"
+ MIMEApplicationWord = "application/msword"
+ MIMEApplicationExcel = "application/vnd.ms-excel"
+ MIMEApplicationPPT = "application/vnd.ms-powerpoint"
+ MIMEApplicationOpenXMLWord = "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
+ MIMEApplicationOpenXMLExcel = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
+ MIMEApplicationOpenXMLPPT = "application/vnd.openxmlformats-officedocument.presentationml.presentation"
+ // MIME image
+ MIMEImageJPEG = "image/jpeg"
+ MIMEImagePNG = "image/png"
+ MIMEImageGIF = "image/gif"
+ MIMEImageBitmap = "image/bmp"
+ MIMEImageWebP = "image/webp"
+ MIMEImageIco = "image/x-icon"
+ MIMEImageMicrosoftICO = "image/vnd.microsoft.icon"
+ MIMEImageTIFF = "image/tiff"
+ MIMEImageSVG = "image/svg+xml"
+ MIMEImagePhotoshop = "image/vnd.adobe.photoshop"
+ // MIME audio
+ MIMEAudioBasic = "audio/basic"
+ MIMEAudioL24 = "audio/L24"
+ MIMEAudioMP3 = "audio/mp3"
+ MIMEAudioMP4 = "audio/mp4"
+ MIMEAudioMPEG = "audio/mpeg"
+ MIMEAudioOggVorbis = "audio/ogg"
+ MIMEAudioWAVE = "audio/vnd.wave"
+ MIMEAudioWebM = "audio/webm"
+ MIMEAudioAAC = "audio/x-aac"
+ MIMEAudioAIFF = "audio/x-aiff"
+ MIMEAudioMIDI = "audio/x-midi"
+ MIMEAudioM3U = "audio/x-mpegurl"
+ MIMEAudioRealAudio = "audio/x-pn-realaudio"
+ // MIME video
+ MIMEVideoMPEG = "video/mpeg"
+ MIMEVideoOgg = "video/ogg"
+ MIMEVideoMP4 = "video/mp4"
+ MIMEVideoQuickTime = "video/quicktime"
+ MIMEVideoWinMediaVideo = "video/x-ms-wmv"
+ MIMEVideWebM = "video/webm"
+ MIMEVideoFlashVideo = "video/x-flv"
+ MIMEVideo3GPP = "video/3gpp"
+ MIMEVideoAVI = "video/x-msvideo"
+ MIMEVideoMatroska = "video/x-matroska"
+)
+```
+
+### HTTP 状态码
+
+```go
+// HTTP status codes were stolen from net/http.
+const (
+ StatusContinue = 100 // RFC 7231, 6.2.1
+ StatusSwitchingProtocols = 101 // RFC 7231, 6.2.2
+ StatusProcessing = 102 // RFC 2518, 10.1
+ StatusOK = 200 // RFC 7231, 6.3.1
+ StatusCreated = 201 // RFC 7231, 6.3.2
+ StatusAccepted = 202 // RFC 7231, 6.3.3
+ StatusNonAuthoritativeInfo = 203 // RFC 7231, 6.3.4
+ StatusNoContent = 204 // RFC 7231, 6.3.5
+ StatusResetContent = 205 // RFC 7231, 6.3.6
+ StatusPartialContent = 206 // RFC 7233, 4.1
+ StatusMultiStatus = 207 // RFC 4918, 11.1
+ StatusAlreadyReported = 208 // RFC 5842, 7.1
+ StatusIMUsed = 226 // RFC 3229, 10.4.1
+ StatusMultipleChoices = 300 // RFC 7231, 6.4.1
+ StatusMovedPermanently = 301 // RFC 7231, 6.4.2
+ StatusFound = 302 // RFC 7231, 6.4.3
+ StatusSeeOther = 303 // RFC 7231, 6.4.4
+ StatusNotModified = 304 // RFC 7232, 4.1
+ StatusUseProxy = 305 // RFC 7231, 6.4.5
+ _ = 306 // RFC 7231, 6.4.6 (Unused)
+ StatusTemporaryRedirect = 307 // RFC 7231, 6.4.7
+ StatusPermanentRedirect = 308 // RFC 7538, 3
+ StatusBadRequest = 400 // RFC 7231, 6.5.1
+ StatusUnauthorized = 401 // RFC 7235, 3.1
+ StatusPaymentRequired = 402 // RFC 7231, 6.5.2
+ StatusForbidden = 403 // RFC 7231, 6.5.3
+ StatusNotFound = 404 // RFC 7231, 6.5.4
+ StatusMethodNotAllowed = 405 // RFC 7231, 6.5.5
+ StatusNotAcceptable = 406 // RFC 7231, 6.5.6
+ StatusProxyAuthRequired = 407 // RFC 7235, 3.2
+ StatusRequestTimeout = 408 // RFC 7231, 6.5.7
+ StatusConflict = 409 // RFC 7231, 6.5.8
+ StatusGone = 410 // RFC 7231, 6.5.9
+ StatusLengthRequired = 411 // RFC 7231, 6.5.10
+ StatusPreconditionFailed = 412 // RFC 7232, 4.2
+ StatusRequestEntityTooLarge = 413 // RFC 7231, 6.5.11
+ StatusRequestURITooLong = 414 // RFC 7231, 6.5.12
+ StatusUnsupportedMediaType = 415 // RFC 7231, 6.5.13
+ StatusRequestedRangeNotSatisfiable = 416 // RFC 7233, 4.4
+ StatusExpectationFailed = 417 // RFC 7231, 6.5.14
+ StatusTeapot = 418 // RFC 7168, 2.3.3
+ StatusUnprocessableEntity = 422 // RFC 4918, 11.2
+ StatusLocked = 423 // RFC 4918, 11.3
+ StatusFailedDependency = 424 // RFC 4918, 11.4
+ StatusUpgradeRequired = 426 // RFC 7231, 6.5.15
+ StatusPreconditionRequired = 428 // RFC 6585, 3
+ StatusTooManyRequests = 429 // RFC 6585, 4
+ StatusRequestHeaderFieldsTooLarge = 431 // RFC 6585, 5
+ StatusUnavailableForLegalReasons = 451 // RFC 7725, 3
+ StatusInternalServerError = 500 // RFC 7231, 6.6.1
+ StatusNotImplemented = 501 // RFC 7231, 6.6.2
+ StatusBadGateway = 502 // RFC 7231, 6.6.3
+ StatusServiceUnavailable = 503 // RFC 7231, 6.6.4
+ StatusGatewayTimeout = 504 // RFC 7231, 6.6.5
+ StatusHTTPVersionNotSupported = 505 // RFC 7231, 6.6.6
+ StatusVariantAlsoNegotiates = 506 // RFC 2295, 8.1
+ StatusInsufficientStorage = 507 // RFC 4918, 11.5
+ StatusLoopDetected = 508 // RFC 5842, 7.2
+ StatusNotExtended = 510 // RFC 2774, 7
+ StatusNetworkAuthenticationRequired = 511 // RFC 6585, 6
+)
+```
+
+### HTTP 头信息
+
+```go
+const (
+ HeaderDate = "Date"
+ HeaderIfModifiedSince = "If-Modified-Since"
+ HeaderLastModified = "Last-Modified"
+ // Redirects
+ HeaderLocation = "Location"
+ // Transfer coding
+ HeaderTE = "TE"
+ HeaderTrailer = "Trailer"
+ HeaderTrailerLower = "trailer"
+ HeaderTransferEncoding = "Transfer-Encoding"
+ // Controls
+ HeaderCookie = "Cookie"
+ HeaderExpect = "Expect"
+ HeaderMaxForwards = "Max-Forwards"
+ HeaderSetCookie = "Set-Cookie"
+ HeaderSetCookieLower = "set-cookie"
+ // Connection management
+ HeaderConnection = "Connection"
+ HeaderKeepAlive = "Keep-Alive"
+ HeaderProxyConnection = "Proxy-Connection"
+ // Authentication
+ HeaderAuthorization = "Authorization"
+ HeaderProxyAuthenticate = "Proxy-Authenticate"
+ HeaderProxyAuthorization = "Proxy-Authorization"
+ HeaderWWWAuthenticate = "WWW-Authenticate"
+ // Range requests
+ HeaderAcceptRanges = "Accept-Ranges"
+ HeaderContentRange = "Content-Range"
+ HeaderIfRange = "If-Range"
+ HeaderRange = "Range"
+ // Response context
+ HeaderAllow = "Allow"
+ HeaderServer = "Server"
+ HeaderServerLower = "server"
+ // Request context
+ HeaderFrom = "From"
+ HeaderHost = "Host"
+ HeaderReferer = "Referer"
+ HeaderReferrerPolicy = "Referrer-Policy"
+ HeaderUserAgent = "User-Agent"
+ // Message body information
+ HeaderContentEncoding = "Content-Encoding"
+ HeaderContentLanguage = "Content-Language"
+ HeaderContentLength = "Content-Length"
+ HeaderContentLocation = "Content-Location"
+ HeaderContentType = "Content-Type"
+ // Content negotiation
+ HeaderAccept = "Accept"
+ HeaderAcceptCharset = "Accept-Charset"
+ HeaderAcceptEncoding = "Accept-Encoding"
+ HeaderAcceptLanguage = "Accept-Language"
+ HeaderAltSvc = "Alt-Svc"
+)
+```
+
+### HTTP 协议版本
+
+```go
+const(
+ // Protocol
+ HTTP11 = "HTTP/1.1"
+ HTTP10 = "HTTP/1.0"
+ HTTP20 = "HTTP/2.0"
+)
+```
+
+
+---
+title: "基本特性"
+linkTitle: "基本特性"
+weight: 2
+keywords: ["Engine", "路由", "客户端", "网络库", "请求上下文", "中间件", "协议", "绑定与校验", "流式处理", "错误处理", "优雅退出", "正向代理和反向代理", "重试", "Hooks", "单测", "适配器", "常量", "渲染", "JSON Marshal 库"]
+description: "Hertz 基本特性。"
+
+---
+
+
+---
+title: "路由"
+date: 2022-09-06
+weight: 2
+keywords: ["路由", "路由组", "静态路由", "参数路由", "路由优先级", "NoRoute", "NoMethod"]
+description: "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 方法 |
+| `Hertz.StaticFile/Static/StaticFS` | 用于注册静态文件 |
+
+示例代码:
+
+```go
+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` ) 的能力,用于支持路由分组的功能,同时中间件也可以注册到路由组上。
+
+示例代码:
+
+```go
+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"))
+ 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:
+
+```go
+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:
+
+```go
+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` 方法,我们可以获取路由中携带的参数。
+
+示例代码:
+
+```go
+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` 方法,我们可以获取路由中携带的参数。
+
+示例代码:
+
+```go
+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()
+}
+
+
+```
+
+完整用法示例详见 [example](https://github.com/cloudwego/hertz-examples/tree/main/route)
+
+## 注意
+
+### 使用匿名函数与装饰器注册路由
+
+在使用匿名函数或装饰器注册路由时,如果我们使用 `RequestContext.HandlerName()` 获取 handler 名称则会获取到错误的名称。
+
+这里需要使用 Hertz 提供的 `GETEX`、`POSTEX`、`PUTEX`、`DELETEEX`、`HEADEX`、`AnyEX`、`HandleEX` 方法并手动传入 handler
+名称注册路由,使用 `app.GetHandlerName` 获取 handler 名称。
+
+示例代码:
+
+```go
+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.AnyEX("/ping", func(c context.Context, ctx *app.RequestContext) {
+ ctx.String(consts.StatusOK, app.GetHandlerName(ctx.Handler()))
+ }, "ping_handler")
+ h.Spin()
+}
+```
+
+### 获取路由注册信息
+
+Hertz 提供了 `Routes` 获取注册的路由信息供用户使用。
+
+路由信息结构:
+
+```go
+// 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
+```
+
+示例代码:
+
+```go
+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` 配合使用。
+
+示例代码:
+
+```go
+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`
+状态码。
+
+可以在配置中取消,如下:
+
+````go
+package main
+
+import "github.com/cloudwego/hertz/pkg/app/server"
+
+func main() {
+ h := server.New(server.WithRedirectTrailingSlash(false))
+ ...
+}
+````
+
+获取更多配置相关信息:https://www.cloudwego.io/zh/docs/hertz/reference/config/
+
+
+---
+title: "单测"
+date: 2022-05-23
+weight: 15
+keywords: ["单测"]
+description: "Hertz 为用户提供的单元测试能力。"
+
+---
+
+一个好的项目的构建离不开单元测试。为了帮助使用者构建出好的项目,hertz 当然也提供了单元测试的工具。
+
+原理和 golang httptest 类似,都是不经过网络只执行 `ServeHTTP` 返回执行后的 response。
+
+## 创建请求上下文
+
+```go
+func CreateUtRequestContext(method, url string, body *Body, headers ...Header) *app.RequestContext
+```
+
+### CreateUtRequestContext
+
+返回一个 `app.RequestContext` 对象,用于单元测试。
+
+函数签名:
+
+```go
+func CreateUtRequestContext(method, url string, body *Body, headers ...Header) *app.RequestContext
+```
+
+示例代码:
+
+```go
+import (
+ "bytes"
+ "testing"
+
+ "github.com/cloudwego/hertz/pkg/common/test/assert"
+ "github.com/cloudwego/hertz/pkg/common/ut"
+)
+
+func TestCreateUtRequestContext(t *testing.T) {
+ body := "1"
+ method := "PUT"
+ path := "/hey/dy"
+ headerKey := "Connection"
+ headerValue := "close"
+ ctx := ut.CreateUtRequestContext(method, path, &ut.Body{Body: bytes.NewBufferString(body), Len: len(body)},
+ ut.Header{Key: headerKey, Value: headerValue})
+
+ assert.DeepEqual(t, method, string(ctx.Method()))
+ assert.DeepEqual(t, path, string(ctx.Path()))
+ body1, err := ctx.Body()
+ assert.DeepEqual(t, nil, err)
+ assert.DeepEqual(t, body, string(body1))
+ assert.DeepEqual(t, headerValue, string(ctx.GetHeader(headerKey)))
+}
+```
+
+## 发送请求
+
+```go
+func PerformRequest(engine *route.Engine, method, url string, body *Body, headers ...Header) *ResponseRecorder
+```
+
+### PerformRequest
+
+`PerformRequest` 函数在没有网络传输的情况下向指定 engine 发送构造好的请求。
+
+url 可以是标准的相对路径也可以是绝对路径。
+
+如果想设置流式的请求体,可以通过 `server.WithStreamBody(true)` 将 engine.streamRequestBody 设置为 true 或者将 body 的 len
+设置为 -1。
+
+该函数返回 [ResponseRecorder 对象](#responserecorder-对象)。
+
+函数签名:
+
+```go
+func PerformRequest(engine *route.Engine, method, url string, body *Body, headers ...Header) *ResponseRecorder
+```
+
+示例代码:
+
+```go
+import (
+ "bytes"
+ "context"
+ "testing"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/common/config"
+ "github.com/cloudwego/hertz/pkg/common/test/assert"
+ "github.com/cloudwego/hertz/pkg/common/ut"
+ "github.com/cloudwego/hertz/pkg/route"
+)
+
+func TestPerformRequest(t *testing.T) {
+ router := route.NewEngine(config.NewOptions([]config.Option{}))
+ router.GET("/hey/:user", func(ctx context.Context, c *app.RequestContext) {
+ user := c.Param("user")
+ assert.DeepEqual(t, "close", c.Request.Header.Get("Connection"))
+ c.Response.SetConnectionClose()
+ c.JSON(201, map[string]string{"hi": user})
+ })
+
+ w := ut.PerformRequest(router, "GET", "/hey/hertz", &ut.Body{bytes.NewBufferString("1"), 1},
+ ut.Header{"Connection", "close"})
+ resp := w.Result()
+ assert.DeepEqual(t, 201, resp.StatusCode())
+ assert.DeepEqual(t, "{\"hi\":\"hertz\"}", string(resp.Body()))
+}
+```
+
+## 接收响应
+
+在执行 [PerformRequest](#performrequest) 函数时,内部已经调用了 `NewRecorder`, `Header`, `Write`, `WriteHeader`, `Flush`
+等函数,用户只需调用 `Result` 函数拿到返回的 `protocol.Response` 对象进行单测即可。
+
+### ResponseRecorder 对象
+
+用于记录 handler 的响应信息,内容如下:
+
+```go
+type ResponseRecorder struct {
+ // Code is the HTTP response code set by WriteHeader.
+ //
+ // Note that if a Handler never calls WriteHeader or Write,
+ // this might end up being 0, rather than the implicit
+ // http.StatusOK. To get the implicit value, use the Result
+ // method.
+ Code int
+
+ // header contains the headers explicitly set by the Handler.
+ // It is an internal detail.
+ header *protocol.ResponseHeader
+
+ // Body is the buffer to which the Handler's Write calls are sent.
+ // If nil, the Writes are silently discarded.
+ Body *bytes.Buffer
+
+ // Flushed is whether the Handler called Flush.
+ Flushed bool
+
+ result *protocol.Response // cache of Result's return value
+ wroteHeader bool
+}
+```
+
+该对象提供的方法如下:
+
+| 函数签名 | 说明 |
+|:-------------------------------------------------------------------|:------------------------------------------------------------------------------|
+| `func NewRecorder() *ResponseRecorder` | 返回初始化后的 `ResponseRecorder` 对象 |
+| `func (rw *ResponseRecorder) Header() *protocol.ResponseHeader` | 返回 `ResponseRecorder.header` |
+| `func (rw *ResponseRecorder) Write(buf []byte) (int, error)` | 将 `[]byte` 类型的数据写入 `ResponseRecorder.Body` |
+| `func (rw *ResponseRecorder) WriteString(str string) (int, error)` | 将 `string` 类型的数据写入 `ResponseRecorder.Body` |
+| `func (rw *ResponseRecorder) WriteHeader(code int)` | 设置 `ResponseRecorder.Code` 以及 `ResponseRecorder.header.SetStatusCode(code)` |
+| `func (rw *ResponseRecorder) Flush()` | 实现了 `http.Flusher`,将 `ResponseRecorder.Flushed` 设置为 true |
+| `func (rw *ResponseRecorder) Result() *protocol.Response` | 返回 handler 生成的响应信息,至少包含 StatusCode, Header, Body 以及可选的 Trailer,未来将支持返回更多的响应信息 |
+
+## 与业务 handler 配合使用
+
+假如已经创建了 handler 以及一个函数 `Ping()`:
+
+```go
+
+package handler
+
+import (
+ "context"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/common/utils"
+)
+
+// Ping .
+func Ping(ctx context.Context, c *app.RequestContext) {
+ c.JSON(200, utils.H{
+ "message": "pong",
+ })
+}
+```
+
+可以在单元测试中直接对 `Ping()` 函数进行测试:
+
+```go
+package handler
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/cloudwego/hertz/pkg/common/test/assert"
+ "github.com/cloudwego/hertz/pkg/common/ut"
+)
+
+func TestPerformRequest(t *testing.T) {
+ h := server.Default()
+ h.GET("/ping", Ping)
+ w := ut.PerformRequest(h.Engine, "GET", "/ping", &ut.Body{bytes.NewBufferString("1"), 1},
+ ut.Header{"Connection", "close"})
+ resp := w.Result()
+ assert.DeepEqual(t, 201, resp.StatusCode())
+ assert.DeepEqual(t, "{\"message\":\"pong\"}", string(resp.Body()))
+}
+```
+
+之后对 `Ping()` 函数进行修改,单元测试文件不需要复制相同的业务逻辑。
+
+更多 examples 参考 [pkg/common/ut](https://github.com/cloudwego/hertz/tree/main/pkg/common/ut) 中的单测文件。
+
+
+---
+title: "客户端"
+date: 2023-07-25
+weight: 3
+keywords: ["Client 配置", "发送请求", "请求超时", "流式处理", "中间件", "服务发现"]
+description: "Hertz 客户端相关功能。"
+---
+
+## 快速开始
+
+```go
+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](/zh/docs/hertz/tutorials/basic-feature/protocol/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 客户端的重试配置,重试配置详细说明请看 [重试](/zh/docs/hertz/tutorials/basic-feature/retry/) |
+| WithWriteTimeout | 0s | HTTP 客户端的写入超时时间 |
+| WithConnStateObserve | nil, 5s | 设置观察和记录 HTTP 客户端的连接状态的函数以及观察执行间隔 |
+| WithDialFunc | network.Dialer | 设置 HTTP 客户端拨号器函数,会覆盖自定义拨号器 |
+
+示例代码:
+
+```go
+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](/zh/docs/hertz/tutorials/service-governance/service_discovery/#withtag) |
+| WithSD | false | 配合服务发现使用,传递 true 时,本次请求使用服务发现,详情见 [WithSD](/zh/docs/hertz/tutorials/service-governance/service_discovery/#withsd) |
+
+示例代码:
+
+```go
+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)
+}
+```
+
+## 发送请求
+
+```go
+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](#get) 函数或 [DoRedirects](#doredirects) 函数或 [Post](#post) 函数来跟随重定向。
+
+如果 resp 为 nil,则会忽略响应。如果所有针对请求主机的 DefaultMaxConnsPerHost 连接都已忙,则会返回 `ErrNoFreeConns`
+错误。在性能关键的代码中,建议通过 AcquireRequest 和 AcquireResponse 获取 req 和 resp。
+
+函数签名:
+
+```go
+func (c *Client) Do(ctx context.Context, req *protocol.Request, resp *protocol.Response) error
+```
+
+示例代码:
+
+```go
+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 ==
+}
+```
+
+### DoRedirects
+
+DoRedirects 函数执行给定的 http 请求并填充给定的 http 响应,遵循最多 maxRedirectsCount 次重定向。当重定向次数超过
+maxRedirectsCount 时,将返回 `ErrTooManyRedirects` 错误。
+
+函数签名:
+
+```go
+func (c *Client) DoRedirects(ctx context.Context, req *protocol.Request, resp *protocol.Response, maxRedirectsCount int) error
+```
+
+示例代码:
+
+```go
+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 ==
+}
+```
+
+### Get
+
+Get 函数返回 URL 的状态码和响应体。如果 dst 太小,则将被响应体替换并返回,否则将分配一个新的切片。
+
+该函数会自动跟随重定向。
+
+函数签名:
+
+```go
+func (c *Client) Get(ctx context.Context, dst []byte, url string, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error)
+```
+
+示例代码:
+
+```go
+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 ==
+}
+```
+
+### Post
+
+Post 函数使用给定的 POST 参数向指定的 URL 发送 POST 请求。如果 dst 太小,则将被响应体替换并返回,否则将分配一个新的切片。
+
+该函数会自动跟随重定向。
+
+如果 postArgs 为 nil,则发送空的 POST 请求体。
+
+函数签名:
+
+```go
+func (c *Client) Post(ctx context.Context, dst []byte, url string, postArgs *protocol.Args, requestOptions ...config.RequestOption) (statusCode int, body []byte, err error)
+```
+
+示例代码:
+
+```go
+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 ==
+}
+```
+
+## 请求超时
+
+> 注意:Do、DoRedirects、Get、Post 等请求函数可以通过 WithRequestTimeout 设置请求超时时间,DoTimeout 和 DoDeadline
+> 函数通过传参的形式设置请求超时时间,两者都是修改 `RequestOptions.requestTimeout` 字段,所以在使用 DoTimeout 和 DoDeadline
+> 函数时无需使用 WithRequestTimeout 函数,若同时使用了,请求超时时间以最后一次设置的为准。
+
+```go
+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 配置](#client-request-配置)
+中的 `WithRequestTimeout` 配置项来设置请求超时返回。
+
+示例代码:
+
+```go
+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](#get) 函数或 [DoRedirects](#doredirects) 函数或 [Post](#post) 函数来跟随重定向。
+
+如果 resp 为 nil,则会忽略响应。如果在给定的超时时间内未能收到响应,则会返回 `errTimeout` 错误。
+
+函数签名:
+
+```go
+func (c *Client) DoTimeout(ctx context.Context, req *protocol.Request, resp *protocol.Response, timeout time.Duration) error
+```
+
+示例代码:
+
+```go
+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 ==
+
+ 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](#get) 函数或 [DoRedirects](#doredirects) 函数或 [Post](#post) 函数来跟随重定向。
+
+如果 resp 为 nil,则会忽略响应。如果在给定的截止日期之前未能收到响应,则会返回 `errTimeout` 错误。
+
+函数签名:
+
+```go
+func (c *Client) DoDeadline(ctx context.Context, req *protocol.Request, resp *protocol.Response, deadline time.Time) error
+```
+
+示例代码:
+
+```go
+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 ==
+
+ 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"
+}
+```
+
+## 请求重试
+
+```go
+func (c *Client) SetRetryIfFunc(retryIf client.RetryIfFunc)
+```
+
+### SetRetryIfFunc
+
+`SetRetryIfFunc`
+方法用于自定义配置重试发生的条件。(更多内容请参考 [retry-条件配置](/zh/docs/hertz/tutorials/basic-feature/retry/#retry-条件配置))
+
+函数签名:
+
+```go
+func (c *Client) SetRetryIfFunc(retryIf client.RetryIfFunc)
+```
+
+示例代码:
+
+```go
+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` 等多种形式的请求内容。
+
+示例代码:
+
+```go
+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 客户端支持向服务器上传文件。
+
+示例代码:
+
+```go
+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 客户端支持流式读取 HTTP 响应内容。
+
+client 有复用连接的问题,如果使用了流式,那连接就会交由用户处理 (`resp.BodyStream()` 底层是对 connection 的封装)
+,这个时候对连接的管理会有一些不同:
+
+1. 如果用户不关闭连接,连接最终会被 GC 关掉,不会造成连接泄漏。但是,由于关闭连接需要等待 2RTT,在高并发情况下可能会出现 fd
+ 被打满导致无法新建连接的情况。
+2. 用户可以调用相关接口回收连接,回收后,该连接会放入连接池中复用,资源使用率更好,性能更高。以下几种方式都会回收连接,注意回收只能回收一次。
+ 1. 显式调用 `protocol.ReleaseResponse(), resp.Reset(), resp.ResetBody()`。
+ 2. 非显式调用:server 侧也会有回收 resp 的逻辑。如果 client 与 server 使用同一个 response 的情况下就不需要显式调用回收的方法了。
+
+示例代码:
+
+```go
+func main() {
+ c, _ := client.NewClient(client.WithResponseBodyStream(true))
+ req := &protocol.Request{}
+ resp := &protocol.Response{}
+ defer func() {
+ protocol.ReleaseRequest(req)
+ protocol.ReleaseResponse(resp)
+ }()
+ req.SetMethod(consts.MethodGet)
+ req.SetRequestURI("http://127.0.0.1:8080/streamWrite")
+ err := c.Do(context.Background(), req, resp)
+ if err != nil {
+ return
+ }
+ bodyStream := resp.BodyStream()
+ p := make([]byte, resp.Header.ContentLength()/2)
+ _, err = bodyStream.Read(p)
+ if err != nil {
+ fmt.Println(err.Error())
+ }
+ left, _ := ioutil.ReadAll(bodyStream)
+ fmt.Println(string(p), string(left))
+}
+```
+
+## 服务发现
+
+Hertz 客户端支持通过服务发现寻找目标服务器。
+
+Hertz
+支持自定义服务发现模块,更多内容可参考 [服务发现拓展](/zh/docs/hertz/tutorials/framework-exten/service_discovery/#服务发现扩展)。
+
+Hertz
+目前已接入的服务发现中心相关内容可参考 [服务注册与发现](/zh/docs/hertz/tutorials/service-governance/service_discovery/)。
+
+## TLS
+
+Hertz 客户端默认使用的网络库 netpoll 不支持 TLS,如果要配置 TLS 访问 https 地址,应该使用标准库。
+
+TLS 相关的配置信息可参考 [tls](/zh/docs/hertz/tutorials/basic-feature/protocol/tls/)。
+
+示例代码:
+
+```go
+func main() {
+ clientCfg := &tls.Config{
+ InsecureSkipVerify: true,
+ }
+ c, err := client.NewClient(
+ client.WithTLSConfig(clientCfg),
+ client.WithDialer(standard.NewDialer()),
+ )
+ if err != nil {
+ return
+ }
+
+ req, res := &protocol.Request{}, &protocol.Response{}
+ req.SetMethod(consts.MethodGet)
+ req.SetRequestURI("https://www.example.com")
+
+ err = c.Do(context.Background(), req, res)
+ fmt.Printf("resp = %v,err = %+v", string(res.Body()), err)
+}
+```
+
+## 正向代理
+
+```go
+func (c *Client) SetProxy(p protocol.Proxy)
+```
+
+### SetProxy
+
+SetProxy 用来设置客户端代理。(更多内容请参考 [正向代理](/zh/docs/hertz/tutorials/basic-feature/proxy/#正向代理))
+
+> 注意:同一个客户端不能设置多个代理,如果需要使用另一个代理,请创建另一个客户端并为其设置代理。
+
+示例代码:
+
+```go
+func (c *Client) SetProxy(p protocol.Proxy)
+```
+
+函数签名:
+
+```go
+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))
+}
+```
+
+## 关闭空闲连接
+
+```go
+func (c *Client) CloseIdleConnections()
+```
+
+### CloseIdleConnections
+
+`CloseIdleConnections` 方法用于关闭任何处于空闲状态的 `keep-alive` 连接。这些连接可能是之前的请求所建立的,但现在已经空闲了一段时间。该方法不会中断任何当前正在使用的连接。
+
+函数签名:
+
+```go
+func (c *Client) CloseIdleConnections()
+```
+
+示例代码:
+
+```go
+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()
+}
+```
+
+## 获取拨号器名称
+
+```go
+func (c *Client) GetDialerName() (dName string, err error)
+```
+
+### GetDialerName
+
+`GetDialerName` 方法用于获取客户端当前使用的拨号器的名称。如果无法获取拨号器名称,则返回 `unknown`。
+
+函数签名:
+
+```go
+func (c *Client) GetDialerName() (dName string, err error)
+```
+
+示例代码:
+
+```go
+func main() {
+ c, err := client.NewClient()
+ if err != nil {
+ return
+ }
+ // get dialer name
+ dName, err := c.GetDialerName()
+ if err != nil {
+ fmt.Printf("GetDialerName failed: %v", err)
+ return
+ }
+ fmt.Printf("dialer name=%v\n", dName)
+ // dName == "standard"
+}
+```
+
+## 中间件
+
+```go
+func (c *Client) Use(mws ...Middleware)
+func (c *Client) UseAsLast(mw Middleware) error
+func (c *Client) TakeOutLastMiddleware() Middleware
+```
+
+### Use
+
+使用 `Use` 方法对当前 client
+增加一个中间件。(更多内容请参考 [客户端中间件](/zh/docs/hertz/tutorials/basic-feature/middleware/#客户端中间件))
+
+函数签名:
+
+```go
+func (c *Client) Use(mws ...Middleware)
+```
+
+### UseAsLast
+
+`UseAsLast` 函数将中间件添加到客户端中间件链的最后。
+
+如果客户端中间件链在之前已经设置了最后一个中间件,`UseAsLast` 函数将会返回 `errorLastMiddlewareExist`
+错误。因此,为确保客户端中间件链的最后一个中间件为空,可以先使用 [TakeOutLastMiddleware](#takeoutlastmiddleware)
+函数清空客户端中间件链的最后一个中间件。
+
+> 注意:`UseAsLast` 函数将中间件设置在了 `c.lastMiddleware` 中,而使用 [Use](#use) 函数设置的中间件链存放在`c.mws`
+> 中,两者相对独立,只是在执行客户端中间件链的最后才执行 `c.lastMiddleware`,因此 `UseAsLast` 函数在 [Use](#use) 函数之前或之后调用皆可。
+
+函数签名:
+
+```go
+func (c *Client) UseAsLast(mw Middleware) error
+```
+
+示例代码:
+
+```go
+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
+ }
+}
+```
+
+### TakeOutLastMiddleware
+
+`TakeOutLastMiddleware` 函数返回 [UseAsLast](#useaslast) 函数中设置的最后一个中间件并将其清空,若没有设置则返回 `nil`。
+
+函数签名:
+
+```go
+func (c *Client) TakeOutLastMiddleware() Middleware
+```
+
+示例代码:
+
+```go
+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
+ }
+ middleware := client.TakeOutLastMiddleware() // middleware == LastMiddleware
+ middleware = client.TakeOutLastMiddleware() // middleware == nil
+}
+```
+
+
+---
+title: "国际化"
+date: 2022-09-01
+weight: 5
+keywords: ["国际化", "i18n"]
+description: "Hertz 提供了国际化 (i18n) 的中间件扩展 。"
+
+---
+
+Hertz 提供了国际化 (i18n) 的 [中间件扩展](https://github.com/hertz-contrib/i18n) ,它参考了 Gin
+的 [实现](https://github.com/gin-contrib/i18n) 。
+
+使用方法可参考如下 [example](https://github.com/hertz-contrib/i18n/tree/main/example)
+
+## 安装
+
+```go
+go get github.com/hertz-contrib/i18n
+```
+
+## 示例代码
+
+```go
+package main
+
+import (
+ "context"
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ hertzI18n "github.com/hertz-contrib/i18n"
+ "github.com/nicksnyder/go-i18n/v2/i18n"
+)
+
+func main() {
+ h := server.New(server.WithHostPorts(":3000"))
+ h.Use(hertzI18n.Localize())
+ h.GET("/:name", func(c context.Context, ctx *app.RequestContext) {
+ ctx.String(200, hertzI18n.MustGetMessage(&i18n.LocalizeConfig{
+ MessageID: "welcomeWithName",
+ TemplateData: map[string]string{
+ "name": ctx.Param("name"),
+ },
+ }))
+ })
+ h.GET("/", func(c context.Context, ctx *app.RequestContext) {
+ ctx.String(200, hertzI18n.MustGetMessage("welcome"))
+ })
+
+ h.Spin()
+}
+```
+
+## 配置
+
+### Localize
+
+用于将 `i18n` 扩展集成进 hertz server
+
+函数标签如下:
+
+```go
+func Localize(opts ...Option) app.HandlerFunc
+```
+
+示例代码:
+
+```go
+package main
+
+import (
+ "context"
+ _ "embed"
+ "time"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ hertzI18n "github.com/hertz-contrib/i18n"
+ "github.com/nicksnyder/go-i18n/v2/i18n"
+ "golang.org/x/text/language"
+ "gopkg.in/yaml.v3"
+)
+
+func main() {
+ h := server.New()
+ h.Use(hertzI18n.Localize())
+ h.GET("/:name", func(c context.Context, ctx *app.RequestContext) {
+ ctx.String(200, hertzI18n.MustGetMessage(&i18n.LocalizeConfig{
+ MessageID: "welcomeWithName",
+ TemplateData: map[string]string{
+ "name": ctx.Param("name"),
+ },
+ }))
+ })
+ h.GET("/", func(c context.Context, ctx *app.RequestContext) {
+ ctx.String(200, hertzI18n.MustGetMessage("welcome"))
+ })
+
+ h.Spin()
+}
+```
+
+### MustGetMessage
+
+`MustGetMessage` 用于获取 i18n 信息,但不做错误处理。
+
+函数签名如下:
+
+```go
+func MustGetMessage(param interface{}) string
+```
+
+示例代码如下:
+
+```go
+h.GET("/:name", func(c context.Context, ctx *app.RequestContext) {
+ ctx.String(200, hertzI18n.MustGetMessage(&i18n.LocalizeConfig{
+ MessageID: "welcomeWithName",
+ TemplateData: map[string]string{
+ "name": ctx.Param("name"),
+ },
+ }))
+})
+h.GET("/", func(c context.Context, ctx *app.RequestContext) {
+ ctx.String(200, hertzI18n.MustGetMessage("welcome"))
+})
+
+```
+
+**LocalizeConfig 配置项**
+
+该配置项移步 [go-i18n](https://github.com/nicksnyder/go-i18n/blob/main/v2/i18n/localizer.go#L53) 自行查看
+
+### WithBundle
+
+`WithBundle`用于将自定义配置加载进入中间件
+
+函数标签如下:
+
+```go
+func WithBundle(cfg *BundleCfg) Option
+```
+
+示例代码如下:
+
+```go
+package main
+
+import (
+ "context"
+ _ "embed"
+ "time"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ hertzI18n "github.com/hertz-contrib/i18n"
+ "github.com/nicksnyder/go-i18n/v2/i18n"
+ "golang.org/x/text/language"
+ "gopkg.in/yaml.v3"
+)
+
+func main() {
+ h := server.New(
+ server.WithHostPorts(":3000"),
+ server.WithExitWaitTime(time.Second),
+ )
+ h.Use(hertzI18n.Localize(
+ hertzI18n.WithBundle(&hertzI18n.BundleCfg{
+ RootPath: "./localize",
+ AcceptLanguage: []language.Tag{language.Chinese, language.English},
+ DefaultLanguage: language.Chinese,
+ FormatBundleFile: "yaml",
+ UnmarshalFunc: yaml.Unmarshal,
+ }),
+ ))
+ h.GET("/:name", func(c context.Context, ctx *app.RequestContext) {
+ ctx.String(200, hertzI18n.MustGetMessage(&i18n.LocalizeConfig{
+ MessageID: "welcomeWithName",
+ TemplateData: map[string]string{
+ "name": ctx.Param("name"),
+ },
+ }))
+ })
+ h.GET("/", func(c context.Context, ctx *app.RequestContext) {
+ ctx.String(200, hertzI18n.MustGetMessage("welcome"))
+ })
+
+ h.Spin()
+}
+```
+
+**配置项**
+
+| 配置项 | 类型 | 默认值 | 描述 |
+|:-----------------|--------------------|--------------------------------------------------|---------------------------------------|
+| DefaultLanguage | language.Tag | language.English | 默认转换语言类型 |
+| FormatBundleFile | string | "yaml" | 转换文件模板类型,例如:yaml, json |
+| AcceptLanguage | []language.Tag | []language.Tag{defaultLanguage,language.Chinese} | 接收转换类型 |
+| RootPath | string | defaultRootPath | 模板文件目录 |
+| UnmarshalFunc | i18n.UnmarshalFunc | yaml.Unmarshal | 模板文件解码函数,例如:yaml.Unmarshal |
+| Loader | Loader | LoaderFunc(ioutil.ReadFile) | 文件读取函数,例如 LoaderFunc(ioutil.ReadFile) |
+
+### WithGetLangHandle
+
+`WithGetLangHandle` 用于配置 i18n 模板触发条件,可以通过从参数,请求头中取出信息
+
+函数标签如下:
+
+```go
+func WithGetLangHandle(handler GetLangHandler)
+```
+
+示例代码如下:
+
+```go
+func main() {
+ h := server.New()
+ h.Use(hertzI18n.Localize(
+ hertzI18n.WithGetLangHandle(
+ func(c context.Context, ctx *app.RequestContext, defaultLang string) string {
+ lang := ctx.Query("lang")
+ if lang == "" {
+ return defaultLang
+ }
+ return lang
+ },
+ ),
+ ))
+ // ...
+ h.Spin()
+}
+```
+
+完整用法示例详见 [i18n](https://github.com/hertz-contrib/i18n/)
+
+
+---
+title: "访问日志"
+date: 2023-03-14
+weight: 10
+keywords: ["HTTP", "访问日志"]
+description: "访问日志可以收集所有 HTTP 请求的详细信息,包括时间、端口、请求方法等。Hertz 也提供了 access log 的实现。"
+
+---
+
+访问日志可以收集所有 HTTP 请求的详细信息,包括时间、端口、请求方法等。Hertz 也提供了 access log
+的 [实现](https://github.com/hertz-contrib/logger)
+,这里的实现参考了 [fiber](https://github.com/gofiber/fiber/tree/master/middleware/logger)。
+
+## 安装
+
+```shell
+go get github.com/hertz-contrib/logger/accesslog
+```
+
+## 示例代码
+
+```go
+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/hertz-contrib/logger/accesslog"
+)
+
+func main() {
+ h := server.Default(
+ server.WithHostPorts(":8080"),
+ )
+ h.Use(accesslog.New())
+ h.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
+ c.JSON(200, utils.H{"msg": "pong"})
+ })
+ h.Spin()
+}
+```
+
+## 配置
+
+用户可以通过自定义初始化配置来设置访问日志的格式以及内容。
+
+### WithFormat
+
+使用 `WithFormat` 自定义日志格式,默认的日志格式为 `[${time}] ${status} - ${latency} ${method} ${path}`
+。传入的格式方式为 `${tag}`,具体 tag 参数可以参考下面的 [支持的标签](#支持的标签)。
+
+函数签名:
+
+```go
+func WithFormat(s string) Option
+```
+
+示例代码:
+
+```go
+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/hertz-contrib/logger/accesslog"
+)
+
+func main() {
+ h := server.Default(
+ server.WithHostPorts(":8080"),
+ )
+ h.Use(accesslog.New(accesslog.WithFormat("[${time}] ${status} - ${latency} ${method} ${path} ${queryParams}")))
+ h.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
+ c.JSON(200, utils.H{"msg": "pong"})
+ })
+ h.Spin()
+}
+
+```
+
+### WithTimeFormat
+
+使用 `WithTimeFormat` 自定义时间格式,默认时间格式为 `15:04:05`
+,具体格式可以参考该 [链接](https://programming.guide/go/format-parse-string-time-date-example.html) 或者 go
+的 [time](https://github.com/golang/go/blob/7bd22aafe41be40e2174335a3dc55431ca9548ec/src/time/format.go#L111) 包。
+
+函数签名:
+
+```go
+func WithTimeFormat(s string) Option
+```
+
+示例代码:
+
+```go
+package main
+
+import (
+ "context"
+ "time"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/cloudwego/hertz/pkg/common/utils"
+ "github.com/hertz-contrib/logger/accesslog"
+)
+
+func main() {
+ h := server.Default(
+ server.WithHostPorts(":8080"),
+ )
+ h.Use(accesslog.New(
+ accesslog.WithTimeFormat(time.RFC822),
+ ))
+ h.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
+ c.JSON(200, utils.H{"msg": "pong"})
+ })
+ h.Spin()
+}
+```
+
+### WithTimeInterval
+
+使用 `WithTimeInterval` 配置时间戳的刷新间隔,默认值为 `500ms`。
+
+函数签名:
+
+```go
+func WithTimeInterval(t time.Duration) Option
+```
+
+示例代码:
+
+```go
+package main
+
+import (
+ "context"
+ "time"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/cloudwego/hertz/pkg/common/utils"
+ "github.com/hertz-contrib/logger/accesslog"
+)
+
+func main() {
+ h := server.Default(
+ server.WithHostPorts(":8080"),
+ )
+ h.Use(accesslog.New(
+ accesslog.WithTimeInterval(time.Second),
+ ))
+ h.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
+ c.JSON(200, utils.H{"msg": "pong"})
+ })
+ h.Spin()
+}
+
+```
+
+### WithAccessLogFunc
+
+使用 `WithAccessLogFunc` 自定义日志打印函数。
+
+函数签名:
+
+```go
+func WithAccessLogFunc(f func(ctx context.Context, format string, v ...interface{})) Option
+```
+
+示例代码:
+
+```go
+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/hertz-contrib/logger/accesslog"
+)
+
+func main() {
+ h := server.Default(
+ server.WithHostPorts(":8080"),
+ )
+ h.Use(accesslog.New(
+ accesslog.WithAccessLogFunc(hlog.CtxInfof),
+ ))
+ h.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
+ c.JSON(200, utils.H{"msg": "pong"})
+ })
+ h.Spin()
+}
+```
+
+### WithTimeZoneLocation
+
+使用 `WithTimeZoneLocation` 自定义时区,默认使用当地时区。
+
+函数签名:
+
+```go
+func WithTimeZoneLocation(loc *time.Location) Option
+```
+
+示例代码:
+
+```go
+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/hertz-contrib/logger/accesslog"
+)
+
+func main() {
+ h := server.Default(
+ server.WithHostPorts(":8080"),
+ )
+
+ location, err := time.LoadLocation("Asia/Shanghai")
+ if err != nil {
+ return
+ }
+ h.Use(accesslog.New(
+ accesslog.WithTimeZoneLocation(location),
+ ))
+ h.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
+ c.JSON(200, utils.H{"msg": "pong"})
+ })
+ h.Spin()
+}
+```
+
+## 日志格式
+
+### 默认日志格式
+
+```
+[${time}] ${status} - ${latency} ${method} ${path}
+```
+
+例子:
+
+```
+[21:54:36] 200 - 2.906859ms GET /ping
+```
+
+### 支持的标签
+
+| 标签 | 介绍 |
+|---------------|--------------------------------------------------------------------------------------------------------|
+| pid | 进程 ID |
+| time | 时间 |
+| referer | 当前请求的来源页面 [地址](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Referer) |
+| protocol | 协议类型 |
+| port | 端口 |
+| ip | Host 中的 ip 地址 |
+| ips | Header 中的 [X-Forwarded-For](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/X-Forwarded-For) |
+| host | HTTP 中的 Host |
+| method | 请求方法 |
+| path | 请求路径 |
+| url | 请求 url |
+| ua | User-Agent 的缩写 |
+| latency | 处理消息的延迟 |
+| status | HTTP 返回的状态码 |
+| resBody | 返回内容 |
+| reqHeaders | 请求的 Header 内容 |
+| resHeaders | 返回的 Header 内容 |
+| queryParams | 请求的 query 参数 |
+| body | 请求的消息体内容 |
+| bytesSent | 返回的消息体长度 |
+| bytesReceived | 请求的消息体长度 |
+| route | 请求路由的路径 |
+
+### 标签扩展
+
+支持自定义标签,前提要保证是线程安全的。
+
+代码示例:
+
+```go
+package main
+
+import (
+ "context"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/cloudwego/hertz/pkg/common/bytebufferpool"
+ "github.com/cloudwego/hertz/pkg/common/utils"
+ "github.com/hertz-contrib/logger/accesslog"
+)
+
+func main() {
+ accesslog.Tags["test_tag"] = func(ctx context.Context, c *app.RequestContext, buf *bytebufferpool.ByteBuffer) (int, error) {
+ return buf.WriteString("test")
+ }
+ h := server.Default(
+ server.WithHostPorts(":8080"),
+ )
+ h.Use(accesslog.New(accesslog.WithFormat("${test_tag}")))
+ h.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
+ c.JSON(200, utils.H{"msg": "pong"})
+ })
+ h.Spin()
+}
+```
+
+
+---
+title: "基本认证"
+date: 2022-10-13
+weight: 2
+keywords: ["HTTP", "基本认证"]
+description: "Hertz 提供了 basic auth 的实现。"
+
+---
+
+在 HTTP 中,基本认证(Basic access authentication)是一种用来允许网页浏览器或其他客户端程序在请求时提供用户名和密码形式的身份凭证的一种登录验证方式。
+在基本认证中,请求包含一个格式为 `Authorization: Basic ` 的头部字段,其中 credentials 是用户名和密码的 Base64
+编码,用一个冒号 `:` 连接。
+
+Hertz 也提供了 basic auth 的 [实现](https://github.com/cloudwego/hertz/tree/main/pkg/app/middlewares/server/basic_auth)
+,参考了 gin 的 [实现](https://github.com/gin-gonic/gin#using-basicauth-middleware) 。
+
+## 导入
+
+```go
+import "github.com/cloudwego/hertz/pkg/app/middlewares/server/basic_auth"
+```
+
+## 示例代码
+
+```go
+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"))
+
+ h.Use(basic_auth.BasicAuth(map[string]string{
+ "username1": "password1",
+ "username2": "password2",
+ }))
+
+ h.GET("/basicAuth", func(ctx context.Context, c *app.RequestContext) {
+ c.String(consts.StatusOK, "hello hertz")
+ })
+
+ h.Spin()
+}
+```
+
+## 配置
+
+Hertz 通过使用中间件可以实现让网页浏览器或其他客户端程序在请求时提供用户名和密码形式作为身份凭证进行登录验证,Hertz
+提供了两种函数帮助用户快速使用基本认证(Basic access authentication)功能,用户可以根据业务场景自行选择不同的函数进行使用。
+
+上述**示例代码**中,只使用了基本配置函数 `BasicAuth`,扩展配置函数 `BasicAuthForRealm` 的参数配置项如下:
+
+**注意:** `BasicAuth` 是对 `BasicAuthForRealm` 的封装并提供了默认配置项。
+
+| 参数 | 介绍 |
+|----------|--------------------------------------------------------|
+| accounts | `Accounts` 被定义为 `map[string]string` 类型,以键值对的形式存储用户名和密码 |
+| realm | 安全域字符串,默认值为 `Authorization Required` |
+| userKey | 认证通过后在上下文中设置的用户名所对应的键值,默认值为 `user` |
+
+### BasicAuth
+
+`basic_auth` 中间件提供了 `BasicAuth` 用于在客户端对服务端发起请求时进行用户名密码形式的身份验证。
+
+函数签名:
+
+```go
+func BasicAuth(accounts Accounts) app.HandlerFunc
+```
+
+示例代码:
+
+```go
+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"))
+
+ h.Use(basic_auth.BasicAuth(map[string]string{
+ "username1": "password1",
+ "username2": "password2",
+ }))
+
+ h.GET("/basicAuth", func(ctx context.Context, c *app.RequestContext) {
+ c.String(consts.StatusOK, "hello hertz")
+ })
+
+ h.Spin()
+}
+```
+
+### BasicAuthForRealm
+
+`basic_auth` 中间件提供了 `BasicAuthForRealm` 用于在使用 `BasicAuth` 进行身份验证的基础上提供更多例如 Realm 等的扩展配置。
+
+函数签名:
+
+```go
+func BasicAuthForRealm(accounts Accounts, realm, userKey string) app.HandlerFunc
+```
+
+示例代码:
+
+```go
+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"))
+
+ // your-realm: 安全域字符串,本例中会以 Www-Authenticate: Basic realm="your-realm" 的形式保存在响应头中
+ // your-userKey: 认证通过后会以 userKey 为键 username 为值的形式设置在上下文中
+ h.Use(basic_auth.BasicAuthForRealm(map[string]string{
+ "username3": "password3",
+ "username4": "password4",
+ }, "your-realm", "your-userKey"))
+
+ h.GET("/basicAuth", func(ctx context.Context, c *app.RequestContext) {
+ c.String(consts.StatusOK, "hello hertz")
+ })
+
+ h.Spin()
+}
+```
+
+## 完整示例
+
+完整用法示例详见 [example](https://github.com/cloudwego/hertz-examples/blob/main/middleware/basicauth/main.go)
+
+
+---
+title: "JWT 认证"
+date: 2022-06-09
+weight: 3
+keywords: ["JWT 认证", "JSON Web Token", "JWT"]
+description: "Hertz 提供了 jwt 的实现。"
+
+---
+
+JSON Web Token(JWT)是一个轻量级的认证规范,这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息。其本质是一个
+token,是一种紧凑的 URL 安全方法,用于在网络通信的双方之间传递。
+Hertz 也提供了 jwt 的 [实现](https://github.com/hertz-contrib/jwt) ,参考了 gin
+的 [实现](https://github.com/appleboy/gin-jwt) 。
+
+## 安装
+
+```shell
+go get github.com/hertz-contrib/jwt
+```
+
+## 示例代码
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "time"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/cloudwego/hertz/pkg/common/utils"
+ "github.com/hertz-contrib/jwt"
+)
+
+type login struct {
+ Username string `form:"username,required" json:"username,required"`
+ Password string `form:"password,required" json:"password,required"`
+}
+
+var identityKey = "id"
+
+func PingHandler(c context.Context, ctx *app.RequestContext) {
+ user, _ := ctx.Get(identityKey)
+ ctx.JSON(200, utils.H{
+ "message": fmt.Sprintf("username:%v", user.(*User).UserName),
+ })
+}
+
+// User demo
+type User struct {
+ UserName string
+ FirstName string
+ LastName string
+}
+
+func main() {
+ h := server.Default()
+
+ // the jwt middleware
+ authMiddleware, err := jwt.New(&jwt.HertzJWTMiddleware{
+ Realm: "test zone",
+ Key: []byte("secret key"),
+ Timeout: time.Hour,
+ MaxRefresh: time.Hour,
+ IdentityKey: identityKey,
+ PayloadFunc: func(data interface{}) jwt.MapClaims {
+ if v, ok := data.(*User); ok {
+ return jwt.MapClaims{
+ identityKey: v.UserName,
+ }
+ }
+ return jwt.MapClaims{}
+ },
+ IdentityHandler: func(ctx context.Context, c *app.RequestContext) interface{} {
+ claims := jwt.ExtractClaims(ctx, c)
+ return &User{
+ UserName: claims[identityKey].(string),
+ }
+ },
+ Authenticator: func(ctx context.Context, c *app.RequestContext) (interface{}, error) {
+ var loginVals login
+ if err := c.BindAndValidate(&loginVals); err != nil {
+ return "", jwt.ErrMissingLoginValues
+ }
+ userID := loginVals.Username
+ password := loginVals.Password
+
+ if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") {
+ return &User{
+ UserName: userID,
+ LastName: "Hertz",
+ FirstName: "CloudWeGo",
+ }, nil
+ }
+
+ return nil, jwt.ErrFailedAuthentication
+ },
+ Authorizator: func(data interface{}, ctx context.Context, c *app.RequestContext) bool {
+ if v, ok := data.(*User); ok && v.UserName == "admin" {
+ return true
+ }
+
+ return false
+ },
+ Unauthorized: func(ctx context.Context, c *app.RequestContext, code int, message string) {
+ c.JSON(code, map[string]interface{}{
+ "code": code,
+ "message": message,
+ })
+ },
+ })
+ if err != nil {
+ log.Fatal("JWT Error:" + err.Error())
+ }
+
+ // When you use jwt.New(), the function is already automatically called for checking,
+ // which means you don't need to call it again.
+ errInit := authMiddleware.MiddlewareInit()
+
+ if errInit != nil {
+ log.Fatal("authMiddleware.MiddlewareInit() Error:" + errInit.Error())
+ }
+
+ h.POST("/login", authMiddleware.LoginHandler)
+
+ h.NoRoute(authMiddleware.MiddlewareFunc(), func(ctx context.Context, c *app.RequestContext) {
+ claims := jwt.ExtractClaims(ctx, c)
+ log.Printf("NoRoute claims: %#v\n", claims)
+ c.JSON(404, map[string]string{"code": "PAGE_NOT_FOUND", "message": "Page not found"})
+ })
+
+ auth := h.Group("/auth")
+ // Refresh time can be longer than token timeout
+ auth.GET("/refresh_token", authMiddleware.RefreshHandler)
+ auth.Use(authMiddleware.MiddlewareFunc())
+ {
+ auth.GET("/ping", PingHandler)
+ }
+
+ h.Spin()
+}
+```
+
+### 提示
+
+因为 JWT 的核心是**认证**与**授权**,所以在使用 Hertz 的 jwt 扩展时,不仅需要为 `/login`
+接口绑定认证逻辑 `authMiddleware.LoginHandler`。
+
+还要以中间件的方式,为需要授权访问的路由组注入授权逻辑 `authMiddleware.MiddlewareFunc()`。
+
+## 配置
+
+Hertz 通过使用中间件,为路由请求提供了 `jwt` 的校验功能。其中 `HertzJWTMiddleware` 结构定义了 `jwt`
+配置信息,并提供了默认配置,用户也可以依据业务场景进行定制。
+
+上述**示例代码**中,只传入了**两项必要的**自定义的配置。关于 `HertzJWTMiddleware` 的更多常用配置如下:
+
+| 参数 | 介绍 |
+|:------------------------------|:-----------------------------------------------------------------------------------------|
+| `Realm` | 用于设置所属领域名称,默认为 `hertz jwt` |
+| `SigningAlgorithm` | 用于设置签名算法,可以是 HS256、HS384、HS512、RS256、RS384 或者 RS512 等,默认为 `HS256` |
+| `Key` | 用于设置签名密钥(必要配置) |
+| `KeyFunc` | 用于设置获取签名密钥的回调函数,设置后 token 解析时将从 `KeyFunc` 获取 `jwt` 签名密钥 |
+| `Timeout` | 用于设置 token 过期时间,默认为一小时 |
+| `MaxRefresh` | 用于设置最大 token 刷新时间,允许客户端在 `TokenTime` + `MaxRefresh` 内刷新 token 的有效时间,追加一个 `Timeout` 的时长 |
+| `Authenticator` | 用于设置登录时认证用户信息的函数(必要配置) |
+| `Authorizator` | 用于设置授权已认证的用户路由访问权限的函数 |
+| `PayloadFunc` | 用于设置登陆成功后为向 token 中添加自定义负载信息的函数 |
+| `Unauthorized` | 用于设置 jwt 验证流程失败的响应函数 |
+| `LoginResponse` | 用于设置登录的响应函数 |
+| `LogoutResponse` | 用于设置登出的响应函数 |
+| `RefreshResponse` | 用于设置 token 有效时长刷新后的响应函数 |
+| `IdentityHandler` | 用于设置获取身份信息的函数,默认与 `IdentityKey` 配合使用 |
+| `IdentityKey` | 用于设置检索身份的键,默认为 `identity` |
+| `TokenLookup` | 用于设置 token 的获取源,可以选择 `header`、`query`、`cookie`、`param`、`form`,默认为 `header:Authorization` |
+| `TokenHeadName` | 用于设置从 header 中获取 token 时的前缀,默认为 `Bearer` |
+| `WithoutDefaultTokenHeadName` | 用于设置 `TokenHeadName` 为空,默认为 `false` |
+| `TimeFunc` | 用于设置获取当前时间的函数,默认为 `time.Now()` |
+| `HTTPStatusMessageFunc` | 用于设置 jwt 校验流程发生错误时响应所包含的错误信息 |
+| `SendCookie` | 用于设置 token 将同时以 cookie 的形式返回,下列 cookie 相关配置生效的前提是该值为 `true`,默认为 `false` |
+| `CookieMaxAge` | 用于设置 cookie 的有效期,默认为 `Timeout` 定义的一小时 |
+| `SecureCookie` | 用于设置允许不通过 HTTPS 传递 cookie 信息,默认为 `false` |
+| `CookieHTTPOnly` | 用于设置允许客户端访问 cookie 以进行开发,默认为 `false` |
+| `CookieDomain` | 用于设置 cookie 所属的域,默认为空 |
+| `SendAuthorization` | 用于设置为所有请求的响应头添加授权的 token 信息,默认为 `false` |
+| `DisabledAbort` | 用于设置在 jwt 验证流程出错时,禁止请求上下文调用 `abort()`,默认为 `false` |
+| `CookieName` | 用于设置 cookie 的 name 值 |
+| `CookieSameSite` | 用于设置使用 `protocol.CookieSameSite` 声明的参数设置 cookie 的 SameSite 属性值 |
+| `ParseOptions` | 用于设置使用 `jwt.ParserOption` 声明的函数选项式参数配置 `jwt.Parser` 的属性值 |
+
+### Key
+
+用于设置 `token` 的签名密钥。
+
+示例代码:
+
+```go
+authMiddleware, err := jwt.New(&jwt.HertzJWTMiddleware{
+ Key: []byte("secret key"),
+})
+```
+
+### KeyFunc
+
+程序执行时 `KeyFunc` 作为 `jwt.Parse()` 的参数,负责为 token 解析提供签名密钥,通过自定义 `KeyFunc` 的逻辑,可以在解析
+token 之前完成一些自定义的操作,如:校验签名方法的有效性、选择对应的签名密钥、将 token 存入请求上下文等。
+
+函数签名:
+
+```go
+func(t *jwt.Token) (interface{}, error)
+```
+
+默认处理逻辑如下:
+
+```go
+authMiddleware, err := jwt.New(&jwt.HertzJWTMiddleware{
+ KeyFunc: func(t *jwt.Token) (interface{}, error) {
+ if jwt.GetSigningMethod(mw.SigningAlgorithm) != t.Method {
+ return nil, ErrInvalidSigningAlgorithm
+ }
+ if mw.usingPublicKeyAlgo() {
+ return mw.pubKey, nil
+ }
+
+ // save token string if valid
+ c.Set("JWT_TOKEN", token)
+
+ return mw.Key, nil
+ },
+})
+```
+
+### Authenticator
+
+配合 `HertzJWTMiddleware.LoginHandler` 使用,登录时触发,用于认证用户的登录信息。
+
+函数签名:
+
+```go
+func(ctx context.Context, c *app.RequestContext) (interface{}, error)
+```
+
+示例代码:
+
+```go
+authMiddleware, err := jwt.New(&jwt.HertzJWTMiddleware{
+ Authenticator: func(ctx context.Context, c *app.RequestContext) (interface{}, error) {
+ var loginVals login
+ if err := c.BindAndValidate(&loginVals); err != nil {
+ return "", jwt.ErrMissingLoginValues
+ }
+ userID := loginVals.Username
+ password := loginVals.Password
+
+ if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") {
+ return &User{
+ UserName: userID,
+ LastName: "Hertz",
+ FirstName: "CloudWeGo",
+ }, nil
+ }
+
+ return nil, jwt.ErrFailedAuthentication
+ },
+})
+```
+
+### Authorizator
+
+用于设置已认证的用户路由访问权限的函数,如下函数通过验证用户名是否为 `admin`,从而判断是否有访问路由的权限。
+
+如果没有访问权限,则会触发 `Unauthorized` 参数中声明的 jwt 流程验证失败的响应函数。
+
+函数签名:
+
+```go
+func(data interface{}, ctx context.Context, c *app.RequestContext) bool
+```
+
+示例代码:
+
+```go
+authMiddleware, err := jwt.New(&jwt.HertzJWTMiddleware{
+ Authorizator: func(data interface{}, ctx context.Context, c *app.RequestContext) bool {
+ if v, ok := data.(*User); ok && v.UserName == "admin" {
+ return true
+ }
+
+ return false
+ }
+})
+
+```
+
+### PayloadFunc
+
+用于设置登录时为 `token` 添加自定义负载信息的函数,如果不传入这个参数,则 `token` 的 `payload` 部分默认存储 `token`
+的过期时间和创建时间,如下则额外存储了用户名信息。
+
+函数签名:
+
+```go
+func(data interface{}) jwt.MapClaims
+```
+
+示例代码:
+
+```go
+authMiddleware, err := jwt.New(&jwt.HertzJWTMiddleware{
+ PayloadFunc: func(data interface{}) jwt.MapClaims {
+ if v, ok := data.(*User); ok {
+ return jwt.MapClaims{
+ identityKey: v.UserName,
+ }
+ }
+ return jwt.MapClaims{}
+ },
+})
+```
+
+### IdentityHandler
+
+`IdentityHandler` 作用在登录成功后的每次请求中,用于设置从 token
+提取用户信息的函数。这里提到的用户信息在用户成功登录时,触发 `PayloadFunc` 函数,已经存入 token 的负载部分。
+
+具体流程:通过在 `IdentityHandler` 内配合使用 `identityKey`,将存储用户信息的 token 从请求上下文中取出并提取需要的信息,封装成
+User 结构,以 `identityKey` 为 key,User 为 value 存入请求上下文当中以备后续使用。
+
+函数签名:
+
+```go
+func(ctx context.Context, c *app.RequestContext) interface{}
+```
+
+示例代码:
+
+```go
+authMiddleware, err := jwt.New(&jwt.HertzJWTMiddleware{
+ IdentityHandler: func(ctx context.Context, c *app.RequestContext) interface{} {
+ claims := jwt.ExtractClaims(ctx, c)
+ return &User{
+ UserName: claims[identityKey].(string),
+ }
+ }
+})
+```
+
+### Unauthorized
+
+用于设置 jwt 授权失败后的响应函数,如下函数将参数列表中的错误码和错误信息封装成 json 响应返回。
+
+函数签名:
+
+```go
+func(ctx context.Context, c *app.RequestContext, code int, message string)
+```
+
+默认处理逻辑如下:
+
+```go
+authMiddleware, err := jwt.New(&jwt.HertzJWTMiddleware{
+ Unauthorized: func(ctx context.Context, c *app.RequestContext, code int, message string) {
+ c.JSON(code, map[string]interface{}{
+ "code": code,
+ "message": message,
+ })
+ }
+})
+```
+
+### LoginResponse
+
+用于设置登录的响应函数,作为 `LoginHandler` 的响应结果。
+
+函数签名:
+
+```go
+func(ctx context.Context, c *app.RequestContext, code int, token string, expire time.Time)
+```
+
+默认处理逻辑如下:
+
+```go
+authMiddleware, err := jwt.New(&jwt.HertzJWTMiddleware{
+ LoginResponse: func(ctx context.Context, c *app.RequestContext, code int, token string, expire time.Time) {
+ c.JSON(http.StatusOK, map[string]interface{}{
+ "code": http.StatusOK,
+ "token": token,
+ "expire": expire.Format(time.RFC3339),
+ })
+ }
+})
+// 在 LoginHandler 内调用
+h.POST("/login", authMiddleware.LoginHandler)
+```
+
+### LogoutResponse
+
+用于设置登出的响应函数,作为 `LogoutHandler` 的响应结果。
+
+函数签名:
+
+```go
+func(ctx context.Context, c *app.RequestContext, code int)
+```
+
+默认处理逻辑如下:
+
+```go
+authMiddleware, err := jwt.New(&jwt.HertzJWTMiddleware{
+ LogoutResponse: func(ctx context.Context, c *app.RequestContext, code int) {
+ c.JSON(http.StatusOK, map[string]interface{}{
+ "code": http.StatusOK,
+ })
+ }
+})
+// 在 LogoutHandler 内调用
+h.POST("/logout", authMiddleware.LogoutHandler)
+```
+
+### RefreshResponse
+
+用于设置 token 有效时长刷新后的响应函数,作为 `RefreshHandler` 的响应结果。
+
+函数签名:
+
+```go
+func(ctx context.Context, c *app.RequestContext, code int, token string, expire time.Time)
+```
+
+默认处理逻辑如下:
+
+```go
+authMiddleware, err := jwt.New(&jwt.HertzJWTMiddleware{
+ RefreshResponse: func(ctx context.Context, c *app.RequestContext, code int, token string, expire time.Time) {
+ c.JSON(http.StatusOK, map[string]interface{}{
+ "code": http.StatusOK,
+ "token": token,
+ "expire": expire.Format(time.RFC3339),
+ })
+ },
+})
+// 在 RefreshHandler 内调用
+auth.GET("/refresh_token", authMiddleware.RefreshHandler)
+```
+
+### TokenLookup
+
+通过键值对的形式声明 token 的获取源,有四种可选的方式,默认值为 header:Authorization,如果同时声明了多个数据源则以 `,`
+为分隔线,第一个满足输入格式的数据源将被选择,其余忽略。
+
+示例代码:
+
+```go
+authMiddleware, err := jwt.New(&jwt.HertzJWTMiddleware{
+ // - "header:"
+ // - "query:"
+ // - "cookie:"
+ // - "param:"
+ // - "form:"
+ TokenLookup: "header: Authorization, query: token, cookie: jwt"
+})
+```
+
+### TimeFunc
+
+用于设置获取当前时间的函数,默认为 time.Now(),在 jwt 校验过程中,关于 token 的有效期的验证需要以 token
+创建时间为起点,`TimeFunc` 提供了 jwt 获取当前时间的函数,可以选择覆盖这个默认配置,应对一些时区不同的情况。
+
+函数签名:
+
+```go
+func() time.Time
+```
+
+默认处理逻辑如下:
+
+```go
+authMiddleware, err := jwt.New(&jwt.HertzJWTMiddleware{
+ TimeFunc: func() time.Time {
+ return time.Now()
+ }
+})
+```
+
+### HTTPStatusMessageFunc
+
+一旦 jwt 校验流程产生错误,如 jwt 认证失败、token 鉴权失败、刷新 token 有效时长失败等,对应 error
+将以参数的形式传递给 `HTTPStatusMessageFunc`,由其提取出需要响应的错误信息,最终以 string 参数形式传递给 `Unauthorized`
+声明的 jwt 验证流程失败的响应函数返回。
+
+函数签名:
+
+```go
+func(e error, ctx context.Context, c *app.RequestContext) string
+```
+
+默认处理逻辑如下:
+
+```go
+authMiddleware, err := jwt.New(&jwt.HertzJWTMiddleware{
+ HTTPStatusMessageFunc: func(e error, ctx context.Context, c *app.RequestContext) string {
+ return e.Error()
+ }
+})
+```
+
+### Cookie
+
+cookie 相关的配置参数有八个,将 `SendCookie` 设置为 true、`TokenLookup` 设置为 cookie: jwt 后,token 将同时以 cookie
+的形式返回,并在接下来的请求中从 HTTP Cookie 获取。
+
+示例代码:
+
+```go
+authMiddleware, err := jwt.New(&jwt.HertzJWTMiddleware{
+ SendCookie: true,
+ TokenLookup: "cookie: jwt",
+ CookieMaxAge: time.Hour,
+ SecureCookie: false,
+ CookieHTTPOnly: false,
+ CookieDomain: ".test.com",
+ CookieName: "jwt-cookie",
+ CookieSameSite: protocol.CookieSameSiteDisabled,
+})
+```
+
+### ParseOptions
+
+利用 ParseOptions 可以开启相关配置有三个,分别为
+
+- `WithValidMethods`: 用于提供解析器将检查的签名算法,只有被提供的签名算法才被认为是有效的
+- `WithJSONNumber`: 用于配置底层 JSON 解析器使用 `UseNumber` 方法
+- `WithoutClaimsValidation`: 用于禁用 claims 验证
+
+示例代码:
+
+```go
+authMiddleware, err := jwt.New(&jwt.HertzJWTMiddleware{
+ ParseOptions: []jwt.ParserOption{
+ jwt.WithValidMethods([]string{"HS256"}),
+ jwt.WithJSONNumber(),
+ jwt.WithoutClaimsValidation(),
+ },
+})
+```
+
+## 完整示例
+
+完整用法示例详见 [example](https://github.com/cloudwego/hertz-examples/tree/main/bizdemo/hertz_jwt)
+
+
+---
+title: "Request ID"
+date: 2022-10-01
+weight: 9
+description: >
+keywords: ["Request ID", "X-Request-ID"]
+description: "Hertz 提供了可以对 `X-Request-ID` 进行操作的 Request ID 中间件。"
+
+---
+
+`X-Request-ID` 在 HTTP Headers 中是一种非标准响应字段,通常用于关联客户端和服务器之间的 HTTP 请求。
+Hertz 也提供了可以对 `X-Request-ID` 进行操作的 [Request ID 中间件](https://github.com/hertz-contrib/requestid),参考了
+gin 的 [实现](https://github.com/gin-contrib/requestid)。
+
+## 安装
+
+下载并安装
+
+```shell
+go get github.com/hertz-contrib/requestid
+```
+
+导入
+
+```go
+import "github.com/hertz-contrib/requestid"
+```
+
+## 示例代码
+
+```go
+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"
+ "github.com/hertz-contrib/requestid"
+)
+
+func main() {
+ h := server.Default()
+
+ h.Use(
+ // 自定义 request id 生成逻辑
+ requestid.New(
+ requestid.WithGenerator(func(ctx context.Context, c *app.RequestContext) string {
+ return "cloudwego.io"
+ }),
+ // 自定义 request id 响应头键值
+ requestid.WithCustomHeaderStrKey("Your-Customised-Key"),
+ ),
+ )
+
+ // Example ping request.
+ h.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
+ hlog.Info(string(c.Response.Header.Header()))
+ c.JSON(consts.StatusOK, utils.H{"ping": "pong"})
+ })
+
+ h.Spin()
+}
+```
+
+## 配置
+
+Hertz 通过使用中间件,可以在响应头中添加一个键为 `X-Request-ID` 的标识符,如果在请求头中设置了 `X-Request-ID`
+字段,则会在响应头中将 `X-Request-ID` 原样返回。
+Request ID 中间件提供了默认配置,用户也可以依据业务场景使用 `WithGenerator`,`WithCustomHeaderStrKey`,`WithHandler`
+函数对以下配置项进行定制。
+
+| 配置 | 介绍 |
+|------------------------|--------------------------------------|
+| WithGenerator | 定义生成 Request ID 的函数,默认生成 UUID 标识符 |
+| WithCustomHeaderStrKey | 定义 Request ID 的键值,默认为 `X-Request-ID` |
+| WithHandler | 定义 Request ID 的处理函数 |
+
+### 初始化 Request ID
+
+`requestid` 中间件提供了 `New` 用于在响应头添加 Request ID 字段。
+
+函数签名:
+
+```go
+func New(opts ...Option) app.HandlerFunc
+```
+
+示例代码:
+
+```go
+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"
+ "github.com/hertz-contrib/requestid"
+)
+
+func main() {
+ h := server.Default()
+
+ h.Use(
+ requestid.New(),
+ )
+
+ // Example ping request.
+ h.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
+ c.JSON(consts.StatusOK, utils.H{"ping": "pong"})
+ })
+
+ h.Spin()
+}
+```
+
+### 自定义 Request ID 键值
+
+`requestid` 中间件提供了 `WithCustomHeaderStrKey` 用于自定义 Request ID 键值。
+
+注意:如果需要在请求头中设置 `X-Request-ID`,则需要保持和自定义响应头键值一致。
+
+函数签名:
+
+```go
+func WithCustomHeaderStrKey(s HeaderStrKey) Option
+```
+
+示例代码:
+
+```go
+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"
+ "github.com/hertz-contrib/requestid"
+)
+
+func main() {
+ h := server.Default()
+
+ // define your own header to save request id here
+ h.Use(
+ requestid.New(
+ requestid.WithCustomHeaderStrKey("Your-Header-StrKey"),
+ ),
+ )
+
+ // Example ping request.
+ h.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
+ c.JSON(consts.StatusOK, utils.H{"ping": "pong"})
+ })
+
+ h.Spin()
+}
+```
+
+### 自定义 Request ID 值
+
+`requestid` 中间件提供了 `WithGenerator` 用于自定义 Request ID 值的生成。
+
+函数签名:
+
+```go
+func WithGenerator(g Generator) Option
+```
+
+示例代码:
+
+```go
+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"
+ "github.com/hertz-contrib/requestid"
+)
+
+func main() {
+ h := server.Default()
+
+ h.Use(
+ // define your own request id generator here
+ requestid.New(requestid.WithGenerator(func(ctx context.Context, c *app.RequestContext) string {
+ return "cloudwego.io"
+ })),
+ )
+
+ h.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
+ c.JSON(consts.StatusOK, utils.H{"ping": "pong"})
+ })
+
+ h.Spin()
+}
+```
+
+### 自定义 Request ID Handler
+
+`requestid` 中间件提供了 `WithHandler` 用于自定义 Request ID 的处理函数。
+
+函数签名:
+
+```go
+func WithHandler(handler Handler) Option
+```
+
+示例代码:
+
+```go
+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"
+ "github.com/hertz-contrib/requestid"
+)
+
+func main() {
+ h := server.Default()
+
+ var bar string
+
+ h.Use(
+ requestid.New(
+ requestid.WithGenerator(func(ctx context.Context, c *app.RequestContext) string {
+ return "hello"
+ }),
+ // define your request id handler here
+ requestid.WithHandler(func(ctx context.Context, c *app.RequestContext, requestID string) {
+ bar = requestID + " hertz"
+ }),
+ ),
+ )
+
+ h.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
+ c.JSON(consts.StatusOK, utils.H{
+ "ping": "pong",
+ "foo": bar, // hello hertz
+ })
+ })
+
+ h.Spin()
+}
+```
+
+### 获取 Request ID
+
+`requestid` 中间件提供了 `Get` 用于从请求头中获取 Request ID,它也支持获取使用 `requestid.WithCustomHeaderStrKey` 自定义
+Request ID 键值。
+
+函数签名:
+
+```go
+func Get(c *app.RequestContext) string
+```
+
+示例代码:
+
+```go
+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"
+ "github.com/hertz-contrib/requestid"
+)
+
+func main() {
+ h := server.Default()
+
+ h.Use(
+ requestid.New(requestid.WithGenerator(func(ctx context.Context, c *app.RequestContext) string {
+ return "cloudwego.io"
+ })),
+ )
+
+ // You may retrieve request id from header by calling requestid.Get
+ h.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
+ c.JSON(consts.StatusOK, utils.H{
+ "ping": "pong",
+ "request-id": requestid.Get(c),
+ })
+ })
+
+ h.Spin()
+}
+```
+
+## 完整示例
+
+完整用法示例详见 [example](https://github.com/hertz-contrib/requestid/tree/main/example)
+
+
+---
+title: "Sentry"
+date: 2022-11-25
+weight: 11
+keywords: ["Sentry", "实时错误监控"]
+description: "Hertz 通过使用中间件 hertzsentry,整合了 Sentry-Go 的 SDK。"
+
+---
+
+Sentry 是一个开源的实时错误监控项目,支持很多平台,包括 Web 前端、服务器端、移动端和游戏端等。Hertz
+通过使用中间件 [hertzsentry](https://github.com/hertz-contrib/hertzsentry)
+,整合了 [Sentry-Go](https://docs.sentry.io/platforms/go/) 的 SDK。提供了一些统一的接口,帮助用户获得 sentry hub 和报告错误信息。
+
+注意:信息上报功能的实现,依旧是以 Sentry 的 Go SDK 为载体。
+
+这个项目参考了 [fibersentry](https://github.com/gofiber/contrib/tree/main/fibersentry) 的实现。
+
+## 安装
+
+```shell
+go get github.com/hertz-contrib/hertzsentry
+```
+
+## 示例代码
+
+```go
+package main
+
+import (
+ "context"
+ "log"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/getsentry/sentry-go"
+ "github.com/hertz-contrib/hertzsentry"
+)
+
+var yourDsn = ""
+
+func main() {
+ // set interval to 0 means using fs-watching mechanism.
+ h := server.Default(server.WithAutoReloadRender(true, 0))
+
+ // init sentry
+ if err := sentry.Init(sentry.ClientOptions{
+ // The DSN to use. If the DSN is not set, the client is effectively disabled.
+ Dsn: yourDsn,
+ // Before send callback.
+ BeforeSend: func(event *sentry.Event, hint *sentry.EventHint) *sentry.Event {
+ return event
+ },
+ // In debug mode, the debug information is printed to stdout to help you understand what
+ // sentry is doing.
+ Debug: true,
+ // Configures whether SDK should generate and attach stacktraces to pure capture message calls.
+ AttachStacktrace: true,
+ }); err != nil {
+ log.Fatal("sentry init failed")
+ }
+
+ // use sentry middleware and config with your requirements.
+ // attention! you should use sentry handler after recovery.Recovery()
+ h.Use(hertzsentry.NewSentry(
+ hertzsentry.WithSendRequest(true),
+ hertzsentry.WithRePanic(true),
+ ))
+
+ h.GET("/hello", func(c context.Context, ctx *app.RequestContext) {
+ // use GetHubFromContext to get the hub
+ if hub := hertzsentry.GetHubFromContext(ctx); hub != nil {
+ hub.WithScope(func(scope *sentry.Scope) {
+ scope.SetTag("hertz", "CloudWeGo Hertz")
+ scope.SetLevel(sentry.LevelDebug)
+ hub.CaptureMessage("Just for debug")
+ })
+ }
+ ctx.SetStatusCode(0)
+ })
+
+ h.Spin()
+}
+```
+
+## 配置
+
+Hertz 通过使用中间件,整合了 Sentry-Go 的功能。其中 `hertzsentry.options` 结构定义了 hertzsentry
+的配置信息,并提供了默认配置,用户也可以依据业务场景进行定制。
+
+| 参数 | 介绍 |
+|-----------------|------------------------------------------------------------------------------------------------------------|
+| rePanic | 用于配置 Sentry 在恢复后是否要再次 panic。如果使用了 Recover 中间件,则设置为 true,默认为 false。 |
+| waitForDelivery | 用于配置是否要在继续处理响应之前阻止请求并清空缓存区(**只有异步传输时才真正意义上有清空缓存区的操作**)。如果使用 Recover 中间件,跳过这个选项或将其设置为 false 是安全的,默认为 false。 |
+| sendRequest | 用于配置在捕获 sentry 事件时是否要添加当前的请求头信息,默认为 false。 |
+| sendBody | 用于配置在捕获 sentry 事件时是否要添加当前的请求正文信息,默认为 false。 |
+| timeout | 用于配置 sentry 事件传递请求的超时时长,默认为 2 秒。 |
+
+### Flush(Go-Sentry)
+
+Go-Sentry 可以选择异步或者同步发送捕获的信息,选择异步发送时,Flush 用于清空缓存区,**同步发送时没有缓存的概念**,直接返回
+true。
+
+触发 Flush 时等待,直到底层传输系统向 Sentry 服务器发送所有事件完毕,返回 true。但最多等待给定的超时时间,如果达到超时,则返回
+false。在这种情况下,有些事件可能没有被发送。(这两种情况下缓存区都将被清空)
+
+应该在终止程序之前调用 Flush,以避免无意中丢弃事件。
+
+不要在每次调用 CaptureEvent、CaptureException 或 CaptureMessage 后不加区分地调用 Flush。相反,要想让 SDK
+在网络上同步发送事件,请将其配置为使用 HTTPSyncTransport。
+
+函数签名:
+
+```go
+func (hub *Hub) Flush(timeout time.Duration) bool
+```
+
+Flush 调用逻辑如下:
+
+```go
+func (hub *Hub) Flush(timeout time.Duration) bool {
+ client := hub.Client()
+
+ if client == nil {
+ return false
+ }
+
+ // client 的传输方式为异步或同步(需提前配置 Go-Sentry 的初始化参数)
+ return client.Flush(timeout)
+}
+```
+
+
+---
+title: "Recovery"
+date: 2022-12-15
+weight: 2
+keywords: ["Recovery", "panic 恢复"]
+description: "Recovery 中间件是 Hertz 框架预置的中间件,为 Hertz 框架提供 panic 恢复的功能。"
+
+---
+
+Recovery 中间件是 Hertz 框架预置的中间件,使用 `server.Default()` 可以默认注册该中间件,为 Hertz 框架提供 panic 恢复的功能。
+
+如果你不使用`server.Default()`,你也可以通过以下方式注册 Recovery 中间件:
+
+```go
+h := server.New()
+h.Use(recovery.Recovery())
+```
+
+Recovery 中间件会恢复 Hertz 框架运行中的任何 panic,在 panic 发生之后,Recover 中间件会默认打印出 panic
+的时间、内容和堆栈信息,同时通过`*app.RequestContext`将返回响应的状态码设置成 500。
+
+## 导入
+
+```go
+import "github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery"
+```
+
+## 示例代码
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+)
+
+func main() {
+ h := server.Default(server.WithHostPorts(":8080"))
+ h.GET("/test", func(ctx context.Context, c *app.RequestContext) {
+ panic("test")
+ c.String(http.StatusOK, "test interface")
+ })
+ h.Spin()
+}
+```
+
+## 配置
+
+Recovery 中间件提供了默认的 panic 处理函数`defaultRecoveryHandler()`。
+
+同时你也可以通过`WithRecoveryHandler()`函数来自定义出现 panic 后的处理函数,函数签名如下:
+
+```go
+func WithRecoveryHandler(f func(c context.Context, ctx *app.RequestContext, err interface{}, stack []byte))
+```
+
+如果你在发生 panic 之后希望能够获取客户端信息,示例代码如下:
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "github.com/cloudwego/hertz/pkg/app/middlewares/server/recovery"
+ "github.com/cloudwego/hertz/pkg/common/hlog"
+ "github.com/cloudwego/hertz/pkg/protocol/consts"
+ "net/http"
+ "time"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+)
+
+func MyRecoveryHandler(c context.Context, ctx *app.RequestContext, err interface{}, stack []byte) {
+ hlog.SystemLogger().CtxErrorf(c, "[Recovery] err=%v\nstack=%s", err, stack)
+ hlog.SystemLogger().Infof("Client: %s", ctx.Request.Header.UserAgent())
+ ctx.AbortWithStatus(consts.StatusInternalServerError)
+}
+
+func main() {
+ h := server.New()
+ h.Use(recovery.Recovery(recovery.WithRecoveryHandler(MyRecoveryHandler)))
+ h.GET("/ping", func(ctx context.Context, c *app.RequestContext) {
+ panic("test")
+ c.String(http.StatusOK, "pong "+fmt.Sprint(time.Now().Unix()))
+ })
+ h.Spin()
+}
+```
+
+
+---
+title: "Pprof"
+date: 2022-09-24
+weight: 7
+keywords: ["pprof", "性能分析"]
+description: "Hertz 提供了 pprof 扩展,帮助用户对 Hertz 项目进行性能分析。"
+
+---
+
+
+Hertz 提供了 [pprof](https://github.com/hertz-contrib/pprof) 扩展,帮助用户对 Hertz
+项目进行性能分析,[pprof](https://github.com/hertz-contrib/pprof)
+扩展的实现参考了 [Gin](https://github.com/gin-contrib/pprof) 的实现。
+
+## 安装
+
+```shell
+go get github.com/hertz-contrib/pprof
+```
+
+## 示例代码
+
+```go
+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"
+ "github.com/hertz-contrib/pprof"
+)
+
+func main() {
+ h := server.Default()
+
+ pprof.Register(h)
+
+ h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
+ ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
+ })
+
+ h.Spin()
+}
+```
+
+## 配置
+
+### PrefixOptions
+
+`pprof` 的默认前缀为 `debug/pprof`,即用户在 Hertz 项目中注册并使用 `pprof` 后,用户可以通过访问
+`localhost:8888/debug/pprof` 来查看当前项目的采样信息。
+此外,用户可以在注册 `pprof` 时指定自定义前缀。
+
+函数签名如下:
+
+```go
+Register(r *server.Hertz, prefixOptions ...string)
+```
+
+示例代码:
+
+```go
+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"
+ "github.com/hertz-contrib/pprof"
+)
+
+func main() {
+ h := server.Default()
+
+ // default is "debug/pprof"
+ pprof.Register(h, "dev/pprof")
+
+ h.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
+ ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
+ })
+
+ h.Spin()
+}
+```
+
+### RouteRegister
+
+`pprof` 不仅可以注册到 Hertz 对象上,还可以注册到路由组(RouterGroup)上。
+
+函数签名如下:
+
+```go
+RouteRegister(rg *route.RouterGroup, prefixOptions ...string)
+```
+
+本方式注册后的 `pprof` 前缀为路由组的前缀与自定义前缀拼接后的结果。
+
+* 用户不指定前缀,注册后的 `pprof` 的前缀为路由组的前缀与默认前缀 `/debug/pprof`
+ 拼接后的结果,即为 `/xxx/debug/pprof`(`xxx` 为路由组前缀);
+* 用户指定前缀,注册后的 `pprof` 的前缀为路由组的的前缀与自定义前缀拼接后的结果,比如下文示例中注册后的 `pprof`
+ 前缀为 `/admin/pprof`。
+
+示例代码:
+
+```go
+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"
+ "github.com/hertz-contrib/pprof"
+)
+
+func main() {
+ h := server.Default()
+
+ pprof.Register(h)
+
+ adminGroup := h.Group("/admin")
+
+ adminGroup.GET("/ping", func(c context.Context, ctx *app.RequestContext) {
+ ctx.JSON(consts.StatusOK, utils.H{"ping": "pong"})
+ })
+
+ pprof.RouteRegister(adminGroup, "pprof")
+
+ h.Spin()
+}
+```
+
+## 查看 pprof 采样信息
+
+### 通过浏览器查看
+
+通过浏览器访问 `localhost:8888/debug/pprof`
+
+* Hertz 端口号默认为 8888
+* pprof 默认地址前缀为 `debug/pprof`
+* 端口号和访问路由与用户实际端口号和 `pprof` 前缀一致
+
+### 通过 `go tool pprof` 查看
+
+使用 `go tool pprof` 工具查看堆栈采样信息:
+
+```bash
+go tool pprof http://localhost:8888/debug/pprof/heap
+```
+
+使用 `go tool pprof` 工具查看 CPU 采样信息:
+
+```bash
+go tool pprof http://localhost:8888/debug/pprof/profile
+```
+
+> 默认采样时间为 30s,可通过查询字符串来自定义采样时间:
+
+```bash
+go tool pprof http://localhost:8888/debug/pprof/profile?seconds=10
+```
+
+使用 `go tool pprof` 工具查看 go 协程阻塞信息:
+
+```bash
+go tool pprof http://localhost:8888/debug/pprof/block
+```
+
+获取执行 trace 信息:
+
+```bash
+wget http://localhost:8888/debug/pprof/trace?seconds=5
+```
+
+### 通过 `go tool pprof` 查看火焰图
+
+安装 [graphviz](http://www.graphviz.org/download/)
+
+```bash
+go tool pprof -http :8080 localhost:8888/debug/pprof/profile?seconds=10
+```
+
+完整用法示例详见 [example](https://github.com/hertz-contrib/pprof/tree/main/example)
+
+
+---
+title: "Paseto"
+date: 2023-05-08
+weight: 16
+keywords: ["Paseto", "JOSE", "JWT", "JWE", "JWS"]
+description: "这是为 Hertz 实现的 PASETO 中间件。"
+---
+
+Paseto 拥有你喜欢 JOSE 的一切(JWT、JWE、JWS),没有困扰 JOSE
+标准的 [许多设计缺陷](https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-bad-standard-that-everyone-should-avoid)。
+
+这是为 [Hertz](https://github.com/cloudwego/hertz) 实现的 PASETO 中间件。
+
+## 安装
+
+```shell
+go get github.com/hertz-contrib/paseto
+```
+
+## 示例代码
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+
+ "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/common/hlog"
+ "github.com/cloudwego/hertz/pkg/protocol"
+ "github.com/hertz-contrib/paseto"
+)
+
+func performRequest() {
+ time.Sleep(time.Second)
+ c, _ := client.NewClient()
+ req, resp := protocol.AcquireRequest(), protocol.AcquireResponse()
+ req.SetRequestURI("http://127.0.0.1:8080/paseto")
+
+ req.SetMethod("GET")
+ _ = c.Do(context.Background(), req, resp)
+ fmt.Printf("get token: %s\n", resp.Body())
+
+ req.SetMethod("POST")
+ req.SetHeader("Authorization", string(resp.Body()))
+ _ = c.Do(context.Background(), req, resp)
+ fmt.Printf("Authorization response :%s", resp.Body())
+}
+
+func main() {
+ h := server.New(server.WithHostPorts(":8080"))
+ h.GET("/paseto", func(c context.Context, ctx *app.RequestContext) {
+ now := time.Now()
+ genTokenFunc := paseto.DefaultGenTokenFunc()
+ token, err := genTokenFunc(&paseto.StandardClaims{
+ Issuer: "cwg-issuer",
+ ExpiredAt: now.Add(time.Hour),
+ NotBefore: now,
+ IssuedAt: now,
+ }, nil, nil)
+ if err != nil {
+ hlog.Error("generate token failed")
+ }
+ ctx.String(http.StatusOK, token)
+ })
+
+ h.POST("/paseto", paseto.New(), func(c context.Context, ctx *app.RequestContext) {
+ ctx.String(http.StatusOK, "token is valid")
+ })
+
+ go performRequest()
+
+ h.Spin()
+}
+```
+
+## 配置项
+
+| 配置 | 默认值 | 介绍 |
+|----------------|----------------------------------------------------------------------------------------------|-----------------------------------------------|
+| Next | [nil](https://github.com/hertz-contrib/paseto/blob/main/option.go#L88) | 用于设置一个函数,当返回 true 时跳过这个中间件 |
+| ErrorFunc | [输出日志并返回 401](https://github.com/hertz-contrib/paseto/blob/main/option.go#L89) | 用于设置一个在发生错误时执行的函数 |
+| SuccessHandler | [将声明保存到 app.RequestContext](https://github.com/hertz-contrib/paseto/blob/main/option.go#L94) | 用于设置一个函数,该函数在令牌有效时执行 |
+| KeyLookup | [header:Authorization](https://github.com/hertz-contrib/paseto/blob/main/option.go#L97) | 用于设置一个“<source>:<key>”形式的字符串,用于创建从请求中提取令牌的提取器 |
+| TokenPrefix | "" | 用于设置一个字符串,用于保存令牌查找的前缀 |
+| ParseFunc | [解析 V4 公共令牌](https://github.com/hertz-contrib/paseto/blob/main/option.go#L98) | 用于设置一个解析并验证令牌的函数 |
+
+### Next
+
+`WithNext` 设置一个函数来判断是否跳过这个中间件。
+
+函数签名:
+
+```go
+func WithNext(f NextHandler) Option
+```
+
+示例代码:
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "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/common/hlog"
+ "github.com/cloudwego/hertz/pkg/protocol"
+ "github.com/cloudwego/hertz/pkg/protocol/consts"
+ "github.com/hertz-contrib/paseto"
+)
+
+func performRequest() {
+ time.Sleep(time.Second)
+ c, _ := client.NewClient()
+ req, resp := protocol.AcquireRequest(), protocol.AcquireResponse()
+ req.SetRequestURI("http://127.0.0.1:8080/paseto")
+
+ req.SetMethod("GET")
+ _ = c.Do(context.Background(), req, resp)
+
+ req.SetMethod("POST")
+ req.SetHeader("Authorization", string(resp.Body()))
+ _ = c.Do(context.Background(), req, resp)
+ fmt.Printf("Authorization response :%s,because I have the token\n", resp.Body())
+
+ req.SetMethod("POST")
+ req.SetHeader("skip", "yes")
+ _ = c.Do(context.Background(), req, resp)
+ fmt.Printf("Authorization response :%s,because I trigger the nextFunc\n", resp.Body())
+
+ req.SetMethod("POST")
+ _ = c.Do(context.Background(), req, resp)
+ fmt.Printf("Authorization response :%s,because I don't have token nor trigger the nextFunc\n", resp.Body())
+}
+
+func main() {
+ h := server.New(server.WithHostPorts(":8080"))
+
+ next := func(ctx context.Context, c *app.RequestContext) bool {
+ return string(c.GetHeader("skip")) == "yes"
+ }
+ h.GET("/paseto", func(c context.Context, ctx *app.RequestContext) {
+ now := time.Now()
+ genTokenFunc := paseto.DefaultGenTokenFunc()
+ token, err := genTokenFunc(&paseto.StandardClaims{
+ Issuer: "cwg-issuer",
+ ExpiredAt: now.Add(time.Hour),
+ NotBefore: now,
+ IssuedAt: now,
+ }, nil, nil)
+ if err != nil {
+ hlog.Error("generate token failed")
+ }
+ ctx.String(consts.StatusOK, token)
+ })
+
+ h.POST("/paseto", paseto.New(paseto.WithNext(next)), func(c context.Context, ctx *app.RequestContext) {
+ ctx.String(consts.StatusOK, "token is valid")
+ })
+
+ go performRequest()
+
+ h.Spin()
+}
+```
+
+### ErrorFunc
+
+`WithErrorFunc` 设置 ErrorHandler。
+
+`ErrorHandler` 定义一个在发生错误时执行的函数。
+
+函数签名:
+
+```go
+func WithErrorFunc(f app.HandlerFunc) Option
+```
+
+示例代码:
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+
+ "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/common/hlog"
+ "github.com/cloudwego/hertz/pkg/common/utils"
+ "github.com/cloudwego/hertz/pkg/protocol"
+ "github.com/cloudwego/hertz/pkg/protocol/consts"
+ "github.com/hertz-contrib/paseto"
+)
+
+func performRequest() {
+ time.Sleep(time.Second)
+ c, _ := client.NewClient()
+ req, resp := protocol.AcquireRequest(), protocol.AcquireResponse()
+
+ req.SetMethod("GET")
+ req.SetRequestURI("http://127.0.0.1:8080/paseto/withsecret")
+ _ = c.Do(context.Background(), req, resp)
+
+ req.SetMethod("POST")
+ req.SetRequestURI("http://127.0.0.1:8080/paseto")
+ req.SetHeader("Authorization", string(resp.Body()))
+ _ = c.Do(context.Background(), req, resp)
+ fmt.Printf("Authorization response:%s\n", resp.Body())
+
+ req.SetMethod("GET")
+ req.SetRequestURI("http://127.0.0.1:8080/paseto/withnosecret")
+ _ = c.Do(context.Background(), req, resp)
+
+ req.SetMethod("POST")
+ req.SetRequestURI("http://127.0.0.1:8080/paseto")
+ req.SetHeader("Authorization", string(resp.Body()))
+ _ = c.Do(context.Background(), req, resp)
+ fmt.Printf("Authorization response:%s", resp.Body())
+}
+
+func main() {
+ h := server.New(server.WithHostPorts(":8080"))
+
+ handler := func(ctx context.Context, c *app.RequestContext) {
+ c.JSON(http.StatusUnauthorized, "invalid token")
+ c.Abort()
+ }
+
+ h.GET("/paseto/withsecret", func(c context.Context, ctx *app.RequestContext) {
+ now := time.Now()
+ genTokenFunc := paseto.DefaultGenTokenFunc()
+ token, err := genTokenFunc(&paseto.StandardClaims{
+ Issuer: "cwg-issuer",
+ ExpiredAt: now.Add(time.Hour),
+ NotBefore: now,
+ IssuedAt: now,
+ }, utils.H{
+ "secret1": "answer1",
+ }, nil)
+ if err != nil {
+ hlog.Error("generate token failed")
+ }
+ ctx.String(consts.StatusOK, token)
+ })
+
+ h.GET("/paseto/witherrorfunc", func(c context.Context, ctx *app.RequestContext) {
+ now := time.Now()
+ genTokenFunc := paseto.DefaultGenTokenFunc()
+ token, err := genTokenFunc(&paseto.StandardClaims{
+ Issuer: "cwg-issuer",
+ ExpiredAt: now.Add(time.Hour),
+ NotBefore: now,
+ IssuedAt: now,
+ }, nil, nil)
+ if err != nil {
+ hlog.Error("generate token failed")
+ }
+ ctx.String(consts.StatusOK, token)
+ })
+
+ h.POST("/paseto", paseto.New(paseto.WithErrorFunc(handler)), func(c context.Context, ctx *app.RequestContext) {
+ ctx.String(consts.StatusOK, "token is valid")
+ })
+
+ go performRequest()
+
+ h.Spin()
+}
+```
+
+### SuccessHandler
+
+`WithSuccessHandler` 设置处理已解析令牌的逻辑。
+
+函数签名:
+
+```go
+func WithSuccessHandler(f SuccessHandler) Option
+```
+
+示例代码:
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ gpaseto "aidanwoods.dev/go-paseto"
+ "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/common/hlog"
+ "github.com/cloudwego/hertz/pkg/common/utils"
+ "github.com/cloudwego/hertz/pkg/protocol"
+ "github.com/cloudwego/hertz/pkg/protocol/consts"
+ "github.com/hertz-contrib/paseto"
+)
+
+func performRequest() {
+ time.Sleep(time.Second)
+ c, _ := client.NewClient()
+ req, resp := protocol.AcquireRequest(), protocol.AcquireResponse()
+
+ req.SetMethod("GET")
+ req.SetRequestURI("http://127.0.0.1:8080/paseto/withsecret")
+ _ = c.Do(context.Background(), req, resp)
+
+ req.SetMethod("POST")
+ req.SetRequestURI("http://127.0.0.1:8080/paseto")
+ req.SetHeader("Authorization", string(resp.Body()))
+ _ = c.Do(context.Background(), req, resp)
+ fmt.Printf("Authorization response:%s\n", resp.Body())
+
+ req.SetMethod("GET")
+ req.SetRequestURI("http://127.0.0.1:8080/paseto/withnosecret")
+ _ = c.Do(context.Background(), req, resp)
+
+ req.SetMethod("POST")
+ req.SetRequestURI("http://127.0.0.1:8080/paseto")
+ req.SetHeader("Authorization", string(resp.Body()))
+ _ = c.Do(context.Background(), req, resp)
+ fmt.Printf("Authorization response:%s", resp.Body())
+}
+
+func main() {
+ h := server.New(server.WithHostPorts(":8080"))
+
+ handler := func(ctx context.Context, c *app.RequestContext, token *gpaseto.Token) {
+ var answer string
+ if err := token.Get("secret1", &answer); err != nil {
+ c.String(consts.StatusBadRequest, "you don't not the answer of secret1")
+ c.Abort()
+ }
+ }
+ h.GET("/paseto/withsecret", func(c context.Context, ctx *app.RequestContext) {
+ now := time.Now()
+ genTokenFunc := paseto.DefaultGenTokenFunc()
+ token, err := genTokenFunc(&paseto.StandardClaims{
+ Issuer: "cwg-issuer",
+ ExpiredAt: now.Add(time.Hour),
+ NotBefore: now,
+ IssuedAt: now,
+ }, utils.H{
+ "secret1": "answer1",
+ }, nil)
+ if err != nil {
+ hlog.Error("generate token failed")
+ }
+ ctx.String(consts.StatusOK, token)
+ })
+
+ h.GET("/paseto/withnosecret", func(c context.Context, ctx *app.RequestContext) {
+ now := time.Now()
+ genTokenFunc := paseto.DefaultGenTokenFunc()
+ token, err := genTokenFunc(&paseto.StandardClaims{
+ Issuer: "cwg-issuer",
+ ExpiredAt: now.Add(time.Hour),
+ NotBefore: now,
+ IssuedAt: now,
+ }, nil, nil)
+ if err != nil {
+ hlog.Error("generate token failed")
+ }
+ ctx.String(consts.StatusOK, token)
+ })
+
+ h.POST("/paseto", paseto.New(paseto.WithSuccessHandler(handler)), func(c context.Context, ctx *app.RequestContext) {
+ ctx.String(consts.StatusOK, "token is valid")
+ })
+
+ go performRequest()
+
+ h.Spin()
+}
+```
+
+### KeyLookup
+
+`WithKeyLookUp` 以“<source>:<key>”的形式设置一个字符串,用于创建从请求中提取令牌的“提取器”。
+
+函数签名:
+
+```go
+func WithKeyLookUp(lookup string) Option
+```
+
+示例代码:
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "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/common/hlog"
+ "github.com/cloudwego/hertz/pkg/protocol"
+ "github.com/cloudwego/hertz/pkg/protocol/consts"
+ "github.com/hertz-contrib/paseto"
+)
+
+func performRequest() {
+ time.Sleep(time.Second)
+ c, _ := client.NewClient()
+ req, resp := protocol.AcquireRequest(), protocol.AcquireResponse()
+ req.SetRequestURI("http://127.0.0.1:8080/paseto")
+
+ req.SetMethod("GET")
+ _ = c.Do(context.Background(), req, resp)
+ fmt.Printf("get token: %s\n", resp.Body())
+
+ req.SetMethod("POST")
+ req.SetBody([]byte("Authorization=" + string(resp.Body())))
+ req.SetHeader("Content-Type", "application/x-www-form-urlencoded")
+ _ = c.Do(context.Background(), req, resp)
+ fmt.Printf("Authorization response :%s", resp.Body())
+}
+
+func main() {
+ h := server.New(server.WithHostPorts(":8080"))
+ h.GET("/paseto", func(c context.Context, ctx *app.RequestContext) {
+ now := time.Now()
+ genTokenFunc := paseto.DefaultGenTokenFunc()
+ token, err := genTokenFunc(&paseto.StandardClaims{
+ Issuer: "cwg-issuer",
+ ExpiredAt: now.Add(time.Hour),
+ NotBefore: now,
+ IssuedAt: now,
+ }, nil, nil)
+ if err != nil {
+ hlog.Error("generate token failed")
+ }
+ ctx.String(consts.StatusOK, token)
+ })
+
+ h.POST("/paseto", paseto.New(paseto.WithKeyLookUp("form:Authorization")), func(c context.Context, ctx *app.RequestContext) {
+ ctx.String(consts.StatusOK, "token is valid")
+ })
+
+ go performRequest()
+
+ h.Spin()
+}
+```
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "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/common/hlog"
+ "github.com/cloudwego/hertz/pkg/protocol"
+ "github.com/cloudwego/hertz/pkg/protocol/consts"
+ "github.com/hertz-contrib/paseto"
+)
+
+func performRequest() {
+ time.Sleep(time.Second)
+ c, _ := client.NewClient()
+ req, resp := protocol.AcquireRequest(), protocol.AcquireResponse()
+ req.SetRequestURI("http://127.0.0.1:8080/paseto")
+
+ req.SetMethod("GET")
+ _ = c.Do(context.Background(), req, resp)
+ fmt.Printf("get token: %s\n", resp.Body())
+
+ req.SetMethod("POST")
+ req.SetHeader("Authorization", "Bearer "+string(resp.Body()))
+ _ = c.Do(context.Background(), req, resp)
+ fmt.Printf("Authorization response :%s", resp.Body())
+}
+
+func main() {
+ h := server.New(server.WithHostPorts(":8080"))
+ h.GET("/paseto", func(c context.Context, ctx *app.RequestContext) {
+ now := time.Now()
+ genTokenFunc := paseto.DefaultGenTokenFunc()
+ token, err := genTokenFunc(&paseto.StandardClaims{
+ Issuer: "cwg-issuer",
+ ExpiredAt: now.Add(time.Hour),
+ NotBefore: now,
+ IssuedAt: now,
+ }, nil, nil)
+ if err != nil {
+ hlog.Error("generate token failed")
+ }
+ ctx.String(consts.StatusOK, token)
+ })
+
+ h.POST("/paseto", paseto.New(paseto.WithTokenPrefix("Bearer ")), func(c context.Context, ctx *app.RequestContext) {
+ ctx.String(consts.StatusOK, "token is valid")
+ })
+
+ go performRequest()
+
+ h.Spin()
+}
+```
+
+### ParseFunc
+
+`WithParseFunc` 设置 ParseFunc。
+
+`ParseFunc` 解析并验证令牌。
+
+函数签名:
+
+```go
+func WithParseFunc(f ParseFunc) Option
+```
+
+示例代码:
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "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/common/hlog"
+ "github.com/cloudwego/hertz/pkg/protocol"
+ "github.com/cloudwego/hertz/pkg/protocol/consts"
+ "github.com/hertz-contrib/paseto"
+)
+
+func performRequest() {
+ time.Sleep(time.Second)
+ c, _ := client.NewClient()
+ req, resp := protocol.AcquireRequest(), protocol.AcquireResponse()
+
+ req.SetMethod("GET")
+ req.SetRequestURI("http://127.0.0.1:8080/paseto/correct-issuer")
+ _ = c.Do(context.Background(), req, resp)
+
+ req.SetMethod("POST")
+ req.SetRequestURI("http://127.0.0.1:8080/paseto")
+ req.SetHeader("Authorization", string(resp.Body()))
+ _ = c.Do(context.Background(), req, resp)
+ fmt.Printf("Authorization response:%s\n", resp.Body())
+
+ req.SetMethod("GET")
+ req.SetRequestURI("http://127.0.0.1:8080/paseto/wrong-issuer")
+ _ = c.Do(context.Background(), req, resp)
+
+ req.SetMethod("POST")
+ req.SetRequestURI("http://127.0.0.1:8080/paseto")
+ req.SetHeader("Authorization", string(resp.Body()))
+ _ = c.Do(context.Background(), req, resp)
+ fmt.Printf("Authorization response:%s,because issuer is wrong", resp.Body())
+}
+
+func main() {
+ h := server.New(server.WithHostPorts(":8080"))
+
+ h.GET("/paseto/correct-issuer", func(c context.Context, ctx *app.RequestContext) {
+ now := time.Now()
+ token, err := paseto.DefaultGenTokenFunc()(&paseto.StandardClaims{
+ Issuer: "CloudWeGo-issuer",
+ ExpiredAt: now.Add(time.Hour),
+ NotBefore: now,
+ IssuedAt: now,
+ }, nil, nil)
+ if err != nil {
+ hlog.Error("generate token failed")
+ }
+ ctx.String(consts.StatusOK, token)
+ })
+ h.GET("/paseto/wrong-issuer", func(c context.Context, ctx *app.RequestContext) {
+ now := time.Now()
+ token, err := paseto.DefaultGenTokenFunc()(&paseto.StandardClaims{
+ Issuer: "CloudWeRun-issuer",
+ ExpiredAt: now.Add(time.Hour),
+ NotBefore: now,
+ IssuedAt: now,
+ }, nil, nil)
+ if err != nil {
+ hlog.Error("generate token failed")
+ }
+ ctx.String(consts.StatusOK, token)
+ })
+
+ parseFunc, _ := paseto.NewV4PublicParseFunc(paseto.DefaultPublicKey, []byte(paseto.DefaultImplicit), paseto.WithIssuer("CloudWeGo-issuer"))
+ h.POST("/paseto", paseto.New(paseto.WithParseFunc(parseFunc)), func(c context.Context, ctx *app.RequestContext) {
+ ctx.String(consts.StatusOK, "token is valid")
+ })
+ go performRequest()
+ h.Spin()
+}
+```
+
+## 版本比较
+
+| 版本 | 本地 | 公共 |
+|----|--------------------------------------------------|------------------------|
+| v1 | 使用“AES-256-CBC”加密并使用 HMAC-SHA-256 签名 | 使用`RSA-SHA-256`签名 |
+| v2 | 使用“XSalsa20Poly-1305”加密并使用“HMAC-SHA-384”签名` | 使用`EdDSA`(`Ed25519`)签名 | |
+| v3 | 使用“XChaCha20Poly1305”加密并使用“HMAC-SHA-384”签名` | 使用`EdDSA`(`Ed25519`)签名 | |
+| v4 | 使用“XChaCha20Poly1305”加密,并使用“HMAC-SHA-512-256”签名` | 使用`EdDSA`(`Ed448`)签名 | |
+
+## 完整示例
+
+完成用法示例详见 [paseto/example](https://github.com/hertz-contrib/paseto/tree/main/example)
+
+
+---
+title: "跨源资源共享"
+date: 2022-05-21
+weight: 1
+keywords: ["跨源资源共享"]
+description: "hertz 提供 cors 跨域中间件的实现。"
+
+---
+
+跨源资源共享(CORS)机制允许服务器标识除了它自己的其它 origin,使得浏览器可以访问加载这些资源;
+该机制也用来检查服务器是否允许浏览器发送真实的请求,通过浏览器发送"预检"请求实现,在预检请求头部中有 HTTP 方法和真实请求会用到的头。
+
+hertz 提供 cors 跨域中间件的 [实现](https://github.com/hertz-contrib/cors) ,这里的实现参考了 gin
+的 [cors](https://github.com/gin-contrib/cors)。
+
+## 安装
+
+```shell
+go get github.com/hertz-contrib/cors
+```
+
+## 示例代码
+
+```go
+package main
+
+import (
+ "time"
+
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/hertz-contrib/cors"
+)
+
+func main() {
+ h := server.Default()
+ // CORS for https://foo.com and https://github.com origins, allowing:
+ // - PUT and PATCH methods
+ // - Origin header
+ // - Credentials share
+ // - Preflight requests cached for 12 hours
+ h.Use(cors.New(cors.Config{
+ AllowOrigins: []string{"https://foo.com"},
+ AllowMethods: []string{"PUT", "PATCH"},
+ AllowHeaders: []string{"Origin"},
+ ExposeHeaders: []string{"Content-Length"},
+ AllowCredentials: true,
+ AllowOriginFunc: func(origin string) bool {
+ return origin == "https://github.com"
+ },
+ MaxAge: 12 * time.Hour,
+ }))
+ h.Spin()
+}
+```
+
+### 预检请求
+
+对于跨源访问来说,如果是**简单请求**,本质上就是在 HTTP 请求头信息中添加一个 Origin 字段,用于描述本次请求来自哪个源,服务端可以直接响应。
+
+而对于**非简单**跨源访问请求来说(比如请求方法是 `PUT` 或 `PATCH`,`Content-Type` 字段类型是 `application/json`
+等),会在正式通信之前,发送一次 HTTP
+预检请求(preflight),用于校验客户端是否有跨源资源访问权限,预检请求使用的方法是 `OPTIONS`,且这是浏览器自发的行为。
+
+**注意:部分** `hertz-cors` **的配置只有在预检请求发生时才会生效。**
+
+## 配置
+
+Hertz 通过使用 cors 中间件,为客户端提供了跨源资源访问的能力。用户可以通过自定义 `Config` 结构的配置参数,精细控制服务端资源允许跨源访问的范围,亦或选择
+hertz-cors 的默认配置,允许来自任意 origin 的客户端访问资源。
+
+上述**示例代码**中只配置了部分可选参数,`Config` 的完整参数列表如下:
+
+| 参数 | 介绍 |
+|------------------------|---------------------------------------------------------------|
+| AllowAllOrigins | 用于设置允许来自任意 origin 的客户端访问服务端资源,默认为 `false` |
+| AllowOrigins | 用于设置允许跨源访问的 origin 列表,默认为 `[]` |
+| AllowOriginFunc | 用于设置校验客户端 origin 的函数,当启用这个配置时,`AllowOrigins` 的内容将被忽略 |
+| AllowMethods | 用于设置允许客户端跨源访问所使用的 HTTP 方法列表(在接收到预检请求时生效) |
+| AllowHeaders | 用于设置客户端发起**非简单**的跨源资源访问请求时,允许使用的头信息字段列表,默认为 `[]`(在接收到预检请求时生效) |
+| AllowCredentials | 用于设置允许客户端请求携带用户凭证,如:cookies,token,SSL 凭证,默认为 `false` |
+| ExposeHeaders | 用于设置允许暴露给客户端的响应头列表,默认为 `[]` |
+| MaxAge | 用于设置预检请求的有效期(有效期内不会发起重复的预检请求) |
+| AllowWildcard | 用于设置允许含通配符的 origin 访问资源,默认为 `false` |
+| AllowBrowserExtensions | 用于设置允许使用流行的浏览器扩展模式,默认为 `false` |
+| AllowWebSockets | 用于设置允许使用 WebSocket 协议,默认为 `false` |
+| AllowFiles | 用于设置允许使用 `file://` 协议(危险)除非你能确保 100% 的安全,才可以使用它,默认为 `false` |
+
+### AllowAllOrigins
+
+该参数设置为 `true` 之后,将允许来自任意 origin 的客户端跨源访问服务端资源。
+
+`AllowAllOrigins` 配置为 true 时,`AllowOriginFunc` 以及 `AllowOrigins` 配置不可以使用,否则将发生冲突。
+
+### AllowOrigins
+
+描述了可以跨源访问的 origin 列表,如果列表中的任何 origin 携带通配符 `*`(每个 origin 内只允许使用一个通配符 `*`
+),则允许任何满足匹配逻辑的 origin 访问。
+
+与 `AllowAllOrigins` 配置**冲突**,同时只能配置一项。
+
+与 `AllowOriginFunc` 配置同时使用时,`AllowOriginFunc` 的优先级高于 `AllowOrigins`。
+
+若需要使用携带通配符的 origin,则 `AllowWildcard` 参数需同时设置为 `true`。
+
+示例代码 1:
+
+```go
+package main
+
+import (
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/hertz-contrib/cors"
+)
+
+func main() {
+ h := server.Default()
+ h.Use(cors.New(cors.Config{
+ AllowOrigins: []string{"https://foo.com"},
+ }))
+ h.Spin()
+}
+```
+
+示例代码 2:
+
+```go
+package main
+
+import (
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/hertz-contrib/cors"
+)
+
+func main() {
+ h := server.Default()
+ h.Use(cors.New(cors.Config{
+ AllowWildcard: true,
+ AllowOrigins: []string{"http://some-domain/*"},
+ }))
+ h.Spin()
+}
+```
+
+### AllowOriginFunc
+
+以 origin 为形参,用于自定义 origin 的校验逻辑,返回 `true` 表示校验通过。
+
+与 `AllowAllOrigins` 配置**冲突**,同时只能配置一项。
+
+与 `AllowOrigins` 配置同时使用时,`AllowOriginFunc` 的优先级高于 `AllowOrigins`。
+
+函数签名:
+
+```go
+func(origin string) bool
+```
+
+示例代码:
+
+```go
+package main
+
+import (
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/hertz-contrib/cors"
+)
+
+func main() {
+ h := server.Default()
+ h.Use(cors.New(cors.Config{
+ AllowOriginFunc: func(origin string) bool {
+ return origin == "https://github.com"
+ },
+ }))
+ h.Spin()
+}
+```
+
+### AllowMethods
+
+该配置只有在接收到预检请求时才会生效,用于设置允许客户端跨源访问所使用的 HTTP 方法列表。
+
+如果是由 GET 或者 POST 发起的**简单请求**,则无需额外设置。
+
+示例代码:
+
+```go
+package main
+
+import (
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/hertz-contrib/cors"
+)
+
+func main() {
+ h := server.Default()
+ h.Use(cors.New(cors.Config{
+ AllowWildcard: true,
+ AllowMethods: []string{"PUT", "PATCH"},
+ }))
+ h.Spin()
+}
+```
+
+### AllowHeaders
+
+该配置只有在接收到预检请求时才会生效,如果浏览器请求包括 `Access-Control-Request-Headers`
+字段,则 `Access-Control-Allow-Headers` 字段是必需的。它是一个逗号分隔的字符串,表明服务器支持的所有头信息字段。
+
+示例代码:
+
+```go
+package main
+
+import (
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/hertz-contrib/cors"
+)
+
+func main() {
+ h := server.Default()
+ h.Use(cors.New(cors.Config{
+ AllowHeaders: []string{"Origin"},
+ }))
+ h.Spin()
+}
+```
+
+### ExposeHeaders
+
+用于设置允许客户端从 HTTP Response 的 Header 中获取的自定义头信息字段名称。
+
+示例代码:
+
+```go
+package main
+
+import (
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/hertz-contrib/cors"
+)
+
+func main() {
+ h := server.Default()
+ h.Use(cors.New(cors.Config{
+ ExposeHeaders: []string{"Content-Length"},
+ }))
+ h.Spin()
+}
+```
+
+更多用法示例详见 [cors](https://github.com/cloudwego/hertz-examples/blob/main/middleware/CORS)
+
+
+---
+title: "CSRF"
+date: 2022-12-6
+weight: 12
+keywords: ["CSRF", "跨站点请求伪造攻击"]
+description: "Hertz 提供了 CSRF 中间件,可帮助您防止跨站点请求伪造攻击。"
+---
+
+Cross-site request forgery(CSRF)是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。
+
+Hertz 提供了 [CSRF](https://github.com/hertz-contrib/csrf) 中间件,可帮助您防止跨站点请求伪造攻击。
+
+## 安装
+
+``` shell
+go get github.com/hertz-contrib/csrf
+```
+
+## 示例代码
+
+```go
+package main
+
+import (
+ "context"
+
+ "github.com/cloudwego/hertz/pkg/app"
+ "github.com/cloudwego/hertz/pkg/app/server"
+ "github.com/hertz-contrib/csrf"
+ "github.com/hertz-contrib/sessions"
+ "github.com/hertz-contrib/sessions/cookie"
+)
+
+func main() {
+ h := server.Default()
+
+ store := cookie.NewStore([]byte("secret"))
+ h.Use(sessions.New("csrf-session", store))
+ h.Use(csrf.New())
+
+ h.GET("/protected", func(c context.Context, ctx *app.RequestContext) {
+ ctx.String(200, csrf.GetToken(ctx))
+ })
+
+ h.POST("/protected", func(c context.Context, ctx *app.RequestContext) {
+ ctx.String(200, "CSRF token is valid")
+ })
+
+ h.Spin()
+}
+
+```
+
+## 配置
+
+| 配置项 | 默认值 | 介绍 |
+|-----------------|-------------------------------------------------------------------------------|----------------------------------------------------------------------|
+| `Secret` | `csrfSecret` | 用于生成令牌(必要配置) |
+| `IgnoreMethods` | "GET", "HEAD", "OPTIONS", "TRACE" | 被忽略的方法将将视为无需 `csrf`保护 |
+| `Next` | `nil` | `Next` 定义了一个函数,当返回真时,跳过这个 `csrf` 中间件。 |
+| `KeyLookup` | `header:X-CSRF-TOKEN` | `KeyLookup` 是一个"