计算机网络(七)——TCP(下)


一、滑动窗口

1.1 缓冲区

TCP是自带发送和接收缓冲区的。

send发送数据是将数据发送/拷贝到TCP的发送缓存区的。

为什么要提供缓冲区呢?

  1. 提高应用层效率, 应用层只用和缓冲区交互,速度很快
  2. 只有TCP协议了解双方状态明细,所以也只有它能控制如何法、什么时候发、发多少、出错了怎么办等细节。 — TCP是传输控制协议。
  3. 将应用层和TCP(传输层)进行解耦。 应用层只用将数据拷贝到缓存区,或者从缓冲区读即可,不用关心到底怎么发送,怎么接受。

1.2 什么是滑动窗口

对每一个发送的数据段, 都要给一个ACK确认应答。收到ACK后再发送下一个数据段。这样做有一个比较大的缺点, 就是性能较差. 尤其是数据往返的时间较长的时候。
既然这样一发一收的方式性能较低, 那么我们一次发送多条数据, 就可以大大的提高性能(其实是将多个段的等待时间重叠在一起了)。
在这里插入图片描述

  • 一次可以发多个数据,但是总的能发多少是由接收方决定的。
  • 窗口大小是指无需等待确认应答而可以继续发送数据的最大值。
  • 收到第一个ACK后, 滑动窗口向后移动, 继续发送第五个段的数据; 依次类推;
  • 操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答; 只有确认应答过的数据, 才能从缓冲区删掉;
  • 窗口越大, 则网络的吞吐率就越高;

在这里插入图片描述
缓冲区的数据可以分为三个部分:

  1. 已经发送,已经收到确认应答。
  2. 可以或者已经发送,但是没有收到确认应答。
  3. 没有发送。

收到的报文中,16位窗口大小表示接受方缓冲区的剩余大小,也是本次发送的窗口大小。所以滑动窗口的大小是可变的。

  • 通过收到的ACK确认序号和对方窗口大小,改变滑动窗口的起始位置。

1.3 丢包情况

那么如果出现了丢包, 如何进行重传? 这里分两种情况讨论.

  • 情况一: 数据包已经抵达, ACK被丢了。
    在这里插入图片描述
    即使2001和3001应答没有收到,但是收到4001,由确认应答序号可知,1-4000的数据是全部被收到的。所以这个时候不用管没有收到的中间的ACK确认序号,只用关心收到的最大序号即可。

  • TCP运行少量的丢包(ACK应答)

  • 情况二: 数据包就直接丢了.
    在这里插入图片描述

  • 当某一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 "我想要的是 1001"一样;

  • 如果发送端主机连续三次收到了同样一个 “1001” 这样的应答, 就会将对应的数据 1001 - 2000 重新发送;

  • 这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中;

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

二、流量控制

2.1 流量控制的基本过程

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

  • 接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 “窗口大小” 字段, 通过ACK端通知发送端;
  • 窗口大小字段越大, 说明网络的吞吐量越高;
  • 接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
  • 发送端接受到这个窗口之后, 就会减慢自己的发送速度;
  • 如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端.
  1. 什么时候知道对方的缓冲区大小(接受能力)
    在对方给自己发送报文的时候,从第一个报文就开始了,即三次握手期间就已经协商好了窗口大小。 根据对方报文中的窗口大小,来设置自己滑动窗口的初始值。
  2. 当某一端满了,发送窗口大小为0。

此时TCP层会进入阻塞状态。
那么什么时候,会进行更新呢?

  • 这时,就需要自己发送报文去询问,是否有空间了
  • 窗口更新通知:
    假设窗口探测间隔为1s,但是窗口立马更新好了,此时要等待1s后再进行通信,是很浪费时间的。因此,需要有窗口更新通知,更新好后,立马通知对方。

在这里插入图片描述

2.2 流量控制的意义

  • 上层不关心底层通信:
    流量控制是由内核进行的,上层不关心这些,上层只关心缓冲区是否还有容量可以让自己写入数据,并不会关心这些通信细节。

  • 分层的意义:
    假如现在接收方窗口满了,发送方给对方发送PSH都不取走数据,这就是应用层的BUG,因为协议是分层的,每层都不关心另外一层是怎么样的。

  • 高低水位线:
    recv、send都是系统调用,在调用的时候都需要进行身份的切换,在进行切换的时候也是需要消耗时间的。而缓冲区的高低水位线就可以提高这种效率,当数据的内容低于低水位线的时候,TCP就不会去通知上层读数据,高于水位线的时候,才会通知上层一次调用读取大量数据。这种减少用户态-内核态的切换,也是效率提升的一种。

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

三、拥塞控制

拥塞控制,其实就是控制由于网络因素,控制本身的发送速度。

3.1 认识拥塞控制

网络是非常庞大的,有很多台主机,网络状态也难以确定,因为了滑动窗口这个大杀器,可以连续发送大量数据,但是若是贸然发送大量数据可能会使当前的网络环境雪上加霜。所以引入了拥塞控制,它是所有主机都要遵守的。
考虑到网络状态,如果出现大量丢包(说明网络环境不好),不会立即重传(会使得网络阻塞更加严重),给网络以喘息。

为此引入拥塞控制,它是类似于指数增长,前期较慢,后期非常快。也就是慢启动和快恢复。

3.2 慢启动

TCP引入慢启动机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;

在这里插入图片描述

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

为了更好的理解,举个例子说明

  1. 连接建好的开始先初始化 cwnd = 10,表明可以传 10 个 MSS(最大报文段) 大小的数据。
  2. 每当收到一个 ACK,cwnd 加 1。这样每当过了一个 RTT(往返时间),收到了对应的十个ACK,cwnd 翻倍,呈2的指数上升。

这里总结一下窗口的概念:

  • 窗口大小:是收到对方发送的ACK报文,里面包含的16位窗口大小的值,是对方所能接受数据的大小。

  • 滑动窗口:是本身能够不用收到对方确认就可以发送数据的最大数量。

  • 拥塞窗口:是网络环境能够传递数据的大小,通过慢启动不断尝试获得,是不确定的。

因此滑动窗口大小 = min(拥塞窗口,窗口大小)

3.3 拥塞避免

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

  • 为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.
  • 此处引入一个叫做慢启动的阈值
  • 当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
  • 当TCP开始启动的时候, 慢启动阈值等于窗口最大值;
  • 在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1;
    在这里插入图片描述

3.4 快恢复

当检测到丢包时,TCP 会触发快速重传并进入降窗状态,但如果直接降为1,又开始慢启动,这也是不科学的。一般 cwnd 会通过快速恢复算法降至一个合理值。

在这里插入图片描述

由上述特性可知,网络拥塞是必然的。因为网络状态是变化的,不可预测的,只能不断试探,但速度有不能太慢。

  • 少量的丢包, 我们仅仅是触发超时重传;
  • 大量的丢包, 我们就认为网络拥塞;
  • 当TCP通信开始后, 网络吞吐量会逐渐上升;
  • 随着网络发生拥堵, 吞吐量会立刻下降;

拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案.

四、延迟应答

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

  • 假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
  • 但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
  • 在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
  • 如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;
  • 一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;

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

  • 数量限制: 每隔N个包就应答一次;
  • 时间限制: 超过最大延迟时间就应答一次;
  • 具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms;

五、捎带应答

发送有效数据的时候可携带上次的应答,在TCP报文里面它们是不冲突的。

其实很多情况下客户端和服务端两边是在互相交流的,有问有答。如果将对上一句的回答,和本次的问题一起发送,就是捎带应答。

六、理解面向字节流

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

七、粘包问题

  • 首先要明确, 粘包问题中的 “包” , 是指的应用层的数据包.
  • 在TCP的协议头中, 没有如同UDP一样的 “报文长度” 这样的字段, 但是有一个序号这样的字段.
  • 站在传输层的角度, TCP是一个一个报文过来的. 按照序号排好序放在缓冲区中.
  • 站在应用层的角度, 看到的只是一串连续的字节数据.
  • 那么应用程序看到了这么一连串的字节数据, 就不知道从哪个部分开始到哪个部分, 是一个完整的应用层数据包.
  • 那么如何避免粘包问题呢? 归根结底就是一句话, 明确两个包之间的边界.
  • 对于定长的包, 保证每次都按固定大小读取即可; 例如上面的Request结构, 是固定大小的, 那么就从缓冲区从头开始按sizeof(Request)依次读取即可;
  • 对于变长的包, 可以在包头的位置, 约定一个包总长度的字段, 从而就知道了包的结束位置;
  • 对于变长的包, 还可以在包和包之间使用明确的分隔符(应用层协议, 是程序猿自己来定的, 只要保证分隔符不和正文冲突即可);
  • 思考: 对于UDP协议来说, 是否也存在 “粘包问题” 呢?
  • 对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用层. 就有很明确的数据边界.
  • 站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半个"的情况.

八、TCP异常情况

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

九、基于TCP应用层协议

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

十、TCP小结

为什么TCP这么复杂? 因为要保证可靠性, 同时又尽可能的提高性能.

  • 可靠性:
  • 校验和
  • 序列号(按序到达)
  • 确认应答
  • 超时重发
  • 连接管理
  • 流量控制
  • 拥塞控制
  • 提高性能:
  • 滑动窗口
  • 快速重传
  • 延迟应答
  • 捎带应答
  • 其他:
  • 定时器(超时重传定时器, 保活定时器, TIME_WAIT定时器等)

十一、理解listen函数的第二个参数

int listen(int sockfd, int backlog);

Linux内核协议栈为一个tcp连接管理使用两个队列,分别是:

  • 半连接队列:用来保存SYN_SENT和SYN_RECV两个状态的连接。也就是三次握手还没有完成,连接还没建立的套接字(fd)。
  • 全连接队列:用来保存保存ESTABLISHED状态的连接。三次握手已经完成的连接,但是没有被accept取走的值。

为什么要维护两个队列?

  • 全连接队列保证资源利用的最大化,将已经建立连接,占用资源的套接字保存下来,需要的使用就可以直接使用了。使用accept快速获取。
  • listen 的第二个参数 + 1 ,是TCP建立正常连接的个数。也就是全连接的队列长度。

至于为什么加1,应该是源码的问题,也有可能是为了防止出现长度为0的情况。
验证参考文章

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

s_persist

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值