基于 WebRTC 的 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 端实现系列文章,欢迎关注了解。如果本篇文章对你有帮助,欢迎点个赞哈~
版权声明: 本文为 InfoQ 作者【IT酷盖】的原创文章。
原文链接:【http://xie.infoq.cn/article/1c643645bdd40ab8443f7983a】。文章转载请联系作者。
IT酷盖
写代码是一件快乐的事儿~ 2021.03.31 加入
还未添加个人简介
评论