Loading

TCP可靠传输

😉 本文共5700字,阅读时间约20min

  • 可靠性和流量控制由滑动窗口协议保证
  • 链路拥塞则由拥塞控制的算法实现

TCP如何保证传输的可靠性?

  1. 流量控制:滑动窗口协议

  2. 拥塞控制: 当网络拥塞时,减少数据的发送。

  3. 超时重传:当发送方发送数据之后,它启动一个定时器,等待目的端确认收到这个报文段。接收端实体对已成功收到的包发回一个相应的确认信息(ACK)。如果发送端实体在合理的往返时延(RTT)内未收到确认消息,那么对应的数据包就被假设为已丢失并进行重传。ARQ协议 SACK。

  4. 校验和: TCP 将保持它首部和数据的检验和。这是一个端到端的检验和,目的是检测数据在传输过程中的任何变化。如果收到段的检验和有差错,TCP 将丢弃这个报文段和不确认收到此报文段。

  5. 对失序数据包重新排序以及去重:TCP 为了保证不发生丢包,就给每个包一个序列号,有了序列号能够将接收到的数据根据序列号排序,并且去掉重复序列号的数据就可以实现数据包去重。

TCP的精髓:滑动窗口协议

TCP 连接的每一方都有固定大小的缓冲空间,TCP 的接收端只允许发送端发送接收端缓冲区能接纳的数据。当接收方来不及处理发送方的数据,能提示发送方降低发送的速率,防止包丢失。TCP 使用的流量控制协议是可变大小的滑动窗口协议(TCP 利用滑动窗口实现流量控制)。

1. 滑动窗口协议(依据接收方返回的ack拓展滑动窗口)

  • “窗口”:对应一段可以被发送者发送的字节序列,其连续的范围称之为“窗口”

  • “滑动”:指这段“允许发送的范围”是随着发送的过程而按顺序“滑动”的

  • 了解一下协议的前提:

    • TCP全双工,A发有发送缓冲区,B收有接收缓冲区。
    • 发送窗口是发送缓冲区的一部分,是可以被发送的部分。其实应用层需要发送的所有数据都被放进了发送者的发送缓冲区。每次发送成功后,发送窗口就会在发送缓冲区内按顺序移动,将新数据包含到发送窗口内准备发送。
    • 发送窗口相关的四个概念:
      • 已发送并收到确认的数据(不在发送缓冲区内;同样也不会在接收缓冲区内)
      • 已发送但未收到确认的数据(在发送窗口内)
      • 允许发送但未发送的数据(在发送窗口内)
      • 暂时不允许发送的数据(不在发送窗口内,但在缓冲区内)
  • 协议例子描述

    1. TCP建立连接的初始,B会告诉A自己的接收窗口大小,比如为‘20’:

      img
    2. A发送11个字节后,发送窗口位置不变,B接收到了乱序的数据分组:

      img

    3. 只有当A成功发送了数据,即发送的数据得到了B的确认之后,才会移动滑动窗口离开已发送的数据;同时B则确认连续的数据分组,对于乱序的分组则先接收下来,避免网络重复传递:

      img

2. 流量控制(防止丢包):

  • 流量控制,指B回传自己的接收窗口给A,让A不要发的太快,是一种端到端控制。
    • B返回的ACK中包含接收窗口rwnd,并且利用其大小控制A的发送

img

可能存在的问题:新生缓冲死锁

  1. rwnd接收窗口为0时,也就是B告诉A接收缓冲区已满,于是A停止发送数据。
  2. 等待一段时间后,B接收缓冲区出现富余,按理说于是给A发报文说rwnd为400。
  3. 报文丢了,于是出现 A等待B的通知 || B等待A发送数据 的死锁状态。

解决方案:TCP引入了持续计时器(Persistence timer),当A收到对方的零窗口通知时,就启用该计时器,时间到则发送一个1字节的探测报文,对方会在此时回应自身的接收窗口大小,如果结果仍未0,则重设持续计时器,继续等待。

3. 传递效率

  • 问题:如果单个字节发送单个确认,或者接收窗口有一个空余即通知发送方发送一个字节,无疑增加了网络中许多不必要的报文(请想想为了一个字节数据而添加的40字节头部吧!)。
  • 解决方案:
    • 思想:批发批收,减少链路中网络包数量,防止网络过载
    • 尽可能一次多发送几个字节(Nagle算法
    • 窗口空余较多的时候才通知发送方一次发送多个字节,也就是让接收方等待一段实践,或者接收方获得足够的空间容纳一个报文或者等到接收缓存有一半空闲时,再通知发送方发送数据。(防止糊涂窗口综合症

Nagle算法(表象):

为了尽可能发送大块数据,避免网络中充斥着许多小数据块

  1. 若进程要把数据逐个字节地送到TCP的发送缓存,则A就把第一个数据字节先发送出去,后面的字节先缓存起来;
  2. 当A收到第一个字节的确认后(得到了网络情况和对方的接收窗口大小),再把缓冲区的剩余字节组成合适大小的报文发送出去;(等待ACK,然后才发下一个)
  3. 当到达的数据已达到发送窗口大小的一半或以达到报文段的最大长度时,就立即发送一个报文段;(不等ACK,还有一种就是紧急指针)

糊涂窗口综合症(silly window syndrome)设想一种情况:TCP接收方的缓存已满,而交互式的应用进程一次只从接受缓存中读取一个字节(这样接收缓存空间就仅腾出一个字节),然后向发送方发送确认,并把窗口设置为一个字节(但发送的数据报是40字节长)。接着,发送方又发来一个字节的数据(此时发送方发送的IP数据报是41字节长)。接收方发回确认,如此进行下去,是网络效率很低。

  • 另一个提高效率机制:ACK延迟机制

    • B收到数据,不会马上回复ACK,而是会延迟一段时间。因为B希望这段时间内B会向A发送应答数据,这样ACK就能和应答数据一起发过去。(两个请求 --> 一个请求),这也是为什么我们看到ACK一般都跟ack、rwnd在一起发
  • Nagle算法本质:

image-20221206223618967

防止链路加塞的精髓:拥塞控制

问题:网络中的链路容量和交换结点中的缓存和处理机都有着工作的极限,当网络的需求超过它们的工作极限时,就出现了拥塞。

解决:拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。

  1. 拥塞控制的相关概念:

    1. cwnd(congestion window)是一个发送方维护的一个状态变量,他会根据网络的拥塞程度动态变化。
    2. swnd = min(cwnd, rwnd),也就是发送窗口是拥塞窗口和接收窗口中的最小值。
  2. 如何知道网络中是否发生了拥塞

    1. 其实只要「发送方」没有在规定时间内接收到 ACK 应答报文,也就是发生了超时重传(快速重传),就会认为网络出现了拥塞。
  3. 拥塞控制算法简介

    1. 拥塞控制的过程分为四个阶段:慢启动、拥塞避免、拥塞发生(快重传),快恢复
    2. 这个图略微不对,TD时cwnd = cwnd/2 ,ssthresh = cwnd。

    在这里插入图片描述

    慢启动算法+拥塞避免算法(TCP Tahoe版本,已废弃):
    1. 发送方维持一个叫做“拥塞窗口”的变量,该变量和接收端口共同决定了发送者的发送窗口;
    2. 当主机开始发送数据时,避免一下子将大量字节注入到网络,造成或者增加拥塞,选择发送一个1字节的试探报文;
    3. 当收到第一个字节的数据的确认后,就发送2个字节的报文;
    4. 若再次收到2个字节的确认,则发送4个字节,依次递增2的指数级;
    5. 最后会达到一个提前预设的“慢开始门限”,比如24,即一次发送了24个分组,此时遵循下面的条件判定:
    1. cwnd < ssthresh, 继续使用慢开始算法;
    2. cwnd > ssthresh,停止使用慢开始算法,改用拥塞避免算法;
    3. cwnd = ssthresh,既可以使用慢开始算法,也可以使用拥塞避免算法;
    6. 所谓拥塞避免算法就是:每经过一个*往返时间RTT*就把发送方的拥塞窗口+1,即让拥塞窗口缓慢地增大,按照线性规律增长;
    7. 当出现网络拥塞,比如丢包时,将慢开始门限设为原先的一半,然后将cwnd设为1,执行慢开始算法(较低的起点,指数级增长);
    
    慢启动+拥塞避免+拥塞发生(快重传)+快恢复(TCP Reno版本):
    
    1. 「快速重传算法」。当接收方发现丢了一个中间包的时候,发送三次前一个包的 ACK,于是发送端就会快速地重传,不必等待超时再重传。
    TCP 认为这种情况不严重,因为大部分没丢,只丢了一小部分,ssthresh 和 cwnd 变化如下:
    cwnd = cwnd/2 ,也就是设置为原来的一半;
    ssthresh = cwnd;进入快速恢复算法;
    
    2. 快速恢复算法是认为,你还能收到 3 个重复 ACK 说明网络也不那么糟糕。
    正如前面所说,进入快速恢复之前,cwnd 和 ssthresh 已被更新了:
    cwnd = cwnd/2 ,也就是设置为原来的一半; ssthresh = cwnd;
    
    *1.不执行慢开始算法(即拥塞窗口cwnd现在不设置为1),而是把cwnd值设置为自身减半后的数值
    *2.然后开始执行拥塞避免算法(“加法增大”),使拥塞窗口缓慢地线性增大
    
  • 这两张图,是不同版本的实现。

有的快重传实现是把开始时的拥塞窗口cwnd值再增大一点,即拥塞窗口 cwnd = ssthresh + 3 ( 3 的意思是确认有 3 个数据包被收到了)。这样做的理由是:既然发送方收到三个重复的确认,就表明有三个分组已经离开了网络。这三个分组不再消耗网络 的资源而是停留在接收方的缓存中。可见现在网络中并不是堆积了分组而是减少了三个分组。因此可以适当把拥塞窗口扩大了些。

重传机制

对丢失的数据进行重传

超时重传 ARQ协议

自动重传请求(Automatic Repeat-reQuest,ARQ)通过使用确认和超时这两个机制,在不可靠服务的基础上实现可靠的信息传输。如果发送方在定时器时间内没有收到确认信息(ACK),它通常会重新发送,直到收到确认或者重试超过一定的次数。TCP 会在以下两种情况发生超时重传:数据包丢失、确认应答丢失。

ARQ 包括停止等待 ARQ 协议和连续 ARQ 协议。

停止等待 ARQ 协议

停止等待协议是为了实现可靠传输的,它的基本原理就是每发完一个分组就停止发送,等待对方确认(回复 ACK)。如果过了一段时间(超时时间后),还是没有收到 ACK 确认,说明没有发送成功,需要重新发送,直到收到确认后再发下一个分组;

在停止等待协议中,若接收方收到重复分组,就丢弃该分组,但同时还要发送确认。

1) 无差错情况:

发送方发送分组,接收方在规定时间内收到,并且回复确认.发送方再次发送。

2) 出现差错情况(超时重传):

停止等待协议中超时重传是指只要超过一段时间仍然没有收到确认,就重传前面发送过的分组(认为刚才发送过的分组丢失了)。因此每发送完一个分组需要设置一个超时计时器,其重传时间应比数据在分组传输的平均往返时间更长一些。这种自动重传方式常称为 自动重传请求 ARQ 。另外在停止等待协议中若收到重复分组,就丢弃该分组,但同时还要发送确认。

3) 确认丢失和确认迟到

  • 确认丢失 :确认消息在传输过程丢失。当 A 发送 M1 消息,B 收到后,B 向 A 发送了一个 M1 确认消息,但却在传输过程中丢失。而 A 并不知道,在超时计时过后,A 重传 M1 消息,B 再次收到该消息后采取以下两点措施:1. 丢弃这个重复的 M1 消息,不向上层交付。 2. 向 A 发送确认消息。(不会认为已经发送过了,就不再发送。A 能重传,就证明 B 的确认消息丢失)。
  • 确认迟到 :确认消息在传输过程中迟到。A 发送 M1 消息,B 收到并发送确认。在超时时间内没有收到确认消息,A 重传 M1 消息,B 仍然收到并继续发送确认消息(B 收到了 2 份 M1)。此时 A 收到了 B 第二次发送的确认消息。接着发送其他数据。过了一会,A 收到了 B 第一次发送的对 M1 的确认消息(A 也收到了 2 份确认消息)。处理如下:1. A 收到重复的确认后,直接丢弃。2. B 收到重复的 M1 后,也直接丢弃重复的 M1。

连续ARQ协议

停止等待协议的优点是简单,但是缺点是信道的利用率太低,一次发送一条消息,使得信道的大部分时间内都是空闲的,为了提高效率,我们采用流水线传输。

连续ARQ协议一般和滑动窗口协议使用,这两个协议主要解决的问题信道效率低和增大了吞吐量,以及控制流量的作用。

连续 ARQ 协议可提高信道利用率。发送方维持一个发送窗口,凡位于发送窗口内的分组可以连续发送出去,而不需要等待对方确认。接收方一般采用累计确认,对按序到达的最后一个分组发送确认,表明到这个分组为止的所有分组都已经正确收到了。

优点: 信道利用率高,容易实现,即使确认丢失,也不必重传。

缺点: 不能向发送方反映出接收方已经正确收到的所有分组的信息。 比如:发送方发送了 5 条 消息,中间第三条丢失(3 号),这时接收方只能对前两个发送确认。发送方无法知道后三个分组的下落,而只好把后三个全部重传一次。这也叫 Go-Back-N(回退 N),表示需要退回来重传已经发送过的 N 个消息。

快速重传 SACK

快速重传的工作方式是当收到三个相同的 ACK 报文时,会在定时器过期之前,重传丢失的报文段。快速重传机制只解决了一个问题,就是超时时间的问题。

在 TCP 头部「选项」字段里加一个 SACK 的东西,这样发送方就可以知道哪些数据收到了,哪些数据没收到,知道了这些信息,就可以只重传丢失的数据

比如,发送方收到了三次同样的 ACK 确认报文,于是就会触发快速重发机制,通过 SACK 信息发现只有 200~299 这段数据丢失,则重发时,就只选择了这个 TCP 段进行重复。

TCP粘包/拆包、原因及其解决方式

问题表现

TCP是一个流协议,其字节流没有明确的分界线。TCP底层并不了解上层数据的具体含义,它会根据TCP缓冲区的实际情况进行包的划分,一个完整的包可能会被TCP拆分成多个包进行发送,也有可能把多个小的包封装成一个大的数据包发送,这就是所谓的TCP粘包和拆包问题。

img
  • 正常的理想情况,两个包恰好满足TCP缓冲区的大小或达到TCP等待时长,分别发送两个包;
  • 粘包:两个包较小,间隔时间短,发生粘包,合并成一个包发送;
  • 拆包:一个包过大,超过缓存区大小,拆分成两个或多个包发送;
  • 拆包和粘包:Packet1过大,进行了拆包处理,而拆出去的一部分又与Packet2进行粘包处理。

TCP协议是面向流的协议,UDP是面向消息的协议

UDP没有粘包拆包问题,因为UDP有消息保护边界。在每个UDP包都有消息头,对于接收端应用程序相当好区分。UDP每一段都是一条消息,应用程序必须以消息为单位提取数据。

UDP消息头包括UDP长度、源端口、目的端口、校验和。

TCP粘包/拆包发生的原因

发送方

  • 应用程序的一次请求发送的数据量(滑动窗口/Nagle算法)
    • 比较小,TCP会把多个请求合并为一个请求发送,导致粘包
    • 比较大,超过缓冲区大小/MTU/MSS,TCP会将其拆分为多次发送,导致拆包
  • MTU/MSS限制
    • Maximum Segment Size,最大报文长度;Maximum Transmission Unit,最大传输单元
    • 传输的数据大于MSS/MTU时,数据会被拆成多个包进行传输。由于MTU由MSS计算出,若满足MTU,则满足MSS(MSS长度=MTU长度-IP Header-TCP Header)

接收方

接收方引起的粘包是由于接收方用户进程不及时接收数据,从而导致粘包现象。这是因为接收方先把收到的数据放在系统接收缓冲区,用户进程从该缓冲区取数据,若下一包数据到达时前一包数据尚未被用户进程取走,则下一包数据放到系统接收缓冲区时就接到前一包数据之后,而用户进程根据预先设定的缓冲区大小从系统接收缓冲区取数据,这样就一次取到了多包数据。

解决策略

不是所有的粘包现象都需要处理,若传输的数据为不带结构的连续流数据(如文件传输),则不必把粘连的包分开(简称分包)。但在实际工程应用中,传输的数据一般为带结构的数据,这时就需要做分包处理。

  • 首先TCP作为面向流的协议,没有消息保护边界,其字节流本身就没有明确的分界线,需要在接收端处理消息边界问题。
  • 粘包/拆包和TCP没啥关系,是应用层没处理好数据包的分割,比如应用层的两个数据包粘一块了,因此粘包拆包应该由业务层处理。

解决思路:编码解码,应用层通过指定收发两端共同的约定,发送方按照特定的规则组装数据,接收方按照同样的规则拆解数据。发送方组装数据的过程称之为编码;接收方拆解数据的过程称之为解码。

  1. 长度编码:将消息分为消息头和消息体,消息头中包含消息的长度
  2. 特殊字符分隔消息
  3. 定长协议:发送端将数据包封装为固定长度(不够的补0),接收端每次从接收缓冲区中读取固定长度的数据就把每个数据包拆分开来。
posted @ 2023-03-05 23:56  iterationjia  阅读(129)  评论(0编辑  收藏  举报