Signal如何使用Rust构建大规模端到端加密视频通话?


Signal 在一年前发布了端到端加密群组通话,从那时起,我们从支持 5 个参与者一直扩展到 40 个。没有现成的软件可以让我们同时支持这种规模的通话确保所有通信都是端到端加密的,因此我们构建了自己的开源信号调用服务来完成这项工作。这篇文章将更详细地描述它的工作原理。
 
选择性转发单元 (SFU)
在群组通话中,每一方都需要将他们的音频和视频发送给通话中的所有其他参与者。有 3 种可能的通用架构可以这样做:

  • 全网状:每个呼叫参与者将其媒体(音频和视频)直接发​​送给其他呼叫参与者。这适用于非常小的呼叫,但不适用于许多参与者。大多数人的 Internet 连接速度不够快,无法同时发送 40 个视频副本。
  • 服务器混合:每个呼叫参与者将其媒体发送到服务器。服务器将媒体“混合”在一起并将其发送给每个参与者。这适用于许多参与者,但与端到端加密不兼容,因为它要求服务器能够查看和更改媒体。
  • 选择性转发:每个参与者将其媒体发送到服务器。服务器将媒体“转发”给其他参与者而不查看或更改它。这适用于许多参与者,并且与端到端加密兼容。

由于 Signal 必须具有端到端加密并扩展到许多参与者,因此我们使用选择性转发。执行选择性转发的服务器通常称为选择性转发单元或 SFU。
 SFU 中代码主循环的简化版本如下所示:

let socket = std::net::UdpSocket::bind(config.server_addr);  
let mut clients = ...;  // changes over time as clients join and leave
loop {
  let mut incoming_buffer = [0u8; 1500];
  let (incoming_size, sender_addr) = socket.recv_from(&mut incoming_buffer);
  let incoming_packet = &incoming_buffer[..incoming_size];

  for receiver in &clients {
     
// Don't send to yourself
     if sender_addr != receiver.addr {
       
// Rewriting the packet is needed for reasons we'll describe later.
       let outgoing_packet = rewrite_packet(incoming_packet, receiver);
       socket.send_to(&outgoing_packet, receiver.addr);
     }
  }
}


为了扩展到更多参与者,我们用 Rust 从头开始​​编写了一个新的 SFU。它现在已经为所有 Signal 组呼叫服务了 9 个月,轻松扩展到 40 个参与者(未来可能更多),并且具有足够的可读性,可以作为基于 WebRTC 协议(ICESRTP运输-cc, 和googcc )。
现在让我们更深入地了解 SFU 中最难的部分。您可能已经猜到了,它比上面的简化循环更复杂。
SFU 中最难的部分
SFU 最困难的部分是在网络条件不断变化的同时将正确的视频分辨率转发给每个呼叫参与者。
这个困难是以下基本问题的组合:

  1. 每个参与者的 Internet 连接容量都在不断变化并且很难知道。如果 SFU 发送过多,则会造成额外的延迟。如果 SFU 发送的太少,质量就会很低。因此,SFU 必须不断仔细地调整它发送给每个参与者的数量,以使其“恰到好处”。
  2. SFU 不能修改它转发的媒体;要调整它发送的数量,它必须从发送给它的媒体中进行选择。如果可供选择的“菜单”仅限于发送可用的最高分辨率或根本不发送,则很难适应各种网络条件。因此,每个参与者必须向 SFU 发送多种分辨率的视频,并且 SFU 必须不断小心地在它们之间切换。

解决方案是结合几种我们将单独讨论的技术:
  • 联播和数据包重写允许在不同的视频分辨率之间切换。
  • 拥塞控制确定要发送的正确数量。
  • 速率分配决定在该预算内发送什么。

详细点击标题原文