
1. 为什么WebRTC的“网络传输”不是加个RTCPeerConnection就能跑通很多人第一次接触WebRTC时会把注意力全放在信令交换、SDP协商、媒体轨道绑定这些“看得见”的环节上。我见过太多开发者在本地两个标签页里跑通了视频对讲Demo就以为自己掌握了WebRTC——直到他把代码部署到真实局域网跨子网环境或者让同事用手机连4G接入测试画面立刻卡成PPT音频断断续续像收音机调频失败。这时候才猛然发现真正决定WebRTC能否落地的从来不是媒体编码器而是网络传输层那套看不见摸不着的机制。这正是标题里“WebRTC网络传输基本知识”被单独拎出来研究的根本原因。它不是教你怎么写new RTCPeerConnection()而是回答一连串更底层的问题当你的视频帧从A端发出它到底经过了几层封装NAT设备为什么会把它拦在门口STUN返回的“反射地址”和你本机IP到底差在哪为什么ICE候选者里既有host类型又有srflx类型甚至还有relayTCP和UDP在WebRTC里究竟谁在干活、谁在打杂这些知识不会出现在MDN文档的API列表里但它们直接决定了你写的WebRTC应用是能稳定支撑百人会议还是三分钟就掉线重连。我参与过某高校实验室的远程显微镜协作项目初期版本在实验室内网跑得飞快但一旦接入校外导师的家用宽带延迟飙升到800ms以上频繁出现黑屏。排查两周后才发现问题出在ICE候选者收集策略上默认只启用了STUN没配TURN服务器而导师家路由器是严格对称型NATSTUN根本拿不到可用的公网映射地址。这个坑就是典型“只懂API、不懂传输”的代价。所以本篇不讲如何创建PeerConnection也不贴一段JS代码完事。我们要回到网络协议栈的视角一层层剥开WebRTC传输层的肌肉与神经——从最基础的UDP包结构开始到ICE框架如何组织候选路径再到DTLS如何在不可靠传输上建立加密通道最后落到SRTP如何保护每一帧音视频数据。这不是理论复习而是为后续源码调试打地基当你在webrtc/src/p2p/base/目录下看到PortInterface、CandidatePair这些类时你能立刻对应到现实中的NAT穿透行为当你在Wireshark里抓到一堆STUN Binding Request包时你知道它正在尝试哪一类NAT穿越。提示本文所有技术点均基于WebRTC M115主线代码2023年中后期稳定版源码路径、类名、关键函数名均与当前主流Chromium分支一致。若你使用的是旧版SDK或封装库如SimplePeer、PeerJS请务必注意其对底层传输逻辑的抽象可能掩盖了关键细节。2. UDP不是“随便发”WebRTC如何用最小代价构建可靠传输骨架WebRTC选择UDP作为底层传输协议绝非因为“UDP简单”。恰恰相反UDP的“不可靠”特性逼迫WebRTC必须在应用层重新发明一套比TCP更精细、更场景化的可靠性保障机制。理解这一点是读懂webrtc/src/modules/rtp_rtcp/下数千行C代码的前提。先看一个反直觉的事实WebRTC里90%以上的数据包走的确实是纯UDP——没有重传、没有确认、没有滑动窗口。那么它凭什么保证视频不花屏、音频不破音答案藏在三个关键设计里分层冗余、前向纠错FEC、以及极其克制的重传NACK。我们以H.264视频流为例。一个GOPGroup of Pictures通常包含一个I帧关键帧和多个P帧预测帧。I帧体积大、信息全丢失一个就导致后续P帧全部无法解码形成长达数秒的黑屏而P帧体积小、依赖强丢失一个影响范围有限。WebRTC的应对策略是分层处理I帧绝不重传因为重传耗时远超其播放间隔比如I帧每2秒一个重传可能要300ms等重传回来下一帧都播完了。取而代之的是在发送I帧的同时附带发送一份低分辨率的冗余I帧Redundant Encoding或者用FEC生成校验包如ULPFEC接收端收到任意一份即可解码。P帧按需NACK当接收端检测到某个P帧丢失通过RTP序列号空缺判断立即向发送端发一个NACK包请求重传该序列号的包。但WebRTC做了关键限制NACK只针对最近100ms内发出的包有效。超过这个时间窗发送端直接丢弃重传请求——因为等重传回来播放时间早已过去强行插入只会加剧抖动。这个“100ms时间窗”不是拍脑袋定的。它源于WebRTC的Jitter Buffer抖动缓冲区设计。Jitter Buffer默认缓存300ms的音视频数据用于吸收网络抖动。如果NACK请求的包能在100ms内返回它大概率还能赶在Jitter Buffer的“播放指针”到达前塞进去超过100ms塞进去反而会拉长整体延迟破坏实时性。再看代码印证。在webrtc/src/modules/rtp_rtcp/source/rtp_receiver_video.cc中OnReceivedPayloadData函数处理接收到的RTP包时会调用video_receiver_-OnReceivedFrame(...)。而真正的NACK逻辑藏在rtp_rtcp_-OnReceivedPacket(...)里——它解析RTP头检查序列号连续性一旦发现空缺触发nack_module_-OnReceivedPacket(...)后者维护一个滑动窗口默认大小1000只对窗口内的丢失序列号发起重传。注意NACK本身也是UDP包它不保证送达。所以WebRTC采用“指数退避重发”策略第一次NACK未响应20ms后发第二次40ms后第三次80ms后第四次……直到达到最大重试次数默认10次或超时默认500ms。这个参数在webrtc/src/modules/nack_module/nack_module.cc的kMaxNackRetries和kNackTimeoutMs中定义。实操中我曾在一个弱网测试环境模拟30%丢包率下对比不同NACK策略的效果。将kNackTimeoutMs从500ms调低到200ms虽然重传成功率下降了12%但端到端平均延迟降低了37ms主观体验更流畅反之调高到1000ms重传成功率升至98%但延迟飙升到1.2s完全失去实时通信意义。这印证了WebRTC的设计哲学在实时性与可靠性之间永远向实时性倾斜。3. ICE框架不是“选一个最好的候选者”而是动态维护一张活的传输地图很多开发者误以为ICEInteractive Connectivity Establishment的作用就是让两端交换一堆IP:Port然后挑一个“延迟最低”的连上。这种理解过于静态也完全忽略了WebRTC在真实网络中面临的动态挑战——NAT映射会老化、WiFi切换成4G、防火墙策略临时收紧……这些都会让昨天还通畅的候选路径今天彻底失效。ICE真正的核心能力是构建并持续维护一张“活的传输地图”Live Transport Map。这张地图包含三类节点候选者host候选者本机直连地址如192.168.1.100:50000仅限同局域网通信srflx候选者经STUN服务器反射后的公网地址如203.208.60.1:50000适用于大部分NAT环境relay候选者经TURN服务器中继的地址如119.147.200.1:3478作为最后兜底方案代价是增加延迟和服务器带宽。但ICE的精妙之处远不止于收集这三类地址。它通过一套严密的状态机和定时探测机制让这张地图始终反映网络真实状况。首先看候选者配对Candidate Pairing。ICE不会让A端的每个候选者都去连B端的每个候选者那样是O(n²)复杂度。它采用“排序优先级”策略为每个候选者计算一个优先级值公式为2^32 * type_preference 2^24 * local_preference 2^16 * component_id 2^0 * foundation_hash然后按优先级从高到低排序。type_preference规定了类型权重host(126) srflx(100) relay(0)所以host-host配对永远优先于srflx-srflx哪怕后者延迟更低——因为host路径最短、最可控。其次看连通性检查Connectivity Check。ICE不是一次性检查所有配对而是按优先级顺序逐个发起STUN Binding Request探测。关键在于每个探测包都携带一个随机事务IDTransaction ID且要求对方必须用相同的ID回包。这个设计解决了NAT映射老化问题。例如A端通过STUN拿到srflx地址后该映射在NAT设备上通常有30-60秒有效期。ICE会每隔25秒就向B端的srflx地址发一次探测包只要B端回包成功NAT设备就会刷新该映射的存活时间。这就形成了一个“心跳保活”机制。再看状态维护State Maintenance。ICE定义了Waiting、InProgress、Succeeded、Failed四种状态。但Succeeded不等于永久成功。WebRTC在webrtc/src/p2p/base/ice_transport_internal.cc中实现了CheckAndPingStaleCandidates函数它会定期扫描所有已成功的候选对对超过kStunKeepaliveIntervalMs默认15000ms未收到响应的配对重新发起探测。一旦连续3次探测失败该配对状态降为Failed并触发OnConnectionStateChange回调通知上层应用链路异常。我在调试某跨平台会议系统时就遇到过一个经典案例iOS端在锁屏后系统会主动关闭后台UDP socket导致所有srflx配对在15秒后全部失效。但ICE并未立即切换到relay路径因为relay配对的初始探测被系统后台策略延迟了。最终解决方案是在iOS端监听UIApplicationDidEnterBackgroundNotification事件主动调用ice_transport_-StopAndClose()强制触发ICE重启确保唤醒后第一时间建立relay连接。注意ICE的“候选者收集”并非一劳永逸。webrtc/src/p2p/base/basic_port_allocator.cc中的AllocatePorts函数会在kPortAllocationIntervalMs默认30000ms后自动触发新一轮候选者收集。这意味着即使你没手动调用pc.addIceCandidate()WebRTC底层也可能悄悄新增relay候选者——这是为应对网络拓扑变化预留的后手。4. DTLS-SRTP在UDP之上如何用两次握手建立加密隧道而不牺牲实时性WebRTC的安全模型常被简化为“用DTLS握手建信道再用SRTP加密媒体流”。但这种说法掩盖了一个关键矛盾DTLS基于TLS 1.2而标准TLS握手需要2-3次往返RTT在实时音视频场景下这会导致数百毫秒的建立延迟。WebRTC是如何把DTLS握手压缩到1个RTT甚至实现0-RTT恢复的答案就在webrtc/src/rtc_base/ssl_adapter.cc和webrtc/src/modules/audio_coding/neteq/tools/rtcp_analyzer.cc的协同设计中。核心突破点是WebRTC对DTLS 1.2的深度定制。标准DTLS握手流程如下Client Hello → Server Hello → Certificate → Server Key Exchange → Server Hello DoneClient Key Exchange → Change Cipher Spec → Encrypted Handshake Message共需2.5个RTT。WebRTC将其优化为Client Hello含密钥共享参数Server Hello Certificate Server Key Exchange Hello Done四合一压缩包Client Key Exchange Change Cipher Spec Finished客户端终确认这将握手压缩到1.5个RTT。更进一步WebRTC支持会话复用Session Resumption当同一对端在短时间内如5分钟内重建连接客户端可在Client Hello中携带上次会话的session_id服务端若查到缓存的主密钥Master Secret可直接跳过证书交换返回Server Hello含相同session_id和Change Cipher Spec客户端验证后立即发送Finished。整个过程仅需1个RTT即所谓的“简短握手”。而真正的0-RTT则依赖于预共享密钥PSK模式。在webrtc/src/rtc_base/ssl_stream_adapter.cc中SetDtlsSrtpKeyAgreement函数允许传入预置的密钥材料。当启用PSK时Client Hello直接包含pre_shared_key扩展服务端验证密钥ID后可立即生成密钥并发送加密的Server Hello客户端收到后无需等待直接用预置密钥解密并发送加密的Finished。此时从第一个包发出到加密通道可用理论上只需1个UDP包的传输时间。但这引出另一个问题DTLS只加密信令和密钥交换真正的音视频载荷由SRTPSecure Real-time Transport Protocol加密。那么DTLS和SRTP的密钥如何安全交接WebRTC采用密钥派生Key Derivation机制DTLS握手完成后双方利用协商出的主密钥Master Secret通过PRFPseudo-Random Function算法派生出四组SRTP密钥client_write_SRTP_master_key客户端发SRTP的密钥server_write_SRTP_master_key服务端发SRTP的密钥client_write_SRTCP_master_key客户端发SRTCP的密钥server_write_SRTCP_master_key服务端发SRTCP的密钥这些密钥长度、算法均由DTLS的Cipher Suite决定。例如若协商TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256则SRTP密钥为128位AES-GCM认证标签Auth Tag为16字节。我在分析某教育直播系统的安全审计报告时发现其DTLS配置未禁用弱密码套件如TLS_RSA_WITH_AES_128_CBC_SHA导致中间人可实施降级攻击。修复方案就是在PeerConnectionDependencies初始化时显式调用configuration.SetBundlePolicy(webrtc::kBundlePolicyMaxBundle)并设置configuration.sdp_semantics SdpSemantics::kUnifiedPlan强制启用现代密码套件。提示DTLS握手失败是WebRTC连接失败的第二大原因仅次于ICE失败。常见日志如DTLS handshake failed: certificate verify failed往往指向证书链不完整或域名不匹配。WebRTC默认要求证书的subjectAltName必须包含实际连接的IP或域名且不能使用自签名根证书——这是生产环境必须解决的硬性门槛。5. 抓包实战从Wireshark里看懂WebRTC传输的每一帧心跳理论终须落地。我建议所有想深入WebRTC网络传输的人第一步不是读源码而是打开Wireshark亲手抓一个真实的WebRTC连接包。下面是我总结的、能快速定位90%传输问题的抓包分析法步骤清晰无需高深网络知识。5.1 抓包前的关键准备过滤规则与环境隔离首先必须在发起连接的本机抓包而非服务器或中间路由器。因为WebRTC的ICE候选者、DTLS握手、SRTP载荷都是端到端行为中间设备只能看到加密后的UDP流。启动Wireshark后设置捕获过滤器Capture Filter为udp port 50000-65535 or tcp port 443这个范围覆盖了绝大多数WebRTC应用的默认端口UDP用于媒体TCP 443用于信令或TURN over TCP。若知道具体端口可精确到udp port 53210。更关键的是显示过滤器Display Filter。WebRTC流量混杂在HTTP、DNS等其他UDP包中必须用以下组合过滤(ip.addr 192.168.1.100 || ip.addr 203.208.60.1) (udp || tcp)其中192.168.1.100替换为你本机IP203.208.60.1替换为对端srflx或relay地址可在Chrome的chrome://webrtc-internals页面的localDescription中找到。为避免干扰务必关闭所有其他网络应用微信、浏览器其他标签页、云同步服务只留测试页面。我曾因未关闭OneDrive导致抓包结果里充斥着大量SMB协议包浪费两小时排查。5.2 三步定位法从STUN到SRTP的完整链路追踪第一步确认ICE是否成功穿透在过滤后的包列表中搜索stun。正常流程应看到A端向STUN服务器如stun.l.google.com:19302发送Binding RequestType0x0001STUN服务器返回Binding Success ResponseType0x0101其中XOR-MAPPED-ADDRESS字段即srflx地址A端随即向B端的srflx地址发送Binding Request这才是真正的连通性检查B端回复Binding Success Response此时Wireshark会标记该UDP流为STUN协议若卡在第一步无响应说明本机到STUN服务器不通检查防火墙或STUN地址是否正确若卡在第二步A发了Request但无Response说明B端NAT阻止了入向UDP包需启用TURN。第二步验证DTLS握手是否完成过滤dtls寻找Client HelloType0x01和Server HelloType0x02。正常应看到Client Hello中Cipher Suites字段包含TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256等现代套件Server Hello后紧跟着Certificate包长度通常1000字节最终以Encrypted Handshake MessageType0x16结束若看到Alert包Type0x15点开查看详情Level2Fatal且Description40Handshake Failure基本确定证书或密钥协商失败。第三步确认SRTP媒体流是否加密这是最关键的一步。过滤rtp找一个RTP包右键→Decode As...→将Transport改为RTP。此时若能看到Payload Type126VP8或100H.264且Sequence Number、Timestamp字段正常递增说明SRTP解密成功。若显示Malformed Packet或Unknown Payload Type说明Wireshark未正确解密SRTP。此时需手动导入密钥在Edit → Preferences → Protocols → SRTP中点击添加密钥Key字段填入webrtc-internals页面中srtp_local_key的值Base64解码后为30字节密钥Salt填srtp_local_saltCipher选AES-CMAuth选HMAC-SHA1-80。5.3 一个真实故障的抓包诊断全过程去年调试某医疗问诊App时用户反馈iOS端视频卡顿严重。抓包后发现STUN和DTLS均正常stun和dtls包流畅通但rtp包中Sequence Number从1000开始跳到1050再跳到1120明显丢失更奇怪的是rtp包的Timestamp字段停滞在123456789长达2秒进一步过滤rtcp发现大量RRReceiver Report包其中Fraction Lost字段高达0x80128/25650%丢包但Cumulative Number of Packets Lost却几乎不增长。这说明丢包发生在接收端Jitter Buffer之后而非网络传输中。最终定位到iOS端AVCaptureSession的minFrameDuration设置为CMTimeMake(1, 15)强制15fps但服务器推送的是30fps流。接收端来不及处理导致Jitter Buffer溢出丢帧。解决方案是改用AVCaptureSessionPresetPhoto并动态调整videoOrientation而非硬编码帧率。经验Wireshark里rtp包的SSRCSynchronization Source Identifier是唯一标识。同一个视频流的所有RTP包SSRC值相同。若看到多个不同SSRC的RTP流说明开启了多路编码Simulcast需分别分析各路质量。