7.9 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/native 是第一版自研 HTTP/1.1 服务端:它自己管理 TCP listener、连接、Keep-Alive、请求解析、响应写回和优雅关闭,再把请求交给现有 http.Handler。请求体支持 Content-Length 和 chunked,并统一受 MaxBodyBytes 限制;其他传输编码会被拒绝。它仍使用标准 http.Request / http.ResponseWriter 作为业务桥接,因此不会影响现有 Handler / Service。
业务 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,不会无限排队。
这个设计借鉴了 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。
可移植路由规则(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 仍使用标准库 net/http.Server,需要显式调用 App.RunWith(ctx, native.New(nil), address) 才会启用 native 预览实现。native 当前通过标准 http.Request / http.ResponseWriter 桥接到业务层,后续可在不改 Handler / Service 的前提下逐步替换解析器和响应写回。