# IGP Unity SDK - KCP 空闲保活与 RTT 指南

## 目标

这份指南说明如何在 Unity 游戏里读取和利用：

- hosted data plane / KCP 连接状态
- SDK 内部 KCP 空闲保活
- App RTT / KCP RTT 统计
- 网络质量 UI

KCP 属于 `cn.indiegp.sdk.unity.multiplayer`。场景中需要同时放置
`IGPRuntimeManager` 和 `IGPMultiplayerRuntime`；调用一次
`IGPRuntimeManager.InitializeAsync()` 后，Multiplayer 会在 hosted session
和 Room/Player context 有效时自动建立数据面。下文所有网络状态都从
`IGPMultiplayerRuntime` 读取。

本指南把 RTT 分成两层：

- **App RTT**：当前 reliable transport 的 SDK 内部 idle probe `ping/pong` 往返时间，由 `AppRTTStats` 提供。KCP/TCP 活跃流量会抑制额外 probe，因此样本可能暂时不更新；该指标会受主线程调度、服务端处理和客户端处理影响。`KcpRTTStats` 只在当前 transport 为 KCP 时可用。
- **KCP RTT**：KCP ACK 层内部的 `srtt/rto/rttVar`，来自 KCP 对底层 segment ACK 的估计，更适合判断底层 UDP/KCP 链路延迟。`srtt=0` 表示还没有可用 ACK RTT 样本，不应当解读成 0ms。

## 最短用法

```csharp
using UnityEngine;
using IGP.Multiplayer;

public class NetworkHealthProbe : MonoBehaviour
{
    [SerializeField] private IGPMultiplayerRuntime runtimeManager;

    void Update()
    {
        var appStats = runtimeManager.AppRTTStats;
        var kcpStats = runtimeManager.KcpTransportStats;
        var appRtt = appStats != null && appStats.SampleCount > 0
            ? $"{appStats.AvgRTT * 1000f:F0}ms"
            : "--";
        var kcpRtt = kcpStats.HasSmoothedRtt ? $"{kcpStats.SmoothedRttMs}ms" : "--";
        var waitSnd = kcpStats.IsAvailable ? kcpStats.WaitSnd.ToString() : "--";

        Debug.Log($"KCP alive={runtimeManager.IsKcpAlive}, appRTT={appRtt}, kcpSrtt={kcpRtt}, waitSnd={waitSnd}");
    }
}
```

## Inspector 相关字段

`IGPMultiplayerRuntime` 当前包含这些网络相关配置：

- `Kcp Max Datagrams Per Tick`
- `Kcp Send Window Size`
- `Kcp Receive Window Size`
- `Show Network Diagnostics`

建议默认先保持：

- `Kcp Max Datagrams Per Tick = 128`
- `Kcp Send Window Size = 256`
- `Kcp Receive Window Size = 256`

KCP 保活、发送停滞阈值、发送队列容量和每 Tick 注入预算属于 SDK 内部策略，不在 Inspector
或公共 API 中开放。连接认证后，KCP 空闲 1 秒会发送一个 reliable control probe；活跃流量、待发送数据和待 ACK 数据都会抑制额外 probe。当前固定策略为 3 秒进入 `SendSuspect`、15 秒触发 fault，
发送队列最多 256 组/4 MiB，每 Tick 最多注入 64 个新 KCP 分片/128 KiB。

当前 KCP 使用 `nocwnd: true`，因此 KCP 自带的 `cwnd` 拥塞窗口不参与发送限速。SDK 上层实现的是有界队列、`WaitSnd` 高低水位、单 Tick 注入预算和停滞检测，属于背压与流量整形，不是另一套基于 RTT 或丢包动态调窗的拥塞控制。Mirror reliable application frame 另有 `ReliableSendRate`，默认 `30 Hz`、限制 `1..60 Hz`；它不改变 KCP 的 10ms 协议更新间隔。

## 常用运行时状态

| 属性 | 作用 |
| --- | --- |
| `IsRealtimeReady` | 当前 RoomNode KCP/TCP reliable handshake 是否完成 |
| `CurrentReliableTransport` | 当前为 `Kcp`、`Tcp` 或尚未连接时的 `null` |
| `IsReliableConnected` / `IsReliableAlive` | transport-neutral reliable 状态 |
| `RealtimeConnectionState` | 当前 descriptor 请求、握手、Ready 或 Failed 状态 |
| `IsKcpConnected` | 当前数据面是否建立连接 |
| `IsKcpAlive` | 仅在实际使用 KCP 时报告 KCP 是否已连接、无确定 fault 且未进入 `SendSuspect`；TCP 模式恒为 `false` |
| `KcpHealthState` | 数据面健康状态：`Disconnected`、`Handshaking`、`Healthy`、`Suspect`、`Stalled` |
| `KcpLastFaultReason` | 最近一次确定的 KCP transport fault；新连接建立后恢复为 `None` |
| `KcpRTTStats` | 应用层 idle probe ping/pong RTT 的别名 |
| `AppRTTStats` | 应用层 idle probe ping/pong RTT；活跃期可能暂时没有新样本 |
| `KcpTransportStats` | KCP ACK、KCP 队列、SDK 待发送队列和 UDP datagram 统计快照，包含 `WaitSnd`、`PendingSendGroups`、`PendingSendBytes`、`SendBackpressured`、发送/接收进展年龄和收发计数等 |
| `KcpMaxDatagramsPerTick` | 每次 KCP Tick 的 UDP datagram 软预算；积压时可继续读取到 1.5ms 额外预算或 2048 硬上限 |

## 网络诊断浮层字段速查

开启 `Show Network Diagnostics` 后，内置小浮窗会显示 KCP 健康状态、最近故障原因、RTT、队列和 UDP 统计。即使尚未产生 RTT 样本或数据面尚未连接，浮层也会显示当前状态：

| 显示项 | 字段 | 含义 |
| --- | --- | --- |
| `KCP health` | 状态 | 当前 `KcpHealthState`：`Disconnected`、`Handshaking`、`Healthy`、`Suspect` 或 `Stalled` |
| `Last fault` | 原因 | 当前 `KcpLastFaultReason`；没有确定故障时为 `None` |
| `App RTT` | `avg` | 应用层 idle probe `ping/pong` 的平均 RTT，来自最近一批样本 |
| `App RTT` | `last` | 最近一次 idle probe RTT |
| `App RTT` | `max` | 当前样本窗口里最大的 idle probe RTT |
| `KCP RTT` | `srtt` | KCP ACK 层内部的平滑 RTT；`--` 表示还没有 ACK RTT 样本 |
| `KCP RTT` | `rto` | KCP 当前重传超时时间，通常会高于 `srtt` |
| `Queue` | `wait` | KCP `WaitSnd` 最近 1 秒采样平均值，等待发送或等待 ACK 的 segment 总数 |
| `Queue` | `q` | KCP `sndQueue` 最近 1 秒采样平均值，还没有进入发送窗口的 segment 数 |
| `Queue` | `buf` | KCP `sndBuf` 最近 1 秒采样平均值，已发出、正在等待 ACK 的 segment 数 |
| `Q peak` | `wait` / `q` / `buf` | 左上角浮窗为了便于肉眼观察，会显示最近 5 秒采样窗口里的 Queue 尖峰值 |
| `UDP` | `in` / `out` | socket 层 UDP datagram 每秒速率，不是 KCP segment 数 |
| `UDP pkts` | `in` / `out` | 当前连接生命周期内累计收到 / 发出的 UDP datagram 数；大数会用 `k` / `m` / `b` 紧凑显示 |
| `UDP bytes` | `in` / `out` | 当前连接生命周期内累计收到 / 发出的 UDP datagram 字节数；这是 socket 层字节数，不是业务 payload 字节数 |

## 底层 KCP 诊断日志

排查 hosted data plane / KCP 链路时，在 `IGPConfig -> Log Level` 里选择日志等级，或运行时修改挂载的配置：

```csharp
if (runtimeManager.Config != null)
{
    runtimeManager.Config.debugLogging = IGP.UnitySDK.IGPLogLevel.Debug;
}
```

日志等级使用最低等级语义：选择 `Warning` 时会记录 `Warning` 和 `Error`；选择 `Debug` 时会包含低频连接与控制流程细节，但不会逐条打印 KCP datagram、probe、pong 或 P2P payload。

`[IGP SDK]` 日志会带同一次进程运行内稳定的 `runId`。SDK 启动时会输出
`event=session-start`，包含 `role`、`localPlayerId`、`roomId`、`appId`、
`sdkVersion`、`appVersion`、`platform`、`transport` 和 `endpoint`，方便和
desktop / server 日志对齐。

成功的 P2P 收发不写日志。需要持续观察 payload 数量、字节数、RTT 和队列时，读取 `KcpTransportStats` 或使用诊断浮层；异常和失败仍会记录必要的 player、message type、字节数与队列摘要。

内部网络日志统一使用 `[IGP SDK]` 前缀，并带 `timestamp`、`runId`、`level`、`scope`、`path` 和 `event` 字段：

- `scope=kcp`：hosted data plane、UDP/KCP 握手、KCP 内部队列、RTT 和 idle probe
- `scope=udp-unreliable`：UDP unreliable lane 的握手、收发和流量异常
- `scope=p2p`：P2P payload、可靠大消息分片 / 重组、接收队列
- `scope=hosted` / `scope=host-session`：hosted session 房间控制面
- `scope=realtime` / `scope=hosted-data-plane`：RoomNode descriptor 请求和 data plane attach
- `scope=mirror-transport`：Mirror 适配层连接、Mirror payload 队列和 Mirror channel

常见 KCP 日志事件：

| event | 怎么看 |
| --- | --- |
| `event=start path=connect` | SDK 已解析 KCP endpoint 并开始创建 UDP/KCP 连接 |
| `event=queued path=handshake` | KCP handshake 已经写入底层发送队列 |
| `event=datagram-sent path=handshake` | 至少一个 UDP datagram 已经发出 |
| `event=ack path=handshake` | 服务端已回应，`IsKcpConnected` 应该转为 `true` |
| `event=enabled path=idle-probe` | 内部空闲保活策略已启用；固定空闲阈值 1 秒、最多 4 个未完成 probe |
| `send-dropped` / `send-rejected` | 消息没有进入 KCP，优先看 `reason` 和 `result` |
| `network-anomaly kind=udp-read-budget-exhausted` | 本帧达到 UDP datagram 读取预算，可结合 `Kcp Max Datagrams Per Tick` 判断是否需要提高预算 |
| `network-anomaly kind=udp-send-would-block` / `udp-receive-would-block` | 非阻塞 socket 暂时不可写/读；首次立即记录，持续发生最多每 30 秒汇总一次 |
| `event=entered/recovered path=backpressure` | `WaitSnd` 达到由发送窗口派生的高/低水位；默认窗口 256 对应 512/256 |
| `event=send-suspect-entered/recovered` | 存在待确认数据且连续 3 秒没有出站 ACK 进展；反向 payload 或 Pong 不会清除此状态 |
| `event=transport-fault reason=SendProgressTimeout` | 出站连续 15 秒没有 ACK 进展，当前 data plane 被终止 |
| `event=sample path=sample` | 每 5 秒一条 Debug 窗口采样；同时查看流量 delta、KCP sequence/queue/RTT、idle probe、Unity Tick gap、Socket 错误和 Mirror application-frame 队列 |
| `network-anomaly kind=arena-pong-response-delayed` | probe 已被 KCP ACK，但 Arena 的 pong 超过 3 秒未返回；这是 Arena 响应/控制写队列诊断，不会单独断开 data plane |
| `kcp-input-failed` | UDP datagram 无法被 KCP 解析，常见于 endpoint/token/协议不匹配 |
| `state level=busy/critical` | KCP 队列达到异常水位时输出的节流摘要，用来看积压、RTT、窗口和计数 |
| `scope=mirror-transport event=configuration` | 每个 MirrorTransport 组件生命周期只记录一次有效 send rate、batching 和 channel 配置 |
| `scope=mirror-transport event=application-frame-backpressure` | 底层暂时 `WouldBlock`；已构建 frame 保留并重试，不断开 Mirror peer |
| `scope=mirror-transport event=outbound-queue-fault` | 某个 peer 达到 1024 条、4 MiB 或 15 秒限制，只断开该逻辑 peer |

## 网络异常 Warning

SDK 会分别跟踪出站 ACK 进展和入站协议进展。存在待确认数据且出站 3 秒没有进展时进入 `Suspect` 并拒绝新的业务可靠消息；出站 15 秒没有进展时以 `SendProgressTimeout` 关闭当前 data plane。反向 payload、Pong、继续调用发送或 Socket 接受 datagram 都不会重置出站计时。故障会进入 `Failed`，通过一次 `ConnectionChanged` 和一次 `ErrorOccurred` 通知应用；下一次高层 realtime 调用可以请求新的 descriptor。绑定的 Mirror Transport 会转成对应的断开回调。

| `kind` / 日志 | 触发条件 |
| --- | --- |
| `high-latency` | App 最近 RTT 或 KCP `srtt` 达到 `200ms`；两者都回落到 `150ms` 以下并稳定 5 秒后记录一次恢复 |
| `high-traffic` | KCP 或 UDP 任一方向达到 `1000 datagrams/s`，或 `1 MiB/s`；回落到 `750 datagrams/s` 且 `768 KiB/s` 以下并稳定 5 秒后记录一次恢复 |
| `udp-read-budget-exhausted` | KCP 单 Tick 越过默认 128 软预算并达到 1.5ms 额外时间预算或 2048 硬上限；该事件进入 `Suspect` |
| `udp-send-would-block` / `udp-receive-would-block` | socket 暂时无法立即完成操作，不触发断线；日志为限流后的次数汇总 |
| `arena-pong-response-delayed` | probe 已获 KCP ACK，但 Arena pong 仍未返回；首次立即记录，持续异常每 30 秒最多一条 |
| `backpressure` | `WaitSnd` 达到 `min(1024, 2 * sendWindow)` 时拒绝新业务消息，降到高水位一半后恢复 |
| `state level=busy` | KCP 收发 queue / buffer 总量达到 `256` |
| `state level=critical` | KCP 收发 queue / buffer 总量达到 `1024` |

异常首次出现时立即记录 `phase=started`，持续异常最多每 30 秒记录一条 `phase=ongoing` 汇总。高延迟和高流量回落并稳定后会记录一次 `phase=recovered`；读取预算耗尽、大包等离散事件只做节流汇总。因此高频包或持续高延迟不会产生逐包、逐帧 Warning。

`state` 摘要里最常用的字段：

| 字段 | 含义 |
| --- | --- |
| `connected` / `authenticated` / `alive` | 底层是否建立、握手是否完成、是否未进入发送停滞 |
| `idleProbe(...)` | 固定间隔、KCP 空闲年龄、probe 尝试/接受/pong/淘汰计数和未完成数量 |
| `limits(...)` | 当前协商出的 payload / frame / reliable message 尺寸上限 |
| `appRtt(...)` | 应用层 idle probe ping/pong RTT 样本、last / avg / min / max |
| `maxDatagramsPerTick` | 每次 KCP Tick 的 UDP datagram 软预算；积压时仍可继续读取到时间/硬上限 |
| `pendingSendGroups` / `pendingSendBytes` | 已被 SDK 原子接受、尚未完全注入 KCP 的逻辑消息组和编码字节数 |
| `sendProgressAgeMs` / `receiveProgressAgeMs` | 出站 ACK 进展和入站协议进展各自距当前的时间 |
| `kcp(waitSnd=...)` | KCP 已等待发送或待 ACK 的 segment 总数 |
| `sndQueue` / `sndBuf` | 尚未进窗口的发送队列 / 已发出等 ACK 的发送缓冲 |
| `rcvQueue` / `rcvBuf` | 已可读的接收队列 / 等待补齐顺序的接收缓冲 |
| `remoteWnd` / `cwnd` | 远端接收窗口 / 本地拥塞窗口 |
| `rto` / `srtt` / `rttVar` | KCP ACK 层内部重传退避和平滑 RTT 数据 |
| `totals(...)` | 连接生命周期内的 datagram、frame、error、dropped send、socket accepted/would-block/fatal 和 KCP Input error 计数 |

`DiagnosticsSummary` 是 SDK 内部日志摘要，不属于公共 API。游戏自己的诊断导出应读取
`runtimeManager.KcpTransportStats`，并结合 `KcpHealthState`、`KcpLastFaultReason`
和 `AppRTTStats` 生成摘要。

排查延迟时建议先看分叉：`appRtt` 高而 `srtt/rto` 不高，通常说明延迟更可能在可靠业务队列、服务端处理、客户端主线程或应用层收包路径；`srtt/rto` 同时升高，才更像底层 UDP/KCP 链路本身变慢或丢包重传。

粗略判断方向：

- 有 `event=queued path=handshake` 但没有 `event=datagram-sent path=handshake`：本地 UDP 发送失败或 socket 未正常创建
- 有 `event=datagram-sent path=handshake` 但没有 `event=ack path=handshake`：服务端没回应，检查 endpoint、token、防火墙和服务端日志
- 持续出现 `kind=udp-read-budget-exhausted`：本地每帧 UDP 读取预算被打满，可以提高 `Kcp Max Datagrams Per Tick`，同时观察主线程耗时
- `waitSnd` / `sndBuf` 持续增长：消息已经进 KCP，但 ACK 回来很慢或丢包严重
- `send-rejected result=WouldBlock`：SDK 已进入背压；整条逻辑消息均未提交，应用应停止继续写入并等待 `is_writable`
- `rcvBuf` 持续增长：收到的 segment 顺序不连续，可能有丢包或抖动
- `idleProbe.pending` 增长且 `waitSnd/sndBuf` 同时增长：probe 没有获得 ACK，继续看 `send-suspect-entered` 和 15 秒 fault
- `arena-pong-response-delayed` 且 `kcpAckProgressing=true`：底层 KCP 已确认发送，问题位于 Arena pong 响应或控制写队列
- `send-dropped reason=not-ready`：上层过早发送，需要等 `IsKcpConnected == true`

## UI 展示建议

开发期可以直接启用 `Show Network Diagnostics`。

正式游戏里更推荐自己接一层 UGUI：

```csharp
using UnityEngine;
using UnityEngine.UI;
using IGP.Multiplayer;

public sealed class NetworkHud : MonoBehaviour
{
    [SerializeField] private IGPMultiplayerRuntime runtimeManager;
    [SerializeField] private Text rttText;

    void Update()
    {
        var appStats = runtimeManager.AppRTTStats;
        var kcpStats = runtimeManager.KcpTransportStats;
        var appText = appStats != null && appStats.SampleCount > 0
            ? $"App RTT: {appStats.AvgRTT * 1000f:F0}ms"
            : "App RTT: measuring";
        var kcpText = kcpStats.HasSmoothedRtt
            ? $"KCP RTT: srtt {kcpStats.SmoothedRttMs}ms | rto {kcpStats.RtoMs}ms"
            : "KCP RTT: --";
        var queueText = kcpStats.IsAvailable
            ? $"Queue: wait {kcpStats.WaitSnd} | q {kcpStats.SndQueue} | buf {kcpStats.SndBuf}"
            : "Queue: --";
        var udpText = kcpStats.IsAvailable
            ? $"UDP: in {kcpStats.DatagramsInPerSecond:F0}/s | out {kcpStats.DatagramsOutPerSecond:F0}/s"
            : "UDP: --";
        var udpPacketsText = kcpStats.IsAvailable
            ? $"UDP pkts: in {kcpStats.DatagramsIn} | out {kcpStats.DatagramsOut}"
            : "UDP pkts: --";
        var udpBytesText = kcpStats.IsAvailable
            ? $"UDP bytes: in {kcpStats.DatagramBytesIn} | out {kcpStats.DatagramBytesOut}"
            : "UDP bytes: --";

        rttText.text = $"{appText}\n{kcpText}\n{queueText}\n{udpText}\n{udpPacketsText}\n{udpBytesText}";
    }
}
```

## 开发建议

推荐：

- 把 `IsKcpAlive` 当成游戏内连接健康信号
- 把 `AppRTTStats` 和 `KcpTransportStats` 一起接到调试 HUD
- 活跃期优先使用 `KcpTransportStats.SmoothedRttMs`，不要要求 idle probe 持续产生 App RTT

避免：

- 在应用层重复实现 KCP ping/pong 或周期保活
- 只看 `IsKcpConnected`，不看 `IsKcpAlive`
- 忽略高 RTT 对玩法同步的影响

## 对应 sample

如果你需要一个带 UI 和日志的综合入口，先看：

- `Samples~/HostedPlayground`

如果你只想验证最小链路，先看：

- `Samples~/HostedQuickstart`
