前言

在上一篇中,我们介绍了视频会议系统的整体架构和核心设计思路。本文将深入传输层的实现——如何基于 KCP 协议在同一个 UDP 连接上同时传输可靠信令和实时媒体数据。


为什么选择 KCP?

标准 WebRTC 传输栈的问题

标准 WebRTC 的传输链路是:

应用数据 → DTLS 加密 → ICE 传输 → UDP

对于 SFU 视频会议场景,这套机制有几个显著问题:

  1. ICE 握手复杂:需要收集 host/srflx/relay 候选,full ICE 可能需要数十秒
  2. DTLS 开销大:每个 PeerConnection 都需要独立的 DTLS 会话
  3. SRTP key derivation 依赖 DTLS:无法单独使用 SRTP

KCP 的优势

KCP 是一个快速可靠协议,相比 TCP:

  • 更低的延迟:牺牲 10%~20% 吞吐量,换取 30%~40% 的延迟降低
  • 快速重传:通过 fastresend 机制加速丢包恢复
  • 可配置的可靠性:可以精细控制重传策略

对于视频会议场景,我们的需求是:

数据类型可靠性要求延迟要求传输方式
信令(JSON)必须可靠允许几十msKCP 可靠传输
RTP 媒体允许丢包要求极低UDP 透传
RTCP 控制允许少量丢包较低UDP 透传

KCP 正好满足这个需求——它允许在同一连接上同时进行可靠传输和不可靠传输。


双通道传输设计

核心思路

在同一个 UDP Socket 上,我们设计了两种数据通道:

┌─────────────────────────────────────────┐
│              KcpSocket                   │
│                                         │
│   KCP 通道(可靠)      UserPacket 通道  │
│   ┌─────────────┐      ┌─────────────┐ │
│   │  JSON 信令   │      │  RTP/RTCP   │ │
│   │  握手/心跳   │      │  媒体数据    │ │
│   └──────┬──────┘      └──────┬──────┘ │
│          │                    │         │
│     KCP 协议处理          原始 UDP 包   │
│     (ARQ/重传)           (无重传)      │
│          │                    │         │
│          └────────┬───────────┘         │
│                   │                     │
│              UDP Socket                 │
└─────────────────────────────────────────┘

关键区分:KCP 协议的数据包和原始 UDP 包通过首字节区分——KCP 包的首字节低 4 位为非零值(KCP conv 的一部分),而我们的 UserPacket 使用特殊标记。

KcpSocket 实现

class KcpSocket : public AsyncSocket, public rtc::MessageHandler {
public:
    // KCP 可靠发送(用于信令)
    int Send(const void* pv, size_t cb) override;

    // UDP 透传发送(用于 RTP)
    int SendUserPacket(const void* pv, size_t cb);

    // 接收 UserPacket 的信号
    sigslot::signal3<AsyncSocket*, const void*, size_t> SignalRecvUserPacketEvent;

private:
    std::unique_ptr<AsyncSocket> socket_;  // 底层 UDP socket
    ikcpcb kcp_;                           // KCP 协议控制块
    std::unique_ptr<PacketCryptoBase> packet_crypto_;  // 加密
};

数据发送流程:

Send(data)                    SendUserPacket(data)
    │                              │
    ▼                              │
ikcp_send(kcp, data)              │
    │                              │
    ▼                              │
ikcp_update → 生成 KCP 包          │
    │                              │
    ▼                              ▼
底层 socket_->Send(kcp_packet)    底层 socket_->Send(raw_packet)

数据接收流程:

OnReadEvent → socket_->Recv(buffer)
    │
    ├── 判断是否为 KCP 控制包 → OnRecvControlPacket
    │
    ├── 判断是否为 KCP 数据包 → ikcp_input → ikcp_recv → SignalReadEvent
    │
    └── 判断是否为 UserPacket → SignalRecvUserPacketEvent

连接建立过程

握手协议

KcpSocket 的连接建立是自定义的三步握手,不依赖 ICE:

Client                              Server
  │                                    │
  │  KCT_CONNECT_REQ                   │
  │  (cookie, sndwnd, rcvwnd,          │
  │   nodelay, fastResend, mtu)        │
  │───────────────────────────────────▶│
  │                                    │
  │  KCT_CONNECT_RSP                   │
  │  (cookie, code=OK)                 │
  │◀───────────────────────────────────│
  │                                    │
  │  连接建立,开始数据传输             │

如果服务器需要重定向客户端到另一台服务器:

Client                              Server A
  │                                    │
  │  KCT_CONNECT_REQ                   │
  │───────────────────────────────────▶│
  │                                    │
  │  KCT_CONNECT_RSP                   │
  │  (code=REDIRECT,                   │
  │   redirect=Server_B:port)          │
  │◀───────────────────────────────────│
  │                                    │
  │  重新连接到 Server B               │

握手包格式

// 连接请求包 (33 bytes)
struct KConnectReqPacket : public KControlPacketHead {
    uint32_t cookie;       // 客户端生成的随机 cookie
    int32_t sndwnd;        // 发送滑动窗口大小 (默认 128)
    int32_t rcvwnd;        // 接收滑动窗口大小 (默认 128)
    int32_t interval;      // KCP 更新间隔 (默认 20ms)
    uint8_t nodelay;       // 是否无延迟发包
    uint8_t fastResend;    // 快速重传阈值 (默认 2)
    uint8_t nocwnd;        // 是否关闭流量控制
    uint16_t mtu;          // 最大传输单元 (默认 1400)
};

// 连接响应包 (20 bytes)
struct KConnectRspPacket : public KControlPacketHead {
    uint32_t cookie;       // 回传客户端的 cookie
    uint32_t code;         // CODE_OK 或 CODE_REDIRECT
    SocketAddress redirect; // 重定向地址(如果 code=REDIRECT)
};

默认参数配置

struct KOptions {
    int32_t timeOutInterval = 3 * 60 * 1000;  // 超时 3 分钟
    int32_t keepAliveInterval = 30 * 1000;     // 心跳 30 秒
    int mtu = 1400;                            // MTU
    int sndwnd = 128;                          // 发送窗口
    int rcvwnd = 128;                          // 接收窗口
    int interval = 20;                         // 更新间隔 20ms
    int udpsendbuf = 128 * 1024;              // UDP 发送缓存
    int udprecvbuf = 128 * 1024;              // UDP 接收缓存
    bool nodelay = false;                      // 延迟模式
    uint8_t fastResend = 2;                    // 快速重传
    bool nocwnd = false;                       // 流量控制
    int minrto = 100;                          // 最小 RTO
};

保活与超时

心跳机制

连接建立后,KcpSocket 通过定时发送 KCT_KEEP_ALIVE 心跳包来保持连接:

Client                              Server
  │                                    │
  │  KCT_KEEP_ALIVE (每 30s)           │
  │───────────────────────────────────▶│
  │                                    │
  │  响应 / 无需响应                    │
  │                                    │

KcpSocket 在 OnMessage 中定时触发心跳:

void KcpSocket::OnMessage(rtc::Message* msg) {
    // KCP update 定时器
    ikcp_update(&kcp_, iclock());

    // 心跳保活
    if (iclock() - last_keepalive_time_ > options_.keepAliveInterval) {
        SendKeepAlive();
        last_keepalive_time_ = iclock();
    }

    // 超时检测
    if (iclock() - last_recv_time_ > options_.timeOutInterval) {
        // 连接超时,通知上层
        ...
    }

    // 注册下一次定时器
    net_thread_->PostDelayed(RTC_FROM_HERE, kcp_.interval, this);
}

超时检测

  • 默认超时时间为 3 分钟(timeOutInterval)
  • 每次收到数据时更新 last_recv_time_
  • 超时后发送 KCT_CLOSED 控制包,通知对端

消息序列化层

MsgSerialize

由于 KCP 的 Send 接口面向的是字节流,我们需要一个消息层来支持结构化消息的传输。MsgSerialize 实现了消息的帧化和分片:

消息格式:
┌──────────┬──────────┬─────────────────┐
│ 1 byte   │ 4 bytes  │  N bytes        │
│ msg_type │ data_len │  data           │
└──────────┴──────────┴─────────────────┘
class MsgSerialize {
public:
    // 发送消息(自动分片)
    bool SendMsg(uint8_t type, const uint8_t* data, size_t size);
    // 接收数据(自动组包)
    bool OnRecvData(const uint8_t* data, size_t len);
    // 发送缓存中的剩余数据
    void SendBuff();

    void SetMaxSize(size_t size);  // 最大消息大小,默认 1MB

private:
    MsgSerializeCallback* callback_;  // 回调接口
    RingBuffer recv_buffer_;          // 接收环形缓冲区
    std::vector<uint8_t> send_buffer_; // 发送缓冲区
};

分片与组包

当消息较大时,MsgSerialize 会将消息切分为多个 KCP 包发送,接收端通过 RingBuffer 缓存数据并重组:

发送端:
  大消息 → [head+part1] → [part2] → [part3] → ...

接收端:
  [part1] → RingBuffer
  [part2] → RingBuffer
  [part3] → RingBuffer → 解析 head → 完整消息 → OnRecvMsg

MediaNetwork — 网络层封装

MediaNetwork 是对 KcpSocket 的上层封装,管理连接生命周期和数据收发:

class MediaNetwork : public sigslot::has_slots<>, public kcp::MsgSerializeCallback {
public:
    MediaNetwork(MediaNetworkObserver* obs, rtc::Thread* netThread);

    void Connect(const rtc::SocketAddress& addr);
    void Close();

    // 发送 JSON 数据(走 KCP 可靠通道)
    bool Send(const Json::Value& data);
    // 发起 JSON-RPC 请求
    uint32_t Request(const std::string& method, const Json::Value& data);
    // 发送 RTP 包(走 UDP 透传通道)
    bool SendPacket(rtc::CopyOnWriteBuffer& packet, const rtc::PacketOptions& options);

private:
    void OnRecvJson(const char* data, size_t len);     // 接收 JSON
    void OnRecvMsg(uint8_t type, const uint8_t* data, size_t size) override;  // MsgSerialize 回调
    int SendData(const uint8_t* data, size_t size) override;                  // MsgSerialize 回调

    std::unique_ptr<kcp::KcpSocket> socket_;
    kcp::MsgSerialize serialize_;
    uint32_t reqId_{0};
};

数据收发流程

发送 JSON 信令:
  Request(method, params)
    → 构建 JSON-RPC 请求
    → Send(json)
    → serialize_.SendMsg(kJson, data, len)
    → SendData → KcpSocket::Send (KCP 可靠)

发送 RTP 媒体:
  SendPacket(packet, options)
    → KcpSocket::SendUserPacket (UDP 透传)

接收数据:
  OnReadEvent
    → Recv(buffer)
    → 判断包类型
    ├── KCP 数据包 → ikcp_input → ikcp_recv → serialize_.OnRecvData → OnRecvJson
    ├── 控制包     → OnRecvControlPacket
    └── UserPacket → OnRecvPacket → observer_->OnRecvPacket (RTP)

接收 RTP 包的识别

MediaNetwork 通过 RTP 头的特征来识别 UserPacket 是否为 RTP 数据:

void MediaNetwork::OnRecvPacket(rtc::AsyncSocket* socket, const void* pdata, size_t len) {
    const uint8_t* data = (const uint8_t*)pdata;
    // RTP 包的第一个字节的高 4 位为版本号,应为 2 (0x80 或 0x90 等)
    if ((data[0] & 0xf0) == 0) {
        RTC_LOG(LS_ERROR) << "packet error";
        return;
    }
    rtc::CopyOnWriteBuffer buffer((const uint8_t*)pdata, len);
    observer_->OnRecvPacket(buffer);
}

包加密

KCP 模块提供了 PacketCryptoBase 加密接口,支持自定义加密算法:

class PacketCryptoBase {
public:
    virtual int Encrypt(const uint8_t* data, int dataLen, unsigned char* dataOut) = 0;
    virtual int Decrypt(const uint8_t* data, int dataLen, unsigned char* dataOut) = 0;
};

默认实现了 RandomPacketCrypto,开发者可以通过继承 PacketCryptoBase 来实现自定义加密(如 AES 等),然后注入到 KcpSocket 或 ReliableLayer 中。


完整的传输栈

将所有层组合起来,从上到下的传输栈如下:

┌────────────────────────────────────────────────┐
│                应用层                           │
│  MediaClient (Publish/Subscribe)               │
├────────────────────────────────────────────────┤
│                信令层                           │
│  RequestClient (JSON-RPC 请求/响应/通知)        │
├────────────────────────────────────────────────┤
│                复用层                           │
│  ClientTransport (信令 + RTP 合并)              │
├──────────────────────┬─────────────────────────┤
│   信令通道           │   媒体通道               │
│   MediaNetwork       │   RtpTransport           │
│   Request/Notify     │   SendPacket/OnRecvPacket│
├──────────────────────┼─────────────────────────┤
│   MsgSerialize       │                          │
│   (消息帧化)         │                          │
├──────────────────────┼─────────────────────────┤
│   KCP (可靠传输)     │   UserPacket (UDP透传)   │
├──────────────────────┴─────────────────────────┤
│              KcpSocket                          │
├────────────────────────────────────────────────┤
│              UDP Socket                         │
└────────────────────────────────────────────────┘

小结

本文详细介绍了视频会议系统的传输层设计:

  1. 双通道传输:KCP 可靠通道传输信令,UDP 透传通道传输 RTP 媒体
  2. 自定义握手:简洁的三步握手,内置重定向支持
  3. 心跳保活:30 秒心跳间隔,3 分钟超时检测
  4. 消息序列化:支持大消息分片,RingBuffer 组包
  5. 包加密:可扩展的加密接口

这套传输层设计在保证信令可靠性的同时,为媒体数据提供了最低延迟的传输路径,是整个视频会议系统的通信基础。

下一篇将介绍如何绕过 PeerConnection,直接集成 WebRTC 的 MediaEngine 来实现音视频编解码和 RTP 处理。

Logo

火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。

更多推荐