信令状态机

在开始介绍端到端通信之前,必须先实现客户端的信令系统,让客户端与信令服务器可以互通,从而为端到端交换信息做好准备。那么客户端的信令系统该如何实现呢?

最简单的办法是通过状态机实现,其基本原理如下:每次发送/接收一个信令后,客户端都根据状态机当前的状态做相应的逻辑处理。比如当客户端刚启动时,其处于 Init 状态,在此状态下,用户只能向服务端发送 join 消息,待服务端返回 joined 消息后,客户端的状态机发生了变化,变成了 joined 状态后,才能开展后续工作。客户端的状态机如图 5.1 所示。

image 2025 02 23 09 42 15 718
Figure 1. 图5.1 信令状态机

从图中可以发现,客户端的状态机共有 4 种状态,分别是 Initjoinedjoined_unbind 以及 joined_conn。下面详述一下各种状态之间是如何变化的。

  • 客户端刚启动时,其初始状态为 Init

  • Init 状态下,用户只能向服务器发送 join 消息;服务端收到 join 消息后,会返回 joined 消息;如果客户端能收到 joined 消息,则说明用户已经成功加入房间中,此时客户端状态更新为 joined

  • joined 状态下,客户端有多种选择,根据不同的选择可以切换到不同的状态:

    • 如果用户离开房间,客户端又回到了初始状态,即 Init 状态。

    • 如果客户端收到第二个用户加入的消息(即 other_joined 消息),则切换到 join_conn 状态。在这种状态下,两个用户就可以进行通信了。

    • 如果客户端收到第二个用户离开的消息(即 bye 消息),则需要将其状态切换到 join_unbind。实际上,join_unbind 状态与 joined 状态基本是一致的,不过可以通过这两种不同的状态值判断出用户之前的状态。

  • 如果客户端处于 join_conn 状态,当它收到 bye 消息时,会变成 joined_unbind 状态。

  • 如果客户端是 joined_unbind 状态,当它收到 other_join 消息时,会变成 join_conn 状态。

接下来看一下客户端状态机是如何实现的,参见代码 5.9。

代码5.9 客户端状态机
var state = 'init';

// 连接信令服务器并根据信令更新状态机
function conn() {
  // 建立 socket.io 连接
  socket = io.connect();

  // 收到 joined 消息
  socket.on('joined', (roomid, id) => {
    state = 'joined'; // 变更状态
    // ...
    // 创建连接
    createPeerConnection();
    bindTracks();
    // ...
  });

  // 收到 otherjoin 消息
  socket.on('otherjoin', (roomid) => {
    // ...
    state = 'joined_conn'; // 更改状态
    call();
    // ...
  });

  // 收到 full 消息
  socket.on('full', (roomid, id) => {
    // ...
    hangup();
    socket.disconnect(); // 关闭连接
    state = 'init'; // 回到初始化状态
    // ...
  });

  // 收到用户离开的消息
  socket.on('left', (roomid, id) => {
    // ...
    hangup();
    socket.disconnect();
    state = 'init'; // 回到初始化状态
    // ...
  });

  socket.on('bye', (room, id) => {
    // ...
    state = 'joined_unbind';
    hangup();
    // ...
  });

  // 向服务端发送 join 消息
  roomid = getQueryVariable('room');
  socket.emit('join', roomid);
}

// ...
conn(); // 与信令服务器建立连接
// ...

在代码 5.9 中,首先执行第一行代码,将状态机的状态初始化为 init;之后调用 conn() 函数,让客户端与信令服务器建立连接。而在 conn() 内部做了三件事:一是调用 socket.ioconnect() 方法与信令服务器建立连接;二是向 socket.io 注册 5 个回调函数,分别对应 5 个信令消息,即 joinedotherjoinfullleft 以及 bye 消息,以便收到不同的消息时做不同的逻辑处理;三是向信令服务器发送 join 消息。至此客户端的运转就由信令驱动起来了。

当客户端收到服务端返回的 joined 消息后,会在回调之前注册到 socket.io 中的回调函数,因此上面代码的第 10∼17 行会被执行。在这段代码中,客户端首先变更自己当前的状态为 joined,然后创建 RTCPeerConnection(关于 RTCPeerConnection 的内容将会在 5.7.1 节详细介绍)对象,最后将采集到的音视频流绑定到之前创建好的 RTCPeerConnection 对象上。

当第二个用户上线后,第一个用户会收到服务端发来的 otherjoin 消息。上面代码中的第 20∼25 行会被执行。在这几行代码中,也是先变更客户端状态为 joined_conn,然后调用 call() 函数。call() 函数实现的是媒体协商,有关媒体协商相关的内容会在 5.7.3 节再做介绍,相关的代码也在 5.7.3 节中给出。

其他几种情况与前面介绍的两种情况是类似的,都是收到服务端的信令后回调对应的函数,在函数中变更状态,然后做相应的逻辑处理,这里就不再赘述了。