计算机网络—TCP协议详解:协议构成、深度解析(3)
🎬慕斯主页:修仙—别有洞天
♈️今日夜电波:マリンブルーの庭園—ずっと真夜中でいいのに。
0:34━━━━━━️💟──────── 3:34
🔄 ◀️ ⏸ ▶️ ☰
💗关注👍点赞🙌收藏您的每一次鼓励都是对我莫大的支持😍
目录
流量控制
滑动窗口
滑动窗口的一些问题
拥塞控制
为什么采用指数增加的方案呢?
延时应答
面向字节流
粘包问题
TCP异常情况
流量控制
我们都知道TCP协议的双方在通信时都是拥有自己的发送和接收缓冲区的。他们是全双工的。因此,可以一边应答一边接收数据。而在发送和应答的报文中都可以携带16位的窗口大小,这个窗口大小即表示接受缓冲区中剩余的空间大小,也就是最多还能接受多少大小的空间。接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应.因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);
接收端将自己可以接收的缓冲区大小放入 TCP 首部中的 "窗口大小" 字段, 通过ACK端通知发送端;
窗口大小字段越大, 说明网络的吞吐量越高;
接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端;
发送端接受到这个窗口之后, 就会减慢自己的发送速度;
如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端
大致的图示如下:
思考一个问题:16位的窗口大小,16位数字最大表示65535, 那么TCP窗口最大就是65535字节么?实际上, TCP首部40字节选项中还包含了一个窗口扩大因子M, 实际窗口大小是 窗口字段的值左移 M 位;
滑动窗口
前面我们提到报文是可以并发的发送的以及并发的应答的,毕竟如果只发一个然后给一个应答再接着发,这样的效率太低了。而接受方的接收能力是有限的,受着前面的16位的窗口大小影响。而滑动窗口就是用于调节发送方和接收方之间的数据传输速率,以避免网络拥塞并提高传输效率。前面我吗提到了TCP的超时重传机制,为了保证超时重传那么久必须得把发送过的报文保存起来,以便后续重传。发送方因为效率问题当然会并发的发送,那么没收到应答前这些报文是如何保存的呢?当然是在自己的发送缓冲区中啦!我们只需要将缓冲区划分为:已发生已确认、已发生未确认、待发送的三个区域就可以做到区分是否应答、保存发送过的报文以便后续超时重传的机制出发的情况。而这个已发生未确认的区域即为滑动窗口。大致的图示如下:
实际上我们也可以将TCP中的滑动窗口理解为我们算法中常用的滑动窗口,因为他们本质上都是通过控制左边和右边的边界来控制这个窗口的!如果收到了应答的报文那么左边界会右移,如果发送了报文那么右边界也会右移。本质上就是双指针的维护作用!
滑动窗口的一些问题
滑动窗口只能右移吗?能不能左移?实际上是不能的,因为左侧已经确认应答了,那么对于系统而言左移是没有意义的!然而,理论上讲,TCP的滑动窗口并非绝对不能左移。在某些特定的情况下,比如当接收方发现之前确认的数据有误,需要重传时,可能会涉及到窗口左移的情况。但这并不是TCP滑动窗口机制的主要操作方式,因为TCP通过序列号和确认机制来保证数据的顺序性和可靠性,而不是通过窗口的左移。
滑动年出口移动中的大小变化?滑动窗口可以变大变小,这是根据对方的接收能力决定的,也就是他回复的16位的窗口大小来决定的!如果右边界不动,左边界一直左移,此时滑动窗口在变小,说明对面的接收缓冲区接收能力也在变小。而相反的如果左边不动右边右移,则说明对面接收缓冲区缓冲区接收能力在变大。当左边界等于有边界的时候,此时滑动窗口变为0表示对方无法接收。
滑动窗口会越界吗?我们都知道算法中的滑动窗口会通过对什么取余来控制窗口的环形遍历,而TCP中的滑动窗口额也会想环形队列一样会回到缓冲区起点重新开始遍历。
丢包问题的发送怎么办?例如我们并发的将1~1000、1001~2000、2001~3000、3001~4000发送给了对方。但是,对方仅仅返回了1~1000、2001~3000、3001~4000的ACK也就是说1001~2000的报文丢失了,如果是发送时丢失了,那么前面我们学到过反回来的ACK携带的确认报文只能是1001。也就是下图所示:
那么我们的滑动窗口是如何表示的呢?如下,他会从下图位置进行移动。
而当1001~2000补发成功并且收到ACK后,那么报文会直接返回4001的确认报文,而滑动窗口的左边界也会滑动至4000的位置!如下:
需要注意的是:这只是客户端收到“两个”同样的“1001”的ACK的情况,因为我们一共只发了这么多的报文。但是如果我们发的报文很多,且也发生了数据包直接丢失的情况呢?
当某一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 "我想要的是 1001"一样; 如果发送端主机连续三次收到了同样一个 "1001" 这样的应答, 就会将对应的数据 1001 - 2000 重新发送; 这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已经收到了, 被放到了接收端操作系统内核的接收缓冲区中; 这种机制被称为 "高速重发控制"(也叫 "快重传" )。
这个时候你就会问了?有了快重传为什么还需要超时重传呢?看完上面的连个例子你大概就理解了!因为快重传是有条件的!只有收到三次重复的确认应答才会触发,而如果总的要收的应答就那么多,这个时候就需要超时重传了!
如果是ACK丢失了呢?那么不会受到影响。为什么呢?还记得前面说的确认序号认为他之前的序号都是已经接受成功了的,因此滑动窗口会直接变的更上面左边界指向4001的地方一样!
拥塞控制
当发生的数据包出现大量的丢包的问题的时候,比如:发送100个报文丢了90多个那么这就可能不是自身发送的问题了,这就是网络的问题。这种网络出现的潜在问题我们称为网络拥塞。网络拥塞会影响该网络覆盖的所有主机,所有的主机都会出现大量丢包!因此,我们需要一定的策略减清拥塞直到恢复正常。对此,可以减少发生量让网络有缓冲的时间,因为网络上有很多的计算机, 可能当前的网络状态就已经比较拥堵. 在不清楚当前网络状态下, 贸然发送大量的数据,是很有可能引起雪上加霜的.TCP引入 慢启动 机制, 先发少量的数据, 探探路, 摸清当前的网络拥堵状态, 再决定按照多大的速度传输数据;如下图所示:
此处引入一个概念程为拥塞窗口
发送开始的时候, 定义拥塞窗口大小为1;每次收到一个ACK应答, 拥塞窗口加1;
每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗口;
结合前面提到的滑动窗口,那么实际的滑动窗口大小=min(返回的16位窗口大小,拥塞窗口大小)。
像上面这样的拥塞窗口增长速度, 是指数级别的. "慢启动" 只是指初使时慢, 但是增长速度非常快.
为了不增长的那么快, 因此不能使拥塞窗口单纯的加倍.此处引入一个叫做慢启动的阈值
当拥塞窗口超过这个阈值的时候, 不再按照指数方式增长, 而是按照线性方式增长
当TCP开始启动的时候, 慢启动阈值等于窗口最大值;在每次超时重发的时候, 慢启动阈值会变成原来的一半, 同时拥塞窗口置回1;少量的丢包, 我们仅仅是触发超时重传; 大量的丢包, 我们就认为网络拥塞;当TCP通信开始后, 网络吞吐量会逐渐上升; 随着网络发生拥堵, 吞吐量会立刻下降;拥塞控制, 归根结底是TCP协议想尽可能快的把数据传输给对方, 但是又要避免给网络造成太大压力的折中方案. TCP拥塞控制这样的过程, 就好像 热恋的感觉。
为什么采用指数增加的方案呢?
因为这种增加的特点为前期增长慢,过了临界点后,后期增长快。这样的方案特别符合网络拥塞的情况。所以慢启动只是前期慢,但是增长的速度快!这也是后面要设置阈值的原因。
延时应答
如何提高对方的发送效率呢?当然是在发的报文中包含更多的报文了!那么我们知道要发报文的大小是根据对面给你的回应决定的,也就说根据对方的应答报文中的窗口大小决定的!,因此要提高发送效率那么就需要返回更大的窗口,这样发送方的滑动窗口才会更大!那么如何提高呢?如果接收数据的主机立刻返回ACK应答, 这时候返回的窗口可能比较小. 假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M;
一定要记得, 窗口越大, 网络吞吐量就越大, 传输效率就越高. 我们的目标是在保证网络不拥塞的情况下尽量提高传输效率;那么所有的包都可以延迟应答么? 肯定也不是;数量限制: 每隔N个包就应答一次;时间限制: 超过最大延迟时间就应答一次;具体的数量和超时时间, 依操作系统不同也有差异; 一般N取2, 超时时间取200ms;
面向字节流
创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;调用write时, 数据会先写入发送缓冲区中;如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;如果发送的字节数太短, 就会先在缓冲区里等待, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;然后应用程序可以调用read从接收缓冲区拿数据;另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可以写数据. 这个概念叫做 全双工
由于缓冲区的存在, TCP程序的读和写不需要一一匹配, 例如:写100个字节数据时, 可以调用一次write写100个字节, 也可以调用100次write, 每次写一个字节;读100个字节数据时, 也完全不需要考虑写的时候是怎么写的, 既可以一次read 100个字节, 也可以一次read一个字节, 重复100次;
粘包问题
粘包问题可能由多个因素引起。首先,发送方可能由于TCP的Nagle算法或滑动窗口机制,将多个小数据包合并成一个大的数据包进行发送,从而导致粘包。其次,接收方的缓冲区处理速度可能慢于网络传输速度,使得多个数据包在缓冲区中连续存放,接收方在读取时无法准确区分各个数据包的边界。此外,网络状况的不稳定、数据包丢失以及发送方和接收方处理速度的不匹配等因素也可能导致粘包问题的发生。
粘包问题对数据传输的准确性和可靠性造成威胁,特别是在需要精确区分数据包边界的应用场景中,如文件传输、远程控制等。因此,解决TCP粘包问题具有重要意义。
针对TCP粘包问题,可以采取多种解决策略。归根结底就是一句话, 明确两个包之间的边界 。一种常见的方法是使用消息定界符或消息长度来标识数据包的边界。发送方在每个数据包的末尾添加特定的定界符或在开头添加表示数据长度的字段,接收方根据这些标识信息来解析数据包。另外,固定长度的消息格式也是一种解决方案,即每个数据包都使用固定大小的长度,接收方按照固定长度读取数据即可。此外,应用层协议设计也可以考虑数据包的分段和重组机制,以确保数据的完整性和准确性。
那么对于UDP,是否存在粘包问题呢?对于UDP, 如果还没有上层交付数据, UDP的报文长度仍然在. 同时, UDP是一个一个把数据交付给应用层. 就有很明确的数据边界.站在应用层的站在应用层的角度, 使用UDP的时候, 要么收到完整的UDP报文, 要么不收. 不会出现"半个"的情况
TCP异常情况
进程终止: 进程终止会释放文件描述符, 仍然可以发送FIN. 和正常关闭没有什么区别.这是因为操作系统会将进程的资源回收,当进程退出时,该进程会通过系统调用创建的链接或者文件,被操作系统自动关闭。
机器重启: 和进程终止的情况相同.
机器掉电/网线断开: 接收端认为连接还在, 一旦接收端有写入操作, 接收端发现连接已经不在了, 就会进行reset. 即使没有写入操作, TCP自己也内置了一个保活定时器, 会定期询问对方是否还在. 如果对方不在, 也会把连接释放.
另外, 应用层的某些协议, 也有一些这样的检测机制. 例如HTTP长连接中, 也会定期检测对方的状态. 例如QQ, 在QQ断线之后, 也会定期尝试重新连接.
感谢你耐心的看到这里ღ( ´・ᴗ・` )比心,如有哪里有错误请踢一脚作者o(╥﹏╥)o!
给个三连再走嘛~