文章首发于微信公众号:云舒编程
关注公众号获取: 1、大厂项目分享 2、各种技术原理分享 3、部门内推
前言
想必不少同学在面试过程中,会遇到「在浏览器中输入www.baidu.com后,到网页显示,其间发生了什么 」类似的面试题。
本专栏将从该背景出发,详细介绍数据包从HTTP层->TCP层->IP层->网卡->互联网->目的地服务器 这中间涉及的知识。
本系列文章将采用自底向上的形式讲解每层的工作原理和数据在该层的处理方式。
系列文章
每天5分钟玩转计算机网络-(数据链路层、物理层)工作原理
每天5分钟玩转计算机网络-(网络层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算法的演进史,有兴趣的同学可以自行查阅):
json
首次计算RTO:
SRTT = R
RTTVAG = R/2
RTO = SRTT + max(G,K*RTTVAG)
后续计算RTO,R'为最新计算出来的RTT:
SRTT = (1-α)*SRTT + α*R'
RTTVAG = (1-β)*RTTVAG + β*|SRTT-R'|
RTO = SRTT + max(G,K*RTTVAG)
============================
其中:
SRTT(Smoothed RTT):表示平滑RTT
R:为第一次计算出来的RTT
RTTVAG(Round Trip Time Variation):表示往返时间变化
G:最小时间颗粒
α=1/8,β=1/4,K=4
快速重传
为了解决超时重传的问题,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是安全审计、防火墙测试等工作的标配工具
vue
# -S 发送SYN数据包
# --flood 泛洪攻击
hping -S -p 端口 --flood ip
2、iptables
iptables 可以过滤发给主机的网络包,我们通过iptables增加规则,丢弃服务端响应的SYN+ACK报文,这样客户端就不会发送ACK报文,达到模拟SYN攻击的目的
tcl
--append INPUT: 将规则添加到INPUT链中,该链用于处理输入的数据包。
--match tcp: 匹配TCP协议的数据包。
--protocol tcp: 指定匹配TCP协议的数据包。
--src 10.211.55.10: 指定源IP地址为10.211.55.10的数据包。
--sport 9090: 指定源端口为9090的数据包。
--tcp-flags SYN SYN: 匹配TCP flags中设置了SYN标志位的数据包,即TCP连接的初始请求包。
--jump DROP: 如果以上条件匹配,则将数据包丢弃。
iptables --append INPUT --match tcp --protocol tcp --src 目标ip地址 --sport 目标端口 --tcp-flags SYN SYN --jump DROP
全连接队列
所有完成了三次握手,但是还未被应用调用accept函数取走的连接都会被存放在【全连接队列】。
当应用调用accept函数后,内核就会移除队列头的连接返回给应用,如果没有可用的连接的话,就会阻塞。
查询全连接队列大小
可以使用 ss 命令,来查看 TCP 全连接队列的情况:
json
命令:ss -lt
输出:
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 *:36000 *:*
LISTEN 0 4096 *:48369 *:*
LISTEN 0 100 127.0.0.1:42222 *:*
注意:
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主动监听一个特定的端口,等待客户端的连接。
go
//golang 可以用以下代码构造处于 listen 状态的 连接。
listener, err := net.Listen("tcp", ":8080")
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。