跳到主要内容

状态化重连

状态化重连是 ASP.NET Core 的 ack 协议:传输断开时,逻辑连接——包括在途调用、 流与出站缓冲——得以保留。连接断开期间发送的消息会被恰好一次重放, 接收方会过滤重复消息。

为什么重要

普通重连会重新协商并重启逻辑连接:挂起的调用失败,socket 断开时"在途"的消息 丢失。状态化重连保留同一个连接 token 与逻辑状态,因此:

  • 长运行的 stream() 在断线后不丢不重地继续。
  • 客户端断线期间发出的广播,在它回来后送达。
  • 已发送但未确认的请求会重发。

开启方式

客户端

connection = HubConnection().with_stateful_reconnect()

服务器

server = SignalRServer(
ChatHub,
options=ServerOptions(
allow_stateful_reconnects=True,
stateful_reconnect_buffer_size=100_000, # 字节
stateful_reconnect_timeout=30.0, # 秒
),
)

状态化重连要求 WebSockets 传输。客户端发送协商标志 useStatefulReconnect=true;服务器同意则回应 useStatefulReconnect: true, 握手使用协议版本 2

工作原理

序号

每个可跟踪的出站消息(invocation、stream item、completion、cancel)被分配 从 1 开始单调递增的序号。Ping、ack、sequence 与 close 消息不计数

缓冲

未确认的消息在发送端缓冲,直到对端确认。缓冲以字节为上限 (stateful_reconnect_buffer_size,默认 100,000);写者在 Ack 释放空间前 因背压阻塞。

Ack

收到可跟踪消息后,双方各发送携带最新已收序号(~1s 防抖,与参考实现一致)的 Ack。发送端随即丢弃所有序号 ≤ ack 的缓冲消息。

[8, 7] # "我已经收到序号 7 为止"

Sequence 与重放

传输重连时,重连方先发送 Sequence 消息告知对端重放将开始的序号,然后重放缓冲:

[9, 5] # "你接下来将看到序号 5"
[1, ...] # 重放的 invocation(seq 5)
[2, ...] # 重放的 stream item(seq 6)

去重

接收方跟踪已处理的序号。传输重连发生后,它会停止接收可跟踪消息,直到 Sequence 重置计数器;序号已被处理过的重放消息会被丢弃。即使 Ack 在线上 丢失,这也保证了恰好一次投递。

握手

客户端 服务器
| negotiate?useStatefulReconnect=true |
|--------------------------------------->|
| {connectionToken, useStatefulReconnect: true, ...} |
|<---------------------------------------|
| WebSocket 连接 (?id=connectionToken) |
|--------------------------------------->|
| handshake {"protocol": "json", "version": 2} |
|--------------------------------------->|
| {}(握手成功) |
|<---------------------------------------|

只有完成版本 2 握手后,双方才会创建消息缓冲。

服务器侧生命周期

  1. WebSocket 传输断开。
  2. 服务器保留连接记录(及其缓冲)stateful_reconnect_timeout 秒。
  3. 客户端重连到同一个 URL(复用 connectionToken)。
  4. 服务器重新挂载传输,发送 Sequence + 重放缓冲消息。
  5. 客户端逾期未归则拆除连接,触发 on_disconnected

客户端侧生命周期

  1. 传输断开。
  2. 客户端把缓冲标记为已断开,停止接收非 sequence 消息。
  3. 重连到保存的连接 URL(同一 token,不重新协商、不重新握手)。
  4. 发送 Sequence + 重放自己的缓冲消息。
  5. 若服务器在服务端超时内应答,连接继续;否则回落到普通重连(如配置了 重试策略)或关闭。

与自动重连组合

connection = (
HubConnection()
.with_stateful_reconnect()
.with_retry_policy(DefaultRetryPolicy())
)

状态化传输重连失败时,客户端回落到普通重连流程(会重新协商并使挂起调用失败)。

互操作

aiosignalr 已针对参考实现做过测试:

  • aiosignalr 客户端 ↔ 真实 ASP.NET Core SignalR 服务器 (AllowStatefulReconnects = true)。
  • 真实 Microsoft.AspNetCore.SignalR.ClientWithStatefulReconnect()) ↔ aiosignalr 服务器。

本地运行:

uv run pytest tests/interop # 需要 dotnet

设计说明

  • 共享的 MessageBufferaiosignalr.message_buffer)同时实现两个方向, 对齐参考实现的 MessageBuffer.cs / MessageBuffer.ts
  • 广播 fanout 每条协议编码一次,并喂给每个接收者的缓冲,因此断线但仍"存活" 的客户端重连后仍能收到消息。
  • 缓冲溢出采用背压而不是丢消息——代价是阻塞写者直到 Ack 释放空间 (上限 5s,然后继续写入)。