【网络知识必知必会】传输层TCP协议(干货分享+图文详解+快速上手)

news/2024/5/18 12:35:47 标签: 网络, tcp/ip, 单片机, udp, 网络协议, python

文章目录

  • 前言
  • 1. TCP 是什么
  • 2. TCP 协议端格式
  • 3. TCP 特性
    • 3.1 确认应答
    • 3.2 超时重传
      • 超时重传情况
      • 如何进行数据去重
      • 等待时间多久开始重传
    • 3.3 连接管理
      • 三次握手(建立连接)
      • 四次挥手(断开连接)
    • 3.4 滑动窗口
      • 如果出现了丢包, 如何进行重传?
    • 3.5 流量控制
    • 3.6 拥塞控制
    • 3.7 延迟应答
    • 3.8 捎带应答
    • 3.9 面向字节流
      • 粘包问题
  • 4. TCP 异常情况的处理
    • 4.1 进程崩溃
    • 4.2 主机关机
    • 4.3 主机掉电
    • 4.4 网线断开
  • 总结


前言

在传输层中, 有两个知名协议是需要我们必知必会的, 一个是UDP协议, 一个是TCP协议, 在上文中, 我们主要讲解了 UDP 协议的一些知识, 本文则来重点讲解一下 TCP 协议. 相比于 UDP 协议, TCP 协议更加常见, 且拥有很多比较使用的特性, 在传输层中, 也经常会使用到 TCP 协议.

关注收藏, 开始学习吧🧐


1. TCP 是什么

TCP, 即Transmission Control Protocol, 是一个传输控制协议. 如它的名字一样, TCP 是用来对数据的传输进行一个详细的控制.

2. TCP 协议端格式

在这里插入图片描述

  • 16位源/目的端口号:表示数据是从哪个进程来,到哪个进程去。端口号是很重要的一个部分,知道了端口号,才能进一步的确认这个数据报交给哪个应用程序。
  • 32位序号/确认序号:后面详细讲。
  • 4位TCP报头长度:表示该TCP头部有多少个32位bit(有多少个4字节);所以TCP头部最大长度是15 * 4 = 60。
  • 6位保留位:相比于 UDP 固定的长度,设计 TCP 时,给之后的扩充留了个退路,虽然现在不用,先占个位置,留着有需求了再使用(大概率用不到了)。
  • 6位标志位
    • URG:紧急指针是否有效
    • ACK:确认号是否有效
    • PSH:提示接收端应用程序立刻从TCP缓冲区把数据读走
    • RST:对方要求重新建立连接;我们把携带RST标识的称为复位报文段
    • SYN:请求建立连接;我们把携带SYN标识的称为同步报文段
    • FIN:通知对方,本端要关闭了,我们称携带FIN标识的为结束报文段
  • 16位窗口大小:后面再说
  • 16位校验和:发送端填充,CRC校验。接收端校验不通过,则认为数据有问题。此处的检验和不光包含TCP首部,也包含TCP数据部分。
  • 16位紧急指针:标识哪部分数据是紧急数据;
  • 40字节头部选项:暂时忽略;

3. TCP 特性

TCP对数据传输提供的管控机制,主要体现在两个方面:安全和效率。这些机制和多线程的设计原则类似:保证数据传输安全的前提下,尽可能的提高传输效率。

TCP 的几个知名特点:有连接,可靠传输,面向字节流,全双工。而可靠传输是设计内核时就实现的一个机制,就是通过 TCP 的确认应答来实现的。

3.1 确认应答

在这里插入图片描述
TCP将每个字节的数据都进行了编号, 即为序列号.

在这里插入图片描述

  • 每一个 ACK(Acknowledge应答) 都带有对应的确认序列号, 意思是告诉发送者, 我已经收到了哪些数据, 下一次你从哪里开始发.
  • 由于 TCP 是面向字节流的, 不是按照 “条” 为单位来进行发送, 而是针对字节进行编号来发送的.
  • 上图中表示, 1001序号之前的数据, 我主机B这边都收到了, 接下来你给我从1001开始发送. 在报文格式中有一个32位确认序号就是用来记录这个确认号的, 确认序号的数据, 就是收到的最后一个字节的编号加上一.
  • 而32位序号用来标志这一串字节第一个字节的序号, 只要知道这一串字节的开始编号, 结合数据的长度, 每个字节的编号自然也都知道了.
  • 服务器之间需要有办法能区分出, 当前这个报文是普通报文, 还是确认应答报文. 标志位中的 ACK 就可以解决, ACK 为 0 时, 表示这是一个普通的报文, 此时只有 32 位序号是有效的, 当 ACK 为 1 时, 表示这是一个应答报文, 这个报文的序号和确认序号都是有效的.

TCP 通过确认应答来实现数据传输中的稳定性, 可以确保对方能否收到数据, 收到数据的一方还会对此做出应答. 由此可见, TCP 保证可靠性的核心机制就是确认应答. 不过我们接下来要讲的超时重传, 也是 TCP 可靠性机制的一个有效补充.

3.2 超时重传

网络中, 有时候会发生一种情况 — 丢包, 就是发一个数据, 结果丢失了. 这是怎么造成的?

在这里插入图片描述

在主机A向主机B传输中, 会经历很多路由器和交换机, 这些路由器和交换机就像是一座座的 “交通枢纽”, 结构复杂, 每个交通枢纽都有很多数据进行中转, 如果当前设备太繁忙了, 后面新来的数据等太久没被接受, 可能就被丢弃了, 所以在网络负载越高的情况, 数据就越容易被丢失, 丢包现象就产生了.

而超时重传, 就是针对确认应答机制的一个重要补充, 在等待一定时间后, 还没有收到应答, 就要进行重传.

超时重传情况

发生数据丢失, 主要有以下两种情况:

情况一:
在这里插入图片描述

  • 主机A发送数据给B之后, 可能因为网络拥堵等原因, 数据无法到达主机B.
  • 如果主机A在一个特定时间间隔内没有收到B发来的确认应答, 就会进行重发.

情况二:
在这里插入图片描述

  • 主机A发送数据给B后, B收到了, B发送了一个应答报文给A. 但是, 主机A未收到B发来的确认应答, 因为ACK丢失了.
  • 于是, 主机A在等待一段时间后, 没有收到来自B的应答报文ACK, 就会进行重发.

如何进行数据去重

发送端无法判断出当前是哪种情况, 所以采取的措施就是进行重传. 但如果是情况二时, 主机B就会收到不止一条主机A发送来的数据, 所以接收方就得对接收到的数据进行去重. 把重复的数据丢弃掉, 保证读到的数据不会发生重复.

为什么要进行去重?
在多数场景下, 收到重复的数据可能没有太大影响, 但如果是在一些特定场景下, 影响可能就会很大, 比如银行充值系统. 假设我们在银行进行充值100元, 我们主机A发送充值请求给主机B, 由于主机B给主机A发送的应答报文丢失了, 主机A以为没有充值成功, 于是又发送充值请求, 如果不对数据进行去重, 那么主机B收到数据后, 又会进行一次充值操作, 最后一条充值100元的命令, 反而实现了两次, 充了200元!!

所以 TCP 协议需要能够识别出哪些包是重复的包, 并且把重复的丢弃掉. 那么 TCP 是如何进行数据去重的, 如何高效的判定当前数据是已经收到的, 是重复的呢?

这时候我们可以利用前面提到的32位序列号, 就可以很容易做到去重的效果.

TCP 会在内核中, 给每个 Socket 对象都安排一个内存空间, 相当于一个队列, 也称为 “接收缓冲区”, 会将收到的数据全部放到该队列中按序号排列好顺序, 此时就可以通过序号很容易的找到新收到的数据是否重复了. 并且, 由于当前缓冲区采取的是有序排列, 应用程序也不必去考虑数据传输的先后顺序问题了.

等待时间多久开始重传

那么, 超时的时间我们该如何确定, 等待多久没收到ACK我们就进行重传呢?

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

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

  • Linux中 (BSD Unix和Windows也是如此), 超时以500ms为一个单位进行控制. 每次判定超时重发的超时时间都是500ms的整数倍.
  • 如果重发一次之后, 仍然得不到应答, 等待 2*500ms 后再进行重传.
  • 如果仍然得不到应答, 等待 4*500ms 进行重传. 依次类推, 以指数形式递增.
  • 重传到一定程度时, 就会尝试重置 TCP 连接, 也就是发送 TCP 复位报文, 当标志位中的 RST 为 1 时, 代表该报文是一个复位报文.
  • 累计到一定的重传次数, 并且复位操作也无法成功, TCP 会认为网络或者对端主机出现了异常, 就会强制关闭连接.

3.3 连接管理

连接管理主要就是主机进行通讯间的建立连接和断开连接, 在正常情况下, TCP要经过三次握手建立连接, 四次挥手断开连接.

在这里插入图片描述

三次握手(建立连接)

两个主机之间建立连接, 就像是打招呼一样, 先发送一条 “打招呼” 的数据, 这个数据并不会携带别的业务信息. A 和 B 之间要想建立连接, 就得完成 “三次” 这样的打招呼过程.

在这里插入图片描述
这样的四次交互完毕后, 连接就算是建立好了, 此时双方都已经保存了对端的信息. 看起来是四次, 实则是三次, 中间的两次, 能够合并成一次. 在这里插入图片描述
那么为什么要将其两两次合并成一次呢, 在之前网络原理基础中, 我们讲到过, 在服务器之间发送和接受数据时, 有一个重要步骤, 封装和分用, 在这里如果我们不进行合并, 两次交互就需要封装分用两次. 合并之后, 就成为了一次, 降低了成本, 所以能合并时, 我们还是尽量合并.

到底什么是三次握手?

在这里插入图片描述
上图就是一个三次握手最简洁的一个表示, 掌握该过程即可, SYN 是 TCP 标志位中的一位, 当 SYN 为 1 时, 表示该报文是一个申请建立连接的请求, 称为 “同步报文段”, 代表一台主机尝试和另一台主机建立连接. ACK 是应答报文, 之前详细介绍过, 再次不再展开.

三次握手的意义是什么?

意义一:

三次握手, 其实也是 TCP 为了保证可靠性的一个机制, 可以进行 “投石问路”.

TCP 要想保证数据的可靠传输, 就得保证网络路劲是畅通的. 三次握手就是来验证当前网络通信是否畅通, 以及验证每个主机的发送能力和接受能力是否正常.

就好比我们通常在打电话时, 经常会说 “喂喂喂”? 其实就是用来检查对方是否能听到自己说话, 以及检查自己是否能将该声音传给对方.

在这里插入图片描述
三次握手结束后, 我们就可以知道双方的接收设备和发送设备都是正常的, 就可以正常进行后续的通讯了. 所以三次握手缺一都不可, 只有三次才能恰好验证双方的接受和发送能力, 并且将这样的信息同步给了双方.

意义二:

三次握手, 还能起到 “消息协商” 这样的效果. 在通信时涉及到一些参数, 需要双方保持一致的. 通过协商来确定参数具体是多少.

TCP 通信过程中, 有很多信息是需要进行协商的, 比如双方的序列号从几开始, 这样做可以保证, 两次连接, 消息的序号有差异, 从而能够判定出某个消息是否属于这个连接的.

网络中传输消息, 有可能会后发先至, 极端情况下, 某个连接中的消息丢包了, 迟到了很久才重新发送了过来, 此时, 服务器和客户端已经断开了上一个连接, 并进行了一个新的连接. 这个时候, 就可以通过序列号, 来判断出这个是上一个连接的消息, 就可以丢弃了.

生活中的例子:
在这里插入图片描述

综上所述, 三次握手的意义主要就是两方面:

  1. 投石问路, 验证通信路径是否畅通, 双方的发送 / 接受能力是否正常.
  2. 协商必要的参数, 使客户端和服务器使用相同的参数进行消息传输.

四次挥手(断开连接)

既然有见面招手, 那么肯定会有再见挥手. 两个主机之间断开连接, 就像是说再见一样, 挥一挥手说再见. A 和 B 之间要想断开连接, 就得完成 “四次” 这样的挥手过程.

连接时, 是需要通信双方保存对端的相关信息, 而不需要连接时, 就需要及时释放该连接资源.

断开连接时的这个挥手过程与之前讲的握手过程十分相似. 三次握手, 必然是客户端发起的. 而四次挥手, 大多数是客户端主动发起的, 不过服务器也可以主动发起.

到底什么是四次挥手?

在这里插入图片描述
上图就是一个四次挥手最简洁的一个表示, 掌握该过程即可, FIN 是报文格式中6位标志位中的一位, 当 FIN 为 1 时, 表示该报文为 “结束报文段”, 用来发送请求断开连接的报文. 经过上述四个步骤之后, 连接就彻底不再使用了, 双方就可以把各自保存对端的信息资源释放了.

为什么是四次挥手?

可能有读者会敏锐的发现, 这里的 “四次挥手” 过程似乎与 “三次握手” 的过程差别不大, 三次握手可以将中间的 ACK 和 SYN 合并为一次, 为什么这里的四次挥手没有将 ACK 和 FIN 合并为一个步骤呢?

在这里插入图片描述
我们直接公布答案, 四次挥手有的时候确实可以三次完成, 但是大部分时候都是分为四次完成的, 因为中间这两次挥手过程, 不一定能够被合并.

与握手不同, 三次握手的过程都是在内核中, 系统自行控制的, 可以说是中间的 SYN 和 ACK 是瞬发的, 而我们四次挥手时发送的 FIN 则是由用户代码来控制的. 只有调用 close 方法, 或者用户进程结束, 才会触发 FIN, 相比之下, ACK 是由内核控制, 第一次收到 FIN 时, 就会立马返回 ACK.

如果服务器始终不进行 close 会发生什么?

此时, 服务器的 TCP 状态, 就处于 CLOSE_WAIT 状态.
在这里插入图片描述
如果不进行 close 操作, 不发送 FIN, 不进行状态转换, 此时就仍然为 CLOSE_WAIT. 虽然这里的连接没有关闭, 但是这个连接也不能正常使用了.

  • 针对当前服务器进行读操作, 如果数据还没读完(缓冲区还有数据), 能正常读到. 如果数据已经读完, 此时就会读到 EOF.
  • 针对当前服务器进行写操作, 就会直接触发异常, 因为连接已经无法正常使用.

无论如何, 这个连接已经有问题了, 此时关闭是唯一的选择. 所以客户端在等待一段时间后, 即使接收不到服务器发来的 FIN, 也会直接单方面宣布断开连接(客户端将自己保存的对端信息删除), 释放资源.

因此, 尽管服务器不进行 close 操作, 也不影响客户端进行单方面释放连接. 当然, 释放资源能够双方同时释放, 是再好不过了. 所以我们在编写网络程序时写服务器时, 切记检查是否写了 close 操作.

如果通信过程中出现丢包怎么处理?

这里涉及到了之前讲的 TCP 超时重传机制, 三次握手 和 四次挥手, 其实都带有重传机制的, 如果在握手或者挥手过程中出现丢包, 会尽可能的去重传, 如果重传多次仍然失败, 则会单方面释放连接.

在这里插入图片描述
两种丢包情况:

  • 如果是第一组 FIN / ACK 丢失, A 直接重传 FIN 即可.
  • 如果是第二组 FIN / ACK 丢失, 丢失 FIN 重传即可, 下面我们来重点关注最后一次 ACK 丢失会发生什么.

在这里插入图片描述

此刻, 站在 A 的角度, 当收到 B 送来的 FIN 后, 并且自己也将 ACK 发出去了, 那么 A 这边就可以视为 “四次挥手” 已经结束了吗? 就可以直接释放连接了吗? 注意, 这时还不可以!!! 因为最后一个 ACK 还有可能丢包.

如果最后一个 ACK 丢失, 由于 B 没有收到 A 发送来的 ACK, B 就会重新给 A 传过一个 FIN 去, 此时如果 A 已经将连接释放掉了, 此时的 FIN, A 也已经认不得了, 无法进行 ACK 了, 毕竟释放连接后已经物是人非了.

因此, 就需要在 A 发出最后一个 ACK 后, 让连接再等待一会. 主要就是看等待过程中, 可否收到对方重传的 FIN, 如果等待一定时间后, 对方还没有重传 FIN, 就认为 A 给 B 发送的 ACK, B 已经收到了. 此时 A 才可以去真正的释放连接. 那么 A 这边要等待多久, 才能释放连接呢? 等待时间就是2MSL(网络上任意两点之间传输数据的最大时间 * 2).

  • MSL 是 TCP 报文的最大生存时间, 因此等待 2MSL 的话就能保证在两个传输方向上的尚未被接收或迟到的报文段都已经消失 (否则服务器立刻重启, 可能会收到来自上一个进程的迟到的数据, 但是这种数据很可能是错误的).
  • 理论上来说, 如果超时时间达到 MSL 上限, 此时包已经丢失了. 所以超时等待时间设置为 2MSL, 已经足够大了, 再大也没有什么意义了.

3.4 滑动窗口

以上我们讲述的三大机制, 确认应答, 超时重传, 连接管理都在保证 TCP 传输可靠性上起到了作用, 不过还请牢记, 起到决定性作用的机制是确认应答. 确认应答, 保证了每次传输数据都是可靠的. 当然, 有得就有舍, 在保证了传输可靠性的同时, TCP 就很难做到像 UDP 那样 “直来直去”.

接下来我们再讲一下 TCP 传输中的 “滑动窗口”, 与以上讲述的机制不同, 这个机制是用来提高 TCP 的传输效率的, 准确的说, 是尽可能保持 TCP 可靠传输时, 也可以尽可能的提高传输效率, 尽管不能使它变得和 UDP 一样快, 但是可以极大地缩小差距.

下图表示对每一个发送的数据段, 都要给一个 ACK 确认应答, 收到 ACK 后再发送下一个数据段. 这样做虽然能保证可靠性, 可时间都消耗在了等待 ACK 上, 尤其是数据往返的时间较长的时候, 效率更加拉胯. 使用滑动窗口就是为了缩短这样的等待时间.
在这里插入图片描述
既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能 (其实是将多个段的等待时间重叠在一起了).

在这里插入图片描述
这样一次性发出一组数据, 发送一组数据的过程中国年, 不等待 ACK, 此时就相当于使用一份等待时间来等四份 ACK. 把一次发多少数据, 不用等 ACK 这样的大小, 就称为 “窗口”.

  • 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值, 上图的窗口大小就是 4000 个字节(四个段).
  • 发送前四个段的时候, 不需要等待任何 ACK, 直接发送.
  • 收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据, 依次类推.

在这里插入图片描述

注意:

  • 窗口越大, 批量发送数据就越多, 效率就越高.
  • 但窗口不能无限大, 如果无限大, 相当于完全不必等待 ACK, 只需要不断发送即可, 此时就和不可靠传输差不多了.
  • 而且还需考虑, 当传输过大时, 接收方能否处理的过来, 中间网络设备能否承受住大流量.

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

采用这种批量的方式来传输数据, 中间丢包了怎么办? 这里我们分两种情况讨论.

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

在这里插入图片描述
当前情况, 部分 ACK 丢失了, 不用做任何处理也是正确的. 除非是当网络出现重大故障时, 所有的 ACK 都丢了, 这时就进行超时等待, 等待无果后直接断开连接了.

否则, 只是丢失一部分 ACK, 对于可靠传输是没有任何影响的, 因为可以通过后续的ACK进行确认. ACK 回复的确认序号, 就代表该序号之前的所有数据已经都收到了, 虽然上图中 A 没有收到确认 1001 的这个 ACK, 但是后续的 2001 确认报文 A 收到了, 表示之前发送给 B 的 2001 之前的数据 B 全部收到了.

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

在这里插入图片描述
当前情况, A 就必须进行重传了, 怎么进行重传呢?

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

在这里插入图片描述
此时, 就相当于是使用最小的成本, 来完成了这个重传数据的操作(只是将丢的数据进行重传, 其他的操作都没有重复进行), 这种机制被称为 “高速重发控制” (也叫 “快速重传”), 其本质还是超时重传, 只不过是结合滑动窗口后, 进行了一定的变形.

3.5 流量控制

此为滑动窗口的补充机制, 之前我们提到过滑动窗口越大, 传输效率就越高, 但是窗口也不能设置的太大, 还需考虑当传输过大时, 接收方能否处理的过来, 中间网络设备能否承受住大流量. 如果处理不过来, 就会频繁出现丢包情况, 服务器就得一直进行重传操作, 反而影响了传输效率.

接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送, 就会造成丢包, 继而引起丢包重传等等一系列连锁反应.

因此 TCP 支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制 (Flow Control).

那么, TCP 是如何衡量接收方的处理速度的呢? 是使用接收缓冲区剩余空间大小来作为衡量指标的. 如果剩余空间越大, 应用程序消耗数据的速度就越快.

接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段, 通过ACK端通知发送端.
在这里插入图片描述

  • 16位窗口大小, 该字段只是对于 ACK 报文才有意义, 这个数字表示了当前接收方缓冲区剩余空间大小, 这个数字返回给发送方, 就可以作为发送方下一轮发送数据的参考依据了. 这里的16位, 并不意味着窗口大小最大就是64kb.
  • 在选项中有一个选项是, 窗口大小扩展因子, 实际的窗口大小为16位窗口大小 << 扩展因子, 此时能够表示的窗口大小, 其实就是非常的大了.

在这里插入图片描述

  • 当前我们假定接收主机 B 的接收缓冲区是 4000 这么大.
  • 首先主机A发送一个数据报文来接受 B 的 ACK, B 收到 1 ~ 1000 的数据后, 发送 ACK 给 A, 并且通过报文的16位窗口大小中告知 A 自己目前剩余空间大小.
  • 主机 A 收到 B 的 ACK 后, 按照 3000 的窗口大小来进行发送数据.
  • 假定接收方 B 在这个过程中没有消耗数据, 接收缓冲区满了, 发送给 A 最新的 ACK 表示窗口大小已为 0.
  • 此时发送方就会暂定发送, 在等待过了超时重传的时间以后还没有收到 B 窗口更新的通知, 就会发送一个窗口探测的包(不携带具体数据, 只是为了让 B 回应 ACK, 反应当前接受缓冲区的空间大小).
  • A 一旦发现接收方剩余空间大小不是 0 了, 就可以继续发送了.
  • 根据这样的机制, 接收方就可以实现通过窗口大小, 反向限制发送方传输速度.

光考虑接收方还是不够的, 我们还需要考虑中间链路的处理能力, 下面讲解一下拥塞控制机制.

3.6 拥塞控制

关于我们总的传输效率, 其实就像一个木桶, 能再借多少水, 取决于木桶的最短板.

在这里插入图片描述
在主机A和主机B进行通信过程中, 中间会有很多交换机和路由器, 如果其中的某个转发环节能力特别差, 此时 A 的发送速度就不应该超过这里的阈值.

那应该怎样去衡量中间设备的转发能力呢? 此处并不是针对中间设备的转发能力进行量化, 而是把中间的设备都看成一个整体, 采取 “实验” 的方式进行动态调整, 产生出一个合适的窗口大小.

  • 使用一个较小窗口进行传输, 如果传输顺利通畅, 就调大当前的窗口.
  • 使用一个较大窗口进行传输, 如果传输丢包, 就调小当前的窗口.

实践是检验真理的唯一标准, 这样做也可以非常好的适应网络环境的动态变化.

在这里插入图片描述

此处, 我们引入一个 “拥塞窗口” 的概念, 就是在拥塞控制机制下, 发送方采用的滑动窗口大小. 在 TCP 中, 拥塞控制具体就是依靠 “拥塞窗口” 来展开的:

  1. 慢启动, 刚开始通信的时候, 会使用一个非常小的窗口去打探情况. 如果一开始就发送一个很大的流量, 当碰到网络拥堵时, 就会让本来不富裕的网络带宽雪上加霜了.
  2. 指数增长, 在传输的过程中(只考虑网络通畅不堵塞), 拥塞窗口的大小就会指数增长. 这个指数增长的速度是极快的, 如果不加以限制, 就会出现非常大的值. 所以我们需要一个阈值来进行控制.
  3. 线性增长, 当窗口大小指数增长到一个阈值后, 就会从指数增长转换为线性增长. 此时线性增长虽然没有指数增长的那么快, 但是也会使发送速度越来越快. 当快到一定程度就会接近网络传输的极限, 就可能出现丢包.
  4. 拥塞窗口回归小窗口, 当传输出现大量丢包后, 就认为当前网络出现拥堵了, 此时就会把窗口大小调整到最初的小窗口. 接下来的操作就会继续回到之前 “指数增长” + “线性增长” 的过程. 另外也会根据当前出现丢包的窗口大小, 调整出一个新的阈值(该阈值指的是指数增长到线性增长那个阈值).

步骤中的指数增长和线性增长, 都是按照传输的轮次来进行的. 比如当前给定的窗口大小为 4000, 全部发送之后, 这一轮就结束了, 当收到 ACK 之后, 继续发送数据时为下一轮.

在这里插入图片描述
如图, 拥塞窗口会在传输过程中, 不断的变化, 以此来适应多变的网络环境.

像上面这样的拥塞窗口增长速度, 是指数级别的. “慢启动” 只是指初使时慢, 但是增长速度非常快. 慢启动要着重注意下面几点.

  • 当 TCP 开始启动的时候, 慢启动阈值等于窗口最大值.
  • 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1.
  • 少量的丢包, 我们仅仅是触发超时重传. 大量的丢包, 我们就认为网络拥塞.

所以在数据传输中, 实际发送方的窗口大小 = min (拥塞窗口大小, 流量控制窗口大小). 拥塞控制和流量控制共同的限制了滑动窗口机制, 可以使滑动窗口在保证可靠性的前提下, 进一步提高传输效率.

3.7 延迟应答

延迟应答也是一个提高传输效率的机制, 是围绕滑动窗口琢磨产生的. 窗口大小越大, 传输效率越高. 那么是否有办法能在保证网络不拥堵的情况下, 尽可能的提升窗口大小呢?

如果接收数据的主机立刻返回 ACK 应答, 这时候返回的窗口可能比较小. 只需要在返回 ACK 时, 拖延一些响应的时间, 利用这个拖延的时间, 给接收方腾出来更多消费数据的时间, 那么接收缓冲区的剩余空间, 就会变大了, 窗口大小也可以变得更大了!

  • 假设接收端缓冲区为1M. 一次收到了500K的数据. 如果立刻应答, 返回的窗口就是500K.
  • 但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了.
  • 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来.
  • 如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M.

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

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

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

在这里插入图片描述

3.8 捎带应答

捎带应答是在延迟应答的基础上, 引入的一个进一步提高传输效率的机制. 延迟应答是让 ACK 传输的时机变慢. 捎带应答, 是让传输的数据报文尽可能的进行合并.

在很多情况下, 客户端和服务器之间的交互, 采取的是一问一答的方式.

在这里插入图片描述

当前由于ACK 进行了延迟, 就可以和 B 的响应一同封装发送给 A. 看到当前这个场景, 不知道读者你是否想到了之前四次挥手时的场景呢? 当时我说, 四次挥手在某些情况下也可以成为三次挥手, 这正是因为延迟应答和捎带应答存在, 所产生的效果.

ACK 和响应内容能合并的原因是因为:

  • 一方面, 在发送时机上可能是同时的.
  • 一方面, ACK 本身也不需要携带载荷, 和正常的数据也不冲突
  • 所以完全可以让一个数据报, 既能够携带数据, 又能够带有 ACK 的信息(ACK 标志位, 窗口大小, 确认序号).

3.9 面向字节流

TCP 一个带有标志性的特性就是面向字节流, 发送的数据并不是像 UDP 一样是一条一条的数据报, 而是将数据都是按一个一个字节去发送的. 但是这样在面向数据流时, 会产生一些问题.

粘包问题

这里的 “粘” 的是 “应用层数据报”, 通过 TCP 的 read / write 操作得到的数据, 都是 TCP 报文的载荷, 也就是应用层数据.

发送方一次性是可以发送多个应用层数据报的. 但是接受的时候, 如何区分从哪里到哪里是一个完整的应用数据报呢? 比如实际上发送了一个包, 结果只读了一个半.

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

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

  • 对于定长的包, 保证每次都按固定大小读取即可.
  • 对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置.
  • 对于变长的包, 还可以在包和包之间使用明确的分隔符 (应用层协议, 是程序员自己来定的, 只要保证分隔符不和正文冲突即可).

约定分隔符
在这里插入图片描述
约定包的长度
在这里插入图片描述
注意, 粘包问题不仅仅是只有 TCP 才有的. 只要是面向字节流的都有同样的问题, 与这里的解决方案也是一致的.

4. TCP 异常情况的处理

在基本了解 TCP 内容后, 我们再来聊聊总结一下网络出现变数, TCP 连接无法正常工作的情况下, TCP 都是怎么处理的. 这些内容, 对于我们实践操作过程中, 也有很大意义.

4.1 进程崩溃

进程崩溃后, 进程会终止并且释放文件描述符(PCB), 文件描述符表也会被释放, 但崩溃的这一方仍然会发送 FIN, 进一步的触发四次挥手过程, 此时连接就被正常释放了, 和正常关闭没有什么区别.

4.2 主机关机

这里说的关机指的是正常步骤的关机, 正常关机, 主机就会尝试关闭所有的进程, 与我们前面讲的进程崩溃的处理是一样的. 不过主机关机会有一定的时间, 在这个时间之内, 四次挥手可能正好挥完, 关闭连接; 也有可能还没挥完, 主机就关机了, 不过也不打紧.

在这里插入图片描述
这里的 FIN 很有可能发不过去了, 因为主机A 已经关闭了, B 就收不到 ACK, 会重传 FIN, 在重传多次后都收不到 A 的 ACK, 就会单方面的释放自己的连接信息了, 此时的释放不会产生任何不良后果.

4.3 主机掉电

主机掉电, 指的是主机非正常的关机, 比如拔电源, 这样的操作不会给主机任何的反应时间, 无法进行任何操作, A 也无法给 B 发起任何的 FIN. 这样就比较尴尬了. 有以下两种情况.

在这里插入图片描述
如果 B 正好给 A 发消息(接收方掉电)

这个情况好办, 当 B 发给 A 的消息得不到 ACK 后, B 就会触发超时重传, 重传多次仍然失败, 就会触发 复位报文(RST 为 1), 尝试重置连接, 当重置操作也失败, 就单方面释放连接, 没有任何负面影响.

如果 A 正好给 B 发送消息(发送方掉电)

此时就稍微复杂了一些, 因为 B 在等待 A 的消息. 突然间 A 就没音了, B 这边也不知道 A 这是等会再发, 还是 A 就一直不发了, 具体等多久, B也不知道.

此处就要涉及到 “心跳包”, B 这边虽然是接收方, 但是会周期性的给 A 发送一个不携带任何业务数据的 TCP 数据包, 发起这个包的目的就是为了触发 ACK, 以此来确认 A 是否还在正常工作. 如果 A 无法正确的返回 ACK, 那么就说明 A 已经下线了.

此处的 “心跳包” 与之前谈到的, “流量控制窗口探测报文” 本质上是一样的.

B 探测到 A 已经下线后, 就会单方面断开连接, 释放资源.

4.4 网线断开

此为主机掉电的升级版本, 网线断开就是指当前 A 和 B 无法进行正常通信了.

我们假设 A 要发送数据给 B.

在这里插入图片描述

  • A 这边采取的措施, 就是和主机掉电的第一种情况一样. (发送方)
  • B 这边采取的措施, 就是和主机掉电的第二种情况一样. (接收方)

总结

✨ 本文主要聊了聊网络中传输层的一个重要协议TCP, 聊到了该协议报文的格式, 并且花大量篇幅去讲述了TCP的一些主要特性, 这些特性用来保证TCP传输的可靠性和传输效率. 文章内提到的特性并不是TCP的全部特性, 想要了解的读者可以去标准文档上进一步查询.
✨ 想了解更多计算机网络的知识, 可以收藏一下本人的计算机网络学习专栏, 里面会持续更新本人的学习记录, 跟随我一起不断学习.
✨ 感谢你们的耐心阅读, 博主本人也是一名学生, 也还有需要很多学习的东西. 写这篇文章是以本人所学内容为基础, 日后也会不断更新自己的学习记录, 我们一起努力进步, 变得优秀, 小小菜鸟, 也能有大大梦想, 关注我, 一起学习.

再次感谢你们的阅读, 你们的鼓励是我创作的最大动力!!!!!


http://www.niftyadmin.cn/n/5136136.html

相关文章

docker 安装minio,访问地址进不去

文章目录 黑马头条P37docker安装minio文图一、启动后页面一直是加载状态进不去 黑马头条P37docker安装minio文图 一、启动后页面一直是加载状态进不去 通过docker logs -f (容器id)查看日志 通过这个报错信息&#xff0c;得知最近minio 升级&#xff0c;一些启动信息和之前不…

【SqlServer】视图中的表增加字段,关联视图列错乱解决

修改字段后&#xff0c;更新一下视图 sp_refreshview VW_TaskTrip

如何备份和恢复微信聊天记录?微信聊天记录1分钟轻松备份和恢复。

微信是一个非常流行的应用程序&#xff0c;不仅在中国&#xff0c;而且在全世界。这个应用程序允许来自其他国家的用户与他们的中国朋友进行群聊、语音消息、视频通话、发送贴纸或 GIF 以及照片。它可以为学生和商人/女性发送重要文件&#xff0c;以及位置共享以防游客在访问中…

【T】分治与倍增

分治&#xff0c;分而治之&#xff0c;其中最经典的便是二分 一、二分 一种经典而且非常好用的思想 将原问题对半转换成两个问题&#xff0c;子问题又继续转换成两个问题&#xff0c;许多子问题会很显然对答案没有关系&#xff0c;所以能讲原本O(n)的东西转化为O(logn) 但一般…

线性代数 第一章 行列式

一、概念 不同行不同列元素乘积的代数和&#xff08;共n!项&#xff09; 二、性质 经转置行列式的值不变&#xff0c;即&#xff1b; 某行有公因数k&#xff0c;可把k提到行列式外。特别地&#xff0c;某行元素全为0&#xff0c;则行列式的值为0&#xff1b; 两行互换行列式…

研究生安排

研一 看论文不懂先记着 论文一定多看的&#xff0c;建议300篇论文&#xff0c;500最好。 选题的时候要心里有谱&#xff0c;先找小论文&#xff0c;再找大论文 研二 确定研究方向和目标 和老师切磋研究的是什么 明确要做的东西是什么&#xff0c;是否已经明确要做什么&#xff…

【音视频|wav】wav音频文件格式详解

&#x1f601;博客主页&#x1f601;&#xff1a;&#x1f680;https://blog.csdn.net/wkd_007&#x1f680; &#x1f911;博客内容&#x1f911;&#xff1a;&#x1f36d;嵌入式开发、Linux、C语言、C、数据结构、音视频&#x1f36d; &#x1f923;本文内容&#x1f923;&a…

【Vue】初步认识<script setup>语法糖和组合式 API

▒ 目录 ▒ &#x1f6eb; 导读需求开发环境 1️⃣ &#x1f6eb; 导读 需求 最近写代码的时候&#xff0c;发现<script setup>这样的代码&#xff0c;没见过&#xff0c;好奇&#xff0c;想知道。 所以就有了这篇文章。 很多文章都说setup是vue3的特权。但是&#xff…