万字图解 | 深入揭秘 TCP 工作原理
大家好,我是「云舒编程」,今天我们来聊聊计算机网络面试之-(传输层 tcp)工作原理。
文章首发于微信公众号:云舒编程
关注公众号获取:1、大厂项目分享 2、各种技术原理分享 3、部门内推
前言
想必不少同学在面试过程中,会遇到「在浏览器中输入 www.baidu.com 后,到网页显示,其间发生了什么」类似的面试题。
本专栏将从该背景出发,详细介绍数据包从 HTTP 层->TCP 层->IP 层->网卡->互联网->目的地服务器 这中间涉及的知识。
本系列文章将采用自底向上的形式讲解每层的工作原理和数据在该层的处理方式。
系列文章
通过上一篇文章每天5分钟玩转计算机网络-(网络层ip)工作原理的介绍,我们知道了网络层的基本工作原理。
本篇将会详解对传输层(tcp)进行介绍。
通过本文你可学到:
什么是面向连接的协议
TCP 协议格式构成
TCP 为什么是可靠的
三次握手、四次挥手
TCP 重传机制
TCP 滑动窗口
TCP 拥塞控制
TCP 连接队列管理
TCP11 种状态转换
TCP KeepAlive 原理
TCP 是什么
tcp 是工作在传输层,也就是网络层上一层的协议。
它是面向连接的,可靠的,基于字节流、全双工的通信协议。
TCP 收到上一层的数据包后,会加上 TCP 头并且进行一些特殊处理后,再传递给网络层
什么是面向连接、无连接?
面向连接:面向连接的协议要求发送数据前需要通过一种手段保证通信双方都准备好了,之后才进行通信。
无连接:无连接的协议则不需要,想发就发
什么是全双工
全双工(Full Duplex)是一种通信方式,指通信的双方可以同时发送和接收数据,而不需要像半双工那样在发送和接收之间切换。在全双工通信中,数据可以在两个方向上同时传输,因此通信速度更快,效率更高。
TCP 协议格式构成
源端口:占 2 字节,标识数据包是哪个应用发出去的。
目的端口:占 2 字节,标识数据包是发给哪个应用的。
序号: 占 4 字节,TCP 连接中传送的数据流中的每一个字节都编上一个序号.序号字段的值则指的是本报文段所发送的数据的第一个字节的序号。
确认号:占 4 字节,是期望收到对方的下一个报文段的数据的第一个字节的序号
数据偏移(首部长度): 占 4 位,它指出 TCP 头部实际长度。
在 TCP 协议中,TCP 头部的长度是可变的,最小长度为 20 个字节,最大长度为 60 个字节。这是因为 TCP 头部中有一些可选字段,如 TCP 选项、窗口缩放因子等,这些字段的长度是可变的,因此 TCP 头部的长度也会随之变化。TCP 头部长度是通过 TCP 头部中的 数据偏移(首部长度)字段来指定的,它表示 TCP 头部的长度以 32 位字为单位计算的值。因此,TCP 头部长度实际上是 数据偏移(首部长度)字段值乘以 4。
状态位,占 6 比特:
ACK:该位为 1 时,「确认号」的字段变为有效,否则无效。
RST:该位为 1 时,表示 TCP 连接中出现异常必须强制断开连接,然后重新建立新链接。
SYN:该位为 1 时,表示希望建立连接,并在其「序号」的字段进行序列号初始值的设定。
FIN:该位为 1 时,表示数据发送传输完毕,希望断开连接。
窗口:占 2 字节,用于流量控制,通信双方各声明一个窗口,标识自己当前的处理能力。控制报文别发太快,也别发太慢。
检验和: 占 2 字节,校验数据是否完整未更改。
填充: 为了使整个首部长度是 4 字节的整数倍。
TCP 怎么保证可靠
三次握手
TCP 连接的建立,常常被称为三次握手。
三次握手过程如下图:
close:刚开始 client 和 server 都处于 close 状态
listen:server 某个进程主动监听某个端口,进入 listen 状态
syn_sent:当 server 进入 listen 状态后,client 就可以发起连接请求了。发送如下请求报文头:然后 client 进入 syn_sent 状态
syn_rcvd:server 收到 client 的请求,并且响应该请求,发送如下报文头后:进入 syn_rcvd 状态。
由于 TCP 是全双工,所以 server 也会向 client 请求建立 server 到 client 的连接,于是也会发送 SYN 和 seq
established(client):client 收到 server 的响应后,进入 established 状态,并且发送响应报文给 server,报文如下:
established(server):server 收到 client 的响应后,进入 established 状态。
一次 wireshark 抓包三次握手过程如下:
可以看到过程跟上述描述一模一样
数据分片和排序
通过前面的文章每天5分钟玩转计算机网络-(网络层ip)工作原理,我们知道 IP 层对于大于 MTU 的数据会进行分包,然后才会在网络上进行传输。
同样的,TCP 对于上层传递过来的数据也会进行分包处理,当包的大小大于 MSS 时 TCP 会对包进行拆分后才会传递给 IP 层。
这里可能有同学会有疑问了:
为什么 IP 层已经进行分包了,TCP 层还要进行一次呢?这不是多此一举吗?
还真不是多此一举,考虑下面一个传输场景:
可以发现,即使只丢失了分片二,但是 TCP 不得不重传整个报文,这是因为 IP 层不具备丢失重传能力,这样的设计造成了极大的资源浪费。
为了避免该问题,于是 TCP 的设计者们,就希望当只有某一分片丢失时,TCP 可以知道是哪个分片,这样就可以只重传该分片即可,极大的节约了资源。
于是就提出了 MSS 的概念。
MSS
TCP MSS,全称为 Maximum Segment Size,是指 TCP 协议中的最大数据段大小。
它是 TCP 传输过程中数据段的最大长度,一般除去 IP 和 TCP 头部之后,一个网络包所能容纳的 TCP 数据的最大长度就是 MSS
有了 MSS 以后,再看下上面的问题怎么解决:
通过 MSS 可以避免包被 IP 层分割,TCP 就可以完整掌握包的生命周期。
排序
正如前面提到的,每个 TCP 报文都有一个序列号(seq),他们是严格有序的,TCP 收到数据后会进行如下处理:
根据“源 IP,目的 IP,源端口,目的端口,协议”定位到一个 socket
数据缓存到 socket 接收队列中
按照序号进行排序重组
校验数据完整性
等待上层调用 recv 函数接收数据
重传机制
通过前面的描述,我们知道 TCP 每收到一个包,都会通过确认号(ack)告诉发送方。
如果发送方发现超时了,还没有收到对方的确认号,就会认为包丢失了,对该包进行重传,通过这样的形式可以保证数据一定送达了。
在谈重传机制之前,我们先说下确认号(ack)的确认机制:
TCP 使用确认号(ack)来告知对方下一个期望接收的序列号,表示小于此确认号的所有字节都已经收到
TCP 采用的是连续确认机制,例如上图,一共发送了 9,10,,11,,12,,13 共 5 个包,只有 5 个包都收到了才会回 ack=14,如果收到了 9,10,12,13,丢失了 11,那么 TCP server 是不会回 ack=14 的,不然客户端会以为小于 14 的包都收到了。
那遇到这种情况怎么处理呢?
超时重传
一直不回 ack=12,等待 client 发现包 11 超时了,然后重发包 11,一旦 server 收到包 11 后就好回 ack=14。
这种机制优点就是简单,但是缺点也很明显:
超时时间不好定
定短了:可能包还在网络上传输,发送方就超时重传了,重复发包,导致网络拥挤
定长了:包丢失了,半天才发送,效率低下
包连续超时:由于死等包 11,即使收到了 12,13 也无法告诉发送方自己收到了。这样可能会导致 12,13 也出现超时未响应 ack,导致发送方也重发 12,13
RTT 算法
从前面的 TCP 重传机制我们知道超时的设置对于重传非常重要。
设长了,重发就慢,效率低,性能差;
设短了,数据包并没有丢就重传了,导致数据重复,网络拥塞,更加容易丢包。
并且由于网络是动态变化的,RTT 也不能定一个固定值,必须动态的去设置。于是 TCP 引入了 RTT 算法,表示一个数据包从发出去到收到响应的时间。这样发送方就可以灵活的设置超时时间(RTO)了。
RFC6298 建议使用以下的公式计算 RTO(这里忽略了 RTT 算法的演进史,有兴趣的同学可以自行查阅):
快速重传
为了解决超时重传的问题,TCP 引入了快速重传机制。
快速重传不以时间为驱动,而是以数据驱动重传。
如图:发送方发出了 9,10,11,12,13 共 5 份数据:
seq9 先到了,于是 ack 回 10;
seq10 丢失;
seq11 到达了,于是 ack 还是回 10;
seq12 到达了,于是 ack 还是回 10;
发送端收到了三个 ack = 10 的包,于是知道了包 10 丢失了,就会在定时器过期之前,重传 seq=10。
最后,收到了丢失的 seq=10,此时因为 seq11,12,13 都收到了,于是回 ack=14。
快速重传解决了超时效率的问题,但是他依旧有缺点:
假设上图丢失的包是 9 和 10,那么在收到包 11,12,13 后,会连续返回三个 ack=9,于是重传包 9,然后又要连续返回三个 ack=10 再重传包 10。
当然也可以选择收到三个 ack=9 后,就把 9 以后的报文全部重传,但是这样包 11,12,13 就重复了
选择性确认-SACK
为了解决上述问题,于是又引入了 SACK(选择性确认),通过在 TCP 头部【选项】字段中加一个叫 SACK 的玩意,告诉发送方接收方已经收到哪些数据了,这样发送方就有了上帝视角,知道哪些数据丢失了,可以精确重传这些数据。
由于 TCP 头部长度有限制,所以一个报文最多可以容纳 4 组 SACK 信息每个 SACK 信息包含一个区间,代表接收端存储的失序数据的起始至最后一个序列号(加 1)
收到三次相同 ack 后,发送端根据 sack 信息就可以选择性的重传丢失报文了
滑动窗口
1、TCP 为了实现可靠性,要求对于发出去的每一个包都必须得到确认,否则会进行重传,所以需要有个地方存储已经发送了,但是还未确认的数据。
2、服务器的处理能力是有限的,类似人一样,有的人一次可以吃一个馒头,有的可以一次吃两个。那么就需要发送方根据服务器方的处理能力来控制发送数据的速率,避免把服务器方“撑死”。
为了解决上诉问题,于是引入滑动窗口。
发送方滑动窗口
应用会把要发送的数据放入 TCP 发送缓冲区,接收到的数据放入接收缓冲区。
而滑动窗口就是工作在缓冲区的,它把缓冲区的数据分为四部分:
1:已发送,并且已收到 ack 确认的数据
2:已发送,但是还未收到 ack 确认,如果超时还未收到 ack 就会重发这部分数据。
3:未发送,但是服务端还有剩余空间可以接受这部分数据
4:未发送,并且服务端没有剩余空间可以接受这部分数据
假设发送方把 3 窗口中的数据全部发送出去,那么可用窗口就会变为 0:
这种情况下,在未收到接收方 ack 前,发送方将无法再发送新的数据。
其中黑色框就是滑动窗口。
假设服务端返回了 ack=37,代表 32~36 共 5 个包已经收到,那么黑框就会向前移动 5 个包的位置:
接收方滑动窗口
重点
发送方的滑动窗口大小(也就是黑框)是由接收方决定并且告知的。
例如下图:
接收方就是告诉发送方,我的窗口大小是 17920,你最多可以发这么多数据过来,再多我就处理不过来了。
发送方看到 win 后,就会把自己的发送滑动窗口设置为 17920,按照该值限制数据发送。
拥塞控制
前面我们已经了解了滑动窗口,他可以避免【发送方】把 【接收方】填满打垮,但是对于网络来说,除了接收方发送方,中间经过的设备,光纤资源也是有限的。
他们的作用就类似于我们生活中的高速路和服务区。如同高速路人多了就会堵车一样,在网络上如果发包太快太多,依旧会发生堵塞。
TCP 并不只满足于控制【发送方】和【接收方】,它还希望可以控制整个网络,在网络“堵车”的时候减少数据包传输,网络“畅通”的时候加快数据包传输。
核心思想:刚开始的时候,只发一点数据,如果可以正常接收到,那就多发一点,如果还能收到,那就继续多发,以此类推。
而拥塞控制就是做这件事的,拥塞控制主要由以下几个算法组成
慢启动
拥塞避免
拥塞发送
快速恢复
提到拥塞控制不得不提拥塞窗口
拥塞窗口
拥塞窗口:在收到对端 ACK 前自己还能传输的最大数据包数可以通过 ss -nil | grep cwnd 查看 cwnd 初始大小
与接收窗口的区别?
接收窗口是对接收端的限制,表示接收端还能接收的数据量大小
拥塞窗口是对发送端的限制,表示发送端还能发送的数据量大小
与发送窗口的区别?
发送窗口是接收端告诉发送端的
拥塞窗口是发送方自己根据网络状态、操作系统设置初始化的
实际的发送窗口 = min(接收端告知的发送窗口大小,拥塞窗口大小)
如果接收端告知的发送窗口比拥塞窗口小,表示接收端处理能力不够。如果拥塞窗口小于接收端告知的发送窗口,表示接收端处理能力 ok,但网络拥塞。
拥塞控制的算法的本质是控制拥塞窗口大小的变化。
慢启动
在连接刚建立的时候,发送方还不了解网络上的情况,所以慢启动的策略是一点一点的增加发送数据包的数量。
具体步骤:
连接建立后,先初始化拥塞窗口(cwnd)=1
每当收到一个 ACK,cwnd++,那么每次 RTT 过后,cwnd = cwnd * 2
当 cwnd > ssthresh(上限值)后,就会进入拥塞避免
刚开始 cwnd=1,可以发送 1 个数据包,过了一段时间,收到 ACK,这个时候 cwnd+1 = 2。就可以发送 2 个数据包了。等收到这两个数据包的 ACK,cwnd+2=4,就可以发送 4 个数据包了。以此类推,可以发现慢启动阶段,拥塞窗口成倍增长。
Linux 3.0 后采用了 Google 的论文《An Argument for Increasing TCP’s Initial Congestion Window》,把 cwnd 初始化成了 10 个 MSS
拥塞避免
当 cwnd > ssthresh 时,拥塞窗口进入「拥塞避免」阶段,每经过一个 RTT,拥塞窗口大约增加 1 个 MSS 大小,直到检测到拥塞为止。
这里可以看到慢启动阶段每经过一个 RTT 是翻倍,但是拥塞避免阶段一个 RTT,窗口只增加 1。
也就是进入了线性增长,降低窗口的增长速度,进一步限制发送方发包的数量,从而避免形成网络拥塞。
但是即使是这样,只要窗口一直在增加,那么最终肯定还是会导致拥塞发生。
拥塞发生
当发生丢包的时候,就证明可能发生网络拥塞了,就会进入拥塞发生阶段。
而发现丢包主要依赖于两种手段:
超时了,还没收到 ACK;
收到三个重复 ACK;
TCP 关于拥塞发生的处理有很多实现算法,下面我们主要介绍几种常见的:
可以通过 cat /proc/sys/net/ipv4/tcp_congestion_control 查询本机使用的拥塞控制算法
TCP Tahoe
TCP Reno/NewReno
TCP Cubic
TCP Tahoe
ACK 超时或者收到三个重复 ACK 时,执行以下操作:
sshthresh = cwnd /2,也就是设置为当前窗口的一半;
cwnd 重置为初始值(如果你是 linux3.0+,那么是 10);
进入慢启动过程。
可以看到这是一种激进的做法,辛辛苦苦增加窗口大小,但是一夜回到解放前。并且容易造成网络抖动,影响应用程序。
TCP Reno
ACK 超时
同 TCP Tahoe
收到三个重复 ACK
sshthresh = cwnd /2,也就是设置为当前窗口的一半;
cwnd = sshthresh (有些实现是 sshthresh+3);
重传丢失的数据段;
再收到重复的 ACK 时,拥塞窗口增加 1。
收到新的 ACK,把 cwnd 设置为第一步中的 ssthresh 的值
进入拥塞避免阶段。
TCP Reno 对收到重复 ACK 的场景进行了优化,TCP 设计者认为既然可以收到三个 ACK,证明网络没有那么拥塞,就不必像超时重传那么激进的做法,不采用 cwnd 置为初始值,而是根据当前值减半,并且 sshthresh 也等于当前窗口减半,那么就会立即进入拥塞避免阶段。如果网络没有那么糟糕,那么 TCP 还能维持一定的发送速率,并且缓慢上涨。如果仍然超时,再进入 ACK 超时算法阶段。
TCP NewReno
TCP Reno 对三个重复 ACK 进行了优化,但是依旧存在问题:
当多个包在一个拥塞窗口丢失时,TCP Reno 会重复减少拥塞窗口的大小。例如:连续丢失了 3 号,4 号包。
刚开始收到关于 3 号包的重复 ACK,窗口减半并且重发 3 号包,然后 3 号包达到了。又会重复收到 4 号包的重复 ACK,这个时候窗口再次减半。但是对于这样的场景,其实拥塞窗口减半一次足以恢复重传已经丢失的数据包。
于是 TCP NewReno 提出:对于同一窗口中的多次数据包丢失,希望减少一次窗口即可。
具体原理如下:
当发生端这边收到了 3 个重复 ACK 后,进入快速恢复模式,重传丢失数据包。
如果只丢了一个包,那么接收端回传回来的 ack 会把滑动窗口中待确认的数据都确认,这个时候会退出快速回复。
反之,那么发送端就可以知道有多个包被丢了,于是继续重传滑动窗口中里未被 ack 的第一个包。直到发生情况一,才真正结束快速恢复这个过程
TCP BIC
随着时间的发展,网络带宽越来越大,可以容纳的数据量也越来越多。上面的算法就面临一个问题:进入拥塞避免状态或快速恢复状态后,每经过一个 RTT 才会将窗口大小加 1。当网络带宽很大,又只是偶尔丢包时,就会导致需要很长时间才能达到最佳拥塞窗口大小,对资源的利用就很低。
其实任何拥塞算法都是想找到一个最佳的拥塞窗口大小。
BIC 认为:当产生丢包时,当前最佳拥塞窗口肯定是小于丢包时的拥塞窗口的,记为 Wmax。同时定义一个乘法缩小因子β,令 Vmin=β*Vmax,那么很明显当前的最佳窗口 w,Vmin < w < Vmax。之前的算法都是采用加法去找,BIC 采用二分的形式去搜索,将当前窗口设置为(Vmin + Vmax) /2。
这样当窗口远离 Vmax 时增长快,靠近 Vmax 增长慢。
四次挥手
当 client 和 server 数据传输完成后,就需要释放连接,TCP 通过四次挥手释放连接。
client 调用 close 函数后,会给 server 发送 FIN 报文,然后进入 fin_wait_1 状态;
server 收到 FIN 报文后,会给 client 返回一个 ACK 报文,然后 sever 进入 closed_wait 状态;
client 收到 server 返回的 ACK 后,进入 fin_wait_2 状态;
server 数据发送完毕,也调用 close 函数,这个时候就会向 client 发送 FIN 报文,然后 server 进入 last_ack 状态;
client 收到 sever 的 FIN 报文后,回复 ACK 并且进入 time_wait 状态;
server 收到 ACK 后进入 close 状态,至此 serve 端完成了链接关闭;
client 等待 2MSL 后,也进入 close 状态,完成 client 端关闭;
为什么 client 最后要等到 2MSL 才进入 close 状态?
MSL (Maximum Segment Lifetime),报文最大生存时间,超过这个时间报文将被丢弃。
在 macOS 可以通过 sysctl net.inet.tcp.msl 查询,该值/2 等于 MSL
在 Linux 上可以通过 sysctl net.ipv4.tcp_fin_timeout 查询,同上
这个问题其实可以拆成两个问题:
1、为什么需要 time_wait?
2、time_wait 的时间为什么是 2MSL?
问题一:为什么需要 time_wait?
前面我们有提到 TCP 是可靠协议,他既要保证数据的可靠传输,也要保证连接的可靠关闭。
我们先假设没有 time_wait,client 收到 server 的 FIN 后,发送 ACK 响应,代表收到了 FIN。但是 client 怎么知道自己发送的 ACK 被 server 收到了呢?只能 sever 收到 ACK,再返回一个 ACK 表示收到了 client 的 ACK,client 又要响应 ACK 表示收到了 ACK,就这样反反复复,形成了死循环。
为了解决这个问题,TCP 设计者们就提出,采取一个默认规则吧:
client 发送 ACK 后,等待一段时间,如果双方都没有数据传递了,那就可以断开连接了。所以就引入了 time_wait。
问题二:time_wait 为什么是 2MSL?
情况一:
server 第一次发送 FIN 到 client,client 响应 ACK,但是 ACK 快到 server 端时丢失了。server 端等待响应超时后,重传 FIN,client 收到新 FIN 后再次响应 ACK,这次 server 成功收到 ACK,于是关闭了连接。
也就是说为了兼容响应 ACK 丢失的情况,client 需要等待一段时间,保证当 server 重传 FIN 时,他也可以处理。
那么考虑极端情况,ACK 快到 server 才丢失,FIN 从 server 重传(也就是图中红线部分),一来一回两个报文,加起来的时间刚好是 2MSL。
情况二:
假设在极端情况下,如上图,旧连接的请求包,在新连接中又达到了,刚好序列号又在接受范围内,那就产生了脏数据。
为了避免这样的情况,在断开连接时,需要保证旧连接的报文全部消亡。前面我们说过,报文的最大生命周期是 MSL,也就是为了保证旧报文消亡,time_wait 至少需要 MSL。
那么综合情况一和二,也就是 time_wait 至少需要 2MSL。
TCP 半连接队列和全连接队列?
在 TCP 三次握手的时候,Linux 内核会维护两个队列,分别是:
半连接队列,也称 SYN 队列;
全连接队列,也称 accept 队列;
半连接队列
当 client 发送 SYN 到 server,server 收到后回复 ACK 和 SYN。同时会将这个连接信息放入【半连接队列】,同时 server 会开启一个定时器,如果超时还未收到 client 的 ACK 就会重传 SYN+ACK ,重传的次数由 tcp_synack_retries 值确定。
可以通过:sysctl net.ipv4.tcp_synack_retries 查询重传值,一般是 5
一旦收到客户端的 ACK,服务端就会把该连接加入另外一个全连接队列。
SYN 泛洪攻击
从上面的描述可以看出,当 client 发送 SYN 并且服务端响应后就会把连接放到【半连接队列】,并且等待 client 的 ACK。如果 client 构造了大量的请求,但是都不回复最后一个 ACK,那么就会有大量的半连接信息占据【半连接队列】,把服务器的资源耗尽,无法响应正常连接请求,这就是 SYN 泛洪攻击。
模拟 SYN 泛洪攻击
我们可以通过两种方式模拟 SYN 泛洪攻击:
1、hping
hping 是用于生成和解析 TCPIP 协议数据包的开源工具。创作者是 Salvatore Sanfilippo。目前最新版是 hping3,支持使用 tcl 脚本自动化地调用其 API。hping 是安全审计、防火墙测试等工作的标配工具
2、iptables
iptables 可以过滤发给主机的网络包,我们通过 iptables 增加规则,丢弃服务端响应的 SYN+ACK 报文,这样客户端就不会发送 ACK 报文,达到模拟 SYN 攻击的目的
全连接队列
所有完成了三次握手,但是还未被应用调用 accept 函数取走的连接都会被存放在【全连接队列】。
当应用调用 accept 函数后,内核就会移除队列头的连接返回给应用,如果没有可用的连接的话,就会阻塞。
查询全连接队列大小
可以使用 ss 命令,来查看 TCP 全连接队列的情况:
注意:
ss 输出的结果 Recv-Q 和 Send-Q 在【LISTEN 状态】和【非 LISTEN 状态】的含义是不一样的
1、LISTEN 状态
Recv-Q:已完成三次握手,但是还未被应用取走的 TCP 连接;
Send-Q:全连接队列的长度。
2、非 LISTEN 状态时
Recv-Q:已收到但未被应用进程读取的字节数;
Send-Q:已发送但未收到确认的字节数;
TCP11 种状态转换
1、CLOSED
TCP 连接还未开始建立或者连接已经释放的状态。
CLOSED -> LISTEN:server 主动监听一个特定的端口,等待 client 的新连接。
CLOSED -> SYN_SENT:client 主动发送一个 SYN 包准备三次握手。
2、LISTEN
server 主动监听一个特定的端口,等待客户端的连接。
3、SYN_RCVD
server 收到 client 的 SYN 请求,并且回复 SYN+ACK 响应该请求后进入 SYN-RCVD 状态。
4、SYN-SENT
client 发送 SYN 报文并且等待 server 回复 ACK 时进入 SYN-SENT 状态。
5、ESTABLISHED
处于 SYN-SENT 的 client 收到 server 的确认 ACK 后,进入 ESTABLISHED 状态
处于 SYN-RCVD 的 server 收到 client 的 ACK 后,进入 ESTABLISHED 状态
6、FIN-WAIT-1
主动关闭的一方(可以是 client 也可以是 server)发送了 FIN 包,等待对端回复 ACK 时进入 FIN-WAIT-1 状态。
7、FIN-WAIT-2
处于 FIN-WAIT-1 状态的连接收到 ACK 确认包以后进入 FIN-WAIT-2 状态。
8、CLOSE-WAIT
当被动关闭方收到对方的 FIN 包时就会进入 CLOSE-WAIT。
9、TIME-WAIT
主动关闭端收到被动关闭端的 FIN 报文后,回复 ACK 就会进入 time_wait 状态;
10、LAST-ACK
被动关闭的一方,发送 FIN 包给对端,并且等待对端的 ACK 包时进入该状态。
TCP KeepAlive
KeepAlive 是什么
keepAlive 是 TCP 连接的一种保活机制,探测连接是否还可用的手段。
为什么需要 KeepAlive
当连接的双方没有数据交互时,如果任意一方产生意外崩溃、当机、网线断开或路由器故障等问题,另一方会无法及时得知 TCP 连接已经失效,它就会一直维护这个连接,这样的连接称为【半打开连接】。非常多的半打开连接会造成系统资源的消耗和浪费。
为了解决这种情况,于是设计了 TCP KeepAlive 来避免。
KeepAlive 怎么工作的
当 TCP 连接建立之后,如果开启了 TCP Keepalive ,那么就会启动一个计时器。当计时器倒计时到 0 后,就会发出一个 TCP 探测包。
TCP 探测包是一个纯 ACK 包(RFC1122#TCP Keep-Alives规范建议:不应该包含任何数据,但也可以包含 1 个无意义的字节,比如 0x0),其 Seq 号 与上一个包是重复的,所以其实探测保活报文不在窗口控制范围内。
TCP 探测报文发出后,可以分为如下几种情况:
KeepAlive 的重要参数
tcp_keepalive_time:
KeepAlive 打开的情况下,最后一次数据交换到 TCP 发送第一个保活探测包的间隔,即允许的持续空闲时长,或者说每次正常发送心跳的周期,默认值为 7200s(2h);
tcp_keepalive_probes:
在 tcp_keepalive_time 之后,没有接收到对方确认,继续发送保活探测包次数,默认值为 9(次);
tcp_keepalive_intvl:
在 tcp_keepalive_time 之后,没有接收到对方确认,继续发送保活探测包的发送频率,默认值为 75s。
推荐阅读
如果你也觉得我的分享有价值,记得点赞或者收藏哦!你的鼓励与支持,会让我更有动力写出更好的文章哦!
更多精彩内容,请关注公众号「云舒编程」
版权声明: 本文为 InfoQ 作者【云舒编程】的原创文章。
原文链接:【http://xie.infoq.cn/article/5b305b5de2fdbdcdce0c97244】。文章转载请联系作者。
评论