共计 9326 个字符,预计需要花费 24 分钟才能阅读完成。
文中援用了参考资料中的局部内容,本文参考资料详见文末“参考资料”一节,感激材料分享者。
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…