Files
go-omni/transport/README.md

180 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Transport adapters
业务 Handler 依赖 GoOmni 的 `httpx.Context`,Service 接收标准 `context.Context`。适配器只在启动入口组装:
```go
import (
omni "git.yuntuo.hk/k2safe/go-omni"
netrouter "git.yuntuo.hk/k2safe/go-omni/transport/nethttp"
)
application := omni.NewWithRouter(netrouter.New())
registerRoutes(application) // 与 omni.New() 复用同一份业务注册代码
```
`transport/nethttp` 是当前内置的标准库路由实现。它提供一套可移植的路径、方法和错误回退规则;自研网络层可以直接实现同一个 `httpx.Router` 契约。
`transport/http2` 是独立的 opt-in 实验 Runner,负责连接接入、TLS/ALPN、连接上限和全局请求背压,底层帧解析与 stream 流控暂由 `golang.org/x/net/http2` 提供。它不替换 `transport/native`,也不改变默认启动器;通过 `WithTransport(TransportHTTP2)` 或 `App.RunWith(ctx, http2.New(nil), address)` 显式启用后运行。
`transport/native` 是第一版自研 HTTP/1.1 服务端:它自己管理 TCP listener、连接、Keep-Alive、请求解析、响应写回和优雅关闭,再把请求交给现有 `http.Handler`。请求体支持 `Content-Length` 和 chunked,并统一受 `MaxBodyBytes` 限制;其他传输编码会被拒绝。它仍使用标准 `http.Request` / `http.ResponseWriter` 作为业务桥接,因此不会影响现有 Handler / Service。
native 提供 `server.Stats()` 读取已接受连接数、连接拒绝数、当前连接数和当前请求数,适合和 `metrics.Metrics` 的业务请求指标一起采集;读取不会阻塞连接生命周期。`RejectedConnections` 统计连接闸门拒绝,`RejectedRequests` 统计并发请求闸门返回 503。
业务 Handler 需要流式返回时可以断言 `http.Flusher` 并调用 `Flush()`;HTTP/1.1 会自动使用 chunked 响应,普通未 Flush 的响应仍自动生成 `Content-Length`。HTTP/1.0 没有 chunked framing,会退回到写完后关闭连接。
Handler 运行超过约 5 ms 时,native 启动一次阻塞读取监测;读到 EOF 或连接错误会取消 `Request.Context()`,流式写入失败也会触发取消。Handler 返回后先停止监测,再复用连接和 Reader;普通短请求不会启动监测 goroutine。
如果客户端提前发送了下一条请求,监测器会保留管线数据并退出,避免预读不断占用内存。这种情况下,断开取消会延后到写失败或 Handler 返回;业务仍应为下游调用设置自己的超时。
服务端不是把连接交给一个全局串行 worker,而是每条连接独立处理请求;Keep-Alive 负责复用客户端连接,`Server` 内部的 `sync.Pool` 负责复用小型 Reader、响应对象和连接状态。Reader 默认只有 4 KiB,需要处理大请求头时按需增长。需要保护内存上限时,可以设置 `MaxConnections` 和 `MaxConcurrentRequests`;超出连接上限的连接会被关闭,超出请求并发上限的请求返回 503,不会无限排队,拒绝数可从 `Stats()` 采集。
访问日志默认只记录 TCP socket 对端地址。部署在 Nginx、Cloudflare 等反向代理后面时,通过 `omni.WithTrustedProxies("127.0.0.1/32")` 或 `GO_OMNI_TRUSTED_PROXIES` 配置可信代理网段,框架才会从 `X-Forwarded-For` 右向左校验并取得真实客户端地址;未配置时转发头不会被信任。
这个设计借鉴了 Nginx 的资源边界,但没有照搬它的进程模型:Nginx 用少量 worker 进程和事件机制管理大量连接,并用 `worker_connections` 限制每个 worker 的同时连接数;Go runtime 自己已经用 epoll / kqueue 等网络轮询器唤醒阻塞的 goroutine,因此 native 采用“accept loop + 每连接轻量 goroutine + 请求并发闸门”的等价实现。这样业务 Handler 可以继续执行同步代码,慢请求不会阻塞其他连接;只有在真实压测证明 goroutine 调度或协议状态机成为瓶颈时,才需要继续下沉为手写事件循环。
使用自研服务端:
```go
application := omni.New()
server := native.New(nil)
if err := application.RunWith(ctx, server, ":8080"); err != nil {
log.Fatal(err)
}
```
`omni.New()` 默认仍使用 `transport/nethttp` 路由;native 服务端是显式启用的预览实现,等协议和连接边界稳定后再考虑切换默认启动器。
TLS 可以直接作为 `App.RunWith` 的启动配置:
```go
server := native.New(nil)
server.TLSConfig = &tls.Config{Certificates: certificates}
if err := application.RunWith(ctx, server, ":8443"); err != nil {
log.Fatal(err)
}
```
需要自己持有 listener 或配置证书文件时,分别使用 `ServeTLS` 或 `RunTLS`;TLS 握手仍受 native 的读超时、连接上限和优雅关闭控制,Handler 可通过 `Request.TLS` 读取标准 `tls.ConnectionState`。
native 当前只宣告 TLS ALPN `http/1.1`。未配置 `NextProtos` 时会自动补上;如果配置了 `h2` 等未实现协议,服务会在启动时直接返回错误,避免 TLS 协商成功后再被 HTTP/1.1 解析器拒绝。需要 HTTP/2 时,先让 Nginx/Cloudflare 在边缘终止 HTTP/2,再以 HTTP/1.1 回源;或继续使用默认的 `transport/nethttp` 适配器。native 的 HTTP/2 会在独立协议设计和压测完成后再进入。
HTTP/2 的进入条件、连接/流生命周期、背压和长测矩阵见 [native HTTP/2 进入条件](native/HTTP2.md)。
## 可移植路由规则(v0.3.0)
| 项目 | 约定 |
| --- | --- |
| 路径 | 顶层路由必须以 `/` 开头;区分大小写。禁止重复斜杠、`.` / `..` 段、查询串和片段标识。 |
| 参数 | `/users/:id` 匹配一个非空路径段;参数必须占据整个段,同一路由内名称唯一。 |
| 通配 | `/files/*path` 仅允许出现在末尾,值包含开头的 `/`;`/files/` 对应 `/`,`/files` 不匹配。 |
| 路由优先级 | 相同方法内,从左向右比较,静态段优先于参数段;更具体的分支匹配失败后允许回退。 |
| 注册冲突 | 同方法重复注册、相同前缀处参数名冲突、catch-all 与同级分支冲突,均在注册时 panic。不同方法可用同一路径。 |
| 分组 | 支持嵌套与相对路径。`Group("/api").Handle("GET", "", h)` 注册 `/api`,传 `"/"` 则注册 `/api/`。 |
| 尾部斜杠 | `/users` 与 `/users/` 是不同路由;不自动重定向、补斜杠或清理请求路径。 |
| URL 编码 | 使用已解码的 `URL.Path`;例如 `%2F` 作为路径分隔符,`+` 保留为加号。 |
| 方法 | `Handle` 接受大写字母组成的方法名;HEAD 不自动复用 GET,OPTIONS 不自动成功。 |
| Any | 注册 GET、POST、PUT、PATCH、HEAD、OPTIONS、DELETE、CONNECT、TRACE 九种方法;其他方法需显式注册。 |
| 404 / 405 | 未找到路径返回纯文本 404;路径存在但方法不支持时返回纯文本 405,`Allow` 去重并按字母排序。 |
| 并发 | 启动前完成路由注册;允许并发处理请求,各请求参数、业务值、requestId 必须隔离。 |
HTTP 回退响应属于路由层,不进入应用路由中间件,不携带统一 JSON 封装或 requestId。业务 Handler 返回的错误则经过统一 JSON 错误处理。
业务层通过 `application.Handle("HEAD", path, handler)` 或 `group.Handle("OPTIONS", path, handler)` 显式注册其他方法,同样经过框架中间件和错误处理。
## 业务行为
- 同一套 Handler、Service、中间件用于两种适配器;应用、分组、嵌套分组、路由中间件依次进入、反序退出。
- `Use` 只影响后续注册的路由。鉴权失败短路处理链,panic 转为统一 500。
- URI / Query / JSON 绑定、业务错误、requestId 与业务日志关联一致。
- `JSON` / `OK` 等方法先编码再发送;编码失败返回统一 500。已通过 Context 提交响应后,后续错误、panic 或重复响应不会追加 JSON。
- 客户端断开会取消传入 Service 的 context;适配时保留原有 context 的值、截止时间和取消信号。
- `Raw()` 是高级逃生口;直接写入、流式响应、Hijack 等行为不属于上述 Context 响应追踪契约,勿与自动响应方法混用。
## 自研适配器验收
实现 `httpx.Router` / `httpx.RouterGroup`,在调用 Handler 前通过 `httpx.WithRouteParams` 和 `httpx.WithRoutePattern` 注入请求元数据。使用外部测试包接入公共验收套件:
```go
package myrouter_test
import (
"testing"
"git.yuntuo.hk/k2safe/go-omni/httpx"
"git.yuntuo.hk/k2safe/go-omni/transport/transporttest"
"example.com/project/myrouter"
)
func TestContract(t *testing.T) {
transporttest.Run(t, func() httpx.Router { return myrouter.New() })
}
```
工厂需每次返回新的空路由器。套件依次创建路由器,内部会发送并发请求、打开本地 HTTP 测试服务,运行时应启用 race 检查:
```sh
go test -race ./transport/... ./examples/reference
python3 scripts/verifyTransports.py --version v0.6.0-preview
```
第一条验证标准库适配器复用完整 Handler / Service / Repository。第二条在源码目录外创建无 replace 的消费模块,从远程版本导入同一套验收包;自研适配器也应使用同一个公共测试包。
当前替换边界分成两层:`transport/nethttp` 提供标准库路由,`transport/native` 进一步接管 TCP 监听、连接管理和 HTTP/1.1 协议 I/O。`App.Run` / `App.RunContext` / `ServeContext` 使用配置选中的 transport,默认 nethttp;推荐 `WithTransport(TransportNative)` 启用 native,框架会统一装配限额和 HTTP 配置。自定义 Runner 才使用 `RunWith`。native 通过标准 `http.Request` / `http.ResponseWriter` 桥接到业务层。
## HTTP 配置(v0.9.1-preview)
`omni.WithHTTPConfig` / `config.Config.HTTP` 提供服务端配置入口,作用于 `Run`、`RunContext`、`ServeContext`;也作用于 `NewServer` 创建的标准库服务器。与客户端 `httpc` 的出站请求超时无关。
```go
application := omni.New(omni.WithHTTPConfig(omni.HTTPConfig{
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20,
// ErrorLog: existingLogger, // *log.Logger,仅 nethttp;不通过 YAML/JSON 序列化
}))
// 注册业务路由后:
return application.RunContext(ctx, ":8080")
```
| 字段 | nethttp | native | 独立实验 http2 |
| --- | --- | --- | --- |
| `ReadHeaderTimeout` | 标准 http.Server 语义 | 请求头读取时限 | 正值拒绝启动 |
| `ReadTimeout` | 整个请求读取时限,包括头与 body | 头解析完成后读取 body 的时限 | 正值拒绝启动 |
| `WriteTimeout` | 标准 http.Server 写截止时间 | 每次底层响应写入/Flush 的截止时间 | 正值拒绝启动 |
| `IdleTimeout` | Keep-Alive 等待下一请求 | 复用连接等待下一请求(含头读取);首个请求读头也取它与读头时限的较小值 | HTTP/2 空闲连接超时 |
| `MaxHeaderBytes` | 标准 http.Server 头限制语义 | native 请求头字节限制 | 解压后请求头限制 |
| `ErrorLog` | 标准库内部错误日志,不是访问日志 | 非 nil 拒绝启动 | 非 nil 拒绝启动 |
参数为正时覆盖适配器默认值;**零值保留默认行为,不统一表示禁用**。nethttp 未配置时遵循 Go 标准服务器零值,包括读头/空闲时限回退到 ReadTimeout 的规则;native 仍默认读头 5s、读 body 30s、写 30s、空闲 60s、头 1 MiB。负数在启动前报错;HTTP/2 不支持的字段也会明确报错,不会悄悄忽略。默认 nethttp 的 TLS/HTTP2 行为仍由标准库管理,表格最后一列专指 `TransportHTTP2`。
I/O 超时不会强制终止 CPU 计算或数据库调用;业务和下游调用继续通过 `context` 控制时限。native 需要流式输出时显式 `Flush()`,否则保持原有缓冲响应方式。不要依赖不同网络层在慢请求时返回完全相同的 HTTP 错误体;有些超时会直接断开连接。
YAML/JSON 字段与上述字段对应的 lowerCamelCase 一致,位于 `config.Config` 的 `http` 对象。脚手架将框架配置包在 `server` 下,因此生成项目填写 `server.http`;直接用 `config.LoadFile` 加载框架配置时填写根级 `http`。环境变量由文件显式声明,例如 `readTimeout: "${HTTP_READ_TIMEOUT:-30s}"`,不再按字段自动覆盖。`omni.New` 不自动读取文件;先 `config.LoadFile` / `config.LoadProject`,再 `WithConfig`。Go-only 的 `ErrorLog` 在文件加载之后设置。
`NewServer` 始终返回普通 `*http.Server`,应用 HTTP 字段但不负责加载 TLS、监听、退出或资源清理;非法 HTTP 值或路由限额冲突会 panic。`RunContext` / `ServeContext` 的非法配置则作为 error 返回并关闭 App 资源。手动修改返回服务器的字段仍然有效。`RunWith` 的网络配置由 Runner 持有;App 自身的请求体策略和取消收尾窗口仍生效,框架无法自动移除自定义 Runner、反向代理或业务中间件的其他限额。
## 统一请求体限额
三种内置 transport 通过 App 共用下列配置,全部以字节为单位,范围 0..1 TiB:
```yaml
server:
http:
defaultMaxBodyBytes: 1048576 # 普通 App 路由默认 1 MiB
maxBodyBytes: 20971520 # 所有请求硬上限 20 MiB
shutdownCancelTimeout: 5s
```
- `DefaultMaxBodyBytes`:未显式声明限额的 App/RouterGroup 路由默认值;零为 8 MiB,如果硬上限更小则取硬上限。显式默认值超过硬上限会报配置错误。
- `HandleWithBodyLimit` / `.api @request(maxBytes: ...)`:覆盖普通路由默认值。生成契约既有默认 1 MiB 保持;服务默认值不会扩大契约限制。
- `MaxBodyBytes`:服务硬上限;零仅保留框架可支持的 1 TiB 上限,不表示请求会预分配相应内存。路由超过硬上限会在监听前报错,包含完整路由、路由限额、服务限额。`RunWith` 也检查冲突;`NewServer` 在创建时检查,外部宿主在注册完成后调用 `app.ValidateHTTP()` 可预检;未经预检直接调用 App/Engine 时,首次请求拒绝非法配置。
App 在中间件读取请求之前安装流式 reader 限额,支持已知长度和未知长度。native/HTTP2 在 App 装配下只保留 1 TiB 协议上界,原来的隐含 8 MiB 不再挡住放宽的上传路由。请求进入 App 后使用 `BODY_TOO_LARGE` 错误路径;服务硬上限提前拒绝时使用 App 响应策略,路由限额使用该路由策略。native 提前拒绝后先发送响应再半关闭写方向,最多等待 500ms 避免未读上传触发 TCP reset 吞掉提示;不会等待上传完成。错误目录将 HTTP 状态映射为 200 时仍保留请求体拒绝语义。已提交的流式响应不能撤回;处理器应先完成有界读取/校验再执行不可逆业务。
底层协议错误(例如非法 framing 或超过 1 TiB)可能在路由前直接拒绝。直接使用 `native.Server` / `http2.Server` 的 `MaxBodyBytes` 默认仍为 8 MiB,由该服务器的拥有者配置;通过 `RunWith` 传入这些服务器也需自行协调其限额,推荐使用 `WithTransport` 获取统一装配。高级 `Router()` 原始注册绕过 App 路由默认值,但经 App/Engine 服务时仍有服务硬限额;直接调用 Router.ServeHTTP 会绕过整个 App 策略。
multipart 的整个请求包含文件、字段和边界,`MaxFileBytes` 与请求体上限分别控制;幂等中间件 `MaxRequestBytes` 控制缓存量,不随路由自动放大。代理层限额继续在代理配置中维护。框架不猜测业务文件规格或代理策略。
迁移:旧 nethttp 手写路由此前无统一默认 reader 限额,现在默认 8 MiB。大请求用路由覆盖或显式默认值;已有生成接口保留契约限额。示例见 [httpSafety](../examples/httpSafety/README.md)。