16 KiB
Transport adapters
业务 Handler 依赖 GoOmni 的 httpx.Context,Service 接收标准 context.Context。适配器只在启动入口组装:
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 调度或协议状态机成为瓶颈时,才需要继续下沉为手写事件循环。
使用自研服务端:
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 的启动配置:
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 进入条件。
可移植路由规则(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 注入请求元数据。使用外部测试包接入公共验收套件:
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 检查:
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 的出站请求超时无关。
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:
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。