信令状态机
在开始介绍端到端通信之前,必须先实现客户端的信令系统,让客户端与信令服务器可以互通,从而为端到端交换信息做好准备。那么客户端的信令系统该如何实现呢?
最简单的办法是通过状态机实现,其基本原理如下:每次发送/接收一个信令后,客户端都根据状态机当前的状态做相应的逻辑处理。比如当客户端刚启动时,其处于 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 节中给出。
其他几种情况与前面介绍的两种情况是类似的,都是收到服务端的信令后回调对应的函数,在函数中变更状态,然后做相应的逻辑处理,这里就不再赘述了。