Files
go-omni/ws/rpc/README.md

99 lines
7.2 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/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` 测协议解析与严格输入校验。