文中引用了参考资料中的部份内容,本文参考资料详见文末“参考资料”一节,感谢资料分享者。html
一、引言
对于IM开发者而言,网络保活这件事再熟悉不过了,好比这是我最近一篇有关网络保活话题文章《一文读懂即时通信应用中的网络心跳包机制:做用、原理、实现思路等》,以及我分享的大量代码实战编码中也都必需要考虑这个问题的实现,好比最近的这篇《跟着源码学IM(五):正确理解IM长链接、心跳及重连机制,并动手实现》。java
对于IM这种应用而言,应用层的网络保活的最直接办法就是心跳机制,好比主流的IM里有微信、QQ、钉钉、易信等等,可能代码实现细节有所差别,但理论上无一例外都是这样实现。(PS:没错,当初微信跟运营商间的“信令危机”就是跟这个有关)linux
所谓的网络心跳,一般是客户端每隔一小段时间向服务器发送一个数据包(即心跳包),通知服务器本身仍然在线(心跳包中同时可能传输一些必要的数据)。发送心跳包,从通讯层面来讲就是为了保持长链接,至于这个包的内容,是没有什么特别规定的,但在移动端IM中为了省流量,通常都是很小的包(好比某些第3方的IM云为了说明心跳不费流量,号称1字节的心跳包)。程序员
但常常有人会问到,既然TCP协议自己有KeepAlive保活这个东西(见:《TCP/IP详解 卷1 - 第23章·TCP的保活定时器》),为何还要自已在应用层去实现网络保活/心跳机制呢?编程
没错,一般面视即时通信/IM方面的程序员时,这几乎是必提问题!浏览器
要解答这个问题,我一般建议看看《为何说基于TCP的移动端IM仍然须要心跳保活?》这篇。但限于篇幅,该篇并无深刻探讨TCP协议自己的KeepAlive机制,因此此次借本文想把TCP协议的KeepAlive保活机制给详细的整理出来,以便你们能深刻其中一窥究竟。安全

二、系列文章
本文是系列文章中的第12篇,本系列文章的大纲以下:服务器
- 《鲜为人知的网络编程(一):浅析TCP协议中的疑难杂症(上篇)》
- 《鲜为人知的网络编程(二):浅析TCP协议中的疑难杂症(下篇)》
- 《鲜为人知的网络编程(三):关闭TCP链接时为何会TIME_WAIT、CLOSE_WAIT》
- 《鲜为人知的网络编程(四):深刻研究分析TCP的异常关闭》
- 《鲜为人知的网络编程(五):UDP的链接性和负载均衡》
- 《鲜为人知的网络编程(六):深刻地理解UDP协议并用好它》
- 《鲜为人知的网络编程(七):如何让不可靠的UDP变的可靠?》
- 《鲜为人知的网络编程(八):从数据传输层深度解密HTTP》
- 《鲜为人知的网络编程(九):理论联系实际,全方位深刻理解DNS》
- 《鲜为人知的网络编程(十):深刻操做系统,从内核理解网络包的接收过程(Linux篇)》
- 《鲜为人知的网络编程(十一):从底层入手,深度分析TCP链接耗时的秘密》
- 《鲜为人知的网络编程(十二):完全搞懂TCP协议层的KeepAlive保活机制》(* 本文)
三、TCP KeepAlive的初衷
采用TCP链接的C/S模式应用中,当链接的双方在链接空闲状态时,若是任意一方意外崩溃、当机、网线断开或路由器故障,另外一方没法得知TCP链接已经失效。微信
那么,链接的另外一方并不知道对端的状况,它会一直维护这个链接。而做为“服务端”来讲,长时间的积累会致使很是多的半打开链接,形成端系统资源的消耗和浪费,且有可能致使在一个无效的数据链路层面发送业务数据,结果就是发送失败。网络
因此各端要作到快速感知失败,减小无效连接操做,这就有了TCP的KeepAlive保活探测机制。
PS:这样宽泛的说TCP的KeepAlive机制的必要性,貌似还不是颇有说服力,下节将带着具体的例子深刻分析。
四、从NAT角度更具体地理解TCP KeepAlive的必要性
讲到TCP的KeepAlive的必要性,多数文章都是像上节这样比较笼统的进行说明,但对于爱刨根问底的开发者来讲,这还远远不够。
本节将以路由器的NAT机制这个角度来具体分析TCP协议的造物主们设计KeepAlive机制的必要性。
4.1 从NAT原理讲起
狭义上,NAT分为SNAT(原地址转换)和DNAT(目标地址转换),关于DNAT,有兴趣的同窗能够自行查阅,这里只讨论SNAT。
咱们都知道,路由器的最基本功能是对第三层(网络层)上的IP报文进行转发。实际上,路由器还有很关键的一个功能,这即是NAT。特别是对于ISP对普通用户链路上的路由器,NAT功能尤其重要。
为何要使用NAT?
缘由很简单:IPv4地址很是稀缺。上网需求庞大,这使得ISP不可能为每个入网用户都提供一个独立的公网IP,所以一般状况下,ISP会把用户接入局域网,使得多个用户共享同一个公网IP,而每个用户各分得一个局域网内网IP。而链接公网和局域网的这台路由器,称之为网关(gateway),NAT的过程就发生在这台网关路由器上。
PS:《P2P技术详解(一):NAT详解——详细原理、P2P简介》这篇文章有助于更深刻的理解NAT原理。
4.2 三层地址转换
局域网内的主机向公网发出的网络层IP报文,将经由网关被转发至公网,而在该转发过程当中发生了地址转换。网关将该IP报文中的 源IP地址 从”该主机的内网IP”修改成”网关的公网IP”。
好比:局域网主机得到的内网IP为192.168.1.100,网关的公网IP为210.177.63.2,局域网主机向公网目标主机发出的IP报文中,源IP字段数据为192.168.1.100,在通过网关时,该字段数据将被修改成210.177.63.2。
为何要这么作,相信你们已经猜到了:公网上的目标主机在收到这个IP报文后,须要知道这个IP报文的来源地址,并向该来源地址发送响应报文,但若是不通过NAT,目标主机拿到的来源地址是192.168.1.100,这显然是一个公网上不可访问到的私有地址,目标主机没法将响应报文发送到正确的来源主机上。开启了NAT以后,IP报文的来源地址被网关修改成210.177.63.2,这是一个公网地址,目标主机将向这个地址(即网关路由器的公网地址)发送响应报文。
可是请注意:若是这个IP报文的数据段不含传输层协议报文,而是一个pure的网络层packet,来自目标主机的响应报文是不能被网关准确转发到多台局域网主机中的其中一台的。
PS:ICMP报文除外,其报头中有Identifier字段用于标识不一样的主机或进程,网关在处理Identifier时相似于下面提到的运输层端口。
4.3 传输层端口转换表
在三层地址转换中,咱们能够保证局域网内主机向公网发出的IP报文能顺利到达目的主机,可是从目的主机返回的IP报文却不能准确送至指定局域网主机(咱们不能让网关把IP报文广播至所有局域网主机,由于这样必然会带来安全和性能问题)。
为了解决这个问题,网关路由器须要借助传输层端口,一般状况下是TCP或UDP端口,由此来生成一张端口转换表。
让咱们经过一个实例来讲明端口转换表如何运做:
假设局域网主机A192.168.1.100须要与公网上的目标主机B210.199.38.2:80进行一次TCP通讯。其中A所在局域网的网关C的公网IP地址为210.177.63.2。
步骤以下:
1)局域网主机A192.168.1.100发出TCP链接请求,A上的TCP端口为系统分配的53600。该TCP握手包中,包含源地址和端口192.168.1.100:53600,目的地址和端口210.199.38.2:80。
2)网关C将该包的原地址和端口修改成210.177.63.2:63000,其中63000是网关分配的临时端口。
3)网关C在端口转换表中增长一条记录:

4)网关C将修改后的TCP包发送至目的主机B。
5)目的主机B收到后,发送响应TCP包。该响应TCP包含有如下信息:源地址和端口210.199.38.2:80,目的地址和端口210.177.63.2:63000。
6)网关C收到这个来自B的响应包后,随即在端口转换表中查找记录。该记录须符合如下条件:目的主机IP==210.199.38.2,目的主机端口==80,网关端口==63000。
7)网关C搜索到这条记录,记录显示内网主机IP为192.168.1.100,内网主机端口为53600。
8)网关C将该包的目的地址和端口修改成192.168.1.100:53600。
9)网关C随即将该修改后的TCP包转发至192.168.1.100:53600,即局域网主机A。此时运输层数据的一次交换已完成。
4.4 问题来了
在网关C上,因为端口数量有限(0~65535),端口转换表的维护占用系统资源,所以不能无休止地向端口转换表中增长记录。对于过时的记录,网关须要将其删除。
如何判断哪些是过时记录?
网关认为:一段时间内无活动的链接是过时的,应定时检测转换表中的非活动链接,并将之丢弃。而这个丢弃的过程,网关不会以任何的方式通告该链接的任何一端。
经过下图能够更直观的理解这个过程:

▲ 上图引用自《TCP保活(TCP keepalive)》
那么问题就来了:若是一个客户端应用程序因为业务须要,须要与服务端维持长链接(例如基于TCP的IM聊天应用),而若是在特别长的时间内这个链接没有任何的数据交换,网关会认为这个链接过时并将这个链接从端口转换表中丢弃。该链接被丢弃时,客户端和服务端对此是彻底无感知的。在链接被丢弃后,客户端将收不到服务端的数据推送,客户端发送的数据包也不能到达服务端。
一个具体的例子来感觉一下这个问题的严重性:
某财务应用,在客户端须要填写大量的表单数据,在客户端与服务器端创建TCP链接后,客户端终端使用者将花费几分钟甚至几十分钟填写表单相关信息,终端使用者终于填好表单所需信息后,点击“提交”按钮。
结果,这个时候因为中间设备早已经将这个TCP链接从链接表中删除了,其将直接丢弃这个报文或者给客户端发送RST报文,应用故障产生,这将致使客户端终端使用者全部的工做将须要从新来过,给使用者带来极大的不便和损失。
4.5 解决方法
针对上述问题,TCP协议这一层的解决方法就是利用KeepAlive机制维持长链接,让网关认为咱们的TCP链接是活动的,从而避免网关“干掉”咱们的长链接。
经过NAT这个具体的例子,相信你已经能更具体地理解TCP协议中KeepAlive保活机制的必要性了。
五、TCP Keepalive工做原理
5.1 技术原理
当一个 TCP 链接创建以后,启用 TCP Keepalive 的一端便会启动一个计时器,当这个计时器数值到达 0 以后(也就是通过tcp_keep-alive_time时间后,这个参数以后会讲到),一个 TCP 探测包便会被发出。这个 TCP 探测包是一个纯 ACK 包(RFC1122#TCP Keep-Alives规范建议:不该该包含任何数据,但也能够包含1个无心义的字节,好比0x0),其 Seq号 与上一个包是重复的,因此其实探测保活报文不在窗口控制范围内。
若是一个给定的链接在两小时内(默认时长)没有任何的动做,则服务器就向客户发一个探测报文段,客户主机必须处于下表中的4个状态之一。

详细解释一下就是:
1)客户主机依然正常运行,并从服务器可达。客户的TCP响应正常,而服务器也知道对方是正常的,服务器在两小时后将保活定时器复位。
2)客户主机已经崩溃,而且关闭或者正在从新启动。在任何一种状况下,客户的TCP都没有响应。服务端将不能收到对探测的响应,并在75秒后超时。服务器总共发送10个这样的探测 ,每一个间隔75秒。若是服务器没有收到一个响应,它就认为客户主机已经关闭并终止链接。
3)客户主机崩溃并已经从新启动。服务器将收到一个对其保活探测的响应,这个响应是一个复位,使得服务器终止这个链接。
4)客户机正常运行,可是服务器不可达,这种状况与2相似,TCP能发现的就是没有收到探测的响应。
直观来讲,TCP KeepAlive的交互过程大体以下图所示:

▲ 上图引用自《TCP保活(TCP keepalive)》
5.2 具体使用举例
以linux内核为例,应用程序若想使用TCP Keepalive,须要设置SO_KEEPALIVE套接字选项才能生效。
对应的,有三个重要的参数:
- 1)tcp_keepalive_time,在TCP保活打开的状况下,最后一次数据交换到TCP发送第一个保活探测包的间隔,即容许的持续空闲时长,或者说每次正常发送心跳的周期,默认值为7200s(2h);
- 2)tcp_keepalive_probes 在tcp_keepalive_time以后,没有接收到对方确认,继续发送保活探测包次数,默认值为9(次);
- 3)tcp_keepalive_intvl,在tcp_keepalive_time以后,没有接收到对方确认,继续发送保活探测包的发送频率,默认值为75s。
上面谈的是linux内核参数的配置,实际上其余编程语言有相应的设置方法。
例如,Java的Netty服务器框架中也提供了相关接口:
ServerBootstrap b = new ServerBootstrap(); b.group(bossGroup, workerGroup) .channel(NioServerSocketChannel.class) .option(ChannelOption.SO_BACKLOG, 100) // 心跳监测 .childOption(ChannelOption.SO_KEEPALIVE, true) .handler(new LoggingHandler(LogLevel.INFO)) .childHandler(new ChannelInitializer<SocketChannel>() { @Override public void initChannel(SocketChannel ch) throwsException { ch.pipeline().addLast( new EchoServerHandler()); } }); // Start the server. ChannelFuture f = b.bind(port).sync(); // Wait until the server socket is closed. f.channel().closeFuture().sync();
PS:Java程序只能作到设置SO_KEEPALIVE选项,至于TCP_KEEPCNT,TCP_KEEPIDLE,TCP_KEEPINTVL等参数配置,应用层面是无法设置的。
六、TCP KeepAlive可能致使的问题
Keepalive 技术只是TCP协议中的一个可选项。由于不当的配置可能会引发一些问题,因此默认是关闭的。
具体来讲,可能致使下列问题:
- 1)在短暂的故障期间,Keepalive设置不合理时可能会由于短暂的网络波动而断开健康的TCP链接;
- 2)须要消耗额外的宽带和流量(对于如今这个时代来讲,这貌似已经不是问题了);
- 3)在以流量计费的互联网环境中增长了费用开销。
七、TCP KeepAlive在移动网络时代的局限性
不能否认,TCP协议做为TCP/IP协议族中最重要部分,对互联的发展确实功不可没(见:《技术往事:改变世界的TCP/IP协议(珍贵多图、手机慎点)》)。
但现在移动网络时代,无线通讯愈来愈普及,做为上个世纪中期发明的TCP协议来讲,客观的讲,在某些场景下确实有先天不足(见:《5G时代已经到来,TCP/IP老矣,尚能饭否?》)。
那么,又回到了本文开头的问题——“既然TCP协议自己有KeepAlive,为何还要自已在应用层实现网络保活/心跳机制?”。
以移动端IM应用为例:
- 1)一方面,运营商ISP的网络资源更为稀缺,TCP协议默认2小时的KeepAlive基本不可能实现IM长链接“保活”(为了提高无线网络资源的利用率,运营商长则几分钟,短则数十秒就有可能回收空闲的网络链接)。
- 2)另外一面,无线网络自己存在弱网问题,即便TCP链接是“好的”,但实际上处于“假死”状态,也没法起到长链接该有的做用。
因此说,IM应用层自已作网络保活(心跳机制)是不可避免的。
有关这方面的更多资料,有兴趣,能够深刻阅读下面这几篇:
《移动端IM开发者必读(一):通俗易懂,理解移动网络的“弱”和“慢”》
《移动端IM开发者必读(二):史上最全移动弱网络优化方法总结》
八、知识拓展:TCP Keepalive和HTTP Keep-Alive有什么区别?
不少人会把TCP Keepalive 和 HTTP Keep-Alive 这两个概念搞混淆。
这里简单介绍下HTTP Keep-Alive 。
在HTTP/1.0中,默认使用的是短链接。也就是说,浏览器和服务器每进行一次HTTP操做,就创建一次链接,但任务结束就中断链接。若是客户端浏览器访问的某个HTML或其余类型的 Web页中包含有其余的Web资源,如JavaScript文件、图像文件、CSS文件等;当浏览器每遇到这样一个Web资源,就会创建一个HTTP会话。
但从 HTTP/1.1起,默认使用长链接,用以保持链接特性。使用长链接的HTTP协议,会在响应头加上Connection、Keep-Alive字段。
以下图所示:

HTTP 1.0 和 1.1 在 TCP链接使用方面的差别以下图所示:

通俗地总结一下:
- 1)HTTP的Keep-Alive是为了让TCP链接活得更久一点,在发起多个http请求时能复用同一个链接,提升通讯效率;
- 2)TCP的KeepAlive机制意图在于探测链接的对端是否存活,是一种检测TCP链接情况的保鲜机制。
九、参考资料
[2] TCP协议的KeepAlive机制与HeartBeat心跳包
[3] HTTP keep-alive和TCP keepalive的区别,你了解吗?
[4] TCP KeepAlive 与 HTTP Keep-Alive 区别
[6] TCP keepalive的探究 (1) : NAT和保活机制
[8] 为什么基于TCP协议的移动端IM仍然须要心跳保活机制?
本文已同步发布于“即时通信技术圈”公众号。
▲ 本文在公众号上的连接是:点此进入。同步发布连接是:http://www.52im.net/thread-3506-1-1.html