03 | 轮询与长连接:如何解决消息的实时到达问题?

你好,我是袁武林。

我在前面第一篇文章中,从使用场景的需求方面,讲到了IM系统的几个比较重要的特性。其中之一就是“消息到达的实时性”。

实时性场景是所有的IM系统绕不开的话题,为了支持互联网的“实时互联”的概念,大部分的App都需要实时技术的支持。

我们现在使用的聊天类App、直播互动类App都已经在实时性方面做得很好了,消息收发延迟基本都能控制在毫秒级别。

当然这一方面得益于快速发展的移动网络,让网络延迟越来越低、网络带宽越来越高;另一个重要原因是:社交网络App在实时性提升方面的技术,也在不断升级迭代。

实时性主要解决的问题是:当一条消息发出后,我们的系统如何确保这条消息最快被接收人感知并获取到,并且尽量让耗费的资源较少。这里关键的几个点是:最快触达,且耗费资源少。

想好了吗?下面我们来看一看,IM在追求“消息实时性”的架构上,所经历过的几个代表性阶段。

短轮询场景

在PC Web的早期时代,对于数据的获取,大部分应用采用一问一答的“请求响应”式模式,实际上,像现在我们浏览大部分门户网站的新闻,以及刷微博其实都是采用的“请求响应”模式。

但这种依赖“手动”触发的模式,在即时消息系统中当有新消息产生时并不能很好地感知并获取到,所以明显不适用于对实时性要求高的场景。

因此,这个时期的IM软件很多采用了一种“短轮询”的模式,来定期、高频地轮询服务端的新消息。

在短轮询模式中,服务器接到请求后,如果有新消息就会将新消息返回给客户端,如果没有新消息就返回空列表,并关闭连接。

这种短轮询的方式就好像一位焦急等待重要信件的人,每天骑车跑到家门口的邮局去问是否有自己的信件,有就拿回家,没有第二天接着去邮局问。

作为从一问一答的请求响应模式孵化出来的短轮询模式,具有较低的迁移升级成本,比较容易落地。但劣势也很明显:

  • 为了提升实时性,短轮询的频率一般较高,但大部分轮询请求实际上是无用的,客户端既费电也费流量;
  • 高频请求对服务端资源的压力也较大,一是大量服务器用于抗高频轮询的QPS(每秒查询率),二是对后端存储资源也有较大压力。

因此,“短轮询”这种方式,一般多用在用户规模比较小,且不愿花费太多服务改造成本的小型应用上。

长轮询场景

正是由于“短轮询”存在着高频无用功的问题,为了避免这个问题,IM逐步进化出“长轮询”的消息获取模式。

长轮询和短轮询相比,一个最大的改进之处在于:短轮询模式下,服务端不管本轮有没有新消息产生,都会马上响应并返回。而长轮询模式当本次请求没有获取到新消息时,并不会马上结束返回,而是会在服务端“悬挂(hang)”,等待一段时间;如果在等待的这段时间内有新消息产生,就能马上响应返回。

这种方式就像等待收信的人每天跑到邮局去问是否有自己的信件,如果没有,他不是马上回家,而是在邮局待上一天,如果还是没有就先回家,然后第二天再来。

比较之下,我们会发现,长轮询能大幅降低短轮询模式中客户端高频无用的轮询导致的网络开销和功耗开销,也降低了服务端处理请求的QPS,相比短轮询模式而言,显得更加先进。

长轮询的使用场景多见于: 对实时性要求比较高,但是整体用户量不太大。它在不支持WebSocket的浏览器端的场景下还是有比较多的使用。

但是长轮询并没有完全解决服务端资源高负载的问题,仍然存在以下问题。

  1. 服务端悬挂(hang)住请求,只是降低了入口请求的QPS,并没有减少对后端资源轮询的压力。假如有1000个请求在等待消息,可能意味着有1000个线程在不断轮询消息存储资源。

  2. 长轮询在超时时间内没有获取到消息时,会结束返回,因此仍然没有完全解决客户端“无效”请求的问题。

服务端推送:真正的边缘触发

短轮询和长轮询之所以没法做到基于事件的完全的“边缘触发(当状态变化时,发生一个IO事件)”,这是因为服务端在有新消息产生时,没有办法直接向客户端进行推送。

这里的根本原因在于短轮询和长轮询是基于HTTP协议实现的,由于HTTP是一个无状态协议,同一客户端的多次请求对于服务端来说并没有关系,也不会去记录客户端相关的连接信息。

因此,所有的请求只能由客户端发起,服务端由于并不记录客户端状态,当服务端接收到新消息时,没法找到对应的客户端来进行推送。

随着HTML5的出现,全双工的WebSocket彻底解决了服务端推送的问题。


这就像之前信件处理的逻辑,等待收信的用户不需要每天都跑到邮局去询问,而只要在邮局登记好自己家里的地址。等真正有信件时,邮局会派专门的邮递员按照登记的地址来把信送过去。

同样,当他需要写信给别人时,也只需要填好收件人地址,然后把信交给邮递员就可以了,不需要再自己跑邮局。

WebSocket

WebSocket正是一种服务端推送的技术代表。

随着HTML5的出现,基于单个TCP连接的全双工通信的协议WebSocket在2011年成为RFC标准协议,逐渐代替了短轮询和长轮询的方式,而且由于WebSocket协议获得了Web原生支持,被广泛应用于IM服务中,特别是在Web端基本属于IM的标配通信协议。

和短轮询、长轮询相比,基于WebSocket实现的IM服务,客户端和服务端只需要完成一次握手,就可以创建持久的长连接,并进行随时的双向数据传输。当服务端接收到新消息时,可以通过建立的WebSocket连接,直接进行推送,真正做到“边缘触发”,也保证了消息到达的实时性。

WebSocket的优点是:

  1. 支持服务端推送的双向通信,大幅降低服务端轮询压力;
  2. 数据交互的控制开销低,降低双方通信的网络开销;
  3. Web原生支持,实现相对简单。

TCP长连接衍生的IM协议

除了WebSocket协议,在IM领域,还有其他一些常用的基于TCP长连接衍生的通信协议,如XMPP协议、MQTT协议以及各种私有协议。

这些基于TCP长连接的通信协议,在用户上线连接时,在服务端维护好连接到服务器的用户设备和具体TCP连接的映射关系,通过这种方式客户端能够随时找到服务端,服务端也能通过这个映射关系随时找到对应在线的用户的客户端。

而且这个长连接一旦建立,就一直存在,除非网络被中断。这样当有消息需要实时推送给某个用户时,就能简单地通过这个长连接实现“服务端实时推送”了。

但是上面提到的这些私有协议都各有优缺点,如:XMPP协议虽然比较成熟、扩展性也不错,但基于XML格式的协议传输上冗余比较多,在流量方面不太友好,而且整体实现上比较复杂,在如今移动网络场景下用得并不多。

而轻量级的MQTT基于代理的“发布/订阅”模式,在省流量和扩展性方面都比较突出,在很多消息推送场景下被广泛使用,但这个协议并不是IM领域的专有协议,因此对于很多IM下的个性化业务场景仍然需要大量复杂的扩展和开发,比如不支持群组功能、不支持离线消息。

因此,对于开发人力相对充足的大厂,目前很多是基于TCP(或者UDP)来实现自己的私有协议,一方面私有协议能够贴合业务需要,做到真正的高效和省流;另一方面私有协议相对安全性更高一些,被破解的可能性小。目前主流的大厂很多都是采用私有协议为主的方式来实现。

小结

这一篇我们介绍了即时消息服务中是如何解决“消息实时性”这个难题。

为了更好地解决实时性问题,即时消息领域经历过的几次技术的迭代升级:

  • 从简单、低效的短轮询逐步升级到相对效率可控的长轮询;
  • 随着HTML5的出现,全双工的WebSocket彻底解决了服务端推送的问题;
  • 同时基于TCP长连接衍生的各种有状态的通信协议,也能够实现服务端主动推送,从而更好解决“消息收发实时性”的问题。

最后给你留一个思考题,TCP长连接的方式是怎么实现“当有消息需要发送给某个用户时,能够准确找到这个用户对应的网络连接”?

你可以给我留言,我们一起讨论,感谢你的收听,我们下期再见。

精选留言

  • 王棕生

    2019-09-02 13:11:10

    TCP 长连接的方式是怎么实现“当有消息需要发送给某个用户时,能够准确找到这个用户对应的网络连接”?
    答: 首先用户有一个登陆的过程: (1)tcp客户端与服务端通过三次握手建立tcp连接;(2)基于该连接客户端发送登陆请求;(3)服务端对登陆请求进行解析和判断,如果合法,就将当前用户的uid和标识当前tcp连接的socket描述符(也就是fd)建立映射关系; (4)这个映射关系一般是保存在本地缓存或分布式缓存中。
    然后,当服务端收到要发送给这个用户的消息时,先从缓存中根据uid查找fd,如果找到,就基于fd将消息推送出去。
    作者回复

    👍

    2019-09-02 19:52:40

  • 菜菜菜菜菜鸟

    2019-09-02 20:21:59

    TCP不是可靠的吗,为什么还需要在应用层实现ack,不应该是消息发出对方就一定可以收到的吗
    作者回复

    是个好问题。因为很多时候消息由tcp层交给应用层之后还可能出现丢失的情况,比如客户端落本地db失败了,类似这种。

    2019-09-03 21:57:58

  • 技术带头

    2019-09-08 10:05:31

    老师你好,我们也落地了一套im,服务端和本地都有存储,和用websoket。现在存在一个问题。群聊离线推送是将离线后产生的所有消息再用户上线的时候推送,还是客户端分页拉取
    作者回复

    可以考虑推拉结合,离线消息太多如果全部都推的话性能和带宽上都不太友好,可以离线推送一页,后续的再由客户端来拉取。

    2019-09-09 21:25:48

  • yic

    2019-09-08 21:40:55

    客户端发送心跳包,一般多久一次比较合适? 我看老师有回复国内不超过5分钟,微信用的是4分半。这个时间有什么依据吗?
    作者回复

    以前有大厂测试过国内各家运营商的NAT超时情况,有的运营商在2/3G网络下NAT超时是5分钟。

    2019-09-09 21:38:19

  • 魏巍

    2019-09-03 00:10:14

    服务器端如果突然进程被kill掉,客户端如何及时得到通知并下线?
    作者回复

    进程kill是会对socket执行close操作的哈,所以客户端是能感知到的。真要是通过拔网线的方式把服务端网络断开,这种情况客户端在发送数据时就会失败,然后短连后重连其他服务器就可以了。

    2019-09-03 22:16:45

  • 飞翔

    2019-09-02 07:04:32

    服务器维护一个hashmap key是 userid,value 是socket instance, 这样 用户A 发给用户B的信息里边含有用户B 的id, 用usedBid 到hashmap 里边查到 用户B的socket 就可以用socket 发送信息给用户B了,老师我还有一个问题 就是如果一台机器连接数有限, 如果需要多台机器,如果用户A的socker 和用户B的socket 不在一台机器上,这样怎么解决呀
    作者回复

    一种做法是用户上线时维护一个全局的 uid->网关机 的映射,下发的时候就能做到精确定位;另一种方式是:下发时把所有消息下发给所有机器,每台机器如果发现当前用户连接在本机就下发,其他的就丢掉,这种会有一定的资源浪费。

    2019-09-02 18:32:24

  • 探索无止境

    2019-10-01 07:12:44

    老师你好,为什么说一台服务器可以最多可以支持1000万的连接请求,这个数值是怎么推算出来的?
    作者回复

    不具备读写操作仅仅只是维护静态连接的话基本上就是个拼内存的活,调整tcp单连接的读写缓冲区大小到4k,优化下其他系统参数比如句柄限制啥的,建连速度稍微慢点,有个200g内存是没问题的。这里其实想说的是,不要光看单机能维护多少连接,每秒收发包数这些才是关键。

    2019-10-05 18:58:03

  • 魏巍

    2019-09-03 00:30:06

    一个linux服务器可以接受多少tcp连接,如何量化这个数字?当业务层对持久化层操作响应慢时,为何客户端会频繁掉线?聊天数据的持久化与下发操作查库对数据库的读写压力如何缓解?实时聊天的数据库如果宕机,能否做自动切换数据库服务器的操作?
    作者回复

    连接数一般不是问题哈,服务端单机几百万上千万都可以的,受限于分配给每个连接的buffer占用的内存。业务层持久化会慢会导致客户端掉线这个没看懂呀,能具体一点吗?第三个问题可以做db的读写分离,第四个问题应该是说db的自动切主吧,这个是可以的哈,很成熟的方案。

    2019-09-03 22:22:13

  • zombo

    2020-03-13 07:09:58

    老师请教一个区分,长连接是不是 "header: keep-alive" 的连接? 英文对应 persistent HTTP connection,但大家用得不多。而 webSocket 是HTML5才出来的,类似的机制,但各种浏览器支持得比较多。
    因为机制和实现上,我好像看不出它们有太大的区别? 还是说 persistent HTTP 仍然是3次握手,websocket是一次?
    作者回复

    最大的区别在于websocket是支持全双工的,也就是说是可以由服务端主动进行推送的,http 1.1 的keep alive只是能够多次http请求复用同一个tcp连接,但只能由客户端发起请求。

    2020-03-13 16:06:55

  • sam

    2019-09-03 20:50:38

    轮询就是前段定时请求,这个比较了解。但长轮询第一次听说,实际开发也没用过,特别是服务器的“悬挂(hang)”怎么理解? 是HTTP协议的机制吗?
    作者回复

    和http没关系哈,实现上比如接收到一个请求后,通过while循环判断,如果请求时间和当前时间的时间差没有到达超时阈值,就继续查询后端是否有新消息,直到超时或者查询到新消息。

    2019-09-03 22:34:53

  • 魏巍

    2019-09-03 00:37:12

    客户端发送心跳包,一般多久一次比较合适?
    作者回复

    国内的话最好不要超过5分钟,微信应该是4分半。

    2019-09-03 22:24:15

  • lecy_L

    2019-10-16 08:53:07

    老师你好,请问如果维护一个全局在线状态的情况下,精确定位通知的方案怎么做呢?
    作者回复

    这个实现也比较简单,用户上线的时候把用户和连接的网关ip作为映射存在中央存储,然后消息推送时读取这个映射,查询待推送消息的接收人所在的网关机,再通过rpc方式把这条消息发给这台网关机就可以了。

    2019-10-16 19:45:12

  • 冷笑的花猫

    2019-09-02 13:35:54

    请问老师,假设mqtt有两个缺点,不支持离线和群组功能,但可以修改源码增加这两类功能,而且mqtt已基本成熟,在mqtt之上开发不更方便和快捷吗?为啥要在tcp基础上自己开发呢?如果仅仅因为这些选择了了基于tcp自己开发,感觉说服力没那么强啊。
    所以请老师能详细说下这些的优缺点吗?晚上搜索到的感觉不太靠谱,谢谢。
    作者回复

    基于mqtt做二次开发是可以的呀,而且目前也有很多公司已经是这么做的了。只是说有开发能力的大厂更愿意自己来实现一套私有协议,主要是完全根据业务定制化,协议设计上会更高效一些。

    2019-09-02 19:54:39

  • 小可

    2019-09-02 08:56:31

    用户使用客户端与服务器建立连接时携带了userid与clientid,连接建立成功后,服务器端记录用户、客户端及连接之间的关系。一个可以用户使用多个客户端建立连接,一般是不同类型的客户端(网页、APP),后续有消息可根据此关系进行多端推送。
    作者回复

    👍

    2019-09-02 19:25:01

  • Shuai

    2019-09-02 10:32:13

    请问,XMPP是基于 XML 格式的协议,那像微信这种成熟的IM软件的私有通信协议是基于什么格式的呢?二进制吗?
    作者回复

    据我了解微信是私有二进制协议。

    2019-09-02 19:45:27

  • Geek_发现

    2019-09-02 07:41:34

    说一下我的猜想吧:不管是websocket全双工还是tcp有状态链接,都是应用了ack机制,用户访问服务器,ack会保存用户的信息,服务器收到ack后会开辟一块专有内存来保存所有的客户的ack信息;同理,服务器发送信息至客户,客户端也会保存服务器的ack信息。
    总的来说,客户端和服务器应该都是采用异步方式来提升效率和客户体验,并且降低服务器连接压力
    作者回复

    嗯,ack机制是确保消息被正确接收了,下节课会讲到哈

    2019-09-02 18:36:11

  • 超威丶

    2019-09-02 07:35:46

    请问websocket为什么能实现实时通信
    作者回复

    websocket支持双向全双工的传输,所以可以做到服务端推送,让消息接收更加实时。

    2019-09-02 18:35:06

  • 来自清真寺的圣诞树

    2019-10-14 21:04:34

    如果是websocket可以使用stomp协议解决?
    作者回复

    有这么玩的,STOMP Over Websocket,可以基于STOMP的消息队列模式来实现消息推送。

    2019-10-16 19:10:03

  • devil

    2019-09-24 10:49:40

    海量用户场景如QQ,微信,除了加机器以外,还有些什么方式可以处理海量的tcp连接吗?
    作者回复

    tcp连接的维护并不是一个耗资源的事情,只是占用一个句柄而已,没有消息传输时基本不怎么费资源,真正有压力的是连接上消息的下推。除了加机器,还可以通过优化系统参数等来提升单机的承载能力。

    2019-09-25 19:26:18

  • 魔曦

    2019-09-03 00:04:42

    有新的消息是全部推送给客户端吗?那么瞬间服务器压力飙高?如何解决?每个服务器能维护多少长链接?如果数量有限那么微博抖音需要多大的集群支撑?
    作者回复

    普通一对一场景消息扇出小,一般网关服务器没啥压力。大型直播间可能会有你刚才说的情况,这种情况可以通过扩容,限流,熔断来解决。单台服务器如果只是挂连接的话几百万上千万都没问题哈,但对于线上实际业务一般都不会真挂这么多,一般单机实际会控制在100w以内。

    2019-09-03 22:04:42