WebRTC学习总结(2):Nodejs和socket.io搭建信令服务器

信令服务器

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的客户端库,通过调用 io.connect();就与服务端连上了。

需要特别强调的是 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代码,这个客户端页面非常简单,就只有两个视频框用来显示本地和远端的媒体流。注意这里在

可以看到,客户端通过io.connect()建立与服务端的连接,通过socket.emit()向服务器发送消息,通过socket.on()接受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
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端口,并通过socket.on()接受消息并做相应的处理。

服务器端引入了node-static库,使服务器具有发布静态文件的功能。服务器具有此功能后,当客户端(浏览器)向服务端发起请求时,
服务器通过该模块获得客户端(浏览器)运行的代码,也就是上我面我们讲到的 index.html 和 main.js 并下发给客户端(浏览器)。

通过上面的步骤,我们已经构建好一个服务器,可以通过下面node server.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 时,为了保证双方可以正常通讯,此时只能通过服务器中转来保证服务质量了。
    所以 中继候选者的优先级是最低的,只有上述两种候选者都无法进行连接时,才会使用它。

刚才搭建好的服务器端,通过socket.on()收到message消息后,不做任何处理,将其广播发给其他客户端。

客户端接收到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的时候详细讲。