草庐IT

数据不够实时:试试长连接?

ELab.liruiliang 2023-03-28 原文

背景

在特定场景下,我们往往需要实时的去获取最新的数据,如获取消息推送或公告、聊天消息、实时的日志和学情等,都对数据的实时性要求很高,面对这类场景,最常用的可能就是轮询,但除了轮询还有长连接(Websocket)和服务端推送(SSE)方案可供选择。

轮询

轮询就是采用循环http请求的方式,通过重复的接口请求去获取最新的数据。

短轮询 (polling)

短轮询可能是我们用的最多的一种实时刷新数据的方式了,我们在讲轮询方案时,大部分指的就是短轮询,其实现方式和普通的接口无异,改造也只要前端增加定时器或useRequest配置轮询参数即可,其原理也非常简单,如下图,如果是http1.1及以上,TCP连接可以复用,当然http1.0及以下也是可以使用,但消耗会更多。短轮询的特点就是接口请求立即会返回,每次请求都可以理解为是一次新的请求。

短轮询的优缺点

短轮询最大的优点就是简单,前端设置时间间隔,定时去请求数据,而服务端只需同步的查询数据返回即可,但缺点也显而易见:

  1. 无用请求过多:从下图可以看出,每隔固定时间,一定有请求发出,且每次接口可能返回一样的数据或返回空结果,服务端会重复查询数据库、前端会重复重渲染
  2. 实时性不可控,如数据更新了,但轮询请求刚结束一轮,会造成轮询间隔内数据都得不到更新

长轮询 (long polling)

看完了上面关于短轮询的介绍,我们知道了轮询有两个主要的缺陷:一个是无用请求过多,另外一个是数据实时性不可控。为了解决这两个问题,于是有了更进一步的长轮询方案。

在上图中,客户端发起请求后,服务端发现当前没有新的数据,这个时候服务端没有立即返回请求,而是将请求挂起,在等待一段时间后(一般为30s或者是60s,设置一个超时返回主要是为了考虑过长的无数据连接占用会被网关或者某层中间件断开甚至是被运营商断开),如发现还是没有数据更新的话,就返回一个空结果给客户端。客户端在收到服务端的回复后,立即再次向服务端发送新的请求。这次服务端在接收到客户端的请求后,同样等待了一段时间,这次好运的是服务端的数据发生了更新,服务端给客户端返回了最新的数据。客户端在拿到结果后再次发送下一个请求,如此反复。

长轮询的优缺点

长轮询很完美地解决了短轮询的问题,首先服务端在没有数据更新的情况下没有给客户端返回数据,所以避免了客户端大量的重复请求。再者客户端在收到服务端的返回后,马上发送下一个请求,这就保证了更好的数据实时性。不过长轮询也有缺点:

  • 服务端资源大量消耗: 服务端会一直hold住客户端的请求,这部分请求会占用服务器的资源。对于某些语言来说,每一个HTTP连接都是一个独立的线程,过多的HTTP连接会消耗掉服务端的内存资源。
  • 难以处理数据更新频繁的情况: 如果数据更新频繁,会有大量的连接创建和重建过程,这部分消耗是很大的。虽然HTTP有TCP连接复用,但每次拿到数据后客户端都需要重新请求,因此相对于WebSocket和SSE它多了一个发送新请求的阶段,对实时性和性能还是有影响的。
从上面的描述来看,长轮询的次数和时延似乎可以更少,那是不是长轮询更好呢?其实不是的,这个两种轮询方式都有优劣势和适合的场景。

短轮询 ,长轮询怎么选?

长 轮询多用于操作频繁,点对点的通讯,而且连接数不能太多情况,每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。

而像WEB网站的http服务一般都用短 轮询,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好。

长连接

​WebSocket​

上面说到长轮询不适用于服务端资源频繁更新的场景,而解决这类问题的一个方案就是WebSocket。用最简单的话来介绍WebSocket就是:客户端和服务器之间建立一个持久的长连接,这个连接是双工的,客户端和服务端都可以实时地给对方发送消息。下面是WebSocket的图示:

WebSocket对于前端的同学来说是非常常见了,因为无论是webpack还是vite,用来HMR的reload就是通过WebSocket来进行的,有代码改动,工程重新编译,新变更的模块通知到浏览器加载新的模块,这里的通知浏览器加载新模块就是通过WebSocket的进行的。如上图,通过握手(协议转换)建立连接后,双方就保持持久连接,由于历史的关系,WebSocket建立连接是依赖HTTP的,但是其建连请求有明显的特征,目的是客户端和服务端都能识别并保持连接。

请求特征

请求头特征

  • HTTP 必须是 1.1 GET 请求
  • HTTP Header 中 Connection 字段的值必须为 Upgrade
  • HTTP Header 中 Upgrade 字段必须为 websocket
  • Sec-WebSocket-Key 字段的值是采用 base64 编码的随机 16 字节字符串
  • Sec-WebSocket-Protocol 字段的值记录使用的子协议,比如 binary base64
  • Origin 表示请求来源
响应头特征

  • 状态码是 101 表示 Switching Protocols
  • Upgrade / Connection / Sec-WebSocket-Protocol 和请求头一致
  • Sec-WebSocket-Accept 是通过请求头的 Sec-WebSocket-Key 生成

兼容性

WebSocket 协议在2008年诞生,2011年成为国际标准。现在所有浏览器都已经支持了。

实现一个简单的 WebSocket

基于原生WebSocket我们实现一个简单的长连。

连接

// 连接只需实例一个WebSocket
const ws = new WebSocket(`wss://${url}`);

发送消息

ws.send("这是一条消息:" + count);

监听消息

ws.onmessage = function (event) {
console.log(event.data);
}

关闭连接

ws.close();

​在工程上使用WebSocket​

在工程上,很少直接基于原生WebSocket实现业务需求,使用WebSocket需要完成下面几个问题:

  • 鉴权:防止恶意连接连接进来接收消息
  • 心跳:客户端意外断开,导致死链占用服务端资源,长时间无消息的连接可能会被中间网关或运营商断开
  • 登录:通过建连需要识别出该连接是哪个用户,有无权限,需要推送哪些消息
  • 日志:监控连接,错误上报
  • 后台:能方便的查看在线连接的客户端数量,消息传输量

服务端推送(SSE)

SSE全称Server-sent Events,是HTML 5 规范的一个组成部分,该规范十分简单,主要由两个部分组成:第一个部分是服务器端与浏览器端之间的通讯协议,第二部分则是在浏览器端可供 JavaScript 使用的 EventSource 对象。通讯协议是基于纯文本的简单协议。服务器端的响应的内容类型是“text/event-stream”。响应文本的内容可以看成是一个事件流,由不同的事件所组成。每个事件由类型和数据两部分组成,同时每个事件可以有一个可选的标识符。不同事件的内容之间通过仅包含回车符和换行符的空行(“rn”)来分隔。每个事件的数据可能由多行组成。

​和 Websocket对比​

SSE

WebSocket

单向:仅服务端能发送消息

双向:客户端、服务端双向发送

仅文本数据

二进制、文本都可

常规HTTP协议

WebSocket协议

​兼容性​

​数据格式​

服务器向浏览器发送的 SSE 数据,必须是 UTF-8 编码的文本,

响应头

Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive

数据传输

服务端每次发送消息,由若干message​组成,使用\n\n​分隔,如果单个messag​过长,可以用\n分隔。

field取值

data
event
id
retry
例子

// 注释,用于心跳包
: this is a test stream\n\n
// 设置断链1000ms重试一次
retry:1000 \n\n
event: 自定义消息\n\n

data: some text\n\n

data: another message\n
data: with two lines \n\n

​实现一个简单的SSE​

web端

实例化EventSource​,监听open、message、error

const source = new EventSource(url, { withCredentials: true });
// 监听消息
source.onmessage = function (event) {
// handle message
};
source.addEventListener('message', function (event) {
// handle message
}, false);

// 监听错误
source.onerror = function (event) {
// handle error
};
source.addEventListener('error', function (event) {
// handle error
}, false);

// 关闭连接
source.close()

服务端

以nodejs为例,服务端代码和普通请求无异,并没有新的处理类库。

res.writeHead(200, {
"Content-Type":"text/event-stream",
"Cache-Control":"no-cache",
"Connection":"keep-alive",
"Access-Control-Allow-Origin": '*',
});
res.write("retry: 10000\n\n");
res.write("event: connecttime\n\n");
res.write("data: " + (new Date()) + "\n");
res.write("data: " + (new Date()) + "\n\n");

// 模拟收到消息推送给客户端
interval = setInterval(function () {
res.write("data: " + (new Date()) + "\n\n");
}, 1000);

和WebSocket不同,SSE并不是新的通信协议,其本质是在普通HTTP请求的基础上定义一个Content-Type​,保持上连接,通过普通的接口也能模拟出SSE的效果,以XMLHttpRequest为例

const xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:8844/long", true);
xhr.onload = (e) => {
console.log("onload", xhr.responseText);
};
xhr.onprogress = (e) => {
// 每次服务端写入response的数据,都会传输过来,并产生一次onprogress事件
console.log("onprogress", xhr.responseText);
};
xhr.send();

参考文献

rfc6455.pdf[1]

WebSocket协议中文版(rfc6455)[2]

深入剖析WebSocket的原理 - 知乎[3]

HTTP长连接实现原理 - 掘金[4]

WebSocket() - Web API 接口参考 | MDN[5]

EventSource - Web API 接口参考 | MDN[6]​

有关数据不够实时:试试长连接?的更多相关文章

  1. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  2. ruby - 续集在添加关联时访问many_to_many连接表 - 2

    我正在使用Sequel构建一个愿望list系统。我有一个wishlists和itemstable和一个items_wishlists连接表(该名称是续集选择的名称)。items_wishlists表还有一个用于facebookid的额外列(因此我可以存储opengraph操作),这是一个NOTNULL列。我还有Wishlist和Item具有续集many_to_many关联的模型已建立。Wishlist类也有:selectmany_to_many关联的选项设置为select:[:items.*,:items_wishlists__facebook_action_id].有没有一种方法可以

  3. ruby - 无法在 60 秒内获得稳定的 Firefox 连接 (127.0.0.1 :7055) - 2

    我使用的是Firefox版本36.0.1和Selenium-Webdrivergem版本2.45.0。我能够创建Firefox实例,但无法使用脚本继续进行进一步的操作无法在60秒内获得稳定的Firefox连接(127.0.0.1:7055)错误。有人能帮帮我吗? 最佳答案 我遇到了同样的问题。降级到firefoxv33后一切正常。您可以找到旧版本here 关于ruby-无法在60秒内获得稳定的Firefox连接(127.0.0.1:7055),我们在StackOverflow上找到一个类

  4. ruby - Ruby 有 `Pair` 数据类型吗? - 2

    有时我需要处理键/值数据。我不喜欢使用数组,因为它们在大小上没有限制(很容易不小心添加超过2个项目,而且您最终需要稍后验证大小)。此外,0和1的索引变成了魔数(MagicNumber),并且在传达含义方面做得很差(“当我说0时,我的意思是head...”)。散列也不合适,因为可能会不小心添加额外的条目。我写了下面的类来解决这个问题:classPairattr_accessor:head,:taildefinitialize(h,t)@head,@tail=h,tendend它工作得很好并且解决了问题,但我很想知道:Ruby标准库是否已经带有这样一个类? 最佳

  5. ruby - 我如何添加二进制数据来遏制 POST - 2

    我正在尝试使用Curbgem执行以下POST以解析云curl-XPOST\-H"X-Parse-Application-Id:PARSE_APP_ID"\-H"X-Parse-REST-API-Key:PARSE_API_KEY"\-H"Content-Type:image/jpeg"\--data-binary'@myPicture.jpg'\https://api.parse.com/1/files/pic.jpg用这个:curl=Curl::Easy.new("https://api.parse.com/1/files/lion.jpg")curl.multipart_form_

  6. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  7. FOHEART H1数据手套驱动Optitrack光学动捕双手运动(Unity3D) - 2

    本教程将在Unity3D中混合Optitrack与数据手套的数据流,在人体运动的基础上,添加双手手指部分的运动。双手手背的角度仍由Optitrack提供,数据手套提供双手手指的角度。 01  客户端软件分别安装MotiveBody与MotionVenus并校准人体与数据手套。MotiveBodyMotionVenus数据手套使用、校准流程参照:https://gitee.com/foheart_1/foheart-h1-data-summary.git02  数据转发打开MotiveBody软件的Streaming,开始向Unity3D广播数据;MotionVenus中设置->选项选择Unit

  8. 使用canal同步MySQL数据到ES - 2

    文章目录一、概述简介原理模块二、配置Mysql使用版本环境要求1.操作系统2.mysql要求三、配置canal-server离线下载在线下载上传解压修改配置单机配置集群配置分库分表配置1.修改全局配置2.实例配置垂直分库水平分库3.修改group-instance.xml4.启动监听四、配置canal-adapter1修改启动配置2配置映射文件3启动ES数据同步查询所有订阅同步数据同步开关启动4.验证五、配置canal-admin一、概述简介canal是Alibaba旗下的一款开源项目,Java开发。基于数据库增量日志解析,提供增量数据订阅&消费。Git地址:https://github.co

  9. ruby-on-rails - 创建 ruby​​ 数据库时惰性符号绑定(bind)失败 - 2

    我正在尝试在Rails上安装ruby​​,到目前为止一切都已安装,但是当我尝试使用rakedb:create创建数据库时,我收到一个奇怪的错误:dyld:lazysymbolbindingfailed:Symbolnotfound:_mysql_get_client_infoReferencedfrom:/Library/Ruby/Gems/1.8/gems/mysql2-0.3.11/lib/mysql2/mysql2.bundleExpectedin:flatnamespacedyld:Symbolnotfound:_mysql_get_client_infoReferencedf

  10. STM32读取串口传感器数据(颗粒物传感器,主动上传) - 2

    文章目录1.开发板选择*用到的资源2.串口通信(个人理解)3.代码分析(注释比较详细)1.主函数2.串口1配置3.串口2配置以及中断函数4.注意问题5.源码链接1.开发板选择我用的是STM32F103RCT6的板子,不过代码大概在F103系列的板子上都可以运行,我试过在野火103的霸道板上也可以,主要看一下串口对应的引脚一不一样就行了,不一样的就更改一下。*用到的资源keil5软件这里用到了两个串口资源,采集数据一个,串口通信一个,板子对应引脚如下:串口1,TX:PA9,RX:PA10串口2,TX:PA2,RX:PA32.串口通信(个人理解)我就从串口采集传感器数据这个过程说一下我自己的理解,

随机推荐