120 lines
9.2 KiB
Markdown
120 lines
9.2 KiB
Markdown
# 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/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` 仍使用标准库 `net/http.Server`,需要显式调用 `App.RunWith(ctx, native.New(nil), address)` 才会启用 native 预览实现。native 当前通过标准 `http.Request` / `http.ResponseWriter` 桥接到业务层,后续可在不改 Handler / Service 的前提下逐步替换解析器和响应写回。
|