Offer/Answer 交换:SDP 协议简介、Offer 消息处理、Answer 消息处理、SDP 中继
聊到 WebRTC 的核心机制,Offer/Answer 交换绝对是个绕不开的坎。说白了,这就是两个浏览器互相「自我介绍」的过程——你支持什么编解码器?你的 IP 地址是什么?你打算用什么端口?这些信息全得靠 SDP 来传递。
我记得刚接触 WebRTC 那会儿,总觉得 SDP 就是一串又臭又长的文本,看着就头疼。后来在项目中踩过几次坑,才真正理解了它的重要性。今天我就把这块掰开了讲清楚。
SDP 协议简介
SDP,全称 Session Description Protocol,会话描述协议。它不负责传输媒体数据,只负责描述媒体会话的「长相」。你可以把它想象成一份简历——上面写满了你的能力、你的联系方式、你希望怎么合作。
一个典型的 SDP 长这样:
v=0
o=- 1234567890 1234567890 IN IP4 192.168.1.100
s=-
t=0 0
m=audio 49170 RTP/AVP 96
a=rtpmap:96 OPUS/48000/2
a=fmtp:96 minptime=10; useinbandfec=1
c=IN IP4 192.168.1.100
a=ice-ufrag:abc123
a=ice-pwd:def456
a=fingerprint:sha-256 12:34:56:78:9A:BC:DE:F0...
这里面每一行都有含义。我挑几个重点说说:
- v=:协议版本,目前都是 0
- o=:会话发起者信息,包含用户名、会话 ID、网络类型和地址
- m=:媒体描述,告诉对方「我要用音频,端口是 49170,协议是 RTP」
- a=:属性行,这是最灵活的部分,ICE、DTLS、编解码器参数全在这里
- c=:连接信息,通常是 IP 地址
核心要点:SDP 本身不加密,但 WebRTC 的媒体流是通过 DTLS-SRTP 加密的。SDP 里携带的指纹(fingerprint)就是用来验证对方身份的。
Offer 消息处理
Offer 是发起方生成的 SDP。它描述了发起方希望建立的媒体会话。在我的项目中,Offer 的生成通常由 createOffer() 触发,但真正麻烦的是后续的处理逻辑。
Offer 消息处理的核心步骤:
- 生成 Offer:调用
pc.createOffer(),浏览器会根据本地能力生成 SDP - 设置本地描述:
pc.setLocalDescription(offer),告诉浏览器「这就是我打算用的配置」 - 发送给信令服务器:通过 WebSocket 或其他方式把 SDP 文本发出去
- 信令服务器中继:服务器不做修改,直接转发给对端
这里有个坑,我曾经踩过:
注意:Offer 必须在 ICE 候选收集完成之前发送吗?不一定。但如果你等所有候选都收集完再发,延迟会很高。我建议的做法是:先发 Offer,然后通过 Trickle ICE 机制逐步发送候选。这样对端可以尽早开始连接尝试。
Offer 里包含的信息量很大。除了媒体信息,还有 ICE 用户名和密码、DTLS 指纹、以及各种候选地址。这些信息对端拿到后,才能构造出合适的 Answer。
Answer 消息处理
Answer 是对端收到 Offer 后生成的回复。它本质上是对 Offer 的「回应」——你支持的编解码器我接受,你的 IP 我记下了,这是我的配置。
Answer 的处理流程:
- 接收 Offer:从信令服务器拿到对端的 SDP
- 设置远程描述:
pc.setRemoteDescription(offer),告诉浏览器「这是对方的能力」 - 生成 Answer:
pc.createAnswer(),浏览器根据 Offer 和本地能力生成 Answer - 设置本地描述:
pc.setLocalDescription(answer) - 发送 Answer:通过信令服务器回传给发起方
为什么 Answer 不能随便改?因为 Offer/Answer 模型遵循的是「协商」原则。你想想看,如果发起方说「我用 OPUS 编码」,应答方说「我用 G.711」,那两边就聊不到一块儿去了。所以 Answer 必须在 Offer 的基础上做选择,不能凭空添加 Offer 里没有的东西。
小技巧:调试时可以用 pc.localDescription.sdp 和 pc.remoteDescription.sdp 打印出当前设置的 SDP。我曾经靠这个定位过一个编解码器不匹配的问题——两边都声称支持 H.264,但一个用的是 packetization-mode=0,另一个用的是 packetization-mode=1,结果视频死活出不来。
SDP 中继
SDP 中继,说白了就是信令服务器只负责「传话」,不修改 SDP 内容。这是最常用的模式,也是我推荐的做法。
为什么不做修改?原因很简单:
- SDP 是浏览器生成的,包含了 ICE 候选、DTLS 指纹等敏感信息
- 修改 SDP 可能导致协商失败,或者引入安全漏洞
- 保持简单,服务器只做转发,出问题也好排查
但有一种情况例外——当你的信令服务器需要做媒体层处理时,比如转码、混流、录制。这时候服务器会作为 SFU(Selective Forwarding Unit)或 MCU(Multipoint Control Unit)存在,需要修改 SDP 来插入自己的媒体能力。不过那是进阶话题了,初学者先别碰。
SDP 中继的典型流程:
// 客户端 A 发送 Offer
ws.send(JSON.stringify({
type: 'offer',
sdp: offerSDP
}));
// 信令服务器收到后,直接转发给客户端 B
ws.on('message', (data) => {
const msg = JSON.parse(data);
// 不做任何修改
sendToPeer(msg.target, msg);
});
// 客户端 B 收到后,生成 Answer 并回传
ws.send(JSON.stringify({
type: 'answer',
sdp: answerSDP
}));
这个模式简单可靠。我在一个生产项目中用了两年,没出过问题。唯一要注意的是,SDP 文本可能很大(尤其是候选很多的时候),传输时要注意分片或压缩。
总结一下:Offer/Answer 交换是 WebRTC 建立连接的第一步。SDP 负责描述能力,Offer 发起协商,Answer 回应协商,信令服务器只做中继。这三者配合好了,后面的 ICE 连接和媒体传输才能顺利进行。
这张图把整个流程串起来了。你看,信令服务器在中间就是个「传话筒」,不修改任何内容。客户端 A 生成 Offer,服务器转发给 B;B 生成 Answer,服务器再转发给 A。两边都设置好远程描述后,ICE 就开始干活了。
嗯,这里要注意一点:Offer 和 Answer 的交换必须严格遵循顺序。你不能先收到 Answer 再收到 Offer,那样协商会失败。我在一个项目中遇到过因为 WebSocket 消息乱序导致的问题,后来加了消息序列号才解决。
好了,关于 Offer/Answer 交换的核心内容就这些。SDP 协议看着复杂,但用多了就会发现,它其实就那几块东西——媒体信息、网络信息、安全信息。掌握了这些,信令服务器的开发就成功了一半。