一、P2P实现的基础思路
# 一、P2P实现的基础思路
1、webrtc的基本核心对象认识:RtcPeerConnection
2、webrtc实现会话的流程:通话前如何建立p2p会话 -- 需要信令转发
3、信令服务器的搭建:构思功能以及数据存储和扩展
4、实现JavaScript端呼叫和被呼叫方的基础流程
5、完成视频通话
6、其他:
datachannel --- 消息类型、消息格式、消息大小
# 二、webrtc的核心对象:PeerConnection
获取到有效的PeerConnection
对象:
var PeerConnection = window.RTCPeerConnection ||
window.mozRTCPeerConnection ||
window.webkitRTCPeerConnection;
载体的核心方法如下:
addIceCandidate()
: 保存 ICE 候选信息,即双方协商信息,持续整个建立通信过程,直到没有更多候选信息。addTrack()
:添加音频或者视频轨道。createAnswer()
:创建应答信令。createDataChannel()
: 创建消息通道,建立WebRTC
通信之后,就可以p2p
的直接发送文本消息,无需中转服务器。createOffer()
: 创建初始信令。setRemoteDescription()
: 保存远端发送给自己的信令。setLocalDescription()
:保存自己端创建的信令。
除了上面的核心方法还包括一些事件监听函数:
ondatachannel
: 创建datachannel
后监听回调以及p2p
消息监听。ontrack
:监听远程媒体轨道即远端音视频信息。onicecandidate
: ICE 候选监听。
# 三、webrtc的会话流程
例如:A 为caller(呼叫端),B为callee(被呼叫端)
流程如下:
1、首先 A 呼叫 B,呼叫之前我们一般通过实时通信协议WebSocket
即可,让对方能收到信息。
2、B 接受应答,A 和 B 均开始初始化PeerConnection
实例,用来关联 A 和 B 的SDP
会话信息。
3、A 调用createOffer
创建信令,同时通过setLocalDescription
方法在本地实例PeerConnection
中储存一份(图中流程①)。
4、然后调用信令服务器将 A 的SDP
转发给 B(图中流程②)。
5、B 接收到 A 的SDP
后调用setRemoteDescription
,将其储存在初始化好的PeerConnection
实例中(图中流程③)。
6、B 同时调用createAnswer
创建应答SDP
,并调用setLocalDescription
储存在自己本地PeerConnection
实例中(图中流程④)。
7、B 继续将自己创建的应答SDP
通过服务器转发给 A(图中流程⑤)。
8、A 调用setRemoteDescription
将 B 的SDP
储存在本地PeerConnection
实例(图中流程⑤)。
9、在会话的同时,从图中我们可以发现有个ice candidate
,这个信息就是 ice 候选信息,A 发给 B 的 B 储存,B 发给 A 的 A 储存,直至候选完成。
# 四、信令服务器的搭建
串通呼叫端和被呼叫端的媒介。是个即时通讯服务器
信令服务器具备的功能:
1、需要临时存储会话双方的信息
2、需要监听特定类型的消息,比如呼叫端创建的初始指令和被呼叫端创建的应答信令区分开来 --- offer信令监听、answer信令监听、呼叫监听、ice候选信息监听
3、需要提供p2p的发消息的功能
4、后期多人会议,需要实现1对多发消息功能
5、要实现会议,需要个人信息和集体信息 --- 个人信息标识userId、集体信息标识roomId
区分userID和roomId,如何存会议室中的用户信息呢?
用到
Redis
的一种数据结构Hash
const httpServer = require('http').createServer();
const io = require('socket.io')(httpServer);
//redis
var redis = require('redis')
const roomKey = "meeting-room::"
var redisClient = redis.createClient(6379, '127.0.0.1')
redisClient.on('error', function (err) {
console.log('redisClient connect Error ' ,err);
});
const userMap = new Map() // user - > socket
io.on('connection', async (socket) => {
await onListener(socket)
});
httpServer.listen(18080, async() => {
console.log('服务器启动成功 *:18080');
await redisClient.connect();
});
/**
* res data
*/
function getMsg(type,msg,status=200,data=null){
return {"type":type,"msg":msg,"status":status,"data":data}
}
function getParams(url,queryName){
let query = decodeURI(url.split('?')[1]);
let vars = query.split("&");
for (var i = 0; i < vars.length; i++) {
var pair = vars[i].split("=");
if (pair[0] === queryName) {
return pair[1];
}
}
return null;
}
/**
* DB data
* @author suke
* @param {Object} userId
* @param {Object} roomId
*/
async function getUserDetailByUid(userId,roomId){
let res = JSON.stringify(({"userId":userId,"roomId":roomId}))
console.log(res)
return res
}
/**
* 监听
* @author suke
* @param {Object} s
*/
async function onListener(s){
let url = s.client.request.url
let userId = getParams(url,'userId')
let roomId = getParams(url,'roomId')
console.log("client uid:"+userId+" roomId: "+roomId+" online ")
//user map
userMap.set(userId,s)
//room cache
if(roomId){
await redisClient.hSet(roomKey+roomId,userId, await getUserDetailByUid(userId,roomId))
oneToRoomMany(roomId,getMsg('join',userId+ ' join then room'))
}
s.on('msg', async (data) => {
console.log("msg",data)
await oneToRoomMany(roomId,data)
});
s.on('disconnect', () => {
console.log("client uid:"+userId+" roomId: "+roomId+" offline ")
userMap.delete(userId)
if(roomId){
redisClient.hDel(roomKey+roomId,userId)
oneToRoomMany(roomId,getMsg('leave',userId+' leave the room '))
}
});
s.on('roomUserList', async (data) => {
// console.log("roomUserList msg",data)
s.emit('roomUserList',await getRoomUser(data['roomId']))
})
s.on('call',(data) => {
let targetUid = data['targetUid']
if(userMap.get(targetUid)){
oneToOne(targetUid,getMsg('call',"远程呼叫",200,data))
}else{
console.log(targetUid+ "不在线")
}
})
s.on('candidate',(data) => {
let targetUid = data['targetUid']
if(userMap.get(targetUid)){
oneToOne(targetUid,getMsg('candidate',"ice candidate",200,data))
}else{
console.log(targetUid+ "不在线")
}
})
s.on('offer',(data) => {
let targetUid = data['targetUid']
if(userMap.get(targetUid)){
oneToOne(targetUid,getMsg('offer',"rtc offer",200,data))
}else{
console.log(targetUid+ "不在线")
}
})
s.on('answer',(data) => {
let targetUid = data['targetUid']
if(userMap.get(targetUid)){
oneToOne(targetUid,getMsg('answer',"rtc answer",200,data))
}else{
console.log(targetUid+ "不在线")
}
})
}
/**
* ono to one (event msg)
* @author suke
* @param {Object} uid
* @param {Object} msg
*/
function oneToOne(uid,msg){
let s = userMap.get(uid)
if(s){
s.emit('msg',msg)
}else{
console.log(uid+"用户不在线")
}
}
/**
* 获取房间用户列表
* @author suke
* @param {Object} roomId
*/
async function getRoomUser(roomId){
return await redisClient.hGetAll(roomKey+roomId)
}
/**
* one to room many
* @author suke
* @param {Object} roomId
* @param {Object} msg
*/
async function oneToRoomMany(roomId,msg){
let ulist = await redisClient.hGetAll(roomKey+roomId)
for(const uid in ulist){
oneToOne(uid,msg)
}
}