当前位置: 首页 > java >正文

基于 Socket.IO 实现 WebRTC 音视频通话与实时聊天系统(Spring Boot 后端实现)

基于 Socket.IO 实现 WebRTC 音视频通话与实时聊天系统(Spring Boot 后端实现)

技术栈:Spring Boot + Socket.IO (Netty-socketio) + WebRTC + Redis + MongoDB


一、引言

随着远程医疗、在线教育、即时通讯等应用的普及,实时音视频通话和文本聊天功能已成为现代 Web 应用的核心需求。本文将详细介绍如何使用 Java 的 Netty-socketio 框架,在 Spring Boot 项目中实现一个完整的 WebRTC 信令服务器实时文本聊天系统

我们将深入剖析两个核心服务类 WebRTCServiceChatService,并提供完整的功能流程图和前端交互示例。


二、系统架构概览

本系统采用典型的 B/S 架构:

  • 前端 (Web/移动端):使用 WebRTC API 处理音视频流,使用 Socket.IO 客户端与服务器通信。
  • 后端 (Java Spring Boot):使用 Netty-socketio 作为 Socket.IO 服务器,处理所有信令和消息逻辑。
  • 数据库:使用 MongoDB 存储聊天消息和用户联系人,使用 Redis 存储在线状态和未读计数。
+----------------+     +---------------------+     +----------------+
|                |     |                     |     |                |
|   Web Client   |<--->|  Netty-socketio     |<--->|  MongoDB/Redis |
| (WebRTC + Chat)|     |  (Java Server)      |     | (Persistence)  |
|                |     |                     |     |                |
+----------------+     +---------------------+     +----------------+

三、核心功能模块详解

3.1 文本聊天服务 (ChatService.java)

ChatService 负责处理用户连接、消息收发、状态更新和房间管理。

3.1.1 核心功能
  1. 用户连接与身份认证

    • 客户端连接时,发送 connect_success 事件,携带 userId, role (patient/doctor), institutionId 等信息。
    • 服务端验证信息后,将用户加入对应的“通知房间”和“聊天房间”。
  2. 消息收发

    • 客户端发送 send_msg 事件。
    • 服务端将消息存入 MongoDB,并广播给房间内其他成员。
  3. 消息状态管理

    • 支持 msg_delivered (已送达) 和 msg_read (已读) 状态。
    • 通过 Redis 维护未读消息计数。
  4. 房间成员管理

    • 实时获取和广播房间内的在线成员列表。
3.1.2 关键代码解析
// 生成唯一的聊天室ID
private String generateChatRoom(String institutionId, String patientId, String doctorId) {return "chat_room_" + institutionId + "_" + patientId + "_" + doctorId;
}// 处理消息发送
private void handleSendMessage(SocketIOClient client, Map<String, Object> data) {// ... (参数校验)String chatRoom = generateChatRoom(institutionId, patientId, doctorId);joinAndTrackMembership(client, chatRoom); // 确保在房间内sendMessage(patientId, doctorId, institutionId, msgContent, role, msgType, client);
}

3.2 WebRTC 音视频通话服务 (WebRTCService.java)

WebRTCService 是 WebRTC 信令服务器的核心,负责交换 SDP Offer/Answer 和 ICE Candidate。

3.2.1 WebRTC 信令流程

WebRTC 本身不提供信令机制,需要开发者自行实现。本系统通过 Socket.IO 事件完成信令交换:

1. A (发起方)             2. Server (信令服务器)           3. B (接收方)|                           |                              ||---- CALL_REQUEST -------->|                              ||                           |                              ||                           |------ CALL_REQUEST --------->||                           |                              ||                           |<----- CALL_ACCEPT -----------||<---- CALL_ACCEPT ----------|                              ||                           |                              ||------ OFFER ------------->|                              ||                           |                              ||                           |-------- OFFER -------------->||                           |                              ||                           |<------- ANSWER ---------------||<------ ANSWER -------------|                              ||                           |                              ||------ ICE_CANDIDATE ----->|                              ||                           |                              ||                           |--- ICE_CANDIDATE ----------->||                           |                              || (建立P2P连接)             |                              |
3.2.2 核心事件与处理
事件 (Event)说明
call_requestA 发起通话请求,服务器转发给 B。
call_acceptB 接受通话,服务器通知 A。
call_rejectB 拒绝通话,服务器通知 A 并清理会话。
call_end任一方结束通话,通知另一方。
offerA 创建 Offer,通过服务器转发给 B。
answerB 创建 Answer,通过服务器转发给 A。
ice-candidate双方收集到 ICE Candidate,通过服务器转发给对方。
call_timeout服务器在 30 秒内未收到 B 的回应,自动通知 A 通话超时。
3.2.3 关键代码解析
// 处理通话请求
socketServer.addEventListener(CALL_REQUEST, Map.class, (client, data, ackSender) -> {String roomId = getRequiredString(data, "roomId");String toUserId = getRequiredString(data, "toUserId");// ... (校验)callSessions.put(roomId, new CallSession(roomId, userId)); // 创建会话sendToUser(toUserId, roomId, CALL_REQUEST, payload); // 转发给接收方startTimeoutTimer(roomId, 30000); // 启动30秒超时定时器
});// 处理 SDP 交换 (Offer/Answer)
private void handleSdpExchange(SocketIOClient client, Map<String, Object> data, String eventType) {String roomId = getRequiredString(data, "roomId");CallSession session = callSessions.get(roomId);// ... (会话校验)broadcastExceptSender(roomId, eventType, payload, client); // 转发给对方
}
3.2.4 会话管理与断线处理
  • 会话缓存:使用 ConcurrentHashMap<String, CallSession> 存储所有通话会话。
  • 超时机制:使用 ScheduledExecutorService 为每个 call_request 设置 30 秒超时。
  • 断线清理:当用户断开连接时,ChatService 会调用 webRTCService.cleanupSessionsOnDisconnect(),清理该用户作为发起者的所有通话。
public void cleanupSessionsOnDisconnect(SocketIOClient client) {String userId = client.get("userId");Iterator<Map.Entry<String, CallSession>> iterator = callSessions.entrySet().iterator();while (iterator.hasNext()) {Map.Entry<String, CallSession> entry = iterator.next();if (userId.equals(entry.getValue().getInitiator())) {broadcast(entry.getKey(), CALL_END, endEventPayload); // 通知对方iterator.remove(); // 安全移除}}
}

四、功能实例与交互图

4.1 文本聊天实例

场景:患者 (P1) 向医生 (D1) 发送一条文本消息。

<!DOCTYPE html>
<html>
<head><title>IM Chat Demo</title><script src="https://cdn.socket.io/4.7.2/socket.io.min.js"></script>
</head>
<body><h2>患者端 - 与医生 D1 聊天</h2><div id="messages"></div><input type="text" id="msgInput" placeholder="输入消息..."><button onclick="sendMessage()">发送</button><script>const socket = io('http://localhost:8080'); // 连接服务器// 连接成功socket.emit('connect_success', {userId: 'P1',role: '2', // 患者institutionId: '1001',doctorId: 'D1'});// 接收消息socket.on('get_send_msg', function(msg) {const div = document.createElement('div');div.textContent = `[${msg.userType === 2 ? '患者' : '医生'}] ${msg.msgContent}`;document.getElementById('messages').appendChild(div);});function sendMessage() {const content = document.getElementById('msgInput').value;socket.emit('send_msg', {userId: 'P1',role: '2',institutionId: '1001',doctorId: 'D1',msgType: 1,msg: { text: content }});document.getElementById('msgInput').value = '';}</script>
</body>
</html>

4.2 音视频通话实例图

患者 (P1)信令服务器医生 (D1)call_request(roomId, toUserId=D1, offer)call_request(roomId, fromUserId=P1, offer)call_accept(roomId)call_accept(roomId)answer(roomId, sdpAnswer)answer(roomId, sdpAnswer)offer(roomId, sdpOffer)offer(roomId, sdpOffer)ice-candidate(roomId, candidate)ice-candidate(roomId, candidate)ice-candidate(roomId, candidate)ice-candidate(roomId, candidate)loop[交换 ICE Candidate]P2P 连接建立call_reject(roomId)call_reject(roomId)cleanupCall(roomId)alt[医生接受][医生拒绝]患者 (P1)信令服务器医生 (D1)

五、Coturn 服务器搭建与 ICE 认证

5.1 为什么需要 Coturn?

WebRTC 使用 ICE (Interactive Connectivity Establishment) 框架来寻找最佳的网络路径。ICE 会收集多种类型的网络地址(称为 Candidate):

  • Host Candidate: 设备自身的内网IP和端口。
  • Server Reflexive Candidate (SRFLX): 通过 STUN 服务器获取的公网IP和端口。
  • Relayed Candidate (RELAY): 当 P2P 连接无法建立时,通过 TURN 服务器中继的媒体流。

STUN 服务器用于发现设备的公网地址,而 TURN 服务器则在 STUN 失败时,作为媒体流的中继服务器。Coturn 是一个开源的、功能强大的 STUN/TURN 服务器实现。

5.2 Coturn 服务器搭建

以下是在 Ubuntu 20.04 系统上搭建 Coturn 服务器的完整步骤。

5.2.1 安装 Coturn
# 更新系统包
sudo apt update
sudo apt upgrade -y# 安装 coturn
sudo apt install coturn -y# 设置开机自启
sudo systemctl enable coturn
5.2.2 配置 Coturn

编辑 Coturn 的主配置文件 /etc/turnserver.conf

sudo nano /etc/turnserver.conf

将以下配置复制到文件中,并根据你的服务器环境进行修改:

# Coturn 配置文件# 外部 IP 地址 (你的服务器公网IP)
external-ip=YOUR_SERVER_PUBLIC_IP# 监听端口
listening-port=3478
tls-listening-port=5349# Realm (域名或标识符)
realm=your-domain.com# 侦听所有接口
listening-ip=0.0.0.0# 强制使用指定的 IP 作为服务器的 IP
# 通常设置为 external-ip
# 如果你的服务器有多个公网IP,可以在这里指定
# relay-ip=YOUR_SERVER_PUBLIC_IP# 启用 STUN
stun-only=false# 启用 TURN
# no-stun=true# 传输协议
# 可以指定 udp, tcp, tls, dtls
# 通常建议都启用
# no-udp
# no-tcp
# no-tls
# no-dtls# 转发协议
# 可以指定 udp, tcp, tls, dtls
# 通常建议都启用
# no-udp-relay
# no-tcp-relay
# no-tls-relay
# no-dtls-relay# 证书文件路径 (用于 TLS/DTLS)
# cert=/etc/ssl/certs/turnserver.pem
# pkey=/etc/ssl/private/turnserver_pkey.pem# 用户名和密码数据库
# 这里使用 long-term credential mechanism
# 用户名和密码将通过 TURN REST API 在应用层面生成
# 数据库文件路径
userdb=/var/lib/turn/turndb# 日志文件
log-file=/var/log/turnserver.log
# 日志级别
# 0: DEBUG, 1: INFO, 2: WARNING, 3: ERROR
verbose
# log-level=1# 限制每个用户的带宽 (kbps)
# total-quota=100000
# user-quota=100000# 安全设置
# 禁用本地 IP 地址
# no-loopback-peers
# no-multicast-peers# 允许来自任何域的 WebRTC 客户端
# 这在生产环境中可能需要更严格的限制
# web-admin-address=0.0.0.0
# web-admin-port=5766
# server-name=your-domain.com# 为 REST API 设置共享密钥
# 这是实现动态凭证的关键
# shared-secret=your-shared-secret-here# 禁用静态用户,强制使用 REST API (动态凭证)
# 你必须选择一种认证方式:
# 1. 静态用户: 在配置文件中定义 user=username:password
# 2. 动态凭证 (推荐): 使用 shared-secret 和 REST API# 方法 1: 静态用户 (简单,但不安全,不推荐用于生产)
# user=static_user:static_password# 方法 2: 动态凭证 (推荐)
# 注释掉或删除所有静态 user 行
# 确保 shared-secret 已设置
shared-secret=your-very-secure-shared-secret-here# 设置监听端口的协议
# 这将强制 TURN 服务器在这些端口上监听指定的协议
# 例如,让 3478 只监听 UDP,5349 只监听 TLS
# 这有助于防火墙配置
# udp-port-range=49152-65535
# min-port=49152
# max-port=65535

重要参数说明:

  • external-ip: 必须设置为你的服务器公网 IP。
  • realm: 可以是你的域名,如 im.yourcompany.com
  • shared-secret: 一个非常安全的密钥,用于在应用层面生成临时凭证。务必修改为一个强密码
5.2.3 启动 Coturn 服务
# 重启 Coturn 服务以应用配置
sudo systemctl restart coturn# 检查服务状态
sudo systemctl status coturn# 查看日志
sudo tail -f /var/log/turnserver.log
5.2.4 防火墙配置

确保服务器的防火墙开放了必要的端口。

# 使用 ufw
sudo ufw allow 3478/udp
sudo ufw allow 3478/tcp
sudo ufw allow 5349/tcp  # TLS
sudo ufw allow 5349/udp  # DTLS (可选)
# Coturn 会动态分配中继端口,通常在 49152-65535 范围内
sudo ufw allow 49152:65535/udp
sudo ufw allow 49152:65535/tcp# 重新加载防火墙
sudo ufw reload

5.3 ICE 服务器与凭证

WebRTC 客户端需要知道如何连接到 STUN/TURN 服务器。这通过 RTCPeerConnection 构造函数的 iceServers 配置项完成。

5.3.1 ICE 服务器配置
const iceServers = [// 1. 公共 STUN 服务器 (免费,但不可靠){ urls: "stun:stun.l.google.com:19302" },{ urls: "stun:global.stun.twilio.com:3478?transport=udp" },// 2. 自建 Coturn 服务器 (推荐){urls: ["turn:your-domain.com:3478?transport=udp","turn:your-domain.com:3478?transport=tcp","turn:your-domain.com:5349?transport=tcp" // TLS],username: "your-generated-username",credential: "your-generated-credential"}
];
5.3.2 动态凭证 (TURN REST API)

硬编码用户名和密码是不安全的。Coturn 支持通过共享密钥(shared-secret)动态生成临时凭证。

生成临时凭证的 Java 工具类:

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.time.Instant;public class TurnCredentialGenerator {private static final String SHARED_SECRET = "your-very-secure-shared-secret-here"; // 与 coturn.conf 一致/*** 生成用于 WebRTC ICE 的 TURN 临时凭证* @param userId 用户ID,用于标识* @param ttl 凭证有效期 (秒)* @return 包含 username 和 credential 的 Map*/public static Map<String, String> generateTemporaryCredentials(String userId, int ttl) {long timestamp = Instant.now().getEpochSecond() + ttl; // 过期时间戳String username = timestamp + ":" + userId; // 格式: <expire-timestamp>:<username>try {// 使用 HMAC-SHA1 签名Mac mac = Mac.getInstance("HmacSHA1");SecretKeySpec keySpec = new SecretKeySpec(SHARED_SECRET.getBytes(StandardCharsets.UTF_8), "HmacSHA1");mac.init(keySpec);byte[] digest = mac.doFinal(username.getBytes(StandardCharsets.UTF_8));// 将摘要进行 Base64 编码,得到 credentialString credential = Base64.getEncoder().encodeToString(digest);Map<String, String> result = new HashMap<>();result.put("username", username);result.put("credential", credential);return result;} catch (Exception e) {throw new RuntimeException("生成 TURN 凭证失败", e);}}// 使用示例public static void main(String[] args) {Map<String, String> credentials = generateTemporaryCredentials("user123", 3600); // 1小时有效System.out.println("Username: " + credentials.get("username"));System.out.println("Credential: " + credentials.get("credential"));}
}

前端获取 ICE 服务器配置:

// 在用户连接或进入聊天页面时,从你的后端API获取ICE配置
async function getIceServers() {const response = await fetch('/api/ice-servers'); // 你的Spring Boot API端点const data = await response.json();return data.iceServers;
}// 在创建 RTCPeerConnection 前调用
const iceServers = await getIceServers();
const peerConnection = new RTCPeerConnection({ iceServers });

Spring Boot Controller 示例:

@RestController
@RequestMapping("/api")
public class IceServerController {@GetMapping("/ice-servers")public ResponseEntity<Map<String, Object>> getIceServers(@RequestParam String userId) {Map<String, Object> response = new HashMap<>();try {// 生成临时凭证Map<String, String> credentials = TurnCredentialGenerator.generateTemporaryCredentials(userId, 3600);List<Map<String, Object>> iceServers = new ArrayList<>();// 添加公共 STUNiceServers.add(Map.of("urls", List.of("stun:stun.l.google.com:19302")));// 添加自建 TURNMap<String, Object> turnServer = new HashMap<>();turnServer.put("urls", List.of("turn:your-domain.com:3478?transport=udp","turn:your-domain.com:3478?transport=tcp"));turnServer.put("username", credentials.get("username"));turnServer.put("credential", credentials.get("credential"));iceServers.add(turnServer);response.put("iceServers", iceServers);return ResponseEntity.ok(response);} catch (Exception e) {log.error("获取ICE服务器配置失败", e);response.put("error", "Internal Server Error");return ResponseEntity.status(500).body(response);}}
}

5.4 集成与测试

  1. 前端集成:确保前端在创建 RTCPeerConnection 时,使用从后端获取的、包含动态凭证的 iceServers 配置。
  2. 信令流程WebRTCService 负责交换 SDP 和 ICE Candidate。当 RTCPeerConnection 收集到 Candidate 时,会触发 icecandidate 事件,前端通过 socket.emit('ice-candidate', ...) 发送给服务端,服务端再转发给对方。
  3. 测试
    • 使用 chrome://webrtc-internals 查看 ICE Candidate 收集情况。
    • 如果看到 relay 类型的 Candidate,说明 TURN 服务器正在工作。
    • 模拟网络问题(如关闭一方的 WiFi),观察通话是否能通过 TURN 中继恢复。

六、总结

至此,构建了一个功能完整、生产可用的实时通讯系统。

系统核心组件:

  1. 信令服务器 (WebRTCService): 基于 Netty-socketio,处理通话的建立、协商和结束。
  2. 消息服务器 (ChatService): 处理文本消息的收发、状态同步和用户在线管理。
  3. STUN/TURN 服务器 (Coturn): 解决 NAT 穿透问题,确保全球范围内的连接成功率。
  4. 持久化层 (MongoDB/Redis): 存储消息、联系人和在线状态。

优势与最佳实践:

  • 高可用:信令和 TURN 服务可以独立部署和扩展。
  • 安全性:使用动态凭证避免了密钥硬编码。
  • 可维护性:代码结构清晰,信令与业务逻辑分离。

注意事项

  • 生产环境需考虑集群部署,使用 Redis 存储 callSessions 以实现多节点共享。
  • 增加更完善的安全认证(如 JWT)。
  • 对 SDP 和 ICE 数据进行更严格的校验。

希望本文能为你的实时通讯项目提供有价值的参考!

http://www.xdnf.cn/news/17272.html

相关文章:

  • LongVie突破超长视频生成极限:1分钟电影级丝滑视频,双模态控制告别卡顿退化
  • PyTorch如何实现婴儿哭声检测和识别
  • 串联所有单词的子串-leetcode
  • 解读 gpt-oss-120b 和 gpt-oss-20b开源模型
  • 多账号管理方案:解析一款免Root的App分身工具
  • 抖音、快手、视频号等多平台视频解析下载 + 磁力嗅探下载、视频加工(提取音频 / 压缩等)
  • 编程之线性代数矩阵和概率论统计知识回顾
  • 基于langchain的两个实际应用:[MCP多服务器聊天系统]和[解析PDF文档的RAG问答]
  • 表单元素与美化技巧:打造用户友好的交互体验
  • 基于Ruby的IP池系统构建分布式爬虫架构
  • Qt帮助文档跳转问题修复指南
  • Flink-1.19.0源码详解9-ExecutionGraph生成-后篇
  • 通信中间件 Fast DDS(一) :编译、安装和测试
  • 汽车线束设计—导线的选取
  • WEB开发-第二十七天(PHP篇)
  • 中国MCP市场:腾讯、阿里、百度的本土化实践
  • Disruptor 消费者核心:BatchEventProcessor解析
  • 脱机部署k3s
  • 嵌入式硬件中MOSFET基本控制详解
  • 前端开发(HTML,CSS,VUE,JS)从入门到精通!第七天(Vue)(二)
  • FluentUI的介绍与使用案列
  • Pytest项目_day06(requests中Session的用法)
  • Spring文件泄露与修复方案总结
  • Go语言版JSON转TypeScript接口生成器:支持智能递归解析与命名优化
  • [Python 基础课程]Set
  • [Oracle] ROUND()函数
  • ORACLE 19C建库时卡在46%、36%
  • 《设计模式之禅》笔记摘录 - 13.迭代器模式
  • Kaggle 经典竞赛泰坦尼克号:超级无敌爆炸详细基础逐行讲解Pytorch实现代码,看完保证你也会!!!
  • 数据结构 二叉树(1)二叉树简单了解