一、Web P2P的前世今生
2010年,Adobe在Flash Player 10.0推出了实时流媒体RTMFP,使用RTMFP在Flash Player之间进行P2P成为可能。在随后的几年内,该协议在网络上如雨后春笋般的被广泛部署,但是从2020年末开始,各家的浏览器都已经彻底不支持Flash了。我们又应该如何在Web里面使用P2P呢?
诞生于2011年的WebRTC技术,在Google、Microsoft、Apple、Mozilla等大厂的支持下,成为了H5的标准,并且Chrome、Edge、Safari、Firefox等主流浏览器也都支持WebRTC。所以就可以通过WebRTC来实现P2P了。
下文把基于WebRTC实现的HTML5 P2P简称为H5 P2P。
二、WebRTC简介
WebRTC的总体架构如下图所示,如果是浏览器开发者,通常需要关注并实现WebRTC C++ API,而我们的目标是基于浏览器实现P2P,所以只需要关注Web API即可。
整体的H5 P2P都是基于RTCPeerConnection和RTCDataChannel来实现的,只是实现数据层面的P2P,不包含音视频通信的内容。
三、总体架构
1. H5 P2P客户端
主要包含通信模块、传输模块、节点管理、调度模块、存储模块和统计模块:
通信模块:主要负责和服务端(上图中的Index、CPS、Tracker)进行信令的通信
传输模块:包含HTTPClient和基于RTCDataChannel实现的RTCClient(上传&下载)
节点管理:不同用户间P2P主被动连接、断开、淘汰、资源信息同步
调度模块:根据buffer水位、分片信息、节点质量和规模进行CDN/P2P调度
存储模块:负责媒体数据的存储、排序、以及淘汰
统计模块:包含系统运行的埋点的实现
2. H5 P2P服务端
主要包含Index、CPS、Tracker和Stun服务:
Index:索引服务,提供其他服务地址。
CPS:资源属性服务,将直播流的URL转换为RID。
Tracker:资源追踪服务,主要存储用户和资源之间的索引关系。
MQTT:信令服务,转发用户间建立P2P连接的offer/answer/candidate SDP协议。
Stun:采用coturn部署,主要用户节点之间建立对等连接的UDP穿透。
四、阶段流程
下图简略的展示了整体方案的各个阶段、流程:
Step 1:网页打开,首先向Index服务请求其他服务的地址,保存于内存中
Step 2:客户端收到直播请求,向CPS发送请求将URL转换为资源ID(RID),相同内容相同清晰度的直播流的资源ID(RID)是相同的,表明可以互相分享数据
Step 3:客户端首先会启动HTTPClient进行数据下载,优先保证秒播。同时做以下2件事
a. 客户端向Tracker服务发送Address协议,获取其他正在观看该直播的用户地址,待后续建立连接,分享数据
b. 客户端向Tracker服务发送Publish协议,将自己的信息告知Tracker,使得别人可以即使搜索到自己,进行数据分享
Step 4:向从Tracker获取的其他用户建立连接,彼此交换信息,数据分享
五、连接建立
假设在Peer1和Peer2之间传输数据,那么在传输之前,首先需要建立连接。在WebRTC中,浏览器间通过ICE框架建立对等连接,主要包含Stun、TURN,下面分别简单介绍一下:
1. ICE
ICE(Interactive Connectivity Establishment)提供的是一种P2P连接框架,统一各种NAT穿透技术(STUN,TURN)。基于offer/answer/candidate模式,通过多次地址交换,然后进行连通性测试,穿透用户网络间各类防火墙。ICE主要包含STUN、TURN等协议。
2. STUN
STUN(Simple Traversal of UDP Through NAT)允许位于NAT设备后的客户端找到自己的公网IP/Port地址,以及查询出自己的NAT设备类型,通过这些信息使得两个同时位于NAT后的2个设备之间建立UDP通信,具体的STUN流程可以参考RFC5389,不在此赘述。
3. TURN
通俗的说,TURN(Traversal Using Relays around NAT)就是中继/转发,并不是所有的设备之间都可以依靠STUN来建立连接的,那么当两个设备之间无法依靠STUN建立连接时,就会启用TURN来进行数据中转。
由于我们的目标是节约带宽,如果采用TURN转发的话,并不能减少服务器的带宽消耗,另外一个原因是,在整个建立连接的过程中,有别于传统的视频通话只能1对1,这里的传输模式是1对多传输,所以并不要求每个连接都可靠,因此在H5 P2P项目我们仅仅通过STUN来实现连接的建立。
六、数据下载
1. 资源编码
Web P2P网络直播方案也可以同时支持任意多路直播同时进行,必须保证各路直播流之间的独立性,因此,我们提供一种统一的资源编码方式,用来标识唯一的一路直播流。这里主要借用直播流中的StreamId,通过计算MD5,生成固定长度的资源ID(RID),不仅保证了内容相同,清晰度不同,RID也不同;也保证了相同的内容,由于断流切换源地址,导致URL变化,保证资源ID(RID)相同。
2. 下载调度
在整个直播过程中,由于可播放缓存的数据是非常小的,通常不超过10秒?因此如何实时的根据当前的网络状态、网络环境,作出使用CDN,又或是使用P2P的决策呢?这里,我们的最佳实践主要如下:
a. 为了保障用户的观看体验,当缓冲区的可播数据低于某个阈值(例如5秒),就切换CDN下载
b. 当发现某一片未来的数据,周围的人都没有的时候,即使可供播放的buffer还有很多,不满足a策略的描述,这个时候,为了尽快的分发数据,也应该及时切换至CDN,出现这种情况时,往往表示这个节点以及他连接的节点都处于整个直播数据分发的顶层,为了防止出现 “一人下载多人围观” 的场景,本着更快的下载导致更早的上传理念,不仅仅要求该节点自身尽快CDN下载,同时也保证周围用户某个比例(例如15%)也尽快采用CDN下载,从而使得整体数据的分发效率提升。
3. P2P任务分配
a. 任务编号
对于每一个数据块都有一个自然数编号(sn),对于这个数据块,我们按照固定长度(默认16KB)的大小进行切分,简称chunk_index,那么(Rid, sn, chunk_index)就可以唯一确定一个数据片了。
b.分配请求
分配过程类似于扑克中的发牌机制,唯一的要求是对方这个节点拥有这个数据块,并且质量越好的节点,越多越分配靠近播放点的、重要的数据。下图简明扼要的展示了3个相同质量节点分配2个数据块(9个数据片)的过程。
4. 内存控制
对于Web P2P来说,所有的媒体数据都缓存在内存中,但是,也并不是会缓存完整的数据。对于直播来说,所有用户的观看点都是非常接近的,因此,内存始终缓存播放点前后的固定大小的数据。因为只有这部分数据分享利用率才是最高的。
如上图所示,有颜色的数据块都是当前在内存缓存中的数据,其中红色数据块表示当前的播放点,黄色数据块表示过去已经播放过的内容,绿色数据块表示未来还未播放的内容,可以看到当播放点从100移动至101时,97号数据块就被淘汰了,同时103号数据块作为最新的数据被下载下来并缓存到内存中。
七、工程实践
1. Goggle FlatBuffers
不管是服务器通信还是点对点的P2P传输均采用了FlatBuffers。FlatBuffers具有跨平台、内存和效率速度高,扩展灵活等优势,被我们全平台统一采用,大幅降低了通信成本。
2. 多浏览器兼容
线上支持WebRTC的浏览器不仅种类繁多,而且同一个产品的不同版本之间也可能千差万别。为了解决这一痛点,最终采用了WebRTC adapter.js来解决这一挑战。
3. 内存池
尽管Node.js自带垃圾回收机制,以及内存管理策略,但是GC和内存分配也是需要成本的,况且Web端是单线程环境,任何性能的提升都会减少时间上的损耗,进而可以处理更大吞吐的网络层传输以及业务逻辑。所以在内存分配上我们采用了内存池策略,基本实现了100%的内存重复使用。
八、技术结果
下图展示某次大型直播的带宽分时统计,显示在19:25到21:00阶段内的真实数据,随着人数的涌入和离去,Web P2P全程带来可观的带宽收益。最终评估Web P2P,在不影响用户使用体验的前提下,不仅可以有效降低服务器的负载压力,并且能够降低可观比例的带宽成本。
九、思考总结
目前,该技术已经大规模应用在各网络直播中,良好的解决了服务质量与带宽成本的平衡。但是,我们也发现在超大规模的直播下,存在了一些提升的空间,主要如下:
(1) coturn与MQTT的服务性能不高,会制约直播的服务质量。
(2) 接入边缘网络,打造多元化网络,缓解CDN负载,并且可以进一步降低CDN的带宽成本。