7.2 KiB
WebSocket 双向调用与订阅
ws/rpc 是可选的 JSON 消息协议。普通 ws 的文本/二进制消息不依赖此协议。双端必须协商 rpc.Subprotocol(go-omni.rpc.v1),客户端和服务端都可发起调用、处理调用和发送事件。它不承诺离线消息、断线重放、事务回滚或 exactly-once。
调用
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。
协议和错误
{"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,可配置;此模块不预设用户、租户或主题权限。
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。恢复只恢复订阅关系,断线期间事件不会补发。若业务需要历史事件,使用业务游标和持久化消息机制另行实现。
验证
go test -race ./ws/rpc
go test ./ws/rpc -run '^$' -fuzz FuzzProtocolDecode -fuzztime=10s
go test ./ws/rpc -run '^$' -bench . -benchmem
单元测试覆盖双向嵌套调用、并发上限、取消、迟到回复、重复键/层数/长度、错误脱敏、业务退出屏障、订阅恢复和真实 TCP 握手/关闭。BenchmarkPeerCall 是内存传输上的协议开销基准,不代表真实网络吞吐;BenchmarkProtocolDecode 测协议解析与严格输入校验。