TCP/IP协议族中的TCP(一):解析其关键特性与机制

小白苦学IT的博客主页⭐


 

初学者必看:Linux操作系统入门⭐


 

代码仓库:Linux代码仓库⭐


 

❤关注我一起讨论和学习Linux系统

前言

TCP(Transmission Control Protocol,传输控制协议)是一种面向连接的、可靠的、基于字节流的传输层通信协议。它是互联网协议族(TCP/IP协议族)中的重要组成部分,旨在适应支持多网络应用的分层协议层次结构。在不同的计算机通信网络的主计算机之间,TCP为各种进程提供可靠的通信服务。

TCP协议的重要性在于其保证了数据在传输过程中的可靠性、顺序性和无差错。它通过一系列复杂的机制,如三次握手建立连接、四次挥手断开连接、数据校验和、确认应答、超时重传、去重、流量控制和拥塞控制等,来确保数据的正确传输。这些机制共同协作,使得TCP成为了一种非常强大且健壮的传输协议。

在本文中,我们将深入探讨TCP协议的各个方面,包括其报文格式、连接管理、数据传输、可靠性保障以及性能优化等。通过对TCP协议的详细解析,读者将能够更深入地理解其工作原理和内部机制,从而更好地应用和优化基于TCP的网络应用。

TCP协议段格式

  1. 源端口号(Source Port):16位,用于标识发送端应用程序的端口号。每个应用程序在发送数据时,都会通过特定的端口号进行标识,以便接收端能够正确识别和处理数据。
  2. 目的端口号(Destination Port):16位,用于标识接收端应用程序的端口号。当接收端收到数据时,会根据目的端口号将数据分发给对应的应用程序。
  3. 序列号(Sequence Number):32位,用于标识发送的数据段的序列号。TCP协议通过序列号对发送的数据进行编号,以确保数据的顺序性和完整性。
  4. 确认号(Acknowledgment Number):32位,用于标识期望收到的下一个数据段的序列号。接收端在收到数据后,会发送一个确认号给发送端,告诉发送端下一个应该发送的数据段的序列号。
  5. 数据偏移(Data Offset):4位,用于标识TCP头部长度。这个字段实际上表示的是TCP首部占用的32位字的数目,也就是有多少个4字节。通过数据偏移,我们可以知道TCP头部的长度,进而确定数据部分的起始位置。
  6. 保留位(Reserved):6位,这些位目前是保留的,用于将来可能的扩展。在当前的TCP协议中,这些位通常被设置为0。
  7. 6位标志位(Flags):URG表示紧急指针字段有效;ACK表示确认字段有效;PSH表示接收方应该尽快将这个报文段交给应用层;RST表示连接重置;SYN表示同步序号;FIN表示发送方已经没有数据要发送了,即发送完成,释放连接。
  8. 窗口大小(Window Size):16位,用于实现TCP的流量控制。发送方会根据接收方的窗口大小来确定一次可以发送多少数据,以避免接收方缓冲区溢出。
  9. 校验和(Checksum):16位,用于检验TCP头部和数据的完整性。在发送数据时,发送方会计算校验和并附加在数据后面;接收方在收到数据后,会重新计算校验和并与发送方发送的校验和进行比较,以判断数据是否在传输过程中被修改。
  10. 紧急指针(Urgent Pointer):16位,只有在URG标志位为1时才有意义,它指出本报文段中的紧急数据的字节数(紧急数据放在本报文段数据部分的最前面)。
  11. 选项(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要经过三次握手建立连接, 四次挥手断开连接

三次握手的过程是这样的:

  1. 客户端向服务器发送一个SYN(同步)包,包含自身的数据序列号,请求建立连接。
  2. 服务器收到SYN包后,回复一个SYN+ACK(同步应答)包给客户端,确认客户端的SYN并包含自身的数据序列号。
  3. 客户端收到SYN+ACK包后,向服务器发送一个ACK(应答)包,确认服务器的SYN。

通过这三次握手,客户端和服务器就建立了一个可靠的TCP连接,双方都可以开始传输数据。这个过程中,双方都会发送应答来确认对方的请求,从而避免了数据包的丢失和乱序问题。

而四次挥手的过程则是为了断开这个连接:

  1. 客户端向服务器发送一个FIN(结束)数据包,主动断开连接。
  2. 服务器收到FIN包后,发送一个ACK包给客户端,确认收到FIN包。
  3. 服务器在发送完所有数据后,向客户端发送一个FIN包,请求关闭连接。
  4. 客户端收到FIN包后,发送一个ACK包给服务器,确认收到FIN包,此时连接关闭。

这四次挥手确保了连接的双方都能正常地结束数据传输,并释放相关的资源。在四次挥手的过程中,如果有一方没有收到对方的确认信息,它会重新发送相关的数据包,直到收到确认为止。这种机制保证了数据传输的可靠性和完整性。

为什么是三次握手,而不是一次或者两次呢?

TCP协议之所以需要三次握手,而不是两次或一次,主要是为了确保连接的可靠性和安全性。

首先,如果只有一次握手,那么客户端发送连接请求后,由于没有收到服务器的确认,客户端无法确定服务器是否收到了请求,也就无法确定连接是否建立成功。这种情况下,数据传输的可靠性无法得到保障。

其次,如果是两次握手,虽然客户端能够收到服务器的确认,但服务器无法确认客户端是否真正收到了自己的确认。这种情况下,如果因为网络拥堵等原因导致客户端的请求丢失,客户端可能会重复发送请求,服务器也会重复确认,从而造成资源的浪费。更严重的是,如果客户端的第一次请求因为某种原因延迟到达服务器,而客户端已经因为超时重传了请求并得到了服务器的确认,这时如果服务器接受了第一次的请求,就会导致历史连接的延续,可能会给网络带来安全隐患。

而三次握手则可以确保双方都准备好进行通信,并且能够确认对方的接收能力。在第三次握手时,客户端发送确认报文给服务器,服务器收到后才能确认客户端已经准备好,从而建立连接。这样的过程既确保了连接的可靠性,又避免了资源的浪费和安全隐患。

此外,TCP协议是面向连接的,它要求双方都是全双工的,即任何一端既是发送数据方,又是接收数据方。这就要求双方既要保证自己的发送能力,又要保证自己的接收能力。通过三次握手,可以确保双方都具备发送和接收数据的能力,从而进行有效的通信。

起始状态:CLOSED

  • 这是TCP连接的初始状态,表示当前没有活动的连接。

客户端主动打开连接

  1. 从CLOSED到SYN_SENT

  • 客户端调用connect函数,向服务器发送一个SYN(同步)报文段,请求建立连接。此时,客户端进入SYN_SENT状态,等待服务器的确认。

服务器端被动打开连接

  1. 从CLOSED到LISTEN

    服务器端调用socket和bind函数创建套接字并绑定到特定端口,然后调用listen函数开始监听来自客户端的连接请求。此时,服务器进入LISTEN状态,等待客户端的连接。
  2. 从LISTEN到SYN_RECEIVED

    当服务器收到客户端发来的SYN报文段后,会发送一个SYN+ACK报文段作为应答,表示同意建立连接,并等待客户端的确认。此时,服务器进入SYN_RECEIVED状态。

连接建立后的状态转换

  1. 从SYN_SENT到ESTABLISHED

    当客户端收到服务器的SYN+ACK报文段后,会发送一个ACK报文段作为确认,表示已经准备好进入数据传输阶段。此时,客户端进入ESTABLISHED状态。
  2. 从SYN_RECEIVED到ESTABLISHED

    当服务器收到客户端的ACK报文段后,也进入ESTABLISHED状态,此时连接建立完成,双方可以进行数据的传输。

数据传输阶段

  • 在ESTABLISHED状态下,客户端和服务器可以进行双向的数据传输。

主动关闭连接

  1. 从ESTABLISHED到FIN_WAIT_1

    客户端决定关闭连接时,会发送一个FIN(结束)报文段给服务器,然后进入FIN_WAIT_1状态,等待服务器的确认。
  2. 从FIN_WAIT_1到FIN_WAIT_2

    当客户端收到服务器发来的ACK报文段后,进入FIN_WAIT_2状态,等待服务器发送FIN报文段。
  3. 从FIN_WAIT_2到TIME_WAIT

    客户端收到服务器发来的FIN报文段后,发送一个ACK报文段给服务器作为确认,然后进入TIME_WAIT状态,等待一段时间以确保服务器收到ACK报文段并关闭连接。
  4. 从TIME_WAIT到CLOSED

    在TIME_WAIT状态等待一段时间后,客户端进入CLOSED状态,连接完全关闭。

被动关闭连接

  1. 从ESTABLISHED到CLOSE_WAIT

    当服务器收到客户端发来的FIN报文段后,进入CLOSE_WAIT状态,等待本地应用层关闭连接。
  2. 从CLOSE_WAIT到LAST_ACK

    当服务器决定关闭连接时,发送一个FIN报文段给客户端,然后进入LAST_ACK状态,等待客户端的确认。
  3. 从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状态的主要作用

  1. 等待可能被延迟的最后一个ACK:在TCP连接关闭过程中,发送方需要等待接收方发送的最后一个确认(ACK)报文段。如果由于某种原因,这个ACK被延迟了,发送方不会误认为连接已经关闭。TIME_WAIT状态确保了发送方在等待一段时间后才关闭连接,从而避免了这种情况。
  2. 确保网络中所有数据包都已被接收方确认:在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 即可解决问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/pingmian/4353.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

牛客社区帖子分页显示实现

下图是前端分页的组件&#xff1a; 下面是对应的静态html页面&#xff0c;每一个方块&#xff0c;都是一个a标签&#xff0c;可以点击&#xff0c;执行的链接是/community/index&#xff0c;GET请求&#xff0c;拼接的参数是current&#xff0c;也就是pageNum&#xff0c;只需…

力扣HOT100 208. 实现Trie(前缀树)

解题思路&#xff1a; class Trie {private Trie[] children; // 存储子节点的数组private boolean isEnd; // 记录是否为单词结尾public Trie() {children new Trie[26]; // 数组大小为26&#xff0c;代表26个小写字母isEnd false;}public void insert(String word) {Trie …

智能小程序 Ray 开发实践——基础内容组件 Text 和 Icon 介绍

Text 文本内容。 导入 import { Text } from ray-js/ray; Props 属性类型默认值说明支持平台classNamestring样式名涂鸦、微信selectablebooleanfalse文本是否可选涂鸦、微信onClick(e: { type: click }) > voidfalse点击事件涂鸦、微信 示例代码 基本使用 import Re…

【yolov8算法道路-墙面裂缝检测-汽车车身凹陷-抓痕-损伤检测】

yolo算法道路-墙面裂缝检测-汽车车身凹陷-抓痕-损伤检测 1. yolo算法裂缝检测-汽车车身凹陷-抓痕检测-汽车车身损伤检测2. yolo房屋墙面路面裂缝-发霉-油漆脱落-渗水-墙皮脱落检测3. 水泥墙面裂缝检测 YOLOv8算法是一种先进的目标检测技术&#xff0c;它基于YOLO系列算法的改进…

探索矿业数字化平台:实现智能化采矿与管理

随着信息技术的迅猛发展&#xff0c;矿业领域也在逐步实现数字化转型。数字化平台的出现为矿业企业带来了更高效、更智能的采矿与管理方式。本文将探讨矿业数字化平台的意义、特点以及未来发展方向。 ### 1. 数字化平台的意义 传统的矿业生产和管理方式存在诸多问题&#xff…

Python赋值运算符

目录 赋值运算符 将值赋给变量&#xff1a; 做加法运算之后完成赋值&#xff1a; 做减法运算之后完成赋值&#xff1a;- 做乘法运算之后完成赋值&#xff1a;* 做除法运算之后完成赋值&#xff1a;/ 做整除运算之后完成赋值&#xff1a;// 做幂次运算之后完成赋值&#xff1a;*…

Pytorch 计算深度模型的大小

计算模型大小的方法 卷积 时间复杂度 与 空间复杂度 的计算方式&#xff1a; C 通道的个数&#xff0c;K卷积核大小&#xff0c;M特征图大小&#xff0c;C_l-1是输入通道的个数&#xff0c;C_l是输出通道的个数 1 模型大小 MB 计算模型的大小的原理就是计算保存模型所需要…

在MySQL中isnull()函数不能作为替代null值!

在MySQL中isnull()函数不能作为替代null值&#xff01; 如下&#xff1a; 首先有个名字为business的表&#xff1a; SELECT ISNULL(business_name,no business_name) AS bus_isnull FROM business WHERE id2 直接运行就会报错&#xff1a; 错误代码&#xff1a; 1582 Incor…

cuDNN-Graph API

Graph API 为了适应越来越重要的算子融合需求&#xff0c;cuDNN8.0版本引入了Graph API&#xff0c;以提供更灵活的API接口。Graph API提供一个声明式的编程模型&#xff0c;此模型将计算操作描述为计算图。 用户首先需要构建操作图。从高层面来说&#xff0c;用户其实是在描…

Swift - Playground

文章目录 Swift - Playground1. 新建Playground2. View3. 图片4. ViewController5. Playground - 多Page6. 注释6.1 Playground的注释支持markup语法&#xff08;与markdown相似&#xff09;6.1.1 语法 Swift - Playground Playground可以快速预览代码效果&#xff0c;是学习语…

设计模式(九):组合模式

设计模式&#xff08;九&#xff09;&#xff1a;组合模式 1. 组合模式的介绍2. 组合模式的类图3. 组合模式的实现 1. 组合模式的介绍 组合模式&#xff08;Composite Pattern&#xff09;属于结构型模式&#xff0c;是用于把一组相似的对象当作一个单一的对象。 组合模式依据…

基于SpringBoot+Vue校园二手交易系统的设计与实现

系统介绍 自从新冠疫情爆发以来&#xff0c;各个线下实体越来越难做&#xff0c;线下购物的人也越来越少&#xff0c;随之带来的是一些不必要的浪费&#xff0c;尤其是即将毕业的大学生&#xff0c;各种用品不方便携带走导致被遗弃&#xff0c;造成大量的浪费。本系统目的就是让…

基于Spring Boot的旅游管理系统设计与实现

基于Spring Boot的旅游管理系统设计与实现 开发语言&#xff1a;Java框架&#xff1a;springbootJDK版本&#xff1a;JDK1.8数据库工具&#xff1a;Navicat11开发软件&#xff1a;eclipse/myeclipse/idea 系统部分展示 前台浏览管理界面图&#xff0c;通过内容列表可以获取网…

PotatoPie 4.0 实验教程(41) —— FPGA实现RISC-V 扩展 GPIO UART Timer功能

TD工程介绍 我们提供的TD工程里的RISC-V核默认就开启了GPIO UART扩展&#xff0c;可以看到还有SPI和I2C扩展。因此后面的实验中TD的工程我们基本不怎么修改TD的内容&#xff0c;只需要修改TD工具中Soc_Top.v文件中的TCM0_INITFILE为FD生成的固件名称即可&#xff0c;主要修我以…

【mysql】mysql命令使用大全,你想要的都在这里

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

go设计模式之工厂方法模式

工厂方法模式 什么是工厂方法模式 工厂方法模式是一种创建型设计模式&#xff0c;它定义了一个用于创建对象的接口&#xff0c;让子类决定实例化哪一个类。工厂方法使一个类的实例化推迟到其子类。 这个接口就是工厂接口&#xff0c;子类就是具体工厂类&#xff0c;而需要创…

《QT实用小工具·四十三》历史编辑器(支持历史搜索 关键字匹配)

1、概述 源码放在文章末尾 该项目实现了在输入框中输入部分信息能全部展现之前的历史输入信息&#xff0c;支持历史搜索和关键词匹配&#xff0c;项目demo演示如下所示&#xff1a; 项目部分代码如下所示&#xff1a; #include "historymodel.h" #include <QM…

实时通讯技术 WebRTC 介绍

WebRTC WebRTC&#xff08;Web Real-Time Communication&#xff09;是一个支持网页浏览器进行实时语音对话或视频对话的技术。 历史 2010年5月&#xff0c;Google以6820万美元收购VoIP软件开发商Global IP Solutions的GIPS引擎&#xff0c;并改为名为“WebRTC”。WebRTC使用…

大语言模型Ollama

版权声明 本文原创作者&#xff1a;谷哥的小弟作者博客地址&#xff1a;http://blog.csdn.net/lfdfhl Ollama简介 Ollama是一个开源的大语言模型平台&#xff0c;它允许用户在本地环境中运行、创建和共享大型语言模型。Ollama提供了丰富的功能和特性&#xff0c;使得用户可以…

解决 Tomcat 跨域问题 - Tomcat 配置静态文件和 Java Web 服务(Spring MVC Springboot)同时允许跨域

解决 Tomcat 跨域问题 - Tomcat 配置静态文件和 Java Web 服务&#xff08;Spring MVC Springboot&#xff09;同时允许跨域 Tomcat 配置允许跨域Web 项目配置允许跨域Tomcat 同时允许静态文件和 Web 服务跨域 偶尔遇到一个 Tomcat 部署项目跨域问题&#xff0c;因为已经处理过…