1. 概述
浏览器之间的通信通常依赖服务器中转消息,这种间接方式不可避免地带来延迟。而 WebRTC(Web Real-Time Communication)是一个开源项目,✅ 让浏览器和移动端应用能够直接建立实时通信,跳过中间服务器。
本文将带你从零实现一个基于 WebRTC 的点对点(P2P)数据传输应用。我们会用到:
- 前端:HTML + JavaScript,利用浏览器内置的 WebRTC API
- 信令服务器(Signaling Server):使用 Spring Boot 搭建 WebSocket 服务
最后还会扩展支持音视频流传输。⚠️ 注意:WebRTC 本身不定义信令协议,需要我们自行实现信令通道。
2. WebRTC 核心概念
传统浏览器通信流程如下:
- 浏览器 A 发消息给服务器
- 服务器转发给浏览器 B
这种“近实时”通信存在明显延迟。
而 WebRTC 的目标是建立直接连接,实现真正的实时通信:
消息直接在客户端之间传输,大幅降低延迟,同时减轻服务器带宽压力。
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 连接,必须完成以下步骤:
- 可发现性:让对方能找到自己
- 网络信息交换:获取对方的 IP、端口等
- 媒体协商:统一音视频编码格式、传输协议
- 数据传输
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) 完成协商:
- 发起方生成
offer
并发送给对方 - 接收方生成
answer
并返回 - 双方确认后,连接建立
这个过程通过信令服务器传递 SDP 描述。
7. 客户端初始化
创建 index.html
和 client.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.candidate
为 null
时,表示收集完成,无需再发送。
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,无法被外网直接访问。
解决方案:
- ✅ STUN:获取公网 IP 和端口
- ⚠️ 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)快速落地。