一、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的会话流程

例如:Acaller(呼叫端),B为callee(被呼叫端)

img

流程如下:

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)
            }

        }