ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot弹幕视频网站源码实战:WebSocket实时推送与视频分片播放

SpringBoot弹幕视频网站源码实战:WebSocket实时推送与视频分片播放 简介这是一份面向Java毕业设计与SpringBoot框架学习者的弹幕视频网站项目源码包适合需要完成课程设计、毕业设计或希望实践前后端交互开发的学生与开发者。项目以SpringBoot为核心实现了用户注册登录、视频上传与播放、弹幕发送与接收等模块构建出可互动的视频观看场景。压缩包共1309个文件约51.04MB涵盖119个Java源文件、155个JSP页面、358个JavaScript脚本及大量CSS、图片与字体资源并附有SQL脚本、properties配置与说明文档源码结构清晰、注释完整。目前已有98人学习下载。借助完整源码与说明文档读者可快速理解项目分层与代码组织掌握弹幕交互与视频播放的实现思路并在此基础上进行二次开发或定制化改造对学习SpringBoot框架与积累项目经验具有参考价值。1. 弹幕视频网站到底难在哪从一份 SpringBoot 完整源码说起很多人拿到「SpringBoot 弹幕视频网站」这个毕业设计题目第一反应是 CRUD 堆一堆就完事。真动手才发现视频上传、分片播放、弹幕实时推送、并发写库这几块随便一个都能让项目在答辩现场翻车。弹幕视频网站的核心难点不在页面好不好看而在「一条弹幕从浏览器发出到出现在别人屏幕上中间经历了什么」——这条链路涉及 WebSocket 长连接管理、消息广播策略、弹幕与视频时间轴的同步、以及高并发下的写入性能。这份基于 SpringBoot 的完整源码解决的正是把上述链路串起来的问题后端用 SpringBoot 做 REST 接口和 WebSocket 服务前端用 Vue 渲染播放器和弹幕层数据库存视频元信息和弹幕记录。适合正在做计算机毕业设计、想找一个技术栈主流且能讲出深度的题目的同学也适合 Java 初中级工程师拿它练手 SpringBoot 整合实时通信的完整流程。下面按「先跑通、再拆解、后优化」的顺序把这份源码里真正值得复现的部分讲清楚。2. 环境搭建与项目跑通从零到看见第一条弹幕2.1 技术栈选型与版本对齐拿到一份 SpringBoot 完整源码第一件事不是急着mvn spring-boot:run而是先看pom.xml里的版本组合。毕业设计最常见的翻车场景就是「源码能跑我本地跑不起来」十有八九是 JDK 版本和 SpringBoot 版本对不上。这份源码的典型组合是 JDK 8 或 JDK 11 配 SpringBoot 2.x如果你本地装了 JDK 17 甚至更高启动时大概率报java.lang.UnsupportedClassVersionError或者 MyBatis 相关的反射异常。选型上后端核心依赖一般包括spring-boot-starter-webREST 接口、spring-boot-starter-websocket弹幕实时推送、mybatis-plus-boot-starter数据库操作、spring-boot-starter-thymeleaf或纯前后端分离的 Vue 静态资源。数据库用 MySQL 5.7 或 8.0 都行但要注意application.yml里 JDBC URL 的时区和字符集参数漏了serverTimezone会直接连不上。组件推荐版本说明JDK8 / 11与 SpringBoot 2.x 兼容性最好SpringBoot2.3.x ~ 2.7.x避免 3.xJDK 要求 17MySQL5.7 / 8.08.0 需加时区参数MyBatis-Plus3.4.x与 SpringBoot 2.x 匹配Vue2.x源码前端多为 Vue2 写法提示如果源码里用了spring-boot-starter-parent指定版本不要手动改成更高版本否则依赖冲突排查起来非常痛苦。2.2 数据库初始化与配置修改源码包里通常有一个sql目录里面是建表语句和初始数据。导入之前先确认字符集弹幕内容涉及中文和 emoji表字段用utf8mb4而不是utf8否则插入 emoji 会报Incorrect string value。-- 创建数据库字符集必须用 utf8mb4 CREATE DATABASE danmu_video DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -- 弹幕表核心字段示例 CREATE TABLE danmu ( id BIGINT NOT NULL AUTO_INCREMENT, video_id BIGINT NOT NULL COMMENT 视频ID, user_id BIGINT DEFAULT NULL COMMENT 发送者ID, content VARCHAR(255) NOT NULL COMMENT 弹幕内容, send_time INT NOT NULL COMMENT 弹幕出现的时间点单位秒, color VARCHAR(10) DEFAULT #FFFFFF COMMENT 弹幕颜色, create_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_video_time (video_id, send_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建表语句里send_time这个字段是弹幕系统的关键设计——它记录弹幕相对于视频播放进度的时间点而不是绝对时间戳。前端播放器在timeupdate事件里拿当前播放秒数去比对决定这条弹幕什么时候从屏幕右侧飘出来。idx_video_time联合索引是为了加速「查某个视频某时间段内的弹幕」这个高频查询。配置修改集中在application.ymlspring: datasource: url: jdbc:mysql://localhost:3306/danmu_video?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver servlet: multipart: max-file-size: 500MB # 视频上传大小限制 max-request-size: 500MB server: port: 8080max-file-size必须改默认 1MB 传视频直接失败。serverTimezone不加的话 MySQL 8.0 会抛The server time zone value is unrecognized。2.3 启动项目与验证接口配置改完后用 Maven 启动# 清理并编译跳过测试加快速度 mvn clean package -DskipTests # 启动项目 java -jar target/danmu-video-0.0.1-SNAPSHOT.jar # 或者开发阶段直接用 mvn spring-boot:run启动成功后先别急着打开浏览器。用 curl 验证基础接口是否通# 测试视频列表接口 curl http://localhost:8080/api/video/list # 测试弹幕查询接口videoId1 curl http://localhost:8080/api/danmu/list?videoId1如果返回 JSON 数据说明后端和数据库通了。如果报 500看控制台堆栈八成是数据库连接或表字段不匹配。前端如果是 Vue 项目进入frontend目录执行npm install npm run serve注意vue.config.js里的代理配置要指向后端 8080 端口。3. 弹幕实时推送WebSocket 链路拆解与参数调优3.1 为什么不用轮询而用 WebSocket弹幕的本质是「一个用户发所有正在看同一视频的用户立刻看到」。用 HTTP 轮询实现前端每隔 1 秒请求一次弹幕接口问题有三个延迟高最多 1 秒才看到、服务器压力大1000 人在线就是 1000 QPS 的无用请求、弹幕时间轴对不齐轮询间隔和视频播放进度不同步。WebSocket 建立一次长连接后服务端可以主动推送延迟降到毫秒级这是弹幕视频网站的标准做法。SpringBoot 整合 WebSocket 有两种常见方式一是用ServerEndpoint注解的原生方式二是用WebSocketHandler接口配合WebSocketConfigurer配置。这份源码里两种都可能出现原生方式代码更直观适合毕业设计讲解。3.2 WebSocket 服务端核心代码Component ServerEndpoint(/ws/danmu/{videoId}) public class DanmuWebSocket { // 用 ConcurrentHashMap 存每个视频对应的在线连接集合 // key 是 videoIdvalue 是该视频下所有 WebSocket 会话 private static final ConcurrentHashMapLong, CopyOnWriteArraySetSession VIDEO_SESSIONS new ConcurrentHashMap(); private Long videoId; OnOpen public void onOpen(Session session, PathParam(videoId) Long videoId) { this.videoId videoId; // computeIfAbsent 保证线程安全地初始化集合 VIDEO_SESSIONS.computeIfAbsent(videoId, k - new CopyOnWriteArraySet()).add(session); System.out.println(新连接加入视频ID videoId 当前在线 VIDEO_SESSIONS.get(videoId).size()); } OnMessage public void onMessage(String message, Session session) { // message 是前端发来的 JSON 字符串包含弹幕内容和时间点 // 广播给同一视频下的所有连接 CopyOnWriteArraySetSession sessions VIDEO_SESSIONS.get(videoId); if (sessions ! null) { for (Session s : sessions) { if (s.isOpen()) { try { s.getBasicRemote().sendText(message); } catch (IOException e) { e.printStackTrace(); } } } } } OnClose public void onClose(Session session) { CopyOnWriteArraySetSession sessions VIDEO_SESSIONS.get(videoId); if (sessions ! null) { sessions.remove(session); // 集合空了就移除这个视频的条目防止内存泄漏 if (sessions.isEmpty()) { VIDEO_SESSIONS.remove(videoId); } } } OnError public void onError(Session session, Throwable error) { error.printStackTrace(); } }这段代码的逻辑说明ServerEndpoint(/ws/danmu/{videoId})定义了连接路径{videoId}是路径参数前端连接时传入当前视频 ID。VIDEO_SESSIONS用ConcurrentHashMap加CopyOnWriteArraySet的组合是因为 WebSocket 的onOpen和onClose可能被多个线程同时触发普通HashMap和HashSet会抛ConcurrentModificationException。onMessage里做的是广播——把收到的消息原样转发给同一视频下的所有连接包括发送者自己这样发送者也能立刻看到自己的弹幕飘过。参数调优方面ServerEndpoint可以加configurator和decoders/encoders但毕业设计阶段用默认配置就够。真正需要注意的是 WebSocket 连接数上限Tomcat 默认最大连接数是 8192如果答辩演示时模拟大量用户需要改server.tomcat.max-connections。3.3 前端弹幕渲染与时间轴同步前端收到 WebSocket 消息后不能立刻渲染而是要根据弹幕的send_time和当前视频播放进度决定何时显示。// 建立 WebSocket 连接videoId 从路由参数获取 const videoId this.$route.params.id; const ws new WebSocket(ws://localhost:8080/ws/danmu/${videoId}); // 弹幕队列按 send_time 排序 let danmuQueue []; ws.onmessage (event) { const danmu JSON.parse(event.data); danmuQueue.push(danmu); // 按出现时间排序保证先出现的先渲染 danmuQueue.sort((a, b) a.sendTime - b.sendTime); }; // 监听视频播放进度每 250ms 检查一次队列 const video document.getElementById(videoPlayer); video.addEventListener(timeupdate, () { const currentTime Math.floor(video.currentTime); // 找出当前时间点应该显示的弹幕 danmuQueue danmuQueue.filter(d { if (d.sendTime currentTime d.sendTime currentTime - 1) { renderDanmu(d); // 渲染到弹幕层 return false; // 已渲染的从队列移除 } return d.sendTime currentTime; // 还没到时间的保留 }); }); function renderDanmu(danmu) { const div document.createElement(div); div.className danmu-item; div.textContent danmu.content; div.style.color danmu.color || #fff; // 随机分配轨道避免弹幕重叠 div.style.top ${Math.random() * 60 5}%; document.querySelector(.danmu-layer).appendChild(div); // 动画结束后移除 DOM防止内存堆积 setTimeout(() div.remove(), 8000); }逻辑说明timeupdate事件在视频播放时大约每 250ms 触发一次用它来驱动弹幕队列的消费。renderDanmu里用Math.random()分配垂直位置是简化做法生产环境应该用轨道分配算法避免重叠。setTimeout在动画结束后移除 DOM 节点否则看一小时视频会积累上万个 DOM 节点页面直接卡死。注意WebSocket 连接地址的协议要匹配页面协议HTTPS 页面必须用wss://否则浏览器会拦截。4. 视频上传与分片播放大文件处理的避坑指南4.1 视频上传的两种方案对比毕业设计里视频上传最容易翻车的地方是「传大文件超时」和「传完播放不了」。常见做法有两种一是直接MultipartFile一次性上传二是前端分片上传后端合并。前者实现简单但超过 100MB 就容易超时后者复杂但稳定。方案优点缺点适用场景一次性上传代码简单几行搞定大文件超时无法断点续传小于 50MB 的短视频分片上传稳定支持断点续传前后端都要写合并逻辑大于 100MB 的视频如果源码里用的是一次性上传答辩演示用短视频没问题但要知道分片上传才是生产级方案。分片上传的核心思路前端用File.slice()把文件切成 5MB 的块逐块上传后端用RandomAccessFile按偏移量写入同一个文件。4.2 视频存储路径与静态资源映射上传的视频不能放在项目目录里否则重新打包部署就丢了。常见做法是配一个外部目录然后在 SpringBoot 里做静态资源映射。Configuration public class WebMvcConfig implements WebMvcConfigurer { // 视频存储的物理路径建议放在项目外 private static final String VIDEO_PATH D:/danmu-video/upload/; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 把 /video/** 的请求映射到物理路径 registry.addResourceHandler(/video/**) .addResourceLocations(file: VIDEO_PATH); } }这样前端用video src/video/xxx.mp4就能播放实际文件在D:/danmu-video/upload/xxx.mp4。file:前缀不能少否则 SpringBoot 会当成 classpath 资源去找。上传接口本身PostMapping(/api/video/upload) public Result upload(RequestParam(file) MultipartFile file, RequestParam(title) String title) { if (file.isEmpty()) { return Result.error(文件为空); } // 用 UUID 重命名避免中文名和重名问题 String originalName file.getOriginalFilename(); String suffix originalName.substring(originalName.lastIndexOf(.)); String newName UUID.randomUUID().toString() suffix; File dest new File(VIDEO_PATH newName); if (!dest.getParentFile().exists()) { dest.getParentFile().mkdirs(); // 目录不存在就创建 } try { file.transferTo(dest); } catch (IOException e) { return Result.error(上传失败 e.getMessage()); } // 把视频元信息写入数据库 Video video new Video(); video.setTitle(title); video.setPath(/video/ newName); videoService.save(video); return Result.success(video); }transferTo是 Spring 封装的方法底层用FileCopyUtils比手动流拷贝简洁。UUID重命名是为了避免用户上传同名文件互相覆盖也避免中文文件名在某些操作系统上的编码问题。4.3 视频播放的 Range 请求支持视频播放器拖动进度条时浏览器会发Range请求要求只返回文件的一部分。如果后端不支持 Range拖动进度条会重新从头加载体验极差。SpringBoot 的静态资源映射默认支持 Range但如果你自己写 Controller 返回视频流就需要手动处理。GetMapping(/api/video/play/{id}) public void play(PathVariable Long id, HttpServletRequest request, HttpServletResponse response) throws IOException { Video video videoService.getById(id); File file new File(VIDEO_PATH video.getPath().replace(/video/, )); // 获取 Range 头格式如 bytes0-1024 String range request.getHeader(Range); long fileLength file.length(); if (range ! null range.startsWith(bytes)) { String[] ranges range.substring(6).split(-); long start Long.parseLong(ranges[0]); long end ranges.length 1 !ranges[1].isEmpty() ? Long.parseLong(ranges[1]) : fileLength - 1; response.setStatus(HttpServletResponse.SC_PARTIAL_CONTENT); // 206 response.setHeader(Content-Range, bytes start - end / fileLength); response.setHeader(Accept-Ranges, bytes); response.setContentLengthLong(end - start 1); try (RandomAccessFile raf new RandomAccessFile(file, r); OutputStream os response.getOutputStream()) { raf.seek(start); byte[] buffer new byte[4096]; long remaining end - start 1; while (remaining 0) { int len raf.read(buffer, 0, (int) Math.min(buffer.length, remaining)); if (len -1) break; os.write(buffer, 0, len); remaining - len; } } } else { // 没有 Range 就返回整个文件 response.setContentLengthLong(fileLength); Files.copy(file.toPath(), response.getOutputStream()); } }关键点是SC_PARTIAL_CONTENT206 状态码和Content-Range响应头浏览器看到这两个才会认为服务器支持分段请求。RandomAccessFile.seek(start)直接跳到指定位置读取不用把整个文件加载到内存。5. 避坑与排查毕业设计答辩前必须过的 5 道坎5.1 弹幕发送后自己看不到现象用户发送弹幕后自己的屏幕上没有出现但别人能看到。原因onMessage广播时排除了发送者或者前端发送弹幕走的是 HTTP 接口而接收走的是 WebSocket两条链路没打通。解决广播时不要排除任何连接发送者也应该收到自己的弹幕。如果发送走 HTTP、接收走 WebSocket在 HTTP 接口保存弹幕后手动调用 WebSocket 的广播方法把消息推给所有连接。5.2 视频上传后播放 404现象上传成功数据库也有记录但video标签报 404。原因静态资源映射路径写错或者addResourceLocations少了file:前缀或者上传目录和映射目录不一致。解决检查WebMvcConfig里的VIDEO_PATH和上传接口里的保存路径是否是同一个。在浏览器直接访问http://localhost:8080/video/文件名看能否下载能下载说明映射没问题问题在前端路径拼接。5.3 WebSocket 连接频繁断开现象弹幕看着看着就不推了刷新页面又恢复。原因Nginx 或 Tomcat 的空闲连接超时默认 60 秒没有数据传输就断开。弹幕不密集时连接长时间空闲就会被回收。解决前端加心跳每 30 秒发一个空消息保持连接。后端onMessage收到心跳消息后不广播直接忽略。// 前端心跳每 30 秒发一次 setInterval(() { if (ws.readyState WebSocket.OPEN) { ws.send(JSON.stringify({ type: heartbeat })); } }, 30000);5.4 弹幕时间轴对不上现象弹幕出现的时间点和视频内容不匹配比如台词都过了弹幕才飘出来。原因send_time存的是发送时的绝对时间戳而不是相对于视频播放进度的时间。或者前端timeupdate里用的currentTime精度不够。解决发送弹幕时前端取video.currentTime取整后作为sendTime传给后端。渲染时用Math.floor(video.currentTime)比对不要用Date.now()。5.5 MySQL 连接数耗尽现象项目跑一段时间后报Too many connections。原因每次请求都新建数据库连接没有用连接池或者连接池配置的max-active太小。解决SpringBoot 默认用 HikariCP检查application.yml里有没有配spring.datasource.hikari.maximum-pool-size默认是 10毕业设计演示够用。如果报错先看是不是代码里有手动DriverManager.getConnection没关闭。6. 从能跑到能讲答辩演示的加分技巧6.1 用压测数据证明弹幕并发能力答辩时老师最爱问「你这个支持多少人同时在线」。光说「支持很多」没用跑个压测拿数据说话。用wrk或JMeter对弹幕接口做压测记录 QPS 和响应时间。# 用 wrk 压测弹幕查询接口10 个连接持续 30 秒 wrk -t4 -c10 -d30s http://localhost:8080/api/danmu/list?videoId1把结果截图放进 PPTQPS 多少、平均延迟多少、错误率多少。如果 QPS 只有几百就诚实说「当前配置下支持约 XX 并发后续可以通过 Redis 缓存弹幕列表、用 Netty 替换 Tomcat WebSocket 来提升」。老师要的是你知道瓶颈在哪而不是吹牛。6.2 弹幕敏感词过滤的最小实现答辩时另一个高频问题是「弹幕内容怎么审核」。毕业设计不用做完整的审核系统但可以加一个敏感词过滤体现你有安全意识。Component public class SensitiveWordFilter { // 用 Trie 树或简单的 Set 存储敏感词演示用 Set 足够 private static final SetString SENSITIVE_WORDS new HashSet(Arrays.asList( 敏感词1, 敏感词2, 广告 )); public String filter(String content) { if (content null || content.isEmpty()) { return content; } String result content; for (String word : SENSITIVE_WORDS) { if (result.contains(word)) { // 用等长的 * 替换 result result.replace(word, *.repeat(word.length())); } } return result; } }在弹幕保存前调用filter方法把过滤后的内容存库和广播。*.repeat(n)是 JDK 11 的方法JDK 8 用String.join(, Collections.nCopies(n, *))替代。6.3 项目结构梳理让老师一眼看懂你的分层毕业设计答辩时间有限老师翻代码的时间更有限。项目结构清晰印象分直接拉满。推荐的包结构com.example.danmu ├── config # WebSocket、MVC、MyBatis 配置 ├── controller # REST 接口层 ├── service # 业务逻辑层 │ └── impl ├── mapper # MyBatis Mapper 接口 ├── entity # 数据库实体 ├── dto # 前端传输对象 ├── vo # 视图返回对象 ├── websocket # WebSocket 服务端 └── util # 工具类controller只做参数校验和调用service不写业务逻辑service里做弹幕过滤、时间轴计算mapper只做数据库操作。这样老师问「你的业务逻辑在哪」你直接指service包清晰明了。6.4 我踩过的一个真实坑当年我做类似项目时弹幕用ServerEndpoint注解方式本地跑得好好的部署到服务器上弹幕死活推不过去。排查了一整天最后发现是服务器上 Tomcat 的server.tomcat.max-connections被改成了 100而 WebSocket 连接占满了之后新连接直接被拒绝。改回默认值 8192 后一切正常。这件事让我养成了一个习惯部署环境和本地环境的配置差异永远优先排查。本地能跑不代表线上能跑尤其是连接数、超时时间、文件路径这三类配置。希望帮到你。本文还有配套的精品资源点击获取
返回列表