1. 概述

浏览器之间的通信通常依赖服务器中转消息,这种间接方式不可避免地带来延迟。而 WebRTC(Web Real-Time Communication)是一个开源项目,✅ 让浏览器和移动端应用能够直接建立实时通信,跳过中间服务器。

本文将带你从零实现一个基于 WebRTC 的点对点(P2P)数据传输应用。我们会用到:

  • 前端:HTML + JavaScript,利用浏览器内置的 WebRTC API
  • 信令服务器(Signaling Server):使用 Spring Boot 搭建 WebSocket 服务

最后还会扩展支持音视频流传输。⚠️ 注意:WebRTC 本身不定义信令协议,需要我们自行实现信令通道。

2. WebRTC 核心概念

传统浏览器通信流程如下:

  1. 浏览器 A 发消息给服务器
  2. 服务器转发给浏览器 B

这种“近实时”通信存在明显延迟。

webrtc 2 1

而 WebRTC 的目标是建立直接连接,实现真正的实时通信:

webrtc 2 3

消息直接在客户端之间传输,大幅降低延迟,同时减轻服务器带宽压力。

3. 浏览器支持与内置能力

主流浏览器(Chrome、Firefox、Edge、Safari)及 Android/iOS 均原生支持 WebRTC,无需安装插件。

更关键的是,WebRTC 在底层解决了音视频通信中的诸多复杂问题:

  • ✅ 丢包补偿(Packet-loss concealment)
  • ✅ 回声消除(Echo cancellation)
  • ✅ 带宽自适应(Bandwidth adaptivity)
  • ✅ 动态抖动缓冲(Dynamic jitter buffering)
  • ✅ 自动增益控制(Automatic gain control)
  • ✅ 降噪处理(Noise reduction)
  • ✅ 图像优化(Image cleaning)

这些能力开箱即用,极大简化了实时通信开发。

4. 点对点连接的挑战

P2P 连接与传统 C/S 模式不同:客户端彼此不知道对方的网络地址

要建立 P2P 连接,必须完成以下步骤:

  1. 可发现性:让对方能找到自己
  2. 网络信息交换:获取对方的 IP、端口等
  3. 媒体协商:统一音视频编码格式、传输协议
  4. 数据传输

WebRTC 提供了一套 API 来完成这些流程,其中关键环节是信令(Signaling)

5. 信令机制(Signaling)

信令负责:

  • 网络发现
  • 会话创建与管理
  • 媒体能力元数据交换

⚠️ WebRTC 不规定信令的具体实现,开发者可自由选择技术栈(WebSocket、HTTP、Socket 等)。

5.1. 构建信令服务器(Spring Boot)

我们使用 Spring Boot + WebSocket 实现信令服务器。

添加依赖:

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

配置 WebSocket 端口:

@Configuration
@EnableWebSocket
public class WebSocketConfiguration implements WebSocketConfigurer {

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

/socket 是客户端连接的接口地址。

5.2. 消息处理器

创建 SocketHandler 处理 WebSocket 消息:

@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);
    }
}

逻辑很简单:新连接加入列表,收到消息后广播给其他所有客户端(不包括自己)。这是最简化的信令转发模型。

6. 元数据交换(SDP 协商)

不同设备(如 Android Chrome 与 Mac Firefox)的音视频能力差异大,必须先协商一致。

WebRTC 使用 SDP(Session Description Protocol) 完成协商:

  1. 发起方生成 offer 并发送给对方
  2. 接收方生成 answer 并返回
  3. 双方确认后,连接建立

这个过程通过信令服务器传递 SDP 描述。

7. 客户端初始化

创建 index.htmlclient.js

连接信令服务器:

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);

configuration 用于配置 STUN/TURN 服务器,此处先设为 null

创建数据通道:

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 连接建立

ICE(Interactive Connectivity Establishment)协议负责穿透 NAT,建立真实连接。流程如下:

9.1. 创建 Offer

发起方创建 offer 并发送:

peerConnection.createOffer(function(offer) {
    send({
        event : "offer",
        data : offer
    });
    peerConnection.setLocalDescription(offer);
}, function(error) {
    // 错误处理
});

9.2. 处理 ICE Candidate

监听 ICE 候选地址:

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

event.candidatenull 时,表示收集完成,无需再发送。

9.3. 接收 ICE Candidate

接收方将候选地址加入连接:

peerConnection.addIceCandidate(new RTCIceCandidate(candidate));

9.4. 接收 Offer 并回复 Answer

接收方设置远端描述并生成 answer:

peerConnection.setRemoteDescription(new RTCSessionDescription(offer));
peerConnection.createAnswer(function(answer) {
    peerConnection.setLocalDescription(answer);
    send({
        event : "answer",
        data : answer
    });
}, function(error) {
    // 错误处理
});

9.5. 接收 Answer

发起方设置远端描述:

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

至此,P2P 连接建立成功 ✅。

10. 数据传输

连接建立后,可通过 dataChannel 直接通信:

dataChannel.send("message");

接收消息:

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

若作为接收方,需监听 ondatachannel 事件获取通道:

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

11. 添加音视频流

11.1. 获取媒体流

使用 getUserMedia 获取摄像头和麦克风:

const constraints = {
    video: true,
    audio: true
};
navigator.mediaDevices.getUserMedia(constraints)
  .then(function(stream) { /* 使用流 */ })
  .catch(function(err) { /* 错误处理 */ });

可指定详细参数:

var constraints = {
    video : {
        frameRate : { ideal : 10, max : 15 },
        width : 1280,
        height : 720,
        facingMode : "user" // "environment" 为后置摄像头
    }
};

11.2. 发送流

将流添加到连接:

peerConnection.addStream(stream);

11.3. 接收流

监听流事件并绑定到 video 标签:

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

12. NAT 穿透问题

大多数设备位于 NAT 后,只有内网 IP,无法被外网直接访问。

解决方案:

  1. STUN:获取公网 IP 和端口
  2. ⚠️ TURN:中继服务器(最后手段)

13. 使用 STUN 服务器

STUN 服务器返回客户端的公网地址:

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

将此配置传入 RTCPeerConnection 即可。

14. 使用 TURN 服务器

当 P2P 无法建立时,使用 TURN 中继:

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

⚠️ 注意:TURN 会经过服务器中转,消耗带宽,仅作为备用方案。

15. 总结

本文介绍了 WebRTC 的核心原理,并实现了:

  • 基于 WebSocket 的信令服务器(Spring Boot)
  • P2P 数据通道(RTCDataChannel)
  • 音视频流传输
  • STUN/TURN 穿透方案

完整代码已托管至 GitHub:https://github.com/yourname/webrtc-demo

WebRTC 虽强大,但信令设计、NAT 穿透、错误处理等仍是踩坑重灾区,建议结合成熟框架(如 PeerJS)快速落地。


原始标题:Guide to WebRTC