Bootstrap

Linux:传输层(2) -- TCP协议(2)

目录

1. 流量控制

2. 滑动窗口

3. 拥塞控制

4. 延迟应答

5. 捎带应答

6. 面向字节流

7. 粘包问题

8. TCP异常情况

1. 流量控制

        接收端处理数据的速度是有限的. 如果发送端发的太快 , 导致接收端的缓冲区被打满 , 这个时候如果发送端继续发送 , 就会造成丢包, 继而引起丢包重传等等一系列连锁反应 .
        因此TCP 支持根据接收端的处理能力 , 来决定发送端的发送速度 . 这个机制就叫做 流量控制 (Flow Control) ;
  • 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段, 通过ACK端通知发送端;
  • 窗口大小字段越大, 说明网络的吞吐量越高;
  • 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
  • 发送端接受到这个窗口之后, 就会减慢自己的发送速度;
  • 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端
  • 第一次发送数据时的窗口大小,是在三次握手时交换的。

2. 滑动窗口

        在上一篇文章中介绍了ACK确认应答和超时重传机制,那么根据前面已有的知识,如果发送数据在没有收到应答之前,必须将已经发送的数据暂时保留,用来在没有收到确认ACK时实现超时重传机制;会保存在滑动窗口中。若是收到ACK后再发送下一个数据段,这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候

为了使得传输效率变得更高,可以有下图中的传输策略:

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

        滑动窗口左侧是已发送并已收到ACK的数据,窗口中的数据未已发送未收到ACK的数据,右侧为未发送数据或无数据。 发送端滑动窗口的初始大小是基于发送方的配置和标准确定的,在发送数据的过程中根据对方接收缓冲区的大小动态的调整。窗口滑动示意如下:

 不过窗口不是一定会向右滑动的

  • 窗口保持不变:发送的数据速度与接收到ACK的速度相当时
  • 窗口变小:对方接收缓冲区满了,窗口end端不在变化,且一直收到对方的ACK,start端不断靠近end端
  • 窗口变大:由于网络拥堵等的情况一直未收到对方的ACK,start端不变化或者速度较慢;而对方接受缓冲区在变大,一直发送新的数据,窗口end端一直向右移动。

若发生丢包情况怎么办呢?

情况一:数据没丢,只是应答丢了 -- 这种情况是无需担心的,因为可以通过后续的ACK进行确认。发送端在收到下一个应答的序号为:ack_seq = x+1,表示x+1之前的数据已经全部收到了,如下图所示:

情况二:数据包真的丢失了

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

这种机制叫做“高速重发控制”(快重传),比超时重发机制要快。

3. 拥塞控制

        虽然TCP 有了滑动窗口这个大杀器 , 能够高效可靠的发送大量的数据 . 但是如果在刚开始阶段就发送大量的数据 , 仍然可能引发问题. 因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵 . 在不清楚当前网络状态下 , 贸然发送大量的数据 , 是很有可能引起雪上加霜的. TCP引入 慢启动 机制 , 先发少量的数据 , 探路 , 摸清当前的网络拥堵状态 , 再决定按照多大的速度传输数据
 

  • 此处引入一个概念程为拥塞窗口
  • 发送开始的时候, 定义拥塞窗口大小为1;
  • 每次收到一个ACK应答, 拥塞窗口加1;
  • 每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口;

慢启动只是启动的时候慢,但是是指数级的增长速度。为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍。此处引入一个叫做慢启动的阈值,当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长。如下图所示:

4. 延迟应答

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

并不是所有的包都要延迟应答:

  • 数量限制: 每隔N个包就应答一次; (N一般为2)
  • 时间限制: 超过最大延迟时间就应答一次  (超时时间取200ms)

延迟应答不仅可以返回简答的窗口大小, 还可以:

  • 合并ACK:如果连续收到两个TCP包,并不一定需要ACK两次,只要回复最终的ACK就可以了,可以降低网络流量。
  • 如果接收方有数据要发送,那么就会在发送数据的TCP数据包里,带上ACK信息。这样做,可以避免大量的ACK以一个单独的TCP包发送减少了网络流量

5. 捎带应答

在延迟应答的基础上 , 我们发现 , 很多情况下 , 客户端服务器在应用层也是 " 一发一收 " . 意味着客户端给服务器说了 "How are you", 服务器也会给客户端回一个 "Fine, thank you";
那么这个时候 ACK 就可以搭顺风车 , 和服务器回应的 "Fine, thank you" 一起回给客户端

6. 面向字节流

        当用户消息通过 TCP 协议传输时,消息可能会被操作系统分组成多个的 TCP 报文,也就是一个完整的用户消息被拆分成多个 TCP 报文进行传输。

创建一个TCPsocket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;

  • 调用write时, 数据会先写入发送缓冲区中;
  • 如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;
  • 如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;
  • 接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;
  • 然后应用程序可以调用read从接收缓冲区拿数据;
  • 另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工

由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:

  • 写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;
  • 读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;

7. 粘包问题

如果一次请求发送的数据量比较小,没达到缓冲区大小,TCP则会将多个请求合并为同一个请求进行发送,这就形成了粘包问题;
如果一次请求发送的数据量比较大,超过了缓冲区大小,TCP就会将其拆分为多次发送,这就是拆包,也就是将一个大的包拆分为多个小包进行发送。

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

  • 对于定长的包, 保证每次都按固定大小读取即可; 例如上面的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
  • 对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;
  • 对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔符不和正文冲突即可)
        对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用层. 就有很明确的数据边界. 站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半个"的情况。UDP是不存在粘包拆包问题的。

8. TCP异常情况

  • 进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.
  • 机器重启: 和进程终止的情况相同.
  • 机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset. 即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.
另外 , 应用层的某些协议 , 也有一些这样的检测机制 . 例如 HTTP 长连接中 , 也会定期检测对方的状态 . 例如 QQ, QQ线之后, 也会定期尝试重新连接 .

详细的可以参考这篇文章:TCP 异常断开连接分析_tcp断连问题剖析-CSDN博客

;