当前位置: 首页 > news >正文

深入解析TCP:可靠传输的核心机制与实现逻辑(三次握手、四次挥手、流量控制、滑动窗口、拥塞控制、慢启动、延时应答、面向字节流、粘包问题)

Linux系列


文章目录

  • Linux系列
  • 一、TCP连接的建立与断开
    • 1.1 TCP 三次握手
    • 1.2 TCP四次挥手
      • 1. TCP连接的本质是应用层间的通信通道
      • 2. 断开连接的核心是终止应用层通信
      • 3. 常见误解澄清
  • 二、TCP协议的机制
    • 2.1 流量控制
    • 2.2 滑动窗口
      • 2.2.1 滑动窗口的工作原理
      • 2.2.2 基于滑动窗口快重传
    • 2.3 拥塞控制
      • 2.3.1 拥塞控制的定义
      • 2.3.2 拥塞窗口机制的实现
    • 2.3.3 慢启动策略
    • 2.4 延时应答
  • 三、TCP协议的其他特点
    • 3.1 面向字节流
    • 3.2 粘包问题
  • 总结


上篇文章:深入解析TCP:可靠传输的核心机制与实现逻辑
本篇我们接着上篇文章继续介绍TCP协议,不说废话,直接开始

一、TCP连接的建立与断开

TCP协议在建立时,通信开始前需要先建立连接,通讯结束后会断开连接,接下来我们就来学习一下具体的实现

TCP协议在建立时遵循以下流程:
在这里插入图片描述
接下来我会依据上图,详细的介绍TCP通讯的实现

1.1 TCP 三次握手

此处需要结合上篇文章介绍的标志为理解
在这里插入图片描述

客户端侧状态

  • CLOSED(初始关闭态)
    客户端 TCP 层、应用层尚未发起连接时的基础状态,无连接相关资源占用,代表“连接未建立”。
  • SYN_SENT(已发同步请求态)
    客户端调用 connect() 发起连接,TCP 层向外发送 SYN 报文后进入此状态,等待服务端 SYN+ACK 响应,标志“主动请求连接,等待确认”。
  • ESTABLISHED(连接建立态)
    客户端收到服务端返回的 SYN+ACK 后,回发 ACK 确认,TCP 层状态转为 ESTABLISHED,此时客户端与服务端完成三次握手,双方进入“可双向传输数据”的就绪状态,应用层 connect() 调用也随之返回 。

服务端侧状态

  • CLOSED(初始关闭态)
    服务端 TCP 层、应用层未监听端口时的初始状态,无连接相关资源,代表“无监听、无连接”。
  • LISTEN(监听态)
    服务端应用层执行 socket()bind()listen() 后,TCP 层进入 LISTEN 状态,开始监听指定端口,准备接收客户端连接请求,标志“主动开放端口,等待连接”。
  • SYN_RCVD(已收同步请求态)
    服务端 TCP 层收到客户端 SYN 报文后进入此状态,会回复 SYN+ACK 报文,等待客户端最终 ACK 确认,是三次握手“中间确认环节”的服务端状态。
  • ESTABLISHED(连接建立态)
    服务端收到客户端回发的 ACK 后,TCP 层状态转为 ESTABLISHED,与客户端共同进入“可双向传输数据”状态,此时服务端应用层 accept() 调用返回,可通过新的 connfd 与客户端通信

在连接建立过程中:

  • 第一次握手: Client 给Server发送请求连接,报文中携带SYN标记位来表明当前报文是和建立连接相关的。
  • 第二次握手: Server接受Client的连接,并询问何时建立连接,报文中也会携带SYN标记位,同时会携带ACK来回复上一条请求,此处使用了捎带应答
  • 第三次握手: Client 回复 Server,立马建立连接,这里只是单纯的回复,只要设置ACK即可。

在连接建立流程里,connect 函数仅负责发起连接建立请求,一旦请求发出,后续连接建立的具体交互流程便不再由它参与;accept 函数也只是从系统底层获取已完成三次握手、建立好的连接,本身不介入三次握手过程。实际上,两台主机间 TCP 连接的建立,完全依靠各自操作系统遵循 TCP 协议自主完成报文交互与状态变迁,应用层的 connectaccept 更多是触发和感知这一内核流程的“接口” 。

下面我们来学习连接建立过程中可能出现的问题及解决策略
在这里插入图片描述
可以看到在建立连接时第三次请求,一经发出客户端的状态就会由同步请求态变为连接建立态,而服务端则是在收到第二次请求的应答后,才会由已收同步请求态变为连接建立态,在第三次请求由客户端到服务端接收的这段时间内,我们就会面临着通信双方对连接状态认知不一致问题,或者换个更形象的例子:当第三次请求丢失了,服务端一直处于已收同步请求态,并且认为连接并没有成功建立,而客户端则认为连接建立成功了,客户端就该给服务端发送报文了,当服务端接收到客户端的请求就会感到很疑惑“连接都没建立成功呢,你小子怎么就开始发信息了”,在出现这种情况的时候,我们就需要用到RST标志位了,这时服务器就会发送一个TCP报文,该报文的RST标志位被置为1,表示重新建立连接(重新进行三次握手)

三次握手的优点

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

在这里插入图片描述

通过对比我们可以看到三次握手,实现了两个方向上的通信,可靠的验证了全双工, 但是全双工也可以通过两次握手验证啊,显然单凭这一点是不够的。
在这里插入图片描述
要回答这个问题首先我们要清楚,通信双方维护连接是需要成本的,而服务器与客户端之间的通信时多对1的,若采用两次握手建立连接,当服务器发出出发起第二次握手的信息后,会默认连接已建立,随即开辟资源、等待接收数据。可一旦该握手信息丢包,服务端会因判定二次握手完成而维持“已连接”状态;客户端却因收不到回应,触发超时重传机制。当服务端再次收到重传的连接请求,会误判为新客户端发起连接,再次同意并分配资源。若此类情况大量出现,服务端资源将被快速耗尽,引发崩溃。此外,若客户端恶意重复发送 SYN 请求,还会直接导致服务端遭遇 SYN 洪水攻击,进一步加剧资源消耗与服务异常风险 。

那么三次握手的优点到底在哪里呢?

在三次握手协议中,服务端与客户端成功建立连接的关键,在于服务端接收到客户端发送的第三次ACK确认。当客户端向服务端发送ACK时,客户端单方面认为连接已建立并开始维护资源,但服务端此时尚未确认连接的有效性。若第三次ACK因网络丢包未能到达服务端,客户端会持续重传SYN请求,而服务端因未收到ACK不会分配资源,从而将连接失败的成本完全转移至客户端。这种机制确保了服务端仅在连接真正建立后才消耗资源,有效规避了无效连接的资源浪费。

三次握手的设计从根本上瓦解了SYN洪水攻击的逻辑:攻击方(客户端)必须持续发送SYN请求,但服务端仅在收到ACK后才会分配资源。由于服务端硬件配置通常远超普通客户端,在资源消耗对抗中,客户端会因资源耗尽率先崩溃。例如,单台客户端每秒发送1000个SYN请求,服务端只需处理1000个合法ACK即可维持正常运行,而客户端自身可能在短时间内就因网络带宽或系统资源耗尽而无法继续攻击。

对于基数次握手,连接建立失败的资源消耗都是由发起方承担的,而过多次的握手是没有必要的,所以我们选择三次握手.

连接在建立的同时,双方也协商了接收能力

1.2 TCP四次挥手

在这里插入图片描述
四次挥手,服务端和客户端都可以主动发起,下面我们以客户端主动发起展开介绍。
TCP通信双方,均维持着连接因此,Client 单方面断开连接需要两次;Server端单方面断开连接需要两次,加起来就是四次(后面详细介绍)。

客户端侧状态

  • FIN_WAIT_1:客户端调用 close(fd) 发起主动关闭,TCP 层发送 FIN 报文后进入此状态,等待服务端对 FINACK 确认,是“主动关闭第一步,等待首次确认”的状态。
  • FIN_WAIT_2:客户端收到服务端返回的 ACK(确认客户端 FIN )后进入该状态,此时客户端需等待服务端发送自己的 FIN ,是“已确认对方收到关闭请求,等待对方发起关闭”的中间态。
  • TIME_WAIT:客户端收到服务端的 FIN 并回复 ACK 后进入此状态,会停留 2MSL(报文最大生存时间)时长,作用是确保最后一个 ACK 能被服务端收到,避免服务端因收不到 ACK 重发 FIN ,同时让旧连接残留报文在网络中自然超时,不干扰新连接。
  • CLOSEDTIME_WAIT 超时后,客户端 TCP 层彻底释放连接资源,回到初始无连接状态,标志“连接完全关闭” 。

服务端侧状态

  • CLOSE_WAIT:服务端通过 read(connfd) 返回 0(感知到客户端 FIN ),进入此状态,代表“被动收到关闭请求,需应用层决定何时发起主动关闭” ,期间服务端仍可向客户端发送数据,若应用层未及时处理(如未调用 close(connfd) ),会长期滞留该状态,引发资源泄漏。
  • LAST_ACK:服务端调用 close(connfd) 发起关闭,TCP 层发送 FIN 报文后进入此状态,等待客户端对 FINACK 确认,是“被动关闭方发起最后关闭请求,等待最终确认”的状态。
  • CLOSED:服务端收到客户端返回的 ACK(确认服务端 FIN )后,彻底释放连接资源,回到初始无连接状态,标志“连接完全关闭” 。

在断开连接过程中:

  • 第一次挥手: Client 通知 Server 自己单方面断开连接,Client发送的报文中就会携带FIN标记位。( 注意:Client单方面断开连接以后,只是断开Client ->Server方向上的数据传输,此时Server可以给Client发送数据,反过来不行。)
  • 第二次挥手: Server收到Client的通知请求,并给Client发送应答报文,报文中携带ACK标记位。
  • 第三次挥手: Server 通知 Client 自己单方面断开连接,Server发送的报文中会携带FIN标记位。
  • 第四次挥手: Client收到Server的通知请求,并给Server发送应答报文,报文中携带ACK标记位。

在继续向下介绍之前需要补充几点:

1. TCP连接的本质是应用层间的通信通道

TCP协议通过三次握手在客户端与服务端的传输层(操作系统内核)之间建立逻辑连接,但最终目的是为应用层提供可靠的数据传输服务。只有当应用层,确认连接建立后,才能真正进行数据交换。若应用层未成功对接,即使传输层完成部分握手,也视为连接失败。

2. 断开连接的核心是终止应用层通信

当应用层调用close()关闭socket时,会触发TCP四次挥手流程:

  • 应用层视角:客户端/服务端的应用程序停止发送或接收数据(如HTTP请求响应结束);
  • 传输层视角:操作系统内核通过发送FIN、ACK等管理报文,有序释放TCP连接资源。
    关键点:断开连接后,应用层无法再通过socket发送数据,但传输层仍可能发送剩余的FIN/ACK以完成关闭流程。

断开连接后,传输层仍可发送两类报文:

  • 连接管理报文:如FIN(请求关闭)、ACK(确认接收)、RST(强制复位),由操作系统内核自动生成,无需应用层干预;
  • 应用层数据报文:需通过socket API(如send())主动调用,连接断开后调用会失败(如EPIPE错误)。

示例

  • 客户端发送FIN后进入FIN_WAIT_1状态,若此时应用层尝试send()数据,会触发RST强制关闭;
  • 服务端收到FIN后发送ACK并进入CLOSE_WAIT状态,此时应用层仍可读取缓冲区残留数据,但无法发送新数据。

3. 常见误解澄清

  • “断开连接后无法发送任何数据”:错误。传输层仍可发送FIN/ACK等管理报文;
  • “TCP连接失败仅影响传输层”:错误。连接失败直接导致应用层无法通信;
  • “连接状态由传输层维护,但服务于应用层”:正确。TCP通过序列号、确认号等机制确保应用层数据的可靠传输。

TCP连接的成功与失败,最终以应用层能否通过socket正常通信为判定标准。断开连接后,传输层的“善后工作”(如发送FIN/ACK)由操作系统自动完成,与应用层的主动数据传输无关。这种分层设计使得TCP既能保证连接可靠性,又能在异常情况下快速释放资源。

在这里插入图片描述
当我们知道了有了上面的知识储备,再来理解接下来的问题就比较简单了:

为什么称为四次挥手,而不利用捎带应答优化为三次挥手呢?

这是因为,当客户端与服务器断开连接时,只能说明客户端已经没有想要发送给服务端的请求了,但是服务端这时仍有可能存在需要发送给客户端的数据,所以第二次挥手和第三次挥手不可合并。

客户端收到服务发送的FIN为什么要先进入TIME_WAIT状态而不直接进入CLOSE状态?

  • 第一个原因: 第四次挥手存在丢失的可能,需要客户端进行补发。
  • 第二个原因: 它正在等待一段时间,允许老的重复报文段在网络中消逝,防止新的连接收到旧的报文段而导致数据错乱。如:客户端与服务端断开连接后立刻重新建立,这时就一可能受到旧报文的干扰。
    TIME_WAIT状态也称为 2MSL 等待状态,在这个状态下,TCP 将会等待两倍于 MSL(最大段生存期)的时间,有时也被称为加倍等待。每个实现都必须为 MSL 设定一个数值,它代表任何报文段在被丢弃前在网络中被允许存在的最长时间

为什么TIME_WAIT状态的持续时间是2倍的MSL?

为保证客户端发送的最后一个ACK报文段能抵达服务器。因该ACK可能丢失,会让处于LAST - ACK状态的服务器收不到对FIN - ACK的确认报文,服务器会超时重传此FIN - ACK,客户端收到后会重传确认、重启时间等待计时器,最终双方可正常关闭。若客户端不等待2MSL,发送完ACK就直接释放关闭,一旦ACK丢失,服务器便无法正常进入关闭连接状态,具体如下:

  • 保障客户端最后一个ACK报文段抵达服务端
    该ACK报文段有丢失可能,会使处于LAST - ACK状态的服务端,收不到已发FIN + ACK报文段的确认。服务端超时重传FIN + ACK报文段后,客户端能在2MSL时间内收到此重传报文,接着客户端重传确认、重启2MSL计时器,最终客户端和服务端都进入CLOSED状态。若客户端在TIME - WAIT状态不等待,发完ACK就立即释放连接,就无法收到服务端重传的FIN + ACK报文段,不会再发确认,服务端也就无法正常进入CLOSED状态。
  • 避免“已失效的连接请求报文段”混入本连接
    客户端发完最后一个ACK报文段,再过2MSL,可让本连接存续期间产生的所有报文段从网络消失,新连接就不会出现这类旧连接请求报文段。
    网络中可能存在多个源IP、目的IP相同的连接。若不等待就复用相同源端口和目的端口,之前连接的报文可能被误认成新连接的一部分。等待2MSL时长,能确保之前连接的所有报文已在网络消失,避免新、旧连接混淆。

二、TCP协议的机制

此处会在上篇介绍的基础上再次理解

2.1 流量控制

在这里插入图片描述
通过之前了理解我们知道了TCP协议在进行通讯时,通讯双方可以通过16位窗口告诉对方自己的接收缓冲区还剩下多少空间资源,即自己的接收能力,而对方只需要根据获得的接收能力,制定适合的发送方案,这样当对方主机空间资源充足就多发一些,空间资源匮乏就少发一些,从而实现了双方通讯的流量控制。

建立连接后首次发送数据时,TCP 三次握手已提前协商好接收能力:三次握手阶段,前两次(SYN、SYN + ACK 报文)虽不能携带应用层数据,但会交互关键信息,发送方发 SYN 报文时,携带自身初始序列号(ISN)与最大段大小(MSS);接收方回 SYN + ACK 报文,包含自己的 ISN、MSS ,还会告知「窗口」大小,体现当前可接收数据量。第三次握手的 ACK 报文可携带应用层数据,待三次握手完成,双方已明确彼此序列号、段大小、窗口大小,基于这些协商好的参数,就能合理调整发送速度、控制数据量,保障首次及后续数据传输适配接收方能力 。

流量控制工作方式:
在这里插入图片描述

在 TCP 流量控制流程里,当主机 B 的接收缓冲区被占满,会将窗口大小置为 0 ,主机 A 接收此状态后,会立即暂停新数据发送。若主机 A 等待时长超出重发超时(RTO)阈值,却仍未收到主机 B 的窗口更新通知,为避免因更新丢失陷入“永久停发”,主机 A 会主动发起窗口探测,通过发送探测包,强制获取主机 B 当前实际可接收数据的窗口状态 。

而从主机 B 角度,一旦其处理完缓冲区数据、窗口可用空间恢复,会主动发送窗口更新报文,将新的窗口大小告知主机 A 。这种“主动更新 + 被动探测”的双机制协同运作,让主机 A 能持续、精准掌握主机 B 的接收窗口动态,为基于滑动窗口的流量控制筑牢基础,确保数据发送速率与接收端处理能力适配 。窗口更新的两种机制,谁先起作用就用谁,一定程度上提高了效率

2.2 滑动窗口

2.2.1 滑动窗口的工作原理

在介绍滑动窗口协议之前,我们先来想一想,如何来保证发送方与接收方之间,每个包都能被收到,并且是按次序的呢?

TCP协议的补发机制,当发送方发送的数据丢失时,需要具备重新发送的能力,也就是说发送方发送的数据,在未确认该数据已被对方主机成功接收的时间里,发送方就需要一直维护这些数据,这一点是不难理解的。

现在问题又来了,发送方该如何维护这些数据,而且在发送方不仅存放有已发送的数据,还有未发送的数据,该如何来区分他们呢?
在这里插入图片描述
TCP协议规定,发送缓冲区的数据可以分为以下四个部分:

  • 已方并且收到确认ACK的数据,这些数据允许被覆盖。
  • 已发送未收到确认ACK的数据
  • 未发送,但接收方准备接收的数据
  • 未发送,且接收方未准备接收的数据

我们将处在,中间两部分的数据称为滑动窗口。

滑动窗口的大小是怎么设定的?未来大小又是怎么变化的?

  • 滑动窗口的大小要始终和对方的接收能力挂钩,因为滑动窗口的大小=一次批量化发送数据段的多少,我们知道TCP有流量控制,而一次批量化发送数据段的多少,其实是由对方的16位窗口大小和网络的拥塞情况共同决定的(后面会介绍)共同决定的,所以滑动窗口的大小=min(16位窗口大小,拥塞窗口大小)。

滑动窗口的更维护:
我们只需要使用两个指针startend就可以将窗口描述出来

  • start:表示滑动窗口起始位置下标,如图中start=16
  • end:表示滑动窗口结束位置下标,如图中end=20

滑动窗口在更新前会收到处于“已发送未收到确认ACK的数据”区域的确认ACK,如:收到的确认报文中,确认序号为19(表示19之前的数据均收到了),这时start指针就会由16更新为19,而end指针则会根据该报文中的16位窗口大小来决定如何调整。
在这里插入图片描述
在上一篇文章中我们介绍的这种机制就是基于滑动窗口优化的,将串行通信,优化为并行通信:

  • 送前四个段的时候, 不需要等待任何ACK, 直接发送;
  • 收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据;
  • 依次类推只有确认应答过的数据, 才能移除滑动窗口;
  • 窗口越大, 则网络的吞吐率就越高;

在这里插入图片描述

在这种情况下:
在这里插入图片描述
即使出现,部分ACK丢了也不要紧, 因为可以通过后续的ACK进行确认(应答机制规定,收到的确认序号表示,该序号前的数据已全部被接收,如:对图中1001、3001、4001应答丢失,只要我们收到了5001确认应答序号,那么5001之前的数据均被接收了)

现在我们有遇到了一个问题,如果1~1000这个数据包丢了,而处于滑动窗口的其他数据,被正确的接收了,那么应答是什么样的呢,TCP又该如何处理呢?为解决这个问题TCP引入了快重传机制

补充:
能不能向左滑动呢?

滑动窗口一定不会向左滑动,左边的数据都是已经被ACK的,而滑动窗口内的数据是未被ACK的,向左滑动是不合理的!

滑动窗口大小会保持不变吗?会变大吗?会变小吗?变化的依据又是什么?

  • 滑动窗口大小会保持不变,比如上面我们说丢包的情况,滑动窗口的大小和位置都会保持不变。
  • 滑动窗口是会变大的,比如对方的接收能力变好了(空间资源充足,带宽资源充足),那么滑动窗口就可以增大,发送数据时就可以一次批量化的发送更多的数据了。
  • 滑动窗口也是会变小的,比如对方的接收能力下降(网络变差、空间资源紧张),而此时滑动窗口也会跟着下降。

2.2.2 基于滑动窗口快重传

在这里插入图片描述
1001~2000的数据包丢失后,即使此后的数据包都成功到达主机B,主机B回复的应答序号依然是1001(表示期望收到的下一个报文是从1001开始的),直到主机A对1001~2000数据包重发,确认序号才改变。
在上述过程中主机A一直没有收到1001的应答序号,所以主机A的滑动串口一直处于图中状态:

在这里插入图片描述

当主机A连续收到了三个相同确认应答序号,就触发快重传,此过程并不需要等到,等待超时,触发超时重传

引入快重传的优势:

  • 如果等待1001~2000应答超时,触发超时重传,这种情况下,不仅延长了等待,而且已经到达的数据,依然面临着应答接收超时问题(要知道他们发送的时间间隔很短,而且主机B一直没有对他们做出应答),而快重传仅需对丢失的数据包进行补发即可。

补充:既然快重传那么优秀,为什么还要有超时重传呢?
快重传是有触发条件的,只有收到连续三个相同确认序号的确认应答时,才会触发快重传,当发送的最后一个数据包丢失后就只能使用超时重传来兜底了。

2.3 拥塞控制

2.3.1 拥塞控制的定义

此前探讨的各类TCP策略与机制,聚焦于通信两端的交互逻辑,未涉及中间网络的数据传输环节。实际场景中,若出现大量丢包问题,除两端自身因素外,还可能源于中间网络故障;当网络因异常或压力过大引发丢包时,就需要TCP借助拥塞控制机制来解决。

若客户端向服务端发送一批数据段,仅丢失少量时,客户端无需特殊处理,超时重传即可;但丢失数量极大时,客户端会判定网络存在问题——毕竟在流量控制机制作用下,发送的数据段本就适配接收方能力,此时大面积丢包,大概率源于网络环境异常,而遇此情况,TCP 需通过拥塞控制机制缓解网络压力 。

当发生大面积网络丢包时,TCP 若仍单纯采取超时重传策略,会引发更严重问题:网络中并非仅通信的两台主机,众多主机同时通信,若都因丢包触发超时重传,会向本就拥塞的网络疯狂“塞”报文,进一步加剧带宽窄、数据拥塞等网络故障,导致大面积丢包反复出现。因此,合理做法是让识别到网络拥堵的主暂缓发送报文,待网络恢复后再正常通信,这种在网络大面积丢包时,让主机暂止或少发报文、等待网络恢复的机制,就是 TCP 的拥塞控制

可能有人觉得,在网络拥塞时就算我停止发送报文,网络就不拥塞了吗?一台主机能有那么大的分量吗?在理解这一点时,我们要清楚,网络资源属于共享资源,识别到网络异常的主机不单是我们的这台,当其他主机识别到网络拥堵时也会采取这种机制,那么当大部分主机都停止向网络中塞数据了,网络就有时间将拥堵的数据“疏通”了

2.3.2 拥塞窗口机制的实现

TCP 引入拥塞窗口 实现拥塞控制,核心逻辑为:

  • 初始阶段:连接建立后,拥塞窗口初始值设为 1(以报文段为单位,代表首轮可发 1 个报文段 );
  • 增长规则:每收到一个对端的 ACK 确认,拥塞窗口 +1(体现“慢启动”思路,逐步试探网络承载能力 );
  • 实际发送窗口:发送数据时,需将拥塞窗口接收端通告窗口(由接收方反馈的可接收窗口) 对比,取两者最小值 作为实际可用的发送窗口(即 实际发送窗口 = min(拥塞窗口, 接收端通告窗口) )。

简单说,拥塞窗口是 TCP 感知网络拥塞、动态调控发送速率的“阀门”,通过慢启动(后面介绍)、与接收窗口协同,在网络和接收端间找平衡,避免盲目发数据压垮网络 。

当网络出现拥塞时,发送端会发送拥塞窗口大小的探测报文段,用于探测网络状况如何。

2.3.3 慢启动策略

在这里插入图片描述

拥塞窗口调整算法:

  • 慢启动: 通信初始阶段,使用极小的窗口探测网络状况。若一开始就发送大量流量,遇到网络拥堵时,会加剧本就紧张的网络带宽压力。
  • 指数增长: 在网络通畅不堵塞的传输过程中,拥塞窗口大小呈指数级增长。由于其增长速度极快,若不加以限制会导致窗口值过大,因此需要设定一个阈值(初始大小为接收端窗口大小)进行控制。
  • 线性增长: 当窗口大小指数增长至设定阈值后,增长模式转为线性增长。尽管线性增长速度慢于指数增长,但仍会使发送速度逐渐加快,当接近网络传输极限时,可能出现丢包。
  • 拥塞窗口回归小窗口: 当传输中出现大量丢包,判定网络发生拥堵时,会将窗口大小调整为初始的小窗口,并将阈值调整为上次出现拥塞时窗口大小的一半,随后重新进入“指数增长+线性增长”的循环过程。

2.4 延时应答

在网络传输过程中,需频繁执行 I/O 操作来访问网卡等外设,而过于频繁的外设访问会降低 TCP 通讯效率。若能一次性发送较多报文数据,可在一定程度上提升通讯效率,但两主机通讯时,一次发送数据量受接收方窗口大小限制。因此,为提高效率,TCP 引入延时应答机制 ,通过适当延迟对接收窗口的应答反馈,争取让发送方一次发送更大量的数据。

延迟应答是 TCP 协议中一种用于提高传输效率的机制,它的诞生围绕着滑动窗口展开。在 TCP 传输过程中,窗口大小与传输效率紧密相关,通常情况下,窗口越大,单位时间内能够发送的数据量就越多,传输效率也就越高 。于是,一个关键问题摆在面前:在确保网络不会出现拥堵的前提下,是否存在可行的方法,来尽可能地提升窗口大小呢?

接收端接收到数据后,并不一定立即处理数据,这些还未来得及处理的数据会暂存在接收缓冲区,如果接收数据的主机立刻返回 ACK 应答,此时返回的窗口可能较小;而若在返回 ACK 时适当拖延响应时间,就能利用这段时间给接收方留出更多消费数据的机会——接收缓冲区的剩余空间会随之变大,窗口大小也能相应提升。需要注意的是,延迟应答并非必然让上层在这段时间内取走缓冲区数据,这是一种概率性事件:大概率情况下上层会取走数据,从而恰巧提高传输效率;若未取走,也只能接受这一结果。毕竟,世界上没有绝对的事情,任何事件都存在概率差异,延迟应答的优势也在于它属于较大概率能提升效率的机制。

显然,并非所有数据包都适合延迟应答,TCP 对此设置了双重限制:

  • 数量限制:每接收 N 个数据包就必须应答一次,避免无限制延迟导致发送方超时重传;
  • 时间限制:若延迟时间超过最大阈值,无论接收了多少包,都必须立即应答。

具体的 N 值和超时时间因操作系统而异,通常 N 取 2(即每接收 2 个包应答一次),超时时间设为 200ms(超过此时长则强制应答)。

三、TCP协议的其他特点

3.1 面向字节流

两主机通信时,发送缓冲区中的数据由TCP自主发送,其字节流大小会依据窗口大小、拥塞控制、流量控制等因素动态调整:若发送的字节数过长,TCP会将其拆分为多个数据包发送;若字节数过短,TCP可能先将数据暂存在发送缓冲区,待合适时机再一并发送。

接收数据时,数据经网卡驱动程序传入内核的接收缓冲区,由TCP自主接收,接收的字节流大小则会根据窗口大小、确认机制等因素动态调整。

由于缓冲区的存在,TCP程序的读写无需一一匹配。例如:写入100字节数据时,既可以调用一次write函数写入100字节,也可以调用100次write函数每次写入1字节;读取100字节数据时,无需考虑写入时的方式,既可以一次read100字节,也可以每次read1字节、重复100次。

对TCP而言,发送缓冲区中的数据仅是一个个字节,它会为每个字节分配序号,并通过序号和确认号保障字节流的顺序与完整性。TCP的核心任务是将这些数据准确无误地送达对方接收缓冲区,而数据的具体含义则由上层应用解释——这就是所谓的“面向字节流”。操作系统同样如此,它仅关注缓冲区的剩余空间,而不关心数据本身的内容。

3.2 粘包问题

首先要明确:

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

导致粘包问题的因素是报文之间的边界不清晰:

粘包问题指的是发送方发送的多个数据包在接收方被合并为一个数据包的现象。这是因为 TCP 是面向字节流的协议,它不关心数据的逻辑结构,只负责将字节流按序和完整地传输给对方。TCP 在发送或接收数据时,都会通过缓冲区来进行优化,根据网络状况和窗口大小来动态调整发送或接收的字节流的大小。这样就可能导致发送方发送的多个数据包被拼接在一起,或者一个数据包被拆分成多个部分。

解决办法:

  1. 对于定长的包,保证每次都按固定大小读取即可。
  2. 对于变长的包,可以在报头的位置,约定一个包总长度的字段,从而就知道了包的结束位置。
  3. 对于变长的包,还可以在包和包之间使用明确的分隔符(该分隔符是由程序员自己定的)。

总结

本篇文章就分享到这里,由于TCP协议较为复杂,协议规则较多,文中有许多知识并未介绍完,后续文章会继续补充。

http://www.dtcms.com/a/268787.html

相关文章:

  • 借助HarmonyOS SDK,《NBA巅峰对决》实现“分钟级启动”到“秒级进场”
  • 【7】PostgreSQL 事务
  • SRAM与三级缓存(L1/L2/L3 Cache)的关系
  • 芯谷科技--高性能双运算放大器D358
  • 第二届云计算与大数据国际学术会议(ICCBD 2025)
  • 火山引擎Data Agent全面上线售卖!以企业级数据智能体,重构数据应用范式
  • PostgreSQL中的HASH分区:原理、实现与最佳实践
  • 查看WPS Ofice是64位还是32位
  • 腾讯云 CDN 不支持 WebSocket 的现状与华为云 CDN 的替代方案-优雅草卓伊凡
  • 缺乏项目进度追踪工具,如何选择适合的工具
  • 中电金信 :十问高质量数据集:金融大模型价值重塑有“据”可循
  • 案例分享:应用VIC-3D High-Speed FFT进行吉他拨弦振动的工作变形ODS测量
  • QML中的Item
  • 【银行测试】手机银行APP专项项目+测试点汇总(二)
  • RESTful API概念和设计原则
  • C++之string类的实现代码及其详解(中)
  • 软件之禅(十二)面向对象和市场经济---平等性原理
  • 对象存储-OSS
  • PC端基于SpringBoot架构控制无人机(三):系统架构设计
  • Vite 常用配置详解
  • 创造一个无限可能的机器人世界!——Genesis开源项目了解一下
  • 【Linux | 网络】网络基础
  • Java面试宝典:异常
  • 145.在 Vue3 中使用 OpenLayers 设置原始图、模糊、色相翻转、阴影效果
  • 创客匠人创始人IP打造实录:从行业观察者到生态构建者
  • 如何解决pip安装报错ModuleNotFoundError: No module named ‘datetime’问题
  • 软件架构升级中的“隐形地雷”:版本选型与依赖链风险
  • 用c#一款U盘批量按扇区复制的程序
  • Nat.C|RiNALMo:通用 RNA 语言模型新突破,3600 万序列预训练,跨家族结构预测、剪接识别与功能注释全能泛化
  • grant之后还需要跟flush privilege吗?