你在下载的时候,有没有体验过 P2P 下载,能够让你的网速从 10KB 直接提升到 10MB? 你在企业内传输文件的时候,有没有体验过文件秒传? 你在看直播的时候,想不想用别人的流量看直播呢? ...
能做到上面这些场景的技术,叫做 P2P。P2P 技术中,最出名的叫做 WebRTC。WebRTC 是一个含金量非常高的技术。做好的话你可以养活一家公司,做不好,那就只能是一个 demo。
WebRTC 虽然能做很多事,但是并不是所有场景都适合。最大的使用场景是 两个终端在同一个 NAT 内,简单来说,都在一个 wifi 内。这个场景中,最显著的效果就是带宽无限并且高速,你走的就是内部的线路,根本不消耗运营商的流量。
P2P 技术在基于 WebRTC 标准下,可以做很多事情:
作为 Web 开发,WebRTC 又能够给前端赋能些什么呢?
在了解这些基本内容后,接下来,我们会从底层一步一步介绍一下 P2P 在 Web 直播的应用。
P2P 穿透也可以叫做 NAT 穿透,这是 P2P 最大的一个难点。为了解决 ipv4 不够用,推出了 NAT 技术。NAT (Network Address Translation)是用来将内网私有 ip,转化为公有 ip。简单点,就是让很多台电脑公用同一个 IP。但是 NAT 有个非常重要的点:
NAT 不允许外网主机主动访问内网主机。
这个不允许访问的机制也有很多种,根据这些特性,我们可以将 NAT 分为多种:
一般情况下,前面三种 NAT 是可以穿透的,但是,对称型NAT 无法穿透。具体内容,大家网上搜一搜 NAT 穿透,资源应该很多。在穿透时,我们不仅需要考虑 NAT 还需要考虑到集群机器的防火墙设定,如果防火墙限制了 UDP 打洞,那么我们还需要切换为 TCP 打洞(TCP 打洞一般会慢一点)。
总的来说,我们穿透时需要考虑的问题就有:
这些问题一旦组合起来,这个复杂度就是 N*N 的关系了。如果搭建 p2p 每次都需要从头解决这个内容,P2P 也不会像现在发展的这么好了。WebRTC 就是用来解决这一问题的标准模板,通过 STUN/TURN Server 来实现打洞穿透。
这里,我们按照一个比较常见的情况作为模板讲解一下。两端都位于 NAT 层背后,并且,NAT 是可以穿透的 Full Cone NAT 类型。具体穿透流程如下:
在 WebRTC 完成这里流程的 API 是: RTCPeerConnection
。通过自建的一个中间 Server,来交换指定的 SDP 和 candidate。
SDP 是当前 Point 的一些基本描述信息,当前 WebRTC 版本 ICE 的描述信息,以及,对已经连接的 ICE 内容的描述,比如 video/audio 信息。SDP 这一环节,其实就是告诉了哪两个 Point 会进行连接。本身和打洞并没有太大的关联。具体内容可以参考:SDP antonomy
candidate 则是打洞的关键信息,里面会包含当前 Point 的内外网 ip:port,以及防火墙设定规则 tcp/upd。这里有一点需要注意的是,一个 point 为了能够提高 NAT 打洞的成功率,会产生多个 candidate。这里主要取决于几个 candidate 里面几个基本参数:
video
、 audio
tcp
、 udp
不过, srflx/prflx
类型的 candidate 只会在你初始化 RTCPeerConnection
时,传入 iceServers
的 STUN 服务器 URL 时,才会获得。
config = {
"iceServers": [{
"urls": ["stun:stun.l.google.com:19302"]
}],
"iceTransportPolicy": "all",
"iceCandidatePoolSize": "0"
}
在 webRTC 代码层面,我们并不需要额外针对 candidate 做逻辑处理。我们只需要将 candidate 传给另外一端,通过 addIceCandidate()
注册到 RTC 内部即可。
local.addIceCandidate(remoteCandidate)
.then(e=>{
console.log("add success");
})
通过云服务器完成 candidate 和 sdp 信息的交换后,我们就已经做完了 RTC 连接的必要准备。剩下的就是在连接建立完成之后做的状态监听和其他扩展事情。
这里面最大的一个问题在于,我们完成数据添加之后,怎么判断 P2P 是否连接上。WebRTC 提供了我们 7 个基本的事件监听:
attribute EventHandler onnegotiationneeded;
attribute EventHandler onicecandidate;
attribute EventHandler onicecandidateerror;
attribute EventHandler onsignalingstatechange;
attribute EventHandler oniceconnectionstatechange;
attribute EventHandler onicegatheringstatechange;
attribute EventHandler onconnectionstatechange;
通过 onconnectionstatechange
就可以得到连接状态的变化。直接绑定该方法,即可获得相关内容:
peer.addEventListener('connectionstatechange',event=>{
// get the state from event
},false);
其能够提供的事件回调信息,可以直接参考:connection states。里面,我们只需要判断状态是否是 connected
,来决定该次连接是否成功。
如果不成功,我们可以直接从 onicecandidateerror
里面获得相关的错误信息,具体可以参考:ICE gather error。当然,在连接过程中,也可以直接从 Promise 中,获得连接失败的信息,这部分内容可以直接参考:RTC Error。
上面整个代码流程可以直接参考:webRTC trickle candidate
打洞过程是 WebRTC 最基础的一步,如果连这一步都没成功,那么后面就需要做一些其它适配的兼容。比如,直接通过 CDN 拉去资源。WebRTC 打洞成功后,我们就可以利用这个打洞包,根据用户的种子资源数、上行带宽、下载进度来判断 P2P 传输的资源。
WebRTC 原生提供了 RTC Media API、RTCDataChannel、DTMF 这三个传输通道。Media 能够直接结合 getUserMedia
API 传输当前摄像头和麦克获取的音视频。并且 Chrome 浏览器在底层内嵌了很多功能强大的编码器,这里可以直接参考:webRTC FRQ.
不过,经过测试 WebRTC 回声消除和双端同时对话的效果并不是特别好。这块,大家可以考虑一下,能不能直接在底层替换编码器或者购买其他服务。
Media 和 DTMF 通常都需要建立在 getUserMedia
的前提下,但是,IOS11 并不支持,它只支持 DataChannel 传输数据的 API。所以,这里,我们只会针对 DataChannel 来做一些讲解。如何通过 DataChannel 来传输你的自定义文件内容。
DataChannel 是 PeerConnection 的一个拓展 API,可以直接通过 createDataChannel
来创建一个 SCTP 通道。SCTP 是一种高效的帧传输协议,它和 TCP/UDP 是在同一层的,集中了两者之间的优势。我们只需要在发送端创建 Channel,接收端直接监听 ondatachannel
事件即可。
let senderChannel = localPeer.createDataChannel('dataChannel');
senderChannel.onopen = ()=>{
if(senderChannel.readyState === 'open'){
senderChannel.send(someBuffer);
}
};
senderChannel.birnayType = 'arrayBuffer';
remotePeer.ondatachannel = function(event){
// receive data from event.channel
remotePeer = event.channel;
remotePeer.binaryType = 'arraybuffer';
remotePeer.onmessage = onReceiveMessageCallback;
}
RTCDataChannel 一共可以发送两种数据:String 和 Binary(也可以叫做 ArrayBuffer,ArrayBufferView or Blob) 具体直接参考:dataView。
Channel 具体的用法其实就和 WebSocket 一样,通过 send 来传递相关的信息,再监听 onmessage
事件获得数据的回调。
上面的流程大致的覆盖了打洞的云端流程,比如穿透 NAT 层,P2P 数据 Channel 建立。不过,中间还有很多细节并没有解决清楚:
而这些问题就需要落地到具体的业务当中了。因为现在直播行业非常的火热,本人也在该行业里面摸爬滚打,了解到该行业一些基本的痛点。最大的就是非常吃带宽,为了解决这一问题,我们完全可以利用 P2P 来做直播带宽的节省。
但是,说起来很容易,怎么做这才是关键?
不过,对于 Web 开发来说,这里只介绍一下 端
的基本思路。在 Web 直播中比较流行的是通过 http-chunked 模式来实现直播。而 http-chunked 协议很难得到具体播放的进度,那我们就需要一个能够很容易获得播放进度的协议--切片协议。
对于切片协议而言,最为突出的就是 HLS,但是它的延时性过高,没办法满足直播低延时的问题。针对这个点,MPEG 提出了一个 DASH 协议,来作为直播内容协议的补充。直播 DASH 是基于 HTTP-URL 的动态直播协议,通过 URL 上面的时间戳就很容易做到对流的时间记录,整个端的 P2P 流程图为:
云端这一步,就主要在 SegmentProtocol
做的事情,右边的则是本地播放的 IS/MS 处理。而如果你能够在云上做好这一整套流程,比如:
那么,你的 P2P 功能和编码能力应该比一般程序员高太多了。
入门 Tip
事情需要一点一点做,饭需要一口一口的吃,DASH 由于协议比较复杂,推荐大家先从 HTTP-Chunked 协议入手,自己先搭建一个直播的 DEMO 试试水,这里推荐一下 HTTPLIVE 库。
另外,如果读者对前端音视频很感兴趣,不甘心只做一个纯纯的 UI 工程师的话,可以直接关注我的公众号前端小吉米,输入 MSE
加入 前端音视频的交流小组
。