> 文章列表 > Linux(传输层二继续讲TCP)

Linux(传输层二继续讲TCP)

Linux(传输层二继续讲TCP)

文章目录

  • 0. 前言
  • 1. 流量控制
  • 2. 滑动窗口
    • 2-1 基础
    • 2-2 重传
  • 3. 拥塞控制
  • 4. 延迟应答
  • 5. 捎带应答
  • 6. 面向字节流
  • 7. 粘包问题
  • 8. TCP异常情况
  • 9. TCP小结
  • 10. 基于TCP应用层协议
  • 11. TCP/UDP对比

0. 前言

上一章我们主讲了TCP,本章我们继续
链接:https://blog.csdn.net/Dingyuan0/article/details/129415670?spm=1001.2014.3001.5502

1. 流量控制

  • 主要保证数据的可靠性

接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应。
因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);

  • 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段, 通过ACK端通知发送端;
  • 窗口大小字段越大, 说明网络的吞吐量越高;
  • 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
  • 发送端接受到这个窗口之后, 就会减慢自己的发送速度;
  • 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端。

Linux(传输层二继续讲TCP)
(图片来自于相关教材资料)

  • 接收端如何把窗口大小告诉发送端呢? 回忆我们的TCP首部中, 有一个16位窗口字段, 就是存放了窗口大小信息;
  • 那么问题来了, 16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?
  • 实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位;

2. 滑动窗口

  • 主要提高传输效率

2-1 基础

确认应答策略:对每一个发送的数据段, 都要给一个ACK确认应答;收到ACK后再发送下一个数据段;这样做有一个比较大的缺点, 就是性能较差,尤其是数据往返的时间较长的时候。

Linux(传输层二继续讲TCP)
(图片来源于相关教材资料)
既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时
间重叠在一起了)。

  • 窗口大小指的是无需等待确认应答而可以继续发送数据的最大值. 下图的窗口大小就是4000个字节(四个段).

  • 发送前四个段的时候, 不需要等待任何ACK, 直接发送;

  • 收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;

  • 操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答;

  • 只有确认应答过的数据, 才能从缓冲区删掉;窗口越大, 则网络的吞吐率就越高
    Linux(传输层二继续讲TCP)
    (图片来源于相关教材资料)

我们如何理解TCP的发送缓存区,我们可以把缓冲区看成一个数组;而数组的下标是序号;而滑动缓冲区就是发送缓冲区的一部分。
Linux(传输层二继续讲TCP)
滑动窗户的起始地址有传过来的确认序号决定的,大小是有拥塞窗口和对方窗口大小共同决定的。

2-2 重传

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

  • 这种情况下, 部分ACK丢了并不要紧, 因为可以通过后续的ACK进行确认。

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

Linux(传输层二继续讲TCP)
(图片来源于相关教材资料)

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

例如:滑动窗口是1000~5000;而1000 ~ 2000 和 3000 ~ 4000数据包丢了,发送端只能收到其它的ACK,此时发送端的窗口起始位置还是1000;直到收到1000 ~ 2000的ACK窗口起始才会改变。

  • 这种机制被称为 “高速重发控制”(也叫 “快重传”)

3. 拥塞控制

滑动窗口的大小 = min(拥塞窗口, 对方窗口大小【接受能力—剩余空间】)

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

相关图片:
Linux(传输层二继续讲TCP)

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

刚开始拥塞窗口增长速度, 是指数级别的,“慢启动” 只是指初使时慢, 但是增长速度非常快;为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍。

  • 此处引入一个叫做慢启动的阈值;
  • 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长;
  • 当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
  • 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1;

就好像谈恋爱一样,刚开始处于热恋期;感情温度升的非常快,之后感觉升温就慢下来了;最后可能还吵了一架,导致感情瞬间到达极小点,如何男生不断的示好,感情也重新开始升温,可是感情的最后点变低了。(当然也可能直接崩了)

TCP拥塞控制这样的过程, 就好像热恋的感觉。

小结:

  • 少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;
  • 当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;
  • 拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案。

4. 延迟应答

如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小。

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

注意:

  • 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;

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

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

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

5. 捎带应答

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

6. 面向字节流

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

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

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

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

类似我们写过的网络计算机中TCP协议
链接:https://blog.csdn.net/Dingyuan0/article/details/129249383?spm=1001.2014.3001.5502

7. 粘包问题

例如:早上我们吃一笼饺子,我们会发现,有时候饺子粘在一起,我们用筷子夹的时候容易只加半个。

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

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

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

对于UDP协议来说, 是否也存在 “粘包问题” 呢?(没有,报文是定长的)

  • 对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用层.,就有很明确的数据边界。
  • 站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半个"的情况。

8. TCP异常情况

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

9. TCP小结

可靠性:

  • 校验和出错
  • 序列号(按序到达)
  • 确认应答
  • 超时重发
  • 连接管理
  • 流量控制
  • 拥塞控制

提高性能:

  • 滑动窗口
  • 快速重传
  • 延迟应答
  • 捎带应答

其他:

  • 定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等)

10. 基于TCP应用层协议

  • HTTP
  • HTTPS
  • SSH
  • Telnet
  • FTP
  • SMTP

当然,也包括你自己写TCP程序时自定义的应用层协议。

11. TCP/UDP对比

  • TCP用于可靠传输的情况, 应用于文件传输, 重要状态更新等场景;
  • UDP用于对高速传输和实时性要求较高的通信领域, 例如, 早期的QQ, 视频传输等. 另外UDP可以用于广播。