1. UDP和TCP的区别
UDP:是一种不提供不必要服务的轻量级运输协议,它仅提供最小服务。UDP是无连接的,即在传送数据前不需要先建立连接(没有握手过程),远地的主机在收到UDP报文后也不需要给出任何确认。虽然UDP不提供可靠交付,但是正是因为这样,省去和很多的开销,使得它的速度比较快,比如一些对实时性要求较高的服务,就常常使用的是UDP,比如视频流、在线游戏和实时语音通话等。
TCP:提供面向连接的服务,在传送数据之前必须先建立连接,数据传送完成后要释放连接。因此TCP是一种可靠的的运输服务,但是正因为这样,不可避免的增加了许多的开销,比如确认,流量控制等。常应用于网页浏览、电子邮件、文件传输等。
二者的主要区别:
- 连接方式:TCP是连接导向,UDP是无连接
- 可靠性:TCP提供可靠传输,UDP不保证可靠性
- 数据传输:TCP面向字节流,UDP面向数据报
- 性能:UDP通常更快,但不保证数据的正确性和顺序,而TCP更稳定但相对较慢
应用 | 数据丢失 | 常用的运输协议 | 时间敏感 |
---|---|---|---|
文件传输 | 不能丢失 | TCP | 不 |
电子邮件 | 不能丢失 | TCP | 不 |
Web文档 | 不能丢失 | TCP | 不 |
因特尔电话/视频会议 | 容忍丢失 | UDP | 是,100ms |
流式存储音频/视频 | 容忍丢失 | UDP | 是,几秒 |
交互式游戏 | 容忍丢失 | UDP | 是,100ms |
智能手机讯息 | 不能丢失 | TCP | 是和不是 |
2. TCP概述
TCP把连接作为最基本的对象,每一条TCP连接都有两个端点,这种断点我们叫作套接字(socket),它的定义为端口号拼接到IP地址即构成了套接字,例如,若IP地址为192.3.4.16 而端口号为80,那么得到的套接字为。192.3.4.16:80。
客户端和服务端的通信图如下所示:
3. TCP连接的建立(三次握手)
三次握手(Three-way Handshake)其实就是指建立一个TCP连接时,需要客户端和服务器总共发送3个包。进行三次握手的主要作用就是为了确认双方的接收能力和发送能力是否正常、指定自己的初始化序列号为后面的可靠性传送做准备。实质上其实就是连接服务器指定端口,建立TCP连接,并同步连接双方的序列号和确认号,交换TCP窗口大小信息。
注意,三次握手必须是一方主动打开,另一方被动打开的
首先,TCP服务器进程先创建传输控制块PCB,时刻准备接受客户进程的连接请求,而客户端处于Closed状态,服务器处于LISTEN(监听)状态。
然后,进行三次握手:
1)第一次握手:客户端随机初始化序列号 client_isn
,放进TCP首部序列号段,然后把SYN置1。把SYN报文发送给服务端,表示发起连接,之后客户端处于SYN-SENT
状态。
- 首部的同步位为SYN=1,表示“请求建立新连接”;
- 随后客户端进入
SYN-SENT
阶段。 - 假设
client_isn
为x(x一般为1);
注:SYN报文段(SYN=1的报文段)不能携带数据,但需要消耗掉一个序号
2)第二次握手:服务器收到客户端的 SYN 报文之后,会以自己的 SYN 报文作为应答,并且指定了自己的初始化序列号 server_isn
。同时会把客户端的 client_isn + 1
作为 确认应答号的值,表示自己已经收到了客户端的 SYN,并把 SYN 和 ACK标志位置为1。此时服务器处于 SYN_RCVD
的状态。
在确认报文段中SYN=1,ACK=1,确认应答号=x+1,初始序号server_isn
假设为y。
注:SYN+ACK报文也不能携带数据,但是同样要消耗一个序号
3)第三次握手:客户端收到服务端报文后,还要向服务端回应最后一个应答报文。首先该应答报文 TCP 首部 ACK 标志位置为 1其次,「确认应答号」字段填入 server_isn +1
,最后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于 ESTABLISHED
状态,表示客户端已经准备好与服务器进行数据传输。
ACK=1,确认应答号=y+1,client_isn
=x+1(初始为seq=x,第二个报文段所以要+1),ACK报文段可以携带数据,不携带数据则不消耗序号。
为什么第三次握⼿是可以携带数据的,但是前两次握⼿是不可以携带数据的?
因为第一次握手时客户端对服务器状态未知,服务器也无法判断客户端是否合法,第二次握手时连接尚未完全确认,携带数据易引发安全问题及导致数据传输混乱;而第三次握手时连接已基本建立,双方已相互确认连接状态与对方能力,此时允许携带数据,既能减少一次往返时间以提高传输效率、优化性能,也不会影响连接建立与数据传输的稳定性。
3.1 为什么需要三次握手?
三次握手可以阻止重复历史连接的初始化(主要原因)
- 当因为网络阻塞原因,客户端向服务器发送了两次SYN(申请连接)报文
- 旧的SYN报文先到达服务端,服务端回一个ACK+SYN报文
- 客户端收到后可以根据自身的上下文,判断这是一个历史连接(序列号过期或超时)那么客户端就会发送RST 报文给服务端,表示中止这一次连接。
- 服务器收到RST报文,会释放旧的连接
- 新的SYN报文抵达之后,客户端和服务器之间进行正常的三次握手
如果只是两次握手,服务端在收到SYN报文之后,就进入到
ESTABLISHED
状态,服务器端并不知道这次是历史连接,直接与客户端建立连接并向客户端发送数据(资源浪费),但是客户端会判定这次连接是历史连接,从而发送RST报文来断开连接。所以要想让服务器发送数据前,阻止掉历史连接,就需要三次握手。三次握手才可以同步双方的初始序列号
- 客户端发送第一个报文,携带客户端初始序列号的SYN报文。
- 服务器发送第二个报文,携带服务器初始序列号的ACK+SYN的应答报文,表示收到客户端的SYN报文。
- 客户端发送第三个报文,携带服务器的ACK应答报文。
这样一来一回,才能确保双方的初始序列号能被可靠的同步。两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。四次握手也能保证双方的初始化序号同步,但是可以省略成三次。
确保双方收发能力正常
3.2 为什么TCP客户端最后还要发送一次确认?
这个问题其实就是3.1的主要原因的图解。
一句话,主要防止已经失效的连接请求报文突然又传送到了服务器,从而产生错误。
如果使用的是两次握手建立连接,假设有这样一种场景,客户端发送了第一个请求连接并且没有丢失,只是因为在网络结点中滞留的时间太长了,由于TCP的客户端迟迟没有收到确认报文,以为服务器没有收到,此时重新向服务器发送这条报文,此后客户端和服务器经过两次握手完成连接,传输数据,然后关闭连接。此时此前滞留的那一次请求连接,网络通畅了到达了服务器,这个报文本该是失效的,但是,两次握手的机制将会让客户端和服务器再次建立连接,此时,客户端认为自己只有一个连接,但是服务器认为有两个连接,导致不必要的错误和资源的浪费。
如果采用的是三次握手,就算是那一次失效的报文传送过来了,服务端接受到了那条失效报文并且回复了确认报文,但是客户端不会再次发出确认。由于服务器收不到确认,就知道客户端并没有请求连接。
那么如何通过三次握手解决该问题?参考下图
- 如果一个「旧 SYN 报文」比「最新的 SYN」 报文早到达了服务端(一次握手),那么此时服务端就会回一个 SYN + ACK (二次握手)报文给客户端,此报文中的确认号是 91(90+1)。
- 客户端收到后,发现自己期望收到的确认号应该是 100 + 1,而不是 90 + 1,于是就会回 RST 报文(三次握手)。
- 服务端收到 RST 报文后,就会释放连接。
- 后续最新的 SYN 抵达了服务端后,客户端与服务端就可以正常的完成三次握手(ACK报文)了。
当第三次握手失败时,服务器并不会发送ACK报文,而是直接发送RST报文段,进入
CLOSED
状态。这样做的目的是为了防止SYN洪泛攻击。
3.3 什么是半连接队列?
服务端调用listen函数后,会创建半连接队列和全连接队列,前者是基于哈希表实现的,后者是基于链表实现的。
服务器第一次收到客户端的 SYN 之后,就会处于 SYN_RCVD
状态,此时双方还没有完全建立其连接,服务器会把此种状态下请求连接放在一个队列里,我们把这种队列称之为半连接队列。简单来说就是,半连接队列用于存储那些已经通过了TCP三次握手的前两次握手(即接收了客户端的SYN,并回复了SYN-ACK)但还未完成第三次握手的连接。
全连接队列:存放已经完成三次握手的连接。
半连接队列的工作原理:
- 当客户端向服务器发送
SYN
请求(第一次握手)时,服务器会为这个请求分配资源,并将这个连接放入半连接队列中,同时向客户端回复SYN-ACK
报文(第二次握手)。 - 客户端收到
SYN-ACK
后,回复ACK
报文(第三次握手),服务器收到ACK
后,才会将该连接从半连接队列移到全连接队列,并认为该连接已经完全建立,可以开始通信。 - 如果客户端没有在超时时间内回复
ACK
(如因网络延迟或故意攻击),服务器会在超时后将这个半连接从队列中移除。
针对上面最后一条,如果服务器发送完SYN-ACK包,未收到客户确认包,服务器将进行首次重传,等待一段时间仍未收到客户确认包,进行第二次重传。如果重传次数超过系统规定的最大重传次数,系统将该连接信息从半连接队列中删除。
每次重传等待的时间不一定相同,一般会是指数增长,例如间隔时间为 1s,2s,4s,8s……
3.4 半连接队列被填满或遇到SYN洪泛攻击是如何处理?
当半连接队列被填满或遇到SYN洪泛攻击时,服务器的TCP连接管理会受到严重影响。这种情况下,服务器可能无法正常接收新的连接请求,导致拒绝服务。
1)半连接队列被填满
半连接队列在上面的问题中已经详细说过,它是用于存储那些完成了TCP三次握手的前两次握手(SYN和SYN-ACK),但还未完成第三次握手(ACK)的连接,它的容量是有限的,由服务器的配置决定。
- 当客户端向服务器发送SYN请求时,服务器会将这个连接放入半连接队列,并向客户端发送SYN-ACK响应。
- 如果半连接队列中的连接数量超过了其容量上限,就表示队列已被填满。
- 当队列被填满后,服务器将无法处理新的SYN请求,因为它已经没有空间存储新的半连接。这时,服务器可能会丢弃新的SYN包,导致新的连接无法建立。
半连接队列填满的原因:
- 正常情况下:如果服务器的负载很高,并且有大量合法客户端同时尝试连接,半连接队列可能会短暂填满。
- 攻击情况下:在SYN洪泛攻击中,攻击者故意发送大量的SYN请求,并不回复ACK,以使半连接队列快速填满,从而导致服务器无法接受新的合法连接。
2)什么是SYN洪泛攻击?
服务器端的资源分配是在二次握手时分配的,而客户端的资源是在完成三次握手时分配的,所以服务器容易受到SYN洪泛攻击。
SYN攻击利用TCP协议的三次握手过程来耗尽服务器的资源,在 SYN 攻击中,攻击者发送大量伪造的 SYN 请求到目标服务器,但不完成后续的握手过程,从而让服务器一直等待确认,消耗服务器的资源(如半连接队列和系统资源),当半连接队列满了之后,后续再收到SYN报文就会丢弃,导致无法与客户端之间建立连接。
如何应对?
1)检测 SYN 攻击非常的方便,当你在服务器上看到大量的半连接状态时,特别是源IP地址是随机的,基本上可以断定这是一次SYN攻击。在 Linux/Unix 上可以使用系统自带的 netstat 命令来检测 SYN 攻击。
1 | netstat -n -p TCP | grep SYN_RECV |
2)SYN Cookie:当服务器检测到半连接队列即将被填满或受到SYN洪泛攻击时,通过使用SYN Cookie解决,在linux中通过tcp_syncookies参数来应对这件事:
- 服务器不在半连接队列中分配空间,而是将序列号经过加密算法生成一个Cookie,并发送给客户端。
- 客户端回复的ACK中会带上这个SYN Cookie。服务器通过这个Cookie验证连接的合法性,从而不依赖半连接队列的大小。
- 这种方式即使在遭受攻击时,也能确保服务器继续接收并验证新的连接请求。
千万别用
tcp_syncookies
来处理正常的大负载的连接的情况。因为,SYN Cookie
是妥协版的TCP协议,并不严谨。对于正常的请求,你应该调整三个TCP参数可供你选择,第一个是:tcp_synack_retries
可以用他来减少重试次数(服务器发出);第二个是:tcp_max_syn_backlog
,可以增大SYN连接数;第三个是:tcp_abort_on_overflow
处理不过来干脆就直接拒绝连接了。
3.5 三次握手过程中可以携带数据吗?
其实第三次握手的时候,理论上是可以携带数据的。但是,第一次、第二次握手不可以携带数据。
为什么这样呢?大家可以想一个问题,假如第一次握手可以携带数据的话,如果有人要恶意攻击服务器,那他每次都在第一次握手中的 SYN 报文中放入大量的数据。因为攻击者根本就不理服务器的接收、发送能力是否正常,然后疯狂着重复发 SYN 报文的话,这会让服务器花费很多时间、内存空间来接收这些报文。
其次,在第一次握手时,客户端仅向服务器发送了 SYN 报文,服务器还不知道客户端的接收能力等情况。第二次握手时,服务器回复 SYN+ACK 报文,此时客户端还未确认服务器的发送能力是否正常。只有到第三次握手时,客户端处于 ESTABLISHED 状态,并且已经确认服务器的接收、发送能力正常,这个时候才相对安全,可以携带数据。
也就是说,第一次握手不可以放数据,其中一个简单的原因就是会让服务器更加容易受到攻击了。而对于第三次的话,此时客户端已经处于 ESTABLISHED
状态(服务器此时还未处于 ESTABLISHED
状态)。对于客户端来说,他已经建立起连接了,并且也已经知道服务器的接收、发送能力是正常的了,所以能携带数据也没啥毛病。
虽然第三次握手理论上可以携带数据,但通常不这样做,这是因为:
- 确保连接的稳定性:三次握手的主要目的是建立一个可靠的连接,以确保双方的发送和接收能力正常。如果在连接建立之前就传输数据,而连接建立失败,那么这些数据就可能丢失。
- 安全性:三次握手阶段的数据传输还没有经过完整的安全和可靠性验证,因此传输数据可能不安全。
- 协议规范:标准的TCP协议并不推荐在SYN和SYN-ACK包中携带数据,以减少复杂性,并确保握手的过程专注于连接建立。
总结:
TCP三次握手过程中客户端可以携带数据,但为了标准性一般不这么做,第一次、第二次握手不可以携带数据。但是在一些优化情况下(如TCP Fast Open),可以在第三次握手过程中携带数据,提高传输效率。
3.6 ISN(Initial Sequence Number)是固定的吗?
对于连接的3次握手,主要是要初始化Sequence Number
的初始值。通信的双方要互相通知对方自己的初始化的Sequence Number
(缩写为ISN:Inital Sequence Number)。这个号要作为以后的数据通信的序号,以保证应用层接收到的数据不会因为网络上的传输的问题而乱序(TCP会用这个序号来拼接数据)。
当一端为建立连接而发送它的SYN时,它为连接选择一个初始序号。ISN随时间而变化,因此每个连接都将具有不同的ISN。ISN可以看作是一个32比特的计数器,每4ms加1 。这样选择序号的目的在于防止在网络中被延迟的分组在以后又被传送,而导致某个连接的一方对它做错误的解释。
三次握手的其中一个重要功能是客户端和服务端交换 ISN(Initial Sequence Number),以便让对方知道接下来接收数据的时候如何按序列号组装数据。如果 ISN 是固定的,攻击者很容易猜出后续的确认号,因此 ISN 是动态生成的。
- 用于确定对方已收到数据
- 根据序列号组装数据
3.7 如何处理丢包问题?重传机制
在TCP协议中,当一个数据包过大或者需要适应网络传输情况时,数据可能会被拆成多个包发送,一般通过数据拆分与组装对数据进行处理。
- 当数据被拆分成多个TCP包发送时,TCP会为每个包分配一个序列号(Sequence Number),这些序列号用于标识每个包在整个数据流中的位置。
- 接收端根据序列号来重组这些包,以确保所有数据按照正确的顺序重新组装。
该序列号和握手过程中的ISN有紧密的关系,一旦三次握手完成,ISN就成为后续数据传输中序列号的基准点:
- 客户端发送的第一个数据包:客户端发送的第一个数据包的序列号是
client_isn
。 - 服务器发送的第一个数据包:同样,服务器发送的第一个数据包的序列号是
server_isn
。 - 从这个基准序列号(ISN)开始,后续的数据包序列号会递增,增量为每个数据包的字节数。
下面介绍如何对数据包进行检测以及处理:
丢包的检测与处理机制:
a. 确认机制(ACK)
- 当接收端成功接收到一个数据包时,它会返回一个 确认包(接收端需要回复一个ACK=序列号+长度,确定该数据包已成功被接收)给发送端,告知其接收到的数据序列号。
- 如果发送端在一定的时间内(超时时间)没有收到某个数据包的ACK(接收端发送的ACK=序列号+长度,发送端会将其作为下一数据包的起始序列号),就会认为该数据包可能丢失了
b. 超时重传
- 当发送端没有在预定的超时时间内收到ACK时,它会认为这个数据包丢失了,并重传这个包。
- TCP使用动态超时重传机制,即根据网络的延迟情况调整超时时间,以提高丢包检测的效率。
- 超时重传时间
RTO
的值应该略大于报文往返RTT
的值,且是动态变化的
c. 快速重传
- 如果接收端收到一个数据包,但发现中间的一个数据包丢失了(例如收到序列号为1、3、4的包,但序列号2的包丢失),它会重复发送对序列号2的重复确认(Duplicate ACK)。
- 当发送端收到连续的三个重复确认(Triple Duplicate ACKs)时,就会触发快速重传机制,立即重发那个丢失的数据包,而不必等待超时。
快速重传通过在TCP头部【选项】字段加入SACK
,将已收到的数据的信息发送给【发送方】,这样发送方就知道哪些数据收到了,就可以只重传丢失的数据:
如果200299的数据没发送成功,接收方会连续发送三个 ACK 200表示未收到序列号 200 开始的数据,每次重传中会发送已收到数据的序列号,这里在第三次重传中发送了300600,表示我在300600的数据已收到,而ACK 200 表示我以200开始的数据没收到,发送方根据这两个信息可知,200299的数据未发送成功。
注意:当200~299的数据未收到时,接收端只会发送ACK200,至于其他收到的数据不会返回ACK,但是SACK中会存有已接收的数据序列长度。直到数据接收完整之后,才会返回整个数据包最后一个的ACK600.
而DSACK
用于告诉【发送方】有哪些数据被重复接收了:
ACK丢包
网络延时
使用D-SACK的好处:
(1)可以让【发送方】知道,是发出去的包丢了,还是接收方回应的ACK包丢了
(2)可以知道是不是【发送方】的数据包被网络演示了;
(3)可以知道网络中是不是把【发送方】的数据包给复制了。
3.7 三次握手总结
三次握手建立连接的首要目的是同步序列号。只有同步了序列号才有可靠的传输,TCP 协议的许多特性都是依赖序列号实现的,比如流量控制、消息丢失后的重发等等,这也是三次握手中的报文被称为 SYN 的原因,因为 SYN 的全称就叫做 Synchronize Sequence Numbers。
a. 客户端
客户端发送 SYN 开启了三次握手,之后客户端连接的状态是 SYN_SENT
,然后等待服务器回复 ACK 报文。正常情况下,服务器会在几毫秒内返回 ACK,但如果客户端迟迟没有收到 ACK 会怎么样呢?客户端会重发 SYN,重试的次数由 tcp_syn_retries
参数(在3.4中详细说过该参数以及yncookies)控制,默认是 6 次:
1 | net.ipv4.tcp_syn_retries = 6 |
第 1 次重试发生在 1 秒钟后,接着会以翻倍的方式在第 2、4、8、16、32 秒共做 6 次重试,最后一次重试会等待 64 秒,如果仍然没有返回 ACK,才会终止三次握手。所以,总耗时是 1+2+4+8+16+32+64=127 秒,超过 2 分钟。
如果这是一台有明确任务的服务器,可以根据网络的稳定性和目标服务器的繁忙程度修改重试次数,调整客户端的三次握手时间上限。比如内网中通讯时,就可以适当调低重试次数,尽快把错误暴露给应用程序。
b. 服务器
当服务器收到 SYN 报文后,服务器会立刻回复 SYN+ACK
报文,既确认了客户端的序列号,也把自己的序列号发给了对方。此时,服务器端出现了新连接,状态是 SYN_RCVD
。这个状态下,服务器必须建立一个 SYN 半连接队列(3.3讲过)来维护未完成的握手信息,当这个队列溢出后,服务器将无法再建立新连接(3.4详细说过)。
如果 SYN 半连接队列已满,只能丢弃连接吗?并不是这样,开启 syncookies
功能就可以在不使用 SYN 队列的情况下成功建立连接。syncookies
是这么做的:服务器将序列号经过加密算法生成一个Cookie,放在己方发出的 SYN+ACK 报文中发出,当客户端返回 ACK 报文时(客户端回复的ACK中会带上这个SYN Cookie),取出该值验证,如果合法,就认为连接建立成功
4. 四次挥手
建立一个连接需要三次握手,而终止一个连接要经过四次挥手,这由TCP的半关闭(half-close)造成的。所谓的半关闭,其实就是TCP提供了连接的一端在结束它的发送后还能接收来自另一端数据的能力(比如服务器将自己的接收端关闭后,仍可以接收来自客户端的发送请求,只不过服务器无法进行回传)。
TCP 连接的断开一共需要发送四个包,因此称为四次挥手。数据传输完毕后,双方都可释放连接。最开始的时候,客户端和服务器都是处于ESTABLISHED
状态,这里假设客户端主动关闭,服务器被动关闭。
1)第一次挥手(主动->被动):客户端发送一个 FIN
报文,报文中会指定一个序列号。此时客户端处于 FIN_WAIT1
状态。
即发出连接释放报文段(FIN=1,序号seq=u,u等于前面已经传送过来的数据的最后一个字节的序号加1),并停止再发送数据,主动关闭TCP连接,进入FIN_WAIT1
(终止等待1)状态,等待服务端的确认。
2)**第二次挥手(被动->主动)**:服务端收到 FIN
之后,会发送 ACK
报文,且把客户端的序列号值 +1 作为 ACK
报文的序列号值,表明已经收到客户端的报文了,此时服务端处于 CLOSE_WAIT
状态。
即服务端收到连接释放报文段后即发出确认报文段(ACK=1,确认号=u+1,并且带上自己的序列号seq=v),服务端进入CLOSE_WAIT
(关闭等待)状态,TCP服务器通知高层的应用进程,客户端向服务器的方向就释放了,这时候处于半关闭状态,即客户端已经没有数据要发送了,但是服务器若发送数据,客户端依然要接受。这个状态还要持续一段时间,也就是整个CLOSE_WAIT
状态持续的时间。
客户端收到服务端的确认后,进入FIN_WAIT2
(终止等待2)状态,等待服务端发出的连接释放报文段(在这之前还需要接受服务器发送的最后的数据,服务器在TCP缓存区插入EOF文件结束符,当服务器读到EOF文件结束符时,表示数据发送完毕,发送FIN报文)。
3)第三次挥手(被动->主动):如果服务端也想断开连接了,和客户端的第一次挥手一样,发给 FIN
报文,且指定一个序列号。此时服务端处于 LAST_ACK
的状态。
即服务端没有要向客户端发出的数据,服务端发出连接释放报文段(FIN=1,ACK=1,由于在半关闭状态,服务器很可能又发送了一些数据,假定此时的序列号为seq=w,确认号ack=u+1),服务端进入LAST_ACK
(最后确认)状态,等待客户端的确认。
4)第四次挥手(主动->被动):客户端收到 FIN
之后,一样发送一个 ACK
报文作为应答,且把服务端的序列号值 +1 作为自己 ACK
报文的序列号值,此时客户端处于 TIME_WAIT
状态。需要过一阵子以确保服务端收到自己的 ACK
报文之后才会进入 CLOSED
状态,服务端收到 ACK
报文之后,就处于关闭连接了,处于 CLOSED
状态。
即客户端收到服务端的连接释放报文段后,对此发出确认报文段(ACK=1,seq=u+1,ack=w+1),客户端进入TIME_WAIT
(时间等待)状态。此时TCP未释放掉,需要经过时间等待计时器设置的时间**2MSL(最长报文段寿命)**,以确保服务端能收到最后的ACK
报文,客户端才进入CLOSED
状态。
服务器结束TCP连接的时间要比客户端早一些。
四次挥手步骤如下:
4.1 为什么客户端最后还要等待2MSL?
MSL是Maximum Segment Lifetime的英文缩写,可译为“最长报文段寿命”,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。
为了保证客户端发送的最后一个ACK报文段能够到达服务器。因为这个ACK有可能丢失,从而导致处在LAST-ACK状态的服务器收不到对FIN-ACK的确认报文。服务器会超时重传这个FIN-ACK,接着客户端再重传一次确认,重新启动时间等待计时器。最后客户端和服务器都能正常的关闭。假设客户端不等待2MSL,而是在发送完ACK之后直接释放关闭,一但这个ACK丢失的话,服务器就无法正常的进入关闭连接状态
总结,需要等待2MSL的原因:
第一,保证客户端发送的最后一个ACK报文能够到达服务器,因为这个ACK报文可能丢失,站在服务器的角度看来,我已经发送了FIN+ACK
报文请求断开了,客户端还没有给我回应,应该是我发送的请求断开报文它没有收到,于是服务器又会重新发送一次,而客户端就能在这个2MSL时间段内收到这个重传的报文,接着给出回应报文,并且会重启2MSL计时器。
第二,防止类似与“三次握手”中提到了的“已经失效的连接请求报文段”出现在本连接中。客户端发送完最后一个确认报文后,在这个2MSL时间中,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样新的连接中不会出现旧连接的请求报文。
第三,如果主动方不保留TIME_TAIT
状态,而是直接CLOSE
,那么此时连接的端口恢复了自由身,可以复用于新连接。然而,被动方的 FIN
报文可能再次到达,这既可能是网络中的路由器重复发送,也有可能是被动方没收到 ACK
时基于 tcp_orphan_retries
参数重发。这样,正常通讯的新连接就可能被重复发送的 FIN 报文误关闭。保留 TIME_WAIT
状态,就可以应付重发的 FIN 报文,当然,其他数据报文也有可能重发,所以 TIME_WAIT 状态还能避免数据错乱。
为什么是2MSL不是1.5MSL或者其他值呢?
TIME_WAIT
确保有足够的时间让对端收到了ACK,如果被动关闭的那方没有收到ACK
,就会触发被动端重发FIN
,一来一去正好2个MSL .
这种2MSL等待的另一个结果是这个TCP连接在2MSL等待期间,定义这个连接的插口(客户的IP地址和端口号,服务器的IP地址和端口号)不能再被使用。这个连接只能在2MSL结束后才能再被使用。
4.2 为什么建立连接是三次握手,关闭连接确是四次挥手?⭐
1)这是因为 TCP 不允许连接处于半打开状态时就单向传输数据,所以在三次握手建立连接时,服务器会把 ACK 和 SYN 放在一起发给客户端,其中,ACK 用来打开客户端的发送通道,SYN 用来打开服务器的发送通道。这样,原本的四次握手就降为三次握手了。
但是当连接处于半关闭状态时,TCP 是允许单向传输数据的。为便于理解,我们把先关闭连接的一方叫做主动方,后关闭连接的一方叫做被动方。当主动方关闭连接时,被动方仍然可以在不调用 close 函数的状态下,长时间发送数据,此时连接处于半关闭状态。这一特性是 TCP 的双向通道互相独立所致,却也使得关闭连接必须通过四次挥手才能做到。
四次挥手的简单步骤:
- 在TCP连接中,关闭连接是一个双向的过程,客户端和服务器都需要分别关闭各自的发送通道
- 当客户端发送FIN请求关闭发送通道时,服务器可能还有数据要发送,因此它会先回复ACK,并在数据发送完毕后再发送FIN
- 客户端收到服务器的FIN后,再发送最后一个ACK确认,这样才能确保连接彻底关闭
简单来说,三次握手是因为只需要确认双方的发送和接收能力即可,但四次挥手需要双方各自关闭发送通道,这个过程需要双方都能确认所有数据都已传输完毕,并且各自关闭了发送和接收通道。
2)建立连接的时候, 服务器在LISTEN状态下,收到建立连接请求的SYN报文后,把ACK和SYN放在一个报文里发送给客户端。
而关闭连接时,服务器收到对方的FIN报文时,仅仅表示对方不再发送数据了但是还能接收数据,而自己也未必全部数据都发送给对方了,所以己方可以立即关闭,也可以发送一些数据给对方后,再发送FIN报文给对方来表示同意现在关闭连接,因此,己方ACK和FIN一般都会分开发送,从而导致多了一次。
4.3 如果已经建立了连接,但是客户端突然出现故障了怎么办?
如果「客户端进程崩溃」客户端的进程在发生崩溃的时候,内核会发送 FIN 报文,与服务端进行四次挥手。
但是,**[客户端主机宕机**」,那么是不会发生四次挥手的,具体后续会发生什么?还要看服务端会不会发送数据?
- 如果服务端会发送数据(心跳),由于客户端已经不存在,收不到数据报文的响应报文,服务端的数据报文会超时重传,当重传总间隔时长达到一定阈值后,会断开 TCP 连接:
- 如果服务端一直不会发送数据,再看服务端有没有开启 TCP keepalive 机制?
- 如果有开启,服务端在一段时间没有进行数据交互时,会触发 TCP keepalive 机制,探测对方是否存在,如果探测到对方已经消亡,则会断开自身的TCP 连接;
- 如果没有开启,服务端的 TCP 连接会一直存在,并且一直保持在 ESTABLISHED 状态。
4.4 客户端time_wait过多怎么办
原因:
- 短连接频繁主动关闭
客户端作为主动关闭方(如 HTTP 客户端、微服务调用方),在每次请求后立即关闭连接,导致大量TIME_WAIT
堆积。 - 端口资源有限
客户端端口范围默认通常是32768-60999
(约 2.8 万个端口),高并发下短连接快速消耗端口,未及时释放。 - 挥手过程异常
网络延迟或丢包导致最后一个ACK
未到达服务端,触发重传,延长TIME_WAIT
持续时间(默认 2MSL,约 2-4 分钟)。
解决方法:
- 调整
tcp_tw_reuse
参数:该参数允许在TIME_WAIT
状态的连接上复用本地地址和端口,允许端口在TIME_WAIT
状态下被重新使用,避免端口占用导致端口资源消耗完。 - 调整
tcp_tw_recycle
参数:该参数可以加快TIME_WAIT
状态的回收速度(一般不用调整该参数)。 - 客户端复用连接:避免频繁地建立和关闭连接,尽量复用已有的连接。例如,在使用 HTTP 协议时,可以使用长连接(HTTP/1.1 支持),通过设置
Connection: keep - alive
头信息来保持连接的持续打开状态。 - 强制关闭
参考: