1. 短轮询(Short Polling):最简单的实时通信
核心原理
- 客户端定期发送 HTTP 请求(如每 2 秒),获取服务器最新数据
- 服务器收到请求后,立即返回当前数据(无论是否有更新)
- 适合数据更新频率低的场景(如公告通知、非实时榜单)
后端实现(Nest.js)
在 backend/src/modules/polling/polling.controller.ts
中实现接口:
typescript
typescript
import { Controller, Get } from '@nestjs/common';
@Controller('polling')
export class PollingController {
// 模拟实时数据(如用户消息)
private message = '初始消息:暂无新内容';
private count = 0;
// 短轮询接口:客户端定期调用
@Get('short')
getShortPollingData() {
// 模拟随机更新数据(实际场景中从数据库/消息队列获取)
if (Math.random() > 0.7) {
this.count++;
this.message = `新消息 ${this.count}:${new Date().toLocaleTimeString()}`;
}
// 立即返回当前数据
return {
code: 200,
data: { message: this.message },
timestamp: Date.now(),
};
}
}
前端实现(Vue3)
在 frontend/src/views/ShortPolling.vue
中实现定期请求逻辑:
xml
<template>
<div class="short-polling">
<h2>短轮询方案</h2>
<p>请求频率:2秒/次</p>
<div class="message-box">
<h3>最新消息:</h3>
<p>{{ message }}</p>
</div>
<button @click="togglePolling">{{ isPolling ? '停止轮询' : '开始轮询' }}</button>
</div>
</template>
<script setup lang="ts">
import { ref, onUnmounted } from 'vue';
import axios from 'axios';
const message = ref('初始消息:暂无新内容');
const isPolling = ref(true);
let timer: NodeJS.Timeout;
// 发起短轮询请求
const fetchShortPollingData = async () => {
try {
const res = await axios.get('http://localhost:3000/polling/short');
if (res.data.code === 200) {
message.value = res.data.data.message; // 更新页面数据
}
} catch (err) {
console.error('短轮询请求失败:', err);
}
};
// 启动/停止轮询
const togglePolling = () => {
isPolling.value = !isPolling.value;
if (isPolling.value) {
fetchShortPollingData(); // 立即请求一次
timer = setInterval(fetchShortPollingData, 2000); // 每2秒请求一次
} else {
clearInterval(timer); // 清除定时器
}
};
// 组件挂载时启动轮询
togglePolling();
// 组件卸载时清除定时器(避免内存泄漏)
onUnmounted(() => {
clearInterval(timer);
});
</script>
<style scoped>
.short-polling { padding: 20px; }
.message-box { margin: 20px 0; padding: 15px; border: 1px solid #eee; border-radius: 4px; }
button { padding: 8px 16px; background: #42b983; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>
优缺点对比
优点 | 缺点 |
---|---|
实现简单,无需特殊协议 | 频繁发送请求,浪费带宽和服务器资源 |
兼容性好(所有浏览器支持) | 实时性差(延迟取决于轮询间隔) |
2. 长轮询(Long Polling):优化版轮询
核心原理
- 客户端发送 HTTP 请求后,服务器保持连接不返回,直到有新数据或超时
- 服务器返回数据后,客户端立即发送下一次请求,形成「长连接循环」
- 相比短轮询,减少了请求次数,适合数据更新频率中等的场景(如 IM 聊天、实时通知)
后端实现(Nest.js)
在 backend/src/modules/polling/polling.controller.ts
中添加长轮询接口:
typescript
import { Controller, Get, HttpStatus } from '@nestjs/common';
import { Observable, Subject, timeout } from 'rxjs';
@Controller('polling')
export class PollingController {
private longPollingSubject = new Subject<string>(); // 用于触发数据更新
private message = '初始消息:暂无新内容';
private count = 0;
// 模拟外部数据更新(如数据库变更、其他服务推送)
constructor() {
setInterval(() => {
if (Math.random() > 0.5) {
this.count++;
this.message = `长轮询新消息 ${this.count}:${new Date().toLocaleTimeString()}`;
this.longPollingSubject.next(this.message); // 触发长连接返回
}
}, 3000);
}
// 长轮询接口:保持连接直到有新数据或超时
@Get('long')
getLongPollingData(): Observable<{ code: number; data: { message: string }; timestamp: number }> {
return new Observable((observer) => {
// 订阅数据更新事件
const subscription = this.longPollingSubject.subscribe((newMessage) => {
observer.next({
code: 200,
data: { message: newMessage },
timestamp: Date.now(),
});
observer.complete(); // 返回数据后关闭连接
subscription.unsubscribe(); // 取消订阅
});
// 超时处理(5秒无数据则返回空,避免连接一直挂着)
setTimeout(() => {
observer.next({
code: HttpStatus.NO_CONTENT, // 204 无内容
data: { message: this.message },
timestamp: Date.now(),
});
observer.complete();
subscription.unsubscribe();
}, 5000);
});
}
}
前端实现(Vue3)
在 frontend/src/views/LongPolling.vue
中实现长轮询循环:
xml
<template>
<div class="long-polling">
<h2>长轮询方案</h2>
<p>机制:服务器保持连接,有新数据才返回</p>
<div class="message-box">
<h3>最新消息:</h3>
<p>{{ message }}</p>
</div>
<button @click="togglePolling">{{ isPolling ? '停止轮询' : '开始轮询' }}</button>
</div>
</template>
<script setup lang="ts">
import { ref, onUnmounted } from 'vue';
import axios from 'axios';
const message = ref('初始消息:暂无新内容');
const isPolling = ref(true);
// 发起长轮询请求(递归调用,形成循环)
const fetchLongPollingData = async () => {
if (!isPolling.value) return; // 停止轮询则退出
try {
const res = await axios.get('http://localhost:3000/polling/long', {
timeout: 6000, // 超时时间需大于后端超时(5秒)
});
if (res.data.code === 200) {
message.value = res.data.data.message; // 有新数据则更新
}
// 无论是否有新数据,立即发起下一次请求
setTimeout(fetchLongPollingData, 0);
} catch (err) {
console.error('长轮询请求失败:', err);
// 出错后延迟重试(避免频繁报错)
setTimeout(fetchLongPollingData, 1000);
}
};
// 启动/停止长轮询
const togglePolling = () => {
isPolling.value = !isPolling.value;
if (isPolling.value) {
fetchLongPollingData(); // 启动循环
}
};
// 组件挂载时启动
togglePolling();
// 组件卸载时停止
onUnmounted(() => {
isPolling.value = false;
});
</script>
<style scoped>
/* 样式与短轮询类似,可复用 */
.long-polling { padding: 20px; }
.message-box { margin: 20px 0; padding: 15px; border: 1px solid #eee; border-radius: 4px; }
button { padding: 8px 16px; background: #42b983; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>
优缺点对比
优点 | 缺点 |
---|---|
减少请求次数,节省资源 | 服务器需保持连接,占用内存 |
实时性比短轮询好 | 超时控制复杂,可能出现连接堆积 |
3. 服务器发送事件(SSE):单向实时推送
核心原理
- 基于 HTTP 协议,建立「客户端 → 服务器」的单向长连接
- 服务器通过该连接持续向客户端推送数据(支持文本、JSON 等格式)
- 适合服务器单向推送场景(如实时日志、数据看板、股票行情)
后端实现(Nest.js)
在 backend/src/modules/sse/sse.controller.ts
中实现 SSE 接口:
typescript
import { Controller, Get, Res, Sse } from '@nestjs/common';
import { Response } from 'express';
import { Observable, interval, map } from 'rxjs';
@Controller('sse')
export class SseController {
// 方案1:使用 Nest.js 内置 Sse 装饰器(推荐)
@Sse('data')
getSseData(): Observable<MessageEvent> {
// 每2秒推送一次数据(模拟实时更新)
return interval(2000).pipe(
map((count) => {
const message = `SSE 推送数据 ${count + 1}:${new Date().toLocaleTimeString()}`;
return {
type: 'message', // 事件类型(客户端可按类型监听)
data: { message }, // 推送数据
id: count.toString(), // 事件ID
} as MessageEvent;
}),
);
}
// 方案2:手动处理 SSE 响应头(适合自定义场景)
@Get('custom')
getCustomSse(@Res() res: Response) {
// 设置 SSE 响应头(必须)
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 每3秒推送一次数据
let count = 0;
const timer = setInterval(() => {
count++;
const message = `自定义 SSE 数据 ${count}:${new Date().toLocaleTimeString()}`;
// SSE 数据格式:data: {JSON}\n\n(必须以\n\n结尾)
res.write(`data: ${JSON.stringify({ message })}\n\n`);
// 模拟客户端断开连接,清理定时器
res.on('close', () => {
clearInterval(timer);
res.end();
});
}, 3000);
}
}
前端实现(Vue3)
在 frontend/src/views/SSE.vue
中监听服务器推送:
xml
<template>
<div class="sse">
<h2>服务器发送事件(SSE)</h2>
<p>机制:服务器单向推送,客户端被动接收</p>
<div class="message-box">
<h3>推送历史:</h3>
<ul>
<li v-for="(item, index) in messageList" :key="index">{{ item }}</li>
</ul>
</div>
<button @click="toggleSSE">{{ isConnected ? '断开连接' : '建立连接' }}</button>
</div>
</template>
<script setup lang="ts">
import { ref, onUnmounted } from 'vue';
const messageList = ref<string[]>(['等待服务器推送...']);
const isConnected = ref(false);
let eventSource: EventSource | null = null;
// 建立/断开 SSE 连接
const toggleSSE = () => {
if (isConnected.value) {
// 断开连接
eventSource?.close();
eventSource = null;
isConnected.value = false;
messageList.value.push('已断开 SSE 连接');
} else {
// 建立连接(使用 Nest.js 内置 SSE 接口)
eventSource = new EventSource('http://localhost:3000/sse/data');
// 监听正常推送(type: 'message')
eventSource.onmessage = (event) => {
const data = JSON.parse(event.data);
messageList.value.push(data.message);
// 只保留最近10条记录,避免页面卡顿
if (messageList.value.length > 10) {
messageList.value.shift();
}
};
// 监听自定义事件类型(可选)
eventSource.addEventListener('customEvent', (event) => {
messageList.value.push(`自定义事件:${event.data}`);
});
// 监听连接错误
eventSource.onerror = (error) => {
console.error('SSE 连接错误:', error);
messageList.value.push('SSE 连接异常,已断开');
isConnected.value = false;
eventSource.close();
};
// 监听连接成功
eventSource.onopen = () => {
isConnected.value = true;
messageList.value.push('已建立 SSE 连接');
};
}
};
// 组件卸载时断开连接
onUnmounted(() => {
eventSource?.close();
});
</script>
<style scoped>
.sse { padding: 20px; }
.message-box { margin: 20px 0; padding: 15px; border: 1px solid #eee; border-radius: 4px; max-height: 300px; overflow-y: auto; }
ul { list-style: disc; margin-left: 20px; }
li { margin: 5px 0; }
button { padding: 8px 16px; background: #42b983; color: white; border: none; border-radius: 4px; cursor: pointer; }
</style>
优缺点对比
优点 | 缺点 |
---|---|
单向推送,节省客户端资源 | 仅支持服务器 → 客户端单向通信 |
自动重连(浏览器原生支持) | 单个连接只能推送文本数据(需序列化) |
轻量级,无需复杂协议 | 部分代理服务器可能不支持长连接 |
4. WebSocket:全双工实时通信
核心原理
- 基于 TCP 协议,建立「客户端 ↔ 服务器」的全双工长连接
- 连接建立后,双方可随时发送数据(无需重复建立连接)
- 实时性最好,适合频繁双向通信场景(如 IM 聊天、在线协作、游戏)
后端实现(Nest.js)
使用 @nestjs/websockets
和 socket.io
实现 WebSocket 服务:
- 首先在
backend/src/app.module.ts
中导入 WebSocket 模块:
typescript
typescript
import { Module } from '@nestjs/common';
import { WebSocketModule } from './modules/websocket/websocket.module';
@Module({
imports: [
WebSocketModule,
// 其他模块...
],
})
export class AppModule {}
- 在
backend/src/modules/websocket/websocket.gateway.ts
中实现网关:
typescript
typescript
import { WebSocketGateway, WebSocketServer, SubscribeMessage, OnGatewayConnection, OnGatewayDisconnect } from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
// 配置跨域(允许前端访问)
@WebSocketGateway({
cors: {
origin: 'http://localhost:5173', // 前端地址
methods: ['GET', 'POST'],
credentials: true,
},
port: 3001, // WebSocket 独立端口(避免与 HTTP 端口冲突)
})
export class WebSocketGateway implements OnGatewayConnection, OnGatewayDisconnect {
@WebSocketServer() server: Server; // 全局服务器实例
private onlineUsers = 0; // 在线用户数
// 客户端连接时触发
handleConnection(client: Socket) {
this.onlineUsers++;
console.log(`客户端连接:${client.id},当前在线:${this.onlineUsers}`);
// 向新连接的客户端发送欢迎消息
client.emit('welcome', { message: `欢迎连接 WebSocket,你的ID:${client.id}` });
// 向所有客户端广播在线人数
this.server.emit('onlineCount', { count: this.onlineUsers });
}
// 客户端断开时触发
handleDisconnect(client: Socket) {
this.onlineUsers--;
console.log(`客户端断开:${client.id},当前在线:${this.onlineUsers}`);
// 广播在线人数更新
this.server.emit('onlineCount', { count: this.onlineUsers });
}
// 监听客户端发送的「sendMessage」事件
@SubscribeMessage('sendMessage')
handleSendMessage(client: Socket, data: { content: string }) {
console.log(`收到 ${client.id} 的消息:${data.content}`);
// 方案1:向所有客户端广播消息(含发送者)
this.server.emit('newMessage', {
from: client.id,
content: data.content,
time: new Date().toLocaleTimeString(),
});
// 方案2:仅向发送者以外的客户端广播(可选)
// client.broadcast.emit('newMessage', { from: client.id, content: data.content });
}
// 模拟服务器主动推送(如定时推送)
constructor() {
setInterval(() => {
this.server.emit('serverPush', {
message: `服务器定时推送:${new Date().toLocaleTimeString()}`,
});
}, 5000);
}
}
- 在
backend/src/modules/websocket/websocket.module.ts
中导出模块:
typescript
python
import { Module } from '@nestjs/common';
import { WebSocketGateway } from './websocket.gateway';
@Module({
providers: [WebSocketGateway],
})
export class WebSocketModule {}
前端实现(Vue3)
- 在
frontend/src/views/WebSocket.vue
中实现客户端逻辑:
xml
<template>
<div class="websocket">
<h2>WebSocket 全双工通信</h2>
<p>机制:客户端 ↔ 服务器双向实时通信</p>
<div class="status">
<p>连接状态:{{ isConnected ? '✅ 已连接' : '❌ 未连接' }}</p>
<p>在线人数:{{ onlineCount }}</p>
</div>
<!-- 消息列表 -->
<div class="message-list">
<h3>消息记录:</h3>
<div v-for="(msg, index) in messageList" :key="index" class="message-item">
<span class="time">{{ msg.time }}</span>
<span class="from">{{ msg.from }}:</span>
<span class="content">{{ msg.content }}</span>
</div>
</div>
<!-- 发送消息表单 -->
<div class="send-form">
<input
v-model="inputContent"
placeholder="输入消息..."
@keyup.enter="sendMessage"
/>
<button @click="sendMessage" :disabled="!isConnected">发送</button>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onUnmounted } from 'vue';
import { io } from 'socket.io-client';
const isConnected = ref(false);
const onlineCount = ref(0);
const inputContent = ref('');
const messageList = ref<Array<{ from: string; content: string; time: string }>>([]);
let socket: ReturnType<typeof io> | null = null;
// 连接 WebSocket
const connectWebSocket = () => {
// 连接后端 WebSocket 服务(端口 3001)
socket = io('http://localhost:3001');
// 连接成功
socket.on('connect', () => {
isConnected.value = true;
messageList.value.push({
from: '系统',
content: '已成功连接 WebSocket',
time: new Date().toLocaleTimeString(),
});
});
// 连接失败
socket.on('connect_error', (error) => {
console.error('WebSocket 连接失败:', error);
isConnected.value = false;
messageList.value.push({
from: '系统',
content: 'WebSocket 连接失败,请重试',
time: new Date().toLocaleTimeString(),
});
});
// 断开连接
socket.on('disconnect', () => {
isConnected.value = false;
messageList.value.push({
from: '系统',
content: 'WebSocket 已断开连接',
time: new Date().toLocaleTimeString(),
});
});
// 监听欢迎消息
socket.on('welcome', (data) => {
messageList.value.push({
from: '系统',
content: data.message,
time: new Date().toLocaleTimeString(),
});
});
// 监听在线人数更新
socket.on('onlineCount', (data) => {
onlineCount.value = data.count;
});
// 监听新消息
socket.on('newMessage', (data) => {
messageList.value.push(data);
// 滚动到最新消息
const messageListEl = document.querySelector('.message-list');
if (messageListEl) {
messageListEl.scrollTop = messageListEl.scrollHeight;
}
});
// 监听服务器定时推送
socket.on('serverPush', (data) => {
messageList.value.push({
from: '服务器',
content: data.message,
time: new Date().toLocaleTimeString(),
});
});
};
// 发送消息
const sendMessage = () => {
if (!inputContent.value.trim() || !isConnected.value) return;
// 向服务器发送「sendMessage」事件
socket?.emit('sendMessage', { content: inputContent.value.trim() });
// 清空输入框
inputContent.value = '';
};
// 组件挂载时连接
connectWebSocket();
// 组件卸载时断开连接
onUnmounted(() => {
socket?.disconnect();
});
</script>
<style scoped>
.websocket { padding: 20px; }
.status { margin: 10px 0; color: #666; }
.message-list { margin: 20px 0; padding: 15px; border: 1px solid #eee; border-radius: 4px; max-height: 300px; overflow-y: auto; }
.message-item { margin: 8px 0; padding: 5px; border-bottom: 1px dashed #f5f5f5; }
.time { color: #999; font-size: 12px; margin-right: 10px; }
.from { font-weight: bold; margin-right: 10px; }
.send-form { display: flex; gap: 10px; }
input { flex: 1; padding: 8px; border: 1px solid #eee; border-radius: 4px; }
button { padding: 8px 16px; background: #42b983; color: white; border: none; border-radius: 4px; cursor: pointer; }
button:disabled { background: #ccc; cursor: not-allowed; }
</style>
优缺点对比
优点 | 缺点 |
---|---|
全双工通信,实时性最好 | 实现复杂度高,需处理连接状态 |
减少连接次数,节省资源 | 部分老旧浏览器不支持(需降级方案) |
支持二进制数据传输 | 服务器需维护长连接,占用内存较多 |
四、四种方案对比与选型建议
通过前面的实现,我们可以清晰看到四种方案的差异,实际项目中需根据业务场景选择:
对比维度 | 短轮询 | 长轮询 | SSE | WebSocket |
---|---|---|---|---|
通信方向 | 客户端 → 服务器(单向) | 客户端 → 服务器(单向) | 服务器 → 客户端(单向) | 双向 |
实时性 | 差(取决于轮询间隔) | 中 | 好 | 最好 |
服务器资源占用 | 高(频繁请求) | 中(保持连接) | 中(单向长连接) | 低(单连接复用) |
实现复杂度 | 极低 | 低 | 中 | 高 |
兼容性 | 所有浏览器支持 | 所有浏览器支持 | IE 不支持 | IE 10+ 支持 |
适用场景 | 数据更新频率低(如公告) | 数据更新频率中等(如通知) | 服务器单向推送(如日志) | 频繁双向通信(如 IM) |
选型建议
- 快速验证需求:优先用短轮询(实现快,无需复杂配置)
- 通知类场景:用长轮询或 SSE(减少请求,节省资源)
- 实时看板 / 日志:用 SSE(单向推送,客户端无需发送数据)
- IM / 在线协作:必须用 WebSocket(全双工通信,实时性要求高)
- 兼容性要求高:短轮询 + WebSocket 降级(老浏览器用短轮询,新浏览器用 WebSocket)
git地址:gitee.com/xcxsj/realt...