写点什么

基于 WebRTC 的 1 对 1 通话实战 (二) 信令服务器实现

用户头像
IT酷盖
关注
发布于: 4 小时前
基于 WebRTC 的1 对 1 通话实战(二)信令服务器实现

一、信令协议封装

1对1音视频通话流程图


本篇文章我们主要讲解上图中 Signal Server(信令服务器)的实现。在讲解信令服务器的具体实现前我们先来了解下信令协议该如何设计。如果对上图不理解的请先阅读WebRTC 基础知识详解


信令服务器中的信令协议使用 JSON 格式来封装(JSON 作为一种轻量级的数据交换格式,易于人们阅读与书写)。现在我们来对上图中蓝青色背景部分相关的信令做 json 格式封装。如下


join:加入房间


var jsonMsg = {'cmd': 'join','roomId': roomId,'uid': localUserId,};
复制代码


resp-­join:当加入房间后发现房间已经有人则返回此人的 uid,只有自己时不返回


jsonMsg = {'cmd': 'resp‐join','remoteUid': remoteUid};
复制代码


leave:离开房间,服务器收到 leave 信令则检查同一房间是否有其他人,如果有则通知他有人离开了


var jsonMsg = {'cmd': 'leave','roomId': roomId,'uid': localUserId,};
复制代码


new-­peer:服务器通知客户端有新人加入,当客户端收到 new-­peer 消息则发起连接请求


var jsonMsg = {'cmd': 'new‐peer','remoteUid': uid};
复制代码


peer-­leave:服务器通知客户端有人离开


var jsonMsg = {'cmd': 'peer‐leave','remoteUid': uid};
复制代码


offer:转发 offer sdp


var jsonMsg = {'cmd': 'offer','roomId': roomId,'uid': localUserId,'remoteUid':remoteUserId,'msg': JSON.stringify(sessionDescription)};
复制代码


answer:转发 answer sdp


var jsonMsg = {'cmd': 'answer','roomId': roomId,'uid': localUserId,'remoteUid':remoteUserId,'msg': JSON.stringify(sessionDescription)};
复制代码


candidate:转发 candidate sdp


var jsonMsg = {'cmd': 'candidate','roomId': roomId,'uid': localUserId,'remoteUid':remoteUserId,'msg': JSON.stringify(candidateJson)};
复制代码


信令协议封装完成后我们就可以参考上边封装的协议一步步实现信令服务器。

二、信令服务器实现


信令服务器使用 nodejs-websocket 来实现。


首先创建 signal_server.js 文件,并在文件开头引入 nodejs-websocket 包


var ws = require("nodejs-websocket")
复制代码


然后我们将信令中 cmd 字段值定义为常量,方便后续使用


const SIGNAL_TYPE_JOIN = "join";// 主动加入房间const SIGNAL_TYPE_RESP_JOIN = "resp-join";// 告知加入者房间中是谁const SIGNAL_TYPE_LEAVE = "leave";// 主动离开房间const SIGNAL_TYPE_NEW_PEER = "new-peer";// 有人加入房间,通知已经在房间的人const SIGNAL_TYPE_PEER_LEAVE = "peer-leave";// 有人离开房间,通知已经在房间的人const SIGNAL_TYPE_OFFER = "offer";// 发送offer给对端peer(发起方)const SIGNAL_TYPE_ANSWER = "answer";//发送answer给对端peer(接收方)const SIGNAL_TYPE_CANDIDATE = "candidate";// 发送candidate给对端peer
复制代码


信令服务器中的房间管理使用 Map,封装房间管理类如下(主要是为了方便房间创建、获取、删除等操作)


var RTCMap = function () {    this._entrys = new Array();
this.put = function (key, value) { if (key == null || key == undefined) { return; } var index = this._getIndex(key); if (index == -1) { var entry = new Object(); entry.key = key; entry.value = value; this._entrys[this._entrys.length] = entry; } else { this._entrys[index].value = value; } }; this.get = function (key) { var index = this._getIndex(key); return (index != -1) ? this._entrys[index].value : null; }; this.remove = function (key) { var index = this._getIndex(key); if (index != -1) { this._entrys.splice(index, 1); } }; this.clear = function () { this._entrys.length = 0; }; this.contains = function (key) { var index = this._getIndex(key); return (index != -1) ? true : false; }; this.size = function () { return this._entrys.length; }; this.getEntrys = function () { return this._entrys; }; this._getIndex = function (key) { if (key == null || key == undefined) { return -1; } var _length = this._entrys.length; for (var i = 0; i < _length; i++) { var entry = this._entrys[i]; if (entry == null || entry == undefined) { continue; } if (entry.key === key) { return i; } } return -1; };}
复制代码


我们还需要封装一个客户端方法,来作为上边房间管理类 map 的值


function Client(uid, conn, roomId) {    this.uid = uid;     // 用户uid    this.conn = conn;   // uid对应的websocket连接    this.roomId = roomId;// 用户所在房间id}
复制代码


接下来创建一个 websocket 服务(监听端口根据自己需求与客户端保持一致即可),在服务中监听客户端信令消息并处理。


var server = ws.createServer(function(conn){    console.log("a new connection was created ~ ")    conn.client = null; // client     conn.on("text", function(str) {             var jsonMsg = JSON.parse(str);        switch (jsonMsg.cmd) {            case SIGNAL_TYPE_JOIN:                conn.client = handleJoin(jsonMsg, conn);            break;            case SIGNAL_TYPE_LEAVE:                handleLeave(jsonMsg);                break;            case SIGNAL_TYPE_OFFER:                handleOffer(jsonMsg);                break;               case SIGNAL_TYPE_ANSWER:                handleAnswer(jsonMsg);                break;             case SIGNAL_TYPE_CANDIDATE:                handleCandidate(jsonMsg);            break;              }    });
conn.on("close", function(code, reason) { console.info("server close , code: " + code + ", reason: " + reason); if(conn.client != null) { // force all clients to exit the room handleForceLeave(conn.client); } });
conn.on("error", function(err) { console.info("server error: " + err); });}).listen(8099);
复制代码


服务 close 时我们需要强制退出房间中所有客户端:handleForceLeave(conn.client),如下


function handleForceLeave(client) {    var roomId = client.roomId;    var uid = client.uid;
// 查找rooId var roomMap = roomTableMap.get(roomId); if (roomMap == null) { console.warn("handleForceLeave can't find the roomId: " + roomId); return; }
// uid是否在房间 if (!roomMap.contains(uid)) { console.info("uid: " + uid +" have leave roomId: " + roomId); return; }
// 客户端没有正常离开,执行强制离开 console.info("uid: " + uid + " force leave room: " + roomId);
roomMap.remove(uid); // 删除发送者 if(roomMap.size() >= 1) { var clients = roomMap.getEntrys(); for(var i in clients) { var jsonMsg = { 'cmd': SIGNAL_TYPE_PEER_LEAVE, 'remoteUid': uid // 离开方uid }; var msg = JSON.stringify(jsonMsg); var remoteUid = clients[i].key; var remoteClient = roomMap.get(remoteUid); if(remoteClient) { console.info("notify peer: " + remoteClient.uid + ", uid: " + uid + " leave"); remoteClient.conn.sendText(msg); } } }}
复制代码


接下来我们介绍服务器监听到客户端消息 text 如何处理,如下


SIGNAL_TYPE_JOIN://加入房间


//server监听到有人加入房间时调用function handleJoin(message, conn) {    var roomId = message.roomId;    var uid = message.uid;
console.info("uid: " + uid + " try to join the room " + roomId);
var roomMap = roomTableMap.get(roomId); if (roomMap == null) {//没有房间时创建房间 roomMap = new RTCMap(); roomTableMap.put(roomId, roomMap); }
if(roomMap.size() >= 2) { console.error("roomId:" + roomId + " the room is full"); // 有需要可以自己加个信令来通知客户端房间已满 return null; }
var client = new Client(uid, conn, roomId); roomMap.put(uid, client);// 将新加入的客户端加入到房间管理Map中
if(roomMap.size() > 1) {// 当房间里面已经有人时要通知刚加进来的人与房间中的人 var clients = roomMap.getEntrys(); for(var i in clients) { var remoteUid = clients[i].key; if (remoteUid != uid) { var jsonMsg = { 'cmd': SIGNAL_TYPE_NEW_PEER, 'remoteUid': uid }; var msg = JSON.stringify(jsonMsg); var remoteClient =roomMap.get(remoteUid); console.info("new-peer: " + msg); //通知已经在房间的人 remoteClient.conn.sendText(msg);
jsonMsg = { 'cmd':SIGNAL_TYPE_RESP_JOIN, 'remoteUid': remoteUid }; msg = JSON.stringify(jsonMsg); console.info("resp-join: " + msg); //通知加入房间的人 conn.sendText(msg); } } } return client;}
复制代码


以上方法会返回一个 client 客户端与服务端建立连接。


SIGNAL_TYPE_LEAVE://离开房间


//server监听到有人离开房间时调用function handleLeave(message) {    var roomId = message.roomId;    var uid = message.uid;
var roomMap = roomTableMap.get(roomId); if (roomMap == null) { console.error("handleLeave can't find then roomId: " + roomId); return; } if (!roomMap.contains(uid)) { console.info("uid: " + uid +" have leave roomId: " + roomId); return; } console.info("uid: " + uid + " leave room: " + roomId); roomMap.remove(uid); // 移除发送者 if(roomMap.size() >= 1) { var clients = roomMap.getEntrys(); for(var i in clients) { var jsonMsg = { 'cmd': SIGNAL_TYPE_PEER_LEAVE, 'remoteUid': uid // 离开方uid }; var msg = JSON.stringify(jsonMsg); var remoteUid = clients[i].key; var remoteClient = roomMap.get(remoteUid); if(remoteClient) { console.info("notify peer: " + remoteClient.uid + ", uid: " + uid + " leave"); remoteClient.conn.sendText(msg); } } }}
复制代码


SIGNAL_TYPE_OFFER://发送 offer 给对端 peer


function handleOffer(message) {    var roomId = message.roomId;    var uid = message.uid;    var remoteUid = message.remoteUid;
console.info("handleOffer uid: " + uid + " transfer offer to remoteUid: " + remoteUid);
var roomMap = roomTableMap.get(roomId); if (roomMap == null) { console.error("handleOffer can't find the roomId: " + roomId); return; }
if(roomMap.get(uid) == null) { console.error("handleOffer can't find the uid: " + uid); return; }
var remoteClient = roomMap.get(remoteUid); if(remoteClient) { var msg = JSON.stringify(message); remoteClient.conn.sendText(msg); } else { console.error("can't find the remoteUid: " + remoteUid); }}
复制代码


SIGNAL_TYPE_ANSWER://发送 answer 给对端 peer


function handleAnswer(message) {    var roomId = message.roomId;    var uid = message.uid;    var remoteUid = message.remoteUid;  
console.info("handleAnswer uid: " + uid + " transfer answer to remoteUid: " + remoteUid);
var roomMap = roomTableMap.get(roomId); if (roomMap == null) { console.error("handleAnswer can't find the roomId: " + roomId); return; }
if(roomMap.get(uid) == null) { console.error("handleAnswer can't find the uid: " + uid); return; }
var remoteClient = roomMap.get(remoteUid); if(remoteClient) { var msg = JSON.stringify(message); remoteClient.conn.sendText(msg); } else { console.error("can't find the remoteUid: " + remoteUid); }}
复制代码


SIGNAL_TYPE_CANDIDATE://发送 candidate 给对端 peer


function handleCandidate(message) {    var roomId = message.roomId;    var uid = message.uid;     var remoteUid = message.remoteUid;
console.info("handleCandidate uid: " + uid + "transfer candidate to remoteUid: " + remoteUid);
var roomMap = roomTableMap.get(roomId); if (roomMap == null) { console.error("handleCandidate can't find the roomId: " + roomId); return; }
if(roomMap.get(uid) == null) { console.error("handleCandidate can't find the uid: " + uid); return; }
var remoteClient = roomMap.get(remoteUid); if(remoteClient) { var msg = JSON.stringify(message); remoteClient.conn.sendText(msg); } else { console.error("can't find remoteUid: " + remoteUid); }}
复制代码


三、总结


笔者对 WebRTC 信令服务器的信令协议封装与信令服务器实现做了较为详尽的介绍,本文讲解的信令服务器主要是用 websocket 来实现客户端各种行为的处理,目的是为了让读者能对信令服务器有一个更好的理解,其中的一些技术细节还需要读者自己做进一步的拓展学习。笔者后续还会有基于 WebRTC 的 1 对 1 通话实战(三)web 端实现、基于 WebRTC 的 1 对 1 通话实战(四)Android 端实现、基于 WebRTC 的 1 对 1 通话实战(五)iOS 端实现系列文章,欢迎关注了解。如果本篇文章对你有帮助,欢迎点个赞哈~


发布于: 4 小时前阅读数: 7
用户头像

IT酷盖

关注

写代码是一件快乐的事儿~ 2021.03.31 加入

还未添加个人简介

评论

发布
暂无评论
基于 WebRTC 的1 对 1 通话实战(二)信令服务器实现