SpringBoot 3 實戰:打造讓屏幕飛起來的實時彈幕系統
在如今的視頻與直播領域,實時彈幕已經不僅是一個“酷炫的裝飾”,而是觀眾互動體驗的核心組成部分。無論是B站的番劇播放,還是各大直播平臺的賽事解說,彈幕早已成為用戶情緒的直接出口:有人激情吐槽,有人在線補充知識點,還有人花式整活帶動氣氛。 如果視頻是舞臺,那么彈幕就是現場觀眾的合唱,它讓屏幕不再孤獨。
本文將帶你使用 Spring Boot 3 + WebSocket,從零搭建一個高性能、可擴展的實時彈幕系統,前后端全流程覆蓋,并包含 消息存儲、內容過濾、歷史回放 等完整功能,讓你的視頻項目立刻具備“會說話的屏幕”。
彈幕系統概述
彈幕的定義
所謂彈幕,就是用戶在觀看視頻時發送的即時評論,這些評論會在視頻畫面上以滾動的形式(從右向左)浮動顯示。它的特點在于:
- 即時性:觀眾的評論會幾乎同步出現在所有人的屏幕上。
- 互動性:觀眾能即時看到他人反應,形成群體觀看氛圍。
- 時間點關聯:彈幕通常綁定在視頻的特定播放時刻。
- 視覺沖擊:大量彈幕同時出現時,屏幕會像被“刷屏”一樣,帶來強烈的視覺體驗。
技術挑戰
一個成熟的彈幕系統,不僅要快,還要穩。主要挑戰包括:
- 高并發下的低延遲通信
- 大量消息的傳輸與分發
- 敏感詞過濾與格式規范
- 彈幕的歷史存儲與回放
系統架構設計
我們實現的實時彈幕系統由以下核心模塊組成:
模塊 | 作用 |
前端播放器 | 播放視頻并渲染彈幕 |
WebSocket 服務端 | 負責實時消息收發 |
彈幕存儲層 | 保存歷史記錄 |
內容過濾組件 | 過濾敏感與違規內容 |
協議選擇
我們需要一種低延遲、雙向通信的協議來驅動彈幕實時性:
- WebSocket:全雙工通信,延遲極低,適合高實時性應用(本文選用)
- SSE:僅支持服務器單向推送,適合數據廣播場景
- 長輪詢:兼容性好但延遲高,僅作為兜底方案
后端實現(Spring Boot 3 + WebSocket)
后端目錄結構
/src
└── main
├── java
│ └── com
│ └── icoderoad
│ └── danmaku
│ ├── config
│ │ └── WebSocketConfig.java
│ ├── controller
│ │ └── DanmakuController.java
│ ├── dto
│ │ └── DanmakuDTO.java
│ ├── entity
│ │ └── Danmaku.java
│ ├── mapper
│ │ └── DanmakuMapper.java
│ └── service
│ └── DanmakuService.java
└── resources
└── application.ymlMaven 依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.5</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>WebSocket 配置
package com.icoderoad.danmaku.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic"); // 客戶端訂閱路徑
config.setApplicationDestinationPrefixes("/app"); // 客戶端發送路徑
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws-danmaku")
.setAllowedOriginPatterns("*")
.withSockJS(); // 兼容性支持
}
}實體與DTO
package com.icoderoad.danmaku.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("danmaku")
public class Danmaku {
@TableId(type = IdType.AUTO)
private Long id;
private String content;
private String color;
private Integer fontSize;
private Double time;
private String videoId;
private String userId;
private String username;
private LocalDateTime createdAt;
}
package com.icoderoad.danmaku.dto;
import lombok.Data;
@Data
public class DanmakuDTO {
private String content;
private String color = "#ffffff";
private Integer fontSize = 24;
private Double time;
private String videoId;
private String userId;
private String username;
}Mapperpackage com.icoderoad.danmaku.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.icoderoad.danmaku.entity.Danmaku;
import org.apache.ibatis.annotations.*;
import java.util.List;
@Mapper
public interface DanmakuMapper extends BaseMapper<Danmaku> {
@Select("SELECT * FROM danmaku WHERE video_id = #{videoId} ORDER BY time ASC")
List<Danmaku> findByVideoIdOrderByTimeAsc(String videoId);
@Select("SELECT * FROM danmaku WHERE video_id = #{videoId} AND time BETWEEN #{start} AND #{end} ORDER BY time ASC")
List<Danmaku> findByVideoIdAndTimeBetween(String videoId, Double start, Double end);
}服務層
package com.icoderoad.danmaku.service;
import com.icoderoad.danmaku.dto.DanmakuDTO;
import com.icoderoad.danmaku.entity.Danmaku;
import com.icoderoad.danmaku.mapper.DanmakuMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;
@Service
public class DanmakuService {
@Autowired
private DanmakuMapper mapper;
@Autowired
private SimpMessagingTemplate template;
public Danmaku saveDanmaku(DanmakuDTO dto) {
Danmaku d = new Danmaku();
d.setContent(filter(dto.getContent()));
d.setColor(dto.getColor());
d.setFontSize(dto.getFontSize());
d.setTime(dto.getTime());
d.setVideoId(dto.getVideoId());
d.setUserId(dto.getUserId());
d.setUsername(dto.getUsername());
d.setCreatedAt(LocalDateTime.now());
mapper.insert(d);
template.convertAndSend("/topic/video/" + d.getVideoId(), d);
return d;
}
public List<Danmaku> listByVideo(String videoId) {
return mapper.findByVideoIdOrderByTimeAsc(videoId);
}
public List<Danmaku> listByVideoAndTime(String videoId, Double start, Double end) {
return mapper.findByVideoIdAndTimeBetween(videoId, start, end);
}
private String filter(String content) {
String[] badWords = {"敏感詞1", "敏感詞2"};
String result = content;
for (String w : badWords) result = result.replaceAll(w, "***");
return result;
}
}控制器
package com.icoderoad.danmaku.controller;
import com.icoderoad.danmaku.dto.DanmakuDTO;
import com.icoderoad.danmaku.entity.Danmaku;
import com.icoderoad.danmaku.service.DanmakuService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/danmaku")
public class DanmakuController {
@Autowired
private DanmakuService service;
@MessageMapping("/danmaku/send")
public Danmaku send(DanmakuDTO dto) {
return service.saveDanmaku(dto);
}
@GetMapping("/video/{videoId}")
public ResponseEntity<List<Danmaku>> list(@PathVariable String videoId) {
return ResponseEntity.ok(service.listByVideo(videoId));
}
@GetMapping("/video/{videoId}/timerange")
public ResponseEntity<List<Danmaku>> listByTime(
@PathVariable String videoId,
@RequestParam Double start,
@RequestParam Double end) {
return ResponseEntity.ok(service.listByVideoAndTime(videoId, start, end));
}
}前端實現
前端主要用 HTML + CSS + JS + SockJS + STOMP 來連接后端 WebSocket 并渲染彈幕。 HTML、CSS 與 JS 代碼邏輯基本與原示例一致,可直接復用。
結論
通過本文的實踐,我們構建了一個 高性能、可擴展、支持實時互動 的彈幕系統。 它不僅適用于視頻平臺,也能擴展到 在線課堂、賽事直播、虛擬演唱會 等場景。 未來我們可以進一步引入 AI 彈幕審核、消息分布式存儲、延遲優化算法 等,讓系統在性能與體驗上更進一步。
屏幕不只是顯示畫面,它還可以承載情緒與對話。 讓我們把屏幕變成一個會回應觀眾的“現場”——這就是實時彈幕的魅力。






























