zl程序教程

您现在的位置是:首页 >  数据库

当前栏目

计算机网络(第九弹) --- 关于 TCP 的十个重要机制的详细解释

2023-04-18 16:45:35 时间

传输控制协议 TCP 在整个计算机网络中占有很高的地位, 它会控制着网络上数据的传输过程, 当然学习 TCP 还要知道它的几个重要特性, 如 确认应答, 超时重传等, 本文主要对这些特性进行介绍并解释, 力求加深印象.
附 6 个标志位:

  • ACK: 确认应答是否有效;
  • SYN: 同步报文段, 请求建立连接, 或者称之为握手信号;
  • FIN: 结束报文段, 通知对方本端马上要关闭了;
  • URG: 紧急指针是否有效;
  • PSH: 提示接收端应用程序立刻从 TCP 缓冲区将数据读走;
  • RST: 复位报文段: 对方要求重新建立连接.

1 ACK (确认应答机制)


  前面已经说过 TCP 是面向字节流的传输, 也就是说 TCP 的传输是以字节的形式; 因此在数据传输过程中, TCP 将每个字节的数据进行了编号, 也就是序列号, 每一个 ACK 都带有对应的确认序列号, 目的就是为了告诉发送者我已经收到了哪些数据, 下一次的传输从哪里开始就可以了.
总之, 发送方发数据给接收方后, 接收方就会回应一个应答报文; 如果发送方收到了这个应答报文, 那么就知道对方已经收到数据了.

在这里插入图片描述

TCP 的核心是可靠性, 而可靠性的核心就在于应答机制.


2 超时重传机制

  关于超时重传的概念也很好理解, 就好比我在微信上给别人发了一个消息, 我说收到后回复收到, 但是可能由于网络问题我迟迟没有收到确认信息, 这个时候我就过一会再发一次.
用专业的术语来说就是主机 A 发送数据给主机 B 之后, 可能因为网络堵塞等原因, 数据无法到达主机 B; 而如果主机 A 在一个特定的时间间隔内没有收到 B 发来的确认应答, 就会进行重发.

这时候, 有人可能会疑问, 会不会有一种情况就是别人收到了消息, 但是忘记了回复确认收到???


也就是说主机 A 未收到主机 B 发来的确认应答, 也有可能是 ACK 丢失了, 这时候主机 A 就会一直给主机 B 发送信息, 主机 B 呢就会收到特别多的重复消息, 这时候就需要将这些重复的数据给丢弃掉; 这时候 TCP 会通过序列号识别出哪些是重复的包, 并把这些重复的包给丢弃掉, 起到去重的效果.
在这里插入图片描述

  关于超时的时间该如何确定这个问题, 其实 Linux 中会以 500ms 为单位进行控制, 每次判定超时重发的超时时间都是 500ms 的整数倍, 累积到一定的重传次数, TCP 就会认为网络或者主机端出现异常, 强制关闭连接.


3 连接管理 (非常重要)


关于连接管理还是非常重要的, 因为这里涉及到传说中的 “三次握手和四次挥手这个概念”; 上过计算机网络的应该都知道这个概念的重要性, 其实连接管理说的就是如何建立连接 (三次握手), 如何断开连接 (四次挥手).

🤝 三次握手建立连接

  建立连接的过程就是一个投石问路的过程, 下面以呼叫器的测试为例子进行介绍, 当前小明和小红不知道自己的呼叫器是否能够正常使用, 如下图所示:
在这里插入图片描述
说明:

  • 本质上, 当小明的 syn 到达小红那的时候, 小红的内核就会第一时间进行应答 ack, 同时也会第一时间发起 syn, 这两件事是同时触发的, 因此也就没必须分成两次传输.
  • 关于上述过程中涉及的状态:
    • LISTEN: 当创建了 Socket 实例的时候, 就进入了 LISTEN 状态; 类似于说明我这边一切正常, 可以接收消息;
    • ESTABLISHED: 双方已经互通, 可以进行会话了.

总之, 三次握手的目的就是来确认两个主机之间的传输是否是正常的, 尤其是两者的发送能力和接收能力是否是正常的; 其实这也是表明 TCP 可靠性的体现之一.

👋🏻 四次挥手断开连接

  断开连接的过程比较容易好理解, 如下图所示:
在这里插入图片描述
这里就有个疑问, 在建立连接的时候 syn 和 ack 可以合并到一起进行发送, 那这里的 FIN 和 ack 为什么就不能合并到一起发送呢???

对于小红来说, ACK 和 FIN 的触发时间是不一样的, 理由如下:

  • 小红只要是收到了 FIN 就会立即触发 ACK 进行确认应答, 这个事情是内核完成的;
  • 小红发送 FIN 实际上是用户代码控制的, 如代码中的 socket.close(), 出现这样的操作后才会触发 FIN.
  • 当然也有可能小红的代码出问题了, 一直不发送 FIN 结束报文段, 但是 socket 是会被 GC (垃圾回收机制) 回收的, 这时候也会关闭释放对应的文件, 这个操作也不是那么的及时; 也有可能代码中没有调用 close, 但是进程结束了, 这时候 PCB 就会被销毁, PCB 上的文件描述符也就没了, 这时候也会触发 FIN.

断开连接的过程也有两个状态:

  • CLOSE_WAIT: 服务器收到 FIN 之后进入的状态, 等待用户代码调用 close 来发送 FIN;
    • 当出现大量的 CLOSE_WAIT 状态, 大概率是服务器没有正确的关闭 socket, 导致四次挥手没有正确完成, 只需要加上对应的 close 即可.
  • TIME_WAIT: 客户端收到 FIN 之后进入 TIME_WAIT, 这个状态存在的意义主要就是为了处理最后一个 ACK 丢包;
    • 即使进程已经退出了, TIME_WAIT 仍然会存在, 因为 TCP 连接不会立即就销毁掉;
    • 如果等待一定时间后还是没有重传 FIN 过来, 才会真正销毁.

4 滑动窗口

  上面的确认应答策略对每一个发送的数据段都要给一个 ack 确认应答, 收到 ack 后再发送下一个数据段, 这样的过程太繁琐复杂了, 尤其是数据往返时间较长的时候; 那么我们可不可以一次发送多条数据, 这样不就大大提高了性能了么.
也就是说现在是批量进行发送, 一次发一波, 然后再等一波的 ack, 把多组数据的 ack 的等待时间重叠起来, 这样就会大大提高效率.
一次批量发的数据长度就是窗口大小, 那么窗口无限大的话可以么?
TCP 的主要特性之一就是可靠性传输, 如果窗口无限大的话, 就没有可靠性而言了. 实际上, TCP 为了提高效率, 滑动窗口下并不是每一条数据都有 ack, 会隔几条数据才有一个 ack.

在这里插入图片描述

总之:

  • 窗口大小指的是无需等待确认应答 ack 而可以继续发送数据的最大值;
  • 发送前四个段时, 无需等待任何 ack, 直接发送;
  • 收到第一个 ack 后滑动窗口向后移动, 继续发送第五个段的数据, 以此类推;
  • 操作系统内核为了维护这个滑动窗口, 需要开辟发送缓冲区来记录当前还有哪些数据没有应答; 只有确认应答过的数据才能从缓冲区中删掉;
  • 窗口越大, 网络吞吐量就越高.

那么出现丢包情况, 是如何进行重传的呢???

  • 第一种情况: 数据已经收到了, 但是 ack 丢了!!! 这种情况下虽然 ack 丢了, 但是部分 ack 丢了并不重要, 因为可以通过后续的 ack 进行确认;
  • 第二种情况: 数据直接丢了!!!
    • 当某一段报文段丢失之后, 发送端会一直收到序列号为 xxx 的 ack, 就相当于提醒发送端我想要的是 xxx;
    • 如果发送端连续收到同样的序列号为 xxx 的确认应答, 就对应着某一段的数据需要重新发送, 这时候接收端收到 xxx 之后, 再次返回的 ack 就不再是 xxx 了.这个过程就是快重传机制 (高速重发控制).

5 流量控制

流量控制本质上就是用来控制滑动窗口大小的. 接收端处理数据的速度是有限的, 如果发送端发的太快导致接收端缓冲区溢满, 这时发送端如果继续发送就会造成丢包的风险, 继而引起丢包重传等一系列连锁反应; 因此为了避免出现这样的情况, TCP 会根据接收端的处理能力来决定发送端的发送速度, 这个机制就是流量控制.

在这里插入图片描述
总之:

  • 接收端一旦发现自己的缓冲区快满了, 就会发送一个 ack 信号告诉发送端窗口快满了, 窗口需要设置一个更小的滑动窗口; 这时候发送端收到信号后就会减缓自己的发送速度.
  • 如果接收端缓冲区满了, 就会将窗口设置为 0, 这时候发送端就不能再发送数据了, 但是需要定期发送一个窗口探测数据段, 这里面不会传输具体的业务数据, 就是一个 ack, 在这个 ack 中就能够知道当前窗口的大小了, 使接收端将窗口大小告诉发送端.
  • 窗口大小的设置是 16 位, 也就是最大为 65535, 是不是就意味着 TCP 滑动窗口最大就是 65535 呢?
    • 其实在 TCP 的首部 40 个字节中还包含一个窗口扩大因子 M, 实际窗口大小是窗口字段值左移 M 位, 相当于乘 2 的 M 次方大小.

6 拥塞控制

  虽然滑动窗口大大提高了数据传输的吞吐量, 但是在发送初期阶段就发送大量的数据可能会引发其它问题, 如当前网络比较拥堵, 在不清楚网络当前情况下贸然发送大量的数据很可能出现更大的丢包问题; 这时候 TCP 就引入了慢启动机制, 先发少量的数据探探路, 当清楚当前网络的拥堵状态后再决定按照一定量的速度传输数据.

总之, 先使用一个比较小的窗口来传输数据, 看看是否丢包, 如果不丢包就说明网络通畅, 这时候可以加大发送的速度; 当出现丢包的情况就意味着网络比较拥堵, 这时候就可以降低发送速率; 通过这样的方式来测试出一个比较合适的窗口大小.
真实的发送窗口大小 = MIN (流量控制的窗口, 拥塞控制的窗口).


7 延时应答

在流量控制那已经说过接收端会立刻将剩余的缓冲区容量告诉接收端, 发送端根据接收端的提示来设置滑动窗口大小; 但实际情况下接收端处理数据的速度可能非常快, 会非常迅速的将缓冲区的某一部分数据消费掉, 这也就意味着接收端还能够接受更多的数据; 这时候如果接收端不立刻做出回应而是稍等一会再去应答, 那么这个时候接收端告诉发送端的的窗口大小会比立刻发送这种情况下吞吐量大很多.

我们的目标就是在保证网络不拥堵的情况下尽量提高传输效率; 也就是说让窗口大小在保证可靠的基础上尽量大一点, 但也不是所有的包都可以延迟应答, 而是有数量限制的, 每隔 N 个包就应答一次, 超过最大延迟时间再应答一次就足够了.


8 捎带应答

  在上面的握手挥手操作中可以看到一发一收情况下会做出回应, 那接收端对缓冲区大小的回复也是发送 ack 回复; 在建立连接或者是断开连接过程中只要接收端收到数据内核就会立刻发送 ack, 但是由于有了延迟应答, 返回的 ack 不是立即返回而是要等一会, 就在这等一会这个功夫上服务器正要返回业务上的数据, 这时候就可以把 ack 和业务上的回复进行合二为一, 也就是将两个包变成了一个包, 这就是捎带应答的核心所在.
其实捎带应答并不是每次都能够触发, 这本身就是一个概率性机制!!!


9 面向字节流

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

  • 调用 write 时, 数据会先写入发送缓冲区中, 如果发送的字节数太长, 就会被拆分成多个 TCP 数据包发出; 如果发送的字节数太短, 就会先从缓冲区里等待, 等到缓冲区长度差不多了或者有更合适的时机了才发送出去;
  • 接收数据的时候数据也是到达内核的接收缓冲区, 然后应用程序再调用 read 从接收缓冲区中拿数据;
  • TCP 连接既有发送缓冲区也有接收缓冲区, 那么对于一个连接也就意味着既可以读数据也可以写数据, 这就是全双工这个概念;
  • 有了缓冲区这个概念也就意味着读写操作不需要一一匹配.

但是, 但是应用程序从接收缓冲区读取数据的时候, 不知道是从哪里读到哪里才是一个完整的应用层数据报, 应用程序此时只能看到接收缓冲区中的一个一个的字节, 无法区分当前接收缓冲区里有多少个应用层数据包, 这就是所谓的粘包问题, 这并不是 TCP 特有的问题, 只要是面向字节流的操作都可能会出现这样的问题!!!


10 粘包问题

上面已经说了为何会出现粘包问题, 对于粘包问题的解决就是要明确两个包之间的边界, 解决办法如下:

  • 设定结束符么约定每个应用层数据包一定以某个符号结尾, 如 " hello word; ",应用程序从缓冲区读数据的时候读到 " h e l l o w o r d ; "就意味着这是一个完整的应用层数据;
  • 设定包的长度, 约定每个应用层数据包的前四个字节来存储数据包的长度, 如 “9helloword”, 应用程序会先读前四个字节读到了 9 这个字节, 然后继续往后读 9 个字节就是一个完整的应用层数据包.

对于 UDP 来说, 会存在粘包问题么???
因为 UDP 是一个一个将数据交付给应用层, 就会有很明确的数据界限, 站在应用层的角度来看, 使用 UDP 的时候要么收到的是完整的 UDP 报文, 要么没有收到, 不会出现半个这样的情况.