theme: channing-cyan
highlight: vs2015
相关简介
信令:驱动系统运转。控制各个模块的前后调用关系;业务不同,逻辑不同,信令也会千差万别
要实现
一对一通信
,驱动系统的核心就是信令。信令控制着系统各个模块之间的前后调用关系
,比如当收到用户成功加入房间后,系统需要立即将RTCPeerConnention
对象创建好,以便向STUN/TURN服务器
请求其
外网的IP地址和端口;而当收到另一个用户加入房间的消息时,系统需要将自己的外网IP地址和端口交换给对方
,从而建立起Socket连接;
WebRTC使用信令服务器交换
媒体信息
和网络候选者
信息,信令服务器承担着消息传输和交换的工作,WebRTC规定了信令服务器的实现方式:任何能够进行网络信息交换的技术都可以用来实现信令服务器
,如HTTP、XMPP及WebSocket
等
信令服务器主要作用
- 实现
业务层
管理- 如用户创建房间,加入房间,退出房间等
- 确定何时初始化、关闭和修改通话会议,也可以进行错误报告
- 让通信双方彼此
交换网络
信息- 最常见的是交换通信双方的
IP地址和端口 - ICE Candidate
- 两个WebRTC之间会尽可能选择
P2P
进行传输,同一个局域网
内直接通过P2P进行传输,不同局域网
内需要使用P2P穿越
后进行数据传输,P2P穿越成功后直接传输,失败后进行中转
等 – 后续的候选人中进行解说
- 最常见的是交换通信双方的
- 通信双方交换
媒体信息
- 媒体信息用
SDP
来表示,这个SDP可以简单理解为:媒体类型的编码器是什么、是否支持该媒体类型和对应的编码器、编码方式是什么等
- 媒体信息用
最简单的信令系统的分类
- 客户端发送给服务器的信令
- 服务端发送给客户端的信令
常见的信令系统
- 客户端
- 用户加入房间 – join
- 用户离开房间 – leave
- 端到端的命令 – message
- 服务端
- 用户已加入 – joined
- 用户已离开 – left
- 其他用户已加入 – other_joined
- 其他用户已离开 – bye
- 房间已满 – full
信令传输协议的选择
一般选择
TCP
或者基于TCP
的HTTP/HTTPS、WS/WSS等协议作为信令服务器的传输协议;
- 原因一:TCP是
可靠的传输协议
,可以保证传输的数据可靠、有序到达 - 原因二:TCP上传输数据是
流式的
,不必担心传输的数据过大导致的拆包传输
的问题
信令服务器的常见特点
- 可以同时支撑
多个WebRTC通话环境
,即多个房间
,且房间之间互不影响 - 每个房间的参与人数不受限制
- 实时性好,不可有明显的延时
- 支持可靠的信令传输,发送者要知道明确的发送反馈,即使发送失败了
- 性能好、可拓展性要好,要兼顾后续的拓展功能如传输应用数据等
信令时序
在发送信令前,各端需要先与信令服务器SigServer建立连接,
建立连接
后终端会向信令服务器发送join消息
,服务器收到该消息后会返回joined消息
(信令服务器在收到该信令后,会先将该用户加入服务器管理的房间,然后向客户端返回joined信令),标识已加入房间,同理其他用户加入也是类似只是会收到otherjoin
的消息;
房间:房间服务器将多端聚集到一起管理;
WebRTC信令如何工作
- A和B通过WebSocket连接或顺序HTTP请求连接到WebRTC信令服务器;
- A与WebRTC信令服务器之间的通信称为
offer-answer
机制,是WebRTC的一部分 - 对等点A和对等点B之间的连接是为了在设备之间直接发送媒体而建立的。要到达哪里,对等点必须首先通过WebRTC信号服务器进行通信
WebRTC一对一架构
WebRTC 组成部分
分别是两个WebRTC终端、一个信令服务器、一台中继服务器(STUN/TURN)和两个NAT,这是最经济的一对一通信架构。其中信令服务器与中继服务器都在
NAT外
,也就是属于外网
。而两个WebRTC终端在NAT内
,属于内网
;
终端知道对方的IP地址后通过NAT穿越进行P2P连接和传输
WebRTC通信步骤
- 两个终端通信前,都要先与信令服务器连接;
- 交换信息前,WebRTC终端还要与STUN/TURN服务器建立连接,这样做的目的是通过STUN/TURN服务器
获得各自的
外网IP地址(子网和公网)、端口和NAT结构(IP地址和端口对我们称之为ICE Candidate) - 连接后通信双方就可以通过信令服务器
彼此交换信息
了,如获取到对方的
外网IP地址和端口等信息; - 当获得彼此信息后,就可以尝试NAT穿越,进行P2P连接了
WebRTC架构细化
在Call端内部,首先调用音视频设备检测模块检测终端是否有可用的音视频设备,即步骤❶;然后执行第❷步,调用音视频采集模块从设备中采集音视频数据;采集到数据后,执行第❸步开启客户端录制(是否开启录制是可选的,用户可以根据自己的需求选择录制或不录制);当数据采集相关的工作就绪后,执行第❹步,通过信令模块与信令服务器建立连接;紧接着执行第❺步,创建RTCPeerConnection对象(RTCPeerConnection对象是WebRTC最核心的对象,后面音视频数据的传输都靠它来完成)。RTCPeerConnection创建好后,系统要先将它与之前采集的音视频数据绑定到一起,这样RTCPeerConnection才知道从哪里获取要发送的数据。以上就是图4.2中前五步所完成的工作。
接下来再来看一下RTCPeerConnection创建socket连接的过程。要建立socket连接,RTCPeerConnection首先要执行图4.2中的第❻步,向STUN/TRUN服务器发送请求。STUN/TURN服务器收到Call的请求后,会将Call的外网IP地址和端口号作为应答消息返回去;之后终端执行第❼步,通过信令服务器将Call的连接地址发送给对端。同理,Called也会将它的IP地址和端口发给Call。当通信双方都获得对端的地址后,执行第❽步,此时socket连接就被建立起来了。至此,RTCPeerConnection就可以将音视频数据源源不断地发送给对端。以上就是WebRTC一对一通信的完整过程。
连接双方通过第三方服务器来交换各自的ICE Candidate,如果连接双方在同一个NAT下那么他们仅可以通过内网Candidate就能建立连接,反之就需要STUN Server识别出的公网Candidate进行通信;
当公网Candidate都无法建立连接时,就说明有一方处于对称NAT下,这就需要处于对称NAT下的客户端去寻找TURN Server提供的转发服务,然后将转发形成的Candidate共享(Signalling)给对方;
总结:同一个NAT
→非对称NAT
→对称NAT
→服务中转共享Candidate
RTCPeerConnection
RTCPeerConnection对象
是WebRTC的核心,同时也是暴露给用户的统一接口,内部包含了网络处理模块
、服务质量模块
、音视频引擎模块
等,可以将其理解成一个Socket
,可以快速稳定的实现端到端的数据传输
,创建RTCPeerConnection
需要传入STUN/TURN服务器等相关信息
WebRTC信令机制
WebRTC信令使用称为
ICE的协议
,该协议收集、交换,然后尝试使用ICE候选者
连接会话,ICE候选者是可以让对等点相互连接的潜在地址
。
ICE使得数据包到达目标对等方的三种方法
通过三种方法,ICE可以计算出最快、最简单的NAT穿越路由,以便使数据包到达其目标对等方
- 方法一:UDP连接
- 使用从设备操作系统和网卡获取的IP地址建立UDP连接,这将不可避免地在NAT后面的设备上失败 – 忽略该方法
- 方法二:STUN服务器
- STUN服务器是WebRTC信令中最常用的方法。
- STUN服务器检查传入请求的IP地址和端口,然后将该地址发送给对等方作为响应。
- 这允许应用程序提供一个可公开访问的地址,然后通过信令机制将其发送给另一个WebRTC对等点
- 方法三:TURN中继服务器
- TURN服务器用于在对等点之间传输音视频和其他实时数据,由于它支持对等方之间的实时数据交换,因此不共享信号信息;
- TURN服务器具有公共地址,因此对等方可以连接到他们,即使他们位于NAT和防火墙之后
构建信令服务器
信令服务器的实现方案
原生方案
使用原生C/C++、Java等语言从零开发一个信令服务器,这种方案的实现成本非常高
现成的技术
利用现成的Web服务器做应用开发,如以Apache、Nginx、NodeJS等为服务,在其上做应用开发是不错的选择
优势
- 一般信令系统都需要使用HTTP/HTTPS、WS/WSS等基于TCP的传输协议,而Apache、Ng inx、NodeJS等服务器显然在这方面有天然的优势
- 实时通信的信令服务器一般负荷不会很高,这个量级对于Node和Nginx来说,单台服务器就可以了,再者通过Nginx和Node实现起来也相对容易些,且稳定性也挺高
信令服务器的业务逻辑
最重要的就是
房间
的概念,只有成功创建和加入了房间后才可以进行数据交换
,再者当通信的双方结束通话后,用户需要发送离开房间的消息
给信令服务器,此时信令服务器需要将房间内的所有人清除
,当房间内没有人了时,还需要将房间进行销毁
;
信令服务器的实现
WebRTC的服务器
- room应用服务器
- 信令服务器
- WebRTC依靠信令服务器来
建立对等点
之间的连接,而信令是计算机如何利用WebRTC发现其他计算机
来连接的; - 信令服务器本身不直接处理数据流,因为这是在对等点之间完成的,WebRTC信令服务器仅处理有关客户端的
元数据或数据
- 一旦每个客户端都获得了所有其他连接客户端的SDP提供,他们就开始点对点连接,而无需使用任何服务器
- 处理信息如下:
- 客户端配置:客户端
窗口分辨率
、Web APP如何连接到该用户的媒体设备
、客户端需要用哪些编解码器
进行媒体播放和传输 - 网络信息:网络地址和端口等信息
- 在WebRTC的每一端,当创建好 RTCPeerConnection 对象,且调用了setLocalDescription 方法后,就开始收集 ICE候选者了
- ICE候选者 – 主机候选者:表示的是本地局域网内的IP地址和端口,其
优先级是最高
的 – 在WebRTC中会首先尝试本地局域网内建立连接 - ICE候选者 – 反射候选者:表示的是获取NAT内主机的外网IP地址和端口。
优先级第二
, – 当本地局域网内连接不通时
会尝试通过反射候选者获得的IP地址和端口进行连接;WebRTC内部会探测用户的NAT类型,最终采用不同的方法进行NAT穿越
- ICE候选者 – 中继候选者:表示的是
中继服务器
上的IP地址与端口,即通过服务器中转媒体数据
,当通信双方无法穿越P2P NAT时,为了保证双方正常通信,只能采取此方法来保证质量了(对称NAT类型的通信双方是无法进行P2P穿越的)
- 会话信息:会话何时终止、如何终止、客户端之间允许流式传输什么样的数据、会话控制消息等简单的消息
- 客户端配置:客户端
- WebRTC依靠信令服务器来
- 流媒体中转服务器
- 媒体服务器的最大特点是不仅在于它可以
向N人发送广播
,而且媒体服务器处理转码和编码
,甚至将WebRTC流重新打包
到其他协议,进行缩放
,甚至添加自适应流功能让观众开心,媒体服务器也可以充当WebRTC信令服务器
,如Ant Media Server
- 媒体服务器的最大特点是不仅在于它可以
- NAP穿越服务器
- socket.io本身就有房间的概念,也具有信令转发的功能,因此可以用socket.io代替WebRTC中的room服务器和信令服务器
存在的问题及解决办法
- 如何创建一个HTTP服务
- 通过Node的
HTTP/HTTPS库
和express库进行实现
- 通过Node的
- 如何使用socket.io库
- 在Node中通过requier进行引入,然后利用
socket.io
的API进行交互实现如on
方法接收消息,emit
发送消息,具体API参考官网
socket.io浅析
- 在Node中通过requier进行引入,然后利用
- 如何进行信令转发
- 需要根据收到的不同的信令,返回不同的结果
io.sockets.on("connection", (socket) => { //收到message时,直接进行转发 socket.on("message", (message) => { //给另一端转发消息 socket.to(room).emit("message", message); }); //收到 join 消息 socket.on("join", (room) => { // 首先将用户加入服务端管理的房间中,之后向客户端返回joined消息 var o = io.sockets.adapter.rooms[room]; //得到房间里的人数 var nc = o ? Object.keys(o.sockets).length : 0; if (nc < 2) { //如 果 房 间中 没 有 超 过 2 人 socket.join(room); //发 送 joined消 息 socket.emit("joined", room); // ... } else { // max two clients socket.emit("full", room); //发 送 full 消 息 } }); // ... });
"use strict "
var log4js = require("log4js "); // 用于输出日志
var http = require("http"); // 提供HTTP 服务
var https = require("https "); // 提供HTTPS 服务
var fs = require("fs"); // 用于读取文件内容
var socketIo = require("socket.io");
var express = require("express ");
var serveIndex = require("serve -index ");
// 一个房间里可以同时在线的最大用户数
var USERCOUNT = 3;
// 日志的配置项
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 "));
// 设置跨域访问
app.all("*", function (req, res, next) {
// 设置允许跨域的域名, * 代表允许任意域名跨域
res.header("Access -Control -Allow -Origin", "*");
// 允许的header 类型
res.header("Access -Control -Allow -Headers", "content -type");
// 跨域允许的请求方式
res.header(
"Access -Control -Allow -Methods",
"DELETE ,PUT ,POST ,GET ,OPTIONS"
);
if (req.method.toLowerCase() == "options ") {
res.send(200); // 让options 尝试请求快速结束
} else {
next();
}
});
//HTTP 服务
var http_server = http.createServer(app);
http_server.listen(80, "0.0.0.0 ");
// 你的网站证书
var options = {
key: fs.readFileSync("./cert/cert.key"),
cert: fs.readFileSync("./cert/cert.pem"),
};
// HTTPS 服务
var https_server = https.createServer(options, app);
var io = socketIo.listen(https_server);
// 处理连接事件
io.sockets.on("connection ", (socket) => {
// 中转消息
socket.on("message ", (room, data) => {
logger.debug("message , room: " + room + ", data , type:" + data.type);
socket.to(room).emit("message ", room, data);
});
// 用户加入房间
socket.on("join", (room) => {
socket.join(room);
var myRoom = io.sockets.adapter.rooms[room];
var users = myRoom ? Object.keys(myRoom.sockets).length : 0;
logger.debug("the user number of room (" + room + ") is: " + users);
// 如果房间里人未满
if (users < USERCOUNT) {
// 发给除自己之外的房间内的所有人
socket.emit("joined ", room, socket.id);
// 通知另一个用户, 有人来了
if (users > 1) {
socket.to(room).emit("otherjoin ", room, socket.id);
}
} else {
// 如果房间里人满了
socket.leave(room);
socket.emit("full", room, socket.id);
}
});
// 用户离开房间
socket.on("leave ", (room) => {
// 从管理列表中将用户删除
socket.leave(room);
var myRoom = io.sockets.adapter.rooms[room];
var users = myRoom ? Object.keys(myRoom.sockets).length : 0;
logger.debug("the user number of room is: " + users);
// 通知其他用户有人离开了
socket.to(room).emit("bye", room, socket.id);
// 通知用户服务器已处理
socket.emit("leaved ", room, socket.id);
});
});
https_server.listen(443, "0.0.0.0 ");
暂无评论内容