基于 WebRTC 的一对一屏幕共享项目(三)——信令服务器
在基于 WebRTC 的一对一屏幕共享项目中,信令服务器起着至关重要的作用。它负责协调客户端之间的连接建立、会话控制以及状态同步等操作。本文将详细讲解信令服务器中使用的房价管理方法以及整个交互流程。
最初版源码已上传:GitCode - 全球开发者的开源社区,开源代码托管平台
1. RTCMap
数据结构
1.1 简介
RTCMap
是一个自定义的数据结构,用于管理房间和客户端信息。它类似于 JavaScript 中的 Map
对象,但在功能上进行了定制,以满足项目的特定需求。RTCMap
提供了基本的键值对存储和操作方法,包括 put
、get
、remove
、clear
等。
1.2 代码实现
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;
};
}
1.3 功能详解
put(key, value)
:将键值对存储到RTCMap
中。如果键不存在,则添加新的键值对;如果键已存在,则更新对应的值。get(key)
:根据键获取对应的值。如果键存在,则返回对应的值;否则返回null
。remove(key)
:根据键删除对应的键值对。clear()
:清空RTCMap
中的所有键值对。contains(key)
:检查RTCMap
中是否包含指定的键。如果包含,则返回true
;否则返回false
。size()
:返回RTCMap
中键值对的数量。getEntrys()
:返回RTCMap
中所有的键值对数组。
1.4 使用场景
在信令服务器中,RTCMap
主要用于管理房间和客户端信息。具体来说,roomTableMap
是一个 RTCMap
实例,用于存储所有房间的信息,每个房间又对应一个 RTCMap
实例,用于存储该房间内的客户端信息。
var roomTableMap = new RTCMap();
function Client(uid, conn, roomId) {
this.uid = uid;
this.conn = conn;
this.roomId = roomId;
}
2. 交互流程
2.1 整体概述
整个交互流程主要涉及客户端和信令服务器之间的通信,包括客户端加入房间、发送会话描述协议(SDP)、发送 ICE 候选信息等操作。信令服务器负责接收客户端的消息,并根据消息类型进行相应的处理,如转发消息、通知其他客户端等。
2.2 详细流程
2.2.1 客户端连接到信令服务器
客户端通过 WebSocket 连接到信令服务器。信令服务器在接收到客户端的连接请求后,创建一个新的连接对象。
var server = ws.createServer(function(conn){
console.log("创建一个新的连接--------")
conn.client = null;
// ...
}).listen(prort);
2.2.2 客户端加入房间
当用户在界面输入房间号并提交后,客户端会构造包含 房间 ID 和 用户 ID 的 join
消息,并通过 WebSocket 发送至信令服务器:
function doJoin(roomId) {
var jsonMsg = {
'cmd': SIGNAL_TYPE_JOIN,
'roomId': roomId,
'uid': localUserId,
};
var message = JSON.stringify(jsonMsg);
RTCEngine.sendMessage(message);
console.info("doJoin");
}
此消息携带两个核心参数:
roomId
:用户请求加入的房间标识符uid
:客户端生成的唯一用户标识
信令服务器接收到 join
消息后,调用 handleJoin
函数执行以下操作:
(1)检查房间是否存在
let roomMap = roomTableMap.get(roomId);
if (roomMap == null) {
roomMap = new RTCMap(); // 创建新房间
roomTableMap.put(roomId, roomMap);
}
roomTableMap
是全局的 房间注册表,使用RTCMap
存储所有房间信息- 若房间不存在,服务器会创建新的
RTCMap
实例并将其注册到roomTableMap
中
(2)检查房间容量
if (roomMap.size() >= 2) {
console.error("房间已满,请使用其他房间");
return null; // 拒绝加入请求
}
Client
对象封装了用户标识、WebSocket 连接和所属房间信息roomMap
存储当前房间的所有客户端,键为uid
,值为Client
实例
(3)房间已有用户时的消息分发
当服务器检测到 房间中已有一名用户(即 roomMap.size() === 1
)时,会执行以下双方向通知:
向新加入者发送 RESP_JOIN
消息:
// 构造响应消息,包含已在房间的用户ID
const respJoinMsg = {
cmd: "RESP_JOIN",
remoteUid: existingUserId
};
conn.sendText(JSON.stringify(respJoinMsg));
RESP_JOIN
消息携带remoteUid
参数,告知新用户 “房间中已有谁”- 客户端收到此消息后,更新 UI 显示 “对方已在房间等待”
向已存在用户发送 NEW_PEER
消息:
// 构造通知消息,告知已有用户“新用户加入”
const newPeerMsg = {
cmd: "NEW_PEER",
remoteUid: newUserId
};
existingUserConn.sendText(JSON.stringify(newPeerMsg));
加入房间部分时序图:
(4)客户端响应
处理 RESP_JOIN
消息(新加入者):
function handleResponseJoin(message) {
console.info("handleResponseJoin, remoteUid: " + message.remoteUid);
remoteUserId = message.remoteUid;
}
处理 NEW_PEER
消息(已存在用户):
function handleRemoteNewPeer(message) {
console.info("handleRemoteNewPeer, remoteUid: " + message.remoteUid);
remoteUserId = message.remoteUid;
doOffer();
}
2.2.3 客户端发送 SDP 和 ICE 候选信息
当客户端 A 收到服务器转发的NEW_PEER
消息(通知有新用户 B 加入房间)时,会立即启动连接建立流程(dooffer):
function doOffer() {
// 创建RTCPeerConnection
console.log("dooffer")
if (pc == null) {
createPeerConnection();
}
pc.createOffer().then(createOfferAndSendMessage).catch(handleCreateOfferError);
}
信令服务器收到客户端 A 的offer
消息后,执行以下操作验证合法性:
function handleOffer(message) {
const roomId = message.roomId;
const senderUid = message.uid;
const receiverUid = message.remoteUid;
// 验证房间存在
const roomMap = roomTableMap.get(roomId);
if (!roomMap) {
return console.error("房间不存在:", roomId);
}
// 验证发送者和接收者
if (!roomMap.contains(senderUid) || !roomMap.contains(receiverUid)) {
return console.error("用户不在房间中");
}
}
然后转发详细到对端客户端:
// 获取接收者的WebSocket连接
const receiverClient = roomMap.get(receiverUid);
if (receiverClient) {
// 直接转发原始消息内容
receiverClient.conn.sendText(JSON.stringify(message));
}
目标客户端处理 Offer 并生成 Answer:
function handleRemoteOffer(message) {
console.info("handleRemoteOffer");
if(pc == null) {
createPeerConnection();
}
var desc = JSON.parse(message.msg);
pc.setRemoteDescription(desc);
doAnswer();
}
function doAnswer() {
pc.createAnswer().then(createAnswerAndSendMessage).catch(handleCreateAnswerError);
}
之后类似之前操作信令服务器转发answer,原始客户端处理 Answer,,完成 SDP 协商,在 SDP 协商的同时,两端客户端会并行收集 ICE 候选信息并通过信令服务器交换:
function handleIceCandidate(event) {
console.info("handleIceCandidate");
if (event.candidate) {
var jsonMsg = {
'cmd': SIGNAL_TYPE_CANDIDATE,
'roomId': roomId,
'uid': localUserId,
'remoteUid': remoteUserId,
'msg': JSON.stringify(event.candidate)
};
var message = JSON.stringify(jsonMsg);
RTCEngine.sendMessage(message);
// console.info("handleIceCandidate message: " + message);
console.info("send candidate message");
} else {
console.warn("End of candidates");
}
}
完整时序图如下:
2.2.4 客户端离开房间
客户端向信令服务器发送 leave
消息,请求离开当前房间。信令服务器接收到消息后,调用 handleLeave
函数进行处理。
function handleLeave(message) {
var roomId = message.roomId;
var uid = message.uid;
console.info("uid: " + uid + "leave room " + roomId);
var roomMap = roomTableMap.get(roomId);
if (roomMap == null) {
console.error("handleLeave can't find then roomId " + roomId);
return;
}
roomMap.remove(uid);
if(roomMap.size() >= 1) {
var clients = roomMap.getEntrys();
for(var i in clients) {
var jsonMsg = {
'cmd': 'peer-leave',
'remoteUid': 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);
}
}
}
}
2.3 交互流程图
3. 总结
本文详细讲解了基于 WebRTC 的一对一屏幕共享项目中信令服务器的 RTCMap
数据结构和整个交互流程。RTCMap
为信令服务器提供了高效的房间和客户端信息管理功能,而交互流程则确保了客户端之间的连接建立、会话控制以及状态同步等操作的顺利进行。通过对这些内容的理解,我们可以更好地掌握 WebRTC 信令服务器的实现原理和开发方法。