目录
一、滑动窗口
传统发送接收机制的缺点
滑动窗口的特性
发送方什么时候会接着发送下一条报文
如果在滑动窗口的机制下面发生了丢包会怎样处理
情况1:ack丢了
情况2:主动发送的syn丢包了
滑动窗口的应用场景
二、TCP流量控制:根据接收方的处理能力,协调发送方的发送速率。
TCP滑动窗口过大的缺点
如何衡量接收方的处理能力
有一种量化的方法:
更简单的方法:(直接查看接收方的接收缓冲区的剩余大小):
探测报文
窗口大小为16位,也就是最大位64kb,那么是否意味着窗口大小最大是64kb呢?
三、拥塞控制
为什么有了流量控制,还需要拥塞控制
拥塞窗口(cwnd)
第一阶段,慢增长:
第二阶段:避免拥塞算法
第三阶段:发生了网络拥塞
拥塞窗口的好处:
如何确定TCP滑动窗口的值
四、延时应答机制
实际场景当中的延时应答机制
五、捎带应答
六、粘包问题
什么是粘包问题
解决粘包问题:两个约定
一、关于协定的约定:
二、约定每个包的长度
三、明确分隔符
七、其余的不可抗力
情况1:进程崩溃了/主机关机
情况2:主机掉电了/网线断开了。
如果是接收方掉电了:
如果是发送方掉电了:
一、滑动窗口
传统发送接收机制的缺点
我们知道:TCP是一个拥有可靠性的协议;它的可靠性是依靠确认应答机制和超时重传机制来保证的。
在这一篇文章当中,也提到了TCP的可靠性是如何实现的。
【网络原理篇2】TCP报头详解_革凡成圣211的博客-CSDN博客TCP报头结构https://blog.csdn.net/weixin_56738054/article/details/128935529?spm=1001.2014.3001.5502
但是关于超时重传,在上述文章的图解当中:是客户端一次会话发送完成之后,下一次客户端再次向服务端发起会话的时候,需要等待直到服务端的ack进行反馈了的时候,才会再次发送文。
为了减少这种不必要的等待,于是引入了TCP滑动窗口机制。
滑动窗口的特性
滑动窗口的特点就是:批量地发送一组数据,只需要使用一份时间来等待多个ack即可。
因此,把可以连续发送(不需要等待,就能直接发送)的数据的最大的量,就称为"窗口大小"。
发送方什么时候会接着发送下一条报文
发送方不是一次等待所有的数据(1-4000)都全部到达了,才会再次发送ack报文。而是等待到一个ack回应过来之后,才会接着发送下一个TCP报文。
在上图当中,数据1-1000从主机A发送给主机B。当主机B回应了1001报文,也就是发送了ack到达主机A之后,主机A就会给B重新发送一个ack报文(4001-5000)。
下面,模拟一下发送方发送——接收一次报文的情况。
时间轴 | 发送方 | 接收方 | 窗口大小 | 窗口范围 | 一次发送的范围 |
t1 | 明确任务:发送报文大小为4000的数据...(第一次发送) | 4000 | (1001,5001) | ||
t2 | 发送数据(1001-2000) | 1000 | |||
t3 | 发送ack报文:确认应答,下一个为:2001(32位确认序号) | 1000 | |||
t4 | 接收到ack应答报文 | ||||
t5 | 再次发送ack报文...(第二次发送) | 4000 | (2001,6001) |
在t5时刻,发送方本来需要等待的ack报文的范围是(1001,5001),但是由于在t3时刻已经收到了确认序号为2001的ack报文了,说明编号<2001的报文都收到了。
那么,再次发送新的报文的时候,滑动窗口的范围就变了,变成了原来的范围+1000,也就(2001,6001)。
如果在滑动窗口的机制下面发生了丢包会怎样处理
关于丢包的问题,需要分为两种情况来讨论:
情况1:ack丢了
如果ack丢了,不需要做任何的处理,也不会影响发送方和接收方之间的正常传输。
为什么不需要做任何的处理呢?再次图解一下:
时间轴 | 发送方 | 接收方 | 报文类型 | 说明 |
t1 | 发送1-1000的数据 | syn | ||
t2 | 发送应答报文给发送方(32位确认序号为1001) | ack | 此时发生了丢包。 | |
t3 | 发送数据1001-2000的数据 | syn | ||
t4 | 发送应答报文给接收方(32位确认序号为2001) | ack | 没有发生丢包 | |
t5 | 收到了确认序号为2001的编号 | ack |
可以看到,t5的时候,发送方收到了确认序号为2001的应答报文(ack),那么也就默认了确认序号<2001的报文都已经收到了,无需再次发送syn。
那么,如果数据丢失得比较多,只返回了最后一次的ack,这样岂不是影响比较大?
这里,我们需要注意的是,丢包毕竟是小概率事件。 只返回窗口发送的最后一次的ack, 更加是不太可能的情况,因此这种情况无需担心。
并且,少发送一两次的ack,可以一定程度地减少封装分用的次数,提高效率。
情况2:主动发送的syn丢包了
如果主动发送的syn丢包了,那么发送方就会不断反复发送数据。
下面,模拟一下主动发送的syn丢包的情况:
时间轴 | 发送方 | 接收方 | 报文类型 | 说明 |
t1 | 发送1-1000的数据 | syn | 此时发生了丢包 | |
t2 | 发送数据1001-2000的数据 | syn | 此时没有发生丢包 | |
t3 | 发送应答报文给接收方(32位确认序号为1001)................t1时刻对应的ack | ack | ||
t4 | 收到了确认序号为1001的报文 | ack | ||
t5 | 发送数据为2001-3000的数据 | syn | 此时没有发生丢包 | |
t6 | 发送应答报文给接收方(32位确认序号为1001)................t1时刻对应的ack | |||
t7 | 接收到了确认序号为1001的报文 |
可以看到,站在发送方的角度,发送方如果发送的syn报文如果出现了丢包,那么接收方会不断重复发送方发送第一次发送的报文的ack。
由于发送方多次收到相同的数据,那么就需要由发送方进行重传了。
于是,发送方继续重传数据(t8时刻开始)。
由于接收方在上面的t3、t6时刻收到了发送方发送的报文,编号分别为(1001-2000),(2001-3000)。那么,下一次接收方再次确认数据的时候,就会从3001开始确认了。
时间轴 | 发送方 | 接收方 | 报文类型 | 说明 |
t8 | 发送(1-1001的数据) | syn | ||
t9 | 接收到了数据,并返回报文(确认序号为1001) | ack |
总结一下:
对于数据报(syn)丢失的情况,接收方会只重传丢失数据对应的ack报文。
直到发送方再次发送之前丢失的数据报。
确认序号,也可以理解为接收方向发送方索要的数据
以上传输的方式,被称为"快速重传",只重传了丢失的数据。
滑动窗口的应用场景
如果需要传输的数据比较密集,那么就使用滑动窗口来处理。
如果需要传输的数据不那么密集,那么就无需采用滑动窗口处理。
二、TCP流量控制:根据接收方的处理能力,协调发送方的发送速率。
TCP流量控制是一种干预发送窗口大小的机制。滑动窗口越大,也就意味着一次可以传输的数据越多,也就意味着效率越高;
但是,窗口如果过大,也会影响发送和接收的效率。
TCP滑动窗口过大的缺点
缺点1:完全不需要等待ack的回应;如果ack丢失过多,可靠性无法保证。
缺点2:接收方来不及接收syn,发了≈白发。
缺点3:窗口过大,也是需要一定的开销的。
因此,在考虑设置滑动窗口的大小的时候:
接收方的处理能力,就是一个很重要的约束依据。
发送方的速度,不能超出接收方的处理能力范围。
这就引入了流量控制机制:根据接收方的处理能力,协调发送方的发送速率。
如何衡量接收方的处理能力
有一种量化的方法:
计算接收方每秒钟能处理多少字节
但是这种方式实际上用起来有一些麻烦,因此不太推荐使用。
更简单的方法:(直接查看接收方的接收缓冲区的剩余大小):
每一次发送方(主机A)给接收方(主机B)发送数据的时候,主机B在发送回应报文(ack)的时候,就需要把缓冲区剩余的空间,通过这个ack报文返回给A。
也就是说,接收方发送ack报文,其中的TCP报头当中的16位窗口大小就是接收方TCP缓冲区的剩余容量大小。
在这一篇文章当中,提到了TCP报头结构:
【网络原理篇2】TCP报头详解_革凡成圣211的博客-CSDN博客TCP报头结构https://blog.csdn.net/weixin_56738054/article/details/128935529?spm=1001.2014.3001.5501
里面有一个属性:16位的窗口大小.
这一个属性描述的就是窗口的大小;这一个属性仅仅只在报文类型是ack的时候才会生效。
把缓冲区剩余大小通过ack应答报文发送给发送方。让发送方明确下一次发送时候的窗口大小。
如下图:
探测报文
当ack应答报文发送的窗口大小为0的时候,就意味着此时接收方的缓冲区已经无法再次处理更多数据了,因此发送方就会暂停发送。
在等待的过程当中,发送方会发送一个不携带任何数据的探测报文,探测接收方的缓冲区大小。如果接收方的缓冲区大小不为0了,才会继续发送报文。
窗口大小为16位,也就是最大位64kb,那么是否意味着窗口大小最大是64kb呢?
不是的。在选项部分,TCP为了让窗口更大,引入了窗口扩展因子。
如果窗口大小为64kb,扩展因子里面写个2,意思就是让64kb<<2,得到的就是64*4=256kb。
三、拥塞控制
为什么有了流量控制,还需要拥塞控制
在二当中,前面考虑A的发送速率,只是参考B的处理能力(缓冲区剩余空间大小),也就是由B的处理报文的情况来决定A发送的窗口的大小。
但是,A与B之间还有其他的网络节点。具体来说,就是A与B之间的传送需要经过一段链路。于是就需要通过拥塞控制+流量控制,再次确定发送方的窗口大小。
拥塞窗口(cwnd)
拥塞窗口,是发送方维护的一个状态的变量。
拥塞控制,本质上就是通过实验的方式,来逐渐找到一个合适的窗口大小(合适的发送速率)。
大致是这样的一个过程:
进行N轮的传输与发送,统计每一轮的拥塞窗口大小
第一阶段,慢增长:
由于窗口大小比较小,每一轮不丢包(发送方收到一个ack)都会让窗口大小扩大一倍,也就是指数增长,这个过程也被称为"慢启动"。
下图来源于《小林coding》
第二阶段:避免拥塞算法
当滑动窗口到达慢启动门限(可以理解为一个阈值)之后,轮次与拥塞窗口大小就变成了线性增长的关系了。
每收到一个ack,cwnd就增加1/cwnd。
第三阶段:发生了网络拥塞
在第三阶段,一旦发生了丢包,说明此时窗口的大小已经比较大了,那么说明此时发生了网络拥塞。那么就需要把窗口大小一下缩小成很小的值,防止丢包的发生。然后重复第一、第二、第三阶段。
拥塞窗口的好处:
拥塞窗口不是固定的值,而是一直动态变化的。
随着时间的推移,逐渐到达一个动态平衡的过程。既可以找到一个合适的窗口大小,又可以让窗口的大小随着网络环境的变化而变化。
如何确定TCP滑动窗口的值
流量控制和拥塞控制共同决定发送方的窗口大小是多少。
取的是拥塞窗口和流量窗口的较小值。
四、延时应答机制
这个机制是在滑动窗口的基础上面新增的一个机制。
也就是发送方把syn报文发送给接收方的时候,接收方并不是立即返回ack,而是稍等一会儿再发送ack报文。在等待的这一段时间内,接收方的应用程序会首先把发送的syn报文给消耗一部分。
实际场景当中的延时应答机制
在实际的滑动窗口当中,并不再是每发送一次syn,就返回一次ack了。而是有可能隔一条syn发送一条ack。
五、捎带应答
捎带应答机制,其实就是一种合并内核报文和普通业务报文的机制;
在这一篇文章当中,我们也提到了四次TCP断开连接的四次挥手的过程:
【网络原理3】TCP连接管理_革凡成圣211的博客-CSDN博客TCP三次握手、四次挥手https://blog.csdn.net/weixin_56738054/article/details/128961091?spm=1001.2014.3001.5501
重新回顾一下,什么情况下四次挥手可以变成三次:那就是第二次挥手(发送ack)和第三次挥手(发送FIN)之间的间隔比较小的时候,就会合并第二次挥手和第三次挥手。
图解一下:
让业务数据捎带上内核数据,合并起来一起发送。这个捎带的过程,就是捎带应答机制。
六、粘包问题
什么是粘包问题
我们都知道TCP是面向字节流的协议。一次读取一个字节,一次读取N个字节都是可以的。
这就导致一次读取的数据,有可能是半个应用层的数据报,有可能是3/4个,也有可能是一个完整的应用层数据报,或者一次读取,读到了多个应用层数据报......
但是,在TCP的socket api当中没有告诉我们应该读取多少个字节。那么就会导致,一次读取的结果是不可预料的。
举个例子,假如有下面一段文字:
儿的生活好痛苦一点儿也没有粮食多病少挣了很多钱。
这句话,如果以TCP的形式进行传输,就有可能产生一些歧义。
一次发送的报文大小直接决定了读取的含义。
解决粘包问题:两个约定
一、关于协定的约定:
例如约定好传输层的协议是TCP还是UDP这样的轨迹。
二、约定每个包的长度
例如明确一次读取的字节大小是多少,例如read的时候尽量把字节数组填满。
也就是限制字节数组的大小。
byte[] buffer=new byte[4];
read(buffer)
一次read的时候,会把这一个字节数组填满
三、明确分隔符
以什么分隔符作为结束的标志等等,像之前的手写tcp,就是以\n作为分隔符的。
下面来一段伪代码:
七、其余的不可抗力
以下四种情况,可以分为两种情况讨论:
情况1:进程崩溃了/主机关机
情况1可以理解为进程没了,也就是对应的PCB没了,对应的文件描述符表也就释放了。
相当于调用了socket.close(),此时内核会继续完成4次挥手。
情况1会触发正常的四次挥手。
情况2:主机掉电了/网线断开了。
这两个情况,是来不及挥手的了。
如果是接收方掉电了:
步骤 | 发送方 |
1 | 发送报文syn |
2 | 迟迟没有等待ack |
3 | 触发超时重传机制(再次发送syn) |
4 | 重传无果,仍然收不到ack |
5 | 重置tcp连接(此处采用的重传是发送复位报文段RST) |
6 | 仍然无果,那么放弃连接。 |
如果是发送方掉电了:
接收方需要周期性的给发送方发送一个消息,确认一下对方是否工作正常。
此处发送一个消息,被形象地称为"心跳包",也就是验证一下接收方是否仍然在工作。
这个"心跳包"类比的就是心跳:周期性地发送
如果心跳"无了",那么说明发送方已经"无了"
以上接收方为了验证发送方是否正常的机制,被称为"保活机制"。