上一篇文章《WebRTC 开发实践:为什么你需要 SFU 服务器》我们了解了 WebRTC SFU 服务器的基本原理和必要性,解决了 What 和 Why,本文则更近一步,探究一下实现 SFU 服务器的关键技术点有哪些 ?重点解决一下 How

1 什么是 SFU ?

首先,我们再看一次 SFU 服务器的定义,什么是 SFU ?

SFU 的全称是:Selective Forwarding Unit,是一种路由和转发 WebRTC 客户端音视频数据流的服务端程序。

WebRTC 开发实践:如何实现 SFU 服务器_SFU

如图所示,SFU 服务器最核心的功能就是与每一个 WebRTC Peer 客户端建立链接,分别接收来自他们的音视频数据,并实现 one-to-many 的能力(即把一个客户端的流转发到其他 WebRTC Peer 客户端),那么,如果我们要实现这样一台 SFU 服务器,有哪些需要解决和处理的问题呢 ?

我们可以想象一下 Web 服务器和直播服务器的工作原理,浏览器/直播客户端,要想完成与服务器之间的数据交换,通常离不开如下几个步骤:

  1. 通过 DNS 解析,拿到服务器的 IP 地址;然后通过 “约定” 的端口(如:80 或者 1935)连接到服务器

  2. 客户端使用 “约定” 的信令协议(如:HTTP,RTMP),发送请求给服务器,实现数据交换的准备工作

  3. 客户端开始上行数据给服务器,或者服务器开始下发数据到客户端,结束后,通过信令关闭连接

  4. 静态的资源型的数据(文件、网页),通常服务端是读取磁盘上的数据拷贝一份给需要的客户端

  5. 非静态的实时数据(如:直播流),服务器则通过在 “内存” 中拷贝并转发给需要的客户端

同样,WebRTC 客户端与 SFU 服务器之间的交互,也是离不开这些步骤的,特别是 4/5,其实就是所谓的 one-to-many 能力。

2  信令和传输通道的建立

首先我们解决第一个问题,即 WebRTC 客户端是如何跟 SFU 服务器建立数据传输通道的 ?

WebRTC 开发实践:如何实现 SFU 服务器_WebRTC_02

如图,我们先看看浏览器与 Web 服务器的建联过程:浏览器通过 DNS 解析 URL 中的域名,拿到 IP 后通过 80 端口连接上服务器(后续的数据传输均复用这条 TCP 链路)。

WebRTC 其实也是类似的,但是与标准的 HTTP 服务或者 RTMP 直播服务相比,还是有些区别的,如下:

  1. 信令和数据通道是 “分离” 的,信令目前没有统一的实现方案,可以使用任何方案(如:HTTP、TCP 自定义协议、SIP 等等),但是数据并不走这条信令链路,而是走单独的 UDP 端口

  2. 数据通道使用的 UDP 协议,不像 TCP 有 “连接” 的概念,客户端仅仅知道服务器的 UDP 端口,但不 “连接” 是无法预判传输通道是否真的 OK(主要是部分 NAT 网关类型的限制,导致并不是所有 UDP 传输都能通),因此需要借助一些框架和协议来判断 UDP 通道的可用性(即 ICE 协议)

上述内容分析完了,我们就可以看看如何实现 SFU 的信令和传输通道了:

  1. 实现 HTTP Web Server 服务(或者 SIP 或者基于 TCP 自定义协议),用于提供 “信令” 的支持(如:推流命令、拉流命令等)

  2. 通过 libnice 库或者自己 coding 的方式,实现 ICE 协议,用于提供 UDP “数据通道” 的检测和建联

  3. 实现 UDP 数据监听和发送,用于接收客户端的数据,转发其他客户端的数据

3 需要实现哪些 “信令” ?

对于 HTTP 协议,实现的 “信令” 包括:GET,POST,DELETE 等等,定义了浏览器期望进行的行为。同理,对于 SFU,我们也要定义一系列必要的信令,以约定客户端和服务器对应的行为,那具体有哪些呢 ?

其实 WebRTC 客户端,与 SFU 服务器需要协商的事情,无外乎就是如下几点:

  1. ICE 建联:交换 ICE 信息(用户名、密码、IP 地址、UDP 端口等)

  2. 发布流/取消发布流:客户端通知服务器准备好接收数据

  3. 订阅流/取消订阅流:客户端通知服务器准备好转发数据

因此,SFU 服务器通过任意一种方式(HTTP/TCP 等),提供 ICE Connection/Publish/Subscribe 信令即可,SFU 在信令背后需要实现的逻辑分别如下:

1. ICE Connection:添加一路 UDP 通道

2. Publish:添加一个逻辑上的数据 Producer,通过 UDP 通道 recv 客户端的数据,通知逻辑上的 Consumers

3. Subscribe:添加一个逻辑上的数据 Consumer,收到 Producer 通知后,通过 UDP 通道 send 给客户端


4 如何实现 one-to-many ?

这是 SFU 最核心的功能,其实也不是 WebRTC SFU 特有,如前面所述,凡是非静态资源型的服务(数据实时产生实时消费)均需要在服务端实现 one-to-many,比较典型的例子就是 RTMP 直播流服务器,需要将客户端推流上来的数据,实时转发给多个拉流的客户端。

实现 one-to-many ,最重要的一点是需要把数据的生产者(Publisher)和数据的消费者(Subscriber)关联起来,怎么关联呢 ?

WebRTC 传输的音视频数据,实际上是封装在 RTP 包里面,RTP 包头有个很重要的字段,叫做 ×××C(同步源标识),就是这路流的唯一标识,如图:

WebRTC 开发实践:如何实现 SFU 服务器_SFU_03

数据的生产者(Publisher)和数据的消费者(Subscriber)即可通过 ×××C 来关联,实现 one-to-many 的核心代码逻辑抽象如下:

WebRTC 开发实践:如何实现 SFU 服务器_SFU_04

即:当 SFU 接收到 Publisher 发送上来的数据后,轮询一下所有的 Subscribers,如果 ×××C 匹配成功,则将数据转发给这个客户端。

5 数据传输协议

WebRTC 采用的是标准的 RTP/RTCP 协议进行数据的封包和网络状态反馈,因此,SFU 服务器也需要支持 RTP/RTCP 的封包和解包,从而能够 “理解” 客户端的 UDP 数据包的含义,如:提取出 ×××C 或者 timestamp 等必要的信息,也能及时地向客户端反馈网络状态(RTCP)。

关于 RTP/RTCP 传输协议,已经发展多年,是比较成熟的多媒体传输协议了,也有很多不错的开源库,这里就不再赘述了。

6 小结

以上就是关于如何实现 SFU 服务器最核心的知识点了,暂且就分享到这里了,如有疑问的小伙伴欢迎来信 lujun.hust@gmail.com 交流。另外,也欢迎大家关注我的新浪微博 @卢_俊 或者 微信公众号 @Jhuster 获取最新的文章和资讯。

WebRTC 开发实践:如何实现 SFU 服务器_RTP_05