1.理论
"实时"在互联网技术中,是指信息从产生到被接收方看到,延迟极低(通常在毫秒到几百毫秒之间),并且具有持续、主动推送的特性,而不是接收方反复询问。
要实现这种效果,需要前端、后端、网络协议三者紧密配合。下面我从最基础的通信模型讲起,逐步深入到现代实时系统的完整架构。
一、核心思想:从"拉"到"推"
传统的 HTTP 请求是 客户端拉取(Pull) 模式:客户端主动问,服务器才回答。
实时系统需要 服务器推送(Push) 模式:服务器一有新数据,立刻主动发给客户端。
打个比方:
拉取 = 你每隔几秒打电话问外卖员"到了吗?"
推送 = 外卖员到了主动打电话给你。
二、实时通信的技术演进(后端 → 前端)
1. 短轮询(Short Polling)------ 最原始,不算真实时
-
过程 :前端用
setInterval每几秒发一次 HTTP 请求,问"有新消息吗?" -
缺点:大量无效请求,浪费带宽和 CPU;实时性差(延迟 = 轮询间隔)。
-
现状:几乎被淘汰。
2. 长轮询(Long Polling)------ 伪实时,兼容性好
-
过程:
-
前端发起 HTTP 请求,服务器挂起这个请求,不立即返回。
-
当有数据可推时,服务器才返回响应。
-
前端收到后立即再发起下一个长轮询请求。
-
-
优点:兼容所有 HTTP/1.1 环境,无需特殊协议。
-
缺点:依然有 HTTP 头开销;服务器需维护大量挂起连接;消息顺序难保证。
-
典型应用:早期网页版 IM(如 Facebook Chat 早期)。
3. Server-Sent Events (SSE) ------ 单向实时推送
-
过程:
-
前端通过
EventSourceAPI 建立连接,服务器持续输出text/event-stream格式的数据流。 -
连接不断开,服务器可随时发消息,但只能服务器→客户端。
-
-
优点:基于 HTTP,轻量,自动重连,支持自定义事件。
-
缺点:单向;不支持二进制数据(需 base64);浏览器连接数有限制。
-
场景:股票行情、微博推送、实时通知。
4. WebSocket ------ 真正的全双工实时通信
-
过程:
-
前端发起 HTTP 升级请求(
Upgrade: websocket)。 -
服务器同意后,协议升级为 WebSocket,连接保持。
-
此后双方可以随时互相发送消息(文本或二进制),帧头极小(2~14 字节)。
-
-
优点:
-
真正的实时、全双工。
-
极低延迟(可做到几十毫秒)。
-
服务端可主动推送,客户端也可主动发。
-
支持二进制数据。
-
-
缺点:需要服务器支持;部分老旧代理可能拦截。
-
场景:在线游戏、实时弹幕、协同编辑、交易系统。
5. WebRTC 数据通道 ------ P2P 极致实时
-
过程:浏览器之间直接建立点对点连接,通过 UDP 传输,中间可绕开服务器(但仍需信令服务器协助建立连接)。
-
优点:延迟极低(几十毫秒甚至更低),适合实时音视频和极速数据交换。
-
缺点:实现复杂,NAT 穿透困难,不适合大规模广播。
-
场景:视频会议、屏幕共享、P2P 游戏。
目前主流实时系统(弹幕、聊天、协作)的首选是 WebSocket,辅以 SSE 或长轮询做降级。
三、后端架构:如何支撑海量实时连接?
单个 WebSocket 连接很简单,但百万、千万并发时,后端面临巨大挑战。
1. 连接管理 ------ 事件驱动 + 非阻塞 I/O
传统线程/进程模型(一个连接一个线程)无法支撑大量连接。现代后端使用 事件驱动模型(如 Node.js、Netty、Nginx、EPoll),一个线程可处理数万连接。
2. 协议层 ------ WebSocket 帧处理
后端需要解析 WebSocket 帧(文本、二进制、ping/pong、关闭帧),并维护连接状态。
3. 消息路由 ------ 房间/频道(Room/Topic)
实时系统通常有"房间"概念(如直播间、聊天群)。
-
当一条消息到达服务器,服务器需根据消息的目标房间,找到该房间内所有在线连接,然后逐个发送。
-
高效的数据结构:哈希表 key = roomId,value = Set<ConnectionId>。
4. 水平扩展 ------ 多节点同步问题
当后端有多个服务器节点时,用户 A 连接到节点 1,用户 B 连接到节点 2。如果 A 发消息给 B,节点 1 必须知道 B 在节点 2 上。
解决方案:
-
消息代理(Message Broker):如 Redis Pub/Sub、Kafka、RabbitMQ。节点 1 将消息发布到 Redis 的某个频道,节点 2 订阅该频道,收到后再推给用户 B。
-
全量广播:节点 1 向所有其他节点广播该消息(效率低)。
-
一致性哈希:根据 roomId 或 userId 将连接固定到某个节点,减少跨节点通信。
5. 状态存储 ------ 在线状态、消息历史
-
在线状态:通常存储在 Redis(内存数据库),支持快速读写和过期设置。
-
消息历史:对于离线用户,需要存储消息,等用户上线后拉取。常用 Kafka + 持久化数据库(Cassandra、TiDB)。
四、前端实现:从接收到渲染的实时管道
后端推来数据后,前端要高效地接收并更新界面。
1. 接收数据 ------ WebSocket / SSE API
javascript
const ws = new WebSocket('wss://example.com/room/123');
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
// 交给渲染引擎
};
2. 数据处理 ------ 防抖、节流、缓冲
如果消息来得太快(如每秒几千条弹幕),直接每条都触发 DOM 更新会导致浏览器卡死。
策略:
-
缓冲队列 :将消息放入数组,用
requestAnimationFrame批量取出并渲染。 -
节流:限制每秒最多更新 UI 的次数(例如 60 次)。
3. 渲染 ------ 高性能动画
以弹幕为例:
-
使用 Canvas 代替 DOM 元素,在每一帧中重绘所有弹幕。
-
使用
requestAnimationFrame驱动动画循环,保证与屏幕刷新率同步。 -
对于文本聊天,使用虚拟滚动技术,只渲染可见区域的消息。
4. 重连与状态管理
-
WebSocket 可能断开(网络波动)。前端需实现指数退避重连。
-
重连后可能需要发送
sync消息,从服务器拉取离线期间遗漏的消息。
五、完整实时数据流示例(直播间弹幕)
-
连接建立
- 用户进入直播间,前端发起 WebSocket 连接,并携带 roomId、token。
-
后端接入
-
网关节点接收连接,将连接信息注册到 Redis(roomId → connectionId)。
-
该节点加入 Redis Pub/Sub 频道
room:123。
-
-
用户 A 发送弹幕
-
前端通过 WebSocket 发送
{type:'danmaku', content:'666', roomId:123}。 -
节点 1 收到后,将消息存储到消息队列(如 Kafka)用于持久化。
-
同时,节点 1 将消息发布到 Redis 频道
room:123。
-
-
广播给房间内所有用户
-
所有订阅了
room:123的节点(包括节点 1、节点 2、节点 3...)都会收到这条消息。 -
每个节点查询本地维护的房间连接表,找到所有连接(用户 B、C...),通过各自的 WebSocket 连接推送消息。
-
-
前端渲染
-
用户 B 的前端 WebSocket
onmessage触发,解析 JSON。 -
将弹幕数据加入渲染队列。
-
requestAnimationFrame循环读取队列,绘制到 Canvas 上。
-
-
延迟分析
-
A 发送 → 节点 1 处理(微秒级)→ Redis 广播(毫秒级)→ 节点 2 收到(毫秒级)→ 推送至 B(毫秒级)→ 浏览器渲染(几毫秒)。
-
端到端延迟通常 20~100ms。
-
六、不同实时场景的技术选型
| 场景 | 推荐技术 | 原因 |
|---|---|---|
| 股票/币价行情 | SSE 或 WebSocket | 单向推送,数据量大,要求轻量。 |
| 在线聊天(群聊/私聊) | WebSocket + Redis Pub/Sub | 全双工,需要发送消息,低延迟。 |
| 实时弹幕 | WebSocket + Canvas | 高吞吐,双向(弹幕可发),低延迟。 |
| 协同编辑(如 Google Docs) | WebSocket + OT/CRDT | 需要顺序保证和冲突解决,双向通信。 |
| 实时音视频 | WebRTC | 极低延迟,P2P,需要媒体传输。 |
| 服务器监控仪表盘 | SSE | 简单,单向推送指标数据。 |
| 游戏同步(非帧同步) | WebSocket / UDP (自定义) | 低延迟,可靠/不可靠混合。 |
七、总结:"实时"的本质
-
通信模型:从客户端拉取 → 服务器推送。
-
协议选择:WebSocket 是目前双向实时的主流;SSE 适合单向推送;WebRTC 用于极致 P2P。
-
后端挑战:连接管理、水平扩展、跨节点消息路由、状态同步。
-
前端挑战:高性能接收、缓冲、渲染、重连。
-
延迟权衡 :真正的实时并不意味着零延迟,而是低且可预测的延迟,用户体验平滑。
一句话:实时 = 持久化全双工通道 + 事件驱动推送 + 高效端到端渲染。
2.代码
用vue+springboot代码示例讲解
下面通过一个实时聊天室 的完整代码示例,讲解如何使用 Vue 3 + Spring Boot 2/3 + WebSocket 实现真正的实时通信。
一、整体架构
-
后端:Spring Boot 提供 WebSocket 端点,支持消息收发与广播。
-
前端:Vue 3 通过原生 WebSocket API 连接后端,发送/接收消息。
-
实时核心:WebSocket 全双工通道,服务器可主动推送消息到所有客户端。
二、后端实现(Spring Boot + WebSocket)
1. 添加依赖(pom.xml)
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
2. WebSocket 配置类
java
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.config.annotation.EnableWebSocket;
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
// 注册 WebSocket 处理器,并允许跨域(方便前端调试)
registry.addHandler(new ChatWebSocketHandler(), "/chat")
.setAllowedOrigins("*");
}
}
3. 核心处理器:处理 WebSocket 消息
java
import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.util.concurrent.CopyOnWriteArraySet;
public class ChatWebSocketHandler extends TextWebSocketHandler {
// 线程安全的在线会话集合
private static final CopyOnWriteArraySet<WebSocketSession> sessions = new CopyOnWriteArraySet<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
sessions.add(session);
System.out.println("新连接:" + session.getId());
broadcast("系统消息:有新人加入", "system");
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
String payload = message.getPayload();
System.out.println("收到消息:" + payload);
// 广播给所有客户端(包括自己,前端可做去重或乐观更新)
broadcast(payload, session.getId());
}
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) {
sessions.remove(session);
System.out.println("连接关闭:" + session.getId());
broadcast("系统消息:有人离开了", "system");
}
// 广播消息给所有在线客户端
private void broadcast(String msg, String excludeSessionId) {
for (WebSocketSession session : sessions) {
if (session.isOpen() && !session.getId().equals(excludeSessionId)) {
try {
session.sendMessage(new TextMessage(msg));
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
}
说明:这里简化了消息格式,实际项目中可将消息包装成 JSON(包含用户名、时间、内容等)。
4. 启动类(无特殊配置)
java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class RealTimeApplication {
public static void main(String[] args) {
SpringApplication.run(RealTimeApplication.class, args);
}
}
三、前端实现(Vue 3 + WebSocket API)
1. 创建 Vue 项目(略)
安装 axios 或直接使用 fetch,这里只需 WebSocket。
2. 聊天组件 ChatRoom.vue
vue
<template>
<div class="chat-container">
<div class="header">💬 实时聊天室 (Vue + Spring Boot WebSocket)</div>
<div class="messages" ref="messagesContainer">
<div v-for="(msg, idx) in messages" :key="idx" class="message" :class="msg.isOwn ? 'own' : 'other'">
<strong>{{ msg.sender }}</strong> {{ msg.text }}
<span class="time">{{ msg.time }}</span>
</div>
</div>
<div class="input-area">
<input v-model="inputText" @keyup.enter="sendMessage" placeholder="输入消息..." />
<button @click="sendMessage">发送</button>
</div>
<div class="status">{{ statusText }}</div>
</div>
</template>
<script>
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
export default {
name: 'ChatRoom',
setup() {
const messages = ref([])
const inputText = ref('')
const statusText = ref('连接中...')
let ws = null
const messagesContainer = ref(null)
// 生成随机用户名
const username = '用户_' + Math.floor(Math.random() * 1000)
// 添加消息
const addMessage = (sender, text, isOwn = false) => {
messages.value.push({
sender,
text,
time: new Date().toLocaleTimeString(),
isOwn
})
// 自动滚动到底部
nextTick(() => {
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
}
})
}
// 发送消息
const sendMessage = () => {
const text = inputText.value.trim()
if (!text || !ws || ws.readyState !== WebSocket.OPEN) return
const messageData = {
type: 'chat',
sender: username,
text: text,
timestamp: Date.now()
}
// 发送 JSON 字符串
ws.send(JSON.stringify(messageData))
// 乐观更新:立即显示在自己的聊天区
addMessage('我', text, true)
inputText.value = ''
}
// 初始化 WebSocket
const initWebSocket = () => {
// 注意:后端地址根据实际修改,Spring Boot 默认端口 8080
const wsUrl = 'ws://localhost:8080/chat'
ws = new WebSocket(wsUrl)
ws.onopen = () => {
statusText.value = '✅ 已连接 (实时推送)'
addMessage('系统', '欢迎进入聊天室', false)
}
ws.onmessage = (event) => {
// 后端广播的是纯文本,实际可解析 JSON
const raw = event.data
try {
const data = JSON.parse(raw)
// 避免重复显示自己刚发的(已经乐观更新过了)
if (data.sender !== username) {
addMessage(data.sender, data.text, false)
}
} catch (e) {
// 纯文本系统消息
addMessage('系统', raw, false)
}
}
ws.onclose = () => {
statusText.value = '❌ 连接断开,5秒后重连...'
setTimeout(() => {
initWebSocket()
}, 5000)
}
ws.onerror = (error) => {
console.error('WebSocket 错误', error)
statusText.value = '⚠️ 连接错误'
}
}
onMounted(() => {
initWebSocket()
})
onUnmounted(() => {
if (ws) {
ws.close()
}
})
return {
messages,
inputText,
statusText,
messagesContainer,
sendMessage
}
}
}
</script>
<style scoped>
.chat-container {
width: 500px;
max-width: 90%;
margin: 20px auto;
border: 1px solid #ccc;
border-radius: 12px;
overflow: hidden;
font-family: sans-serif;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.header {
background: #4f46e5;
color: white;
padding: 12px;
text-align: center;
font-weight: bold;
}
.messages {
height: 400px;
overflow-y: auto;
padding: 12px;
background: #f9fafb;
display: flex;
flex-direction: column;
gap: 8px;
}
.message {
max-width: 70%;
padding: 8px 12px;
border-radius: 18px;
word-wrap: break-word;
}
.message.own {
background: #4f46e5;
color: white;
align-self: flex-end;
}
.message.other {
background: #e5e7eb;
color: #1f2937;
align-self: flex-start;
}
.time {
font-size: 10px;
margin-left: 8px;
opacity: 0.7;
}
.input-area {
display: flex;
padding: 12px;
gap: 8px;
border-top: 1px solid #e2e8f0;
}
.input-area input {
flex: 1;
padding: 8px 12px;
border-radius: 24px;
border: 1px solid #cbd5e1;
outline: none;
}
.input-area button {
background: #4f46e5;
color: white;
border: none;
border-radius: 24px;
padding: 8px 16px;
cursor: pointer;
}
.status {
padding: 6px;
text-align: center;
font-size: 12px;
background: #f1f5f9;
color: #475569;
}
</style>
3. 在 App.vue 中使用该组件
vue
<template>
<ChatRoom />
</template>
<script>
import ChatRoom from './components/ChatRoom.vue'
export default {
components: { ChatRoom }
}
</script>
四、运行与测试
-
启动 Spring Boot :运行
RealTimeApplication,后端监听8080端口,WebSocket 路径ws://localhost:8080/chat。 -
启动 Vue 前端 :
npm run serve,通常端口5173或8080(注意与后端不冲突)。 -
打开多个浏览器窗口 访问前端页面,每个窗口会分配随机用户名。发送消息后,所有其他窗口会立即收到消息(延迟通常 < 50ms)。
五、实时原理讲解(结合代码)
| 层面 | 关键组件 | 实时性如何实现 |
|---|---|---|
| 传输协议 | WebSocket | 前端 new WebSocket(url) 发起 HTTP 升级握手,建立持久化全双工通道,不再需要客户端轮询。 |
| 后端连接管理 | CopyOnWriteArraySet<WebSocketSession> |
每当有客户端连接,会话存入集合;断开时移除。集合用于后续广播。 |
| 消息接收 | handleTextMessage |
后端收到任何消息后,立即触发 broadcast(),遍历所有在线会话并调用 sendMessage()。 |
| 主动推送 | session.sendMessage() |
服务器不等待请求,直接通过已建立的 WebSocket 连接将数据推送给客户端。这是"实时"的核心。 |
| 前端接收 | ws.onmessage |
浏览器接收到服务器推送的数据,立即解析并更新 DOM(Vue 响应式数据),用户瞬间看到消息。 |
| 断开重连 | ws.onclose + 定时器 |
保证网络波动后自动恢复连接,重新建立实时通道。 |
六、扩展:生产环境增强点
-
消息格式 :使用 JSON 统一结构,包含
type、data、sender、timestamp。 -
房间/频道 :增加
roomId,广播时只发送给同一房间的会话(使用ConcurrentHashMap存储房间与会话的映射)。 -
安全性 :添加 JWT 或 Token 验证(在 WebSocket 握手时通过
HandshakeInterceptor校验)。 -
水平扩展:使用 Redis Pub/Sub 实现多节点间的消息同步(一个节点收到消息后发布到 Redis,其他节点订阅并推送给本地客户端)。
-
前端优化 :对高频消息(如弹幕)使用
requestAnimationFrame+ Canvas 渲染,避免大量 DOM 操作。
通过这个完整的 Vue + Spring Boot 示例,你可以清晰理解"实时"的本质:WebSocket 全双工通道 + 服务器主动推送 + 前端即时渲染。