1. 标题与引言

标题: 低延迟互动直播实战:SpringBoot + SRS 实现多人在线课堂(含全套源码与 Nginx 配置)

引言: 传统的直播方案(RTMP/HLS)动辄数秒甚至十秒的延迟,在互动教学场景下简直是灾难。作为后端开发者,如何利用 WebRTC 实现超低延迟的多人互动?本文将带你实战一套完整的“互动课堂”方案,解决推流控制、房间自动清理、以及公网 HTTPS 部署等核心痛点。

2. 技术架构图

在开始编码前,先看整体数据流向:

  • 信令流:客户端 <-> SpringBoot (WebSocket) <-> 控制状态

  • 媒体流:客户端 <-> SRS (WebRTC) <-> 转发音视频

3. 第一步:环境搭建(SRS 与 Nginx)

WebRTC 在公网环境下必须运行在 HTTPS 之上。我们使用 Nginx 进行 SSL 卸载并转发信令。(推流必须在https环境,使用自签证书也可)

Nginx 核心配置:

server {
    listen 443 ssl;
    server_name your_domain.com;

    ssl_certificate /etc/nginx/cert/your_cert.crt;
    ssl_certificate_key /etc/nginx/cert/your_cert.key;

    # 转发 SRS 的 WebRTC HTTP API (信令端口)
    location /rtc/v1/ {
        proxy_pass http://127.0.0.1:1985/rtc/v1/;
        proxy_set_header Host $host;
    }

    # 静态资源或 API 转发...
}

4. 第二步:定义核心模型(Room & User)

我们需要在内存中维护房间和用户状态,使用线程安全的容器是关键。

@Data
public class Room {
    private String id;
    private String name;
    // 使用 CopyOnWriteArrayList 避免并发修改异常
    private List<User> users = new CopyOnWriteArrayList<>();
}

@Data
public class User {
    private String userId;
    private String username;
    private String streamUrl; // 活跃推流地址
}

5. 第三步:编写 RoomService(业务心脏)

这里实现了两个核心逻辑:全局广播自动清理

@Service
public class RoomService {
    private final Map<String, Room> roomMap = new ConcurrentHashMap<>();

    // 1. 用户加入房间
    public String joinRoom(String roomId, User user) {
        Room room = roomMap.get(roomId);
        if (room != null) {
            room.getUsers().add(user);
            log.info("用户{}加入房间{}", user.getUserId(), roomId);
        }
        return roomId;
    }

    // 2. 流状态激活(关键:推流成功后触发广播)
    public void setStreamActive(String roomId, String userId, String streamUrl) {
        Room room = roomMap.get(roomId);
        if (room != null) {
            room.getUsers().stream()
                .filter(u -> u.getUserId().equals(userId))
                .findFirst()
                .ifPresent(u -> {
                    u.setStreamUrl(streamUrl);
                    // 全局通知所有人:有新流上线,协议号 1
                    ExamWebSocketHandler.broadcastToAll(1, u);
                });
        }
    }

    // 3. 核心:被动退出清理逻辑
    public void globalCleanUser(String userId) {
        roomMap.forEach((roomId, room) -> {
            boolean removed = room.getUsers().removeIf(u -> u.getUserId().equals(userId));
            if (removed) {
                // 通知所有人该流已下线,协议号 0
                ExamWebSocketHandler.broadcastToAll(0, userId);
                if (room.getUsers().isEmpty()) roomMap.remove(roomId);
            }
        });
    }
}

6. 第四步:WebSocket 实时同步

利用 WebSocket 保持客户端与后端的长连接,确保指令“秒达”。

@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
    String clientId = (String) session.getAttributes().get("clientId");
    if (clientId != null) {
        clientSessions.remove(clientId);
        // 如果 WebSocket 断开(关浏览器),自动清理房间防止“僵尸流”
        roomService.globalCleanUser(clientId);
    }
}

public static void broadcastToAll(Integer protocolHead, Object data) {
    // 遍历所有在线 Session 广播 ProtocolMessage...
}

7. 第五步:实战流程对比

在开发过程中,我对比了重构前后的差异:

步骤 传统逻辑 重构后(本方案)
感知他人加入 手动刷新列表 Socket 实时弹窗/渲染
流上线 等待 HLS 切片 (10s+) WebRTC 瞬间通知并拉流
异常断线 房间信息残留,流黑屏 后台自动检测并全员销毁窗口

8. 避坑指南(干货)

  1. UDP 端口开通:云服务器不仅要开 TCP 1985,必须开 UDP 8000 及其 Candidate 指定的端口范围。

  2. WebSocket 跨域:在 registerWebSocketHandlers 中配置 .setAllowedOriginPatterns("*"),否则前端连接会报 403。

  3. 白名单:记得在 Spring Security 或 Filter 中放行 /exam/** 路径。


9. 结语

通过这套方案,我们不仅实现了一个低延迟的互动系统,更通过 SpringBoot 完成了对 SRS 这种 C++ 媒体服务器的“精细化管控”。

Debug笔记

记录我在代码背后踩过的坑,也分享那些曾经提升过我效率的关键思路。

微信搜索公众号:Debug笔记,关注我,一起成为更优雅的程序员。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐