信令状态机
在开始介绍端到端通信之前,必须先实现客户端的信令系统,让客户端与信令服务器可以互通,从而为端到端交换信息做好准备。那么客户端的信令系统该如何实现呢?
最简单的办法是通过状态机实现,其基本原理如下:每次发送/接收一个信令后,客户端都根据状态机当前的状态做相应的逻辑处理。比如当客户端刚启动时,其处于 Init
状态,在此状态下,用户只能向服务端发送 join
消息,待服务端返回 joined
消息后,客户端的状态机发生了变化,变成了 joined
状态后,才能开展后续工作。客户端的状态机如图 5.1 所示。

从图中可以发现,客户端的状态机共有 4 种状态,分别是 Init
、joined
、joined_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。
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.io
的 connect()
方法与信令服务器建立连接;二是向 socket.io
注册 5 个回调函数,分别对应 5 个信令消息,即 joined
、otherjoin
、full
、left
以及 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 节中给出。
其他几种情况与前面介绍的两种情况是类似的,都是收到服务端的信令后回调对应的函数,在函数中变更状态,然后做相应的逻辑处理,这里就不再赘述了。