# IGP Unity SDK - KCP 心跳与 RTT 指南

## 目标

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

- hosted data plane / KCP 连接状态
- 心跳保活
- App RTT / KCP RTT 统计
- 网络质量 UI

本指南把 RTT 分成两层：

- **App RTT**：SDK 应用层 heartbeat `ping/pong` 往返时间，也就是旧的 `KcpRTTStats` 语义。它会受可靠业务消息排队、主线程调度、服务端处理和客户端处理影响。
- **KCP RTT**：KCP ACK 层内部的 `srtt/rto/rttVar`，来自 KCP 对底层 segment ACK 的估计，更适合判断底层 UDP/KCP 链路延迟。`srtt=0` 表示还没有可用 ACK RTT 样本，不应当解读成 0ms。

## 最短用法

```csharp
using UnityEngine;
using IGP.UnitySDK;

public class NetworkHealthProbe : MonoBehaviour
{
    [SerializeField] private IGPRuntimeManager 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 相关字段

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

- `Enable KCP`
- `Kcp Heartbeat Interval`
- `Enable Kcp Heartbeat`
- `Kcp Max Datagrams Per Tick`
- `Show Network Diagnostics`

建议默认先保持：

- `Enable KCP = true`
- `Enable Kcp Heartbeat = true`
- `Kcp Heartbeat Interval = 1.0`
- `Kcp Max Datagrams Per Tick = 256`

## 常用运行时状态

| 属性 | 作用 |
| --- | --- |
| `IsHostedDataPlaneAttached` | 宿主数据面是否已经附着 |
| `IsKcpConnected` | 当前数据面是否建立连接 |
| `IsKcpAlive` | 心跳是否仍然健康 |
| `KcpHealthState` | 数据面健康状态：`Disconnected`、`Handshaking`、`Healthy`、`Suspect`、`Stalled` |
| `KcpLastFaultReason` | 最近一次确定的 KCP transport fault；新连接建立后恢复为 `None` |
| `KcpRTTStats` | 兼容属性：应用层 heartbeat ping/pong RTT，也就是 App RTT |
| `AppRTTStats` | 推荐的新名称：应用层 heartbeat ping/pong RTT |
| `KcpTransportStats` | KCP ACK/队列和 UDP datagram 统计快照，包含 `SmoothedRttMs`、`RtoMs`、`WaitSnd`、`SndQueue`、`SndBuf`、`DatagramsIn`、`DatagramsOut`、`DatagramBytesIn`、`DatagramBytesOut` 等 |
| `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` | 应用层 heartbeat `ping/pong` 的平均 RTT，来自最近一批样本 |
| `App RTT` | `last` | 最近一次应用层 heartbeat RTT |
| `App RTT` | `max` | 当前样本窗口里最大的应用层 heartbeat 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 -> Debug Logging` 里选择日志等级，或运行时修改挂载的配置：

```csharp
runtimeManager.Config.debugLogging = IGPLogLevel.Debug;
```

日志等级使用最低等级语义：选择 `Warning` 时会记录 `Warning` 和 `Error`；选择 `Debug` 时会包含每条 KCP / P2P 收发摘要。

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

P2P 日志使用明确的 `localPlayerId`、`sourcePlayerId`、`targetPlayerId`
字段。默认不输出完整 payload，只保留 `messageType`、`reliable`、
`payloadBytes`、必要时的 `base64Bytes`、`checksum`、`reliableMessageId`、
`chunkIndex` 和 `chunkCount`。

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

- `scope=kcp`：hosted data plane、UDP/KCP 握手、KCP 内部队列、RTT 和心跳
- `scope=udp-unreliable`：raw UDP unreliable lane 的握手、收发和流量异常
- `scope=p2p`：P2P payload、可靠大消息分片 / 重组、接收队列
- `scope=hosted` / `scope=host-session` / `scope=hosted-realtime`：hosted session 控制面和 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` |
| `send-dropped` / `send-rejected` | 消息没有进入 KCP，优先看 `reason` 和 `result` |
| `udp-read` | 本帧读到了 UDP datagram，会带 `tickDatagrams` 和 `tickBytes` |
| `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 秒汇总一次 |
| `network-anomaly kind=kcp-send-backpressure` | `WaitSnd >= 1024`，新的业务发送被拒绝；日志包含累计 dropped send 和 KCP 队列状态 |
| `kcp-input-failed` | UDP datagram 无法被 KCP 解析，常见于 endpoint/token/协议不匹配 |
| `kcp-receive-drained` | KCP 收到并拆出了上层 frame |
| `ping-sent` / `heartbeat-pong` | 心跳和 RTT 样本正常循环 |
| `state` | 节流后的状态摘要，用来看积压、RTT、窗口和计数 |
| `scope=mirror-transport event=configuration` | 每个 MirrorTransport 组件生命周期只记录一次有效 batching 和 channel 配置 |

## 网络异常 Warning

SDK 会对已经采集到的网络指标做结构化判断。高 RTT、高流量和 Mirror payload/queue 告警是 `action=observe-only`；协议序号与 Pong 连续 3 秒无进展时进入 `Suspect`；KCP dead-link、fatal socket error 或硬队列溢出会关闭当前 data-plane、进入 `ConnectionFailed` 并通过 `onError` 上报。绑定的 Mirror Transport 会立即清空待派发的入站消息，并向对应的客户端和服务端连接派发一次断开回调。入站 datagram 会先批量完成 KCP Input，再在本 Tick 的正常 KCP Update 中统一发送 ACK 和推进已有发送队列。

| `kind` / 日志 | 触发条件 |
| --- | --- |
| `high-latency` | App 最近 RTT 或 KCP `srtt` 达到 `200ms`；两者都回落到 `150ms` 以下并稳定 5 秒后记录一次恢复 |
| `high-traffic` | KCP 或 raw UDP 任一方向达到 `1000 datagrams/s`，或 `1 MiB/s`；回落到 `750 datagrams/s` 且 `768 KiB/s` 以下并稳定 5 秒后记录一次恢复 |
| `udp-read-budget-exhausted` | KCP 单 Tick 越过 256 软预算并达到 1.5ms 额外时间预算或 2048 硬上限；该事件进入 `Suspect` |
| `udp-send-would-block` / `udp-receive-would-block` | socket 暂时无法立即完成操作，不触发断线；日志为限流后的次数汇总 |
| `kcp-send-backpressure` | `WaitSnd >= 1024` 时拒绝新的业务消息，避免 `snd_queue` 无界增长 |
| `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` | 底层是否建立、握手是否完成、心跳是否健康 |
| `pendingPings` | 已发出但尚未收到 pong 的心跳数 |
| `limits(...)` | 当前协商出的 payload / frame / reliable message 尺寸上限 |
| `appRtt(...)` | 应用层 heartbeat ping/pong RTT 样本、last / avg / min / max |
| `maxDatagramsPerTick` | 每次 KCP Tick 的 UDP datagram 软预算；积压时仍可继续读取到时间/硬上限 |
| `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 计数 |

如果需要把同一份摘要写入游戏自己的诊断导出，可以读取 `runtimeManager.KcpClient?.DiagnosticsSummary`。

排查延迟时建议先看分叉：`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 回来很慢或丢包严重
- `rcvBuf` 持续增长：收到的 segment 顺序不连续，可能有丢包或抖动
- `pendingPings` 持续增长且没有 `heartbeat-pong`：心跳回包没有回来，`IsKcpAlive` 可能即将变 false
- `send-dropped reason=not-ready`：上层过早发送，需要等 `IsKcpConnected == true`

## 根据状态调节心跳

```csharp
using IGP.UnitySDK;

public sealed class AdaptiveHeartbeat : MonoBehaviour
{
    [SerializeField] private IGPRuntimeManager runtimeManager;

    public void EnterBattle()
    {
        runtimeManager.SetHeartbeatIntervalForGameState(GameState.InBattle);
    }

    public void EnterMatch()
    {
        runtimeManager.SetHeartbeatIntervalForGameState(GameState.InGame);
    }

    public void EnterLobby()
    {
        runtimeManager.SetHeartbeatIntervalForGameState(GameState.Idle);
    }
}
```

推荐值：

- `Idle`: 5s
- `InGame`: 1s
- `InBattle`: 100ms

## UI 展示建议

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

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

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

public sealed class NetworkHud : MonoBehaviour
{
    [SerializeField] private IGPRuntimeManager 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
- 在战斗态和大厅态之间切换心跳频率

避免：

- 把心跳间隔压到 `50ms` 以下
- 只看 `IsKcpConnected`，不看 `IsKcpAlive`
- 忽略高 RTT 对玩法同步的影响

## 对应 sample

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

- `Samples~/HostedPlayground`

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

- `Samples~/HostedQuickstart`
