WebRTC的手动SDP交换是指开发者自行实现信令传输逻辑,完成Offer和Answer的交换,不依赖默认的信令服务。在这种模式下,ICE机制的配置直接影响连接建立的效率和成功率,不合理的配置会导致连接耗时过长甚至失败。

手动SDP交换的基本流程
手动SDP交换的核心步骤分为创建连接、生成SDP、交换SDP、设置远端描述、收集ICE候选并交换候选五个部分,具体流程如下:
- 创建
RTCPeerConnection实例,配置ICE服务器地址 - 发起端调用
createOffer生成Offer SDP,通过自定义信令通道发送给接收端 - 接收端收到Offer后调用
setRemoteDescription设置远端描述,再调用createAnswer生成Answer SDP返回给发起端 - 发起端收到Answer后调用
setRemoteDescription设置远端描述 - 双方收集ICE候选,通过信令通道互相交换候选地址,完成连接建立
ICE机制对连接时效性的影响
ICE(Interactive Connectivity Establishment)的作用是在NAT和防火墙环境下找到双方可用的通信地址,默认情况下ICE会收集所有类型的候选地址(主机候选、服务器反射候选、中继候选),并按照优先级进行排序尝试连接。如果配置不当,会出现以下问题:
- 收集全部候选地址耗时过长,尤其是中继候选需要连接TURN服务器,增加等待时间
- 无效候选地址没有被过滤,尝试连接时浪费时间
- ICE超时时间设置不合理,要么过早放弃连接,要么等待时间过长
ICE机制优化方案
1. 调整候选地址收集策略
可以根据实际场景限制候选地址的类型,减少不必要的收集耗时。比如内网场景下只需要主机候选,公网场景下可以优先使用服务器反射候选,减少中继候选的收集。配置RTCPeerConnection时可以通过iceTransportPolicy参数控制:
// 只收集主机候选,适合内网场景,连接速度最快
const pc = new RTCPeerConnection({
iceServers: [
{ urls: "stun:stun.ipipp.com:3478" }
],
iceTransportPolicy: "host"
});
// 收集主机和服务器反射候选,不使用中继候选,适合大部分公网场景
const pc2 = new RTCPeerConnection({
iceServers: [
{ urls: "stun:stun.ipipp.com:3478" }
],
iceTransportPolicy: "all"
});
2. 设置合理的ICE超时时间
默认的ICE检查超时时间较长,可以通过监听iceconnectionstatechange事件,在检测到连接失败时及时重试,或者自定义超时逻辑。以下示例设置了10秒的ICE连接超时:
let iceTimeout = null;
const pc = new RTCPeerConnection({
iceServers: [{ urls: "stun:stun.ipipp.com:3478" }]
});
pc.addEventListener("iceconnectionstatechange", () => {
if (pc.iceConnectionState === "checking") {
// 进入检查状态后启动超时计时
iceTimeout = setTimeout(() => {
if (pc.iceConnectionState !== "connected" && pc.iceConnectionState !== "completed") {
console.log("ICE连接超时,尝试重新交换候选");
// 这里可以触发重新收集候选或者重新建立连接的逻辑
}
}, 10000);
} else if (pc.iceConnectionState === "connected" || pc.iceConnectionState === "completed") {
// 连接成功清除超时计时
clearTimeout(iceTimeout);
}
});
3. 过滤无效ICE候选
在交换ICE候选的过程中,可以过滤掉无用的候选地址,比如重复的候选、IPv6地址(如果不需要支持IPv6)、端口为0的无效候选。以下示例是候选过滤的实现:
const validCandidates = [];
const seenCandidateKeys = new Set();
function filterIceCandidate(candidate) {
// 解析候选字符串
const parts = candidate.split(" ");
const foundation = parts[0];
const componentId = parts[1];
const protocol = parts[2];
const priority = parts[3];
const ip = parts[4];
const port = parts[5];
const type = parts[7];
// 过滤端口为0的无效候选
if (parseInt(port) === 0) return null;
// 过滤IPv6地址
if (ip.includes(":")) return null;
// 过滤重复候选
const key = `${foundation}_${componentId}_${ip}_${port}_${type}`;
if (seenCandidateKeys.has(key)) return null;
seenCandidateKeys.add(key);
return candidate;
}
// 收集候选时过滤
pc.addEventListener("icecandidate", (event) => {
if (event.candidate) {
const filtered = filterIceCandidate(event.candidate.candidate);
if (filtered) {
validCandidates.push(filtered);
// 发送过滤后的候选到对端
sendSignalingMessage({ type: "candidate", candidate: filtered });
}
}
});
4. 优化SDP交换顺序
为了减少ICE候选的等待时间,可以在生成SDP之后就先发送SDP,同时异步收集ICE候选,不需要等所有候选收集完成再发送SDP。WebRTC的icecandidate事件会在候选收集到的时候逐个触发,因此可以边收集边发送,对端收到候选后就可以开始尝试连接,不需要等所有候选都到齐。
// 发起端生成Offer后先发送SDP,再等待候选发送
async function createAndSendOffer() {
const pc = new RTCPeerConnection({ iceServers: [{ urls: "stun:stun.ipipp.com:3478" }] });
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
// 先发送Offer SDP到对端
sendSignalingMessage({ type: "offer", sdp: pc.localDescription.sdp });
// 监听候选事件,逐个发送候选
pc.addEventListener("icecandidate", (event) => {
if (event.candidate) {
sendSignalingMessage({ type: "candidate", candidate: event.candidate.candidate });
}
});
}
优化效果验证
可以通过浏览器的开发者工具查看WebRTC的连接耗时,在chrome://webrtc-internals页面中可以查看ICE候选收集时间、连接状态变化时间。优化前默认配置下连接建立可能需要3-5秒,优化后内网场景可以缩短到1秒以内,公网场景也可以缩短30%以上的连接耗时。
注意:ICE优化需要结合实际业务场景调整,比如需要支持NAT穿透的场景不能关闭服务器反射候选,需要高可靠性的场景不能关闭中继候选,需要在连接速度和可用性之间做好平衡。