RTP Keepalive

Abstract

RTP Keepalive

Authors

Walter Fan

Status

v1.0

Updated

2026-03-20

概述

在实时通信中,RTP/RTCP 流通常需要穿越 NAT (Network Address Translation) 设备。NAT 设备会为每个通过它的 UDP 流维护一个映射表项 (binding),将内部地址和端口映射到外部地址和端口。然而,这些映射表项并非永久存在——如果在一段时间内没有数据包通过该映射,NAT 设备会将其删除,导致后续的数据包无法正确路由,从而造成通信中断。

RTP Keepalive 机制的核心目的就是:在媒体流暂时静默(例如音频静音、视频暂停)时,定期发送少量数据包以维持 NAT 映射的活跃状态,防止 NAT binding 超时。

RFC 6263 "Application Mechanism for Keeping Alive the NAT Mappings Associated with RTP / RTP Control Protocol (RTCP) Flows" 专门定义了用于维持 RTP/RTCP 相关 NAT 映射的应用层 keepalive 机制。

为什么需要 Keepalive

在以下场景中,RTP keepalive 尤为重要:

  1. 音频静音期 (Silence Suppression): 当使用 VAD (Voice Activity Detection) 进行静音抑制时,发送方在检测到静音后会停止发送 RTP 包。如果静音持续时间超过 NAT binding 超时时间,NAT 映射将被删除。

  2. 视频暂停 (Video Pause): 当用户暂停视频发送(例如关闭摄像头)时,RTP 流停止,NAT 映射面临超时风险。

  3. Hold 状态: 在 VoIP 通话中,当一方将通话置于保持 (hold) 状态时,媒体流停止。

  4. 单向媒体流: 在某些场景下(如单向直播),接收方不发送 RTP 数据,仅依赖 RTCP 来维持 NAT 映射,但 RTCP 的发送间隔可能过长。

  5. ICE 连接维护: 即使 ICE 连接已建立,如果没有持续的数据流,NAT 映射仍可能超时。

典型的 NAT binding 超时场景:

时间线:
t=0s     发送方发送最后一个 RTP 包
t=0s     发送方进入静音状态 (VAD 检测到静音)
...      (无 RTP 包发送)
t=30s    NAT 设备开始考虑回收该 binding
t=60s    NAT binding 被删除 ✗
t=65s    发送方恢复发送 RTP 包
         → 包被 NAT 丢弃,接收方收不到
         → 通信中断!

使用 Keepalive 后:
t=0s     发送方发送最后一个 RTP 包
t=0s     发送方进入静音状态
t=15s    发送 keepalive 包 → NAT binding 刷新 ✓
t=30s    发送 keepalive 包 → NAT binding 刷新 ✓
t=45s    发送 keepalive 包 → NAT binding 刷新 ✓
t=65s    发送方恢复发送 RTP 包
         → NAT binding 仍然有效,包正常转发 ✓

NAT 映射超时问题

UDP NAT Binding 超时

NAT 设备对 UDP 流的 binding 超时时间因设备类型和配置而异。RFC 4787 "Network Address Translation (NAT) Behavioral Requirements for Unicast UDP" 对此进行了规范。

常见 NAT 设备的 UDP Binding 超时时间

NAT 设备类型

典型超时时间

说明

家用路由器 (Consumer Router)

30s - 120s

不同品牌差异较大,部分低端设备仅 30 秒

企业级防火墙 (Enterprise Firewall)

60s - 300s

通常可配置,默认值较长

运营商级 NAT (Carrier-Grade NAT, CGN)

120s - 300s

RFC 4787 推荐至少 120 秒

移动网络 NAT (Mobile Network)

20s - 60s

移动运营商为节省资源,超时时间通常较短

云服务商 NAT (Cloud NAT)

30s - 300s

AWS、GCP、Azure 各有不同默认值

对称 NAT (Symmetric NAT)

30s - 60s

最严格的 NAT 类型,超时通常较短

注解

RFC 4787 建议 NAT 设备的 UDP binding 超时时间不应少于 2 分钟 (120 秒),但实际上许多设备并未遵循此建议。因此,keepalive 间隔通常设置为远小于 120 秒的值。

TCP vs UDP NAT Binding 差异

TCP 和 UDP 在 NAT binding 方面有显著差异:

TCP vs UDP NAT Binding 对比

特性

TCP

UDP

连接状态

有状态 (stateful),NAT 可跟踪 SYN/FIN

无状态 (stateless),NAT 仅基于超时

典型超时

数小时 (通常 2h+)

30s - 300s

Keepalive 机制

TCP keepalive (内置)

需要应用层实现

Binding 创建

SYN 包触发

首个 UDP 包触发

Binding 删除

FIN/RST 或超时

仅超时

对 RTP 的影响

RTP over TCP 较少受 NAT 超时影响

RTP over UDP 必须考虑 keepalive

由于 WebRTC 中 RTP 主要通过 UDP 传输(即使使用 DTLS-SRTP,底层仍是 UDP),NAT binding 超时是一个必须解决的问题。

Keepalive 机制详解 (RFC 6263)

RFC 6263 定义了多种维持 NAT 映射的方法。以下逐一介绍各种方法及其优缺点。

方法 1: 发送空 RTP 包 (Empty RTP Packet)

这是 RFC 6263 推荐的主要方法。发送一个没有负载(或负载为零长度)的 RTP 包,使用与当前媒体流相同的 SSRC 和递增的序列号。

 RTP 包的结构:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|V=2|P|  X=0  |M|     PT        |       sequence number         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                           timestamp                           |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                              SSRC                             |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
(无负载数据)

总大小: 12 字节 (RTP 头部) + 0 字节 (负载) = 12 字节

关于 Payload Type 的选择:

  • 使用 Comfort Noise (CN, PT=13): 对于音频流,可以使用 comfort noise payload type。接收方会将其视为舒适噪声,不会产生异常行为。

  • 使用当前媒体的 PT: 使用与正在进行的媒体流相同的 payload type,但负载为空或最小。

  • 使用 PT=0 (PCMU): 某些实现使用 PT=0 作为 keepalive 包的 payload type。

注解

发送空 RTP 包时,序列号应递增,但时间戳不应改变(或使用与静音期一致的时间戳),以避免接收方的 jitter buffer 产生混乱。

方法 2: 发送 RTCP 包 (SR/RR)

利用 RTCP 报文(Sender Report 或 Receiver Report)作为 keepalive 机制。由于 RTCP 本身就需要定期发送,这种方法不需要额外的协议支持。

RTCP 作为 Keepalive 的工作方式:

发送方                                    接收方
  |                                         |
  |--- RTP (媒体数据) --------------------->|
  |--- RTP (媒体数据) --------------------->|
  |                                         |
  | (进入静音期,停止发送 RTP)               |
  |                                         |
  |--- RTCP SR --------------------------->| ← 维持 NAT binding
  |                                         |
  |<-- RTCP RR -----------------------------|  ← 维持反向 NAT binding
  |                                         |
  |--- RTCP SR --------------------------->| ← 再次维持
  |                                         |

局限性: RTCP 的发送间隔由 RTCP bandwidth 规则控制(RFC 3550),最小间隔通常为 5 秒,但在参与者较多的会话中,间隔可能远大于 NAT 超时时间。此外,如果 RTP 和 RTCP 使用不同的端口(非 RTCP-mux),RTCP 只能维持 RTCP 端口的 NAT binding,无法维持 RTP 端口的 binding。

方法 3: 发送 STUN Binding Indication

在 ICE (Interactive Connectivity Establishment) 框架下,使用 STUN Binding Indication 消息作为 keepalive。这是 WebRTC 中最常用的 keepalive 方法。

STUN Binding Indication 格式:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|0 0|     STUN Message Type     |         Message Length        |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                         Magic Cookie                          |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                                                               |
|                     Transaction ID (96 bits)                  |
|                                                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

STUN Message Type = 0x0011 (Binding Indication)
总大小: 20 字节 (STUN 头部)

特点:
- Binding Indication 是一个 "fire-and-forget" 消息
- 不需要响应 (与 Binding Request 不同)
- 开销小,仅 20 字节
- 不影响 RTP/RTCP 的序列号和时间戳

方法 4: 发送 STUN Binding Request

与 Binding Indication 类似,但使用 STUN Binding Request,对端需要回复 Binding Response。这种方法不仅能维持 NAT binding,还能检测连接的活跃性 (liveness check)。

STUN Binding Request/Response 流程:

端点 A                                    端点 B
  |                                         |
  |--- STUN Binding Request --------------->|
  |                                         |
  |<-- STUN Binding Response ---------------|
  |                                         |

优点: 可以确认对端仍然可达
缺点: 需要对端响应,开销略大

各方法优缺点对比

Keepalive 方法对比

方法

包大小

需要响应

维持 RTP 端口

维持 RTCP 端口

适用场景

空 RTP 包

12 字节

音频静音期

RTCP SR/RR

~100 字节

否 (SR→RR)

✗ (非 mux)

RTCP-mux 场景

STUN Binding Indication

20 字节

✓ (mux)

WebRTC (推荐)

STUN Binding Request

20+ 字节

✓ (mux)

需要 liveness check

Keepalive 间隔

推荐间隔

RFC 6263 推荐的 keepalive 间隔为 15 秒。这个值的选择基于以下考虑:

Keepalive 间隔的选择依据:

1. NAT binding 最短超时时间: ~30 秒 (部分设备)
2. 安全余量: 间隔应远小于最短超时时间
3. 带宽开销: 间隔不宜过短,避免浪费带宽
4. 移动网络: 某些移动网络 NAT 超时仅 20 秒

推荐值: 15 秒
- 远小于大多数 NAT 的 30 秒最短超时
- 即使一个 keepalive 包丢失,下一个 (30 秒后) 仍在超时前到达
- 带宽开销极小: 20 字节 / 15 秒 ≈ 10.7 bps

与 NAT 超时的关系

Keepalive 间隔与 NAT 超时的关系:

NAT 超时 (T_nat)    推荐 Keepalive 间隔 (T_ka)
─────────────────    ──────────────────────────
20s (移动网络)       ≤ 10s (更激进)
30s (家用路由器)     ≤ 15s (标准)
60s (企业防火墙)     ≤ 15s (标准)
120s (CGN)           ≤ 15s (标准,无需更长)
300s (宽松配置)      ≤ 15s (标准,无需更长)

一般原则:
T_ka ≤ T_nat / 2

考虑丢包:
如果 keepalive 包可能丢失,应确保:
2 * T_ka < T_nat
即连续丢失一个 keepalive 包后,下一个仍能在超时前到达

自适应间隔

某些高级实现会根据网络环境动态调整 keepalive 间隔:

自适应 Keepalive 间隔算法:

初始间隔: T_ka = 15s

1. 检测 NAT 类型:
   if NAT_type == "symmetric":
       T_ka = min(T_ka, 10s)  # 对称 NAT 通常超时更短

2. 检测网络类型:
   if network_type == "mobile":
       T_ka = min(T_ka, 10s)  # 移动网络超时更短

3. 检测连接中断:
   if connection_lost_after_silence:
       T_ka = T_ka * 0.7  # 缩短间隔
       T_ka = max(T_ka, 5s)  # 但不低于 5 秒

WebRTC 中的实现

ICE Keepalive (STUN Binding Indication)

RFC 8445 (ICE) 规定,ICE agent 应该在选定的候选对 (selected candidate pair) 上发送 keepalive 以维持 NAT binding。WebRTC 实现中,这通常通过 STUN Binding Indication 完成。

ICE Keepalive 的工作流程:

1. ICE 连接建立完成,选定候选对
2. 启动 keepalive 定时器 (默认 15 秒)
3. 每次定时器触发:
   a. 检查是否有最近的数据包发送
   b. 如果在过去 T_ka 秒内没有发送任何包:
      → 发送 STUN Binding Indication
   c. 如果有数据包发送:
      → 跳过本次 keepalive (数据包已刷新 NAT binding)
4. 重置定时器

伪代码:
class IceKeepalive:
    def __init__(self, interval=15.0):
        self.interval = interval
        self.last_sent_time = time.time()

    def on_packet_sent(self):
        """任何包发送时调用"""
        self.last_sent_time = time.time()

    def on_timer(self):
        """定时器触发时调用"""
        elapsed = time.time() - self.last_sent_time
        if elapsed >= self.interval:
            self.send_stun_binding_indication()
            self.last_sent_time = time.time()

RFC 8445 Section 11 明确指出:

"An agent MUST keep the candidate pair alive. The agent MUST use STUN Binding Indications for keepalives as described in Section 11."

RTCP 作为隐式 Keepalive

在 RTCP-mux (RFC 5761) 场景下,RTP 和 RTCP 共享同一端口。此时,定期发送的 RTCP 报文(SR/RR)可以作为隐式的 keepalive,帮助维持 NAT binding。

RTCP-mux 下的隐式 Keepalive:

端口 5004 (RTP + RTCP 复用)
─────────────────────────────
t=0.0s   RTP 包 (媒体数据)
t=0.02s  RTP 包 (媒体数据)
...
t=5.0s   最后一个 RTP 包 (进入静音)
t=10.0s  RTCP SR/RR → 刷新 NAT binding ✓
t=15.0s  STUN Binding Indication → 刷新 NAT binding ✓
t=20.0s  RTCP SR/RR → 刷新 NAT binding ✓
t=25.0s  STUN Binding Indication → 刷新 NAT binding ✓
...

在 RTCP-mux 模式下,RTCP 和 STUN keepalive 共同维持 NAT binding,
提供了双重保障。

然而,仅依赖 RTCP 作为 keepalive 是不够的,原因包括:

  • RTCP 发送间隔可能大于 NAT 超时时间(特别是在多参与者会话中)

  • RTCP 发送时间具有随机性,可能出现较长的间隔

  • 在非 RTCP-mux 场景下,RTCP 无法维持 RTP 端口的 NAT binding

媒体静默期的处理

WebRTC 中处理媒体静默期的策略:

音频静音 (Audio Mute / Silence Suppression):

音频静音期的 Keepalive 策略:

方案 1: 发送 Comfort Noise (CN) 包
- 使用 PT=13 (Comfort Noise)
- 包含舒适噪声参数
- 接收方播放背景噪声,用户体验更好
- 同时维持 NAT binding

方案 2: 发送空 RTP 包
- 使用当前音频 PT,负载为空
- 接收方忽略空负载
- 仅用于维持 NAT binding

方案 3: 依赖 STUN Keepalive
- 不发送任何 RTP 包
- 由 ICE 层的 STUN Binding Indication 维持 NAT binding
- WebRTC 中最常用的方案

视频暂停 (Video Pause):

视频暂停期的 Keepalive 策略:

1. 用户关闭摄像头:
   - 停止发送视频 RTP 
   - ICE keepalive (STUN) 维持 NAT binding
   - 可选: 发送 RTCP BYE 通知接收方

2. 带宽不足导致视频暂停:
   - 拥塞控制算法决定暂停视频
   - 仅发送音频 RTP 
   - 音频包同时维持视频端口的 NAT binding (BUNDLE 模式下)

3. 视频 hold:
   - 发送方发送 SDP re-INVITE 将视频设为 inactive
   - ICE keepalive 维持 NAT binding
   - 恢复时重新协商 SDP

DTLS Keepalive

DTLS Heartbeat Extension (RFC 6520)

WebRTC 使用 DTLS-SRTP 进行密钥协商和媒体加密。DTLS 本身也提供了 keepalive 机制——Heartbeat Extension (RFC 6520)。

DTLS Heartbeat 消息格式:

struct {
    HeartbeatMessageType type;  // 1 = heartbeat_request
                                // 2 = heartbeat_response
    uint16 payload_length;
    opaque payload[HeartbeatMessage.payload_length];
    opaque padding[padding_length];
} HeartbeatMessage;

工作流程:
端点 A                                    端点 B
  |                                         |
  |--- Heartbeat Request ------------------>|
  |                                         |
  |<-- Heartbeat Response ------------------|
  |                                         |

特点:
- 双向确认,可检测连接活跃性
- 在 DTLS 层工作,独立于 RTP/RTCP
- 可用于 DTLS 连接的 keepalive

警告

DTLS Heartbeat 曾因 Heartbleed 漏洞 (CVE-2014-0160) 而广为人知。该漏洞允许攻击者通过构造恶意的 heartbeat 请求读取服务器内存。现代 DTLS 实现已修复此漏洞,但部分实现出于安全考虑默认禁用了 heartbeat 扩展。

在 WebRTC 中,DTLS heartbeat 通常不作为主要的 keepalive 机制,因为 ICE 层的 STUN keepalive 已经足够。但在某些特殊场景下(如 DTLS 连接需要独立维护时),heartbeat 仍然有用。

DTLS 与 ICE Keepalive 的关系

WebRTC 协议栈中的 Keepalive 层次:

┌─────────────────────────────────┐
│  应用层 (Application)            │
├─────────────────────────────────┤
│  SRTP/SRTCP (加密媒体)           │  ← RTP keepalive (空包)
├─────────────────────────────────┤
│  DTLS (密钥协商)                 │  ← DTLS heartbeat
├─────────────────────────────────┤
│  ICE (连接管理)                  │  ← STUN Binding Indication ★
├─────────────────────────────────┤
│  UDP (传输层)                    │
└─────────────────────────────────┘

在 WebRTC 中:
- ICE keepalive (STUN) 是主要的 keepalive 机制 ★
- RTCP 提供隐式 keepalive
- DTLS heartbeat 通常不使用
- 空 RTP 包在某些实现中作为补充

多路复用场景

BUNDLE 下的 Keepalive 策略

WebRTC 中广泛使用 BUNDLE (RFC 8843) 将多个媒体流复用到同一传输通道上。在 BUNDLE 模式下,所有音频和视频 RTP/RTCP 流共享同一个 UDP 端口。

BUNDLE 模式下的端口复用:

非 BUNDLE 模式:
┌──────────────┐     端口 5000: 音频 RTP
│              │     端口 5001: 音频 RTCP
│   WebRTC     │     端口 5002: 视频 RTP
│   端点       │     端口 5003: 视频 RTCP
│              │     → 需要维持 4 个 NAT binding
└──────────────┘

BUNDLE + RTCP-mux 模式:
┌──────────────┐     端口 5000: 音频 RTP + RTCP + 视频 RTP + RTCP
│   WebRTC     │     → 只需维持 1 个 NAT binding ✓
│   端点       │     → 一个 keepalive 包即可维持所有流
└──────────────┘

BUNDLE 模式下的 keepalive 策略:

  1. 单一 keepalive 即可: 由于所有流共享同一端口,一个 STUN Binding Indication 即可维持所有流的 NAT binding。

  2. 任何流的数据包都能刷新 binding: 即使视频暂停,音频 RTP 包仍然在发送,自然维持了 NAT binding。

  3. 简化管理: 不需要为每个媒体流单独管理 keepalive 定时器。

BUNDLE 模式下的 Keepalive 逻辑:

class BundleKeepalive:
    def __init__(self, interval=15.0):
        self.interval = interval
        self.last_activity = time.time()

    def on_any_packet_sent(self):
        """任何媒体流的包发送时调用"""
        self.last_activity = time.time()

    def check_keepalive(self):
        """定期检查是否需要发送 keepalive"""
        if time.time() - self.last_activity >= self.interval:
            # 所有流都静默,发送 keepalive
            self.send_stun_binding_indication()
            self.last_activity = time.time()

# 在 BUNDLE 模式下:
# - 音频 RTP 包会刷新 last_activity
# - 视频 RTP 包会刷新 last_activity
# - RTCP 包会刷新 last_activity
# - 只有当所有流都静默时才需要显式 keepalive

非 BUNDLE 模式的注意事项

在非 BUNDLE 模式下(虽然在现代 WebRTC 中较少见),每个媒体流使用独立的端口,需要分别维持各自的 NAT binding:

非 BUNDLE 模式下的 Keepalive:

音频端口 (5000):
- 音频 RTP 包持续发送 → 自然维持 binding
- 静音期: 需要 keepalive

视频端口 (5002):
- 视频 RTP 包持续发送 → 自然维持 binding
- 视频暂停: 需要 keepalive

RTCP 端口 (5001, 5003):
- RTCP 定期发送 → 通常足够维持 binding
- 但间隔可能过长,需要额外 keepalive

→ 需要为每个端口独立管理 keepalive 定时器

常见问题

对称 NAT 下的 Keepalive 失效

对称 NAT (Symmetric NAT) 为每个不同的目标地址创建不同的映射。这意味着:

对称 NAT 的问题:

内部端点 A (192.168.1.100:5000)
    |
    | → 发送到 STUN 服务器 (1.2.3.4:3478)
    |   NAT 映射: 192.168.1.100:5000 → 203.0.113.1:10000
    |
    | → 发送到 端点 B (5.6.7.8:6000)
    |   NAT 映射: 192.168.1.100:5000 → 203.0.113.1:10001 (不同!)
    |
    | → 发送到 TURN 服务器 (9.10.11.12:3478)
    |   NAT 映射: 192.168.1.100:5000 → 203.0.113.1:10002 (又不同!)

问题:
- Keepalive 必须发送到正确的目标地址
- 发送到 STUN 服务器的 keepalive 无法维持到端点 B 的映射
- 必须在每个活跃的候选对上分别发送 keepalive

在对称 NAT 环境下,WebRTC 通常需要使用 TURN relay 来建立连接,keepalive 需要在 TURN allocation 上进行。

移动网络 NAT 超时更短

移动网络(3G/4G/5G)的 NAT 设备通常配置了更短的超时时间,以节省有限的公网 IP 地址资源:

移动网络的特殊挑战:

1. NAT 超时更短:
   - 某些运营商的 NAT 超时仅 20 秒
   - 需要更频繁的 keepalive (如 10 秒间隔)

2. 网络切换:
   - Wi-Fi → 4G 切换时,NAT binding 完全改变
   - 需要 ICE restart 重新建立连接
   - Keepalive 无法解决网络切换问题

3. 省电模式:
   - 移动设备进入省电模式时,可能暂停网络活动
   - Keepalive 定时器可能不准确
   - 需要使用平台特定的后台任务 API

4. 运营商级 NAT (CGN):
   - 移动运营商通常使用 CGN
   - 双重 NAT: 设备 NAT + 运营商 NAT
   - 超时时间取决于两层 NAT 中较短的那个

VPN/防火墙对 Keepalive 的影响

企业 VPN 和防火墙可能对 keepalive 机制产生影响:

VPN/防火墙的影响:

1. 深度包检测 (DPI):
   - 某些防火墙会检测 STUN 包并阻止
   - 解决方案: 使用 TURN over TLS (端口 443)

2. UDP 阻断:
   - 某些企业网络完全阻断 UDP
   - Keepalive  (STUN/RTP) 无法通过
   - 解决方案: 使用 TURN over TCP/TLS

3. VPN 隧道:
   - VPN 隧道内的 NAT binding  VPN 网关管理
   - VPN 自身的 keepalive 可能与 RTP keepalive 冲突
   - 某些 VPN 会修改 UDP 包的源端口

4. 状态防火墙:
   - 状态防火墙跟踪 UDP "连接" 状态
   - 超时时间可能与 NAT 不同
   - 需要同时满足 NAT 和防火墙的超时要求

Wireshark 抓包分析

如何识别 Keepalive 包

使用 Wireshark 可以方便地识别和分析各种 keepalive 包:

Wireshark 过滤器:

1. 过滤 STUN Binding Indication:
   stun.type == 0x0011

2. 过滤所有 STUN :
   stun

3. 过滤空 RTP  (长度为 12 字节的 RTP):
   rtp && udp.length == 20
   (UDP 头部 8 字节 + RTP 头部 12 字节 = 20 字节)

4. 过滤 RTCP SR/RR:
   rtcp.pt == 200 || rtcp.pt == 201

5. 过滤 DTLS Heartbeat:
   dtls.heartbeat_message

6. 综合过滤 (所有可能的 keepalive):
   stun.type == 0x0011 ||
   (rtp && udp.length == 20) ||
   dtls.heartbeat_message

分析 Keepalive 间隔

在 Wireshark 中分析 Keepalive 间隔:

1. 应用过滤器: stun.type == 0x0011
2. 查看 "Time" 列,观察相邻 keepalive 包的时间差
3. 使用 Statistics → I/O Graphs 绘制 keepalive 频率图

典型的 Wireshark 输出:

No.  Time      Source          Dest            Protocol  Info
1    0.000     192.168.1.100   203.0.113.50    STUN      Binding Indication
2    15.003    192.168.1.100   203.0.113.50    STUN      Binding Indication
3    30.001    192.168.1.100   203.0.113.50    STUN      Binding Indication
4    45.005    192.168.1.100   203.0.113.50    STUN      Binding Indication

间隔: ~15 秒 (符合 RFC 6263 推荐值)

如果观察到间隔不规律或过长,可能存在以下问题:
- Keepalive 定时器实现有误
- 系统负载过高导致定时器延迟
- 其他数据包 (RTP/RTCP) 替代了 keepalive

识别 NAT Binding 超时

识别 NAT Binding 超时的迹象:

1. 单向媒体中断:
   - 一方能发送但对方收不到
   - 抓包显示包已发出但对端无响应

2. STUN Binding Request 超时:
   - 连续多个 STUN request 无 response
   - Wireshark 过滤: stun.type == 0x0001 && !stun.type == 0x0101

3. RTCP RR 报告突然丢包率 100%:
   - 接收方报告完全丢包
   - 可能是 NAT binding 已失效

4. ICE 连接状态变化:
   - ICE 状态从 "connected" 变为 "disconnected"
   - 触发 ICE restart

libwebrtc 中的实现

在 Google 的 libwebrtc (WebRTC Native) 实现中,keepalive 主要通过 ICE 层的 STUN Binding Indication 实现。

// libwebrtc 中 keepalive 相关的关键代码路径:

// 1. P2PTransportChannel 中的 keepalive 定时器
// 文件: p2p/base/p2p_transport_channel.cc

// 默认 keepalive 间隔 (毫秒)
static const int STUN_KEEPALIVE_INTERVAL = 10000;  // 10 秒 (比 RFC 推荐更短)

// 稳定连接后的 keepalive 间隔
static const int STABLE_KEEPALIVE_INTERVAL = 25000;  // 25 秒

void P2PTransportChannel::OnCheckAndPing() {
    // 检查是否需要发送 keepalive
    auto now = rtc::TimeMillis();
    auto selected = selected_connection_;

    if (selected) {
        // 计算距离上次发送的时间
        auto last_sent = selected->last_ping_sent();
        auto interval = GetKeepaliveInterval();

        if (now - last_sent >= interval) {
            // 发送 STUN Binding Request (也用作 keepalive)
            selected->Ping(now);
        }
    }
}

// 2. Connection 类中的 Ping 实现
// 文件: p2p/base/connection.cc

void Connection::Ping(int64_t now) {
    // 构造 STUN Binding Request
    auto request = new StunBindingRequest(this);
    // 发送请求
    SendStunBindingRequest(request);
    last_ping_sent_ = now;
}

注解

libwebrtc 实际使用 STUN Binding Request(而非 Indication)作为 keepalive,这样可以同时检测连接的活跃性。如果连续多个 Binding Request 没有收到响应,ICE 会认为连接已断开。

最佳实践

RTP Keepalive 最佳实践总结:

1. 使用 BUNDLE + RTCP-mux:
   - 减少需要维持的 NAT binding 数量
   - 简化 keepalive 管理

2. 使用 STUN Binding Indication 作为主要 keepalive:
   - 开销小 (20 字节)
   - 不影响 RTP/RTCP 状态
   - WebRTC 标准推荐

3. 设置合理的 keepalive 间隔:
   - 默认 15 秒
   - 移动网络可缩短到 10 秒
   - 不要超过 25 秒

4. 智能 keepalive:
   - 如果有数据包发送,跳过 keepalive
   - 避免不必要的带宽浪费

5. 监控 NAT binding 状态:
   - 使用 STUN Binding Request 检测连接活跃性
   - 检测到 binding 失效时触发 ICE restart

6. 考虑移动网络:
   - 更短的 keepalive 间隔
   - 处理网络切换 (Wi-Fi ↔ 移动网络)
   - 使用平台后台任务 API 维持 keepalive

7. 准备 fallback:
   - 如果 UDP keepalive 失败,考虑 TURN over TCP/TLS
   - 实现 ICE restart 机制

参考文献