Files
go-omni/ws/README.md

150 lines
12 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.
# WebSocket
`ws` 提供通用 Go Client、Server 和单次物理连接 Session。核心使用标准 RFC 6455 WebSocket,可连接浏览器或第三方 WS 服务。`hub`、`rpc`、`observe` 按需引入;不预设用户、租户、设备、错误码或数据存储。
- [回显、分组广播与浏览器示例](../examples/websocket/README.md)
- [双向 RPC 与订阅恢复示例](../examples/websocketRPC/README.md)
- [RPC 协议与订阅规则](rpc/README.md)
- [日志与指标适配](observe/README.md)
- [测试与基准报告](../reports/websocket-validation-2026-10-29.md)
## 独立 Server 与 Client
```go
server, err := ws.NewServer(ws.ServerConfig{
MaxConnections: 4096,
// 省略时允许同源和无 Origin 的非浏览器请求。
AllowedOrigins: []string{"https://console.example.com"},
})
if err != nil { return err }
mux.Handle("/ws", server.Handler(ws.Handlers{
OnMessage: func(ctx context.Context, session *ws.Session, msg ws.Message) error {
return session.Send(ctx, msg)
},
}))
// HTTP 宿主退出后、依赖释放前调用 server.Shutdown(ctx)。
client, err := ws.NewClient(ws.ClientConfig{ProxyMode: "direct"})
if err != nil { return err }
session, err := client.Dial(workCtx, "wss://service.example.com/ws", ws.Handlers{
OnMessage: func(ctx context.Context, session *ws.Session, msg ws.Message) error {
// msg.Type 是 ws.Text 或 ws.Binary;此回调中处理业务消息。
return nil
},
})
if err != nil { return err }
err = session.SendJSON(ctx, map[string]string{"message": "hello"})
// 退出前 client.Shutdown(ctx),确保回调结束后再关闭其依赖。
```
`NewServer` / `NewClient` 只验证并构造,无监听或拨号。Server 是 `http.Handler`,复用 HTTP 监听器及 TLS;客户端 `TLSConfig` 支持系统信任链、自定义 CA 与客户端证书。注入的证书/CA/回调对象由调用方保持并发安全且不再修改。TLS 默认正常验证,不自动跳过验证。
`Server.Accept(w,r,handlers)` 会写出握手成功或错误响应,成功后启动托管会话并返回。原 HTTP Handler 可以立即返回;不要保留整个 `*httpx.Context`、Request 或可变鉴权对象。Server 的会话生命周期由 `ServerConfig.Context`(默认 Background)管理,握手 context 仅用于握手。Client.Dial 的 ctx 持有会话寿命,其配置的 HandshakeTimeout 只限制握手。
## 握手与安全配置
`ServerConfig` 支持 `Session`、`MaxConnections`、`HandshakeTimeout`、`AllowedOrigins`、`RequireOrigin`、`Subprotocols`、`RequireSubprotocol`、`EnableCompression`、`HandshakeRate`、`HandshakeBurst`。连接上限包含进行中的握手。速率为每秒令牌数、burst 为突发数,零 rate 默认关闭;超过速率返回 429,连接上限/停机返回 503。HTTP 读头超时和头大小限制仍由宿主配置。
Origin 校验与 CORS 独立:
- 默认只接受与请求的真实 scheme/host/port 一致的 Origin;缺少 Origin 默认允许,便于 Go 等非浏览器客户端,设置 RequireOrigin 可拒绝。
- 显式 AllowedOrigins 是完整来源白名单,支持明确的 `http://` / `https://` 和端口;不支持通配 `*`、路径、userinfo、null 或重复 Origin。默认端口归一化。
- 不信任 Forwarded/X-Forwarded-* 来推断来源。TLS 在代理终止时,应填写实际外部 Origin 白名单。
- Origin 不是身份认证。`Prepare(ctx,*http.Request)(Handlers,error)` 在升级前验证业务身份,同时可为每连接创建 handlers。失败默认 401;返回 `*ws.RejectError{StatusCode:403}` 可选择 4xx。响应不包含底层错误。
- 浏览器原生 WebSocket 不能自定义 Authorization Header,业务可采用已受保护的 Cookie 或短期握手票据。若使用 Cookie,继续严格检查 Origin;避免在 URL 中放长期凭证,并配置代理访问日志脱敏。
RequireSubprotocol 要求协商成功,否则握手失败;纯 WS 默认无必选子协议。RPC 使用 `go-omni.rpc.v1`,两端显式声明并要求它。
## Session 的收发与关闭
Session 提供 `ID / Context / Done / Err / Subprotocol / Stats / Send / TrySend / SendJSON / Close / Abort / Wait`。
| 操作 | 语义 |
| --- | --- |
| `Send(ctx,Message)` | 有界队列入场后等待写入结果;队列满立即返回 ErrQueueFull,不无限等待空位。 |
| `TrySend(Message)` | 只确认入队,随后仍可能写失败;用于广播、单向通知。 |
| `SendJSON(ctx,value)` | JSON 编码后按 Text 发送,适用于应用自己的协议。 |
| `Close(code,reason)` | 幂等启动关闭,取消回调 context,尽力发送已接纳且未取消的出站消息,再发关闭帧;不等待自身回调。 |
| `Abort()` | 立即关闭 socket,仍需 Wait 才能证明回调退出。 |
| `Wait(ctx)` / `Done()` | 等待读、写、分发、监控及 OnClose 全部结束;Wait 返回运行错误或等待超时。 |
文本严格验证 UTF-8;二进制保持字节内容。入队复制发送 payload,Send/TrySend 返回后调用方可以复用原切片。不得在调用进行中并发修改它。接收 payload 不复用,回调可以保留,但应用自行承担保留后的内存。业务不得直接抢占底层 reader/writer。
单个会话 OnOpen、OnMessage 顺序执行,OnClose 在 I/O 和消息回调结束后执行;不同会话可并发。回调必须响应 ctx 取消,不要在回调内调用自身 Wait、Client/Server.Shutdown 或 App.Close,可调用 Session.Close 或 Server.Stop。OnOpen 中不要等待必须依赖 OnMessage 才完成的操作;RPC Call/订阅恢复应在 WaitReady 后从独立受管理任务调用。
排队时 Send ctx 取消会跳过该消息;写入期间取消可能已部分送出,此时终止物理连接。成功写出也不代表对端业务处理成功。框架不缓存离线消息、不自动重放;业务需要 ACK、去重、持久化时显式设计。Close 不是业务事务提交屏障,会取消当前业务并丢弃尚未分发的入站消息。
## 资源上限与心跳
`SessionConfig` 零字段使用默认值,负值或超过支持边界的值在构造前报错。明确关闭心跳使用 `DisableHeartbeat`,零时间不表示禁用。
| 配置 | 默认 |
| --- | --- |
| MaxMessageBytes | 1 MiB,包括解压后的消息 |
| ReadQueueSize / WriteQueueSize | 每方向 64 条 |
| ReadQueueBytes / WriteQueueBytes | 每方向 4 MiB |
| WriteTimeout | 10 秒 |
| PingInterval / PongTimeout | 30 / 60 秒 |
| CloseTimeout | 5 秒 |
队列计数包含正在执行的回调/正在写的消息;此外还需计入一条正在解码的消息、协议缓冲和应用持有的对象。实例总内存还受连接数影响。接收队列溢出关闭该连接;发送满明确报错,广播逐连接返回错误。有限队列允许持续处理控制帧,不让慢业务无限阻塞 reader。压缩默认关闭;启用后仍执行解压大小限制,但会消耗额外 CPU,应结合并发连接限额验证。
协议 Ping/Pong 由底层持续处理;浏览器自动回复 Pong。心跳监测连接活性,不证明业务健康。
## Client 重连与网络设置
`ClientConfig` 支持 Session、HandshakeTimeout(默认 10s)、MaxConnections(默认 64)、Subprotocols/RequireSubprotocol、EnableCompression、ProxyMode/ProxyURL、TLSConfig、Headers、HeaderProvider、Prepare、Reconnect、ShouldReconnect。
- `Dial` 只拨一次。`Run(ctx,url,handlers)` 管理一条重连链;同 Client 可以并发运行多条连接,统一限制和关闭。
- `Reconnect.Enabled` 显式开启;默认 minBackoff=100ms、maxBackoff=30s、stableAfter=30s。指数退避带抖动;短暂反复断线不重置预算。MaxAttempts/MaxElapsed 可限制重试次数和时间;开启重连后两者为零表示不限。
- 默认不重试 401/403、协议不兼容、证书验证错误、正常 1000 关闭;远端先发 1001(going away)、1012 等重启关闭码和临时网络故障可重试。本地主动 Close/Client.Stop 不触发重连。ShouldReconnect 可改变分类,不能绕过显式关闭和预算。
- 每次握手调用 HeaderProvider(ctx),用于刷新凭证;返回头会复制。Prepare(ctx) 可为每代创建新的 RPC Peer/handlers,禁止复用已经绑定的 Peer。
- ProxyMode 默认为 environment,可显式 direct 或 url;固定代理支持 HTTP CONNECT、socks5、socks5h。HTTPS 代理当前明确拒绝,不会降级直连。
- 自有 HTTP 包装链 `httpc.Do`、`telemetry.Transport`、`resilience.Transport` 不直接用于 WS 升级;WS 独立处理握手、消息与连接寿命。
`Client.Close` / `Stop` 停止入场、取消握手/退避并发起会话关闭;`Drain` / `Shutdown` 等待所有 Run、握手、会话和回调结束。Server 同样提供 Stop/Drain/Shutdown。终止后对象不可重新启动。
## Hub 与扩展
`hub.New(Config)` 提供 `Add / Remove / Join / Leave / Send / SendGroup / Broadcast / Kick / Stats / Close`。Session 实现其 `Connection` 接口,也可实现其他有界非阻塞连接适配器。组键是业务自定义字符串,Join 前由业务鉴权。
Send/Broadcast/SendGroup 返回 `Result{Attempted,Queued,Errors}`,逐连接尝试入队,慢连接不会等待网络写出。Queued 不等于远端收到。Hub 借用连接,Close 只清注册和 watcher;Kick 才主动关闭,Session.Done 后自动清理。上限可配置连接数、组数、总成员关系、每连接组数量。
Hub 是单进程注册表。跨节点广播由业务消息总线投递到各节点的本地 Hub;此版不含分布式路由、离线存储或 exactly-once 保证。通过接口扩展时仍需保持 TrySend 有界、非阻塞、并发安全。
## GoOmni 接入与停机
独立 App:
```go
application := omni.New()
server, err := ws.NewServer(ws.ServerConfig{})
if err != nil { return err }
if err = wsomni.Mount(application, "/ws", server, handlers); err != nil { return err }
// Mount 同时登记 Server.Stop/Drain,App.OnShutdown 的资源晚于会话释放。
return application.RunContext(ctx, ":8080")
```
`wsomni` 的 import 是 `git.yuntuo.hk/k2safe/go-omni/ws/omni`。App 新增通用 `OnStop(name,hook)` / `OnDrain(name,hook)`:HTTP 结束 → 全部 Stop → 全部 Drain → OnShutdown。复用 lifecycle 配置及 ErrResourcesRetained 规则;只在用到后台组件时创建额外 Runtime。
生成项目在 `svc.Init` 创建 server,Context 设为 svc.WorkContext,并登记 `svc.OnStop("ws",server.Stop)` / `svc.OnDrain("ws",server.Drain)`。项目可手写以下方法,新版生成 NewApp 自动调用:
```go
func (c *ServiceContext) RegisterCustomRoutes(app *omni.App) error {
app.GET("/ws", wsomni.Handler(c.WebSocket, c.WebSocketHandlers))
return nil
}
```
不要同时把同一组件登记到 App 和 svc;依赖归属与排空屏障必须一致。自定义路由在生成路由与内建端点之后注册,重复路径由路由器拒绝,不会覆盖现有接口。重新运行 `api generate` 更新入口,无需修改受管文件;普通项目没有此方法时行为不变。握手成功会记录 HTTP 101,HTTP 日志只计算握手时间。
ServerConfig/ClientConfig 直接组合进项目的 Config/Default/Validate;文件参数仍采用显式 `${NAME}` 引用。框架不新增 WS DSL 或自动业务 Token。
兼容范围:默认 nethttp 的 HTTP/1.1 Upgrade、标准 HTTP/1.1 反向代理、WS/WSS。自研 native 和独立实验 http2 Runner 目前不支持 WS;Mount 在配置时拒绝,普通 Handler 遇到不支持接管的 ResponseWriter 返回 501。不宣称支持 HTTP/2 extended CONNECT。切换 Runner 时仍需遵守此边界。
## 错误与观测
`errors.Is` 判断 ErrClosed、ErrNotConnected、ErrQueueFull、ErrMessageTooLarge、ErrInvalidMessage、ErrUnsupportedTransport、ErrConnectionLimit;`errors.As` 获取 HandshakeError.StatusCode、CloseError.Code、OpError.Op。Cause 保留诊断链,但可能包含敏感数据;默认错误文本不包含 URL、认证头、payload 或对端 reason,不要无条件递归记录 Cause。
Server.Stats 提供握手数、活跃连接、累计接受/拒绝;Client.Stats 提供活跃连接、握手、Run数、关闭状态及累计 DialAttempts、HandshakeFailures、Reconnects;Session.Stats 提供消息/字节和队列占用。`observe` 提供安全 logx 日志和跨会话统计快照,可由项目导出到 metrics/OTel。连接 ID 不适合作为指标标签;不要把整条长连接当成持续数小时的 HTTP Span。业务消息 tracing 由业务协议显式传播。