传输层——TCP协议

目录

TCP协议

TCP协议段格式

确认应答机制(ACK)

序号与确认序号

32位序号

32位确认序号

确认应答(ACK)机制

16位窗口大小

六个标志位

超时重传机制

连接管理机制

三次握手

四次挥手

理解CLOSE_WAIT状态

理解TIME_WAIT状态

​编辑流量控制

滑动窗口

拥塞控制

延迟应答

捎带应答

面向字节流 

粘包问题

TCP异常情况 

TCP小结

基于TCP应用层协议


TCP协议

TCP(Transmission Control Protocol,传输控制协议)是互联网中最广泛使用的传输层协议。它的广泛应用源于其提供的高可靠性。TCP通过复杂的机制确保数据在传输过程中的完整性和顺序性,避免数据丢失或重复。此外,TCP还具有流量控制和拥塞控制功能,有助于优化网络资源的使用。

基于TCP的上层应用协议众多,其中包括HTTP(用于网页浏览)、HTTPS(用于安全网页浏览)、FTP(用于文件传输)、SSH(用于安全远程登录)等。此外,即使是像MySQL这样的数据库系统,其底层通信也依赖于TCP协议。

TCP比UDP可靠,为什么还要UDP协议呢?

尽管TCP协议提供了强大的数据可靠性保证,但UDP协议仍然有其存在的必要性和应用场景。以下是UDP协议相较于TCP协议的一些优势:

  1. 简单性:UDP协议相较于TCP协议更为简单,没有建立连接、断开连接等复杂的过程。这使得UDP协议在处理一些简单的通信需求时更为高效。

  2. 传输速度快:由于UDP协议没有TCP协议中的拥塞控制和流量控制机制,因此在处理大量数据时,UDP协议的传输速度更快。此外,UDP协议的数据包头部开销较小,也进一步提高了传输效率。

  3. 支持广播和多播:UDP协议支持广播和多播,这使得它在一些特定的应用场景中更具优势,如网络广播、实时音视频传输等。

  4. 低资源消耗:由于UDP协议没有TCP协议中的连接管理、重传等机制,因此在处理大量数据时,UDP协议对系统资源的消耗较小。

因此,尽管TCP协议提供了强大的数据可靠性保证,但UDP协议仍然有其独特的优势和应用场景。在实际应用中,需要根据具体的需求和场景选择合适的协议。

TCP协议段格式

TCP报头当中各个字段的含义如下:

  • 源/目的端口号:表示数据是从哪个进程来,到发送到对端主机上的哪个进程。
  • 32位序号/32位确认序号:分别代表TCP报文当中每个字节数据的编号以及对对方的确认,是TCP保证可靠性的重要字段。
  • 4位TCP报头长度:表示该TCP报头的长度,以4字节为单位。
  • 6位保留字段:TCP报头中暂时未使用的6个比特位。
  • 16位窗口大小:保证TCP可靠性机制和效率提升机制的重要字段。
  • 16位检验和:由发送端填充,采用CRC校验。接收端校验不通过,则认为接收到的数据有问题。(检验和包含TCP首部+TCP数据部分)
  • 16位紧急指针:标识紧急数据在报文中的偏移量,需要配合标志字段当中的URG字段统一使用。
  • 选项字段:TCP报头当中允许携带额外的选项字段,最多40字节。

TCP报头当中的6位标志位:

  • URG:紧急指针是否有效。
  • ACK:确认序号是否有效。
  • PSH:提示接收端应用程序立刻将TCP接收缓冲区当中的数据读走。
  • RST:表示要求对方重新建立连接。我们把携带RST标识的报文称为复位报文段。
  • SYN:表示请求与对方建立连接。我们把携带SYN标识的报文称为同步报文段。
  • FIN:通知对方,本端要关闭了。我们把携带FIN标识的报文称为结束报文段。

TCP报头在内核当中本质就是一个位段类型,给数据封装TCP报头时,实际上就是用该位段类型定义一个变量,然后填充TCP报头当中的各个属性字段,最后将这个TCP报头拷贝到数据的首部,至此便完成了TCP报头的封装。

TCP如何将报头与有效载荷进行分离?

TCP将报头与有效载荷进行分离的过程可以概括为以下步骤:

  1. TCP从底层获取到一个报文后,首先读取报文的前20个字节,这20个字节是TCP报文头的基本长度。
  2. 在这20个字节中,TCP提取出4位的首部长度字段,这个字段表示了整个TCP报文头的长度。
  3. TCP根据这个长度信息计算出整个报文头的实际大小。如果报文头的长度大于20字节,TCP会继续读取额外的字节,这些字节构成了报文头的选项字段。
  4. 一旦TCP读取了完整的报文头,剩下的数据就是有效载荷了。TCP将这部分数据交付给上层协议或应用程序进行处理。

需要注意的是,TCP报文头中的4位首部长度字段是以4字节为单位进行描述的,因此其取值范围是从0到15,对应的报文头长度范围是0到60字节。由于基本报文头长度是20字节,所以选项字段的最大长度是40字节。

TCP如何决定将有效载荷交付给上层的哪一个协议?

TCP的报头中涵盖了目的端口号,TCP可以提取出报头中的目的端口号,找到对应的应用层进程,进而将有效载荷交给对应的应用层进程进行处理。

确认应答机制(ACK)

序号与确认序号

网络通信中,数据的传输并不总是能够保证成功到达对方主机。由于网络的不稳定性、信号干扰、设备故障等多种原因,数据可能会在传输过程中出现错误或丢失。

因此,可靠通信需要发送方和接收方之间的双向确认,以确保数据完整无误地到达目的地。

但TCP要保证的是双方通信的可靠性,虽然此时主机A能够保证自己上一次发送的数据被主机B可靠的收到了,但主机B也需要保证自己发送给主机A的响应数据被主机A可靠的收到了。因此主机A在收到了主机B的响应消息后,还需要对该响应数据进行响应,但此时又需要保证主机A发送的响应数据的可靠性…,这样就陷入了一个死循环。

因为只有当一端收到对方的响应消息后,才能保证自己上一次发送的数据被对端可靠的收到了,但双方通信时总会有最新的一条消息,因此无法百分之百保证可靠性。

所以互联网通信不可能达到百分之百的可靠性,因为总是存在最新一条消息无法得到回应的可能性。然而,在实际应用中,我们并不需要追求所有消息的绝对可靠性。关键在于确保通信双方发送的核心数据都能得到相应的确认。至于那些非关键性的数据,如响应数据,其可靠性并非必须。如果接收方未收到这些响应数据,它会认为先前的报文已丢失,并会重新发送该数据。因此,在互联网通信中,合理的做法是优先确保重要数据的可靠性,而对于非重要数据,则允许一定程度的容错性。

这种策略在TCP当中就叫做确认应答机制。需要注意的是,确认应答机制不是保证双方通信的全部消息的可靠性,而是只要一方收到了另一方的应答消息,就说明它上一次发送的数据被另一方可靠的收到了。

32位序号

为了确保数据的可靠传输,通常需要在数据传输后收到对方主机的确认(ACK)消息。如果双方在进行数据通信时,只有收到了上一次发送数据的响应才能发下一个数据,那么此时双方的通信过程就是串行的,效率是非常低下的。

因此双方在进行网络通信时,允许一方向另一方连续发送多个报文数据,只要保证发送的每个报文都有对应的响应消息就行了,此时也就能保证这些报文被对方收到了。

在连续发送多个报文时,每个报文可能选择不同的网络路径进行传输,导致它们到达接收端主机的顺序与发送时的顺序不一致。然而,报文的有序性被视为通信可靠性的一个重要方面。为了解决这一问题,TCP报头中引入了32位序号,其目的之一就是为了确保报文的有序性。通过为每个报文分配唯一的序号,并在接收端根据这些序号对报文进行排序,可以确保数据按照正确的顺序被接收和处理。这种机制有助于保证在连续通信过程中数据的完整性和准确性。 

TCP将每个字节的数据都进行了编号。即为序列号

  • 当发送端需要传输3000字节的数据时,如果每次发送的数据块大小为1000字节,那么整体数据将被划分为三个TCP报文进行发送。这三个TCP报文中的32位序号代表的是每个报文所携带数据中的首个字节在整体数据中的序列号。
  • 因此,第一个报文的序号被设置为1,表示它携带的数据从原始数据的第1个字节开始;第二个报文的序号设为1001,表示它携带的数据从原始数据的第1001个字节开始;第三个报文的序号则是2001,代表其携带的数据从原始数据的第2001个字节开始。

当接收端成功接收到这三个TCP报文后,它会在传输层根据每个报文头部中的32位序列号对这些报文进行排序。这个重排过程确保了报文按照正确的顺序被放置在TCP的接收缓冲区中

通过这一机制,接收端能够确保接收到的报文顺序与发送端发送的顺序完全一致,从而保证了数据的完整性和准确性。这个过程是TCP协议确保可靠传输的关键环节之一。

接收端在重新排列报文时,可以通过分析当前接收到的报文的32位序号及其所携带的有效数据长度(即有效载荷的字节数),来精准预测并确认下一个应该接收的报文序号。

32位确认序号

TCP报头中的32位确认序号是一个重要的机制,它告诉发送方接收方已经成功接收了哪些数据,并指导发送方下一次应该从哪里开始发送数据。

以之前的例子来说,当主机B接收到主机A发送的序号为1的TCP报文时,这意味着主机B已经成功接收了从序列号1开始到1000字节的数据。为了确认这一点并指导主机A的后续发送,主机B在返回给主机A的响应报文的TCP报头中填入32位确认序号为1001。

  • 这个确认序号不仅表示主机B已经接收到了序列号为1000之前的所有数据
  • 而且还指示主机A下一次发送数据时应该从序列号为1001的字节开始。

主机B在对其他来自主机A的报文进行响应时,也会采用类似的方式来填写32位确认序号,确保数据传输的准确性和连续性。这种机制是TCP协议中实现可靠数据传输的关键部分。

响应数据与其他数据一样,也是一个完整的TCP报文,尽管该报文可能不携带有效载荷,但至少是一个TCP报头。

报文丢失的情况

  • 在刚才的例子中,主机A发送了三个TCP报文给主机B,每个报文的有效载荷为1000字节,且它们的32位序号分别为1、1001和2001。然而,在网络传输过程中,由于某些原因,序号为1001的报文丢失了,只有序号为1和2001的报文成功到达了主机B。
  • 当主机B收到这些报文后,它会在传输层对报文进行顺序重排。在这个过程中,主机B会发现它只接收到了从序列号1到1000以及从序列号2001到3000的字节数据,而序列号1001到2000的数据缺失了。
  • 为了通知主机A这一缺失,并指示主机A从缺失的数据部分开始重新发送,主机B在发送给主机A的响应报文中,会在TCP报头中填写32位确认序号为1001。这样,主机A就知道从序列号1001开始的数据没有被主机B成功接收,因此在下次发送数据时,主机A会从序列号1001开始重新发送丢失的部分,确保数据的完整性和连续性。

主机B在给主机A发送确认响应时,32位确认序号不能填为3001,因为这样会让主机A误以为所有到3001之前的数据都已成功接收,包括实际上丢失的1001到2000字节的数据。为了准确告知主机A数据丢失的情况,主机B应当发送确认序号为1001的响应。这样做,主机A会意识到序号为1001的报文丢失了,并据此选择重传这部分数据。

也就是说发送端可以根据对端发来的确认序号,来判断是否某个报文可能在传输过程中丢失了。

为什么要用两套序号机制?

  • TCP通信机制之所以在报头中采用两套序号(一套用于发送数据的序号,另一套用于确认对方发送数据的序号),并非因为简单的单工通信只需要一套序号。根本原因在于TCP是支持全双工通信的协议,这意味着通信的双方在任何时刻都可以同时发送和接收数据。
  • 在TCP通信过程中,发送端使用32位序号来标识当前发送的数据位置,确保数据的顺序性和连续性。同时,接收端也使用32位序号作为确认序号,来告诉发送端已成功接收了哪些数据,并指示发送端下一次发送数据应从哪个字节序号开始。这种机制确保了数据传输的可靠性和准确性。
  • 由于TCP通信是全双工的,双方可能同时发起数据传输,因此仅依赖一套序号无法满足这种复杂场景的需求。为了避免序号冲突和混乱,TCP报头中设计了两套序号,分别用于发送和确认应答,从而确保了双方通信的顺利进行。

小结:

  • 32位序号在TCP通信中不仅是确保数据按序到达的关键,同时也是对端在发送报文时用来填充32位确认序号的依据。
  • 而32位确认序号则扮演着告知对方当前接收状态的角色。它告诉对端哪些字节的数据已经被成功接收,并指示对端下一次发送数据时应该从哪个字节序号开始。
  • 序号和确认序号共同构成了TCP的确认应答机制的数据化表示。正是通过这种机制,双方能够实时掌握数据传输的状态,确保数据的完整性和准确性。同时,通过比较序号和确认序号,还可以判断某个报文是否丢失,从而采取相应的补救措施。

确认应答(ACK)机制

TCP报头中的32位序号和32位确认序号构成了确认应答机制。确认应答(ACK)机制是TCP协议中确保数据可靠传输的关键机制。当发送端发送数据后,会等待接收端的确认应答。一旦收到确认应答,发送端就知道数据已经成功到达接收端。

发送端在发送数据时,会在TCP报头中填入序号,这个序号表示发送数据的首个字节在发送缓冲区中的位置。接收端在收到数据后,会回复一个ACK报文,其中包含一个确认序号,这个确认序号是接收缓冲区中最后一个有效数据的下一个位置所对应的下标。发送端收到这个ACK后,就知道哪些数据已经被接收端成功接收,并可以从下一个序号继续发送数据。

这种确认应答机制不仅保证了数据的可靠传输,还通过序号管理实现了数据的按序到达。即使在传输过程中出现丢包或乱序,TCP也能通过重传和序号调整来恢复正常的通信状态。

16位窗口大小

TCP的接收缓冲区和发送缓冲区

TCP协议在传输层内部实现了接收缓冲区和发送缓冲区这两个关键组件。

  • 接收缓冲区负责暂时存储接收到的数据,以确保数据的完整性和有序性;
  • 而发送缓冲区则负责暂存还未发出的数据,以便于数据的顺畅传输和流量控制。

  • TCP协议在其发送缓冲区中管理着待发送的数据。这些数据并非直接由应用层写入网络,而是当应用层通过write/send等系统调用接口发送数据时,数据实际上是从应用层拷贝到TCP的发送缓冲区中。
  • 同样地,TCP接收缓冲区中的数据也并非直接由应用层从网络中读取。当应用层调用read/recv等系统调用接口来接收数据时,数据是从TCP的接收缓冲区拷贝到应用层的。
  • 这个过程与文件读写操作中的缓冲区机制类似。调用read和write进行文件读写时,数据并不是直接从磁盘读取或写入,而是与文件缓冲区进行交互。TCP的发送和接收缓冲区在数据传输中扮演着类似的角色,它们提高了数据传输的效率,并确保了数据的完整性和顺序性。

当数据被写入TCP的发送缓冲区后,write/send函数会立即返回,而数据实际发送的具体时机和方式则完全由TCP协议本身来控制。这意味着用户无需关心数据如何被拆分、打包、传输以及何时发送等细节,这些复杂的传输过程均由TCP负责处理。

TCP之所以被称为传输层控制协议,是因为它负责决定数据的发送和接收策略,以及在传输过程中遇到的各种问题(如丢包、乱序、拥塞等)的应对策略。用户只需要将数据写入发送缓冲区,并从接收缓冲区读取数据,无需深入了解底层网络传输的细节。这种机制简化了应用程序的编写,同时也保证了数据传输的可靠性和效率。

注意:通信双方的TCP层都是一样的,因此通信双方的TCP层都是既有发送缓冲区又有接收缓冲区。 

TCP的发送缓冲区和接收缓冲区的作用

发送缓冲区和接收缓冲区在TCP传输过程中发挥着不可或缺的作用。

  • 在网络传输中,由于各种因素可能导致数据出错,因此TCP需要发送缓冲区来暂存已发送但尚未确认的数据。只有当这些数据被对端成功接收并确认后,发送缓冲区中的对应部分才会被覆盖或释放。这样的机制确保了数据传输的可靠性,使得在必要时可以进行数据重传。
  • 与此同时,接收缓冲区负责暂时保存到达但尚未被上层应用处理的数据。这是因为接收端处理数据的速度可能有限,缓冲区的作用在于防止数据的丢失和确保数据的有序性。此外,TCP的数据重排也是在接收缓冲区中进行的,确保了数据的完整性和顺序性。
  • 从生产者与消费者的角度看,发送缓冲区和接收缓冲区的运作可以被类比为经典的生产者消费者模型。对于发送缓冲区,上层应用充当生产者的角色,不断向缓冲区添加数据;而底层网络层则扮演消费者的角色,从缓冲区中取出数据进行传输。相反,在接收缓冲区中,底层网络层是生产者,将接收到的数据放入缓冲区;而上层应用则是消费者,从缓冲区中取出数据进行处理。
  • 这种生产者消费者模型的引入不仅实现了上层应用与底层通信细节的解耦,还支持了并发操作和处理能力的差异。无论是上层应用的快速数据产生还是底层网络层的数据接收速率不均,这种模型都能有效地平衡数据的流动,确保数据的稳定传输和处理。

窗口大小 

发送端在准备向对端发送数据时,其实质上是将其发送缓冲区中的数据传送到对端的接收缓冲区。然而,缓冲区空间有限,当接收端处理数据的速度跟不上发送端发送数据的速度时,接收端的接收缓冲区可能很快被填满。一旦缓冲区满溢,继续发送的数据将会导致数据丢包,进而触发丢包重传等一系列连锁反应。

为了避免这种情况,TCP协议在报头中引入了16位的窗口大小字段。这个窗口大小字段表示的是接收端当前接收缓冲区中剩余的空间大小,即接收端当前能够接收数据的能力。

当接收端对发送端发送的数据进行响应时,它会通过这16位窗口大小字段告知发送端当前接收缓冲区剩余的空间大小。发送端在接收到这个信息后,就可以根据窗口大小字段的值来调整自己的发送速率,确保发送的数据不会超出接收端接收缓冲区的处理能力,从而避免数据丢包和不必要的重传。这种机制有效地平衡了发送和接收之间的速度差异,保证了数据传输的稳定性和效率。

  • 窗口大小字段越大,说明接收端接收数据的能力越强,此时发送端可以提高发送数据的速度。
  • 窗口大小字段越小,说明接收端接收数据的能力越弱,此时发送端可以减小发送数据的速度。
  • 如果窗口大小的值为0,说明接收端接收缓冲区已经被打满了,此时发送端就不应该再发送数据了。

下面我们通过一个现象来理解

  • 在TCP套接字编程中,当我们调用read/recv函数尝试从套接字读取数据时,如果此时TCP的接收缓冲区中没有数据可供读取,程序将会进入阻塞状态。这种阻塞实际上是等待接收缓冲区中有数据可读的状态。
  • 同样地,当我们调用write/send函数向套接字写入数据时,如果TCP的发送缓冲区已满,无法容纳更多的数据,那么程序也会进入阻塞状态。这是因为发送缓冲区已经写满,需要等待缓冲区中有空间可用才能继续写入。
  • 从生产者消费者模型的角度来看,这种情况可以类比为:如果生产者(即发送数据的程序)在生产数据时被阻塞,或者消费者(即接收数据的程序)在消费数据时被阻塞,那么这通常是因为某些必要条件尚未满足,如缓冲区满或空,导致生产或消费操作无法继续进行。换句话说,程序的阻塞是由于等待某些资源或条件就绪。

六个标志位

标志位存在的意义

TCP协议中,报文的种类丰富多样,涵盖了正常通信、建立连接以及断开连接等多种场景。每当收到不同种类的报文时,系统需要执行相应的操作。例如,正常通信的报文需要被放置到接收缓冲区中,等待上层应用程序进行读取和处理。而建立和断开连接的报文则不需要直接交给用户处理,它们需要操作系统在TCP层执行相应的握手和挥手动作,以建立或终止通信会话。

为了区分这些不同种类的报文,TCP协议在报头中使用了六个标志字段这六个标志字段各自只占用一个比特位,其值为0时表示否定或未设置状态,值为1时表示肯定或已设置状态。

因此,标志位存在的意义:区分tcp报文的类型!!!

#1.SYN

  • 用于建立连接。在TCP三次握手过程中,SYN标志用于发起连接请求。当SYN标志为1时,表示这是一个连接请求报文。

#2. ACK

  • 表示确认序号是否有效。当ACK标志为1时,表示报文段中包含有效的确认号,用于确认已成功接收到对方发送的数据。这是TCP中可靠传输的重要机制之一,确保发送端知道数据已被正确接收。

#3.PSH(推送)

  • 指示接收端应用程序应尽快从TCP接收缓冲区中读取数据,为接收后续数据腾出空间。这有助于减少数据在接收端的延迟,提高数据传输的效率。

我们认为使用read/recv从缓冲区读取数据时,如果缓冲区有数据,这些函数会立即返回数据。若缓冲区无数据,read/recv通常会阻塞,等待数据到达。

但实际上,接收和发送缓冲区都有一个“水位线”的概念,这决定了何时开始读取或发送数据,以及何时可能会阻塞。

例如100字节。只有当接收缓冲区中的数据量达到或超过这个水位线时,read/recv函数才会读取并返回数据。这是为了避免频繁的读取和返回操作,提高数据读取效率。因此,read/recv并非只要接收缓冲区有数据就立即返回,而是需要数据量达到水位线后才会进行读取。

当TCP报文的PSH标志被设置为1时,它告诉对方操作系统应尽快将接收缓冲区中的数据传递给上层应用,即使数据量尚未达到预设的水位线。这解释了为什么使用read/recv函数读取数据时,期望读取与实际读取的字节数可能不一致。

#4. RST(复位)

  • 用于请求重新建立连接。当TCP连接中出现错误或异常情况时,发送RST标志被设置为1的报文可以终止当前连接,并允许双方重新建立连接。

#5. URG(紧急指针):

  • 用于将输入数据标识为“紧急”。如果设置了URG标志,TCP报文的紧急指针字段会指示紧急数据的末尾位置。这样,接收端可以优先处理紧急数据,而无需等待其他非紧急数据被处理。

在双方进行网络通信时,由于TCP协议确保了数据的按序到达,发送端可以将待发送的数据分割成多个TCP报文进行传输。这些报文在到达接收端时,会通过TCP的序号机制进行顺序重排,从而确保数据在到达对端接收缓冲区时保持有序。

然而,在某些情况下,发送端可能需要发送一些“紧急数据”,这些数据需要被接收端的上层应用尽快处理。

在这种情况下,TCP协议提供了相应的机制来处理这些紧急数据:URG标志位,以及TCP报头当中的16位紧急指针。

当URG标志位被设置为1时,需要通过TCP报头当中的16位紧急指针来找到紧急数据,否则一般情况下不需要关注TCP报头当中的16位紧急指针。
16位紧急指针代表的就是紧急数据在报文中的偏移量。

recv函数的flags参数中,MSG_OOB选项用于读取带外数据(即重要数据)。当上层需要读取紧急数据时,可以在调用recv函数时设置MSG_OOB选项来实现。

send函数的flags参数中的MSG_OOB选项用于发送紧急数据。当上层需要发送这类数据时,可以使用send函数并设置MSG_OOB选项。

#6. FIN(结束):

  • 用于结束一个TCP会话。当一方希望关闭连接时,会发送带有FIN标志的报文,通知对方本端已经完成了数据发送,准备释放连接。

超时重传机制

双方在进行网络通信时,发送方发出去的数据在一个特定的事件间隔内如果得不到对方的应答,此时发送方就会进行数据重发,这就是TCP的超时重传机制。

TCP确保通信可靠性的方法不仅体现在其协议报头的设计上,还通过代码逻辑来实现。例如,当发送方发送数据后,会启动一个定时器等待确认。若超时未收到确认,则会重传数据。这种超时重传机制是TCP代码逻辑的一部分,不会在TCP报头中直接体现。

丢包的两种情况

  • 主机A发送数据给B之后,可能因为网络拥堵等原因,数据无法到达主机B;

如果主机A在一个特定时间间隔内没有收到B发来的确认应答,就会进行重发;

  • 主机A未收到B发来的确认应答, 也可能是因为ACK丢失了; 

此时发送端也会因为收不到对应的响应报文,而进行超时重传。 

  • 当出现丢包时,发送方无法确定是发送的数据丢失还是响应丢失,都会触发超时重传。
  • 若响应报文丢失导致重传,接收方会收到重复数据。但接收方可根据报头当中的32位序号来判断曾经是否收到过这个报文,从而达到报文去重的目的。
  • 发送出去的数据在收到响应前会保留在发送缓冲区中,以确保可以重传,收到响应后才删除或覆盖。

那么, 如果超时的时间如何确定?

  • 最理想的情况下,找到一个最小的时间,保证 "确认应答一定能在这个时间内返回".
  • 但是这个时间的长短,随着网络环境的不同,是有差异的.
  • 如果超时时间设的太长,会影响整体的重传效率;
  • 如果超时时间设的太短,有可能会频繁发送重复的包;

TCP为了保证无论在任何环境下都能比较高性能的通信,因此会动态计算这个最大超时时间.

  • Linux中(BSD Unix和Windows也是如此),超时以500ms为一个单位进行控制,每次判定超时重发的超时时间都是500ms的整数倍。
  • 如果重发一次之后,仍然得不到应答,等待 2*500ms 后再进行重传.
  • 如果仍然得不到应答,等待 4*500ms 进行重传。依次类推,以指数形式递增.
  • 累计到一定的重传次数,TCP认为网络或者对端主机出现异常,强制关闭连接. 

连接管理机制

在正常情况下,TCP要经过三次握手建立连接,四次挥手断开连接

TCP是面向连接的

TCP的可靠性机制是基于连接的,不是直接从一台主机到另一台主机。服务器与多个客户端通信时,如果TCP不是基于连接,那么所有数据都会进入同一个接收缓冲区,导致数据干扰。因此,在TCP通信前需要先建立连接,以确保数据的可靠传输。

操作系统对连接的管理

面向连接是TCP可靠性的一种,只有在通信建立好连接才会有各种可靠性的保证,而一台机器上可能会存在大量的连接,此时操作系统就不得不对这些连接进行管理。

  • 操作系统在管理这些连接时需要“先描述,再组织”,操作系统通过结构体描述每个连接,并使用数据结构组织这些连接。
  • 建立连接就是定义结构体变量并填充属性,然后加入数据结构;
  • 断开连接则是从数据结构中删除并释放资源。

三次握手

三次握手的过程

双方在进行TCP通信之前需要先建立连接,建立连接的这个过程我们称之为三次握手。

客户端和服务器进行TCP通信前,需先建立连接。客户端主动发送连接请求给服务器,随后双方通过三次握手完成连接建立。

  • 第一次握手,客户端发送SYN位为1的报文,请求连接。
  • 第二次握手,服务器收到请求后,发送SYN和ACK位均为1的报文,既响应客户端请求又发起自己的连接请求。
  • 第三次握手,客户端收到服务器报文后,发送响应报文,确认连接建立。

需要注意的是,虽然客户端发起的是从客户端到服务器的连接请求,但TCP支持全双工通信,因此服务器也需要向客户端发起连接请求,确保双向通信的建立。

那么为什么是三次握手呢?

连接建立并非总是成功,通信双方在三次握手过程中,前两次握手能够得到对方的确认,因为每次都有对应的回应。然而,第三次握手缺乏直接的响应报文,如果这一阶段的ACK报文丢失,连接建立就会失败。虽然客户端在完成第三次握手后认为连接已建立,但服务器若未收到这一握手,则不会建立连接。

因此,无论采用几次握手,最后一次握手的可靠性都是无法保证的。鉴于连接建立存在不确定性,选择握手次数的关键在于权衡不同次数握手的优点。

三次握手是验证双方通信信道(全双工)的最小次数:

TCP作为全双工通信协议,其核心任务是验证双方通信信道的连通性。三次握手是验证双方通信信道连通性的最小次数,通过这一过程,双方都能确认彼此能够正常发送和接收数据。

  • 对于客户端而言,收到服务器的第二次握手意味着自己发出的第一次握手已被对方可靠接收,从而验证了客户端的发送能力和服务器的接收能力。同时,由于客户端能收到服务器的第二次握手,也证明了服务器能发以及自己能收。
  • 对服务器而言,收到客户端的第一次握手即证明客户端能发且自己能收。而当它收到客户端的第三次握手时,说明自己发出的第二次握手已被对方可靠接收,从而验证了服务器的发送能力和客户端的接收能力。
  • 既然三次握手已经足够验证双方通信信道的正常性,那么虽然更多次的握手也能达到验证目的,但并无必要,因为三次已经足够。

奇数次握手,可以确保一般情况握手失败的连接成本是嫁接在client端的:

  • 在TCP三次握手过程中,客户端和服务器建立连接的时间点存在差异。当客户端收到服务器的第二次握手时,它认为双方通信信道已连通,因此在客户端端点,连接已建立。然而,服务器必须收到客户端的第三次握手才能确认通信信道的连通性,进而在服务器端建立连接。
  • 若客户端发出的第三次握手丢包,服务器将不会建立连接,而客户端则会暂时维护一个异常连接。这种维护涉及时间和空间的成本,但三次握手的设计确保了异常连接主要出现在客户端,从而减轻了服务器的负担。因为通常情况下,客户端的异常连接数量相对较少,而服务器在处理多个客户端连接请求时,若大量连接建立失败,将耗费大量资源来管理这些异常连接。
  • 值得注意的是,异常连接不会无限期地维持。如果服务器长时间未收到第三次握手,会进行超时重传,给予客户端重新发送第三次握手的机会。此外,当客户端试图向服务器发送数据时,若服务器发现未与该客户端建立连接,会要求客户端重新建立连接。

采用三次握手的理由:

  • 三次握手是验证双方通信信道(全双工)的最小次数
  • 奇数次握手,可以确保一般情况握手失败的连接成本是嫁接在client端的

三次握手时的状态变化 

三次握手过程中的状态变化如下:

  • 初始时,客户端和服务器均处于CLOSED状态。服务器为了准备接受客户端的连接请求,会从CLOSED状态转变为LISTEN状态。
  • 随后,客户端发起第一次握手,进入SYN_SENT状态,等待服务器的响应。服务器在LISTEN状态下接收到客户端的连接请求后,将其放入内核等待队列,并发起第二次握手作为响应,此时服务器状态转变为SYN_RCVD。
  • 当客户端收到服务器的第二次握手后,会立即发送第三次握手作为确认,此时客户端的连接已经成功建立,状态更新为ESTABLISHED,意味着它已准备好进行数据交互。
  • 服务器在收到客户端的第三次握手后,同样确认连接建立成功,状态也转变为ESTABLISHED。

至此,三次握手过程完成,通信双方的状态都已变为ESTABLISHED,可以进行数据传输了。

套接字和三次握手之间的关系 

  • 在TCP连接建立的过程中,服务器首先需要调用listen函数,使自身进入LISTEN状态,以等待客户端的连接请求。一旦服务器进入这个状态,客户端就可以使用connect函数发起三次握手过程。然而,connect函数本身并不直接参与底层的三次握手,它的主要作用是触发这一过程的开始。当connect函数返回时,它表示底层的三次握手要么已经成功完成,连接已建立,要么握手失败。
  • 如果服务器与客户端成功完成了三次握手,服务器内核会等待队列中创建一个新的连接。但此时,这个连接仍然停留在内核中,服务器端需要调用accept函数来从内核等待队列中取出这个已经建立好的连接,使其能够被应用程序所使用。
  • 一旦服务器通过accept函数获取了连接,客户端和服务器之间就可以通过调用read/recv函数和write/send函数来进行数据交互了。

四次挥手

四次挥手的过程

由于双方维护连接都是需要成本的,因此当双方TCP通信结束之后就需要断开连接,断开连接的这个过程我们称之为四次挥手。

在客户端与服务器完成通信后,需要执行四次挥手来断开连接。这一过程具体如下:

  • 首先,客户端发起第一次挥手,它通过发送一个包含FIN标志位设置为1的报文,向服务器表示希望断开连接。
  • 接着,服务器接收到客户端的断开请求后,进行第二次挥手,即对客户端的请求作出响应。
  • 随后,当服务器完成所有数据传输并确认无需再向客户端发送数据时,它会发起第三次挥手,向客户端发送一个包含FIN标志位的报文,表示服务器也希望断开连接。
  • 最后,客户端在收到服务器的断开请求后,进行第四次挥手,即发送一个确认报文,告知服务器已接收到其断开请求,并确认双方连接已经关闭。

通过这四次挥手,客户端和服务器完成了连接的断开过程,确保了数据传输的完整性和连接的优雅关闭。

为什么是四次挥手?

  • TCP是全双工通信协议,意味着在建立连接时,需要确保客户端到服务器和服务器到客户端两个方向的通信信道都建立成功。同样地,在断开连接时,也需要确保这两个方向的通信信道都被正确地关闭。因此,断开连接时需要进行四次挥手。
  • 这四次挥手分别对应两个方向的通信信道关闭过程。前两次挥手负责关闭从客户端到服务器的通信信道,而后两次挥手则负责关闭从服务器到客户端的通信信道。

值得注意的是,第二次和第三次挥手不能合并进行。第三次挥手实际上是服务器在确认不再需要向客户端发送数据后,发起的断开连接请求。在服务器收到客户端的断开请求并响应后,它可能还需要继续发送一些数据给客户端,因此不会立即发起第三次挥手。只有当服务器完成所有数据的发送后,它才会发起第三次挥手。

四次挥手时的状态变化

在TCP四次挥手过程中,客户端和服务器在断开连接前都处于ESTABLISHED状态,即连接已建立状态。

  • 首先,客户端为了断开与服务器的连接,会主动发起连接断开请求,此时客户端的状态会转变为FIN_WAIT_1,表示等待服务器的确认。
  • 服务器在收到客户端的连接断开请求后,会对其进行响应,并进入CLOSE_WAIT状态,这意味着服务器已经接收到断开请求,但还在等待完成本地应用层上的数据发送和处理。
  • 一旦服务器完成所有数据的发送,并且没有更多数据需要发送给客户端时,它会向客户端发起断开连接请求,并进入LAST_ACK状态,等待客户端的最后一个确认。
  • 客户端收到服务器的断开连接请求后,会发送最后一个响应报文给服务器,此时客户端的状态变为TIME_WAIT,表示等待一段时间以确保服务器收到了最后的确认。
  • 当服务器收到客户端的最后一个响应报文时,它会彻底关闭连接,状态转变为CLOSED,表示连接已经完全断开。

而客户端在发送完最后一个响应报文后,会等待一个2MSL(报文最大生存时间)的时间,以确保网络中的所有相关报文都已过期,然后才会进入CLOSED状态,完成连接的最终断开。这一过程确保了TCP连接的可靠断开,并防止了已断开连接的报文在网络中滞留造成的问题。

套接字和四次挥手之间的关系

  • 在客户端和服务器通信的过程中,当客户端想要断开连接时,它会主动调用close函数。这个close函数的调用实际上触发了TCP的四次挥手过程的前两次挥手,即客户端发送FIN报文给服务器,并等待服务器的确认。
  • 同样地,当服务器也想要断开连接时,它会主动调用close函数。这个close函数的调用则触发了四次挥手的后两次,即服务器发送FIN报文给客户端,并等待客户端的确认。

因此,每一个close函数的调用都对应着TCP四次挥手中的两次,整个断开连接的过程需要四次挥手来完成。

理解CLOSE_WAIT状态

  • 当双方进行TCP连接的四次挥手过程时,如果只有客户端调用了close函数,而服务器并未调用close函数,这种情况下,服务器会进入CLOSE_WAIT状态,而客户端则会停留在FIN_WAIT_2状态。然而,只有当双方都完成了这四次挥手,连接才算是真正地被断开,各自的资源也才会被释放。
  • 如果服务器没有主动关闭不再需要的文件描述符,那么在其内部就会积累大量处于CLOSE_WAIT状态的连接。每个这样的连接都会占用服务器的资源,随着时间的推移,这些累积的资源占用会导致服务器可用的资源逐渐减少。
  • 因此,如果不及时关闭不再使用的文件描述符,不仅会造成文件描述符的泄漏,还可能意味着连接资源没有被完全释放,这实际上也是一种内存泄漏的现象。
  • 所以,在编写网络套接字相关的代码时,如果发现服务器端存在大量处于CLOSE_WAIT状态的连接,那么应该检查是否是因为服务器没有及时地调用close函数来关闭那些不再需要的文件描述符。及时关闭这些文件描述符,有助于避免资源泄漏和确保服务器的稳定运行。

理解TIME_WAIT状态

主动断开连接的一方,在4次挥手完成之后要进入time_wait状态,等待若干时长之后,自动释放。

TIME_WAIT状态存在的必要性:

  • 实现TCP全双工连接的可靠终止:

在四次挥手的过程中,当一方(如客户端)主动中断连接时,它必须接收另一方(如服务器)的FIN信息并给予ACK应答。如果在这个过程中ACK丢失,根据超时重传机制,服务端会重发FIN。为了确保能够重传ACK,客户端必须维持一个TIME_WAIT状态,以便能够找到对应的连接并回复ACK。否则,如果客户端直接关闭连接,服务器虽然进行了超时重传,但已经得不到客户端的响应了,因为客户端已经将连接关闭了。

服务器在经过若干次超时重发后得不到响应,最终也一定会将对应的连接关闭,但在服务器不断进行超时重传期间还需要维护这条废弃的连接,这样对服务器是非常不友好的。

实际第四次挥手丢包后,可能双方网络状态出现了问题,尽管客户端还没有关闭连接,也收不到服务器重发的连接断开请求,此时客户端TIME_WAIT等若干时间最终会关闭连接,而服务器经过多次超时重传后也会关闭连接。这种情况虽然也让服务器维持了闲置的连接,但毕竟是少数,引入TIME_WAIT状态就是争取让主动发起四次挥手的客户端维护这个成本。

  • 让通信双方历史数据得以消散,防止旧的数据包干扰新连接:

假设在一个TCP连接关闭后,很快在相同的IP地址和端口上建立了新的连接。如果没有TIME_WAIT状态,之前连接中可能存在的、在网络中延迟的数据包可能会被新的连接误认为是正常的数据传输,从而导致数据传输的混乱。TIME_WAIT状态的存在确保了在新的连接建立之前,旧的数据包有足够的时间在网络中过期并消失。

所以,TIME_WAIT状态对于确保TCP连接的可靠终止和防止数据混淆具有关键作用,是TCP协议中不可或缺的一部分。

TIME_WAIT的等待时长是多少?

TIME_WAIT状态的等待时长对于TCP连接的管理至关重要。

  • 如果等待时间过长,会导致等待方需要长时间维持TIME_WAIT状态,这意味着在等待期间需要消耗额外的资源来维护这个连接,这无疑是一种资源浪费的现象。
  • 过短的等待时间无法保证ACK应答被对方较大概率地接收到,同时也无法确保在网络中传输的数据包已经消散。这样,TIME_WAIT状态就失去了其存在的意义。

因此,TCP协议规定了主动关闭连接的一方在四次挥手后必须进入TIME_WAIT状态,并等待两个MSL(报文最大生存时间)的时间,以确保数据传输的可靠性和避免潜在的资源浪费。这样的设计既考虑了数据传输的效率,又确保了资源的有效利用。

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状态引起的bind失败的方法

现在做一个测试,首先启动server,然后启动client,然后用Ctrl-C使server终止,这时马上再运行server,结果是:

这是因为,虽然server的应用程序终止了,但TCP协议层的连接并没有完全断开,因此不能再次监 听同样的server端口。我们用netstat命令查看一下:我们使用Ctrl-C终止了server,所以server是主动关闭连接的一方,在TIME_WAIT期间仍然不能再次监听同样的server端口;

在server的TCP连接没有完全断开之前不允许重新监听, 某些情况下可能是不合理的:

  • 服务器需要处理非常大量的客户端的连接(每个连接的生存时间可能很短,但是每秒都有很大数量的客户端来请求)。
  • 这个时候如果由服务器端主动关闭连接(比如某些客户端不活跃,就需要被服务器端主动清理掉),就会产生大量TIME_WAIT连接。
  • 由于我们的请求量很大, 就可能导致TIME_WAIT的连接数很多,每个连接都会占用一个通信五元组(源ip,源端口,目的ip,目的端口,协议)。其中服务器的ip和端口和协议是固定的。如果新来的客户端连接的ip和端口号和TIME_WAIT占用的链接重复了,就会出现问题。

创建socket后,使用setsockopt()设置socket描述符的选项SO_REUSEADDR为1,表示允许创建端口号相同但IP地址不同的多个socket描述符。

流量控制

接收端处理数据的速度是有限的。如果发送端发的太快,导致接收端的缓冲区被打满,这个时候如果发送端继续发送,就会造成丢包,继而引起丢包重传等等一系列连锁反应。
因此TCP支持根据接收端的处理能力,来决定发送端的发送速度。这个机制就叫做流量控制(Flow Control);

因此接收端可以将自己接收数据的能力告知发送端,让发送端控制自己发送数据的速度。

  • 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段,通过ACK端通知发送端;
  • 窗口大小字段越大,说明网络的吞吐量越高;
  • 接收端一旦发现自己的缓冲区快满了,就会将窗口大小设置成一个更小的值通知给发送端;
  • 发送端接受到这个窗口之后,就会减慢自己的发送速度;
  • 如果接收端缓冲区满了,就会将窗口置为0;这时发送方不再发送数据,但是需要定期发送一个窗口探测数据段,使接收端把窗口大小告诉发送端。

当发送端得知接收端接收数据的能力为0时会停止发送数据,此时发送端会通过以下两种方式来得知何时可以继续发送数据。 

  • 接收端窗口更新通知。接收端上层将接收缓冲区当中的数据读走后,接收端向发送端发送一个TCP报文,主动将自己的窗口大小告知发送端,发送端得知接收端的接收缓冲区有空间后就可以继续发送数据了。
  • 发送端窗口探测。发送端每隔一段时间向接收端发送报文,该报文不携带有效数据,只是为了询问发送端的窗口大小,直到接收端的接收缓冲区有空间后发送端就可以继续发送数据了。

接收端如何把窗口大小告诉发送端呢?回忆我们的TCP首部中,有一个16位窗口字段,就是存放了窗口大小信息;
那么问题来了,16位数字最大表示65535,那么TCP窗口最大就是65535字节么?

  • 理论上确实是这样的,但实际上TCP报头当中40字节的选项字段中包含了一个窗口扩大因子M,实际窗口大小是窗口字段的值左移M位得到的。 

第一次向对方发送数据时如何得知对方的窗口大小? 

  • 双方在进行TCP通信之前需要先进行三次握手建立连接,而双方在握手时除了验证双方通信信道是否通畅以外,还进行了其他信息的交互,其中就包括告知对方自己的接收能力,因此在双方还没有正式开始通信之前就已经知道了对方接收数据能力,所以双方在发送数据时是不会出现缓冲区溢出的问题的。

滑动窗口

连续发送多个数据

双方在进行TCP通信时一发一收的方式性能较低,那么我们可以一次向对方发送多条数据,这样可以将等待多个响应的时间重叠起来,进而提高数据通信的效率。

需要注意的是,虽然双方在进行TCP通信时可以一次向对方发送大量的报文,但不能将自己发送缓冲区当中的数据全部打包发送给对端,在发送数据时还要考虑对方的接收能力。 

滑动窗口

发送方可以一次发送多个报文给对方,此时也就意味着发送出去的这部分报文当中有相当一部分数据是暂时没有收到应答的。

其实可以将发送缓冲区当中的数据分为三部分:

  • 已经发送并且已经收到ACK的数据。
  • 已经发送还但没有收到ACK的数据。
  • 还没有发送的数据。

这里发送缓冲区的第二部分就叫做滑动窗口。(也有人把这三部分整体称之为滑动窗口,而将其中的第二部分称之为窗口大小)

而滑动窗口描述的就是,发送方不用等待ACK一次所能发送的数据最大量。 

滑动窗口极大地提升了数据传输的效率。

  • 滑动窗口的大小是由接收方的窗口大小和发送方的拥塞窗口大小共同决定的,取二者中的较小值。这样做是为了在发送数据时同时考虑到对方的接收能力以及当前网络的拥塞状况。
  • 现在,假设我们不考虑拥塞窗口的影响,且接收方的窗口大小始终固定为4000字节。在这样的条件下,发送方能够连续发送数据,而无需等待每一段数据的ACK确认。
  • 例如,发送方可以连续发送1001-2000、2001-3000、3001-4000、4001-5000这四个数据段,无需等待任何ACK即可继续发送。
  • 当发送方收到接收方确认序号为2001的ACK时,意味着1001-2000这一数据段已被对方成功接收。此时,滑动窗口可以向右移动,释放已确认接收的数据段所占用的空间,并继续发送新的数据段,如5001-6000。

滑动窗口的大小直接反映了网络的吞吐率及接收方的接收能力。窗口越大,意味着发送方能够连续发送更多的数据而无需等待ACK,从而提高了整体的数据传输效率。

此外,滑动窗口机制还支持TCP的重传机制。发送方需要暂时保存已发出但尚未收到确认的数据段,这些数据段正好位于滑动窗口的范围内。只有当数据段被对方成功接收并返回ACK时,这些数据段才会从滑动窗口的左侧移出,成为可覆盖或删除的部分。因此,滑动窗口不仅限定了可直接发送的数据量,还为TCP的重传机制提供了必要的支持。

滑动窗口一定会整体右移吗?

滑动窗口并不是简单地整体向右移动窗口。以上面的情况为例,假设接收方已经成功接收了1001-2000的数据段,并对此进行了响应,但如果接收方的上层应用未能及时从接收缓冲区中读取这些数据,那么接收窗口的大小会发生变化。

在这种情况下,当发送方收到接收方确认收到2001序号的数据响应时,它会将已经确认的1001-2000数据段从滑动窗口的右侧移至左侧。然而,由于接收方未能及时处理数据,其接收能力有所下降,导致窗口大小从原来的4000减小到3000。

当发送方将已确认的数据段移动至窗口左侧后,滑动窗口的大小恰好与接收方当前的接收能力(3000)相匹配。这意味着滑动窗口的右侧边界无法继续向右扩展,因为发送方需要等待接收方进一步确认数据并释放缓冲区空间,以便能够继续发送新的数据段。

因此滑动窗口在向右移动的过程中并不一定是整体右移的,因为对方接收能力可能不断在变化,从而滑动窗口也会随之不断变宽或者变窄。 

滑动窗口的实现原理 

TCP通信中,接收和发送缓冲区被抽象为字符数组,而滑动窗口则是通过两个指针界定的一个数据范围。我们可以将这两个指针命名为start和end,其中start指向滑动窗口的起始位置,end指向滑动窗口的结束位置。在这两个指针之间的数据区间,即start到end的范围,便构成了当前的滑动窗口。

当发送端接收到来自接收方的响应时,它会解析响应中的确认序号x和窗口大小win。根据这些信息,发送端会更新其滑动窗口的位置和大小。具体来说,它会将start指针移动到确认序号x所指示的位置,表示这部分数据已经被接收方成功接收并可以从发送端的窗口中移除。随后,发送端会根据窗口大小win来计算新的end指针位置,即start+win,从而确定滑动窗口的新边界。

丢包问题:如果出现了丢包,如何进行重传? 

当发送端一次发送多个报文数据时,此时的丢包情况也可以分为两种。

情况一:数据包已经抵达, ACK被丢了

这种情况下,部分ACK丢了并不要紧,因为可以通过后续的ACK进行确认;

假设上图中,前三个数据包对应的ACK确认信息丢失了。然而,只要发送端收到了最后5001-6000数据包的ACK响应,它就可以推断出前三个数据包实际上已经被接收端成功接收了。这是因为,如果接收端没有收到前三个数据包,它是不可能设置确认序号为6001的。确认序号为6001的含义是,接收端已经收到了序号为1到6000的所有字节数据,并通知发送端下一次应该从序号为6001的字节数据开始发送。因此,尽管中间某些数据包的ACK丢失,但根据最后的ACK确认信息,发送端仍然能够准确地了解接收端的数据接收状态,并继续后续的数据传输。

情况二:数据包就直接丢了

  • 当某一段报文段丢失之后,发送端会一直收到 1001 这样的ACK,就像是在提醒发送端 "我想要的是 1001"一样;
  • 如果发送端主机连续三次收到了同样一个 "1001" 这样的应答,就会将对应的数据 1001 - 2000 重新发送;
  • 这个时候接收端收到了 1001 之后,再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了,被放到了接收端操作系统内核的接收缓冲区中;

这种机制被称为“快重传”或“高速重传控制”,它旨在在数据重传过程中找到平衡点,既要确保丢失的数据包得到及时重传,又要避免不必要的大量数据重复发送。在实际应用中,发送端可能无法确切知道是哪一个数据包丢失了。例如,当发送端连续收到多个确认序号为1001的响应报文时,理论上它可能会选择重传从1001到更高序号(如7000)的所有数据包。然而,这种做法可能导致大量已正确接收的数据被不必要地重传,浪费网络资源。

因此,发送端会采取一种更精细的策略:它首先尝试仅重传疑似丢失的数据包(如1001-2000),然后观察重传后的确认序号反馈。根据这些反馈,发送端可以进一步判断是否需要重传其他数据包。这种逐步逼近的方法有助于在个别数据包丢失的情况下实现快速恢复,同时避免了大规模的数据重传,从而提高了数据传输的效率和可靠性。

快重传和超时重传

快重传和超时重传是TCP协议中用于确保数据可靠传输的两种重要机制,它们之间的主要区别体现在触发条件和重传效率上。

  • 超时重传是当发送方在设定的时间内没有收到接收方的确认应答报文时,会重发该数据。这是TCP协议中确保数据可靠传输的基本机制。然而,超时重传的问题在于其效率较低,因为发送方需要等待超时时间后才能重传数据,这可能导致数据传输的延迟。
  • 相比之下,快速重传则是一种更高效的机制。它不以时间为驱动,而是以数据为驱动。当发送方连续收到三个相同的ACK报文时,意味着接收方在某个地方出现了失序的报文,发送方会立即重传那个疑似丢失的报文段,而无需等待超时时间。这种机制可以显著减少数据传输的延迟,提高传输效率。

但快重传并不能完全取待超时重传,因为有时数据包丢失后可能并没有收到对方三次重复的应答,此时快重传机制就触发不了,而只能进行超时重传。快重传虽然是一个效率上的提升,但超时重传却是所有重传机制的保底策略,也是必不可少的。

拥塞控制

为什么要有拥塞控制?

在TCP通信中,偶尔的数据包丢失是正常的现象,这可以通过快速重传或超时重发机制进行修复。然而,当通信过程中出现大量丢包时,这种情况就不再被视为正常。 

TCP不仅关注通信双方主机的性能,还充分考虑了网络状况的影响。其中,流量控制机制关注接收端的接收缓冲区能力,通过控制发送速度来防止缓冲区溢出。滑动窗口机制则旨在提高发送效率,允许发送端在不等待每个ACK的情况下发送一定量的数据。

拥塞窗口机制则是考虑双方通信时网络的状态。因为网络上有很多的计算机,可能当前的网络状态就已经比较拥堵。在不清楚当前网络状态下,贸然发送大量的数据,是很有可能引起雪上加霜的.当发送的数据量超出拥塞窗口的限制时,可能会导致网络拥塞。

因此,当通信中出现大量丢包时,TCP会判断这不再仅仅是接收或发送端的问题,而是通信信道网络出现了拥塞。此时,TCP会采取相应的措施来缓解拥塞,确保通信的可靠性和效率。

如何应对网络拥塞问题?

如果通信双方出现了大量的数据丢包问题, tcp会判断网络出问题了(网络拥塞了)

我们发送方,应该怎么办?我们不能立即对报文进行超时重发!因为这样会加重网络的拥塞!

因此双方通信时如果出现大量丢包,不应该立即将这些报文进行重传,而应该少发数据甚至不发数据,等待网络状况恢复后双方再慢慢恢复数据的传输速率。 

网络拥塞是一个全局性的问题,它不仅局限于单一主机,而是对整个网络中的所有主机都产生深远影响。在这种情境下,采用TCP传输控制协议的所有主机都会自动启动拥塞避免算法。尽管拥塞控制策略在表面上似乎只是单个主机上的通信手段,但实际上,它是所有主机在网络出现不稳定时共同遵循的规范。只要网络出现拥塞现象,该网络中的每一台主机都会受到波及,因此它们都必须执行拥塞避免策略,这样才能共同协作,有效减轻网络拥塞带来的压力,确保网络的顺畅运行。

拥塞控制

虽然TCP有了滑动窗口这个大杀器,能够高效可靠的发送大量的数据,但如果在刚开始阶段就发送大量的数据,就可能会引发某些问题。因为网络上有很多的计算机,有可能当前的网络状态就已经比较拥塞了,因此在不清楚当前网络状态的情况下,贸然发送大量的数据,就可能会引起网络拥塞问题。

因此TCP引入了慢启动机制,在刚开始通信时先发少量的数据探探路,摸清当前的网络拥堵状态,再决定按照多大的速度传输数据。

TCP除了有窗口大小和滑动窗口的概念以外,还有一个窗口叫做拥塞窗口。拥塞窗口是可能引起网络拥塞的阈值,如果一次发送的数据超过了拥塞窗口的大小就可能会引起网络拥塞。 

  • 发送开始的时候, 定义拥塞窗口大小为1;
  • 每次收到一个ACK应答, 拥塞窗口加1;
  • 每次发送数据包的时候,将拥塞窗口和接收端主机反馈的窗口大小做比较,取较小的值作为实际发送的窗口; 

像上面这样的拥塞窗口增长速度,是指数级别的。"慢启动" 只是指初使时慢,但是增长速度非常快。如果拥塞窗口的值一直以指数的方式进行增长,此时就可能在短时间内再次导致网络出现拥塞。

  • 为了避免短时间内再次导致网络拥塞,因此不能一直让拥塞窗口按指数级的方式进行增长。
  • 此时就引入了慢启动的阈值,当拥塞窗口的大小超过这个阈值时,就不再按指数的方式增长,而按线性的方式增长。

  • 当TCP开始启动的时候,慢启动阈值等于窗口最大值;
  • 在每次超时重发的时候,慢启动阈值会变成原来的一半,同时拥塞窗口置回1;

少量的丢包,我们仅仅是触发超时重传; 大量的丢包,我们就认为网络拥塞;
当TCP通信开始后,网络吞吐量会逐渐上升; 随着网络发生拥堵,吞吐量会立刻下降;
拥塞控制,归根结底是TCP协议想尽可能快的把数据传输给对方,但是又要避免给网络造成太大压力的折中方案。

延迟应答

如果接收数据的主机收到数据后立即进行ACK应答,此时返回的窗口可能比较小。

  • 假设对方接收端缓冲区剩余空间大小为1M,对方一次收到500K的数据后,如果立即进行ACK应答,此时返回的窗口就是500K。
  • 但实际接收端处理数据的速度很快,10ms之内就将接收缓冲区中500K的数据消费掉了。
  • 在这种情况下,接收端处理还远没有达到自己的极限,即使窗口再放大一些,也能处理过来。
  • 如果接收端稍微等一会再进行ACK应答,比如等待200ms再应答,那么这时返回的窗口大小就是1M。

延迟应答的目的不是为了保证可靠性,而是留出一点时间让接收缓冲区中的数据尽可能被上层应用层消费掉,此时在进行ACK响应的时候报告的窗口大小就可以更大,窗口越大,网络吞吐量就越大, 传输效率就越高。我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;

那么所有的包都可以延迟应答么? 肯定也不是;

  • 数量限制: 每隔N个包就应答一次;
  • 时间限制: 超过最大延迟时间就应答一次;

具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms; 

捎带应答

在延迟应答的基础上,我们发现,很多情况下,客户端服务器在应用层也是 "一发一收" 的。意味着客户端给服务器说了 "How are you",服务器也会给客户端回一个 "Fine, thank you";
那么这个时候ACK就可以搭顺风车,和服务器回应的 "Fine, thank you" 一起回给客户端 

由于捎带应答的报文携带了有效数据,因此对方收到该报文后会对其进行响应,当收到这个响应报文时不仅能够确保发送的数据被对方可靠的收到了,同时也能确保捎带的ACK应答也被对方可靠的收到了。 

面向字节流 

创建一个TCP的socket,同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;

  • 调用write时,数据会先写入发送缓冲区中;
  • 如果发送的字节数太长,会被拆分成多个TCP的数据包发出;
  • 如果发送的字节数太短,就会先在缓冲区里等待,等到缓冲区长度差不多了,或者其他合适的时机发送出去;
  • 接收数据的时候,数据也是从网卡驱动程序到达内核的接收缓冲区;
  • 然后应用程序可以调用read从接收缓冲区拿数据;
  • 另一方面,TCP的一个连接,既有发送缓冲区,也有接收缓冲区,那么对于这一个连接, 既可以读数据,也可以写数据。这个概念叫做 全双工

由于缓冲区的存在,TCP程序的读和写不需要一一匹配, 例如:

  • 写100个字节数据时,可以调用一次write写100个字节,也可以调用100次write,每次写一个字节;
  • 读100个字节数据时,也完全不需要考虑写的时候是怎么写的,既可以一次read 100个字节,也可以一次read一个字节,重复100次;

TCP(传输控制协议)的本质是专注于在网络层面实现可靠的数据传输。在TCP的视角中,发送缓冲区中的数据并无特定含义,它们仅仅是待发送的字节序列。TCP的核心职责是确保这些字节序列能够完整且无误地抵达目标主机的接收缓冲区。至于这些字节序列如何被解释为具体的信息或数据,那完全是上层应用程序的责任这种特性被称为“面向字节流”,意味着TCP关注的是字节级别的数据传输,而不是具体的数据内容或格式。

粘包问题

首先要明确,粘包问题中的 "包",是指的应用层的数据包.

  • 在TCP的协议头中,没有如同UDP一样的 "报文长度" 这样的字段,但是有一个序号这样的字段。
  • 站在传输层的角度,TCP是一个一个报文过来的。按照序号排好序放在缓冲区中。
  • 站在应用层的角度,看到的只是一串连续的字节数据。
  • 那么应用程序看到了这么一连串的字节数据,就不知道从哪个部分开始到哪个部分,是一个完整的应用层数据包。

那么如何避免粘包问题呢?归根结底就是一句话,明确两个包之间的边界。

  • 对于定长的包,保证每次都按固定大小读取即可;例如上面的Request结构,是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
  • 对于变长的包,可以在包头的位置,约定一个包总长度的字段,从而就知道了包的结束位置;比如HTTP报头当中就包含Content-Length属性,表示正文的长度。
  • 对于变长的包,还可以在包和包之间使用明确的分隔符(应用层协议,是程序猿自己来定的,只要保证分隔符不和正文冲突即可如用进行分割"\n"); 

对于UDP协议来说,是否也存在 "粘包问题" 呢?

  • 对于UDP,如果还没有上层交付数据,UDP的报文长度仍然在。同时,UDP是一个一个把数据交付给应用层。就有很明确的数据边界。
  • 站在应用层的站在应用层的角度,使用UDP的时候,要么收到完整的UDP报文,要么不收。不会出现"半个"的情况。

因此UDP(用户数据报协议)本身不存在粘包问题,原因在于UDP报文头部包含一个16位的长度字段,这个字段明确记录了每个UDP报文的实际长度。因此,在底层传输过程中,UDP能够清晰地区分不同报文之间的边界,确保每个报文都能够被完整地接收和处理。

相比之下,TCP(传输控制协议)则存在粘包问题。这是因为TCP是一个面向字节流的协议,它并不在报文之间设置明确的边界。

TCP异常情况 

进程终止

进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.

例如:

当客户端正常地与服务器建立连接后,如果客户端进程意外崩溃,该进程之前打开并用于与服务器通信的文件描述符(通常是套接字)会被操作系统自动关闭。这一关闭动作相当于客户端主动调用了close函数来关闭连接。

在客户端进程崩溃后,操作系统会在底层自动执行TCP的四次挥手过程。然后释放对应的连接资源。也就是说,进程终止时会释放文件描述符,TCP底层仍然可以发送FIN,和进程正常退出没有区别。

机器重启

和进程终止的情况相同

当客户端正常地与服务器建立连接后,如果客户端主机重启。操作系统会先杀掉所有进程然后再进行关机重启,因此机器重启和进程终止的情况是一样的,此时双方操作系统也会正常完成四次挥手,然后释放对应的连接资源。

机器掉电/网线断开

当客户端在正常访问服务器时突然掉线,服务器端在短时间内并不会立即意识到这一点,因此它会继续维持与客户端建立的连接。然而,TCP协议具有一套保活策略来确保连接的可靠性和有效性(保活定时器 )。

  • 服务器会定期向客户端发送探测报文,以检查客户端是否仍然在线并能够响应。
  • 如果服务器连续多次发送探测报文都没有收到客户端的ACK应答,那么服务器就会认为客户端已经掉线,并主动关闭这条连接,释放相关的网络资源。

除了基于保活定时器的心跳机制外,有些应用层协议也实现了类似的检测机制。例如,在使用长连接的HTTP协议时,客户端和服务器之间也会定期交换心跳消息,以检测对方的存在状态。如果长时间没有收到对方的响应,那么相应的连接也会被关闭。

TCP小结

为什么TCP这么复杂? 因为要保证可靠性,同时又尽可能的提高性能.

可靠性:

  • 检验和。
  • 序列号。
  • 确认应答。
  • 超时重传。
  • 连接管理。
  • 流量控制。
  • 拥塞控制。

提高性能:

  • 滑动窗口。
  • 快速重传。
  • 延迟应答。
  • 捎带应答。

需要注意的是,TCP的这些机制有些能够通过TCP报头体现出来的,但还有一些是通过代码逻辑体现出来的。

基于TCP应用层协议

  • HTTP
  • HTTPS
  • SSH
  • Telnet
  • FTP
  • SMTP

当然,也包括你自己写TCP程序时自定义的应用层协议;

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值