WebRTC指南

2023/05/12

1. 概述

当两个浏览器需要通信时,它们之间通常需要一个服务器来协调通信,在它们之间传递消息。但是在中间有一个服务器会导致浏览器之间的通信延迟。

在本教程中,我们将了解WebRTC,这是一个开源项目,它使浏览器和移动应用程序能够直接相互实时通信。然后,我们将通过编写一个创建点对点连接以在两个 HTML 客户端之间共享数据的简单应用程序来查看它的运行情况。

我们将使用 HTML、JavaScript 和 WebSocket 库以及 Web 浏览器中的内置 WebRTC 支持来构建客户端。而且,我们将使用Spring Boot构建一个信令服务器,使用 WebSocket 作为通信协议。最后,我们将看到如何将视频和音频流添加到此连接。

2. WebRTC的基础和概念

让我们看看两个浏览器在没有 WebRTC 的典型场景下是如何通信的。

假设我们有两个浏览器,浏览器 1需要向浏览器 2发送消息。浏览器 1首先将它发送到服务器:

WebRTC 2 1

Server收到消息后,进行处理,找到Browser 2,并向其发送消息:

WebRTC 2 2

由于服务器必须在将消息发送到浏览器 2 之前对其进行处理,因此通信几乎是实时进行的。当然,我们希望它是 实时的。

WebRTC 通过在两个浏览器之间创建一个直接通道解决了这个问题,消除了对服务器的需求:

WebRTC 2 3

结果,将消息从一个浏览器传递到另一个浏览器所需的时间大大减少,因为消息现在直接从发送者路由到接收者。它还消除了服务器带来的繁重工作和带宽,并使其在相关客户端之间共享。

3. 支持 WebRTC 和内置功能

WebRTC 受到 Chrome、Firefox、Opera 和 Microsoft Edge 等主要浏览器以及 Android 和 iOS 等平台的支持。

WebRTC 不需要在我们的浏览器中安装任何外部插件,因为该解决方案与浏览器捆绑在一起,开箱即用。

此外,在一个典型的涉及视频和音频传输的实时应用中,我们不得不严重依赖C++库,我们必须处理很多问题,包括:

  • 丢包隐藏
  • 回声消除
  • 带宽适应性
  • 动态抖动缓冲
  • 自动增益控制
  • 降噪和抑制
  • 图片“清洁”

但是WebRTC 在后台处理所有这些问题,使客户端之间的实时通信变得更加简单。

4.点对点连接

与客户端-服务器通信不同,服务器有一个已知地址,并且客户端已经知道要与之通信的服务器的地址,在 P2P(点对点)连接中,没有一个点有直接地址给另一个同行。

要建立点对点连接,需要几个步骤来允许客户端:

  • 让自己可以进行交流
  • 相互识别并共享网络相关信息
  • 共享并同意所涉及的数据格式、模式和协议
  • 共享数据

WebRTC 定义了一组用于执行这些步骤的 API 和方法。

为了让客户端相互发现、共享网络细节,然后共享数据格式,WebRTC 使用了一种称为信令的机制。

5.信令

信令是指涉及网络发现、会话创建、会话管理和交换媒体功能元数据的过程。

这是必不可少的,因为客户需要预先了解对方才能启动通信。

为了实现所有这些,WebRTC 没有指定信令标准,而是将其留给开发人员实现。因此,这为我们提供了在具有任何技术和支持协议的一系列设备上使用 WebRTC 的灵活性。

5.1. 构建信令服务器

对于信令服务器,我们将使用Spring Boot构建一个 WebSocket 服务器。我们可以从Spring Initializr生成的空Spring Boot项目开始。

要将 WebSocket 用于我们的实现,让我们将依赖项添加到我们的pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-websocket</artifactId>
    <version>2.4.0</version>
</dependency>

我们总能从Maven Central找到要使用的最新版本。

信令服务器的实现很简单——我们将创建一个客户端应用程序可以用来注册为 WebSocket 连接的端点。

要在Spring Boot中执行此操作,让我们编写一个@Configuration类来扩展WebSocketConfigurer并覆盖registerWebSocketHandlers方法:

@Configuration
@EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(new SocketHandler(), "/socket")
          .setAllowedOrigins("");
    }
}

请注意,我们已将/socket标识为我们将从下一步构建的客户端注册的 URL。我们还将SocketHandler作为参数传递给addHandler方法——这实际上是我们接下来要创建的消息处理程序。

5.2. 在信令服务器中创建消息处理程序

下一步是创建一个消息处理程序来处理我们将从多个客户端接收的 WebSocket 消息。

这对于帮助不同客户端之间交换元数据以建立直接的 WebRTC 连接至关重要。

在这里,为了简单起见,当我们收到来自客户端的消息时,我们会将其发送给除自身以外的所有其他客户端。

为此,我们可以从 Spring WebSocket 库扩展TextWebSocketHandler并覆盖handleTextMessage和 afterConnectionEstablished方法:

@Component
public class SocketHandler extends TextWebSocketHandler {

    List<WebSocketSession>sessions = new CopyOnWriteArrayList<>();

    @Override
    public void handleTextMessage(WebSocketSession session, TextMessage message)
      throws InterruptedException, IOException {
        for (WebSocketSession webSocketSession : sessions) {
            if (webSocketSession.isOpen() && !session.getId().equals(webSocketSession.getId())) {
                webSocketSession.sendMessage(message);
            }
        }
    }

    @Override
    public void afterConnectionEstablished(WebSocketSession session) throws Exception {
        sessions.add(session);
    }
}

正如我们在afterConnectionEstablished 方法中看到的那样,我们将接收到的会话添加到会话列表中,以便我们可以跟踪所有客户端。

当我们收到来自任何客户端的消息时,如handleTextMessage 中所示,我们遍历列表中的所有客户端会话,并通过比较发送者的会话 ID 和列表中的会话。

6. 交换元数据

在 P2P 连接中,客户端之间可能非常不同。例如,Android 上的 Chrome 可以连接到 Mac 上的 Mozilla。

因此,这些设备的媒体功能可能差异很大。因此,对等方之间的握手必须就用于通信的媒体类型和编解码器达成一致。

在此阶段,WebRTC 使用 SDP(会话描述协议)在客户端之间就元数据达成一致。

为实现这一点,发起对等点创建一个必须由另一个对等点设置为远程描述符的提议。此外,另一个对等端随后生成一个应答,该应答被发起对等端接受为远程描述符。

当这个过程完成时,连接就建立了。

7. 设置客户端

让我们创建我们的 WebRTC 客户端,这样它既可以充当发起端,也可以充当远程端。

我们将首先创建一个名为index.html的 HTML 文件和一个名为client.js 的 JavaScript 文件, index.html 将使用该文件。

为了连接到我们的信令服务器,我们创建了一个 WebSocket 连接。假设我们构建的Spring Boot信令服务器运行在http://localhost:8080上,我们可以创建连接:

var conn = new WebSocket('ws://localhost:8080/socket');

要向信令服务器发送消息,我们将创建一个发送方法,用于在接下来的步骤中传递消息:

function send(message) {
    conn.send(JSON.stringify(message));
}

8. 设置一个 简单的RTCDataChannel

在client.js中设置客户端后,我们需要为RTCPeerConnection类创建一个对象:

configuration = null;
var peerConnection = new RTCPeerConnection(configuration);

在此示例中,配置对象的目的是传入 STUN(NAT 会话遍历实用程序)和 TURN(使用中继遍历 NAT)服务器以及我们将在本教程后面部分讨论的其他配置。对于此示例,传入null就足够了。

现在,我们可以创建一个dataChannel用于消息传递:

var dataChannel = peerConnection.createDataChannel("dataChannel", { reliable: true });

随后,我们可以为数据通道上的各种事件创建监听器:

dataChannel.onerror = function(error) {
    console.log("Error:", error);
};
dataChannel.onclose = function() {
    console.log("Data channel is closed");
};

9. 与 ICE 建立连接

建立 WebRTC 连接的下一步涉及 ICE(交互式连接建立)和 SDP 协议,其中对等点的会话描述在两个对等点被交换和接受。

信令服务器用于在对等点之间发送此信息。这涉及客户端通过信令服务器交换连接元数据的一系列步骤。

9.1. 创建报价

首先,我们创建一个报价并将其设置为peerConnection的本地描述。然后我们将报价发送给另一个同行:

peerConnection.createOffer(function(offer) {
    send({
        event : "offer",
        data : offer
    });
    peerConnection.setLocalDescription(offer);
}, function(error) {
    // Handle error here
});

此处,发送方法调用信令服务器以传递报价信息。

请注意,我们可以使用任何服务器端技术自由实现发送方法的逻辑。

9.2. 处理 ICE 候选人

其次,我们需要处理 ICE 候选人。 WebRTC 使用 ICE(交互式连接建立)协议来发现对等点并建立连接。

当我们在peerConnection上设置本地描述时,它会触发一个icecandidate事件。

此事件应将候选者传输到远程对等点,以便远程对等点可以将其添加到其远程候选者集中。

为此,我们为onicecandidate事件创建一个侦听器:

peerConnection.onicecandidate = function(event) {
    if (event.candidate) {
        send({
            event : "candidate",
            data : event.candidate
        });
    }
};

当收集到所有候选者时, icecandidate事件再次触发,候选字符串为空。

我们还必须将这个候选对象传递给远程对等方。我们传递这个空的候选字符串以确保远程对等方知道所有icecandidate对象都已收集。

此外,再次触发相同的事件以指示 ICE 候选对象收集已完成,候选对象的值在该事件中设置为null 。这不需要传递给远程对等点。

9.3. 接收 ICE 候选人

第三,我们需要处理其他节点发送的 ICE 候选。

远程对等方在收到此候选人后,应将其添加到其候选人池中:

peerConnection.addIceCandidate(new RTCIceCandidate(candidate));

9.4. 收到报价

之后,当其他对等方收到报价时,必须将其设置为远程描述。此外,它必须生成一个answer,发送给发起方:

peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
peerConnection.createAnswer(function(answer) {
    peerConnection.setLocalDescription(answer);
        send({
            event : "answer",
            data : answer
        });
}, function(error) {
    // Handle error here
});

9.5. 收到答复

最后,发起对等点收到答案并将其设置为远程描述:

handleAnswer(answer){
    peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
}

这样,WebRTC 就建立了一个成功的连接。

现在,我们可以直接在两个对等点之间发送和接收数据,而无需信令服务器。

10.发送消息

现在我们已经建立了连接,我们可以使用dataChannel的send方法在对等点之间发送消息:

dataChannel.send(message);

同样,要在另一个节点上接收消息,让我们为onmessage事件创建一个侦听器:

dataChannel.onmessage = function(event) {
    console.log("Message:", event.data);
};

要在数据通道上接收消息,我们还必须在peerConnection对象上添加一个回调:

peerConnection.ondatachannel = function (event) {
    dataChannel = event.channel;
};

通过这一步,我们创建了一个功能齐全的 WebRTC 数据通道。我们现在可以在客户端之间发送和接收数据。此外,我们可以为此添加视频和音频频道。

11.添加视频和音频通道

当WebRTC建立P2P连接后,我们可以方便的直接传输音视频流。

11.1. 获取媒体流

首先,我们需要从浏览器获取媒体流。WebRTC为此提供了一个API:

const constraints = {
    video: true,audio : true
};
navigator.mediaDevices.getUserMedia(constraints).
  then(function(stream) { / use the stream / })
    .catch(function(err) { / handle the error / });

我们可以使用约束对象指定视频的帧速率、宽度和高度。

约束对象还允许指定在移动设备的情况下使用的相机:

var constraints = {
    video : {
        frameRate : {
            ideal : 10,
            max : 15
        },
        width : 1280,
        height : 720,
        facingMode : "user"
    }
};

此外,如果我们想要启用后置摄像头,可以将facingMode的值设置为“environment”而不是“user” 。

11.2. 发送流

其次,我们必须将流添加到 WebRTC 对等连接对象:

peerConnection.addStream(stream);

将流添加到对等连接会触发已连接对等点上的addstream事件。

11.3. 接收流

第三,为了接收远程端的流,我们可以创建一个监听器。

让我们将此流设置为 HTML 视频元素:

peerConnection.onaddstream = function(event) {
    videoElement.srcObject = event.stream;
};

12.NAT问题

在现实世界中,防火墙和 NAT(网络地址遍历)设备将我们的设备连接到公共互联网。

NAT 为设备提供一个 IP 地址以供在本地网络中使用。所以,这个地址在本地网络之外是不可访问的。没有公共地址,同行无法与我们沟通。

为了解决这个问题,WebRTC 使用了两种机制:

  1. 眩晕
  2. 转动

13. 使用STUN

STUN 是解决此问题的最简单方法。在与对等方共享网络信息之前,客户端向 STUN 服务器发出请求。STUN 服务器的职责是返回它收到请求的 IP 地址。

因此,通过查询 STUN 服务器,我们可以获得自己的面向公众的 IP 地址。然后,我们将此 IP 和端口信息共享给我们要连接的对等方。其他同行可以做同样的事情来共享他们面向公众的 IP。

要使用 STUN 服务器,我们可以简单地在配置对象中传递 URL 以创建RTCPeerConnection对象:

var configuration = {
    "iceServers" : [ {
        "url" : "stun:stun2.1.google.com:19302"
    } ]
};

14. 使用转

相比之下,TURN 是一种在 WebRTC 无法建立 P2P 连接时使用的回退机制。TURN 服务器的作用是直接在对等点之间中继数据。在这种情况下,实际的数据流流经 TURN 服务器。使用默认实现,TURN 服务器也充当 STUN 服务器。

TURN 服务器是公开可用的,即使在防火墙或代理后面,客户端也可以访问它们。

但是,使用 TURN 服务器并不是真正的 P2P 连接,因为存在中间服务器。

注意:当我们无法建立 P2P 连接时,TURN 是最后的手段。当数据流经 TURN 服务器时,它需要大量带宽,在这种情况下我们不使用 P2P。

与 STUN 类似,我们可以在同一个配置对象中提供 TURN 服务器 URL:

{
  'iceServers': [
    {
      'urls': 'stun:stun.l.google.com:19302'
    },
    {
      'urls': 'turn:10.158.29.39:3478?transport=udp',
      'credential': 'XXXXXXXXXXXXX',
      'username': 'XXXXXXXXXXXXXXX'
    },
    {
      'urls': 'turn:10.158.29.39:3478?transport=tcp',
      'credential': 'XXXXXXXXXXXXX',
      'username': 'XXXXXXXXXXXXXXX'
    }
  ]
}

15.总结

在本教程中,我们讨论了什么是 WebRTC 项目并介绍了它的基本概念。然后我们构建了一个简单的应用程序来在两个 HTML 客户端之间共享数据。

我们还讨论了创建和建立 WebRTC 连接所涉及的步骤。

此外,我们研究了在 WebRTC 失败时使用 STUN 和 TURN 服务器作为后备机制。

与往常一样,本教程的完整源代码可在GitHub上获得。

Show Disqus Comments

Post Directory

扫码关注公众号:Taketoday
发送 290992
即可立即永久解锁本站全部文章