什么是WebRtc
webrtc本质上提供了一个点对点的连接,这里的点对点是指客户端与客户端之间直接进行数据交换而非经由服务器转发。详情如下图
传统模式下,用户1向用户2发送"你好",需要用户1将数据包发送至服务器,经由服务器转发至用户2。webrtc使用户1与用户2之间通信不经过服务器直接传递。
NAT网关
本部分介绍NAT网关相关信息,了解的可跳过
书接上文,为什么用户之间通信要经过服务器呢?
众所周知,网络中个人的唯一标识是ip和端口,例如我要登陆qq就需要知道qq服务器的ip和端口才能发送数据包进行登陆。那么,如果A知道B的ip和端口是不是也可以向B直接发送消息呢? 答案是肯定的,通常来说不可行。这就要提到NAT网关。
如果我向我的怨种朋友通过qq发送消息,消息在网络中的路线简化如下
数据包经过路由器发送到网关,这里网关可以理解为电信、联通、移动三大运营商。经过网关封装后发送给服务器,服务器转发到对方的网关由网关逐层转发。路由器在转发的时候,数据包的包头ip,port字段会被覆盖为路由器自己的ip,port,网关同理。因此,对于外网来说,他们看到的我的ip和port其实是网关给我分配的ip和port。ip可以很方便的知道,去百度搜索ip就可以知道自己的网关ip是多少。但是用户并不清楚本次连接被分配给了网关的那个端口转发,因此对于唯一标识ip和端口来说,端口获取不到,也就无法进行点对点的通信。
但是问题来了,为什么我想我的怨种朋友发消息,qq服务器能够知道转发给哪个网关的哪个端口就可以发送给我的怨种朋友呢?
回顾一下数据包的发送流程,不难发现,除了网关知道本次连接的ip,port之外,服务器也知道本次连接的ip,port。
那么有没有一个“好人”服务器,我向服务器发送消息,服务器返回给我本次连接的ip,port,在知道ip,port之后我与我的怨种朋友通过任意方式交换ip,port不就可以不经过服务器,点对点的发送消息了么。这个“好人”服务器就是stun服务器,这个过程被称作内网穿透,也叫打洞。
网关类型
NAT网关在分配端口的时候有不同的类型:
条件:
我从本机9001端口向服务器A_1:2.2.2.2,500端口发送消息,网关给本次连接分配的端口为300
(注意这里是网关的端口)。此时有服务器A_2:2.2.2.2,400端口和服务器B:3.3.3.3,500端口。
在本机向服务器A_1发送消息后,A_1, A_2, B三台服务器向均向网关的300端口发送一条消息。
1. 全锥型(Full Cone):
网关会将三台服务器发送的所有消息转发给本机,也就是没有限制。
2. IP受限
网关只会转发服务器A_1, A_2的消息,服务器B的消息则会被丢弃,不予转发。
3. 端口受限
网关只转发服务器A_1的消息。
4. 对等连接
无法进行内网穿透
对于ip受限和端口受限型如何进行内网穿透呢?注意前提条件,本机向服务器A_1发送消息后,两种类型均可收到A_1发送的消息。因此,只需要向目标发送一条消息,网关就会转发目标向本机发送的消息而不会丢弃。
WebRtc介绍及用法
对于webrtc来说,一条连接分为两个角色,连接发起方offer,以及连接接收方answer。
var config = {
// bundlePolicy: 'max-bundle',
// bundlePolicy: 'balanced',
iceTransportPolicy: 'all',
// rtcpMuxPolicy: 'require',
// iceCandidatePoolSize: 1,
iceServers: [
{
urls: 'stun:stun.intervoip.com:3478'
// urls: 'stun:stun.l.google.com:19302'
}
]
}
var pc = new RTCPeerConnection(config)
peerconnection作用相当于socket,通过config进行创建,config中iceServers就是用来打洞的stun服务器,具体服务器url百度有好多。
下面是最关键的,setLocalDescription和setRemoteDescription
pc.setLocalDescription(desc)
pc.setRemoteDescription(remoteDescription)
这里对于两个角色来说略有不同,对于offer来说:
const offerOptions = {offerToReceiveAudio: 1}
var desc = await pc.createOffer(offerOptions)
pc.setLocalDescription(desc)
pc.setRemoteDescription(对方answer)
对于answer:
const answerOption = {
offerToReceiveAudio: true,
offerToReceiveVideo: true,
}
var desc = await pc.createAnswer(answerOption)
pc.setLocalDescription(desc)
pc.setRemoteDescription(对方的offer)
可以看到对于offer来说本地设置为offer,远程设置为answer。对于answer来说本地设置为answer,远程设置为offer。
连接
在设置完本地角色setLocalDescription后,webrtc会自动向stun服务器发送消息,stun服务器将返回连接的ip和port,此时触发ononicecandidate,webrtc将其封装成candidate
pc.onicecandidate = async (ev) => {
if(ev.candidate){
console.log('------------------------------')
console.log('address: ' + ev.candidate.address)
console.log('port: ' + ev.candidate.port)
console.log('protocol: ' + ev.candidate.protocol)
console.log('type: ' + ev.candidate.type)
console.log('usernameFragment: ' + ev.candidate.usernameFragment)
// offerPeer.addIceCandidate(ev.candidate)
console.log('------------------------------')
}
}
pc.addIceCandidate
通过addIceCandidate添加对方candidate正式连接。
数据传输
webrtc数据有三种,audiostream,videostream,channel。前两个顾名思义,音视频流,channel可以理解为传统封包形式。demo中使用的channel形式。双方共用channel,由offer创建channel
var channel = new Channel(param)
channel.onopen = (ev)=>{
console.log('onopen: ' + JSON.stringify(ev))
}
channel.onmessage = (ev)=>{
var data = JSON.parse(ev.data)
this.deal(data)
}
channel.onclose = (ev)=>{
console.log('onclose: ' + JSON.stringify(ev))
}
channel.onerror = (ev)=>{
console.log('onerror: ' + JSON.stringify(ev))
}
双方互相addIceCandidate后,answer会触发onDataChannel回调
pc.ondatachannel = (ev) => {
channelFunc(connectId, ev.channel)
pc.channel = channel
pc.channel.send({type: 'hand'})
// 连接成功
}
ev.channel就是对方的channel,answer将对方的channel保存到本地,也就是双方使用“同一个对象”进行数据传输。
channel.send()