webrtc之实现简单信令服务器

1,075 阅读6分钟

上小节介绍了webrtc是如何进行连接的,它们都离不开一个非常重要的组成部分--> 信令服务器。没有这个信令服务器,双方就不能建立连接。那么这小节就来介绍下信令服务器,以及如何用Node来实现一个简单版本。

功能

信令服务器,说白了就是一个中转站,本身并不会处理音视频数据,只是负责连接信息。回想下媒体协商的过程(如果忘了,再看看这篇),里面有offeranswerice的传递,就是通过信令服务器来实现的。当然,还可以传递一些其他信息,比如 麦克风的关闭和开启、是否启用桌面分享等。

总结一下,信令服务器可以有以下功能:

  1. 交换会话描述协议(SDP): SDP包含了媒体信息,如编解码器、媒体格式、网络信息等。对等端需要交换SDP以协商连接参数。
  2. 交换ICE候选者(ICE): ICE候选者包含了对等端的网络地址信息,用于建立点对点连接,对等端交换这些候选者信息。
  3. 传递控制信息: 信令服务器传递各种控制信息,如音视频的开启和关闭、连接的建立和终止、错误处理等。
  4. 用户发现和会话管理: 信令服务器可以帮助对等端发现彼此,并管理会话的生命周期。

实现

现在了解了它的功能,现在再来看看如何实现一个简单的、具备基础功能的信令服务器。

这里使用node的expresssocket.io来实现。

我们来分析下具备基础功能的信令服务器(p2p情况下)到底应该具有哪些功能才能正常使用呢?

首先,要想和对方进行连接,你必须得知道对方的存在,知道他此时此刻在线才能发起连接请求。假如说他不在线,发起了连接请求也没啥用。 这里确定了第一个功能,知道对方在线。

然后就是发起连接请求,传递SDP和ICE,使双方建立连接。这是第二个功能,传递SDP和ICE。

再然后,建立之后,到底有没有建立成功呢?这里也需要返回一个状态表示成功与否。这是第三个功能,返回连接状态。

假如在音视频过程中出现未知错误导致连接中断,这里也得提示双方,让他们重新连接。这是第四个功能,捕获错误。

双方在音视频过程中,一方关闭了麦克风或者摄像头(不是断开连接),又或者开启了桌面共享,这些都要提示对方。这是第五个功能,功能操作后提示对方。

双方在通话结束后,点击了关闭按钮;或者不小心点了关闭按钮。总之就是人为的关闭了音视频,这里也得通知对方,告知对方连接已经被终止了。这是第六个功能,告知对方终止连接。

从感知对方存在,到连接再到执行操作,再到终止连接,再到捕获错误。最基本的使用功能都貌似考虑到了。下面就来逐个实现:

// 从url中获取指定属性的值
function getParams(url,queryName) {
	const params = new URLSearchParams(url.split('?')[1]);
	return params.get(queryName);
}

// 获取用户列表
const getAllUsersList = () => {

  const usersList = []

  for (const i of usersMap.values()) {
    usersList.push(i.userInfo)
  }

  return usersList
}

// 监听连接 拿到socket
io.on('connection',(socket) => {
  onListener(socket)
})

const onListener = async (socket) => {
  // ------ 获取到用户信息--------
  const id = getUUID();
  const name = getName();
  const handshake = socket.handshake;
  const { latitude,longitude } = handshake.query;
  const ip = handshake.address;

  // console.log(socket.id,'socket.id');

  // 将用户信息放到 socket.userInfo中 方便之后操作
  socket.userInfo = {
    name,
    id,
    socketId: socket.id,
    latitude,
    longitude,
    city,
    hidden: [] // 屏蔽的用户, 将不会收到消息
  } // 将用户信息放入userInfo

  // console.log('用户信息',socket.userInfo);

  socket.join(roomKey); // 加入 roomKey 房间 相当于是用户上线后,进入到一个整体的大房间内

  usersMap.set(id,socket); // 将所有user的id放入usersMap  用户ID是不会变的 但是SOCKET会变 当刷新后会重新连接 所以不能用来当做KEY

  socket.to(roomKey).emit('joinRoom',`欢迎 ${name} 加入房间!`); // 通知房间内所有人(除了本人之外)

  const usersList =  getAllUsersList(); // 重新获取房间内所有用户列表
  io.to(roomKey).emit('usersList',usersList); // 通知房间内所有人, 更新用户列表
}

到这里,就实现了第一个功能,也是最重要的功能。当用户上线后,自动进入到一个大房间内,这个房间包含所有用户,上线后会自动通知其他用户有新用户上线。这里将用户信息都保存在socket中,方便后面操作,不必再查找一遍。然后将所有用户的信息都保存在usersMap中,当然,有数据库的话,也可以放在数据库中。这里就简单点处理,直接用变量来保存。

接下来再看其他功能,就简单多了。还是在这个onListener函数中添加内容:

const onListener = async (socket) => {

  // ......

  const onToOneMsg = (data) => {
    const toSocket = usersMap.get(data.toUserId); // 找出对方的socket
    toSocket && toSocket.emit('msg',data); // 通知对方
  }

  // -------- 第二个功能 传递SDP和ICE ---------------
  // 候选信息
  socket.on('candidate',(data) => {
    onToOneMsg({ ...data,type: 'candidate' });
  })

  // offer信令监听
  socket.on('offer',(data) => {
    onToOneMsg({ ...data,type: 'offer' });
  })

  // answer信令监听
  socket.on('answer',(data) => {
    onToOneMsg({ ...data,type: 'answer' });
  })


  // -------------- 第三个功能  返回连接状态 ----------------
  socket.on('status',(data) => {
    onToOneMsg({ ...data,type: 'status' });
  })

  // ----------- 第四个功能 捕获错误 --------------
  socket.on('error',(data) => {
    onToOneMsg({ ...data,type: 'error' });
  })

  // ----------- 第五个功能  功能操作 --------------
  // 这里还可以拓展很多,比如桌面共享 发送消息  
  // 接收文件传输
  socket.on('receiveFile',(data) => {
    onToOneMsg({ ...data,type: 'receiveFile' });
  })

  // 麦克风状态改变
  socket.on('micStatusChange',(data) => {
     onToOneMsg({ ...data,type: 'micStatusChange' });
  })

  // 摄像头状态改变
  socket.on('webcamStatusChange',(data) => {
     onToOneMsg({ ...data,type: 'webcamStatusChange' });
  })

  // ------------- 第六个功能 终止连接 ---------------
  socket.on('close',(data) => {
     onToOneMsg({ ...data,type: 'close' });
  })

 
  // 房间用户列表
  socket.on('usersList',async () => {
    const usersList = await getAllUsersList();
    socket.emit('usersList',usersList);
  })
}

可以看到,剩下的几个功能都是非常简单的,只要监听对应的事件就行了。当然,这里也可以进行优化一下,将所有这几个事件都放在一个事件里,然后用type去进行区分,写过switch就行了,就不用写这么多的监听事件。

还有一点要注意,就是当双方进行连接后,也要同步修改双方的状态,并且广播一下通知其他用户,避免双方在进行连接时又突然有第三个人。终止连接后记得也要这么做。

OK,到这里就基本实现了一个简单、最小可用的信令系统了,非常简单!

小节

本小节分析了信令系统应该具备的功能,交换SDPICE、交换功能操作信息、用户发现和会话管理。然后从实际使用的角度分析了一个简单的信令系统应该具有的基础功能点,分别是知道对方在线、传递SDP和ICE、返回连接状态、捕获错误、功能操作后提示对方和告知对方终止连接,然后根据功能点逐个列出代码示例。

整体来说还是非常简单的,这里只是实现了 P2P 。如果要实现多人的话,思路也是一样的,多人也是两两进行连接。这里就不详细展开,后面也会有介绍和实现。

下一小节来介绍如何实现双方建立连接进行音视频通话和发送消息。