当前位置: 首页 > news >正文

面试官:服务端推送到Web前端有哪几种方式?

这个问题?

这个问题一般会出现在面试题里面,然后回答一些诸如轮询、WebSocket之类的答案。当然,实际开发中,也会遇到类似别人给你赞了,要通知给你的情况。这时服务端推送给Web前端(先局限在Web前端,毕竟其他端还有一些特殊方法)到底有多少种方法?它们到底是怎么实现的?

写个Demo看看吧,这样正好把主要(不清楚是否还有漏的)的方案都实现一遍。先看效果:

其中的代码也上传到GitHub了,在server-push( github.com/waiter/serv… )这里。

各种方案

从上面的截图也已经可以看出,本文主要写了5种方案,那么接下来也就一个一个简单介绍一下吧。

另外,本文涉及的Demo,后端直接使用原生的Node.js开发,没有使用KoaExpress之类的,也没有使用额外的库,类似socket.io,主要是想保持最精简的状态来呈现。前端也只是在最基础的HTML上,引入了jQuery来方便做DOM操作,也引入了Bootstrap来快速实现统一的样式,而未再引入类似VueReact之类的框架。

还有,为了触发服务端推送,这边在前端页面上加了个输入框和按钮,来将消息发送给后端,后端会缓存消息,并触发推送,后端大体代码类似:

// 缓存需要推送的信息
const datas = [];
// 各种方案触发推送时的回调
const callbacks = {};

// 注册接口回调
server.on('request', (req, res) => {const { pathname, query } = parse(req.url, true);// 如果发现是前端触发推送接口if (pathname === '/api/push') {if (query.info) {// 缓存推送信息datas.push(query.info);const d = JSON.stringify([query.info]);// 触发所有推送回调Object.keys(callbacks).forEach(k => callbacks[k](d));}res.end('ok');}
}); 

1. 轮询(短轮询

这是最简单直观的方法,就是每隔一段时间发起一个请求到后端询问是否有新信息。至于为什么又叫短轮询,其是相对于后续要说的长轮询来对比的。

这样前端只要设置一个setTimeout来定时请求就行:

// 缓存前端已经获取的最新id
let id = 0;

function poll() {$.ajax({url: '/api/polling',data: { id },}).done(res => {id += res.length;}).always(() => {// 10s后再次请求setTimeout(poll, 10000);});
}

poll(); 

后端也是否简单,根据前端给到的id,看看有没有新消息,有就返回,没有就返回空

const id = parseInt(query.id || '0', 10) || 0;
res.writeHead(200, { 'Content-Type': 'application/json;' });
res.end(JSON.stringify(datas.slice(id))); 

这个看起来其实时性与请求频率成正相关,但是当请求频率上来了,性能浪费也就越高,毕竟可能大部分请求都是无意义的。

2. 长轮询

在翻找资料的时候,发现有些资料会直接把这个当作短轮询,有点匪夷所思。这里的长轮询相对前面的轮询来说,算是一种优化。具体就是前端发起请求到后端,后端不直接返回,而是等待有新信息时再返回。所以这样发起的一个请求,可能需要很长的时间才能等到返回,故而叫做长轮询。

其前端代码基本和短轮询一致,只不过把请求的超时时间设置较长比如1分钟),然后无论请求成功或失败,马上再次发起请求即可。

相对来说,后端的写法就要稍微改动一下

const id = parseInt(query.id || '0', 10) || 0;
const cbk = 'long-polling';
delete callbacks[cbk];
const data = datas.slice(id);
res.writeHead(200, { 'Content-Type': 'application/json' });
// 发起请求时,正好有新消息就返回
if (data.length) {return res.end(JSON.stringify(data));
}
req.on('close', () => {delete callbacks[cbk];
});
// 注册新消息回调
callbacks[cbk] = (d) => {res.end(d);
}; 

这样,**相对于短轮询,少了很多无意义的请求,而且消息的实时性也非常好。**不过,当服务端有异常时,会导致长轮询短时间内不断发起请求,可能让服务端承受更大的压力,所以两次长轮询之间最好有一定间隔,或者异常检测机制。

3. SSE(Server-sent events

Traditionally, a web page has to send a request to the server to receive new data; that is, the page requests data from the server. With server-sent events, it’s possible for a server to send new data to a web page at any time, by pushing messages to the web page. These incoming messages can be treated as Events + data inside the web page.

前面提到的轮询、长轮询都是一问一答式的,一次请求,无法推送多次消息到前端。而SSE就厉害了,一次请求,N次推送

其原理,或者说类比,个人认为可以理解为下载一个巨大的文件,文件的内容分块传给前端,每块就是一次消息推送

听起来很厉害,先看看后端代码要怎么写

const cbk = 'sse';
delete callbacks[cbk];
res.writeHead(200, {// 这个是核心'Content-Type': 'text/event-stream','Connection': 'keep-alive',
});
// 把缓存的信息推送给前端
res.write(`data: ${JSON.stringify(datas)}\n\n`);
// 注册新消息回调
callbacks[cbk] = (d) => {res.write(`data: ${d}\n\n`);
};
req.on('close', () => {delete callbacks[cbk];
}); 

后端代码很简单,核心在于Content-Type: text/event-stream,这要让前端知道这是SSE,还有就是传输信息的格式比较特别一点,详细的可以看 MDN( developer.mozilla.org/en-US/docs/… )

而前端有专门的EventSource来接收,使用起来很方便

const es = new EventSource('/api/sse');
es.onmessage = (e) => { try {const c = JSON.parse(e.data);} catch (err) {console.log(err);}
} 

这样就好了,如果你打开Chrome的开发者工具中的网络标签,你就会发现Chrome对于SSE请求,有专门的展示标签

另外,**SSE还支持自动重连!**服务器短时间异常,恢复之后,无需额外代码,SSE就自动重连上了。不过,本人在实际工作中却没有碰到过SSE,也就在面试题中见过。

4. WebSocket

既然有了SSE,那还要WebSocket干啥啊?因为WebSocket可以一次连接,双向推送,而SSE只能从服务端推送到前端。从这个角度来看,用WebSocket来单做服务端推送,有点大材小用了。

另外,初见WebSocket,可能会对其与Socket的联系有点疑惑。Socket协议是与HTTP协议平级的,而WebSocket协议是基于HTTP协议的,不过两者在使用层面上是十分相近的。

其前端使用写法与SSE类似,十分简单,只不过请求链接为ws://或者wss://开头(相当于http://https://

const ws = new WebSocket('ws://localhost:3000/ws');
ws.onmessage = e => {try {const c = JSON.parse(e.data);} catch (err) {console.log(err);}
}; 

而如果要用原生Node.js来写WebSocket服务,就会麻烦一些了,一般情况都会使用类似socket.io之类的三方库来降低实现成本。这边也就在网上摘抄了一段代码来简单实现一下,详细的可以看Github上的Demo代码

server.on('upgrade', (req, socket) => {const cbk = 'ws';delete callbacks[cbk];const acceptKey = req.headers['sec-websocket-key']; const hash = generateAcceptValue(acceptKey); const responseHeaders = [ 'HTTP/1.1 101 Web Socket Protocol Handshake', 'Upgrade: WebSocket', 'Connection: Upgrade', `Sec-WebSocket-Accept: ${hash}` ];// 告知前端这是WebSocket协议socket.write(responseHeaders.join('\r\n') + '\r\n\r\n');// 发送数据socket.write(constructReply(datas));callbacks[cbk] = (d) => {socket.write(constructReply(d));}socket.on('close', () => {delete callbacks[cbk];});
}); 

这个在Chrome浏览器中,也有专门的标签页展示

不过,它没有像SSE一样有自动重连,这块需要自行实现。

一般网页实时聊天之类需要双向推送的,都会使用WebSocket来实现。

5. iFrame

这算是找资料的时候意外发现的,之前并不知道还有这样的玩法。原理类似使用iFrame加载一个巨大的网页,利用浏览器会一边加载一边解析执行返回的HTML,通过分次返回Script标签来实现消息推送。其实现类似SSE,不过看起来就比较hack

前端代码很简单,只不过要注册一个回调给iframe使用

// 注册给iframe使用的方法
window.change = function(data) {

};
$('body').append('<iframe src="/api/iframe"></iframe>'); 

而后端也很简单,有消息的时候返回script标签即可

const cbk = 'iframe';
delete callbacks[cbk];
// 返回缓存信息
res.write(`<script>window.parent.change(${JSON.stringify(datas)});</script>`);
callbacks[cbk] = (d) => {res.write(`<script>window.parent.change(${d});</script>`);
};
req.on('close', () => {delete callbacks[cbk];
}); 

相当奇淫巧技了。不过,似乎没找到怎么判断加载异常的情况,可能需要自行加心跳来实现了

另外,很多文章在说使用iFrame方法时,会导致浏览器显示未加载完,图标一直转的样子。但是个人认为,图标一直转是因为页面一直没有onload,那么在页面onload之后,再创建iFrame就应该没有这个问题了

总结一下

上面实现了5种推送的方案,弄了一个表格简单对比一下

方案)实时单次连接自动重连断线检测双向推送无跨域
短轮询
长轮询
SSE
WebSocket
iFrame

最后

最近还整理一份JavaScript与ES的笔记,一共25个重要的知识点,对每个知识点都进行了讲解和分析。能帮你快速掌握JavaScript与ES的相关知识,提升工作效率。



有需要的小伙伴,可以点击下方卡片领取,无偿分享

相关文章:

  • JAVA每日总结day6.21
  • 力扣(2024.06.20)
  • 编写水文专业串口通讯软件的开发经历
  • 充电学习— 9、Typec Pd
  • 大模型应用开发实践:RAG与Agent
  • 程序员做电子书产品变现的复盘(10)
  • 论文阅读-CheckFreq:频繁、精细的DNN检查点操作。
  • JVM-垃圾回收
  • windows xrdp 到 ubuntu 的一些问题记录
  • 个人玩航拍,如何申请无人机空域?
  • IDEA如何进行debug调试
  • 【flask+python】利用魔术方法,更优雅的封装model类
  • selenium使用篇_键盘鼠标事件
  • Java第17章 - I/O流(一)
  • STM32F103移植FreeRTOS必须搞明白的系列知识---4(FreeRTOSConfig.h配置文件)
  • C语言中的字符串转数字函数常见问题详解
  • 从零开始搭建仿抖音短视频APP-构建后端项目
  • 力扣 221. 最大正方形
  • 爬虫报错:twisted.web._newclient.ResponseNeverReceived
  • 前后端分离技术渲染动态页面 - 表白墙(Servlet)
  • 关于springboot多环境设设置说明
  • 一幅长文细学CSS3
  • P2-Net:用于2D像素与3D点匹配的局部特征的联合描述符和检测器(ICCV 2021)
  • 电磁兼容(EMC)基础(二)
  • 面向终客户和设备制造商的Ethernet-APL
  • React.lazy与Suspence实现延迟加载
  • React 中 memo useMemo useCallback 到底该怎么用
  • git命令
  • Vue的思维导图
  • 【Linux】4.0进程控制
  • 【Python应用】自制截图取词小工具-- 解锁文字识别新姿势
  • MYSQL学习笔记(DDL[数据定义]、DML[数据操作]、DQL[数据查询])