☰
网络原理(三)TCP通信十大核心机制解析
2026/10/10 12:07:44 网站建设 项目流程

前言

hello hello💕,这里是洋不写bug~😄,欢迎大家点赞👍👍,关注😍😍,收藏🌹🌹
网络原理(一)和网络原理(二)中一共解析了TCP通信中的三个重要机制:确认应答、超时重传、连接管理(三次握手、四次挥手),这篇博客就会解析其他的7个常用机制
这篇博客中也会把TCP报文格式的剩余部分给解析完🐵
🎆个人主页:洋不写bug的博客
🎆所属专栏:JavaEE学习
🎆铁汁们对于JavaEE的各种常用核心语法,都可以在上面的前端专栏学习,专栏正在持续更新中🏀,有问题可以写在评论区或者私信我哦~

1,滑动窗口

有一类算法题,解决方法也叫滑动窗口(双指针,两个指针都往一个方向移动,围成的区域的变化就像是滑动窗口),但滑动窗口这个概念最早来自计算机网络(TCP)的

在前面的确认应答和超时重传机制下,铁汁们想下,发送方发送完成一个数据,是否需要等待返回ACK后,再发送下个数据?

  1. 如果等待返回ACK后,才发送下个数据,这样虽然能保证可靠性,但是效率就比较低,因为接收方接收信息,回复ACK,ACK传回来,都是需要时间的
  2. 如果不等这个数据的ACK回来,就直接发送下个数据,这样就可能会发送的特别快,把接受方的缓冲区冲爆

滑动窗口机制就是用来解决这个问题的,如下图,批量发送4组数据,等待一组的时间,能够同时等待四组,等待时间减少了,效率就高了

那铁汁们思考个问题:上面这个机制,是等4组的ACK都到达了,再继续往后发4组?还是收到一组的ACK,就立刻继续往后发一组?


明显后者的效率是更高的,第1组数据因为先发送,第1组的ACK大概率是会最先到达的,这时候直接发送第5组数据,在第二种方案下,第5组数据ACK到达的时间是一定早于第一种方案的,同理,第6组和第7组数据ACK到达的时间也是早于第一种方案,这些组的ACK到达后又继续发下一组,故第二种方案的效率是更高的



如下图,第1组收到了ACK,就立刻发送第5组的数据,可以看作有一个窗口,窗口中一直有4组数据(这个不是固定的),随着收到ACK,发送新的一组数组,这个窗口是一直向右滑动的,因此这个机制就称为“滑动窗口”🐵
滑动窗口提高了数据传输速度,但是还是有等待时间的,速度是不会超过UDP通信的




那使用滑动窗口时,出现丢包的情况,应该如何处理呢?

如果主机A数据发送给主机B了,主机B发送的ACK丢失,是无所谓的
因为滑动窗口ack确认序号的设定规则是:返回的ack的确认序号表示的是该序号前的所有数据都已经接收到了
如下图,虽然确认序号为1001的ack丢失了,但后面确认序号为2001的ack是正常返回了,就说明前面1 - 1000数据都已经传输过来了,返回确认序号为6001的ack,就说明1 - 6000的数据主机B都已经收到了
(最后一个数据的ack丢包属于特殊情况,后面会提到)



如果主机A发送的数据包丢失了,如下图,主机A发送完10001 - 2000数据后,紧接着发送后面的数据,主机B一边接收后面的数据,一边一直向主机A发送确认序号为1001的ack,也就是反复向A索要开头为1001的数据,当A感知到B多次索要开头为1001的数据时,就会重传1001 - 2000数据
A重传后,B后面返回的ack的确认序号,就正常了,当A收到3个同样的确认序号的ack时,就认为B在索要该数据了(不同系统中这个次数是不同的,主要还是要搞懂这里的逻辑)




滑动窗口的重传机制就称为“快速重传”,也就是多次接收到同一个确认序号的ack后,触发重传;前面还提到了超时重传,超过一定时间没有收到ack后,就进行重传操作

  1. 当TCP传输的数据量较大时,就会触发滑动窗口,采取快速重传机制
  2. 当TCP传输的数据量较小时,就是按照确认应答和超时重传机制

前面提到的最后一段数据ACK丢包的情况,这时候就使用超时重传机制,一段时间后没有收到ACK,就把最后一段数据再重发一遍

2,流量控制

TCP传输较大的数据时,就用滑动窗口机制来提升效率,前面是一个窗口中放四组数据,窗口越大,放的数据越多,传输速度就越快
但是窗口也不能无限大,如果发送数据的速度过快,把接收方的缓冲区塞满,后面再发送的一些数据,就会被直接丢弃,就会影响可靠性

那如何衡量窗口的大小,在保证可靠性的同时,让数据传输的速度尽量快一点,这就要用到流量控制机制

如下图,发送方发送的数据会先到接收缓冲区,接收方调用read之类的操作从接收缓冲区读取数据,接收缓冲区就类似于阻塞队列,如果里面没有数据,接收方调用read就会阻塞

接收方的处理能力,就是接收方应用程序调用read的速度,以及每次read读取多少数据,这些指标是应用程序自己规定的,不太好衡量,因此就引入了另一个指标来衡量接收方的处理能力,直接看接收缓冲区剩余空间的大小



如下图,可以把缓冲接收区看作是一个水桶,水桶上有两个孔,一个孔进水,一个孔出水,通过看桶中剩余空间的大小来判断进水速度和出水速度的差异
如果桶中剩余空间大,就说明出水(接收方read)的速度更大,如果桶中剩余空间小,就说明进水(发送方发送数据)的速度更大

接收方返回ACK报文时,就在TCP报头中把接收缓冲区剩余空间大小的数值,放到ACK的报头中,等发送方知道ACK后,就能根据这个数值判断出接收方的处理速度,这个数值就是存到了TCP报头中的16位窗口大小部分

窗口大小为16位无符号整数,转为十进制也就是65535,TCP传输是字节流,大小单位就是字节,那是不是接收缓冲区最大就是64KB?(网络协议中,K就是1024)
并不是这样,TCP报文部分还有个选项,方便后续的扩展,可以在选项中设置窗口扩展因子,通过位运算,把二进制位进行左移窗口扩展因子位,每左移一位,就相当于乘以2,这个是呈指数级增长的,这个数值就会变得很大(想复习下位运算的铁汁可以看博主的运算符解析博客,链接放在下面了🐵)

Java运算符详解



流程如下图,主机A先发送了一组数据,主机B返回的ACK中显示窗口大小为3000,这里A再连续发送三组数据,主机B返回的ACK中显示窗口大小为0,这时候主机A就不再主动发送数据了(图中A发送1 - 4000数据时,B是没有处理数据的)




当主机A收到显示窗口为0的ACK时,就会停止发送数据,那后面A如何知道B的缓冲区剩余空间是否为0,判断能不能继续发送数据?

  1. 当主机B的缓冲区剩余空间不为0时,就会主动向主机A发送窗口更新ACK
  2. 只靠1的话,这个窗口更新ACK是可能会出现丢包的,一旦丢包,那主机A就卡死了,第2条机制就是用来兜底的,主机A每隔一段时间就会向主机B发送窗口探测包(不包含业务数据),让主机B返回ACK

3,拥塞控制

拥塞控制就是限制滑动窗口的发送速率,设定滑动窗口的大小不能只看接收方处理数据的能力,因为两个主机通信时,是有很多中间节点(路由器、交换机)作为数据中转站的,还要综合考虑这些中间节点处理数据的速度

那就既不能让接收方的数据缓冲区溢出,也不能让中间节点过载
流量控制是看接收方应用的数据处理速率,拥塞控制是看中间链路节点的承载能力,发送窗口的大小取决于传输速度更慢的那个,这个就类似于木桶效应




中间节点的承载能力理论上是不好计算的,拥塞控制的核心就是通过实验的方式来确定滑动窗口的大小,先按照比较小的速率发送数据,看下是否丢包:

  1. 如果丢包,说明中间链路有节点顶不住了,就减小窗口大小,降低速度
  2. 如果不丢包,就说明中间链路的节点还能再加数据,就增大窗口大小,提升速度

关于拥塞控制实验的具体流程,如下图:

  1. 这个横坐标就是数据实验的轮次,纵坐标的单位是“份”,一份有多个字节,这个具体看传输时的设定
  2. 刚开始,网络的畅通情况是未知的,初始窗口是非常小的,这个就称为“慢启动”
  3. 慢启动之后如果数据不丢包,窗口大小就会按照指数方式快速增长
  4. 当指数增长到一定程度时(达到阈值),指数增长就会变成线性增长,如果一直指数增长的话,就很可能某次翻倍后,一下超出上限很多,直接把中间路由器冲爆了
  5. 线性增长到一定程度,网络承载能力就达到上限了,出现丢包(收到三个重复的ACK)
  6. 达到上限时有两种处理方法:
    ①Tahoe版本,出现丢包后,回到最初的慢启动窗口大小,接下来重复指数增长/线性增长的过程(阈值会减小)
    ②Reno版本,出现丢包后,重新计算阈值(丢包的窗口大小/2),从阈值开始作为新的拥塞窗口,继续线性增长(相比Tahoe版本,省略了指数增长的过程),现在Tachoe版本因为传输速率不稳定,大起大落,已经废弃了

4,延时应答

TCP 数据传输是基于滑动窗口的,可靠性由确认 + 重传保证;重传机制主要包括超时重传和快速重传,在滑动窗口中,返回确认序号为2001,就意味着2001序号前的数据都接收到了,滑动窗口过程如下图,每次接收方收到数据后,立刻返回ack



延时应答就是让接收方收到数据后,不立刻返回ack,先延时一段时间
例如收到1 - 1000数据时不发送ack,后面等收到1001 - 2000的数据时再返回确认序号为2001的ack,就说明2001之前的数据都收到了,确认序号为1001的ack也就不用发了,少发送一次ack,开销就降低了,如下图:

有的铁汁会说:每次都延时应答,那发送2001这里不是也应该延时,等到3001一起发送吗,为什么2001这里发送了呢?

这是因为在大多数情况下延时应答规定最多攒 2 个报文,收到 2 个有序段就立刻输出 ACK,不会无限攒下去

那延时应答并不会触发超时重传,因为延时应答的时间是要远远小于超时重传的等待时间的
RFC(互联网规范文档) 1122 规定:延时应答的延迟不能超过 0.5 秒。
RFC 6298 建议:RTO(Retransmission Timeout,超时重传时间) 最小值通常为 1 秒。
日常开发中,延时应答通常是 40ms~200ms 左右,远小于 RTO

5,捎带应答

网络通信中,经常是一问一答的模型,客户端发起request,服务器返回response

如下图,服务器在收到客户端的请求后,立刻返回ack,接下来服务器需要经过一定的时间来计算响应再返回,如果服务器计算响应的时间比较短的话,就可以把返回ack和返回响应两步合并在一起,也就是先不返回ack,响应报文的ack值为1,32位确认序号和16位窗口大小中设置相应的值
把两个报文合并为一个TCP报文,减轻了中间节点的压力,也节约了主机系统的开销

捎带应答经常和和延时应答搭配起来使用,延时应答会等待一段时间才发送ack,这样就算服务器计算响应需要一段时间,也能将返回ack和返回响应报文合并在一起(延时应答的等待时间是有限的,如果服务器处理响应的时间比等待时间长,那仍然是合并不了两个报文的)

6,面向字节流

TCP是面向字节流的,在读取/写入数据时,操作是非常灵活的,例如读取100个字节:

  1. 一次读10个字节,10次完成
  2. 一次读取20个字节,5次完成
  3. 一次读取50个字节,2次完成
    …




TCP字节流的特征:收到多个TCP数据报的时候,把所有的载荷混到一起,放到接收缓冲区中
这时候就会出现“粘包问题”,如下图,发送方给接收方传输一些数据,接收方分用的时候,会去掉报头,把载荷内容放到接收缓冲区中

接收方的应用程序,read的时候,就会有多种可能性:

  1. a a a b b b c c c
  2. aa ab bb cc c
  3. aaa bbb ccc
  4. aaab bbcc c




粘包问题,粘的是应用层的数据包,包的边界比较模糊,好像黏上了一样,解决粘包问题,需要从应用层入手,合理的设计应用层协议,让包之间的边界比较清晰,这样才能确保读到一个完整的应用层数据包,具体就有两种方案:

  1. 通过特殊的分隔符,来区分包的边界
  2. 在应用层数据包开头的地方,通过固定长度,约定整个应用层数据包的长度

第1种方案,选用分隔符时,要确保包的数据部分不包含分隔符,ASCII 0‑31 属于控制字符(不可打印、不可见),就有一部分专门设计用来做数据分隔,如下图:

前面网络编程(三)博客中解析的TCP回显服务器的代码,使用的分隔符就是\n,客户端构建请求发送给服务器,用的是writer.println(request),发送请求的时候,就会在末尾加个/n,服务器读取请求用的是request = scanner.next(),就是读到空白字符(空格、换行、回车、制表符、分页符…)就结束了



第二种方案应用程序在read的时候,就能根据长度,确定每次读多少个字节读到的是完整的应用层数据包

在文件操作中,使用文件存储多个结构化数据,也是会涉及到粘包问题的,例如用文件来存储多个学生的信息,就可以约定每个学生信息占一行,使用\n作为结束标记,作为分隔符

对于UDP来说,就不存在粘包问题,UDP是面向数据包传输的,TCP是面向字节流传输的,UDP的应用层每次从接收缓冲区中读取到的都是一个完整的数据包

10,异常情况

在进行网络通信的时候,会出现下面这些异常情况:

  1. 进程崩溃
  2. 主机关机(正常流程)
  3. 主机掉电(直接拔电源)
  4. 网线断开

进程崩溃是正常的流程,正常调用close,干掉进程,关闭对应的文件描述符,就属于是进程崩溃
只要是进程退出,都会释放文件描述符,触发FIN,开始四次挥手,TCP的连接还会保留一会,等四次挥手完成再删除



对于主机关机,就会杀死所有的线程,也会触发FIN,开始四次挥手:

  1. 如果关机的速度比较慢,那四次挥手就可能会执行完
  2. 如果关机的速度比较快,刚发FIN,机器就关了

对于关机比较快的这种情况,关机方发完FIN就关机了,对端可以正常的返回ack和FIN,只是发送的FIN不会收到ack,尝试重传几次FIN,还是没有收到ack,对端就直接放弃连接(删除存储的连接信息)




主机掉电(台式机直接拔电源)就是直接啥都没了,是来不及发送FIN的,这时候分为两种情况:

  1. 掉电的一方是接收方,对方是发送方,对方继续发送数据,但是收不到ack,就会触发超时重传,超时重传后还是收不到ack,就向掉电方发送一个复位报文(RST),表示要断开当前连接,RST发完后就断开TCP连接,而且RST是单方面发送的,不关心对方有没有收到(因为发送RST时本身通信就已经有异常了)
  2. 掉电的一方是发送方,那接收方的感觉就是对方突然不发送数据了,接收方就会阻塞等待,那接收方如何判断只是这会发送方没有数据发送,还是说发送方已经挂了呢?
    这就要靠“心跳包”,接收方会周期性的和发送方交换“心跳包”,接收方给发送方主动发送个无业务的数据的报文,发送方返回个ack,这个就称为“心跳包”,也就是判断对方有没有挂掉
    如果对方有应答,就认为对方是正常工作的,如果心跳包也没有应答,就认为对方已经挂掉了,就可以单方面的释放连接了

在服务器机房中,也会通过心跳包来验证服务器有没有挂掉
如下图,客户端向服务器发送请求,请求量可能非常大,一台业务服务器处理不过来,就需要多台业务服务器分工合作
网关服务器就是把收到的请求分发给各个业务服务器,例如收到了2000个请求,有5个业务服务器,那就每个服务器分配400个请求来处理
如果有一个业务服务器挂掉了,网关服务器需要及时的发现,重新分配请求给剩下的服务器,这样就能保证整体的服务是稳定可用的
网关和业务服务器之间,就是通过“心跳包”来感知存活状态的,这个“心跳包”的发送频率很高,进而快速做出响应,维持服务的稳定性




网线断开也是一瞬间的事情,来不及发送FIN,处理过程跟主机掉电是类似的,这里就不再赘述了

11,TCP报文补充

至此,TCP报文中的大部分内容已经解析完成了,标志位中的URG和PSH和16位紧急指针也没有解析

标志位已经解析了四个

  • ACK:应答报文
  • RST:复位报文(单方面放弃连接)
  • SYN:同步报文
  • FIN:结束报文

URG(Urgent)为1,就表示紧急指针是有效的,正常情况下,tcp数据的传输是顺序传输的,紧急指针就意味着后面有些数据要先传输(插队),16位紧急指针的值就表示从当前位置往后多少个字节位置的部分,要进行插队
PSH(Push)就是催促接收方,尽快把缓冲区的数据交给应用程序

URG和PSH都用的比较少,这里铁汁们了解下即可

结语💕💕

在网络原理(一)(二)(三)三篇博客中,就完成了TCP通信十种核心机制:

  1. 确认应答
  2. 超时重传
  3. 连接管理(三次握手,四次挥手)
  4. 滑动窗口(快速重传)
  5. 流量控制
  6. 拥塞控制
  7. 延时应答
  8. 捎带应答
  9. 面向字节流(粘包问题)
  10. 异常情况(心跳包)

当然,网络通信的机制是非常多的,这里只是解析了工作中经常涉及到的10种

有个经典的面试题,问如何用UDP实现可靠传输
其实还是在考察TCP,要基于UDP实现可靠传输,就需要在应用层自己写代码,来实现可靠传输,写代码的思路还是参考TCP的做法,例如确认应答(给数据包编号)、超时重传、滑动窗口、流量\拥塞控制…(这里只是让我们说一下,是不会让当场去写代码实现的)

也解析完成了TCP和UDP的报文格式和通信特征:

  • TCP是有连接的,可靠传输,面向字节流,大部分传输情况优先考虑TCP
  • UDP是无连接的,不可靠传输,面向数据报,经常用于机房内部的数据传输,因为机房内部传输丢包概率很低,而且要求数据传输效率高

像竞技类网游,既需要可靠性(不是那么严格),也需要传输效率,就可以用KCP协议(传输层不只是TCP和UDP协议)


以上就是今天的所有内容啦~完结撒花~🥳🎉🎉

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询