文中援用了参考资料中的局部内容,本文参考资料详见文末“参考资料”一节,感激材料分享者。
1、引言
对于IM开发者而言,网络保活这件事再相熟不过了,比方这是我最近一篇无关网络保活话题文章《一文读懂即时通讯利用中的网络心跳包机制:作用、原理、实现思路等》,以及我分享的大量代码实战编码中也都必须要思考这个问题的实现,比方最近的这篇《跟着源码学IM(五):正确理解IM长连贯、心跳及重连机制,并入手实现》。
对于IM这种利用而言,应用层的网络保活的最间接方法就是心跳机制,比方支流的IM里有微信、QQ、钉钉、易信等等,可能代码实现细节有所差别,但实践上无一例外都是这样实现。(PS:没错,当初微信跟运营商间的“信令危机”就是跟这个无关)
所谓的网络心跳,通常是客户端每隔一小段时间向服务器发送一个数据包(即心跳包),告诉服务器本人依然在线(心跳包中同时可能传输一些必要的数据)。发送心跳包,从通信层面来说就是为了放弃长连贯,至于这个包的内容,是没有什么特地规定的,但在挪动端IM中为了省流量,个别都是很小的包(比方某些第3方的IM云为了阐明心跳不费流量,号称1字节的心跳包)。
但常常有人会问到,既然TCP协定自身有KeepAlive保活这个货色(见:《TCP/IP详解 卷1 - 第23章·TCP的保活定时器》),为什么还要自已在应用层去实现网络保活/心跳机制呢?
没错,通常面视即时通讯/IM方面的程序员时,这简直是必提问题!
要解答这个问题,我通常倡议看看《为什么说基于TCP的挪动端IM依然须要心跳保活?》这篇。但限于篇幅,该篇并没有深入探讨TCP协定自身的KeepAlive机制,所以这次借本文想把TCP协定的KeepAlive保活机制给具体的整理出来,以便大家能深刻其中一窥到底。
学习交换:
- 即时通讯/推送技术开发交换5群:215477170 [举荐]
- 挪动端IM开发入门文章:《新手入门一篇就够:从零开发挪动端IM》
- 开源IM框架源码:https://github.com/JackJiang2...
(本文已同步公布于:http://www.52im.net/thread-35...
2、系列文章
本文是系列文章中的第12篇,本系列文章的纲要如下:
《鲜为人知的网络编程(一):浅析TCP协定中的疑难杂症(上篇)》
《鲜为人知的网络编程(二):浅析TCP协定中的疑难杂症(下篇)》
《鲜为人知的网络编程(三):敞开TCP连贯时为什么会TIME_WAIT、CLOSE_WAIT》
《鲜为人知的网络编程(四):深入研究剖析TCP的异样敞开》
《鲜为人知的网络编程(五):UDP的连接性和负载平衡》
《鲜为人知的网络编程(六):深刻地了解UDP协定并用好它》
《鲜为人知的网络编程(七):如何让不牢靠的UDP变的牢靠?》
《鲜为人知的网络编程(八):从数据传输层深度解密HTTP》
《鲜为人知的网络编程(九):实践联系实际,全方位深刻了解DNS》
《鲜为人知的网络编程(十):深刻操作系统,从内核了解网络包的接管过程(Linux篇)》
《鲜为人知的网络编程(十一):从底层动手,深度剖析TCP连贯耗时的机密》
《鲜为人知的网络编程(十二):彻底搞懂TCP协定层的KeepAlive保活机制》(* 本文)
3、TCP KeepAlive的初衷
采纳TCP连贯的C/S模式利用中,当连贯的单方在连贯闲暇状态时,如果任意一方意外解体、当机、网线断开或路由器故障,另一方无奈得悉TCP连贯曾经生效。
那么,连贯的另一方并不知道对端的状况,它会始终保护这个连贯。而作为“服务端”来说,长时间的积攒会导致十分多的半关上连贯,造成端系统资源的耗费和节约,且有可能导致在一个有效的数据链路层面发送业务数据,后果就是发送失败。
所以各端要做到疾速感知失败,缩小有效链接操作,这就有了TCP的KeepAlive保活探测机制。
PS:这样宽泛的说TCP的KeepAlive机制的必要性,貌似还不是很有说服力,下节将带着具体的例子深入分析。
4、从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保活机制的必要性了。
5、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等参数配置,利用层面是没法设置的。
6、TCP KeepAlive可能导致的问题
Keepalive 技术只是TCP协定中的一个可选项。因为不当的配置可能会引起一些问题,所以默认是敞开的。
具体来说,可能导致下列问题:
1)在短暂的故障期间,Keepalive设置不合理时可能会因为短暂的网络稳定而断开衰弱的TCP连贯;
2)须要耗费额定的宽带和流量(对于当初这个时代来说,这貌似曾经不是问题了);
3)在以流量计费的互联网环境中减少了费用开销。
7、TCP KeepAlive在挪动网络时代的局限性
不可否认,TCP协定作为TCP/IP协定族中最重要局部,对互联的倒退的确功不可没(见:《技术往事:扭转世界的TCP/IP协定(宝贵多图、手机慎点)》)。
但现在挪动网络时代,无线通信越来越遍及,作为上个世纪中期创造的TCP协定来说,主观的讲,在某些场景下的确有先天不足(见:《5G时代曾经到来,TCP/IP老矣,尚能饭否?》)。
那么,又回到了本文结尾的问题——“既然TCP协定自身有KeepAlive,为什么还要自已在应用层实现网络保活/心跳机制?”。
以挪动端IM利用为例:
1)一方面,运营商ISP的网络资源更为稀缺,TCP协定默认2小时的KeepAlive根本不可能实现IM长连贯“保活”(为了晋升无线网络资源的利用率,运营商长则几分钟,短则数十秒就有可能回收闲暇的网络连接)。
2)另一面,无线网络自身存在弱网问题,即便TCP连贯是“好的”,但实际上处于“假死”状态,也无奈起到长连贯该有的作用。
所以说,IM应用层自已做网络保活(心跳机制)是不可避免的。
无关这方面的更多材料,有趣味,能够深刻浏览上面这几篇:
《为何基于TCP协定的挪动端IM依然须要心跳保活机制?》
《挪动端IM开发者必读(一):通俗易懂,了解挪动网络的“弱”和“慢”》
《挪动端IM开发者必读(二):史上最全挪动弱网络优化办法总结》
《IM开发者的零根底通信技术入门(十三):为什么手机信号差?一文即懂!》
《IM开发者的零根底通信技术入门(十四):高铁上无线上网有多难?一文即懂!》
8、常识拓展: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连贯情况的保鲜机制。
9、参考资料
[1] TCP保活(TCP keepalive)
[2] TCP协定的KeepAlive机制与HeartBeat心跳包
[3] HTTP keep-alive和TCP keepalive的区别,你理解吗?
[4] TCP KeepAlive 与 HTTP Keep-Alive 区别
[5] tcp连贯探测Keepalive和心跳包
[6] TCP keepalive的探索 (1) : NAT和保活机制
[7] 了解TCP长连贯(Keepalive)
[8] 为何基于TCP协定的挪动端IM依然须要心跳保活机制?
[9] 挪动端IM开发者必读(二):史上最全挪动弱网络优化办法总结
[10] IM开发者的零根底通信技术入门(十三):为什么手机信号差?一文即懂!
附录:更多网络编程精髓文章
[1] 网络编程(根底)材料:
《网络编程懒人入门(一):疾速了解网络通信协定(上篇)》
《网络编程懒人入门(二):疾速了解网络通信协定(下篇)》
《网络编程懒人入门(三):疾速了解TCP协定一篇就够》
《网络编程懒人入门(四):疾速了解TCP和UDP的差别》
《网络编程懒人入门(五):疾速了解为什么说UDP有时比TCP更有劣势》
《网络编程懒人入门(六):史上最艰深的集线器、交换机、路由器性能原理入门》
《网络编程懒人入门(七):深入浅出,全面了解HTTP协定》
《网络编程懒人入门(八):手把手教你写基于TCP的Socket长连贯》
《网络编程懒人入门(九):艰深解说,有了IP地址,为何还要用MAC地址?》
《网络编程懒人入门(十):一泡尿的工夫,疾速读懂QUIC协定》
《网络编程懒人入门(十一):一文读懂什么是IPv6》
《网络编程懒人入门(十二):疾速读懂Http/3协定,一篇就够!》
《脑残式网络编程入门(一):跟着动画来学TCP三次握手和四次挥手》
《脑残式网络编程入门(二):咱们在读写Socket时,到底在读写什么?》
《脑残式网络编程入门(三):HTTP协定必知必会的一些常识》
《脑残式网络编程入门(四):疾速了解HTTP/2的服务器推送(Server Push)》
《脑残式网络编程入门(五):每天都在用的Ping命令,它到底是什么?》
《脑残式网络编程入门(六):什么是公网IP和内网IP?NAT转换又是什么鬼?》
《脑残式网络编程入门(七):面视必备,史上最艰深计算机网络分层详解》
《脑残式网络编程入门(八):你真的理解127.0.0.1和0.0.0.0的区别?》
《脑残式网络编程入门(九):面试必考,史上最艰深大小端字节序详解》
《网络编程入门从未如此简略(一):如果你来设计网络,会怎么做?》
《网络编程入门从未如此简略(二):如果你来设计TCP协定,会怎么做?》
[2] 网络编程(高阶)材料:
《高性能网络编程(一):单台服务器并发TCP连接数到底能够有多少》
《高性能网络编程(二):上一个10年,驰名的C10K并发连贯问题》
《高性能网络编程(三):下一个10年,是时候思考C10M并发问题了》
《高性能网络编程(四):从C10K到C10M高性能网络应用的实践摸索》
《高性能网络编程(五):一文读懂高性能网络编程中的I/O模型》
《高性能网络编程(六):一文读懂高性能网络编程中的线程模型》
《高性能网络编程(七):到底什么是高并发?一文即懂!》
《鲜为人知的网络编程(一):浅析TCP协定中的疑难杂症(上篇)》
《鲜为人知的网络编程(二):浅析TCP协定中的疑难杂症(下篇)》
《鲜为人知的网络编程(三):敞开TCP连贯时为什么会TIME_WAIT、CLOSE_WAIT》
《鲜为人知的网络编程(四):深入研究剖析TCP的异样敞开》
《鲜为人知的网络编程(五):UDP的连接性和负载平衡》
《鲜为人知的网络编程(六):深刻地了解UDP协定并用好它》
《鲜为人知的网络编程(七):如何让不牢靠的UDP变的牢靠?》
《鲜为人知的网络编程(八):从数据传输层深度解密HTTP》
《鲜为人知的网络编程(九):实践联系实际,全方位深刻了解DNS》
《鲜为人知的网络编程(十):深刻操作系统,从内核了解网络包的接管过程(Linux篇)》
《鲜为人知的网络编程(十一):从底层动手,深度剖析TCP连贯耗时的机密》
《鲜为人知的网络编程(十二):彻底搞懂TCP协定层的KeepAlive保活机制》
《IM开发者的零根底通信技术入门(十一):为什么WiFi信号差?一文即懂!》
《IM开发者的零根底通信技术入门(十二):上网卡顿?网络掉线?一文即懂!》
《IM开发者的零根底通信技术入门(十三):为什么手机信号差?一文即懂!》
《IM开发者的零根底通信技术入门(十四):高铁上无线上网有多难?一文即懂!》
《IM开发者的零根底通信技术入门(十五):了解定位技术,一篇就够》
本文已同步公布于“即时通讯技术圈”公众号。
▲ 本文在公众号上的链接是:点此进入。同步公布链接是:http://www.52im.net/thread-35...