跳到主要内容WebRTC + Java 信令服务器 + Vue 实时视频聊天实现 | 极客日志Java大前端java
WebRTC + Java 信令服务器 + Vue 实时视频聊天实现
通过一个完整项目,详解基于 WebRTC、Java(SpringBoot + Netty)和 Vue 的点对点视频聊天实现。内容涵盖 WebRTC 核心概念(SDP、ICE、STUN)、信令服务器架构设计、后端 Netty WebSocket 服务搭建以及前端 Vue 交互逻辑。旨在帮助开发者理解实时音视频通信原理,掌握从信令交换到媒体流传输的全流程开发。
魔尊29 浏览 WebRTC + Java 信令服务器 + Vue 实时视频聊天实现
很多同学用过微信、QQ 视频聊天,但一问到底层怎么实现,十有八九只会说一句:"应该是 WebSocket / WebRTC 吧?"——但是:
- WebRTC 到底负责什么?
- WebSocket / Netty 在里面干嘛?
- STUN / ICE / SDP 是啥?为什么一上来就一堆名词?
这篇文章会用一套完整的小项目,从 0 到 1 带你实现一个:
基于 WebRTC + Java(SpringBoot + Netty)+ Vue 的点对点视频聊天 Demo
重点是:
不是只给你一堆代码,而是把每个概念都讲清楚,让小白也能看懂、改得动、举一反三。
一、整体架构总览:谁负责干什么?
先看一张逻辑图(可以脑补成 PPT):
┌────────────────────────────────────────────┐
│ 后端(Java) │
│ ├─ SpringBoot 负责: │
│ │ - 启动项目、整合 Netty、管理 Bean │
│ └─ Netty WebSocket 负责: │
│ - 管理 WebSocket 长连接 │
│ - 维护'用户 ID ⇔ Channel'映射 │
│ - 转发信令:register / call / answer / ice│
└────────────────────────────────────────────┘
▲ ▲
│ WebSocket 信令
▼ ▼
┌────────────────────────┐ ┌────────────────────────┐
│ 前端 A(浏览器,Vue) │ │ 前端 B(浏览器,Vue) │
│ - 采集本地音视频 │ │ - 采集本地音视频 │
│ - WebRTC 建立 P2P │ │ - WebRTC 建立 P2P │
│ - 通过 WebSocket 发信令 │ │ - 通过 WebSocket 发信令 │
└────────────────────────┘ └────────────────────────┘
▲ ▲
│ WebRTC P2P 音视频流(真正传输媒体)
▼ ▼
一句话概括:
- WebRTC:浏览器内置的实时音视频引擎,负责采集 + 编码 + 传输音视频。
- Netty + WebSocket:负责信令交换(谁给谁打电话、会话参数、网络地址)。
- SpringBoot:把 Netty 集成在 Java 项目中,提供统一的启动入口。
- Vue 前端:提供 UI、调用 WebRTC API,并与后端 WebSocket 交互。
二、WebRTC 必备概念扫盲(面向小白)
2.1 WebRTC 是什么?
WebRTC(Web Real-Time Communication):
浏览器原生支持的实时音视频通信技术,特点:
- 无需安装插件 / 客户端,直接在浏览器里用 JS 调用。
- 可以实现:音视频通话、屏幕共享、文件传输等。
- 重点:浏览器之间可以点对点(P2P)传输音视频数据。
2.2 为什么还需要 WebSocket / 信令服务器?
WebRTC 只负责媒体通道,但它不负责:
- 谁给谁打电话?
- 打电话时双方用什么音视频参数(SDP)?
- 双方的网络地址(ICE 候选)怎么互相知道?
这些'撮合、协商、交换信息'的过程,就叫做**'信令(Signaling)'**。
我们通常用 WebSocket + 后端服务器 来做信令通道。
可以把 WebRTC 想成'电话线 + 麦克风 + 摄像头',
WebSocket/Netty 相当于'中间的红娘 + 前台接线员'。
2.3 ICE / STUN / TURN 是什么?
2.3.1 NAT 问题:为什么两台电脑找不到彼此?
日常网络中,绝大部分设备使用的都是内网 IP(如 192.168.x.x),对外通过路由器共享一个公网 IP。
A 和 B 都在各自的内网里,它们只知道自己的内网 IP,
但对方的内网 IP 在自己的网络范围外根本访问不到。
- 你知道自己在'小区 1:1 栋 101'
- 对方在'小区 2:3 栋 602'
- 但你只知道'101'和'602',不知道两个小区的具体地址和大门。
2.3.2 STUN:帮你查'你在互联网眼中的地址'
STUN(Session Traversal Utilities for NAT) 是一个网络服务,它做的事情很简单:
- 设备 A(在内网)给 STUN 服务器发请求:'你看到的我的 IP + 端口是什么?'
- STUN 服务器返回:'我看到你的公网 IP 是 X.X.X.X,端口是 YYY'。
- 设备 A 就知道了'在互联网眼中的自己'的地址。
同理,设备 B 也这么干一遍。
然后二者通过信令服务器互相交换这个'公网地址',就可以尝试点对点打洞通信。
2.3.3 ICE:一整套'试探连接'的框架
ICE(Interactive Connectivity Establishment) 是 WebRTC 中用于 NAT 穿透的一个框架 / 策略集合:
- 它会收集各种可能的地址(候选者 Candidate):
- 本地地址(内网 IP)
- 通过 STUN 获得的服务器反射地址(公网 IP + 端口)
- 通过 TURN 中继获得的中转地址(如果有 TURN)
- 然后双方互相交换这些地址,一边试一边连,看哪条路径能打通。
const iceServers = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun.qq.com:3478' },
{ urls: 'stun:stun.aliyun.com:3478' }
]
};
这里的 iceServers 就是传给 RTCPeerConnection 的配置,告诉它:
'你做 ICE 穿透时,可以去这些 STUN 服务器问路。'
2.3.4 TURN:兜底方案(本教程暂不实现)
当各种 NAT 太恶心、P2P 打洞失败时,就只能退而求其次,让媒体流通过 TURN 服务器中转。
- 好处:几乎能保证连通。
- 缺点:带宽压力巨大,成本高,一般用于商业产品。
本教程的 Demo 为了简单,只使用 STUN,不涉及 TURN。
三、整体授课/学习流程设计(给老师也给自学者)
3.1 步骤总览
- 后端:基于 SpringBoot + Netty 搭建 WebSocket 信令服务器。
- 前端:基于 Vue 3 实现:
- WebSocket 连接与信令发送
- WebRTC
RTCPeerConnection 建立 P2P
- 本地 / 远程视频展示 + 挂断功能
- 联调:本机开 2 个浏览器窗口(或不同浏览器),实现 A ↔ B 视频通话。
四、后端服务端开发(一):SpringBoot 项目与依赖配置
4.1 Maven 依赖(pom.xml 关键片段)
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.94.Final</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.25</version>
</dependency>
</dependencies>
- SpringBoot 负责项目的整体启动与 Bean 管理。
- Netty 用来开启一个 WebSocket 服务端,监听如
ws://localhost:8081/ws。
- FastJSON 用来把前端发来的 JSON 文本转成 Java 对象
Message,也能把对象转回 JSON。
五、后端开发(二):信令消息实体类 Message
5.1 为什么需要消息实体类?
在 WebRTC 信令交互中,我们需要传递的信息包括:
- 消息类型:
register(注册)、call(呼叫)、answer(应答)、ice(候选)
- 发送方 ID:
from
- 接收方 ID:
to
- 具体内容:
data(比如 SDP、ICE 候选的 JSON 字符串)
5.2 Message.java 示例实现(带详细注释)
package com.qcby.springboot.entity;
public class Message {
private String type;
private String from;
private String to;
private String data;
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getFrom() {
return from;
}
public void setFrom(String from) {
this.from = from;
}
public String getTo() {
return to;
}
public void setTo(String to) {
this.to = to;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
@Override
public String toString() {
return "Message{" +
"type='" + type + '\'' +
", from='" + from + '\'' +
", to='" + to + '\'' +
", data='" + data + '\'' +
'}';
}
}
六、后端开发(三):Netty WebSocket 处理器 WebSocketHandler
6.1 职责说明
WebSocketHandler 是整个服务端的核心类,主要负责:
- 管理用户 WebSocket 连接(一个用户对应一个 Netty Channel)。
- 接收前端发来的信令消息(JSON 文本)。
- 按消息类型进行分发处理:
register:记录用户 ID 与 Channel 的映射。
call / answer / ice:根据 to 字段找到目标用户 Channel,转发消息。
- 处理连接断开 / 异常等情况。
6.2 核心代码实现(附详细注释)
package com.qcby.springboot.handler;
import com.alibaba.fastjson.JSON;
import com.qcby.springboot.entity.Message;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import org.springframework.context.annotation.Configuration;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Configuration
@ChannelHandler.Sharable
public class WebSocketHandler extends SimpleChannelInboundHandler<TextWebSocketFrame> {
public static final ConcurrentHashMap<String, Channel> USER_CHANNEL_MAP = new ConcurrentHashMap<>();
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与客户端建立连接,通道开启:" + ctx.channel().id());
}
@Override
protected void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg) throws Exception {
String text = msg.text();
System.out.println("收到原始消息文本:" + text);
Message message = JSON.parseObject(text, Message.class);
System.out.println("解析后的 Message 对象:" + message);
switch (message.getType()) {
case "register":
handleRegister(message, ctx.channel());
break;
case "call":
case "answer":
case "ice":
handleForward(message, text);
break;
default:
System.out.println("未知消息类型:" + message.getType());
}
}
private void handleRegister(Message message, Channel channel) {
String userId = message.getFrom();
if (userId == null || userId.trim().isEmpty()) {
System.out.println("注册失败:userId 为空");
return;
}
System.out.println("用户 " + userId + " 注册成功,ChannelId=" + channel.id());
}
private void handleForward(Message message, String rawText) {
String targetUserId = message.getTo();
if (targetUserId == null || targetUserId.trim().isEmpty()) {
System.out.println("转发失败:目标用户 ID 为空");
return;
}
Channel targetChannel = USER_CHANNEL_MAP.get(targetUserId);
if (targetChannel != null && targetChannel.isActive()) {
targetChannel.writeAndFlush(new TextWebSocketFrame(rawText));
System.out.println("已将消息转发给用户:" + targetUserId);
} else {
System.out.println("用户 " + targetUserId + " 不在线或 Channel 不可用");
}
}
@Override
public void channelInactive(ChannelHandlerContext ctx) throws Exception {
System.out.println("与客户端断开连接,通道关闭:" + ctx.channel().id());
USER_CHANNEL_MAP.entrySet().removeIf(entry -> entry.getValue().id().equals(ctx.channel().id()));
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
System.out.println("连接异常:" + cause.getMessage());
USER_CHANNEL_MAP.entrySet().removeIf(entry -> entry.getValue() == ctx.channel());
ctx.close();
}
}
七、后端开发(四):Netty WebSocket 服务启动类
7.1 设计目标
- 在 SpringBoot 项目启动时,自动启动一个 Netty WebSocket 服务器。
- 监听端口(如
8004),对外提供 ws://localhost:8004/ws。
- 配置 Netty 的 ChannelPipeline:
- HTTP 编解码
- WebSocket 协议处理
- 自定义
WebSocketHandler
7.2 核心代码实现
package com.qcby.springboot.commun;
import com.qcby.springboot.handler.WebSocketHandler;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.websocketx.WebSocketServerProtocolHandler;
import io.netty.handler.stream.ChunkedWriteHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
@Configuration
public class NettyWebSocketServer {
@Autowired
private WebSocketHandler webSocketHandler;
public void start() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup workerGroup = new NioEventLoopGroup();
try {
ServerBootstrap serverBootstrap = new ServerBootstrap();
serverBootstrap
.option(ChannelOption.SO_BACKLOG, 1024)
.group(bossGroup, workerGroup)
.channel(NioServerSocketChannel.class)
.localAddress(8004)
.childHandler(new ChannelInitializer<SocketChannel>() {
@Override
protected void initChannel(SocketChannel ch) {
ch.pipeline().addLast(new HttpServerCodec());
ch.pipeline().addLast(new ChunkedWriteHandler());
ch.pipeline().addLast(new HttpObjectAggregator(8192));
ch.pipeline().addLast(new WebSocketServerProtocolHandler("/ws", "WebSocket", true, 65536 * 10));
ch.pipeline().addLast(webSocketHandler);
}
});
ChannelFuture channelFuture = serverBootstrap.bind().sync();
System.out.println("Netty WebSocket 服务器启动成功,监听端口:8004");
channelFuture.channel().closeFuture().sync();
} finally {
workerGroup.shutdownGracefully().sync();
bossGroup.shutdownGracefully().sync();
}
}
}
八、后端开发(五):SpringBoot 启动类集成 Netty
8.1 通过 CommandLineRunner 启动 Netty
SpringBoot 提供了 CommandLineRunner 接口,当应用启动完成后,会自动调用其中的 run() 方法,非常适合我们在其中启动 Netty。
package com.qcby.springboot;
import com.qcby.springboot.commun.NettyWebSocketServer;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.qcby.springboot.dao")
public class Application implements CommandLineRunner {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Autowired
private NettyWebSocketServer nettyServer;
@Override
public void run(String... args) throws Exception {
nettyServer.start();
}
}
- 启动 SpringBoot 项目 → 自动启动 Netty WebSocket。
- 前端可以通过:
ws://localhost:8004/ws 建立连接。
- 信令消息通过 JSON 文本传输,由
WebSocketHandler 转发。
九、前端:基于 Vue3 的 WebRTC 视频聊天页面
9.1 页面结构与样式(template + style)
- 用户 ID 输入与连接服务器按钮。
- 目标用户 ID 输入与'发起视频呼叫'按钮。
- 本地 / 远程视频展示区域。
- 基本的 UI 美化。
<template>
<div>
<h2>WebRTC 视频聊天</h2>
<div>
<input v-model="userId" placeholder="输入你的用户 ID(如 user1)" type="text" />
<button @click="connect">连接服务器</button>
</div>
<div v-if="socketConnected">
<input v-model="targetUserId" placeholder="输入对方用户 ID(如 user2)" type="text" />
<button @click="call">发起视频呼叫</button>
</div>
<div v-if="socketConnected">
<button @click="hangUp">挂断通话</button>
</div>
<div>
<div>
<p>本地视频</p>
<video ref="localVideo" autoplay muted></video>
</div>
<div>
<p>远程视频</p>
<video ref="remoteVideo" autoplay></video>
</div>
</div>
</div>
</template>
<style scoped>
.container {
width: 900px;
margin: 20px auto;
text-align: center;
font-family: "Microsoft YaHei", sans-serif;
}
.input-group {
margin: 25px 0;
}
input {
padding: 10px 15px;
width: 220px;
margin-right: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
button {
padding: 10px 20px;
background: #42b983;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
button:hover {
background: #359469;
}
.video-container {
display: flex;
justify-content: center;
gap: 30px;
margin-top: 30px;
}
.video-item {
: center;
}
{
: ;
: ;
: ;
}
{
: ;
: ;
: solid ;
: ;
: ;
}
</style>
十、前端脚本(script setup):WebSocket + WebRTC 全流程
10.1 基础变量与依赖导入
<script setup>
import { ref, onUnmounted } from 'vue';
const userId = ref('');
const targetUserId = ref('');
const socketConnected = ref(false);
const localVideo = ref(null);
const remoteVideo = ref(null);
let socket = null;
let peerConnection = null;
let localStream = null;
const iceServers = {
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{ urls: 'stun:stun.qq.com:3478' },
{ urls: 'stun:stun.aliyun.com:3478' }
]
};
</script>
10.2 WebSocket 连接与消息发送
<script setup>
const sendMessage = (message) => {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(message));
console.log('📤 发送消息:', message);
} else {
console.error('❌ WebSocket 未连接,无法发送消息');
alert('未连接服务器,请先点击'连接服务器'!');
}
};
const connect = () => {
if (!userId.value.trim()) {
alert('请输入你的用户 ID!(不能为空)');
return;
}
socket = new WebSocket('ws://localhost:8004/ws');
socket.onopen = () => {
console.log('✅ WebSocket 连接成功');
socketConnected.value = true;
sendMessage({ type: 'register', from: userId.value, to: '', data: '' });
};
socket.onmessage = (event) => {
try {
const message = JSON.parse(event.data);
console.log('📥 收到服务端消息:', message);
handleMessage(message);
} catch (err) {
console.error('❌ 消息解析失败:', err);
}
};
socket.onclose = () => {
console.log('❌ WebSocket 连接关闭');
socketConnected.value = false;
};
socket.onerror = (err) => {
console.error('❌ WebSocket 连接出错:', err);
socketConnected.value = false;
};
};
</script>
10.3 处理收到的信令消息
<script setup>
const handleMessage = async (message) => {
switch (message.type) {
case 'call':
await answerCall(message);
break;
case 'answer':
await setRemoteSDP(message.data);
break;
case 'ice':
await addIceCandidate(message.data);
break;
default:
console.log('📌 未处理的消息类型:', message.type);
}
};
</script>
10.4 WebRTC 核心:PeerConnection 初始化与事件绑定
<script setup>
const initPeerConnection = async () => {
if (peerConnection) {
peerConnection.close();
}
peerConnection = new RTCPeerConnection(iceServers);
peerConnection.onicecandidate = (event) => {
if (event.candidate) {
sendMessage({
type: 'ice',
from: userId.value,
to: targetUserId.value,
data: JSON.stringify(event.candidate)
});
}
};
peerConnection.ontrack = (event) => {
remoteVideo.value.srcObject = event.streams[0];
console.log('🎥 收到远程音视频流');
};
};
</script>
10.5 发起呼叫(Caller 端逻辑)
<script setup>
const call = async () => {
if (!targetUserId.value.trim()) {
alert('请输入对方用户 ID!');
return;
}
try {
await initPeerConnection();
localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
localVideo.value.srcObject = localStream;
localStream.getTracks().forEach((track) => {
peerConnection.addTrack(track, localStream);
});
const offer = await peerConnection.createOffer();
await peerConnection.setLocalDescription(offer);
sendMessage({
type: 'call',
from: userId.value,
to: targetUserId.value,
data: JSON.stringify(offer)
});
console.log('📞 已向用户发起呼叫:', targetUserId.value);
} catch (err) {
console.error('❌ 发起呼叫失败:', err);
alert('发起呼叫失败,请检查摄像头/麦克风权限或网络环境');
}
};
</script>
10.6 接听呼叫(Callee 端逻辑)
<script setup>
const answerCall = async (message) => {
targetUserId.value = message.from;
try {
await initPeerConnection();
localStream = await navigator.mediaDevices.getUserMedia({ video: true, audio: true });
localVideo.value.srcObject = localStream;
localStream.getTracks().forEach((track) => {
peerConnection.addTrack(track, localStream);
});
await peerConnection.setRemoteDescription(JSON.parse(message.data));
const answer = await peerConnection.createAnswer();
await peerConnection.setLocalDescription(answer);
sendMessage({
type: 'answer',
from: userId.value,
to: targetUserId.value,
data: JSON.stringify(answer)
});
console.log('📞 已应答视频呼叫:', targetUserId.value);
} catch (err) {
console.error('❌ 应答呼叫失败:', err);
alert('应答呼叫失败!');
}
};
</script>
10.7 设置远程 SDP 与添加 ICE 候选
<script setup>
const setRemoteSDP = async (sdpStr) => {
try {
const sdp = JSON.parse(sdpStr);
await peerConnection.setRemoteDescription(new RTCSessionDescription(sdp));
console.log('✅ 设置远程 SDP 成功');
} catch (err) {
console.error('❌ 设置远程 SDP 失败:', err);
}
};
const addIceCandidate = async (iceStr) => {
try {
const ice = JSON.parse(iceStr);
await peerConnection.addIceCandidate(new RTCIceCandidate(ice));
console.log('✅ 添加 ICE 候选成功');
} catch (err) {
console.error('❌ 添加 ICE 候选失败:', err);
}
};
</script>
10.8 挂断通话与资源清理
<script setup>
const hangUp = () => {
if (localStream) {
localStream.getTracks().forEach((track) => track.stop());
localVideo.value.srcObject = null;
localStream = null;
}
if (remoteVideo.value) {
remoteVideo.value.srcObject = null;
}
if (peerConnection) {
peerConnection.close();
peerConnection = null;
}
targetUserId.value = '';
console.log('📞 已挂断通话');
alert('已挂断通话!');
};
onUnmounted(() => {
if (socket) {
socket.close();
console.log('🔌 关闭 WebSocket 连接');
}
if (peerConnection) {
peerConnection.close();
console.log('🔌 关闭 PeerConnection');
}
if (localStream) {
localStream.getTracks().forEach((track) => track.stop());
console.log('🔇 停止本地音视频流');
}
});
</script>
十一、联调与测试步骤
- 启动后端 SpringBoot 项目(确保 Netty WebSocket 端口如
8004 已监听)。
- 启动前端 Vue 项目,打开浏览器访问页面。
- 打开两个浏览器标签页(或两个不同浏览器):
- 页面 A:输入
user1,点击'连接服务器'。
- 页面 B:输入
user2,点击'连接服务器'。
- 在 A 页面输入对方 ID
user2,点击'发起视频呼叫'。
- 浏览器会弹出摄像头/麦克风权限授权,点击允许。
- 正常情况下,两边都能看到:
十二、常见问题与排查思路
12.1 两边都连上了 WebSocket,但看不到视频?
- 浏览器拒绝了摄像头 / 麦克风权限 → 重新授权。
- STUN 服务器被墙或不可达 → 尝试换其他 STUN 地址,或在局域网内先不依赖 STUN。
- 前后端端口、路径不一致 → 检查:
- Netty 是否监听
8004。
- 前端 WebSocket 地址是否为
ws://localhost:8004/ws。
12.2 控制台报 SDP / ICE 相关错误?
setRemoteDescription called in wrong state:
- 通常是 SDP 设置顺序有误,需要先
setRemoteDescription 再 createAnswer。
addIceCandidate failed:
- 可能是 SDP 还没设置好就添加 ICE,需要确保先完成 SDP 的 setRemoteDescription。
十三、常见面试题与参考答案(围绕本项目)
问题 1:WebRTC 和 WebSocket 有什么区别?在这个项目里分别做什么?
- WebRTC 是浏览器原生提供的实时音视频通信技术,重点在于:
- 采集本地音视频。
- 编解码媒体数据。
- 建立点对点(P2P)媒体通道。
- WebSocket 是一种全双工的长连接通信协议,通常跑在 TCP 之上,用于:
- 在本项目中:
- WebRTC 负责在两个浏览器之间建立 P2P 连接,传输音视频流。
- WebSocket + Netty 负责信令交换(register / call / answer / ice),相当于为 WebRTC 做'牵线搭桥'和'会话协商'。
问题 2:什么是 ICE / STUN / TURN?为什么需要它们?
- 由于大多数用户在 NAT(如家用路由器)后面,只拥有内网 IP,导致两台设备很难直接互相访问。
- STUN:Session Traversal Utilities for NAT
- 一种轻量级的网络服务,用于让设备知道自己在互联网眼中的'外网 IP + 端口'。
- ICE:Interactive Connectivity Establishment
- 一整套用于 NAT 穿透的框架,会综合尝试各种候选地址(本地、STUN、TURN),找到一条能打通的路径。
- TURN:Traversal Using Relays around NAT
- 当 P2P 无法直接打通时,通过中继服务器中转媒体流,保证可靠性,但成本较高。
- 在本项目中,我们通过配置
iceServers(STUN 列表)让浏览器可以获取自己的公网候选地址,从而尝试建立 P2P 连接。
问题 3:SDP 在 WebRTC 中的作用是什么?
- SDP(Session Description Protocol) 是会话描述协议,用来描述一次多媒体会话的参数。
- 在 WebRTC 中,SDP 主要包含:
- 媒体类型(音频/视频)。
- 编码格式(codec,如 VP8、H264)。
- 传输协议、带宽限制等。
- WebRTC 通过
createOffer() / createAnswer() 生成 SDP,并通过信令通道(本项目中是 WebSocket)在两个端之间交换。
SDP 就是一张'合同',双方约定好'怎么说话、用什么语言、在什么频道说',然后再真正开始传输。
问题 4:为什么要用 Netty 实现 WebSocket,而不是直接用 SpringBoot 自带的 WebSocket 支持?
- SpringBoot 本身也支持基于 STOMP / SockJS 的 WebSocket 方案,更偏向业务开发的方便性。
- Netty 是一个更底层的高性能网络框架,优势在于:
- 性能好,可控性强,适用于高并发、多连接场景。
- 可以灵活定制 Pipeline 与 Handler。
- 在实际生产中:
- 如果只是简单的消息推送,小项目可以直接使用 Spring 的 WebSocket 支持。
- 如果需要高性能 IM、网关、长连接服务等,Netty 更适合。
- 本项目的目的之一是让学习者接触 Netty,理解更底层的网络处理方式。
问题 5:如果在实际项目中,要支持多人会议 / 群聊,该怎么扩展?
- 信令层面:
- 当前项目是一对一,通过
from / to 建立单对单信令。
- 多人会议可以采用房间(room)的概念,引入
roomId,让服务器管理房间内成员列表,并进行广播式信令分发。
- 媒体层面:
- 纯 P2P 的网状拓扑:
n 人则最多有 n*(n-1)/2 条连接,浏览器负担较大。
- 生产环境通常会引入 SFU(Selective Forwarding Unit)等媒体服务器,对流进行中转、混流或转发。
- 带宽与性能:
- 多人会议对上行带宽要求高,纯前端 P2P 方案不够稳定。
- 需要结合业务场景选择合适的架构(如 Janus、Jitsi、mediasoup 等现成 SFU)。
十四、总结与延伸学习方向
- WebRTC 基本概念:SDP、ICE、STUN 的作用与关系。
- 信令服务器的职责:为何需要 WebSocket + 后端。
- Netty + SpringBoot 集成方式:如何启动一个高性能 WebSocket 服务端。
- Vue3 + WebRTC 的端到端流程:从采集本地流、建立 PeerConnection,到 P2P 传输与挂断。
- 增加'是否接听'的弹窗,而不是自动应答。
- 支持文字聊天、屏幕共享功能。
- 引入 TURN 服务器,提高复杂网络环境下的连通率。
- 尝试对接真实域名与 HTTPS(wss + https),部署到公网演示。
如果你把这套 Demo 完整跑通,并能向别人清楚讲解其中的每一个名词与步骤,那么在大多数初中级面试中,关于实时音视频 / WebRTC / Netty 长连接相关的问题,你已经可以比较自信地应对了。希望这篇文章能成为你学习 WebRTC + Java 全栈实时通讯的一个良好起点。
text-align
.video-item
p
margin-bottom
8px
font-size
16px
color
#333
video
width
400px
height
300px
border
1px
#ccc
border-radius
8px
background-color
#f5f5f5
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online