基于 WebRTC 打造视频会议系统(二):KCP 传输层设计与实现
前言
在上一篇中,我们介绍了视频会议系统的整体架构和核心设计思路。本文将深入传输层的实现——如何基于 KCP 协议在同一个 UDP 连接上同时传输可靠信令和实时媒体数据。
为什么选择 KCP?
标准 WebRTC 传输栈的问题
标准 WebRTC 的传输链路是:
应用数据 → DTLS 加密 → ICE 传输 → UDP
对于 SFU 视频会议场景,这套机制有几个显著问题:
- ICE 握手复杂:需要收集 host/srflx/relay 候选,full ICE 可能需要数十秒
- DTLS 开销大:每个 PeerConnection 都需要独立的 DTLS 会话
- SRTP key derivation 依赖 DTLS:无法单独使用 SRTP
KCP 的优势
KCP 是一个快速可靠协议,相比 TCP:
- 更低的延迟:牺牲 10%~20% 吞吐量,换取 30%~40% 的延迟降低
- 快速重传:通过
fastresend机制加速丢包恢复 - 可配置的可靠性:可以精细控制重传策略
对于视频会议场景,我们的需求是:
| 数据类型 | 可靠性要求 | 延迟要求 | 传输方式 |
|---|---|---|---|
| 信令(JSON) | 必须可靠 | 允许几十ms | KCP 可靠传输 |
| 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 │
└────────────────────────────────────────────────┘
小结
本文详细介绍了视频会议系统的传输层设计:
- 双通道传输:KCP 可靠通道传输信令,UDP 透传通道传输 RTP 媒体
- 自定义握手:简洁的三步握手,内置重定向支持
- 心跳保活:30 秒心跳间隔,3 分钟超时检测
- 消息序列化:支持大消息分片,RingBuffer 组包
- 包加密:可扩展的加密接口
这套传输层设计在保证信令可靠性的同时,为媒体数据提供了最低延迟的传输路径,是整个视频会议系统的通信基础。
下一篇将介绍如何绕过 PeerConnection,直接集成 WebRTC 的 MediaEngine 来实现音视频编解码和 RTP 处理。
火山引擎视频云技术社区,是面向 AI 音视频开发者的技术交流平台。这里汇聚源自抖音、豆包等亿级 DAU 产品的 RTC、直播、点播、AI 媒体处理、音视频互动技术,提供接入指南、最佳实践、性能调优、场景案例、Demo 代码、开源项目、白皮书和 API 文档。社区汇聚官方工程师与一线开发者,为 AI 视频通话、数字人、AI 视频处理等应用的开发与落地提供技术支持。
更多推荐
所有评论(0)