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

基于 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.dtcms.com/a/318748.html

相关文章:

  • C语言中级_动态内存分配、指针和常量、各种指针类型、指针和数组、函数指针
  • MATLAB科研数据可视化
  • cuda编程笔记(13)--使用CUB库实现基本功能
  • 嵌入式硬件中MOSFET基本控制详解
  • 嵌入式硬件学习(十一)—— platform驱动框架
  • OpenAI 开源模型 GPT-OSS深度拆解:从1170亿参数到单卡部署,重构AI开源生态
  • 亚马逊采购风控突围:构建深度隐匿的环境安全体系
  • 360纳米AI、实在Agent、CrewAI与AutoGen……浅析多智能体协作系统
  • LabVIEW实验室测试框架
  • 《深入浅出 Django ORM:设计理念与惰性查询实现详解》
  • 炫酷圆形按钮调色器
  • 共识算法介绍
  • macOS 彻底卸载 Python 的完整指南
  • Mac+Chrome滚动截图
  • mac中多版本JDK配置和切换
  • 数据推荐|标贝科技方言自然对话数据集 构建语音交互新基建
  • 两种格式数据介绍——bin 、 yuv文件
  • 【C语言】文件操作全解析
  • 【感知机】感知机(perceptron)模型与几何解释
  • 第14届蓝桥杯Scratch_选拔赛_初级及中级(STEMA)真题2022年12月18日
  • 深度学习之pytorch安装与tensor(张量)
  • 美式期权定价模型之Barone-Adesi-Whaley定价模型
  • Linux 防火墙(firewalld)详解与配置
  • 第14届蓝桥杯Scratch选拔赛初级及中级(STEMA)真题2022年10月30日
  • Linux中firewalld(防火墙)配置与管理指南
  • 【golang】基于redis zset实现并行流量控制(计数锁)
  • InfluxDB 集群部署与高可用方案(一)
  • C基础 15_day
  • 从代码学习LLM - llama3 PyTorch版
  • css优化、提升性能方法都有哪些?