TCP三次握手、四次挥手及状态转换详解

​什么是TCP协议?TCP/UDP 的特点、区别及优缺点-CSDN博客

TCP通信过程

  1. 建立TCP连接通道,三次握手;
  2. 传输数据;
  3. 断开TCP连接通道,四次挥手。

TCP连接状态

  • LISTEN:服务端等待客户端的连接请求;
  • SYN-SENT:同步已发送。客户端向服务器发送SYN报文,等待服务端ACK;
  • SYN-RECEIVED:同步已接收。服务端收到客户端的SYN报文,并发送SYN+ACK报文,等待客户端ACK;
  • ESTABLISHED:连接已建立,正在进行数据传输;
  • FIN-WAIT-1:主动发起关闭连接的一方发送FIN报文给对端,告知对端我方不再发送数据(仍可接收数据),等待对端ACK;
  • CLOSE-WAIT:被动接受关闭的一方收到FIN报文,并发送ACK报文给对端,继续传输数据;
  • FIN-WAIT-2:主动发起关闭连接的一方收到对端的ACK报文,并继续等待对端的FIN+ACK报文;
  • LAST-ACK:最后确认。被动接受关闭的一方传输完所有数据后,发送FIN+ACK报文给对端,等待对端的最后一次ACK;
  • TIME-WAIT:主动发起关闭连接的一方收到对端的FIN+ACK报文,并发送ACK报文给对端,等待2MSL后关闭连接;
  • RESET:客户端或服务器收到一个无效连接请求,发送一个重置信号给对方。
# 查询各状态的连接数量
[root@reader ~]# netstat -n | awk '/^tcp/ {++S[$NF]} END {for(i in S) print i, S[i]}'
ESTABLISHED 4
FIN_WAIT2 1
TIME_WAIT 1
[root@reader ~]# 

三次握手

        建立连接时,区分主动建立连接和被动等待建立连接,主动发起建立连接的一端(应用程序)称为客户端(client),被动等待建立连接的一端称为服务端(server)。

        三次握手建立连接的时候,可将连接分为两大类,一类是三次握手中的连接,即半开连接,服务端收到客户端发送的SYN报文后,内核会先把半开连接存放于“未完成连接队列/半连接队列/SYN队列”中,再向客户端返回SYN+ACK报文。还有一类是三次握手完成的连接,即完全连接,服务端接收到客户端第三次握手发送来的ACK报文后,内核会先把半开连接从半连接队列中移除,然后创建新的完全连接并将该连接存放于“已完成连接队列/全连接队列/ accept 队列”中。连接建立成功后,应用程序再调用accep函数从“全连接队列”中获取一个完全连接。

第一次握手

        希望建立连接。客户端给服务端发送SYN报文,报头需初始化seq值,之后进入 SYN-SENT 状态,等待服务端的ACK报文。客户端会在第一次握手中会声明自己的MSS、WS(Window Scale)和SACK Permitted。

第二次握手

        允许建立连接。服务端收到客户端的SYN报文,如果允许建立连接,就向客户端发送SYN+ACK报文,报头需初始化seq和ack值,之后进入 SYN-RCVD 状态,等待客户端的ACK报文。服务端也会在第二次握手中声明自己的MSS、WS和SACK Permitted。

第三次握手

        确认建立连接。客户端收到SYN+ACK报文后,表明服务端允许建立连接,会再向服务端发送一个ACK报文。发送完ACK报文后,客户端便进入 ESTABLISHED 状态,服务端在收到客户端的ACK报文后也进入 ESTABLISHED 状态,三次握手结束,TCP连接成功建立。

四次挥手

        关闭连接时,也区分主动发起关闭和被动接受关闭,首次发出FIN报文的一端我们称其为主动关闭连接方,对端自然就是被动关闭连接方,客户端和服务端都可以主动发起关闭连接。主动关闭连接方在挥手过程中通常会经历 FIN-WAIT-1 → FIN-WAIT-2 → TIME-WAIT → CLOSED 四个状态切换,但有时候不会经过FIN-WAIT-2状态(FIN-WAIT-1 → TIME-WAIT → CLOSED)。而被动关闭连接方在挥手过程中只会经历 CLOSE-WAIT → LAST-ACK → CLOSED三个状态切换。通常情况下是客户端主动关闭连接,服务端被动关闭连接。

        在讲四次挥手的过程中,我们不区分客户端和服务端,只区分谁是主动关闭连接的一方,谁是被动关闭连接的一方。

第一次挥手

        请求关闭连接。当客户端/服务端发送完所有报文数据后,应用层会调用close()函数关闭socket,此时内核协议栈会自动给对端发送一个FIN报文,告知对方我方不再发送数据(半连接状态,仍可接收数据),请求关闭连接。主动关闭连接方发送完FIN报文后,会进入 FIN-WAIT-1 状态,等待对端返回ACK报文。

第二次挥手

        收到关闭请求。被动关闭连接方在收到FIN报文后,内核协议栈会立即返回一个ACK报文用以响应对方,并通知应用层对端不再向本端发送数据,之后进入 CLOSE-WAIT 状态。此时,如果接收缓冲区还有数据就继续读取,如果还有待传输的数据还是正常传输,如果没有就开始第三次挥手。

第三次挥手

        可以关闭连接。被动关闭连接方发送完所有数据后,应用层也调用close()函数关闭socket,内核协议栈自动给主动关闭连接方发送一个FIN报文,告知对方我方也不再发送数据,可以关闭连接,之后由 CLOSE-WAIT 状态进入 LAST-ACK 状态。

        请注意,有时候TCP为了提高传输效率,会根据两端的通信情况将第二次挥手和第三次挥手合并起来,这时候挥手过程就只有3次了。当被动关闭连接方收到FIN报文后,发现自己也没有数据要发送给对端,就将两次挥手合并成一次,直接给对端发送FIN+ACK报文,主动关闭连接方收到FIN+ACK报文后,立即发送ACK报文,并从 FIN-WAIT1 直接进入TIME-WAIT 状态,不经过 FIN-WAIT2 状态。

第四次挥手

        确认关闭连接。主动关闭连接方收到FIN+ACK报文后,也会立即发送一个ACK报文,正式告知对方连接即将断开,之后便进入 TIME-WAIT 状态(再等待2MSL后TCP连接将才正式关闭)。至此,四次挥手结束,TCP连接关闭。

核心问题

        TCP是全双工的、可靠的传输控制协议,确认应答机制是TCP保证可靠性的最核心机制。只有彻底理解了这个,接下来的问题就很好解答。TCP的核心工作机制在这篇文章里有详细介绍 TCP 核心工作机制

1.为什么建立连接时必须得握手三次?

        一句话:为了确保建立的每条连接都是有效连接,避免资源浪费新建、维护和断开连接都需要消耗系统资源,服务端总不能说你来一条SYN报文,我就给你建立一个连接吧,万一是恶意攻击或已失效的SYN报文呢?所以第三次握手尤为重要,只有在完成第三次握手后才能确定双方收发信息都正常,此时建立的连接才能确保有效。TCP是全双工通信协议,必须确保双方都能正常收发信息。

2.为什么关闭连接时必须得挥手四次?

        至少要挥手四次才能确认双方都已发送完数据。因为TCP是一个全双工可靠传输控制协议,两端都可读可写,所以为了保证传输的可靠性,在关闭连接前,双方都必须得确认对方已完成数据传输(都收到对方的FIN报文并确认ACK)。

        A:我方数据传输完成,请求关闭连接,FIN;

        B:收到关闭请求,我方数据还没传完,请稍等,ACK;

        B:我方数据也传输完成,可以完毕连接,FIN + ACK;

        A:收到,开始关闭,ACK。

3.主动关闭方为什么需要设置 TIME_WAIT 状态?

主要有以下两个原因:

  • 为了防止历史连接中的延迟数据,被后面相同四元组(源地址、源端口、目的地址、目的端口)的连接错误地接收,因此 TCP 才设计了 TIME_WAIT 状态,此状态会持续 2MSL 时长,这个时间足以让两个方向上的所有数据包都被丢弃,使得原来连接的数据包在网络中都自然消亡,再出现的数据包一定都是新建立的连接所产生的。
  • 确保被动关闭连接方的连接能被正确关闭。等待足够的时间以确保最后的 ACK 能让被动关闭方接收,从而让其正常关闭。

4.什么是MSL?为什么主动关闭连接方要等2MSL后才被closed?

        报文段最长生存时间(MSL,Maximum Segment Lifetime),即TCP报文段在网络中存在的最大时长,超时会被丢弃。在RFC793版本中,MSL被任意规定为2分钟。Windows系统中MSL默认是120秒(可以修改),Linux系统中MSL是固定的30秒Linux系统中MSL是无法修改的,别听他人胡咧咧说可以通过修改tcp_fin_timeout来修改MSL,tcp_fin_timeout只是默认等于2MSL而已,但并不代表它就是2MSL。

        TCP虽然是可靠传输,但网络是不可靠的,如果主动关闭连接方最后一次发送的ACK报文发生丢失,那么被动关闭连接方会认为自己发送的FIN+ACK报文丢失,将尝试再次给主动关闭连接方发送FIN+ACK报文,主动关闭连接方收到FIN+ACK报文后,会重新给被动关闭连接方发送ACK报文,然后重置等待时间为新的2MSL,被动关闭连接方收到ACK报文后,TCP状态由 LAST-ACK 转为 CLOSED 。主动关闭连接方如果在2MSL内未收到对端的FIN报文,也会将TCP状态由 TIME-WAIT 转为 CLOSED。所以,多等待1个MSL就是为了防止因主动关闭连接方最后一次发送的ACK报文丢失而影响被动关闭连接方TCP连接状态无法正常转换为CLOSED的情况。

2MSL = 主动关闭连接方最后一次发送的ACK报文所对应的MSL + 被动关闭连接方重传的FIN+ACK报文所对应的MSL。

        主动发起关闭连接的一方,从TIME-WAIT 转为 CLOSED 前,必须得等2MSL,只有在2MSL时间段内未收到FIN报文时,才说明对端已收到上次发送的ACK报文,我方也可以正常关闭连接了。

5.为什么在建立连接时要声明并协商MSS呢?

        防止因报文过大而在网络层被IP协议切片,切片后TCP报文段不再完整,接收方也无法解析数据包,将会报告发送端重传这段报文,重传后又被切片....又重传....没完没了。所以在建立连接时,双方先各自声明自己发送TCP报文段数据部分的最大长度MSS,然后再取较小一方的值作为最终使用的MSS值,这样就可确保双方互传数据时都不会被IP协议切片,保证了传输的可靠性。

6.三次握手时可以携带应用层数据吗?

        为防止SYN洪泛攻击,第一次和第二次握手不可携带数据,第三次握手可以携带数据。

7.四次挥手时可以携带应用层数据吗?

        第二次挥手后,如果被动关闭连接方判定自己还有数据需要发送给主动关闭连接方,此时数据还是能够正常发送的。

8.三次握手时seq一定是从0开始的么?

        三次握手过程中,无论是客户端给服务端首次发送的seq还是服务端给客户端首次发送的seq都是动态变化的,不一定非得从0开始。

9.ACK数据包会消耗TCP序号吗?

        什么是序号?请参考 TCP报文头(首部)详解

        回答这个问题要看你怎么理解ACK数据包了。不妨先来区分下ACK数据包。TCP为提高传输效率,有些时候会将ACK标志位与其它标志位一起使用,如 SYN+ACK、FIN+ACK、PSH+ACK、RST+ACK,这样的消息确认机制称为捎带应答机制。如果我们把携带ACK标志位的报文也看成一类ACK报文的话,可以称其为携带ACK报文,还有一种就是纯ACK报文,报头中只有ACK标志位。

        纯ACK报文是不消耗序号的,而携带ACK的报文要消耗序号,如 SYN+ACK、FIN+ACK、RST+ACK这类不携带应用层数据的报文每次会消耗一个序号,PSH+ACK这类报文会携带应用层数据,此类报文消耗的序号个数是数据长度值。

        如果我们把携带ACK的报文不当成ACK报文,那就仅剩纯ACK报文了,而纯ACK报文是不消耗序号的。

        我们再回头重新理解下ACK标志位。ACK只是为了响应对方我方已正确收到贵方最后一次发送的报文,标识着前一轮应答已结束,继续下一轮应答,它不会对应答本身产生任何逻辑影响(即使是重传数据也是在下一轮应答),所以ACK它本身是不消耗序号的,至于携带了ACK的报文要不要消耗序号,具体消耗多少个序号,还要看这个报文的另一个标志位或携带应用层数据的长度大小。

10.如果服务端是被动关闭连接方,出现了大量处于 TIME-WAIT 状态的连接是什么原因?

        被动关闭连接方在收到对端的FIN报文后,会先将状态切换为 TIME-WAIT 状态,等数据发送完毕后,应用层关闭socket,内核协议栈自动给主动关闭连接方发送FIN报文,之后状态转为 CLOSED。出现大量TIME-WAIT连接,说明应用层调用close()函数失败或者是调用close()函数前应用层需要耗费较长时间进行逻辑处理。出现这种情况的原因都是在应用层,我们需要从应用层排查问题,检验应用层是否因线程阻塞而无法调用close(),或者是应用层逻辑处理陷入死循环,亦或者是应用层数据陷入死锁。

11.如果服务端应用程序不调用accept()函数取用新连接,最多可完成三次握手建立多少个连接?

         最多可完成三次握手建立的连接数=全连接队列最大值+1

12.什么情况下服务端会把SYN报文丢弃?如何解决(如何防御SYN Flood攻击)?

参考 4.4 TCP 半连接队列和全连接队列 | 小林coding

出现以下三种情况时,服务端会丢弃SYN报文:

  • 半连接队列已满,且未开启tcp_syncookies;
  • 全连接队列已满,且未重传SYN+ACK报文的连接个数大于1;
  • 未开启tcp_syncookies,当前半连接队列长度 > (tcp_max_syn_backlog -  tcp_max_syn_backlog >> 2 )

针对以上三种情况,有如下解决方案:

  • 开启 tcp_abort_on_overflow
  • 增大半连接队列
  • 减少 SYN+ACK 重传次数
  • 开启 tcp_syncookies

12.1 如何开启 tcp_abort_on_overflow?

        该参数用于设置当accept队列已满时,是否要将后续的TCP半开连接置为RST,即返回RST报文。可选值是0或1,默认值是0,表示关闭。当accept队列已满时,如果该参数设置为0,服务端会丢弃掉客户端返回的ack报文。如果设置为1,服务端将发送RST报文给客户端,要求重置连接并重新建立连接。谨慎开启该配置!

[root@reader ~]# find / -name tcp_abort_on_overflow
/proc/sys/net/ipv4/tcp_abort_on_overflow
[root@reader ~]# sysctl -a | grep tcp_abort_on_overflow
net.ipv4.tcp_abort_on_overflow = 0
[root@reader ~]# 

### 永久修改 tcp_abort_on_overflow
在系统配置文件 /etc/sysctl.conf 中,添加如下一行代码,添加完记得执行sysctl -p
net.ipv4.tcp_abort_on_overflow = 1

12.2 如何增大TCP全连接队列?

全连接队列的最大值 = min(somaxconn, backlog)

        全连接队列的最大值取决于 somaxconn 和 backlog (listen 函数中的 backlog) 之间的最小值,也就是 min(somaxconn, backlog)

        somaxconn 是Linux 的内核参数,默认值是128,可以通过修改 /proc/sys/net/core/somaxconn 来设置其值;

listen( int sockfd, int backlog ) 

- sockfd:套接字描述符,传入socket函数的返回值。含义是侦听套接字。

- backlog:已完成连接队列的大小。

        backlog 是 listen( int sockfd, int backlog ) 函数中的 backlog 大小,每个 Web 服务都不同,Nginx 默认值是 511,可以通过修改配置文件设置其长度。

### 临时修改 somaxconn
[root@reader ~]# find / -name somaxconn*
/proc/sys/net/core/somaxconn
[root@reader ~]# cat /proc/sys/net/core/somaxconn 
128
[root@reader ~]# echo 1024 > /proc/sys/net/core/somaxconn 
[root@reader ~]# cat /proc/sys/net/core/somaxconn 
1024
[root@reader ~]# sysctl -a | grep somaxconn
net.core.somaxconn = 1024
[root@reader ~]# sysctl -w net.core.somaxconn=1000
net.core.somaxconn = 1000
[root@reader ~]# sysctl -a | grep somaxconn
net.core.somaxconn = 1000
[root@reader ~]# 

### 永久修改 somaxconn
在系统配置文件 /etc/sysctl.conf 中,添加如下一行代码,添加完记得执行sysctl -p
net.core.somaxconn = <new_value>
# Nginx修改 backlog值来设置全连接队列/accept队列的最大长度,默认值是511。

listen 127.0.0.1 default_server accept_filter=dataready backlog=1024;

12.3 如何增大TCP半连接队列?

        半连接队列的最大值跟 tcp_max_syn_backlog、somaxconn 和 backlog都有关,不同内核版本取值方法可能不一样。在调高  tcp_max_syn_backlog 值的同时,还要适当调高 somaxconn 和 backlog 的值(也就是增大全连接队列最大值)。somaxconn 和 backlog 的修改方法,我们在上面已经介绍了,下面只介绍如何修改 tcp_max_syn_backlog。

修改 tcp_max_syn_backlog,/proc/sys/net/ipv4/tcp_max_syn_backlog

[root@reader ~]# find / -name *backlog*
/proc/sys/net/core/netdev_max_backlog
/proc/sys/net/ipv4/tcp_max_syn_backlog
[root@reader ~]# find / -name *backlog*
/proc/sys/net/core/netdev_max_backlog
/proc/sys/net/ipv4/tcp_max_syn_backlog
[root@reader ~]# cat /proc/sys/net/ipv4/tcp_max_syn_backlog
1024
[root@reader ~]# sysctl -a | grep backlog
net.core.netdev_max_backlog = 1000
net.ipv4.tcp_max_syn_backlog = 1024
[root@reader ~]# 

可以用echo或sysctl -w 来临时修改tcp_max_syn_backlog
也可以在系统配置文件 /etc/sysctl.conf 中,添加如下一行代码,永久修改 tcp_max_syn_backlog
net.ipv4.tcp_max_syn_backlog = <new_value>

12.4 如何修改 SYN+ACK 重传次数?

        当服务端受到 SYN 洪泛攻击时,就会有大量处于 SYN_RECV 状态的 TCP 连接,处于这个状态的 TCP 会重传 SYN+ACK ,当重传超过次数达到上限后,就会断开连接,并将半开连接从半连接队列中移除。我们可以通过减少 SYN+ACK 的重传次数,来加快无效半开连接的断开速率。

[root@reader ~]# find / -name *synack_retries*
/proc/sys/net/ipv4/tcp_synack_retries
[root@reader ~]# cat /proc/sys/net/ipv4/tcp_synack_retries
2
[root@reader ~]# sysctl -a | grep synack_retries
net.ipv4.tcp_synack_retries = 2
[root@reader ~]# tail -n 20 /etc/sysctl.conf | grep synack_retries
net.ipv4.tcp_synack_retries = 2


可以用echo或sysctl -w 来临时修改tcp_synack_retries
也可以在系统配置文件 /etc/sysctl.conf 中,添加如下一行代码,永久修改 tcp_synack_retries
net.ipv4.tcp_synack_retries = <new_value>

12.5 如何开启 tcp_syncookies?

        该参数是防御SYN Flood攻击的最后手段,如果开启了 tcp_syncookies 功能,即使半连接队列满了,服务端也不会丢弃SYN报文,而是返回一个带有cookie的SYN+ACK报文。

官方不建议将该参数作为高负载服务器的性能调优选项,而是推荐使用 tcp_max_syn_backlog、tcp_synack_retries 和 tcp_abort_on_overflow


It is not recommended as a tuning mechanism for heavily loaded servers to help with overloaded or misconfigured conditions. For recommended alternatives see tcp_max_syn_backlogtcp_synack_retries, and tcp_abort_on_overflow.

        syncookies的工作原理:当服务端接收到客户端的SYN报文时,如果半连接队列已满,syncookies会根据当前这个SYN包计算出一个cookie值,并将其放在 SYN+ACK 报文(报头的seq参数中)中发送给客户端,客户端返回 ACK 报文时也会携带这个cookie值,服务端收到ACK报文后会根据cookie值来校验这个ACK包的合法性。如果合法,就创建一个完全连接并放入全连接队列中,全程不需要半连接队列参与。

syncookies 参数主要有以下三个值:

  • 0 值,表示关闭该功能;
  • 1 值,表示仅当 SYN 半连接队列溢出时,才启用它;linux默认值是1。
  • 2 值,表示无条件开启功能;

        通过修改 /proc/sys/net/ipv4/tcp_syncookies 来设置 syncookies 值

[root@reader ~]# find / -name *syncookies*
/proc/sys/net/ipv4/tcp_syncookies
[root@reader ~]# cat /proc/sys/net/ipv4/tcp_syncookies
1
[root@reader ~]# sysctl -a | grep syncookies 
net.ipv4.tcp_syncookies = 1


可以用echo或sysctl -w 来临时修改tcp_syncookies 
也可以在系统配置文件 /etc/sysctl.conf 中,添加如下一行代码,永久修改 tcp_syncookies 
net.ipv4.tcp_syncookies = <new_value>

13.进入TIME-WAIT 状态的TCP连接,下一步一定会被Close么?

        No,处于 TIME-WAIT 状态的TCP连接是可以接收新的SYN报文,随后直接从 TIME-WAIT 状态重新打开连接(但是有条件的哦!)。

        RFC 9293: Transmission Control Protocol (TCP)   3.6.1章节 半关闭连接

  • 18
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值