Rails的ActionCable对于在应用程序中构建实时功能是非常好的。然而,在一些实时通信应用中,ActionCable就显得不够好了。特别是在秩序重要的堆栈中。
在构建实时通信应用时,其他范式是至高无上的。我说的是事件驱动范式,就像Node.js和JavaScript生态系统给我们的那样。
通过WebRTC,有序性保证了可靠的通信。
这里有一个例子。在建立点对点连接之前创建的SDP交换中,要约总是首先出现,然后是回答。没有提议,就不会有回答。一切都是事件驱动的:事件触发建立连接所需的后续事件。

在发送报价和收到答复之间,还有一些其他事件发生。最重要的是候选人对的生成。在前端,在Stimulus的帮助下,我们可以有这样的东西:
subscription() {
received(data) {
// ...
switch (data.type) {
case _this.JOIN_ROOM:
return _this.joinRoom(data)
case _this.EXCHANGE:
if (data.to !== _this.currentUser) return
return _this.exchange(data)
default:
return
}
}
// ...
}
ActionCable按顺序传递信息
如果只有ActionCable按顺序传递消息的话,这种方法是没有问题的。Rails团队在构建ActionCable时并没有考虑到信息的有序传递,而这正是问题的开始。如果你不注意信息的传递方式,你的视频会议应用就会变得不稳定。
我以前也经历过这种情况。为了确认ActionCable没有按顺序传递消息,我在控制器中写了一些代码:
(1..10).each do |int|
ActionCable.server.broadcast "room_channel_#{params[:id]}", int
end
浏览器控制台中显示的消息,是不按顺序传递的。

消息的随机传递确保了我在GitHub上找到的几个有类似实现的信号库的行为是不可预测的,有时会传递媒体轨道。你希望媒体轨道在任何时候都能被传递,这样你的软件才能被认为是可用的。
在这种情况下,你会得到SDP和候选对的混合,如下图所示:
我们在这里可以看到的是交换的杂乱无章的传播。我们可以看到,首先是一个报价,接着是大量的候选对,然后是底部的答案。这样的顺序并不好,因为两端的ICE代理人需要知道对方的意图,并同意在交换候选对之前相互交谈。
按照现在的方式,当一个ICE代理得到答案时,好的候选对已经过去了,代理会错过它们;这将导致点对点连接失败,意味着代理不会交换任何媒体轨道。
用后台工作分离消息
为了给交换流增加一些可靠性,你应该用一个后台作业把SDP和候选人分开。这个问题的解决方案是这样的:
- 从候选人中分离出SDP blobs。
- 立即交付SDP。
- 通过一个有一定延迟的后台作业发送候选对。
def receive(data)
if data.key?('sdp') || data.value?('JOIN_ROOM')
ActionCable.server.broadcast("room_channel_#{params[:room_id]}", data)
else
CandidateWorker.perform_async("room_channel_#{params[:room_id]}", data)
end
end
后台工作,有一定的延迟,像往常一样广播传递给它的消息:
class CandidateWorker
include Sidekiq::Worker
sidekiq_options retry: 3
def perform(channel, data)
sleep 0.2
ActionCable.server.broadcast(channel, data)
end
end
我在这里实现了延迟,以使SDP的回答和提议能够在候选人之前先进行。尽管Sidekiq有一个延迟作业的功能,但它的时间安排对这个目的来说不够精确,这就是为什么我使用好的老式sleep 来实现延迟。
以上保证了ICE代理之间首先交换答案和报价,使得每次都能有稳定的连接:
在Node.js环境中,事情是由事件驱动的,这个问题是不存在的。事件进入事件循环,并从那里被挑选出来,所以你保证了顺序,而且事情会按预期进行。