而TCP的KeepAlive无法代替应用层心跳保活机制的本质原因还是网络本身的不可靠,之前提到的第三点是最重要的。因为TCP是一个基于连接的协议,其连接状态是由一个状态机进行维护的,连接完毕后,通信双方都会处于 established 状态,这之后的状态并不会主动进行变化。这意味着如果上层不进行任何调用,一直使 TCP 连接空闲,那么这个连接虽然没有任何数据,但仍是保持连接状态,一天、一星期、甚至一个月,即使在这期间中间路由崩溃重启无数次,网络抖动,网线插拔无数次,那么TCP的状态机还是保持established的状态,连接还是处于逻辑上连接的状态。比如,现实中经常会遇到例子:当我们SSH到特定的VPS上,不小心踢掉了网线,此时的网络变化不会被TCP检测出来,当我们重新插回网线,仍然可以继续正常使用SSH,同时此时并没有发生任何TCP的重连。
而当移动 App 在前台时则按照原来规则操作。连接可靠性的判断也可以放宽,避免一次心跳超时就认为连接无效的情况,使用错误积累,只在心跳超时 n 次后才判定当前连接不可用。当然还有一些小 trick 比如从收到的最后一个指令包进行心跳包周期计时而不是固定时间,这样也能够一定程度减少心跳次数。
4.消息的可达性(QoS机制)
Qos即Quality of service,翻译过来就是服务质量。在移动网络下这个消息可达更加困难,因为移动端网络,丢包,重连等情况非常之多,为了保证消息的可达,需要做消息的回执和重发机制。比如易信,每条消息会最多有3次重发,超时时间为15秒,同时在发送之前会检测当前连接状态,如果当前连接并没有正确建立,缓存消息且定时检查(每2秒检测一次,检查15次)。所以一条消息在最差情况下会有2分钟左右的重试时间,以保证消息可达。
title: 有关于即时通讯的开发要点 date: 2017-09-10 09:26:00 tags:
消息推送
前言
现在互联网应用如火如荼,微信,QQ等产品就先暂时不提了。每个移动端App的应用几乎有自己的消息推送系统,还有传统的电商网站京东,淘宝等都有自己的聊天工具,阿里旺旺,或者可以直接在Web端发送消息给卖家。等等这些产品都用到了IM(Instant Messaging)技术。下面我会通过我经过网络上的资料,整理转变我自己的理解,以及对这方面相关开发的的见解和看法。当然,这篇文章是综述性的文章,不会讲解到每个细节,我只能尽力而为。
从UDP和TCP开始
对于有网络Socket编程经验的开发者来说,使用何种传输层协议来实现数据的通信是一个非常基础的问题。从PC时代的IM开始,IM开发者们在为数据传输协议的选型争论不休。到了移动互联网时代,鉴于移动网络的不可靠等特点,再加上手机的省点策略,流量压缩等,为这个问题的回答增加了更多的不确定因素。
在分析到底使用UDP还是TCP之前,有必要先讨论一下互联网与移动互联网的网络环境特点。
互联网的网络基础建设,经过十几年长期的发展,已经较为稳定和成熟,PC终端、操作系统的能力也达到了较高的水平。
而移动互联网,由于涉及到无线电话网络基站、2G、3G和4G技术的不断发展,其稳定性、带宽、资源分配等各方面虽日趋完善,但当前终究还有不少问题的存在。另外,由于移动互联网其“移动”的本质,加上智能终端设备(智能手机、平板电脑)的发展较晚,目前还在不断演变的情况,与互联网相比,移动互联网还是低速、不稳定、终端能力稍弱的情况。而且由于其“移动”本质,短时间内很难达到互联网的质量。
所以,在互联网的环境里面,网络应用程序由于网络设施、操作系统的成熟,开发使用起来比较容易,资源也较为充足。而移动互联网还是要“斤斤计较”。
智能移动终端电池续航能力和系统休眠
智能终端设备的电池续航能力始终是技术瓶颈。在连续使用的情况下,绝大部分智能设备电池无法支持两个小时以上。所以在没有外部电源的情况,智能终端设备必须频繁、长时间休眠,这将极大地影响两种网络环境下的网络应用场景。
IPv4资源,端口资源
这个话题往往被很多人忽略,但它有着至关重要的影响。虽然大部分人都很清楚IP地址的紧缺导致的动态IP分配的必然,却忽略了由于IP地址不足引起的端口资源不足。
由于需要动态分配IP地址(这里不仅仅指互联网入口的IP,还包括局域网内部的IP),路由器的工作原理都是经过端口映射,把内部网络(包括PC、手机、平板、Wifi、2G、3G、4G)IP与端口映射成外部IP(通常是公网IP)和对应的端口,并维持这个映射关系,才能正常地修改、转发报文信息,保证内部各个ip、端口与外部的各个ip、端口的通信。
然而,单个IP地址的端口资源是有限的,理论上限是65535个端口。对于普通宽带路由器来说,这个已经很充足了。但是!对于大型的网络服务、网络主干接入点等来说,如果IP资源不足,每个IP几万个端口的资源很快会耗尽,从而影响正常通讯。
端口映射老化时间
正因为如此,所有的路由器都会为每个端口映射关系设置老化时间,如果老化时间过期,则端口映射关系失效,该端口被释放给其他连接使用。如果端口全部耗尽,则无法再新建内部与外部的网络连接。
端口映射老化时间,比很多人想象中的要短很多。一般的家用宽带路由器,老化时间一般是两三分钟;在有线宽带运营商接入部分,老化时间可能少于两分钟。在无线电话网络运营商接入部分(例如GPRS连接),老化时间甚至不超过一分钟!
也就是说,任何一个网络通讯(不管是TCP或UDP),如果几分钟之内没有网络报文传输,其占用的IP地址端口将被路由器回收。这个时候该次通信必将终止,不管TCP还是UDP,那么都是浮云。
更残酷的事实是,互联网可认为是由无数个路由器连接而成的,一个网络通信往往需要通过n个路由器,每个路由器都会为一次通信建立自己的端口映射。只要其中一个路由器回收其端口,则整个通讯中断。
这也是很多人疑惑为什么TCP的KeepAlive参数无法保证长连接的原因。TCP的KeepAlive默认是两个小时(而且该参数还是TCP的可选实现,不是必然实现),在路由器端口映射老化时间的影响下,必然无法发挥其作用。实际上,该参数在单一的局域网内才可能被使用上,还要依赖具体的操作系统。
由于路由器端口映射的存在,加上智能终端频繁、长时间的休眠,TCP长连接的实用性在移动互联网情况下极大地打了折扣。
也因为如此,移动端IM、推送系统必须实现所谓的心跳包机制,以保持端口映射关系的老化时间不会减少到0而被回收,从而避免连接中断。
服务端承载能力
不管是UDP还是TCP,最终都是应用服务端的设备去提供服务的。而TCP由于提供了安全可靠的流服务,其对计算机、网络资源的消耗是远远大于UDP协议的。对于配置较好的主流服务器,配备大量的内存(数十G至上百G内存),与高速的磁盘、网卡,是能同时支持数百万个TCP连接的。不过这里需要较专业的服务器设置,需要调整不少系统参数,再加上服务程序的配合。另外,TCP连接的建立、维持与释放,都是需要较昂贵的计算、网络资源的。
终端在线服务,若是一个较为简单的服务,未必使用上TCP众多的高级功能,但承受TCP的昂贵成本,未必值得。如果能用UDP来提供服务,单服务器的承载能力,是可以去到TCP服务的数十倍,甚至上百倍的增长。这也是为什么DNS这种并发数巨大的服务器提供UDP接口的原因。
另外,上百万TCP连接的网络服务,其编程的难度、程序复杂度、调试难度、服务器运维成本、网络成本等都远远高于UDP。
而UDP编程,与上百万个终端通讯的难度与成本则低很多。如果提供的网络服务不是基于流的服务,也允许一定的失败机率(例如P2P),则UDP往往是更适合的方式。
QQ的选择
从最经常接触的QQ来说,它既有UDP又有TCP,最终登录成功后,QQ都会有一个TCP连接来保持在线状态。这个TCP连接的远程端口一般是80,采用UDP方式登录的时候,端口是8000。
UDP协议是无连接方式的协议,它的效率高,速度快,占资源少,但是其传输机制为不可靠传送,必须依靠辅助的算法来完成传输控制。QQ采用的通信协议以UDP为主,辅以TCP协议。由于QQ的服务器设计容量是海量级的应用,一台服务器要同时容纳十几万的并发连接,因此服务器端只有采用UDP协议与客户端进行通讯才能保证这种超大规模的服务。
QQ客户端之间的消息传送也采用了UDP模式,因为国内的网络环境非常复杂,而且很多用户采用的方式是通过代理服务器共享一条线路上网的方式,在这些复杂的情况下,客户端之间能彼此建立起来TCP连接的概率较小,严重影响传送信息的效率。而UDP包能够穿透大部分的代理服务器,因此QQ选择了UDP作为客户之间的主要通信协议。
采用UDP协议,通过服务器中转方式。因此,现在的IP侦探在你仅仅跟对方发送聊天消息的时候是无法获取到IP的。大家都知道,UDP 协议是不可靠协议,它只管发送,不管对方是否收到的,但它的传输很高效。但是,作为聊天软件,怎么可以采用这样的不可靠方式来传输消息呢?于是,腾讯采用了上层协议来保证可靠传输:如果客户端使用UDP协议发出消息后,服务器收到该包,需要使用UDP协议发回一个应答包。如此来保证消息可以无遗漏传输。之所以会发生在客户端明明看到“消息发送失败”但对方又收到了这个消息的情况,就是因为客户端发出的消息服务器已经收到并转发成功,但客户端由于网络原因没有收到服务器的应答包引起的。
UDP包中的一个包的大小最大能多大?
既然大部分都采用了UDP来实现IM,那么UDP包的大小应该设置到多大呢?因为UDP是无连接的,它不需要TCP的3次握手,直接向特定IP和端口上扔数据就可以了,不管对方是否开启了服务。而且UDP包是有边界的,一次数据互交就是一次UDP包传输,没有拆包的概念,但是由于它的不可靠,UDP包到达目的地包的顺序可能会乱掉,或者丢失。所以一次UDP包的发送如果数据太多的话,用UDP实现可能就不优美。
学过计算机网路都都知道,工业界主流的都是采用TCP/IP协议栈,总共有四层,从上到下是,应用层,传输层,网络层,数据链路层。UDP与TCP是通用在传输层的协议。所以以下两个结论是很多开发人员的总结。
结论一:局域网环境下,建议将UDP数据控制在1472字节以下
以太网(Ethernet)数据帧的长度必须在46-1500字节之间,这是由以太网的物理特性决定的,这个1500字节被称为链路层的MTU(最大传输单元)。但这并不是指链路层的长度被限制在1500字节,其实这这个MTU指的是链路层的数据区,并不包括链路层的首部和尾部的18个字节。
所以去除头部和尾部,那么实际上这个MTU就是网络层IP数据报的长度限制。由于IP数据报文本身也有头部20字节,所以IP数据包的数据区长度最大为1480字节。而这个1480字节就是用来存放TCP或UDP数据报文的。
因为IM大部分采用了UDP传输,所以又去除UDP报文的头部8个字节,所以UDP数据报的数据区最大长度为1472字节。这个1472字节就是我们一次UDP Socket所发送的最大字节数。也就是说,聊天消息的内容最好不要超过1472个字节。
如果我们发送的聊天内容超过了1472个字节了呢? 换句话说就是在数据链路层超过了1500字节的MTU长度呢?这个时候发送放的网络层(IP层),就会把这个内容进行分片(fragmentation),把数据报文分成若干片,使每一片都小于MTU,而接收方的网络层需要进行报文重组。这样麻烦的事情就来了,在这里你可能会有疑问,这些都是TCP/IP协议栈帮你实现好了啊,IP层会自动拆分和重组啊,有什么好担心的?应用层根本不用管嘛。如果你这样想,那么就大错特错了,因为UDP的特性,当由于一次发送超过MTU进行分片发送时,如果在传输过程中某一片数据丢失了,那么接收方就无法重组IP数据报文,那么将丢弃整个UDP数据报,简而言之就是,这次发送完全失败,接收方传输层完全看不到。如果网络环境很烂,分片数据会经常丢失,那么对于用户来将,发送方发10次聊天内容,接收方有9次收不到。
结论二:Internet编程时,建议将UDP数据控制在548字节以下
因为Ineternet上的网络环境更加复杂和拥堵,而且Internet上的路由器可能会将MTU设置为不同的值(路由器一般来说是三层设备,实现了物理层,链路层,网络IP层。所以可以设置数据链路层的MTU)。
鉴于Internet上的标准MTU为576字节,所以在进行Internet上的UDP编程时,最好将UDP的数据区长度控制在548字节以内,548字节怎么得来的?还是结论一的算法,576 - 20 - 8 = 548。
IPv4标准 指出, IP报文中的Total Length字段说明有以下文字:
也就是说,IPv4规定IP层的最小重组缓冲区大小为576字节,也就是超过这个字节数就重组,一旦重组就会大概率丢失。所以本质原因很可能不是Internet上的标准MTU是576个字节。
应用层采用什么数据传输格式
之前讨论过了,传输层尽量采用UDP传输,但是基于UDP报文的数据区的格式应该采用什么呢?这个阶段的选择也比较有争议。
精要分析,原因大概在以下三点:
可选择的协议或封装格式多种多样:XMPP、Protobuf、JSON、私有2进制、MQTT、XML、类似与http协议的纯文本方式等等;
同一种格式并不能适用于大多数的场景: 不同的场景有同的考虑而协议的选择往 跟这是挂钩在一起的,比如:移动端IM或推送技术用XMPP这样的协议时,多数情况下都会被喷
开发者对所选格式有各自的偏好: 有的人或团队对某种或某几种格式有不一样的经验和技术积累,也促成了他们对某种或某几种协议的偏好。
当然,这些协议的选择应该还是具体问题具体分析,该选什么协议由场景决定、由团队的技术积累决定、甚至由项目的周期和成本决定,这里不存在唯一解,只有最适合的数据传输格式,不存在最好的格式一说。
数据格式的选择需要考虑的方面
1.网络数据大小:占用带宽,传输效率
虽然对单个用户来说,数据量传输很小,但是对于服务器端要承受众多的高并发数据传输(尤其现时高并发、大用户量的IM聊天应用和实时推送服务端等场景),必须要考虑到数据占用带宽,尽量不要有冗余数据,这样才能够少占用带宽,少占用资源,少网络IO,提高传输效率。
2.网络数据安全性:敏感数据的网络安全
对于相关业务的部分数据传输都是敏感数据,所以必须考虑对部分传输数据进行加密。这通常出现在银行等数据安全性要求很高的应用行业和场景里,当然传统的即时通讯应用里基于用户隐私考虑,数据加密也是同样是个必须考虑的问题。安全性是应用的基础条件,需求是一样的,只是加密程度、安全性级别要求有不同而已。
3.编码复杂度
编码复杂度包括序列化和反序列化复杂度、效率、数据结构的可扩展性和可维护性,开发人员编码工作量。
对于平台相关业务的代码实现也需要考虑到数据发送方和数据接收方数据处理的复杂度和数据结构的可扩展性,可维护性,人力成本和实施复杂度也必须考虑在内。通常情况下,即时通讯应用(比如IM聊天应用)在开发的前期,为了方便调试,很多团队会用简单的文本协议、JSON等能直观查看的方式,但后期生产部署后,为了流量等考虑,可能会转用Protobuf等更省流量的协议。但总之,协议的定义不可能永远一成不变,但如果在实现的时候就有这些预见性,相性会大大减轻未来的运营风险。
4.协议通用性、大众规范
数据类型必须是跨平台,数据格式是通用的,大家普遍能接受上手的。当然,现在已经迈入移动互联网时代,多端、多平台、异构平台的数据通讯是先决条件,而协议的选择,通用性也最多只是应用层有区别。当然,无论如何,异构平台的一致性,是毫无争议的必备条件。
不同类别的数据传输协议格式的对比
1.自定义二进制
像大多数嵌入式网络应用开发的公司一般都会选择自定义应用层二进制协议,比如自己定义协议头,协议正文。那个时候还需要开发人员自己手工序列化,把主机字节序转换为网络字节序,或者反之。协议规范小,这没问题,一旦很大,需要耗费很大精力和时间。如果用protobuf这样的序列化库就轻松很多了。
优点:信息体积小,占用带宽小,传输效率高,即使不加密,破解难度也高。 缺点:编码复杂度高,自己定义消息格式,自己编写序列化和反序列化方法,自己进行容错处理,可扩展性不强,比如添加个字段,就必须改两端的逻辑处理。
2.提供序列化和反序列化库的开源协议
比如Potobuf,json,还有Apache的 Thrift。
优点:支持多语言,通用流行的数据格式,扩展方便,库都支持序列化和反序列化,错误处理也库也支持。信息体积小,占用带宽小,传输效率高。 缺点: protobuf其实本质上也是自定义二进制协议,只不过是库帮开发人员做了序列化和反序列化的工作,而且还有容错处理。如果要说缺点,那么就是调试不方便,抓包的时候不直观,可视化不强。
3.纯文本协议
比如XML,json,类http自定义纯文本格式,SIP。 优点:支持序列化和反序列化,错误处理库也支持,调试方便,可视化强。 缺点:相对于二进制存储占用体积大
所以归而结网,到底怎么选择呢?尽量选择方便调试,可视化强的并且库支持的数据格式。现在XML已经不是主流了,主流网站提供的API,基本都是http传输JSON格式的数据。无非就是延迟性高点,传输效率低点。
自定义二进制格式太复杂了,整个过程都在设计协议消息格式,通过网络库或者Socket的read,write的过程过于复杂,容易出错,如果是基于TCP协议设计的二进制格式,开发人员还必须自己编写代码对TCP拆包,对于很多数据互交的程序,会话费大量时间调试,关注底层细节。
自定义二进制格式不便于扩展,增加,删除协议字段客户端和服务端逻辑都得更改。
如果真选用JSON这样的文本化格式,占用网络带宽这样的问题,其实可以通过数据压缩等手段来解决,JSON本身格式并不复杂,传输效率不低。
当然,如果是我自己的选择,综合考虑我会选择protobuf。因为它支持C++,Java,Python语言等。而且用其作为传输格式,带宽占用与自定义二进制格式只少不多,传输效率很高。对于消息数据大小很敏感的应用是个很好的选择,据说手机QQ的数据传输协议就是使用的protobuf。我以前有个大学同学在的一家手机游戏移动端与服务端的数据传输也是采用的protobuf,基本上不用采取第三方的额外数据压缩手段来调优。从各方面的权衡都好于蘑菇街开源的企业级IM应用---TeamTalk。
根据网络上其他开发人员的实际测试,用protobuf序列化后的数据大小是json的10分之一,XML格式的20分之一,自定义二进制序列化的10分之一。基于占尽了各种优势,在效率、数据大小、易用性之间取得了很好的平衡,只有一个调试可视化不强的缺点了。
protobuf的易用性在于,它采用了某种定义结构化的消息格式的语言定义消息格式,然后通过protobuf的命令行工具生成序列化和反序列消息的类,非常轻松。与大多数主流RPC框架一样的使用方式。只不过更加底层一点罢了。
IM客户端设计面临的问题
前面讨论的技术选型也差不多了,接下来就要考虑IM客户端的问题了。这里所说的客户端包括PC的客户端,手机移动端,还有Web端。主要是这三种客户端。
P2P传输还是服务器中转?
IM的通讯方式无非两种:设备直连(P2P)和通过服务器中转
1.P2P方式
P2P多见于局域网内的聊天工具,典型的应用有:飞鸽传书,飞Q等。这类软件在启动后一般做两件事:
P2P的方式有种种限制和不便,一方面,它只适合在线的点对点消息传输,对离线,群组等业务支持不够。另以方面,由于NAT的存在,使得局域网内机器互联难度大大提升,在某些网络类型(对称NAT)下无法建立连接。
2.服务器中转
因为IM通讯工具和相关云服务都是建立在互联网上的,所以几乎所有市面上的IM产品(QQ,微信,YY语音等等)都采用服务器中转这种方式进行消息传输,相对于P2P方式,它有如下优点:
当然,也有缺点,就是服务器架构复杂,并发要求高,团队成员技术要求高。
采用的通讯技术
因为之前讨论过UDP和TCP的选型,所以这里不过多讲解,目前主流的IM网络通讯技术有三种:
第二种常见于Web端的IM客户端,它的优点是实现简单,方便开发上手,问题是流量大,服务器负载较大,消息实时性无法很好的保证,对大规模的用户量支持不够,比较适合小型的IM应用。当然,现在Html5标准下,有了WebSocket这样的全双工协议,服务器可以主动向Web端推送消息了,所以对于Web端优先考虑WebSocket协议。由于WebSocket协议比较复杂,是二进制协议,开源的Socket.io就是为了降低WebSocket协议使用难度了进行了封装。
基于TCP长连接能够更好的支持大量用户,问题是客户端和服务器实现比较复杂。也有一些变种,如下行消息推送使用MQTT进行服务器通知/消息的下发,上行使用Http短链接进行指令和消息的上传。这种变种方式能够保证下行消息的及时性,但是在弱网络环境下,上行慢的问题还是比较严重。
基于UDP报文的应答是保证消息QoS机制的关键要点,因为UDP是不可靠的,所以要在应用层做消息重发和应答保证消息可靠性。
其他不可忽视的问题
1.协议加密
这个不过多讲解。
2.掉线重连
由于之前提到过的端口,UDP等老化机制,连接不可能一直有效,在失效的时候必须要重新连接服务器,特别对于之前版本的iOS APP而言并没有真正的后台程序,所以在每一次APP启动的时候都需要一次重连登录。移动端的网络环境随时不稳定,所以更需要考虑此问题。
3.连接保持(心跳机制)
一般IM客户端实现连接保持的方式无非是采用应用层的心跳,通过心跳包的超时和其他条件来进行重连机制,也防止了之前提到的UDP等端口老化的问题。这里懂计算机网络的人可能会问了,TCP协议有KeepAlive这个设置选项,设置为KeepAlive后,客户端每隔N秒(默认7200秒)会向服务器发送一个心跳包。但是这个是理想状况,做工程就是解决很多不是理想状况的问题,因为从实际角度考虑:
基于以上特点,有必要在IM客户端设计应用层的心跳机制。如果客户端是移动端一般会在心跳包上做一些优化:
所以之前说的QQ用UDP发送接收消息,用TCP来告知服务器客户端的在线状态。这样的设计是合理的。
如果到现在还认为在TCP的IM中没有必要用心跳保活,那么我还可以详细分析其必要性。
对于客户端而言,使用TCP长连接来实现业务的最大驱动力在于:在当前连接可用的情况下,每一次请求都只是简单的数据发送和接受,免去了 DNS 解析,连接建立等时间,大大加快了请求的速度,同时也有利于接受服务器的实时消息。但前提是连接可用。
如果连接无法很好地保持,每次请求就会变成撞大运:运气好,通过长连接发送请求并收到反馈。运气差,当前连接已失效,请求迟迟没有收到反馈直到超时,又需要一次连接建立的过程,其效率甚至还不如 HTTP。而连接保持的前提必然是检测连接的可用性,并在连接不可用时主动放弃当前连接并建立新的连接。
基于这个前提,必须要有一种机制用于检测连接可用性。同时移动网络的特殊性也要求客户端需要在空余时间发送一定的信令,避免连接被回收。
而对于服务器而言,能够及时获悉连接可用性也非常重要:一方面服务器需要及时清理无效连接以减轻负载,另一方面也是业务的需求,如游戏副本中服务器需要及时处理玩家掉线带来的问题。
而TCP的KeepAlive无法代替应用层心跳保活机制的本质原因还是网络本身的不可靠,之前提到的第三点是最重要的。因为TCP是一个基于连接的协议,其连接状态是由一个状态机进行维护的,连接完毕后,通信双方都会处于 established 状态,这之后的状态并不会主动进行变化。这意味着如果上层不进行任何调用,一直使 TCP 连接空闲,那么这个连接虽然没有任何数据,但仍是保持连接状态,一天、一星期、甚至一个月,即使在这期间中间路由崩溃重启无数次,网络抖动,网线插拔无数次,那么TCP的状态机还是保持established的状态,连接还是处于逻辑上连接的状态。比如,现实中经常会遇到例子:当我们SSH到特定的VPS上,不小心踢掉了网线,此时的网络变化不会被TCP检测出来,当我们重新插回网线,仍然可以继续正常使用SSH,同时此时并没有发生任何TCP的重连。
以上的举例意味着,TCP的KeepAlive机制没法检测出来物理网线掉线的情况,它可能会认为一直逻辑上连接着,但是事实上服务的提供方已经不能提供服务了,因为网线被踢掉了,数据链路层已经断开掉了。所以,KeepAlive机制和应用层心跳机制是有区别的,KeepAlive机制是用于检测连接的死活状态(是否有效),而心跳机制则附带一个功能就是检测通信双方的软件的存活状态,是否正在工作运行。
考虑一种实际场景,某台服务器因为某些原因导致负载很高,CPU 100%。无法响应任何请求,但是TCP的KeepAlive机制发送的KeepAlive探针知道并确定这个连接还活着,但是显然服务器已经不算正常运行了,不能提供服务了。而这种情况对于客户端而言,这时最好的选择就是主动关闭TCP Socket断线后重连其他服务器,而不是一直认为当前服务器是可用状态。当然,一般采用TCP长连接的分布式RPC框架都内置这个负载均衡的路由功能了,也有心跳机制,所以业务层开发人员不需要当心。
下面有心跳保活的参考实现方案:
最简单粗暴的做法当然是定时心跳,如每30秒客户端发送心跳一次,15秒内没收到心跳回包则认为当前连接失效,断开连接并重连。这种做法简单直接。如果是PC端这没问题,但是对于手机移动端就麻烦了,这样做比较耗电和耗流量。以一个心跳协议包5个字节计算,一天24小时,就有1460=840分钟,一天就有84060=50400秒,那么一天就有50400/30=1680个心跳客户端需要发送,然而,还需要接收服务端返回来的心跳应答,那么总共需要收发16802=3360个心跳包,那么一天就要耗费33605=16800字节流量,一个月就是16800*30/1024/1024=0.48MB流量,如果多装几款IM软件,每个月光心跳就可以耗费几兆流量,更不用说频繁的心跳带来的电量损耗了。
既然频繁心跳会带来耗电和耗流量的弊端,改进的方向自然是减少心跳频率,但也不能过于影响连接检测的实时性。基于这个需求,一般可以将心跳间隔根据程序状态进行调整,当程序在后台时,尽量拉长心跳间隔,5 分钟甚至10分钟都可以。
而当移动 App 在前台时则按照原来规则操作。连接可靠性的判断也可以放宽,避免一次心跳超时就认为连接无效的情况,使用错误积累,只在心跳超时 n 次后才判定当前连接不可用。当然还有一些小 trick 比如从收到的最后一个指令包进行心跳包周期计时而不是固定时间,这样也能够一定程度减少心跳次数。
4.消息的可达性(QoS机制)
Qos即Quality of service,翻译过来就是服务质量。在移动网络下这个消息可达更加困难,因为移动端网络,丢包,重连等情况非常之多,为了保证消息的可达,需要做消息的回执和重发机制。比如易信,每条消息会最多有3次重发,超时时间为15秒,同时在发送之前会检测当前连接状态,如果当前连接并没有正确建立,缓存消息且定时检查(每2秒检测一次,检查15次)。所以一条消息在最差情况下会有2分钟左右的重试时间,以保证消息可达。
因为重发的存在,接收端偶尔会收到重复的消息,这种情况下就需要接收端进行去重。通用的做法是每条信息都携带上自己唯一的message id(一般是UUID)。
对于消息可达性,也就是Qos机制的实现又是一个非常大的话题,需要自己分析理解。所以之后再另写一篇新的文章来巩固Qos的知识,这篇文章至此为止就完结了。