一、前言
如今,Web 应用愈发复杂,用户对实时交互体验的要求也越来越高,比如:社媒的即时通讯、大屏的数据更新、实时消息的提醒等,这些都表明实时交互已成高品质应用的必备特性。而作为开发者,我们常面对复杂的开发环境,要应对即时通讯与数据实时更新的问题。那么,该如何精准高效实现这些功能呢?我们将共同探讨下,轮询、Web Socket 、SSE(Server-Sent Events) 三种解决方案,最终根据当下场景选出最优方案,打造更为出色的产品。
二、方案一:轮询(Polling)
1. 短轮询
实现短轮询,我们可以采用定时器的方式来实现,让客户端每隔较短固定时间就向服务端发起请求,无论服务器有无新消息,都会正常给予回应。短轮询的优劣势一目了然。优势在于,容易理解、实现过程简便,同时兼容性又很好,在几乎所有支持 HTTP 协议的浏览器及服务器环境都能很好的运行短轮询。不过,缺点也非常显而易见。当按照很短的固定时间间隔去频繁请求数据,如果此时的数据并未更新,这些请求就成了无效请求,但是每一个无效的请求都得完成 HTTP 建立连接的一系列流程,像三次握手 、 四次挥手 ,这无疑造成了不必要的资源浪费。同时也是由于按固定间隔请求,数据更新也可能会存在延迟的现象,在要求实时性的场景下就不满足了。
图片
2. 长轮询
相对于短轮询,长轮询的实现过程会复杂一些。首先,同样是由客户端向服务端发送HTTP请求后,不过与短轮询的差别在于,服务器接到请求后并不即刻返回响应,而是选择将该请求暂时挂起,静静等待数据更新,或是直至达到特定的超时时间(这个超时时间通常比短轮询间隔长)。在等待期间,如果有数据更新了,就会立即将新数据返回给客户端,若一直未等到数据更新,直到达到超时时间,服务器才返回一个空响应或者告知客户端无新数据。客户端收到响应后,无论是否有新数据,都会立即再次发起新的长轮询请求,如此循环往复。
这种长轮询方式相较短轮询存在一定优势,首先减少了无效请求的次数,因为只有在数据更新的时候,服务器才会响应,实时性得到了增强,也同时在一定程度上节省了网络资源。长轮询虽然减少了无效请求,但是长时间挂起的请求仍会占用服务器的内存、线程等关键资源。若同时存在大量长轮询请求,且长时间处于等待数据更新状态,服务器资源可能会被大量消耗,同样可能引发性能问题。
图片
由此可见, 无论是运用长轮询还是短轮询策略,在对实时性有着严格要求的场景之下这都不是一个非常好的方案。一方面,频繁的无效请求以及长时间挂起的连接,极易造成服务器资源的浪费。另一方面,由于轮询时间间隔的固定性,一旦设置的不合理,就可能错失数据更新的最佳时机,无法及时将关键信息推送给客户端,导致实时交互出现延迟。
三、方案二:Web Socket
Web Socket 是一种基于单个 TCP 连接的协议,拥有实现 全双工 通信的能力。它让客户端和服务端可以双向、实时传输数据,摆脱了传统 HTTP 请求那种请求 - 响应模式的限制。有了 Web Socket,服务器能随时主动给客户端推送消息,客户端也能马上向服务器发数据,就如同构建起了一条实时双向通道。
图片
1.Web Socket通信原理
1)握手阶段
- 初始请求阶段
a.首先,我们可以从下图实现效果中看出来,起始于客户端向服务端发送了一个HTTP请求,但这个请求和常规的HTTP请求又不太一致,他包含了两个特殊的请求头, Upgrade: websocket 和 Connection: Upgrade。
b.Upgrade: websocket 像是一个信号,它的作用就是用来告知服务器,客户端希望将当前的通信协议从 HTTP升级为Web Socket协议。
c.Connection: Upgrade 是与Upgrade: websocket 进行配合使用的,它的作用是告诉服务器,不要把这个请求当作普通的 HTTP 请求来处理,而是要关注 “Upgrade” 请求头中的内容,按照要求进行协议的升级。
- 服务器响应阶段
- 服务器收到客户端的升级请求后,如果支持 Web Socket 协议,就会返回一个响应来完成握手过程。响应状态码通常是 101,表示 协议切换。响应头也会包含和客户端请求对应的 Upgrade 和 Connection 字段,确认连接升级。Sec - WebSocket - Accept ,它是根据客户端请求中的 Sec - WebSocket - Key 生成的,用于安全验证。通过这个握手过程,双方就建立了一个稳定的 Web Socket 连接这样,这个连接是基于 TCP的,为后续的双向通信做好了准备。
2) 数据传输阶段
- 我们从下图中可以看出,Web socket全双工通道的特点,客户端可以向服务端主动发送消息,服务端也可以推送数据到服务端。具体的数据格式,我们可以通过抓包软件来进行抓包看下。
3) 连接维护和关闭阶段
- 在整个通信过程中,Web Socket 协议要求客户端和服务器都要维护连接的状态。连接状态包括连接是否打开、正在关闭或者已经关闭等。客户端和服务器通过心跳机制(发送周期性的小数据包来检测对方是否还在线)或者其他自定义的连接检测方法来确保连接的稳定性。
- 当需要关闭 WebSocket 连接时,无论是客户端还是服务器都可以发起关闭请求。关闭请求也是通过发送一个特定的帧(关闭帧)来实现的。对方在收到关闭帧后,会进行一些必要的清理工作,如释放资源等,然后关闭连接。
2.示例
- 模拟客户端代码
<div id="app">
<input v-model="message" placeholder="输入要发送的消息" />
<button @click="sendMessage">发送消息</button>
<div>
<h3>收到的回复记录:</h3>
<ul>
<li v-for="(msg, index) in receivedMessages" :key="msg">{{ msg }}</li>
</ul>
</div>
</div>
<script>
const app = new Vue({
el: "#app",
data() {
return {
message: "",
receivedMessages: [], // 新增数组用于存储所有收到的消息
socket: null,
};
},
mounted() {
// 创建WebSocket连接,这里的地址要和后端服务器监听的地址对应,这里假设后端在本地3000端口
this.socket = new WebSocket("ws://localhost:3000");
// 连接成功时触发的事件
this.socket.addEventListener("open", () => {
console.log("已连接到WebSocket服务器");
});
// 接收服务器发送消息的事件
this.socket.addEventListener("message", (event) => {
const receivedMsg = event.data;
this.receivedMessages.push(receivedMsg); // 将收到的消息添加到数组中
});
// 连接关闭时触发的事件
this.socket.addEventListener("close", () => {
console.log("与WebSocket服务器的连接已关闭");
});
},
methods: {
sendMessage() {
if (this.socket.readyState === WebSocket.OPEN) {
this.socket.send(this.message);
this.message = "";
} else {
console.log("WebSocket连接未就绪,无法发送消息");
}
},
},
});
</script>
- 模拟服务端发送请求
注意:下方代码中,定时器仅用来模拟,在实际业务中应该替换为业务代码
const WebSocket = require('ws');
// 创建WebSocket服务器实例,监听在3000端口,你可以根据需求修改端口号
const wss = new WebSocket.Server({ port: 3000 });
// 用于存储已连接的客户端WebSocket实例,方便后续向所有客户端发送消息等操作
const clients = [];
// 当有客户端连接时触发的事件
wss.on('connection', (ws) => {
console.log('客户端已连接');
clients.push(ws);
// 接收客户端发送的消息
ws.on('message', (message) => {
console.log(`收到客户端消息: ${message}`);
// 这里简单地将收到的消息加上一个后缀后再发回客户端
const responseMessage = `你发送的消息是:${message}`;
ws.send(responseMessage);
});
// 当客户端关闭连接时触发的事件
ws.on('close', () => {
console.log('客户端已断开连接');
const index = clients.indexOf(ws);
if (index > -1) {
clients.splice(index, 1);
}
});
});
// 【注意: 模拟一个定时任务,每隔1秒向所有已连接的客户端发送一条消息,你可以编写自己的业务代码】
setInterval(() => {
const messageToSend = '这是服务端主动发送的消息,当前时间:' + new Date().toLocaleString();
clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(messageToSend);
}
});
}, 1000);
console.log('WebSocket服务器已启动,正在监听3000端口...');
- 实现效果
图片
图片
3. 特点
相较于传统的 HTTP 请求,Web Socket 在实时性方面展现出了卓越优势,它打破了以往被动等待响应的模式,服务器得以实时且主动地将数据推送至客户端。在面对高实时性的场景下,大大的提升了用户体验。并且只需建立一次 TCP 连接,后续数据传输高效便捷,既减少了网络带宽的无谓占用,又减轻了服务器频繁处理连接请求的负担。然而,当应用场景出现变化,假设客户端仅仅是获取服务端推送的消息,自身并无向服务端发送信息的需求,此时,我们可以考虑下 SSE(Server-Sent Events)这个方案。
四、方案三:SSE
SSE(Server-Sent Events) 同样具备了服务端主动向客户端推送数据的能力,而无需客户端不断地发起请求。与Web Socket不同的是,SSE 是基于 HTTP 协议的,使用的是单向通信, 而Web Socket是基于TCP协议的,使用的话双向通信。
图片
1、SSE通信原理
1) 客户端发起请求
- 客户端需要发送一个特定的请求告知服务器准备接收事件。在传统的 HTTP 请求 - 响应模式中,一次请求完成后连接通常会关闭,但 SSE 通过在服务器端和客户端设置特定的头部信息,让连接持续开启。例如:设置了Content - Type头部为text/event - stream,并设置Cache - Control为no - cache以及Connection为keep - alive,这样告知客户端,这是一个 SSE 连接,数据会持续推送,并且不需要缓存数据。客户端只需向服务器发送一个普通的 HTTP 请求,指向服务器端提供 SSE 服务的特定端点就行,例如/events,就可以顺利开启SSE连接。
2)服务器响应请求
- 服务器收到客户端请求后,会维持一个长连接,并且定期向客户端发送事件数据。每个事件都是通过特定的格式(例如 data:\n\n)发送给客户端的。
3) 客户端接收数据
- 客户端通过 JavaScript 的 EventSource 对象接收从服务器推送过来的数据。这些数据可以是普通文本,也可以是 JSON 格式的对象,取决于服务器如何发送。
4) 事件流的关闭
- 一旦不再需要推送数据,比如客户端主动关闭连接,又或是中途出现错误,导致无法继续推送时,服务器便会果断关闭连接。
2、示例
- 服务端,使用express框架为例
注意:下方代码中,定时器仅用来模拟,在实际业务中应该替换为业务代码
const express = require('express');
const app = express();
const port = 3000;
// 设置响应头,表明这是一个SSE流
app.get('/events', (req, res) => {
res.setHeader('Content-Type','text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
// 【注意:模拟定时发送数据(实际应用中需要根据真实业务逻辑触发发送)!!!】
const interval = setInterval(() => {
const data = `data: { "time": "${new Date().toLocaleString()}"}\n\n`;
res.write(data);
}, 3000);
// 当客户端关闭连接时,清除定时器
req.on('close', () => {
clearInterval(interval);
});
});
app.listen(port, () => {
console.log(`服务器运行在 http://localhost:${port}`);
});
- 模拟客户端代码
mounted() {
const source = new EventSource('http://localhost:3000/events');
source.onmessage = (event) => {
const data = JSON.parse(event.data); // data 就是发送的消息
this.message = data.message;
};
source.onerror = (error) => {
console.log('SSE连接出错:', error);
};
}
- 实现效果
图片
图片
3.特点
对于SSE来说,优势上在于简单易用,客户端用 EventSource可以接收,服务器端定期发数据即可推送。且单向通信、基于长连接,服务器资源消耗低。不过,SSE 是有局限的。只能够进行单向通信,现代浏览器大多支持,但是IE或者旧版本浏览器不支持。SSE 只支持文本格式的数据流,不支持二进制数据传输。总之,SSE 在合适场景能发挥优势,支撑实时数据推送需求。
五、总结
上述文章中,介绍了轮询、Web Socket、SSE 三种方案。适合的场景也有一些差别,例如:轮询的方式可以在更新频率不高的场景下使用、Web Socket可以在需要双向交互的场景下使用、SSE适用于服务器到客户端的单向数据流的场景下使用。开发者可以根据场景选择最优方案,提升产品实时交互体验。