feat(ws): release generic WebSocket client and server v0.31.0-preview

This commit is contained in:
2026-10-29 19:20:12 +08:00
parent 19ee9746e9
commit 3272974bc1
58 changed files with 7654 additions and 21 deletions

98
ws/rpc/README.md Normal file
View File

@@ -0,0 +1,98 @@
# WebSocket 双向调用与订阅
`ws/rpc` 是可选的 JSON 消息协议。普通 `ws` 的文本/二进制消息不依赖此协议。双端必须协商 `rpc.Subprotocol`(`go-omni.rpc.v1`),客户端和服务端都可发起调用、处理调用和发送事件。它不承诺离线消息、断线重放、事务回滚或 exactly-once。
## 调用
```go
peer, err := rpc.New(rpc.Config{})
if err != nil { return err }
err = peer.Handle("echo", func(ctx context.Context, params json.RawMessage) (any, error) {
return params, nil
})
if err != nil { return err }
client, err := ws.NewClient(ws.ClientConfig{
Subprotocols: []string{rpc.Subprotocol}, RequireSubprotocol: true,
})
if err != nil { return err }
session, err := client.Dial(ctx, endpoint, peer.Handlers())
if err != nil { return err }
if err := peer.WaitReady(ctx); err != nil { return err }
var result map[string]any
err = peer.Call(ctx, "echo", map[string]any{"message": "hello"}, &result)
// Notify 确认本地写入完成,不等待对端处理,也不返回业务执行结果。
err = peer.Notify(ctx, "changed", map[string]any{"revision": 2})
// 退出时使用有界关闭 context,并检查返回值。
err = client.Shutdown(shutdownCtx)
_ = session
```
双向调用允许 Handler 内发起另一个 `Call`。接收路径立即处理回复,把请求分派到有上限的任务中,避免串行消息回调形成双向等待。路由在连接建立前通过 `Handle` 注册,重复方法或连接后注册返回错误。方法最多 128 字节,使用 ASCII 字母、数字及 `._-/:`。
服务端用 `ws.ServerConfig.Prepare` 为每次握手创建独立 Peer 并返回 `peer.Handlers()`;客户端自动重连用 `ws.ClientConfig.Prepare` 为每次连接创建独立 Peer。旧 Peer 永不绑定新连接。将新 Peer 交给应用后,先 `WaitReady`,再发起调用或恢复订阅。**不要在 `OnOpen` 中同步 Call/Restore**,因为 Session 在 OnOpen 返回后才分派回复。
完整真实 TCP 示例和退出流程见 [`examples/websocketRPC`](../../examples/websocketRPC)。
## 协议和错误
```json
{"v":1,"type":"call","id":"1","method":"echo","params":{"message":"hello"},"timeoutMs":30000}
{"v":1,"type":"reply","id":"1","result":{"message":"hello"}}
{"v":1,"type":"reply","id":"2","error":{"code":"DENIED","message":"access denied"}}
{"v":1,"type":"event","method":"changed","params":{"revision":2}}
{"v":1,"type":"cancel","id":"1"}
```
一条 WS 文本消息对应一个 envelope。协议版本、类型、字段组合、方法名和请求 ID 都校验;未知字段、重复 JSON 键(包括嵌套 params/result)、超过 32 层的 JSON、无效 UTF-8、二进制消息及超限消息被拒绝。收到重复的在途请求 ID 视为协议错误;迟到或重复回复丢弃,不创建新的 pending。未知调用回复 `METHOD_NOT_FOUND`;未知事件为协议错误,会终止连接。
`Call` 的执行预算取调用方 context 与 `Config.CallTimeout` 的较小值,预算毫秒数发送给对端,接收方再次按自己的配置裁剪。取消时尽力发送 cancel;取消消息入队失败不会无限等待,接收方仍有自己的超时。超时不表示远端业务已撤销。`Call` 与 `Notify` 都不会自动重试。
仅显式返回 `*rpc.Error{Code, Message}` 才会向对端发送自定义文本。普通错误、编码错误和 panic 使用通用 `INTERNAL`,不发送实现细节。超时/取消分别映射为 `DEADLINE_EXCEEDED` / `CANCELED`。事件处理无响应,可通过 `Config.OnEventError` 在本地观察失败;回调纳入排空等待。对端的公开错误仍是外部输入,日志不要自动输出其正文。
## 有界执行和退出
| 配置 | 默认 | 含义 |
| --- | --- | --- |
| `MaxMessageBytes` | 1 MiB | 完整 JSON envelope 的大小上限,应与底层 WS 消息限额匹配 |
| `MaxPending` | 128 | 本端等待响应的调用上限 |
| `MaxConcurrent` | 32 | 同时执行的入站调用及事件总数 |
| `CallTimeout` | 30s | 入站业务及出站 Call 的最大预算 |
| `WriteTimeout` | 5s | 事件和响应的写入期限 |
超出 pending 返回 `ErrBusy`;对端超出入站执行容量时,调用收到 `BUSY`,事件导致连接关闭,避免静默丢失。handler 必须响应 context;Go 不能强杀不退出的业务 goroutine。`Stop` 停止接收协议工作并取消 pending/incoming;`Wait` 等待所有入站 handler 和事件错误回调实际退出;`Shutdown` 额外发起 Session 关闭,但 Session 的最终关闭结果仍通过其 `Wait` 或所属 Client/Server.Shutdown 检查。
`Peer.Handlers().OnClose` 将上述任务接入 Session 完成屏障;若自定义包装 Handlers,必须保留这一回调。不要从本 Peer 的 Handler 内调用 `Wait/Shutdown`,可以调用 `Stop` 请求退出。关闭超时后,按框架生命周期规则保留仍被业务任务使用的依赖。
## 订阅和重连恢复
`Subscriptions` 管理本地**期望订阅**;服务端使用普通 RPC 路由处理加入/退出,执行自己的授权并可结合 `ws/hub`。默认方法为 `rpc.subscribe` / `rpc.unsubscribe`,可配置;此模块不预设用户、租户或主题权限。
```go
subscriptions, err := rpc.NewSubscriptions(rpc.SubscriptionConfig{})
if err != nil { return err }
// nil Peer 仅保存期望状态,适合首次连接前配置。
err = subscriptions.Subscribe(ctx, nil, "updates", map[string]any{"filter": "all"})
// 每次连接创建新 Peer,WaitReady 后从 OnOpen 之外显式恢复。
restored, err := subscriptions.Restore(ctx, newPeer)
// 在线新增/移除同时发出 RPC 并等待确认。
err = subscriptions.Subscribe(ctx, newPeer, "metrics", nil)
err = subscriptions.Unsubscribe(ctx, newPeer, "updates")
keys, err := subscriptions.Keys(ctx)
_, _ = restored, keys
```
订阅请求形如 `{"key":"updates","params":{...}}`;取消请求形如 `{"key":"updates"}`。服务端应将 key 的授权绑定到已验证的连接身份,每次恢复重新验证。相同连接中同一 key 的订阅应幂等;同 key 参数改变表示更新期望条件,由服务端定义其通用处理规则。
本地 Subscribe 在发送前保存期望状态,即使远端拒绝仍保留,应用必须检查错误并决定移除或以后重试;Unsubscribe 在发送前移除本地期望状态,若确认失败,当前连接仍可能收到事件,直至远端移除或连接关闭。Restore 按 key 排序,遇首个错误停止并返回已确认数量。操作串行且等待可通过 context 取消,防止并发恢复和移除颠倒状态。
默认最多 256 个订阅,每个 params 最多 16 KiB。key 是最多 256 字节的 UTF-8 不透明字符串,禁止 CR/LF/NUL。恢复只恢复订阅关系,断线期间事件不会补发。若业务需要历史事件,使用业务游标和持久化消息机制另行实现。
## 验证
```sh
go test -race ./ws/rpc
go test ./ws/rpc -run '^$' -fuzz FuzzProtocolDecode -fuzztime=10s
go test ./ws/rpc -run '^$' -bench . -benchmem
```
单元测试覆盖双向嵌套调用、并发上限、取消、迟到回复、重复键/层数/长度、错误脱敏、业务退出屏障、订阅恢复和真实 TCP 握手/关闭。`BenchmarkPeerCall` 是内存传输上的协议开销基准,不代表真实网络吞吐;`BenchmarkProtocolDecode` 测协议解析与严格输入校验。