webRTC信令服务器的作用

express搭建信令服务器_服务器

信令服务器用于交换三种类型的信息:

会话控制消息:初始化/关闭,各种业务逻辑消息以及错误报告。
网络相关:外部可以识别的IP地址和端口。
媒体能力:客户端能控制的编解码器、分辩率,以及它想与谁通讯。

会话控制消息

        会话控制消息比较简单,像房间的创建与销毁、加入房间、离开房间、开启音频/关闭音频、开启视频/关闭视频等等这些都是会话控制消息。

        对于一个真正商业的WebRTC信令服务器,还有许多的会话控制消息。像获取房间人数、静音/取消静音、切换主讲人、视频轮询、白板中的画笔、各种图型等等。但相对来说都是一引起比较简单的消息。

网络信息消息

        网络信息消息用于两个客户端之间交换网络信息。在WebRTC中使用 ICE 机制建立网络连接。

        在WebRTC的每一端,当创建好 RTCPeerConnection 对象,且调用了setLocalDescription 方法后,就开始收集 ICE候选者 了。

        在WebRTC中有三种类型的 候选者 ,它们分别是:

  • 主机候选者
  • 反射候选者
  • 中继候选者

        主机候选者,表示的是本地局域网内的 IP 地址及端口。它是三个候选者中优先级最高的,也就是说在 WebRTC 底层,首先会偿试本地局域网内建立连接。

        反射候选者,表示的是获取 NAT 内主机的外网IP地址和端口。其优先级低于 主机候选者 。也就是说当WebRTC偿试本地连接不通时,会偿试通过 反射候选者 获得的 IP地址和端口进行连接。

其结构如下图所示:

express搭建信令服务器_webrtc_02

 

        在上面这幅图中可以看到,WebRTC通过 STUN server 获得自己的外网IP和端口,然后通过信令服务器与远端的WebRTC交换网络信息。之后双方就可以偿试建立 P2P 连接了。

以上就是我们通常所说的 P2P NAT 穿越。在WebRTC内部会探测用户的 NAT 类型,最终采用不同的方法进行 NAT 穿越。不过,如果双方都是 对称NAT 类型,是无法进行 P2P NAT 穿越的,此时只能使用中继了。

        中继候选者,表示的是中继服务器的IP地址与端口,即通过服务器中转媒体数据。当WebRTC客户端通信双方无法穿越 P2P NAT 时,为了保证双方可以正常通讯,此时只能通过服务器中转来保证服务质量了。

        所以 中继候选者 的优先级是最低的,只有上述两种候选者都无法进行连接时,才会使用它。

交换媒体能力消息

        在WebRTC中,媒体能力最终通过 SDP 呈现。在传输媒体数据之前,首先要进行媒体能力协商,看双方都支持那些编码方式,支持哪些分辨率等。协商的方法是通过信令服务器交换媒体能力信息。

express搭建信令服务器_express搭建信令服务器_03

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 方法,将其保存起来。

        通过以上步骤就完成了通信双方媒体能力的交换。

        上以就是信令服务器应该处理的所有消息,这些消息组成了信令服务器最基本的信令,每一个都必不可少,否则的话双方就无法进行最终的通信了。

        在WebRTC 通讯时,光有信令是远远不够的。因为 WebRTC真正要传输的是媒体数据,信令只不过是其中的一部分。在WebRTC中他会尽可能的通过P2P进行数据的传输,但在 P2P穿越不成功时怎么办呢?

        那就需要通过媒体中继服务器进行媒体数据的转发,下面我们就来看一下如何搭建媒体中继服务器吧

为什么要使用 socket.io

  • socket.io是websocket超集
  • socket.io有房间的概念
  • socket.io跨平台,跨终端,跨语言

socket.io 工作原理

express搭建信令服务器_express搭建信令服务器_04

Soket.IO发送消息

给本次链接发消息,包括自己

socket.emit()

给某个房间内所有人发消息,io代表房间结点,包括自己

(root).emit()

除本连接外,给某房间内所有人发消息,给某个房间内所有人,不包括自己,如全体静音

(root).emit()

除本连接外,给所有人发消息,一个IO结点会有多个房间,给所有房间的所有人

socket.broadcast.emit()

Socket.IO客户端处理消息

发送action命令

S:socket.emit('action');
C: socket.on('action', function(){...});

发送了一个action命令,还有data数据

S: socket.emit('action', data);
C:socket.on('action', function(data){...});

发送了action命令,还有两个数据

S:socket.emit(action, arg1, arg2);
C: socket.on('action', function(arg1, arg2){...});

发送了一个action命令,在emit方法中包含回调函数

S: socket.emit('action', data, function(arg1,arg2){...});
C: socket.on('action', function(data, fn){fn('a','b');});

安装:npm install     log4js

>npm install socket.io@2.2.0 高版本接口有变化

forever stop server.js

forever start server.js

'use strict'

var http = require('http');
var https = require('https');
var fs = require('fs');

var express = require('express');
var serveIndex = require('serve-index');

//socket.io
var socketIo = require('socket.io');

//
var log4js = require('log4js');

log4js.configure({
    appenders: {
        file: {
            type: 'file',
            filename: 'app.log',
            layout: {
                type: 'pattern',
                pattern: '%r %p - %m',
            }
        }
    },
    categories: {
       default: {
          appenders: ['file'],
          level: 'debug'
       }
    }
});

var logger = log4js.getLogger();

var app = express();
app.use(serveIndex('./public'));
app.use(express.static('./public'));

//http server
var http_server = http.createServer(app);
http_server.listen(80, '0.0.0.0');

var options = {
	key : fs.readFileSync('./cert/1557605_www.learningrtc.cn.key'),
	cert: fs.readFileSync('./cert/1557605_www.learningrtc.cn.pem')
}

//https server
var https_server = https.createServer(options, app);

//bind socket.io with https_server
var io = socketIo.listen(https_server);//
var sockio = socketIo.listen(http_server);

//connection 当客户端连接后触发
io.sockets.on('connection', (socket)=>{

	socket.on('message', (room, data)=>{
		(room).emit('message', room, data)//房间内所有人,除自己外
	});

	//该函数应该加锁 加入房间
	socket.on('join', (room)=> {

		socket.join(room);

        //获取房间
		var myRoom = io.sockets.adapter.rooms[room];
		//获取人数
		var users = Object.keys(myRoom.sockets).length;

		logger.log('the number of user in room is: ' + users);

		//在这里可以控制进入房间的人数,现在一个房间最多 2个人
		//为了便于客户端控制,如果是多人的话,应该将目前房间里
		//人的个数当做数据下发下去。
		if(users < 3) {
			socket.emit('joined', room, socket.id);	
			if (users > 1) {
				(room).emit('otherjoin', room);//除自己之外
			}
		}else {
			socket.leave(room);
			socket.emit('full', room, socket.id);	
		}
	 	//(room).emit('joined', room, socket.id);//除自己之外
		//(room).emit('joined', room, socket.id)//房间内所有人
	 	//socket.broadcast.emit('joined', room, socket.id);//除自己,全部站点	
	});

	socket.on('leave', (room)=> {
		var myRoom = io.sockets.adapter.rooms[room];
		var users = Object.keys(myRoom.sockets).length;
		//users - 1;

		logger.log('the number of user in room is: ' + (users-1));

		socket.leave(room);
		(room).emit('bye', room, socket.id)//房间内所有人,除自己外
	 	socket.emit('leaved', room, socket.id);	
	 	//(room).emit('joined', room, socket.id);//除自己之外
		//(room).emit('joined', room, socket.id)//房间内所有人
	 	//socket.broadcast.emit('joined', room, socket.id);//除自己,全部站点	
	});

});

//connection
sockio.sockets.on('connection', (socket)=>{

	socket.on('message', (room, data)=>{
		sock(room).emit('message', room, socket.id, data)//房间内所有人
	});

	socket.on('join', (room)=> {
		socket.join(room);
		var myRoom = sockio.sockets.adapter.rooms[room];
		var users = Object.keys(myRoom.sockets).length;
		logger.log('the number of user in room is: ' + users);
	 	socket.emit('joined', room, socket.id);	
	 	//(room).emit('joined', room, socket.id);//除自己之外
		//(room).emit('joined', room, socket.id)//房间内所有人
	 	//socket.broadcast.emit('joined', room, socket.id);//除自己,全部站点	
	});

	socket.on('leave', (room)=> {
		var myRoom = sockio.sockets.adapter.rooms[room];
		var users = Object.keys(myRoom.sockets).length;
		//users - 1;

		logger.log('the number of user in room is: ' + (users-1));

		socket.leave(room);
	 	socket.emit('leaved', room, socket.id);	
	 	//(room).emit('joined', room, socket.id);//除自己之外
		//(room).emit('joined', room, socket.id)//房间内所有人
	 	//socket.broadcast.emit('joined', room, socket.id);//除自己,全部站点	
	});
});

https_server.listen(443, '0.0.0.0');

实现聊天室

express搭建信令服务器_数据_05

<html>
	<head>
		<title>Chat Room</title>
		<link rel="stylesheet" href="./css/main.css"></link>
	</head>
	<body>
		<table align="center">
			<tr>
				<td>
					<label>UserName: </label>
					<input type=text id="username"></input>
				</td>
			</tr>
			<tr>
				<td>
					<label>room: </label>
					<input type=text id="room"></input>
					<button id="connect">Conect</button>
					<button id="leave" disabled>Leave</button>
				</td>
			</tr>
			<tr>
				<td>
					<label>Content: </label><br>
					<textarea disabled style="line-height: 1.5;" id="output" rows="10" cols="100"></textarea>
				</td>
			</tr>
			<tr>
				<td>
					<label>Input: </label><br>
					<textarea disabled id="input" rows="3" cols="100"></textarea>
				</td>
			</tr>
			<tr>
				<td>
					<button id="send">Send</button>
				</td>
			</tr>
		</table>

		<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.0.3/socket.io.js"></script>
		<script src="./js/client.js"></script>
	</body>

</html>
'use strict'

//
var userName = document.querySelector('input#username');
var inputRoom = document.querySelector('input#room');
var btnConnect = document.querySelector('button#connect');
var btnLeave = document.querySelector('button#leave');
var outputArea = document.querySelector('textarea#output');
var inputArea = document.querySelector('textarea#input');
var btnSend = document.querySelector('button#send');

var socket;
var room;

btnConnect.onclick = ()=>{

	//connect
	socket = io.connect(); 
	
	//recieve message
	socket.on('joined', (room, id) => {
		btnConnect.disabled = true;
		btnLeave.disabled = false;
		inputArea.disabled = false;
		btnSend.disabled = false;
	});	
	
	socket.on('leaved', (room, id) => {
		btnConnect.disabled = false;
		btnLeave.disabled = true;
		inputArea.disabled = true;
		btnSend.disabled = true;

		socket.disconnect();
	});	

	socket.on('message', (room, id, data) => {
		outputArea.scrollTop = outputArea.scrollHeight;//窗口总是显示最后的内容
		outputArea.value = outputArea.value + data + '\r';
	});	

	socket.on('disconnect', (socket)=>{
		btnConnect.disabled = false;
		btnLeave.disabled = true;
		inputArea.disabled = true;
		btnSend.disabled = true;
	});

	//send message
	room = inputRoom.value;
	socket.emit('join', room);
}

btnSend.onclick = ()=>{
	var data = inputArea.value;
	data = userName.value + ':' + data;
	socket.emit('message', room, data);
	inputArea.value = '';
}

btnLeave.onclick = ()=>{
	room = inputRoom.value;
	socket.emit('leave', room);
}

inputArea.onkeypress = (event)=> {
    //event = event || window.event;
    if (event.keyCode == 13) { //回车发送消息
	var data = inputArea.value;
	data = userName.value + ':' + data;
	socket.emit('message', room, data);
	inputArea.value = '';
	event.preventDefault();//阻止默认行为
    }
}