信令服务器
webRTC采用的是“端对端”对等连接,在信息通路形成之后,可以没有服务器参与,但是信息通路的搭建不能没有信令服务器。
信令服务器主要用于交换以下信息:
- 会话控制信息:比如加入房间,离开房间,禁言,错误等信息。
- 媒体信息:中转通过SDP来表示的offer,answer信息,如如各自的音视频解码方式,带宽等。
- 网络信息:通过信令服务器“发现”参与P2P连接的两个webRTC客户端。首先将由一端将网络相关信息传到信令服务器,服务器帮它交换到对端,对端拿到你的信息后,若在同一局域网内,直接通过P2P传输;若不在,首先进行P2P穿越,看是否能打通,打通则传输,打不通则中转等。

Nodejs和socket.io
其实,在服务器的选择方面,有许多成熟好用的web服务器可以使用。Nodejs 现在是非常流行的Web服务器,选择Nodejs的主要原因是安装快捷,同时可以使用JS语言开发服务器程序,非常适合新手。
socket.io是一个基于Nodejs的库,在现有的Node Server上引入socket.io即可使用。socket.io特别适合用来开发WebRTC的信令服务器,通过它来构建信令服务器特别的简单,这主要是因为它内置了房间 的概念。

上图是socket.io与Nodejs配合使用的逻辑关系图。可以看到,socket.io分为服务端和客户端两部分。
服务端由Nodejs设置侦听某个服务端口。客户端要想与服务端相连时,首先要加载socket.io的客户端库,通过调用
需要特别强调的是 socket.io 消息的发送与接收。socket.io 有很多种发送消息的方式,其中最常见的有下面几种:
-
给本次连接发消息:
socket.emit() -
给某个房间内所有人发消息:
io.in(room).emit() -
除本连接外,给某个房间内所有人发消息:
socket.to(room).emit() -
除本连接外,给所有人发消息:
socket.broadcast.emit()
消息的接受也非常简单:
- 发送 command 命令:
1 2 | 发送端: socket.emit('cmd’); 接收端: socket.on('cmd',function(){...}); |
- 发送一个 command 命令,带 data 数据
1 2 | 发送端: socket.emit('action', data); 接收端: socket.on('action',function(data){...}); |
- 发送了command命令,还有两个数据
1 2 | 发送端: socket.emit(action,arg1,arg2); 接收端: socket.on('action',function(arg1,arg2){...}); |
信令服务器的搭建
接下来我们来看一下,通过 Nodejs + socket.io 构建的一个服务器。
首先是客户端的HTML代码,这个客户端页面非常简单,就只有两个视频框用来显示本地和远端的媒体流。注意这里在
其中,socket.io.js 是用来与服务端建立 socket 连接的,adapter-latest.js是用于解决webRTC中不同浏览器之间的适配的问题的,main.js是自己写的一些业务逻辑,可以通过socket与服务端通讯。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | <!-- index.html --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Realtime communication with WebRTC</title> <link rel="stylesheet" href="css/main.css"/> </head> <body> <h1>Realtime communication</h1> <div id="videos"> <video id="localVideo" autoplay muted playsinline></video> <video id="remoteVideo" autoplay playsinline></video> </div> <!-- This file is automatically added/served when running "node index.js". --> <script src="/socket.io/socket.io.js"></script> <script src="//i2.wp.com/webrtc.github.io/adapter/adapter-latest.js"></script> <script src="js/main.js"></script> </body> </html> |
接下来是main.js的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | //客户端的代码 main.js var isInitiator = false; var room = 'foo'; //为了方便直接指定一个房间名称 // 也可以提示用户输入房间名称 // room = prompt('Enter room name:'); var socket = io.connect(); //与服务端建立socket连接 if (room !== '') { //如果房间不空,则发送 "create or join" 消息 socket.emit('create or join', room); console.log('Attempted to create or join room', room); } socket.on('created', function (room) { console.log('Created room ' + room); isInitiator = true; }); socket.on('full', function (room) { console.log('Room ' + room + ' is full'); }); socket.on('join', function (room) { console.log('Another peer made a request to join room ' + room); console.log('This peer is the initiator of room ' + room + '!'); isChannelReady = true; }); socket.on('joined', function (room) { console.log('joined: ' + room); isChannelReady = true; }); socket.on('log', function (array) { console.log.apply(console, array); }); |
可以看到,客户端通过
我们再来看看服务器端的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 | //服务器端的代码 index.js 'use strict'; var os = require('os'); var nodeStatic = require('node-static'); var http = require('http'); var socketIO = require('socket.io');//引入socket.io var fileServer = new(nodeStatic.Server)(); var app = http.createServer(function(req, res) { fileServer.serve(req, res); }).listen(8080); var io = socketIO.listen(app); //将socket.io注入express模块 //每个客户端socket连接时都会出发connection事件 io.sockets.on('connection', function(socket) { //log函数用于向客户端发送消息 function log() { var array = ['Message from server:']; array.push.apply(array, arguments); socket.emit('log', array); } //收到message时,进行广播 socket.on('message', function(message) { log('Client said: ', message); //在真实的应用中,应该只在房间内广播 socket.broadcast.emit('message', message); }); //每次访问网站,server都会收到‘create or join'消息 socket.on('create or join', function(room) { log('Received request to create or join room ' + room); var clientsInRoom = io.sockets.adapter.rooms[room]; //获得房间里的人数 var numClients = clientsInRoom ? Object.keys(clientsInRoom.sockets).length : 0; log('Room ' + room + ' now has ' + numClients + ' client(s)'); if (numClients === 0) { //如果房间里没人,发送‘created’消息 socket.join(room); log('Client ID ' + socket.id + ' created room ' + room); socket.emit('created', room, socket.id); } else if (numClients === 1) { //如果房间里有一个人,发送‘joined’消息 log('Client ID ' + socket.id + ' joined room ' + room); io.sockets.in(room).emit('join', room); socket.join(room); socket.emit('joined', room, socket.id); io.sockets.in(room).emit('ready'); } else { //房间最懂容纳两人,更多人加入发送'full'消息 socket.emit('full', room); } }); socket.on('ipaddr', function() { var ifaces = os.networkInterfaces(); for (var dev in ifaces) { ifaces[dev].forEach(function(details) { if (details.family === 'IPv4' && details.address !== '10.126.80.52') { socket.emit('ipaddr', details.address); } }); } }); socket.on('bye', function(){ console.log('received bye'); }); }); |
服务器监听8080端口,并通过
服务器端引入了
服务器通过该模块获得客户端(浏览器)运行的代码,也就是上我面我们讲到的 index.html 和 main.js 并下发给客户端(浏览器)。
通过上面的步骤,我们已经构建好一个服务器,可以通过下面
网络信息的交换
网络信息消息用于两个客户端之间交换网络信息。在WebRTC中使用ICE机制建立网络连接。ICE全称Interactive Connectivity Establishment,即交互式连通建立方式。
ICE参照RFC5245建议实现,是一组基于offer/answer模式解决NAT穿越的协议集合,它可以综合利用现有的STUN,TURN等协议,以更有效的方式来建立会话。
在WebRTC的每一端,当创建好 RTCPeerConnection 对象,且调用了setLocalDescription方法后,就开始收集ICE候选者(iceCandidate)了,
IceCandidate是一个模板类,里面主要包含着会话描述协议。
webRTC中有三种类型的候选者,分别是:
- 主机候选者:表示的是本地局域网内的 IP 地址及端口。它是三个候选者中优先级最高的,也就是说在 WebRTC 底层,首先会偿试本地局域网内建立连接。
- 反射候选者:表示的是获取 NAT 内主机的外网IP地址和端口。其优先级低于 主机候选者。也就是说当WebRTC偿试本地连接不通时,会偿试通过反射候选者获得的 IP地址和端口进行连接。
- 中继候选者:表示的是中继服务器的IP地址与端口,即通过服务器中转媒体数据。
当WebRTC客户端通信双方无法穿越 P2P NAT 时,为了保证双方可以正常通讯,此时只能通过服务器中转来保证服务质量了。
所以 中继候选者的优先级是最低的,只有上述两种候选者都无法进行连接时,才会使用它。
刚才搭建好的服务器端,通过
客户端接收到message之后,会做进一步判断,如果消息类型为 candidate,即网络消息信令时,会生成 RTCIceCandidate 对象,并将其添加到 RTCPeerConnection 对象中,从而使 WebRTC 在底层自动建立连接。
媒体信息的交换
在WebRTC中,媒体能力最终通过 SDP 呈现。在传输媒体数据之前,首先要进行媒体能力协商,看双方都支持那些编码方式,支持哪些分辨率等。协商的方法是通过信令服务器交换媒体能力信息。
WebRTC 媒体协商的过种如图所示。

主要步骤是:
- 第一步,Amy 调用 createOffer 方法创建 offer 消息。offer 消息中的内容是 Amy 的 SDP 信息。
- 第二步,Amy 调用 setLocalDescription 方法,将本端的 SDP 信息保存起来。
- 第三步,Amy 将 offer 消息通过信令服务器传给 Bob。
- 第四步,Bob 收到 offer 消息后,调用 setRemoteDescription 方法将其存储起来。
- 第五步,Bob 调用 createAnswer 方法创建 answer 消息, 同样,answer 消息中的内容是 Bob 的 SDP 信息。
- 第六步,Bob 调用 setLocalDescription 方法,将本端的 SDP 信息保存起来。
- 第七步,Bob 将 anwser 消息通过信令服务器传给 Amy。
- 第八步,Amy 收到 anwser 消息后,调用 setRemoteDescription 方法,将其保存起来。
通过以上步骤就完成了通信双方媒体能力的交换。
后面两个部分的代码,会在建立PeerConnection的时候详细讲。