在传统Web应用中,客户端需要不断向服务器发起请求才能获取最新数据,这种方式对于实时性要求高的场景(如在线聊天、股票行情、多人协作编辑等)存在明显不足。服务器主动推送技术的出现,彻底改变了这种被动获取数据的模式,让服务器能够在数据产生时立即推送给客户端。
我经历过从早期Comet技术到现代WebSocket的完整技术演进过程。记得2012年做一个在线客服系统时,当时还不得不使用长轮询方案来模拟推送效果,每次看到那个不断旋转的浏览器加载图标都让人头疼。如今随着HTML5标准的普及,我们已经有了更优雅的解决方案。
短轮询是最简单粗暴的"伪推送"实现方式。客户端通过setInterval定时器定期向服务器发送请求,无论服务器是否有新数据都会立即响应。
javascript复制// 典型短轮询实现
const pollInterval = 5000; // 每5秒轮询一次
function startPolling() {
setInterval(async () => {
try {
const response = await fetch('/api/updates');
const data = await response.json();
updateUI(data);
} catch (error) {
console.error('轮询请求失败:', error);
}
}, pollInterval);
}
在我参与过的一个物流跟踪系统中,初期就采用了短轮询方案。当时的需求是每30秒更新一次货物位置,这种低频更新场景下短轮询确实能胜任。
但存在三个明显问题:
实际经验:短轮询间隔不宜低于10秒,否则服务器压力会呈指数级增长。我曾见过一个每1秒轮询的页面,在用户量达到2000时直接拖垮了整个后端服务。
长轮询是对短轮询的改进,核心区别在于服务器会保持连接直到有数据或超时。下面是典型实现:
javascript复制// 客户端实现
async function longPoll() {
try {
const response = await fetch('/api/long-poll');
if (response.status === 204) { // 超时无数据
return longPoll();
}
const data = await response.json();
processData(data);
longPoll(); // 立即发起下一次请求
} catch (error) {
setTimeout(longPoll, 5000); // 错误时延迟重试
}
}
java复制// 服务端实现(Spring Boot)
@GetMapping("/api/long-poll")
public DeferredResult<String> longPollRequest() {
DeferredResult<String> deferredResult = new DeferredResult<>(30000L);
// 模拟5秒后返回数据
scheduledExecutor.schedule(() -> {
deferredResult.setResult("New data at " + new Date());
}, 5, TimeUnit.SECONDS);
return deferredResult;
}
在电商秒杀系统的实时库存更新中,我们曾采用长轮询方案。遇到几个典型问题:
连接泄漏:服务器未正确关闭超时连接,导致Tomcat线程池耗尽
消息乱序:网络延迟导致消息到达顺序错乱
重连风暴:网络抖动时客户端集体重连
SSE是HTML5标准中的轻量级推送协议,基于HTTP长连接。与WebSocket不同,SSE是单向通信(服务端→客户端),但实现更简单。
javascript复制// 客户端实现
const eventSource = new EventSource('/api/sse');
eventSource.onmessage = (event) => {
console.log('新消息:', event.data);
};
eventSource.addEventListener('stock', (event) => {
const stockData = JSON.parse(event.data);
updateStockChart(stockData);
});
python复制# Flask服务端实现
@app.route('/api/sse')
def sse_stream():
def generate():
while True:
data = get_live_data()
yield f"data: {json.dumps(data)}\n\n"
time.sleep(1)
return Response(generate(), mimetype='text/event-stream')
在金融行情系统中,我们充分利用了SSE的高级特性:
事件类型区分:为不同数据类型定义不同事件名
javascript复制// 服务端
res.write(`event: kline\ndata: ${JSON.stringify(klineData)}\n\n`);
// 客户端
eventSource.addEventListener('kline', processKlineData);
重连机制:自动重连+Last-Event-ID
http复制GET /api/sse
Last-Event-ID: 12345
自定义重试时间:
http复制retry: 10000\n
性能提示:SSE默认每个浏览器域名限制6个并发连接。对于多数据流场景,建议使用单个连接+多事件类型,而非创建多个SSE连接。
WebSocket建立连接需要经过HTTP升级握手:
http复制GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
http复制HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
javascript复制// 客户端
const socket = new WebSocket('wss://api.example.com/ws');
socket.onopen = () => {
console.log('连接已建立');
socket.send(JSON.stringify({action: 'subscribe', topic: 'live'}));
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
renderLiveData(data);
};
socket.onclose = () => {
console.log('连接断开,5秒后重连...');
setTimeout(connectWebSocket, 5000);
};
java复制// Spring Boot服务端
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(new LiveDataHandler(), "/ws")
.setAllowedOrigins("*");
}
}
public class LiveDataHandler extends TextWebSocketHandler {
@Override
public void afterConnectionEstablished(WebSocketSession session) {
// 新连接处理
}
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage message) {
// 处理客户端消息
}
}
心跳机制:防止中间设备断开空闲连接
javascript复制// 每30秒发送心跳
setInterval(() => {
if (socket.readyState === WebSocket.OPEN) {
socket.send('{"type":"ping"}');
}
}, 30000);
消息压缩:对于高频小消息,建议启用permessage-deflate扩展
nginx复制location /ws {
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_http_version 1.1;
proxy_pass http://backend;
}
安全防护:
html复制<!-- 客户端 -->
<iframe id="comet" src="/comet-stream" style="display:none;"></iframe>
<script>
window.handleCometData = function(data) {
console.log('收到数据:', data);
};
</script>
php复制// 服务端实现
header('Content-Type: text/html');
header('Connection: keep-alive');
for ($i = 0; $i < 10; $i++) {
echo "<script>parent.handleCometData('msg-$i');</script>";
flush();
sleep(1);
}
javascript复制function jsonpPoll() {
const script = document.createElement('script');
script.src = `https://api.example.com/push?callback=handlePush&_=${Date.now()}`;
document.body.appendChild(script);
}
window.handlePush = function(data) {
processData(data);
jsonpPoll(); // 立即发起下一次请求
};
历史经验:在2010年左右做跨域实时推送时,JSONP长轮询是唯一可行的方案。但存在严重的安全隐患,现在应该优先考虑CORS+SSE/WebSocket。
| 技术指标 | 短轮询 | 长轮询 | SSE | WebSocket | Iframe流 |
|---|---|---|---|---|---|
| 实时性 | 差 | 中 | 高 | 极高 | 高 |
| 带宽效率 | 低 | 中 | 高 | 极高 | 中 |
| CPU消耗 | 高 | 中 | 低 | 低 | 中 |
| 双向通信 | 否 | 否 | 否 | 是 | 否 |
| 二进制支持 | 是 | 是 | 否 | 是 | 否 |
| 浏览器兼容 | 全部 | 全部 | IE11+ | IE10+ | 全部 |
| 移动端友好 | 差 | 中 | 优 | 优 | 差 |
金融实时行情:
新闻实时推送:
在线协作编辑:
IoT设备监控:
在实际项目中,我通常会采用分层架构:
javascript复制function createRealtimeConnection(options) {
// 特性检测
if (typeof WebSocket !== 'undefined') {
return new WebSocketTransport(options);
} else if (typeof EventSource !== 'undefined') {
return new SSETransport(options);
} else {
return new LongPollTransport(options);
}
}
// 统一API接口
const connection = createRealtimeConnection({
url: 'wss://api.example.com/realtime',
onMessage: handleMessage,
onError: handleError
});
connection.subscribe('room:123');
这种架构的优势在于:
连接管理:
消息广播:
java复制// 使用Redis PubSub进行集群内广播
@Autowired
private RedisTemplate<String, String> redisTemplate;
public void broadcast(String channel, String message) {
redisTemplate.convertAndSend(channel, message);
}
消息批处理:
javascript复制let batch = [];
const BATCH_INTERVAL = 100;
socket.onmessage = (event) => {
batch.push(JSON.parse(event.data));
if (!batchTimeout) {
batchTimeout = setTimeout(processBatch, BATCH_INTERVAL);
}
};
function processBatch() {
renderBatch(batch);
batch = [];
batchTimeout = null;
}
离线缓存:
javascript复制// 使用IndexedDB缓存离线消息
const dbPromise = idb.open('message-store', 1, upgradeDB => {
upgradeDB.createObjectStore('messages', {keyPath: 'id'});
});
socket.onmessage = async (event) => {
const db = await dbPromise;
const tx = db.transaction('messages', 'readwrite');
tx.objectStore('messages').put(event.data);
};
连接健康度:
消息质量:
问题1:连接频繁断开
问题2:消息延迟高
问题3:CPU使用率高
HTTP/3基于QUIC协议,具有:
javascript复制// 实验性WebSocket over HTTP/3
const socket = new WebSocket('wss://example.com/ws', {
transport: 'quic'
});
新一代传输协议,结合了WebSocket和HTTP/3的优点:
javascript复制const transport = new WebTransport('https://example.com:4433/transport');
async function setup() {
await transport.ready;
const stream = await transport.createBidirectionalStream();
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();
}
在实际项目中选择推送技术时,需要综合考虑团队技术栈、业务需求和用户设备情况。对于新项目,我的建议是优先采用WebSocket+SSE降级的方案,既能满足现代浏览器的优秀体验,又能兼容旧版浏览器的基础功能。