这是我在公司内部的一次分享,想要让小伙伴对WebRTC都有所了解,并且可以上手去做一个基于webrtc的应用。虽然几乎所有人都知道,webrtc是一个浏览器端内置的点对点接口,甚至是准标准了。但是,到底怎么利用这一个已经不是新特性,但是很不幸的是,大家对这东西还是只停留在听说过,怎么才能使用它呢?怎么利用webrtc作出一个我们想要的p2p应用呢?这篇文章结合我的分享,再加一些补充,把关于webrtc入门的东西讲清楚。
什么是WebRTC?
到底什么是WebRTC?其实这个问题并没有三两句那么清除,要解释很多词。我总结起来,只能用一些侧面的,但是容易理解的内容进行解释:
- 全称为Web Real-Time Communications,即web实时通讯
- Peer-to-peer,点对点
- to capture and optionally stream audio and/or video media, as well as to exchange arbitrary data between browsers 抓取用户的视频或音频流,也可以传输任意数据类型,在浏览器之间(between是两个之间的意思,所以是说webrtc仅是两个之间的事,没法整3个之间的事)。另外,还要注意“浏览器”这个点,webrtc是浏览器内置的,跟很多其他浏览器自带的接口,例如websql一样。但是实际上,webrtc的接口完全独立出来,所以也不一定非得在浏览器环境下,目前node、react-native都有对应的包使得它们支持使用webrtc。
- without requiring an intermediary 不需要中间就可以传输(忽悠吧)
- without requiring that the user install plug-ins or any other third-party software 不需要插件或第三方软件
这是从MDN上面抄来的解释,这里面有个坑,就是,webrtc的初衷,是为了解决点对点的媒体传输问题,从这个点考虑,视频通话这样的场景是最适合的,没有之一。但是,我们还想把这个事情整深入一点。不过,在这之前,我们必须了解,作为开发者,怎么一行一行,把这些接口都使用起来。
历史和现状
作为一篇完整的文章,还是需要有一些废话把webrtc的前世今生讲一下。讲到点对点媒体通讯技术,不得不讲到一家公司。2011年的时候,Google 收购了GIPS,它是一个为RTC开发出许多组件的一个公司,例如编解码和回声消除技术。Google收购了它才一年,就在2012年开源了GIPS开发的技术,开源的时候,就以WebRTC作为开源技术的名称,并开始积极与相关机构IET和W3C制定行业标准。
GIPS早就被很多公司购买使用,例如QQ(如图)
可以说这家公司开发了从早期开始,点对点媒体通讯领域最可靠的技术,被全球各家公司使用。因此,它们的技术不可言喻的属于顶级。谷歌拿到它们的技术之后,就把它们开源了,可以说对于其他开发厂商而言真的是福音中的福音。而这个被开源出来的东西,就叫webrtc。
目前,webrtc已经得到了多个浏览器的支持,主要是chrome、firefox、opera,但是ie和safari还不完全支持。如果想要做一款基于webrtc的应用,就必须在你的客户端里面去使用支持webrtc的浏览器内核。不过幸运的是,node和react-native都已经有人做了包,可以实现将webrtc集成到应用中,这样,基于electron和react-native的点对点应用就显得非常容易了。
WebRTC的API
webrtc给了我们三个主要的api接口,我们可以利用这三个接口创建完整的媒体传输,甚至是任意数据的传输通道。
- MediaStream (getUserMedia)
- RTCPeerConnection
- RTCDataChannel
MediaStream(getUserMedia)
MediaStream是获取用户媒体输入信息的接口,比如设备的摄像头输入、麦克风输入等,将来可能还支持其他类型的设备输入,不过目前而言,主要就是这两个。在获取到这些输入之后,它以“流”(stream)的形式返回给程序代码使用,而“流”又由“轨”组成,比如音轨和视轨。获得这些流之后,直接把它塞到html里面的一个video或audio标签上,就可以看到或听到输入的内容了。传送给peer连接的另外一端时,也是要把流传过去,不过现在已经改成了传轨。
我们用代码来实现:
navigator.mediaDevices.getUserMedia({ audio: true, video: true, }).then((stream) => { let video = document.querySelector('#video') video.srcObject = stream video.onloadedmetadata = () => video.play() })
在html里面放一个video#video,就可以把摄像头和麦克风的stream塞给它,看到自己的影像了。
RTCPeerConnection
这个接口主要用于创建一个peer实例,得到这个实例之后,利用这个实例的各种方法,创建出真实的peer to peer连接。这个过程里面需要了解STUN、TURN协议,ICE框架,Signaling服务,SDP等知识,这我会在下文讲。
创建peer实例
let peer = new RTCPeerConnection(servers)
听上去,p2p挺方便的,但是并不是一个简单的创建过程。要建立一个peer-to-peer连接,可没想的那么容易,用一个new就可以建立?不可能的。要建立一个真正的peer connection,需要用实例化出来的peer的方法进行一系列的操作。
交换身份
peer.addIceCandidate(candidate)
这个用来把要建立连接的对方的网络信息加入自己的本地。什么是对方的网络信息呢?就是它的网络唯一识别地址,如果是普通的网络环境,我们用ip地址就可以标记它。
但是,实际上的网络环境往往会是,client会隐藏在NAT网络背后。因此,要有一种方法,从这种复杂的网络环境下,得到对方peer的识别信息。怎么整呢?这个时候,就要用到STUN协议,这个协议的作用,就是要从NAT网络中,找出另一端在网络中可以被正常访问到的网络路径。
可是,在一些极端情况下,STUN也无法搞定,某些网络设备屏蔽了STUN的识别能力。在这种情况下,只能采用另外一种办法来解决两个peer之间的数据传输了,就是采用TURN协议,实现一个媒体中转服务。所谓媒体中转,其实就是先把视频发送到服务器上面,再由另外一个peer把它下载下来。
上面这套方案被webrtc内置了,它采用ICE框架来实现这套方案,作为开发者,要做的是,告诉程序,你的STUN服务器信息和TURN服务器地址和认证信息。也就是说,作为产品级架构,需要自己搭建STUN和TURN服务器。
怎么把这些服务器信息传给webrtc呢?就是在new RTCPeerConnection的时候,作为参数传进去。
let peer = new RTCPeerConnection({ iceServers: [ { urls: 'stun:stun.l.google.com:19302', }, { url: 'turn:my_username@<turn_server_ip_address>', credential: 'my_password', }, ], })
这样就可以让程序使用你自己的stun & turn服务器了。
但是,怎么最终把candidate身份信息传给对方呢?单凭webrtc是无法做到的,我们需要借助一个服务器来实现,这个就是signaling服务器。这个signaling服务器的作用,就是在利用peer的传输能力之前,建立连接阶段,传输各自的身份信息、描述信息(下面会讲)的。
不是说peer to peer是点对点通讯吗?怎么还要一个服务器呢?这也是没办法的,webrtc实现的时候,完全放开了上述信息交换的协议,因此开发者需要自己实现这块。一般我们会使用一个websocket来实现这个signaling服务,当一个peer需要发送一个signaling的时候,发送一个socket消息到服务器,再由服务器发送一个socket消息给另外一个peer,这样,它们就可以交换信息。
peer.onicecandidate = function(e) { socket.emit('icecandidate', e.candidate) // socket是假设的一个websocket实例 } socket.on('icecandidate', function(e) { let data = e.message peer.addIceCandidate(data) })
在onicecandidate的回调函数里面使用socket发送candidate,在另外一个peer里面,通过soket的事件接收candidate,并且把candidate加入到自己本地。
交换描述信息
什么是描述信息呢?大概就是一个设备的信息,当一个peer打算把自己的设备stream发送给另外一个peer使用的时候,需要将设备信息告诉给对方,比如视频的编码之类的。怎么交换呢?peer需要通过一个offer和answer操作来实现。
let desc = await peer.createOffer() await peer.setLocalDescription(desc) socket.emit('offer', desc) socket.on('offer', async function(e) { let data = e.message await peer.setRemoteDescription(data) let desc = await peer.createAnswer() peer.setLocalDescription(desc) socket.emit('answer', desc) }) socket.on('answer', async function(e) { let data = e.message await peer.setRemoteDescription(data) })
上面这段代码,实际上会在不同的情况下运行不同的部分。当它作为首先发出消息的一方时,它要发送一个offer给远端的peer。而发送的内容,就是通过createOffer创建的description。这个description叫做Session Description Protocol,即很多文档里面说的SDP。当远端peer发送了SDP之后,远端的peer通过websocket接收到之后,用setRemoteDescription把信息塞到本地。
上图里面还反映了一个问题,那就是onicecandidate被触发的时间。我原本以为,这个事件会在new完成之后,但事实上,它会在createOffer或者createAnswer的时候发生。
总之,完成上面的offer和answer之后,两个peer就建立了连接,之后才能传输stream或者其他数据。
发送媒体流
通过前面的getUserMedia接口,我们已经可以拿到当前用户的摄像头、麦克风输入了。怎么把这些输入发送给远端的peer呢?这个时候就不需要借助signaling服务器了,当上面的连接创建之后,只需要调用peer的对应方法,就可以做到了,这里才是真正的点对点数据传输了。
function sendStream(stream) { let tracks = stream.getTracks() tracks.forEach((track) => { peer.addTrack(track, stream) }) } peer.ontrack = function(e) { let stream = e.streams[0] // 把stream塞到video上 }
这样就可以做到将自己的视频流信息发送给远端的peer,并触发远端peer的ontrack。当然,反过来也是一样,对方也可以把自己的stream发送给自己,自己执行ontrack里面的操作。
RTCDataChannel
在使用RTCPeerConnection创建了peer实例,并且创建了连接之后,就可以使用RTCDataChannel接口创建出一个数据传输通道,用来传输任意数据的信息。虽说官方给出的解释是“任意数据”,但是在实际编码中,传输的是字符串……
它的使用就简单的多了:
let channel = peer.createDateChannel('a channel')
通过peer的createDataChannel方法创建一个channel,然后它拥有:
channel.onopen = function() {} channel.onmessage = function(e) { let data = e.data } channel.onerror = function() {} channel.onclose = function() {} channel.send(data) channel.close()
这些方法,一看就知道是干嘛用的,就不赘述了。
其实,使用RTCDataChannel接口的大多数场景,都是为了实现文件传输,特别是一些大文件传输。当两台处于同一个网络里面的电脑使用webrtc进行文件传输的时候,由于不用经过服务器,所以可以实现更高效的文件传输。但是,由于datachannel其实并不能直接发送二进制流,而是只能发送文本(Firefox除外),所以没办法,我们还必须利用html5的特性把文件转换为可转码文本,再进行分片,通过启用多个peer(下文会解释)把文件发送给另外一个客户端,再由另外一个客户端组装文件。
不过在firefox里面,就方便的多,注意,下面的代码仅适用于Firefox:
document.querySelector('input[type=file]').onchange = function () { var file = this.files[0]; dataChannel.send(file); }; dataChannel.onmessage = function (event) { var blob = event.data; // Firefox allows us send blobs directly var reader = new window.FileReader(); reader.readAsDataURL(blob); reader.onload = function (event) { var fileDataURL = event.target.result; // it is Data URL...can be saved to disk SaveToDisk(fileDataURL, 'fake fileName'); }; };
上面的代码出自这里。
关于如何分片传输文件的方法,可以自己谷歌搜一下,方案也挺多的,选择自己喜欢的一种即可。
基于WebRTC的P2P网络
上一部分我们已经了解到了,如何用代码去实现创建一个peer to peer的通讯。现在的问题是,我们如何利用webrtc技术,实现一套应用解决方案,真正把这套技术用到自己的产品里面。要了解这套知识,我把它分为四个层面:
- Level 1: Peer Instance
- Level 2: Client (node)
- Level 3: Network
- Level 4: Complete Service
第一层:peer实例层面
这其实就是前面关于webrtc api的一整套知识。如何利用api接口,创建实例,并且使得两个实例能够创建连接,实现视频、音频甚至是任意类型数据的交换。
但是有一个点不知道你有没有发现?
webrtc顶多在两个peer之间建立连接,不能有第三个peer插足进来。我们看peer的方法就会发现,setLocalDescription, setRemoteDescription等方法,都仅是为了把peer分为local和remote两个角色。这也就是说,peer to peer是指两个peer实例之间的故事,而不是我们平时里说的点对点(node to node),也可能是因为我们平日里对“点对点”这个概念有所误解。
既然一个连接仅能在两个peer之间通信,那怎么可能让很多用户使用这项技术来实现点对点传输呢?
第二层:client层面
我们平时说的“点对点”其实是指“节点对节点”,一个节点(node)是一个客户端的架构设计,对于一个应用客户端而言,你需要把它想象为一个容器。这个容器会与网络中的其他节点进行p2p连接。但是前面已经说了,一个peer to peer只会包含一对peer实例,那么怎么构建多人网络呢?那就是要在容器中放置多个peer实例,每一个实例与另外一个节点容器中的某个peer实例建立连接。
就像图里面显示的一样,一个客户端,想和其他的客户端建立连接,就new一个新的peer出来,用这个peer和对方建立连接。一个client里面有多少个peer,取决于它想和多少个客户端建立连接。
第三层:network层面
当一个一个的节点连接在一起,所有能够相互通信的节点的集合,就是一个network。而对于一个应用而言,可能会出现多个network。这理解起来非常简单,我们以聊天室为例子,一个聊天室里面的所有人,都是一个节点,而整个聊天室就是一个网络。但是,假如我们有两个聊天室,那就会有两个网络。但是很显然,这两个聊天室可能存在相同的一个用户(客户端),而他之所以能在两个不同的聊天室聊天,是因为他的客户端起来了n个peer实例,每个实例跟不同的远端peer连接。
简单的说,一个client可能同时属于多个network。
第四层:service层面
如何保证应用给用户提供完整的可靠的点对点服务呢?比如迅雷下载、微信聊天等等。在上面3层我们已经做好了对peer的管理,也就是在client中创建多个peer,每一个peer完成自己的使命。但是,如何管理好用户在不同network之间的连接和内容传输,需要客户端、服务器通过严格的逻辑进行分发。这里包括用户的认证、权限的分配、组别划分等等。因此,说一个webrtc应用无法离开服务端,也是没有错的。
通过更为复杂的网络架构,可以提高你不同地域、不同网络间的性能或者实现特别的功能。总之,在基于前面的技术基础上,你可以在任何一个环节进行变化,以适应实际的需求。
然而,你有没有发现一个更严重的问题?如果P2P网络依赖于服务端,那么倘若服务端发生故障,也就会导致整个网络瘫痪。有没有一种可能使得提供服务的能力,也通过p2p网络来实现呢?其实,我们有一种方案,就是将stun、turn、signaling服务内置于客户端内部,当用户打开客户端的时候,也起一个本地的服务器。这样,只要当两个客户端可以相互访问时,就可以不在依靠一个中心化的服务器了。
不过这里面有一点需要注意,就是两个客户端可以相互访问对方起的服务器。做到这一点其实不难,对于同一局域网下面客户端,一般都是可以相互访问的,但是,即使处于局域网下面的客户端不能被访问,整个网络中,只要节点数量足够多,也一定存在于公网,能够被任何客户端访问的节点,这样它就可以作为一个对其他可以访问他的节点的signaling服务器了。当然,如果要作为turn服务器,感觉还是不是很好,一方面是安全性受到质疑,另一方面是消耗的资源比较多,如果几千个节点同时连到它,那它估计马上就挂了。
但是无论如何,这都给了我们想象的空间。这种架构下面,利用webrtc做一款区块链应用也是非常容易的。怎么做到呢?我们可以使用electron来做,它既提供了web的能力,又提供了node的能力,因此是非常好的选择。
小结
至此,有关如何利用webrtc这项技术来开发一个应用的知识就介绍完了。这篇文章仅仅介绍了技术层面,如何把基于webrtc的通信搭起来,但是有关webrtc的东西其实还有挺多可以探讨,例如:
- 如何实现视频通话
- 如何创建多人视频会议
- 如何实现文件传输与分享
- 如何实现一个区块链
- 用户认证的细节是什么
另外,也有一些遗留问题有待深入探讨,例如:
- 如何保证安全问题
- 性能如何,最大支持创建多少个peer 实例
- 网络差的情况下,如何保证连接
这些问题都有待你深入了解,如果你对webrtc感兴趣,或者对本文的一些阐述有自己的看法,可以在下方的留言框给我留言,一起探讨。
2018-07-02 4524 WebRTC