注册

即时通讯 - 短轮询、长轮询、长连接、WebSocket

实现即时通讯主要有四种方式,它们分别是短轮询、长轮询、长连接、WebSocket


1. 短轮询


1.1 说明


传统的web通信模式。后台处理数据,需要一定时间,前端想要知道后端的处理结果,就要不定时的向后端发出请求以获得最新情况,得到想要的结果,或者超出规定的最长时间就终止再发请求。


1.2 优点:


前后端程序编写比较容易


1.3 缺点:



  • 效率低:轮询的请求间隔时间一般是固定的,无论服务器是否有新的数据,都需要等待一段固定的时间。当数据更新的频率较低时,大部分请求都是无效的;
  • 实时性差:如果数据在两次请求间发生了更新,那么用户只能在下一次轮询时才能得到最新数据;
  • 浪费资源:高频率的操作功能,或者页面访问,导致的大量用户使用轮询时,会占用大量的网络资源,降低整体网络速度

1.4 基础实现:


每隔一段时间发送一个请求即可,得到想要的结果,或者超出规定的最长时间就终止再发请求。


let count = 0;
const timer = null;
// 超时时间
const MAX_TIME = 10 * 1000;
// 心跳间隙
const HEARTBEAT_INTERVAL = 1000;

/**
* @description: 模拟请求后端数据 (第6次时返回true)
*/

const fetchData = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log('fetch data...', count)
count += 1
if(count === 5) {
resolve(true);
}else {
resolve(false);
}
}, 1000)
});
};

/**
* @description: 异步轮询,当超时时或者接口返回true时,中断轮询
*/

const doSomething = async () => {
try {
let startTime = 0;
const timer = setInterval(async ()=>{
const res = await fetchData();
startTime += HEARTBEAT_INTERVAL;
if(res || startTime > MAX_TIME) {
clearInterval(timer)
}
}, HEARTBEAT_INTERVAL)

} catch (err) {
console.log(err);
}
};

doSomething();


2. 长轮询


2.1 说明


客户端向服务器发送Ajax请求,服务器接到请求后hold住连接,直到有新消息才返回响应信息并关闭连接,客户端处理完响应信息后再向服务器发送新的请求


long-polling.svg


长轮询的实现原理与轮询类似,只是客户端的请求会保持打开状态,直到服务器返回响应或超时。在服务器端,可以使用阻塞方式处理长轮询请求,即服务器线程会一直等待直到有新的数据或事件,然后返回响应给客户端。客户端收到响应后,可以处理数据或事件,并随后发送下一个长轮询请求。


2.2 优点


长轮询相较于轮询技术来说,减少了不必要的网络流量和请求次数,降低了服务器和客户端的资源消耗


2.3 缺点


但是相对于传统的轮询技术,长轮询的实现更加复杂,并且需要服务器支持长时间保持连接的能力。


2.4 基础实现


超时和未得到想要的结果都需要重新执行原方法(递归实现)


async function subscribe() {
let response = await fetch("/subscribe");

if (response.status == 502) {
// 状态 502 是连接超时错误,
// 连接挂起时间过长时可能会发生,
// 远程服务器或代理会关闭它
// 让我们重新连接
await subscribe();
} else if (response.status != 200) {
// 一个 error —— 让我们显示它
showMessage(response.statusText);
// 一秒后重新连接
await new Promise(resolve => setTimeout(resolve, 1000));
await subscribe();
} else {
// 获取并显示消息
let message = await response.text();
showMessage(message);
// 再次调用 subscribe() 以获取下一条消息
await subscribe();
}
}

subscribe();

3. 长链接


3.1 说明


HTTP keep-alive 也称为 HTTP 长连接。它通过重用一个 TCP 连接来发送/接收多个 HTTP请求,来减少创建/关闭多个 TCP 连接的开销


3.1.1 为什么HTTP是短连接?


HTTP是短连接,客户端向服务器发送一个请求,得到响应后,连接就关闭。


例如,用户通过浏览器访问一个web站点上的某个网页,当网页内容加载完毕之后(已得到响应),用户可能需要花费几分钟甚至更多的时间来浏览网页内容,此时完全没有必要继续维持底层连。当用户需要访问其他网页时,再创建新的连接即可。


因此,HTTP连接的寿命通常都很短。这样做的好处是,可以极大的减轻服务端的压力。一般而言,一个站点能支撑的最大并发连接数也是有限的,


面对这么多客户端浏览器,不可能长期维持所有连接。每个客户端取得自己所需的内容后,即关闭连接,更加合理。


3.1.2 为什么要引入keep-alive(也称HTTP长连接)


通常一个网页可能会有很多组成部分,除了文本内容,还会有诸如:js、css、图片等静态资源,有时还会异步发起AJAX请求。


只有所有的资源都加载完毕后,我们看到网页完整的内容。然而,一个网页中,可能引入了几十个js、css文件,上百张图片,


如果每请求一个资源,就创建一个连接,然后关闭,代价实在太大了。


基于此背景,我们希望连接能够在短时间内得到复用,在加载同一个网页中的内容时,尽量的复用连接,这就是HTTP协议中keep-alive属性的作用。



  • HTTP 1.0 中默认是关闭的,需要在http头加入"Connection: Keep-Alive",才能启用Keep-Alive;
  • HTTP 1.1 中默认启用Keep-Alive,如果加入"Connection: close ",才关闭

注意:这里复用的是 TCP连接,并不是复用request


image.png


HTTP 的 Keep-Alive 也叫 HTTP 长连接,该功能是由「应用程序」实现的,可以使得用同一个 TCP 连接来发送和接收多个 HTTP 请求/应答,减少了 HTTP 短连接带来的多次 TCP 连接建立和释放的开销。


TCP 的 Keepalive 也叫 TCP 保活机制,该功能是由「内核」实现的,当客户端和服务端长达一定时间没有进行数据交互时,内核为了确保该连接是否还有效,就会发送探测报文,来检测对方是否还在线,然后来决定是否要关闭该连接


4. WebSocket


4.1 说明


Websocket是基于HTTP协议的,在和服务端建立了链接后,服务端有数据有了变化后会主动推送给前端;


一般可以用于 股票交易行情分析、聊天室、在线游戏,替代轮询和长轮询。


image.png


4.2 优点


请求响应快,不浪费资源。(传统的http请求,其并发能力都是依赖同时发起多个TCP连接访问服务器实现的(因此并发数受限于浏览器允许的并发连接数),而websocket则允许我们在一条ws连接上同时并发多个请求,即在A请求发出后A响应还未到达,就可以继续发出B请求。由于TCP的慢启动特性(新连接速度上来是需要时间的),以及连接本身的握手损耗,都使得websocket协议的这一特性有很大的效率提升;http协议的头部太大,且每个请求携带的几百上千字节的头部大部分是重复的,websocket则因为复用长连接而没有这一问题。)


4.3 缺点



  • 主流浏览器支持的Web Socket版本不一致;
  • 服务端没有标准的API。

4.4 基础实现


这里使用了一个 网页和打印app的通信举例(部分敏感代码已省略)


const printConnect = () => {
try {
const host = 'ws://localhost:13888'
cloundPrintInfo.webSocket = new WebSocket(host)

// 通信
cloundPrintInfo.webSocket.onopen = () => {
// 获取打印机列表
cloundPrintInfo.webSocket.send(
JSON.stringify({
cmd: 'getPrinters',
version: '1.0',
})
)
}

// 通信返回
cloundPrintInfo.webSocket.onmessage = (msg: any) => {

const { data: returnData } = msg

// code 1000: 全部成功 1001: 部分失败 1002: 全部失败
const { cmd } = JSON.parse(`${returnData}`)

// 获取打印机数据
if (cmd === 'GETPRINTERS') {
printerInfoSet(returnData)
}

// 处理发送打印请求结果
if (cmd === 'PRINT') {
handlePrintResult(returnData)
}

// 批量推送打印结果
if (cmd === 'NOTIFYPRINTRESULT') {
cloudPrintTip(returnData)
}
}

// 通信失败
cloundPrintInfo.webSocket.onerror = () => {
printClose()
}

// 关闭通信
cloundPrintInfo.webSocket.onclose = () => {
printClose()
}
} catch (exception) {
console.log('建立连接失败', exception)
printClose()
}
}


image.png


image.png


在实际应用中,你可能需要处理更复杂的情况,比如重连逻辑、心跳机制来保持连接活跃、以及安全性问题等


重连逻辑:当WebSocket连接由于网络问题或其他原因断开时,客户端可能需要自动尝试重新连接


var socket;
var reconnectInterval = 5000; // 重连间隔时间,例如5秒

function connect() {
socket = new WebSocket('ws://localhost:3000');

socket.onopen = function(event) {
console.log('Connected to the WebSocket server');
};

socket.onclose = function(event) {
console.log('WebSocket connection closed. Reconnecting...');
setTimeout(connect, reconnectInterval); // 在指定时间后尝试重连
};

socket.onerror = function(error) {
console.error('WebSocket error:', error);
socket.close(); // 确保在错误后关闭连接,触发重连
};
}

connect(); // 初始连接



心跳机制:指定期发送消息以保持连接活跃的过程。这可以防止代理服务器或负载均衡器因为长时间的不活动而关闭连接


function heartbeat() {
if (socket.readyState === WebSocket.OPEN) {
socket.send('ping'); // 发送心跳消息,内容可以是'ping'
}
}

// 每30秒发送一次心跳
var heartbeatInterval = setInterval(heartbeat, 30000);

// 清除心跳定时器,通常在连接关闭时调用
function clearHeartbeat() {
clearInterval(heartbeatInterval);
}

socket.onclose = function(event) {
clearHeartbeat();
};


4种对比


从兼容性角度考虑,短轮询>长轮询>长连接SSE>WebSocket;


从性能方面考虑,WebSocket>长连接SSE>长轮询>短轮询。


参考文章:



作者:椰子鑫
来源:juejin.cn/post/7451612338408521743

0 个评论

要回复文章请先登录注册