目录
一、TCP的基本认识
TCP全称为 "传输控制协议(Transmission Control Protocol"). 人如其名, 要对数据的传输进行一个详细的控制。
1. TCP头部格式
源/目标端口号:表示数据是从哪个进程来,到哪个进程去;
序列号:在建立连接时由计算机生成的随机数作为初始值,通过SYN包传给接收端主机,每发一次数据,就累加一次数据字节数大小。用来解决网络包乱序问题。
确认应答号:指下一次期望收到的序列号,发生端收到这个确认应答号后可以认为这个序列号之前的数据都已经被正常接收。用来解决丢包问题。
6位标志位:
- URG:该位为1时,表明紧急指针字段有效。它告诉系统此报文段中有紧急数据,应当尽快传输(相当于高优先级的数据),而不要按原来的排队顺序来传送。
- ACK:该位为1时,确认应答的字段变为有效,TCP规定除了最初建立连接时的SYN包之外改位必须置为1;
- PSH:该位为1时,提示接收端应用程序立刻从TCP缓冲区把数据读取走;
- RST:该位为1时,表示TCP连接中出现了异常必须强制断开连接;
- SYN:该位为1时,表示希望建立连接,并在序列号的字段进行初识序列号值的设定。
- FIN:该位为1时,表示今后不会再有数据发送,希望断开连接。
2. TCP协议的特点
面向连接:一定是一对一才能连接,不能像UDP协议那样可以一个主机同时向多个主机发生消息,也就是一对多是无法做到的;
可靠的:无论网络链路层出现了什么变化,TCP都可以保证一个报文一定能够到达接收端;
字节流:消息是没有边界的,所以无论我们的消息有多大都可以进行传输。并且消息是有序的,当前一个消息还没收到的时候,即便收到了后面的字节,那么也不能交付给应用层去处理,同时对重复的报文会自动丢弃。
3. 什么是TCP连接
TCP——传输控制协议,是由IETF的RFC 793定义的一个重要传输协议;我们看看它是如何定义连接的,如下图所示:
从上图红线部分可以总结,要保证可靠性和流量控制机制,则要求TCP终端为每个数据流初始化并维护某些状态信息。这些状态信息包括:套接口、序列号和窗口大小被称为连接;所以建立一个TCP连接就需要客户端和服务端达成这三个共识;
图中还有一句话:每个连接由标识两端的套接口对唯一指定,套接口对就是IP地址和端口号组成。
这说明TCP四元组能够唯一确定一个连接,四元组如下:
源地址和目的地址(在IP头部中)—— 通过IP协议发生报文给对方主机。
源端口和目的端口(在TCP头部中)—— 通过TCP协议将报文发给对应的进程。
4. TCP如何封装与分用
4位首部长度
4位首部长度:
它表示的是TCP的首部长度,基本单位是4字节 ,能够表示的范围 [ 0000 , 1111 ],当这个字段填入的是 1111 时(及15),那么TCP首部长度为 15 * 4 = 60 字节,TCP首部长度 - TCP标准长度 = 60 - 20 = 40字节,这个就表示选项字段的长度,一般4位首部长度用0101表示(及5),那么TCP首部长度就等于20字节,也就是TCP的标准长度;
如何封装:
将应用层的有效载荷加上TCP的首部长度,交付给下一层;
如何分用:
根据4位首部长度,计算出TCP的标准长度与选项长度,如果TCP的标准长度刚好是20字节,那么剩下的就是有效载荷;如果大于20字节,则减去20字节后,得到选项字段的长度,再读取完选项的长度后,便可得到有效载荷;
二、通过序列号和确认应答号提高可靠性
从上文中TCP的特点中了解到TCP是可靠的,那究竟是如何保证可靠性的呢?其实和TCP报文协议中的32位序列号和32位确认应答号来保证的。
1. 32位序列号
对于32位序列号,TCP将每个字节的数据都进行了编号,即为序列号。从上图可以看出,当我们想要发送比较大的数据时,TCP会将这些数据按字节进行编号,然后按照编号的顺序发送给服务器。这样一来,32位序列号的作用就是能够保证报文的按序到达。
注意:客户端向服务端发送报文时,序列号初始值并非为0,而是随机初始化这个序列号;
2. 32位确认应答号
32位确认应答号主要是用来对历史报文数据的一个响应,以客户端发送报文给服务端为例:当客户端发送报文给服务端时,本质上发送的是序列号,如果发生的序列号为100,当服务端收到客户端发来的序列号后,服务端就会给客户端响应一个报文,报文内部附带着确认应答号,这个确认应答号就是对客户端发来的序列号+1;
3. 保证可靠性
最核心的机制就是基于序号的确认应答机制 ,TCP不能做到100%可靠,但是通过应答机制来确保上一条信息是不是100%被对方收到。只要有一条信息应答了,就可以肯定上一条消息是可靠的被对方收到了。
当客户端向服务端发送了0~100之间的数据时,本质上就是发送了一个报文,如果客户端没有响应,那么客户端就不知道自己发出的数据是否被服务端收到,所有只有当服务端给客户端一个响应后,客户端就知道自己刚刚发出的数据已经被服务端收到了,这个确认应答号就保证了历史报文的可靠性;
对于服务端来说,它既可以对客户端发来的序列号进行响应,也可以向客户端发送序列号,表明自己也有数据要发送,当客户端给出响应后,就能够保证可靠性了;
4. 为什么序列号和确认应答号是单独的字段
我们试想一下,如果TCP的报头将这两个字段设计成了一个字段(我们这里假设为序列号),那么新的TCP头部格式如下:
如下图所示:当客户端向服务端发送数据时,服务端给出响应;当服务端给客户端发送数据时,客户端再给出响应;
这样的效率明显很低。并且TCP是全双工通信协议,双方通信时,一个报文,既可以携带要发送的数据,也可以携带对历史报文的确认;
三、窗口大小
1. TCP的发送和接收缓冲区
TCP本身是具有接收缓冲区和发送缓冲区的:
- 接收缓冲区用来暂时保存接收到的数据。
- 发送缓冲区用来暂时保存还未发送的数据。
- 这两个缓冲区都是在TCP传输层内部实现的。
你可以理解为在TCP内部malloc了2段空间
当客户端的应用层调用write/send时,并不是将数据发送到网络上,而是将应用层的数据拷贝到TCP自带的发送缓冲区中,在已经连接好的客户服务器中,经由网络传输到服务端的接收缓冲取中,服务端的接收缓冲区有数据后,调用read/recv时,是将服务端的接收缓冲区的数据拷贝给应用层;
当数据写入到TCP的发送缓冲区后,对应的write/send函数就可以返回了,至于发送缓冲区当中的数据具体什么时候发,怎么发等问题实际都是由TCP决定的。
我们之所以称TCP为传输层控制协议,就是因为最终数据的发送和接收方式,以及传输数据时遇到的各种问题应该如何解决,都是由TCP自己决定的,用户只需要将数据拷贝到TCP的发送缓冲区,以及从TCP的接收缓冲区当中读取数据即可。
TCP的发送缓冲区和接收缓冲区存在的意义?
- 数据在网络中传输时可能会出现某些错误,此时就可能要求发送端进行数据重传,因此TCP必须提供一个发送缓冲区来暂时保存发送出去的数据,以免需要进行数据重传。只有当发出去的数据被对端可靠的收到后,发送缓冲区中的这部分数据才可以被覆盖掉。
- 接收端处理数据的速度是有限的,为了保证没来得及处理的数据不会被迫丢弃,因此TCP必须提供一个接收缓冲区来暂时保存未被处理的数据,因为数据传输是需要耗费资源的,我们不能随意丢弃正确的报文。此外,TCP的数据重排也是在接收缓冲区当中进行的。
- 因为缓冲区的存在,所以可以做到应用层和TCP进行解耦。
2. 窗口大小
窗口大小反映的就是客户端或服务端发送和接收缓冲区剩余空间的大小,如下图所示,假设客户端和服务端各自的缓冲区只能容纳100字节的数据,当客户端向服务端发送20个字节的数据后,服务端的接收缓冲区的大小就变成了80字节;对于客户端也是一样;
当发送端要将数据发送给对端时,本质是把自己发送缓冲区当中的数据发送到对端的接收缓冲区当中。但缓冲区是有大小的,如果接收端处理数据的速度小于发送端发送数据的速度,那么总有一个时刻接收端的接收缓冲区会被打满,这时发送端再发送数据过来就会造成数据丢包,进而引起丢包重传等一系列的连锁反应。
在TCP报文中有了这个字段后, 接收端在对发送端发来的数据进行响应时,就可以通过16位窗口大小这个字段告知发送端自己当前接收缓冲区剩余空间的大小,此时发送端就可以根据这个窗口大小字段来调整自己发送数据的速度。
- 窗口大小字段越大,说明接收端接收数据的能力越强,此时发送端可以提高发送数据的速度。
- 窗口大小字段越小,说明接收端接收数据的能力越弱,此时发送端可以减小发送数据的速度。
- 如果窗口大小的值为0,说明接收端接收缓冲区已经被打满了,此时发送端就不应该再发送数据了。
四、TCP连接建立
1. TCP三次握手全过程
TCP是面向连接的协议,所以使用TCP进行通信的时候就要先建立连接,而建立连接是通过三次握手来完成的。过程如下:
一开始,客户端和服务器都处于close状态,先是服务端主动监听某个端口,处于Listen状态。client_isn:客户端初始化序列号 server_isn:服务端初始化序列号
三次握手的第一个报文:(客户端发起)
客户端会随机初始化序列号(client_isn),将此序号置于TCP首部序号字段中,同时把SYN标志位置为1,表示SYN报文。接着把第一个SYN报文发生给服务端,表示向服务端发起连接,该报文不包含应用层数据,之后客户端处于SYN_SENT状态。
三次握手的第二个报文:(服务端发起)
服务端收到客户端的 SYN 报文后,首先服务端也随机初始化自己的序号(server_isn),将此序号填入TCP 首部的序号字段中,其次把 TCP 首部的确认应答号字段填入 client_isn + 1, 接着把 SYN 和 ACK 标志位置为 1。最后把该报文发给客户端,该报文也不包含应用层数据,之后服务端处于 SYN-RCVD 状态。
三次握手的第三个报文:(客户端发起)
客户端收到服务端报文后,还要向服务端回应最后一个应答报文,首先该应答报文 TCP 首部 ACK 标志位置为 1 ,其次确认应答号字段填入 server_isn + 1 ,最后把报文发送给服务端,这次报文可以携带客户到服务器的数据,之后客户端处于 ESTABLISHED 状态。
服务器收到客户端的应答报文后,也进入 ESTABLISHED 状态。
一旦完成三次握手,双方都处于ESTABLISHED状态,此时连接就已建立完成,客户端和服务端就可以相互发送数据了。
2. 为什么是三次握手?不是两次、四次?
主要有以下三个原因:
- 三次握手才可以阻止重复历史连接的初始化。
- 三次握手才可以同步双方的初始化序列号。
- 三次握手才可以有效避免SYN攻击和避免资源浪费。
1. 三次握手才可以阻止重复历史连接的初始化
假设有这样一种场景,客户端给服务端发送了一个SYN报文(seq=100),但是这个由于网络波动阻塞了,于是客户端又重新发送了一个新的SYN报文(seq=200),注意不是重传,重传的SYN的序列号是一样的,来看看三次握手是如何阻止历史连接的:
从上图我们可以看出,当客户端发送的SYN报文被网络阻塞后,再次发送新的SYN报文,由于旧的报文比新的报文先抵达服务端,客户端肯定会回一个SYN+ACK报文给客户端,此时客户端就可以根据这个报文来判断这是一个历史连接,由此客户端就会发送一个RST报文,要求断开此次连接(即历史连接)。等新的SYN报文抵达服务端后,才会正式的建立连接。
如果是两次握手,那就不能阻止历史连接,我们先看一下两次握手成功的状态图:
两次我握手成功如上图所示,当服务端收到客户端发来的SYN报文后,就进入了ESTABLISHED状态,这就意味着服务端此时就可以给客户端发送数据了,但是客户端还没有进入ESTABLISHED状态,必须收到服务端的SYN+ACK报文后,才会进入ESTABLISHED状态。
从上图可以看出,当服务端收到第一个SYN报文后(旧的)就已近建立了连接(服务端并不知道这是历史连接),并且在回复给客户端的报文中携带了数据,但是客户端通过收到的SYN+ACK报文,发现这是历史连接;对于客户端来说,它根本来不及在服务端给客户端发送数据前来阻止这个历史连接,导致这个历史连接被创建,服务端白白发送了数据(数据创建是需要时间和空间的);造成资源浪费;只有在收到客户端发来的RST报文后才会断开连接;
因此,要解决这样的问题,客户端就必须在服务端发送数据之前来阻止掉这个历史连接,而要实现这个功能就需要三次握手。
2. 三次握手才可以同步双方的初始化序列号
TCP协议通信的双方,都必须要维护序列号,序列号是实现可靠传输的一个关键因素,其作用如下:
- 接收端可以根据序列号进行重复数据的去重;
- 接收端可以根据序列号按序接受;
- 通过ACK报文中的序列号可以识别发出去的数据包中,那哪些已经被对方收到了;
当客户端给服务端发送SYN(携带着自己的序列号)报文时候,需要服务端回一个ACK应答报文,表明客户端的SYN报文已经成功接收;同样,在这个报文中处理ACK应答号还有服务端自己的序列号(发送的是SYN+ACK报文),也需要客户端回一个ACK应答报文,来确保服务端的SYN被成功接收;这样一来一回就能保证双方的初始化序列号被可靠同步;
从上图可以看出:
四次握手其实也能够可靠的同步双方的初始化序号,但由于第二步和第三步可以优化成一步,所以就成了三次握手。
而两次握手只保证了一方的初始序列号能被对方成功接收,没办法保证双方的初始序列号都能被确认接收。
3. 三次握手才可以避免资源浪费
假设有这样一种场景,服务端收到了大量的SYN报文,客户端对服务端发过来的ACK报文压根不管,因为是两次握手,服务端只要收到一个SYN报文就进入了ESTABLISHED状态,对于服务端来说,一定要为这些连接分配资源,但是资源是有限的;这些连接占用着资源却什么事都不干,完全是浪费资源;
小结:
TCP 建立连接时,通过三次握手能防止历史连接的建立,能减少双方不必要的资源开销,能帮助双方同步初始化序列号。序列号能够保证数据包不重复、不丢弃和按序传输。
不使用两次握手和四次握手的原因:
- 两次握手:无法防止历史连接的建立,会造成双方资源的浪费,也无法可靠的同步双方序列号;
- 四次握手:三次握手就已经理论上最少可靠连接建立,所以不需要使用更多的通信次数。
3. 第一次握手失败,会发生什么?
- 当客户端想和服务端建立 TCP 连接的时候,首先第一个发的就是 SYN 报文,然后进入到 SYN_SENT 状态。
- 在此之后,如果客户端迟迟收不到服务端的 SYN-ACK 报文(第二次握手),就会触发超时重传机制,重传 SYN 报文。
- 不同版本的操作系统可能超时时间不同,这个超时时间是写死在内核里的,Linux预设是1秒。如果想要更改则需要重新编译内核,比较麻烦。
- 当客户端在 1 秒后没收到服务端的 SYN-ACK 报文后,客户端就会重发 SYN 报文,那到底重发几次呢?在 Linux 里,客户端的 SYN 报文最大重传次数由 tcp_syn_retries(客户端SYN包的重试次数)内核参数控制,这个参数是可以自定义的,默认值一般是 6。我们可以通过命令进行查看cat /proc/sys/net/ipv4/tcp_syn_retries
- 通常,第一次超时重传是在 1 秒后,第二次超时重传是在 2 秒,第三次超时重传是在 4 秒后,第四次超时重传是在 8 秒后,第五次是在超时重传 16 秒后。没错,每次超时的时间是上一次的 2 倍。
- 当第五次超时重传后,会继续等待 32 秒,如果服务端仍然没有回应 ACK,客户端就不再发送 SYN 包,然后断开 TCP 连接。
- 所以,总耗时是 1+2+4+8+16+32=63 秒,大约 1 分钟左右
4. 第二次握手失败,会发生什么?
当服务端收到客户端的第一次握手后,就会回 SYN-ACK 报文给客户端,这个就是第二次握手,此时服务端会进入 SYN_RCVD 状态。
第二次握手的 SYN-ACK 报文其实有两个目的 :
- 第二次握手里的 ACK,是对第一次握手的确认报文;
- 第二次握手里的 SYN,是服务端发起建立 TCP 连接的报文;
所以,如果第二次握手丢了,就会发送比较有意思的事情,具体会怎么样呢?
- 因为第二次握手报文里是包含对客户端的第一次握手的 ACK 确认报文,所以,如果客户端迟迟没有收到第二次握手,那么客户端就觉得可能自己的 SYN 报文(第一次握手)丢失了,于是客户端就会触发超时重传机制,重传 SYN 报文。
- 然后,因为第二次握手中包含服务端的 SYN 报文,所以当客户端收到后,需要给服务端发送 ACK 确认报文(第三次握手),服务端才会认为该 SYN 报文被客户端收到了。如果第二次握手丢失了,服务端就收不到第三次握手,于是服务端这边会触发超时重传机制,重传 SYN-ACK 报文。
在 Linux 下,SYN-ACK 报文的最大重传次数由 tcp_synack_retries (服务端SYN+ACK包的重试次数)内核参数决定,默认值是 5。我们可以通过命令进行查看
cat /proc/sys/net/ipv4/tcp_synack_retries
因此,当第二次握手丢失了,客户端和服务端都会重传:
- 客户端会重传 SYN 报文,也就是第一次握手,最大重传次数由 tcp_syn_retries内核参数决定;
- 服务端会重传 SYN-AKC 报文,也就是第二次握手,最大重传次数由 tcp_synack_retries 内核参数决定。
5. 第三次握手失败,会发生什么?
- 客户端收到服务端的 SYN-ACK 报文后,就会给服务端回一个 ACK 报文,也就是第三次握手,此时客户端状态进入到 ESTABLISH 状态。
- 因为这个第三次握手的 ACK 是对第二次握手的 SYN 的确认报文,所以当第三次握手丢失了,如果服务端那一方迟迟收不到这个确认报文,就会触发超时重传机制,重传 SYN-ACK 报文,直到收到第三次握手,或者达到最大重传次数。
- 注意,ACK 报文是不会有重传的,当 ACK 丢失了,应该由服务端重传SYN+ACK。
五、TCP连接断开
1. TCP 四次挥手的过程
TCP 断开连接是通过四次挥手方式。
四次挥手过程如下:
- 客户端打算关闭连接,此时会发送一个FIN报文,之后客户端进入 FIN_WAIT_1 状态。
- 服务端收到FIN报文后,就会向客户端回一个 ACK 应答报文,然后服务端进入 CLOSED_WAIT 状态。
- 客户端收到服务端的 ACK 应答报文后,之后进入 FIN_WAIT_2 状态。
- 等待服务端处理完数据后,也向客户端发送 FIN 报文,之后服务端进入 LAST_ACK 状态。
- 客户端收到服务端的 FIN 报文后,回一个 ACK 应答报文,之后进入 TIME_WAIT 状态
- 服务器收到了 ACK 应答报文后,就进入了 CLOSED 状态,至此服务端已经完成连接的关闭。
- 客户端在经过 2MSL 一段时间后,自动进入 CLOSED 状态,至此客户端也完成连接的关闭。
- 注意:主动关闭连接的,才有 TIME_WAIT 状态。
2. 为什么要四次挥手
首先,断开连接是客户端和服务端协商的一个过程;
- 客户端发送 FIN 报文,想要告诉服务端,我没有任何数据需要请求了,我想和你断开连接;这里表明客户端不会再给服务端发送数据,但是还可以接受数据;
- 而服务端可能还有数据需要处理和发送,等服务端不再发送数据时,才发送 FIN 报文给客户端来表示同意现在关闭连接。
- 从上面过程可知,服务端通常需要等待完成数据的发送和处理,所以服务端的 ACK 和 FIN 一般都会分开发送,从而比三次握手导致多了一次。
- 前两次握手是客户端请求断开连接的,后两次握手是服务器请求断开连接的,因此4次挥手是协商次数最少的;
3. 第一次挥手失败,会发生什么?
- 当客户端调用 close 函数后,就会向服务端发送 FIN 报文,试图与服务端断开连接,此时客户端的连接进入到 FIN_WAIT_1 状态。
- 正常情况下,如果能及时收到服务端的 ACK,则会很快变为 FIN_WAIT_2 状态。
- 如果第一次挥手丢失了,那么客户端迟迟收不到被动方的 ACK 的话,也就会触发超时重传机制,重传 FIN 报文,重发次数由 tcp_orphan_retries 参数控制,默认值是 0,特指 8 次
- 当客户端重传 FIN 报文的次数超过 tcp_orphan_retries 后,就不再发送 FIN 报文,直接进入到 close 状态。
4. 第二次挥手失败,会发生什么?
- 当服务端收到客户端的第一次挥手后,就会先回一个 ACK 确认报文,此时服务端的连接进入到 CLOSE_WAIT 状态。
- 在前面我们也提了,ACK 报文是不会重传的,所以如果服务端的第二次挥手丢失了,客户端就会触发超时重传机制,重传 FIN 报文,直到收到服务端的第二次挥手,或者达到最大的重传次数。
- 这里提一下,当客户端收到第二次挥手,也就是收到服务端发送的 ACK 报文后,客户端就会处于 FIN_WAIT_2 状态,在这个状态需要等服务端发送第三次挥手,也就是服务端的 FIN 报文。
- 对于 close 函数关闭的连接,由于无法再发送和接收数据,所以FIN_WAIT_2 状态不可以持续太久,而 tcp_fin_timeout 控制了这个状态下连接的持续时长,默认值是 60 秒。
- 这意味着对于调用 close 关闭的连接,如果在 60 秒后还没有收到 FIN 报文,客户端的连接就会直接关闭。
5. 第三次挥手失败,会发生什么?
- 当服务端收到客户端的 FIN 报文后,内核会自动回复 ACK,同时连接处于 CLOSE_WAIT 状态,顾名思义,它表示等待应用进程调用 close 函数关闭连接。
- 此时,内核是没有权利替代进程关闭连接,必须由进程主动调用 close 函数来触发服务端发送 FIN 报文。
- 服务端处于 CLOSE_WAIT 状态时,调用了 close 函数,内核就会发出 FIN 报文,同时连接进入 LAST_ACK 状态,等待客户端返回 ACK 来确认连接关闭。
- 如果迟迟收不到这个 ACK,服务端就会重发 FIN 报文,重发次数仍然由 tcp_orphan_retries 参数控制,这与客户端重发 FIN 报文的重传次数控制方式是一样的。
6. 第四次挥手失败,会发生什么?
- 当客户端收到服务端的第三次挥手的 FIN 报文后,就会回 ACK 报文,也就是第四次挥手,此时客户端连接进入 TIME_WAIT 状态。
- 在 Linux 系统,TIME_WAIT 状态会持续 2MSL 后才会进入关闭状态。
- 然后,服务端没有收到 ACK 报文前,还是处于 LAST_ACK 状态。
- 如果第四次挥手的 ACK 报文没有到达服务端,服务端就会重发 FIN 报文,重发次数仍然由前面介绍过的 tcp_orphan_retries 参数控制。
7. TIME_WAIT状态和CLOSED_WAIT状态
1. TIME_WAIT状态
①为什么 TIME_WAIT 等待的时间是 2MSL?
- MSL 是 Maximum Segment Lifetime,报文最大生存时间,它是任何报文在网络上存在的最长时间,超过这个时间报文将被丢弃。因为 TCP 报文基于是 IP 协议的,而 IP 头中有一个 TTL 字段,是 IP 数据报可以经过的最大路由数,每经过一个处理他的路由器此值就减 1,当此值为 0 则数据报将被丢弃,同时发送 ICMP 报文通知源主机。
- MSL 与 TTL 的区别: MSL 的单位是时间,而 TTL 是经过路由跳数。所以 MSL 应该要大于等于 TTL 消耗为 0 的时间,以确保报文已被自然消亡。
- TTL 的值一般是 64,Linux 将 MSL 设置为 30 秒,意味着 Linux 认为数据报文经过 64 个路由器的时间不会超过 30 秒,如果超过了,就认为报文已经消失在网络中了。
- TIME_WAIT 等待 2 倍的 MSL,比较合理的解释是: 网络中可能存在来自发送方的数据包,当这些发送方的数据包被接收方处理后又会向对方发送响应,所以一来一回需要等待 2 倍的时间。
- 比如,如果被动关闭方没有收到断开连接的最后的 ACK 报文,就会触发超时重发 FIN 报文,另一方接收到 FIN 后,会重发 ACK 给被动关闭方, 一来一去正好 2 个 MSL。
- 可以看到 2MSL时长 这其实是相当于至少允许报文丢失一次。比如,若 ACK 在一个 MSL 内丢失,这样被动方重发的 FIN 会在第 2 个 MSL 内到达,TIME_WAIT 状态的连接可以应对。
- 为什么不是 4 或者 8 MSL 的时长呢?你可以想象一个丢包率达到百分之一的糟糕网络,连续两次丢包的概率只有万分之一,这个概率实在是太小了,忽略它比解决它更具性价比。
- 2MSL 的时间是从客户端接收到 FIN 后发送 ACK 开始计时的。如果在 TIME-WAIT 时间内,因为客户端的 ACK 没有传输到服务端,客户端又接收到了服务端重发的 FIN 报文,那么 2MSL 时间将重新计时。
- 在 Linux 系统里 2MSL 默认是 60 秒,那么一个 MSL 也就是 30 秒。Linux 系统停留在 TIME_WAIT 的时间为固定的 60 秒。
②为什么需要TIME_WAIT状态?
主动发起断开连接的一方才会有 TIME_WAIT 状态,它是四次挥手的最后一个状态。它的出现,就是为了解决网络的丢包和网络不稳定所带来的其他问题。
需要 TIME_WAIT 状态有以下两个原因:
- 防止历史连接中因网络延迟的数据包或者丢失重传的数据包,被新的连接(与历史连接的端口号相同)复用;
- 保证被动关闭连接的一方,能够正确关闭;
1. 防止历史连接中因网络延迟的数据包或者丢失重传的数据包,被新的连接(与历史连接的端口号相同)复用;
- 服务端向客户端发送了 Seq=300 的报文和 Seq=301 的报文,因为是处于连接状态的,此次报文必然是夹带着数据的,由于网络原因,导致 Seq=301 的报文阻塞在网络中;
- 紧接着,服务端以相同的四元组重新打开了新连接,前面被延迟的 Seq=301 这时抵达了客户端,而且该数据报文的序列号刚好在客户端接收窗口内,因此客户端会正常接收这个数据报文,但是这个数据报文是上一个连接残留下来的,这样就产生数据错乱等严重的问题。
- 为了防止历史连接中的数据,被后面相同四元组的连接错误的接收,因此 TCP 设计了 TIME_WAIT 状态,状态会持续 2MSL 时长,这个时间足以让两个方向上的数据包都被丢弃,使得原来连接的数据包在网络中都自然消失,再出现的数据包一定都是新建立连接所产生的
2. 确保连接方能在时间范围内,关闭自己的连接;
- 客户端在进行四次挥手后进入TIME_WAIT 状态,如果第四次挥手的报文丢包了,客户端在一段时间内仍然能够接收服务器重发的 FIN 报文并对其进行响应,能够较大概率保证最后一个ACK被服务器收到。
2. CLOSED_WAIT状态
- 双方在进行四次挥手时,如果只有客户端调用了close函数,而服务器不调用close函数,此时服务器就会进入CLOSE_WAIT状态,而客户端则会进入到FIN_WAIT_2状态。
- 但只有完成四次挥手后连接才算真正断开,此时双方才会释放对应的连接资源。如果服务器没有主动关闭不需要的文件描述符,此时在服务器端就会存在大量处于CLOSE_WAIT状态的连接,而每个连接都会占用服务器的资源,最终就会导致服务器可用资源越来越少。
- 因此如果不及时关闭不用的文件描述符,除了会造成文件描述符泄漏以外,可能也会导致连接资源没有完全释放,这其实也是一种内存泄漏的问题。
- 因此在编写网络套接字代码时,如果发现服务器端存在大量处于CLOSE_WAIT状态的连接,此时就可以检查一下是不是服务器没有及时调用close函数关闭对应的文件描述符。