⭐小白苦学IT的博客主页⭐
⭐初学者必看:Linux操作系统入门⭐
⭐代码仓库:Linux代码仓库⭐
❤关注我一起讨论和学习Linux系统
前言
TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。它是互联网协议族(TCP/IP协议族)中的重要组成部分,旨在适应支持多网络应用的分层协议层次结构。在不同的计算机通信网络的主计算机之间,TCP为各种进程提供可靠的通信服务。
TCP协议的重要性在于其保证了数据在传输过程中的可靠性、顺序性和无差错。它通过一系列复杂的机制,如三次握手建立连接、四次挥手断开连接、数据校验和、确认应答、超时重传、去重、流量控制和拥塞控制等,来确保数据的正确传输。这些机制共同协作,使得TCP成为了一种非常强大且健壮的传输协议。
在本文中,我们将深入探讨TCP协议的各个方面,包括其报文格式、连接管理、数据传输、可靠性保障以及性能优化等。通过对TCP协议的详细解析,读者将能够更深入地理解其工作原理和内部机制,从而更好地应用和优化基于TCP的网络应用。
TCP协议段格式
- 源端口号(Source Port):16位,用于标识发送端应用程序的端口号。每个应用程序在发送数据时,都会通过特定的端口号进行标识,以便接收端能够正确识别和处理数据。
- 目的端口号(Destination Port):16位,用于标识接收端应用程序的端口号。当接收端收到数据时,会根据目的端口号将数据分发给对应的应用程序。
- 序列号(Sequence Number):32位,用于标识发送的数据段的序列号。TCP协议通过序列号对发送的数据进行编号,以确保数据的顺序性和完整性。
- 确认号(Acknowledgment Number):32位,用于标识期望收到的下一个数据段的序列号。接收端在收到数据后,会发送一个确认号给发送端,告诉发送端下一个应该发送的数据段的序列号。
- 数据偏移(Data Offset):4位,用于标识TCP头部长度。这个字段实际上表示的是TCP首部占用的32位字的数目,也就是有多少个4字节。通过数据偏移,我们可以知道TCP头部的长度,进而确定数据部分的起始位置。
- 保留位(Reserved):6位,这些位目前是保留的,用于将来可能的扩展。在当前的TCP协议中,这些位通常被设置为0。
- 6位标志位(Flags):URG表示紧急指针字段有效;ACK表示确认字段有效;PSH表示接收方应该尽快将这个报文段交给应用层;RST表示连接重置;SYN表示同步序号;FIN表示发送方已经没有数据要发送了,即发送完成,释放连接。
- 窗口大小(Window Size):16位,用于实现TCP的流量控制。发送方会根据接收方的窗口大小来确定一次可以发送多少数据,以避免接收方缓冲区溢出。
- 校验和(Checksum):16位,用于检验TCP头部和数据的完整性。在发送数据时,发送方会计算校验和并附加在数据后面;接收方在收到数据后,会重新计算校验和并与发送方发送的校验和进行比较,以判断数据是否在传输过程中被修改。
- 紧急指针(Urgent Pointer):16位,只有在URG标志位为1时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据放在本报文段数据部分的最前面)。
- 选项(Options)和填充(Padding):长度可变。选项字段用于支持TCP的各种选项功能,例如最大传输单元大小、窗口扩大因子、时间戳等。填充字段用于保证TCP头部长度是4字节的整数倍。
确认应答(ACK)机制
TCP协议的确认应答(ACK)机制是一种保证数据可靠传输的重要机制。在TCP协议中,当一方发送数据包给另一方时,接收方必须发送一个确认应答包,以确保数据包已经被正确接收。这种机制对于确保数据的完整性和顺序性至关重要。
TCP将每个字节的数据都进行了编号. 即为序列号.
每一个ACK都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据; 下一次你从哪里开始发.
ACK机制的实现依赖于TCP头部中的ACK标志位。当接收方收到数据包后,会将ACK标志位设置为1,并将确认号设置为已经接收到的数据包的序列号加1。这个确认号是对接收到的数据的最高序列号的确认,并向发送端返回一个下次接收时期望的TCP数据包的序列号。发送方在收到这个确认包后,会继续发送后续的数据包。
以一个简单的例子来说明这个过程:假设主机A向主机B发送一个TCP数据包,数据序号是1,数据长度是1000字节。主机B在成功接收到这个数据包后,会发送一个确认应答给主机A,确认号设置为1001,表示已经成功接收到序号为1至1000的字节。主机A在收到这个确认应答后,就可以继续发送序号为1001及以后的数据包。
通过这种方式,TCP协议可以在通信过程中保证数据的可靠传输。如果发送方在一定时间内没有收到接收方的确认应答,就会认为数据包可能已经丢失,从而进行重传。这种机制大大提高了数据传输的可靠性,使得TCP协议成为互联网上广泛应用的一种传输协议。
TCP协议的确认应答(ACK)机制通过发送确认应答包来确保数据的可靠传输,是TCP协议实现可靠通信的重要保障之一。
超时重传机制
TCP协议的超时重传机制是确保数据在网络传输过程中可靠性的重要手段。当TCP发送端发送一个数据段后,它会启动一个计时器并等待接收端的确认应答(ACK)。如果在计时器设定的时间内没有收到ACK,发送端会认为这个数据段可能已经丢失或者在网络中遇到了问题,然后触发超时重传机制,重新发送这个数据段。
- 主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B;
- 如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发;
但是, 主机A未收到B发来的确认应答, 也可能是因为ACK丢失了;
因此主机B会收到很多重复数据. 那么TCP协议需要能够识别出那些包是重复的包, 并且把重复的丢弃掉.
这时候我们可以利用前面提到的序列号 , 就可以很容易做到去重的效果.
如何通过序列号来进行去重呢?
在TCP通讯中,每个数据包都会被分配一个唯一的序列号。这个序列号在数据包的头部中标识,用于确保接收端能够按照正确的顺序重组数据,并检测和丢弃重复的数据包。
当发送端发送一个数据包时,它会在数据包头部中为该数据包分配一个序列号。接收端在收到数据包后,会检查该数据包的序列号。如果接收端发现已经收到过相同序列号的数据包,那么它会认为这是一个重复的数据包,并将其丢弃。通过这种方式,TCP协议能够确保每个数据包只被处理一次,从而避免了重复数据对传输的影响。
此外,接收端还会通过确认序列号来告知发送端它已经成功接收到的数据包的序列号。发送端在收到确认序列号后,会根据该序列号来确定哪些数据包已经成功传输,从而避免重新发送这些数据包。
那么超时的时间如何确定呢?
这个超时时间并不是固定的,而是根据网络状况动态调整的。TCP协议会根据往返时间(RTT,Round-Trip Time)来计算并调整这个超时时间。如果网络状况良好,RTT较小,那么超时时间也会相应较短;反之,如果网络状况较差,RTT较大,那么超时时间就会相应延长。这样可以更好地适应不同的网络环境,确保数据的可靠传输。
现在,让我用一个例子来帮助你更好地理解这个机制。假设你正在通过网络向你的朋友发送一个大型文件,这个文件被TCP协议分割成了多个数据段进行传输。当TCP发送端发送了某个数据段后,它会等待接收端的确认应答。
然而,在这个例子中,网络突然出现了波动,导致发送端在设定的时间内没有收到确认应答。这时,TCP的超时重传机制就会被触发。发送端会重新发送那个没有收到确认应答的数据段,并再次启动计时器等待确认。
如果这次网络状况好转,接收端成功收到了重传的数据段并发送了确认应答,那么发送端就会继续发送下一个数据段。如果仍然没有收到确认应答,发送端会在新的超时时间后再次重传这个数据段,如此往复,直到收到确认应答或者达到一定的重传次数限制为止。
通过这种方式,TCP协议能够确保即使在网络不稳定的情况下,数据也能尽可能地可靠传输。当然,这种机制也可能在网络状况非常恶劣的情况下导致传输效率下降,但这是为了保证数据的完整性和正确性所必须付出的代价。
- 最理想的情况下, 找到一个最小的时间, 保证 "确认应答一定能在这个时间内返回".
- 但是这个时间的长短, 随着网络环境的不同, 是有差异的.
- 如果超时时间设的太长, 会影响整体的重传效率;
- 如果超时时间设的太短, 有可能会频繁发送重复的包;
TCP为了保证无论在任何环境下都能比较高性能的通信, 因此会动态计算这个最大超时时间.
- Linux中(BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍.
- 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
- 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
- 累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接.
连接管理机制
在正常情况下, TCP要经过三次握手建立连接, 四次挥手断开连接
三次握手的过程是这样的:
- 客户端向服务器发送一个SYN(同步)包,包含自身的数据序列号,请求建立连接。
- 服务器收到SYN包后,回复一个SYN+ACK(同步应答)包给客户端,确认客户端的SYN并包含自身的数据序列号。
- 客户端收到SYN+ACK包后,向服务器发送一个ACK(应答)包,确认服务器的SYN。
通过这三次握手,客户端和服务器就建立了一个可靠的TCP连接,双方都可以开始传输数据。这个过程中,双方都会发送应答来确认对方的请求,从而避免了数据包的丢失和乱序问题。
而四次挥手的过程则是为了断开这个连接:
- 客户端向服务器发送一个FIN(结束)数据包,主动断开连接。
- 服务器收到FIN包后,发送一个ACK包给客户端,确认收到FIN包。
- 服务器在发送完所有数据后,向客户端发送一个FIN包,请求关闭连接。
- 客户端收到FIN包后,发送一个ACK包给服务器,确认收到FIN包,此时连接关闭。
这四次挥手确保了连接的双方都能正常地结束数据传输,并释放相关的资源。在四次挥手的过程中,如果有一方没有收到对方的确认信息,它会重新发送相关的数据包,直到收到确认为止。这种机制保证了数据传输的可靠性和完整性。
为什么是三次握手,而不是一次或者两次呢?
TCP协议之所以需要三次握手,而不是两次或一次,主要是为了确保连接的可靠性和安全性。
首先,如果只有一次握手,那么客户端发送连接请求后,由于没有收到服务器的确认,客户端无法确定服务器是否收到了请求,也就无法确定连接是否建立成功。这种情况下,数据传输的可靠性无法得到保障。
其次,如果是两次握手,虽然客户端能够收到服务器的确认,但服务器无法确认客户端是否真正收到了自己的确认。这种情况下,如果因为网络拥堵等原因导致客户端的请求丢失,客户端可能会重复发送请求,服务器也会重复确认,从而造成资源的浪费。更严重的是,如果客户端的第一次请求因为某种原因延迟到达服务器,而客户端已经因为超时重传了请求并得到了服务器的确认,这时如果服务器接受了第一次的请求,就会导致历史连接的延续,可能会给网络带来安全隐患。
而三次握手则可以确保双方都准备好进行通信,并且能够确认对方的接收能力。在第三次握手时,客户端发送确认报文给服务器,服务器收到后才能确认客户端已经准备好,从而建立连接。这样的过程既确保了连接的可靠性,又避免了资源的浪费和安全隐患。
此外,TCP协议是面向连接的,它要求双方都是全双工的,即任何一端既是发送数据方,又是接收数据方。这就要求双方既要保证自己的发送能力,又要保证自己的接收能力。通过三次握手,可以确保双方都具备发送和接收数据的能力,从而进行有效的通信。
起始状态:CLOSED
- 这是TCP连接的初始状态,表示当前没有活动的连接。
客户端主动打开连接
从CLOSED到SYN_SENT
- 客户端调用connect函数,向服务器发送一个SYN(同步)报文段,请求建立连接。此时,客户端进入SYN_SENT状态,等待服务器的确认。
服务器端被动打开连接
从CLOSED到LISTEN
服务器端调用socket和bind函数创建套接字并绑定到特定端口,然后调用listen函数开始监听来自客户端的连接请求。此时,服务器进入LISTEN状态,等待客户端的连接。从LISTEN到SYN_RECEIVED
当服务器收到客户端发来的SYN报文段后,会发送一个SYN+ACK报文段作为应答,表示同意建立连接,并等待客户端的确认。此时,服务器进入SYN_RECEIVED状态。连接建立后的状态转换
从SYN_SENT到ESTABLISHED
当客户端收到服务器的SYN+ACK报文段后,会发送一个ACK报文段作为确认,表示已经准备好进入数据传输阶段。此时,客户端进入ESTABLISHED状态。从SYN_RECEIVED到ESTABLISHED
当服务器收到客户端的ACK报文段后,也进入ESTABLISHED状态,此时连接建立完成,双方可以进行数据的传输。数据传输阶段
- 在ESTABLISHED状态下,客户端和服务器可以进行双向的数据传输。
主动关闭连接
从ESTABLISHED到FIN_WAIT_1
客户端决定关闭连接时,会发送一个FIN(结束)报文段给服务器,然后进入FIN_WAIT_1状态,等待服务器的确认。从FIN_WAIT_1到FIN_WAIT_2
当客户端收到服务器发来的ACK报文段后,进入FIN_WAIT_2状态,等待服务器发送FIN报文段。从FIN_WAIT_2到TIME_WAIT
客户端收到服务器发来的FIN报文段后,发送一个ACK报文段给服务器作为确认,然后进入TIME_WAIT状态,等待一段时间以确保服务器收到ACK报文段并关闭连接。从TIME_WAIT到CLOSED
在TIME_WAIT状态等待一段时间后,客户端进入CLOSED状态,连接完全关闭。被动关闭连接
从ESTABLISHED到CLOSE_WAIT
当服务器收到客户端发来的FIN报文段后,进入CLOSE_WAIT状态,等待本地应用层关闭连接。从CLOSE_WAIT到LAST_ACK
当服务器决定关闭连接时,发送一个FIN报文段给客户端,然后进入LAST_ACK状态,等待客户端的确认。从LAST_ACK到CLOSED
当服务器收到客户端发来的ACK报文段后,进入CLOSED状态,连接完全关闭。
下图是TCP状态转换的一个汇总:
- 较粗的虚线表示服务端的状态变化情况;
- 较粗的实线表示客户端的状态变化情况 ;
- CLOSED是一个假想的起始点, 不是真实状态;
理解半关闭状态
半关闭是TCP协议中的一种状态,它指的是TCP连接的一端在发送完数据后,调用shutdown操作来关闭数据发送通道,但数据接收通道仍然保持打开的状态。这种状态下,连接的一端可以继续接收来自另一端的数据,但不能再发送数据。半关闭状态主要用于实现数据传输的异步性,使得一方可以在发送完数据后不再发送,但仍能接收对方发送过来的数据。
在实际应用中,半关闭状态可以很好地完成发送端对接收端的数据发送完的提醒,并且不影响发送端接收对端的结果回应。例如,当发送方发现数据到达文件尾时,即没有数据发送了,这时发送方发送一个FIN来通知接收方数据已经发送完了,接收方收到FIN后,向发送方回复一个ACK表示收到状态,此时发送方进入了半关闭状态,但还可以接收对方发送的数据。
需要注意的是,半关闭状态并不意味着TCP连接已经完全关闭,只有当双方都完成了关闭操作,连接才真正结束。此外,如果一方在处于半关闭状态时异常崩溃或断开连接,可能会导致另一方无法及时感知到连接状态的变化,因此需要引入心跳机制等策略来检测和处理这种情况。
我们用一个男女朋友分手的故事来理解半关闭状态
假设小明和小芳是一对男女朋友,他们之间的感情逐渐出现了问题,最终决定分手。在这个例子中,我们可以将分手过程与TCP的半关闭状态进行类比。
当小明决定结束这段关系时,他向小芳表达了自己的决定,告诉她他不再愿意继续这段感情了。这就像TCP连接中的发送端(小明)发送了一个FIN包,表示它完成了数据的发送,并希望关闭连接。
小芳收到小明的决定后,她可能会感到伤心、失望,但她仍然需要时间来处理这个分手的事实。在这个阶段,小芳仍然可以和小明交流,尽管她已经知道小明不再愿意继续这段关系。这就像TCP连接中的接收端(小芳)在收到FIN包后,发送了一个ACK包确认收到,并继续保持接收通道打开,以便接收可能还存在的任何数据或消息。
然而,需要注意的是,尽管小芳仍然保持着接收通道的打开状态,但小明已经关闭了发送通道,他不再主动向小芳发送任何消息或表达任何情感。这就像TCP连接中的发送端在发送FIN包后进入了半关闭状态,它不能再发送数据,但可以继续接收数据。
最终,当小芳也准备好结束这段关系,并告诉小明她的决定时,这就相当于TCP连接中的接收端也发送了一个FIN包,双方都完成了关闭操作,连接正式结束。
通过这个例子,我们可以看到半关闭状态在情感关系中的体现。一方可能决定结束关系,但另一方可能还需要时间来接受和处理这个事实。在这个过程中,尽管发送端已经停止了进一步的情感表达,但接收端仍然可以接收到对方可能存在的消息或回应。这与TCP连接中的半关闭状态是类似的,它允许连接的一端在关闭发送通道的同时,仍然保持接收通道的打开状态,以实现异步的数据传输和处理。
理解CLOSING状态
CLOSING状态是TCP协议中的一个重要状态,它发生在双方几乎同时尝试关闭连接的情况下。换句话说,当TCP连接的双方几乎在同一时刻都发送了FIN报文,表示它们都想要结束这个连接时,TCP连接就会进入CLOSING状态。
为了更好地理解CLOSING状态,我们可以借助一个日常生活中的故事来进行类比。
想象这样一个场景:小明和小芳是一对恋人,他们因为各种原因决定分手。在一个晴朗的午后,两人坐在公园的长椅上,几乎同时向对方说出了“我们分手吧”这句话。在这个时刻,小明和小芳都表达了结束关系的意愿,但他们的决定几乎同时发生,导致双方都处于一种暂时的“僵持”状态,即都等待对方的回应来确认这个决定。
将这个场景与TCP的CLOSING状态进行类比,小明和小芳的“几乎同时说出分手”就相当于TCP连接中双方几乎同时发送FIN报文的情况。在这个状态下,双方都表达了关闭连接的意愿,但连接并没有立即断开,而是进入了一个暂时的、等待对方确认的状态。这就像小明和小芳在等待对方对分手决定的确认一样,TCP连接也在等待双方对关闭操作的确认。
一旦双方都收到了对方的确认(在TCP中是通过ACK报文来确认的),连接就会正式关闭,就像小明和小芳在相互确认后正式分手一样。在这个过程中,CLOSING状态起到了一个过渡的作用,它确保了双方都能正确地感知到对方的关闭意愿,并避免了因为网络延迟或丢包等原因导致的连接状态不一致的问题。
通过这个故事,我们可以更好地理解TCP的CLOSING状态:它发生在双方几乎同时尝试关闭连接的情况下,是一种暂时的、等待确认的状态。这种状态确保了TCP连接的可靠关闭,避免了资源泄露和连接状态不一致的问题。
理解TIME_WAIT状态
做一个测试看现象
现在做一个测试,首先启动tcpserver,然后用telnet来与tcpserver建立连接,然后用Ctrl-C使tcpserver终止,这时马上再运行tcpserver, 结果是:
这是因为,虽然tcpserver的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监 听同样的tcpserver端口.
我们用netstat命令查看一下:
- TCP协议规定,主动关闭连接的一方要处于TIME_ WAIT状态,等待两个MSL(maximum segment lifetime)的时间后才能回到CLOSED状态.
- 我们使用Ctrl-C终止了server, 所以server是主动关闭连接的一方, 在TIME_WAIT期间仍然不能再次监听同样的server端口;
- MSL在RFC1122中规定为两分钟,但是各操作系统的实现不同, 在Centos7上默认配置的值是60s;
- 可以通过 cat /proc/sys/net/ipv4/tcp_fin_timeout 查看msl的值;
想一想, 为什么是TIME_WAIT的时间是2MSL?
- MSL是TCP报文的最大生存时间, 因此TIME_WAIT持续存在2MSL的话就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失(否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的);
- 同时也是在理论上保证最后一个报文可靠到达(假设最后一个ACK丢失, 那么服务器会再重发一个FIN. 这时虽然客户端的进程不在了, 但是TCP连接还在, 仍然可以重发LAST_ACK);
TIME_WAIT状态的主要作用
- 等待可能被延迟的最后一个ACK:在TCP连接关闭过程中,发送方需要等待接收方发送的最后一个确认(ACK)报文段。如果由于某种原因,这个ACK被延迟了,发送方不会误认为连接已经关闭。TIME_WAIT状态确保了发送方在等待一段时间后才关闭连接,从而避免了这种情况。
- 确保网络中所有数据包都已被接收方确认:在TCP连接关闭时,可能还有一些数据包在网络中传输,这些数据包可能会因为各种原因而延迟到达接收方,或者在网络中丢失。TIME_WAIT状态确保了发送方在等待一段时间后关闭连接,这样可以确保接收方已经收到了所有数据包,并向发送方发送了确认。
解决TIME_WAIT状态引起的bind失败的方法
在server的TCP连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的
服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短, 但是每秒都有很大数量的客户端来请求).
这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃, 就需要被服务器端主动清理掉), 就会产生大量TIME_WAIT连接.
由于我们的请求量很大, 就可能导致TIME_WAIT的连接数很多, 每个连接都会占用一个通信五元组(源ip,源端口, 目ip, 目的端口 , 协议). 其中服务器的ip和端口和协议是固定的. 如果新来的客户端连接的ip和端口号和TIME_WAIT占用的链接重复了, 就会出现问题.
使用setsockopt()设置socket描述符的 选项SO_REUSEADDR为1, 表示允许创建端口号相同但IP地址不同的多个socket描述符
理解 CLOSE_WAIT 状态
如果服务器出现大量的CLOSE_WAIT状态,这通常意味着服务器没有正确关闭TCP连接,可能是因为服务器端的程序存在bug,或者在处理完数据后没有主动调用close()或shutdown()来关闭连接。此外,IP异常或网络问题也可能导致连接中断,使服务器进入CLOSE_WAIT状态。
下面我们就以下面的代码为例服务器做一个不主动调用close()的测试:
#pragma once
#include"Log.hpp"
#include"Socket.hpp"
#include<signal.h>
#include<functional>
using func_t = std::function<std::string (std::string & package)>;
class TcpServer
{
public:
TcpServer(){}
TcpServer(const uint16_t & port,func_t fun):_port(port),callback_(fun)
{}
bool ServerInit()
{
_listensock.Socket();
_listensock.Bind(_port);
_listensock.Listen();
log.LogMessage(INFO,"server init ... done");
return true;
}
void Start()
{
signal(SIGCHLD,SIG_IGN);
signal(SIGPIPE,SIG_IGN);
while(true)
{
std::string serverip;
uint16_t serverport;
int sockfd = _listensock.Accept(&serverip,&serverport);
if(sockfd<0)
{
continue;
}
log.LogMessage(INFO,"accept a new link,sockfd: %d, clientip: %s, clientport: %d",sockfd,serverip.c_str(),serverport);
//提供服务
if(fork() == 0)
{
_listensock.Close();
std::string inbuffer_stream;
//数据计算
while(true)
{
char buffer[128];
ssize_t n = read(sockfd,buffer,sizeof(buffer));
if(n>0)
{
buffer[n] = 0;
inbuffer_stream+=buffer;
log.LogMessage(DEBUG,"\n%s ",inbuffer_stream.c_str());
std::string info = callback_(inbuffer_stream);
if(info.empty()) continue;
write(sockfd,info.c_str(),info.size());
}
else if(n == 0) break;
else break;
}
exit(0);
}
//把close(sockfd)去掉
//close(sockfd);
}
}
~TcpServer(){}
private:
uint16_t _port;
Sock _listensock;
func_t callback_;
};
我们编译运行服务器. 启动客户端链接, 查看 TCP 状态, 客户端服务器都为 ESTABLELISHED 状态, 没有问题.
然后我们关闭客户端程序, 观察 TCP 状态
此时服务器进入了 CLOSE_WAIT 状态, 结合我们四次挥手的流程图, 可以认为四次挥手没有正确完成.
CLOSE_WAIT状态是TCP连接中的一个关键状态,主要出现在服务端或客户端的TCP连接过程中。这个状态的含义是一方(通常是客户端)已经发送了FIN数据包,表示希望关闭连接,但另一方(通常是服务器端)还没有发送FIN数据包,即还没有确认关闭连接。
具体来说,当客户端决定关闭连接并发送FIN数据包给服务器时,服务器接收到这个请求后会发送一个ACK数据包作为确认,然后服务器进入CLOSE_WAIT状态。在这个状态下,服务器等待本地应用层关闭连接,也就是说,虽然服务器已经知道了客户端希望关闭连接的请求,但服务器本身还没有决定关闭连接,或者还在处理一些未完成的任务,如发送剩余的数据给客户端。
CLOSE_WAIT状态的主要作用是等待服务器或对方连接端的响应。如果服务器处理完所有事务并准备关闭连接,它会发送一个FIN数据包给客户端,然后进入LAST_ACK状态,等待客户端的确认。如果服务器在CLOSE_WAIT状态下长时间没有响应或处理完所有事务,这可能会导致资源泄露和连接问题。
小结: 对于服务器上出现大量的 CLOSE_WAIT 状态, 原因就是服务器没有正确的关闭 socket, 导致四次挥手没有正确完成. 这是一个 BUG. 只需要加上对应的 close 即可解决问题。