滑动窗口异常:
1.丢包问题
1.1应答报文丢失
对于并行发送多个数据报文丢失,如果是中间的应答报文丢失则不会关心,因为确认序号的存在是允许除了最后一个数据报文的应答不可以丢失之外,其他应答的丢失是可以丢失的;如果最后一个报文也丢失了,那就进行丢包重传;
1.2数据报文丢失
对于并行发送多个数据报文丢失,如果是中间报文丢失,将来接收方返回的应答报文里确认序号只能是连续成功发送的数据报文的最后一个序号+1;
以上两种情况表明了滑动窗口的滑动是连续的,不会越过某一个丢包异常;
对于补发是要进行等待的,所以当客户端连续收到重复确认序号的应答报文时,就会直接进行数据的补发,然后因为接收端就会发送应答,确认序号为以收到连续数据报文的最后一个序号+1;即快重传就是如果收到了3个同样的确认应答时,则立即补发对应数据报文;
超时重传和快重传的机制必须都有,快充穿是为了提高效率,但是在如控制的方面无法实现快重传,因为要保证发送数据报文和接收到应答报文是串行的或者在发送一批数据报文的最后就不满足快充穿的条件了,需要使用超时重传来进行兜底;即快重传提高了上限,超时重传保证了下限;
总结:1.使用滑动窗口使得可以支持发送多个数据报文,并且利用确认序号的机制可以保证数据的可靠性;2.使用快重传保证可靠性的同时也提高了重传的效率;
2.滑动窗口滑动问题
2.1左移问题
滑动窗口是不会向左移动的,即数据不可能从已发送已确认变成已发送未确认,不符合逻辑;
2.2滑动窗口大小问题
由于滑动窗口的大小与应答报文的16位窗口大小有关,所以需要大小动态变化;当滑动窗口向右移动时,窗口可以变大也可以变小;
流量控制就是通过滑动窗口实现的;
实际上发送缓冲区是被分成了四部分,除了待发送区域外,最后一部分就是空区域,表示不进行发送了;空区域其实是可以归类到已发送已确认区域,因为两者都是有了数据就可以进行覆盖了;
是所以用来进行区域划分的两个特殊下标应该是win_start = 应答报文的确认序号, win_end = 确认序号 + min(接收发接收窗口的大小,有效数据的大小);即可能存在接收窗口大小较大,但是有效数据的大小很少的情况;
3.TCP滑动窗口滑动越界问题
对于发送缓冲区这种数组结构,由于TCP采用了环状算法(如模运算)所以是不会越界的;
4.序号问题
由于存在历史数据报文还可能存在,尽管可能性极低,但是也是会造成问题,所以就是用了随机序号进行解决;双方在一开始的序号都是随机的,在双方通信的时候会进行序号的协商;即在三次握手的时候发送方发送SYN,其中包含了数据报文,其中自己设定了随机序号,接收方收到了报文会返回ACK,应答报文的随机序号是接收方自己设计的;通过三次握手双方就协商好了通信时使用的随机序号;最后会默认两个随机序号的较小值为之后通信时的起始序号;
由于起始序号是随机的,所以与历史遗留的数据报文冲突的概率就减小了;
真正的序号 = 起始序号 + 报文最后位置的数组下标;确认序号 -起始序号 = 下次在发送缓冲区中要进行发送的位置;所以通信双方都要将约定好的起始序号保存起来;由于每次建立连接创建的起始序号大概率不同所以降低了收到历史遗留报文的可能性;
11.3.9延迟应答与捎带应答
当接收方收到报文的时候先不着急进行应答,而是等上层应用层充分地拿到数据报文进行处理,这样接收窗口就增大了,这种方式就叫做延迟应答,但是要注意的是延迟是不可以超过重传时间的;
但是要注意的是,延迟不一定真正地提高了发送效率,而是一种可能性,还是取决于上层是否取走了数据;所以为了提高效率,上层应该尽快地取走数据,TCP根据延迟应答就大概率的可以实现提高发送的效率;本质上就是接收窗口增大,应答的时候尽量保证是大窗口;
由于并不是所有的报文都要进行延迟应答,所以延迟应答要进行限制,如:1.数量上进行限制,每达到一定的数量就发送一次报文;2.时间进行限制,如:设置最大延迟时间,当时间超过了就立即发送一次应答;一般数量设置为2,最大延迟时间为200ms;
捎带应答通过合并报文提高效率;
11.3.10拥塞控制
拥塞控制考虑到了网络信道的情况;
网络问题:硬件设备出现了问题;数据量太大引起阻塞问题;
通过之前的策略,已经可以保证出现少量的丢包是常规的情况,但是还是大量丢包就说明不是本地的问题,极大概率是网络的问题了,会认为是网络拥塞了;
网络是一个共享资源有着大量的主机进行着通信,当出现网络拥塞时,TCP就会使得所有的主机降低发送的效率,使得网络可以处理掉拥塞的数据,然后恢复发送效率;
大量丢包引起的问题:1.不断地超时重传,此时不能立即进行数据报文的超时重传,会造成更加拥塞或者发送是无意义的,发送方采取的策略可以是等待或者发送少量的数据;
拥塞对于不同的主机是不可能立即都检测到拥塞,而是一种渐变的过程,随着主机不断地发送数据报文,拥塞程度增加,进而更多的主机就可以检测到拥塞并进行了同步,所有这种设计是一种动态调整的设计;
拥塞控制采取的策略,TCP引入了慢启动(开始时较慢,实际上之后是很快的)机制,先发送少量的数据来探测网络拥堵的状态,如果还是丢包了,那么就进行超时重传,如果收到了应答就发送报文数量加一,这样就实现了在拥塞状况下,下一次发送报文是上一次发送报文数量的两倍;
拥塞窗口用来进行判断网络健康程度的指标,当发送到数据量超过拥塞窗口的大小是就会引发网络拥塞;
当识别到网络拥塞了,就定义拥塞窗口 的大小是1,每收到一个应答,拥塞窗口的大小就加一,最终滑动窗口的大小就是拥塞窗口,接收端窗口,还有有效数据大小的较小值;
慢启动阈值:最近一次放生网络拥塞时,拥塞窗口的大小/2;
当TCP开始启动的时候, 慢启动阈值设定了初始值,开始指数增长,之后达到阈值线性增长;在每次超时重发的时候, 慢启动阈值会变成发送拥塞时,窗口大小的一半, 同时拥塞窗口大小置回1;
总结:拥塞控制就是指数增长+线性增长+拥塞调整;
11.4TCP对网络状态的评估
TCP不仅仅对通信的两个主机的可靠性和高效性设计了策略,而且还在设计策略时考虑到了网络信道的状态,但是并不可以对网络状态进行控制;尽管不可以采取直接的措施,但是却可以设计一些策略,如:拥塞控制;
11.5TCP协议保证可靠性和高效性的策略总结
11.5.1可靠性策略
1.校验和,保证接收到的报文至少是有一个完整的报头;
2.序列号,保证按序到达和去重;
3.确认应答,保证可靠性的核心机制,通信只有一方是无法实现的,注定了通信过程中一定不可能所有的数据都是可靠的,只需要保证单向通信通信是可靠的就行;
4.超时重传,防止数据丢包或者超时导致问题,当然也会引发重复数据的问题,需要使用序号进行去重;
5.连接管理,进行三次握手和四次挥手,实现通信前和通信后的可靠性准备,如:三次握手和四次挥手尽量保证信道畅通,风险损失降低,协商起始序号降低异常或者恶意风险概率,CLOSE_WAIT状态的提醒关闭,TIME_WAIT状态的安全收尾以及全连接和半连接的并发设计;
6.流量控制,使得发送效率提高或者降低;使用窗口探测和窗口更新实现接收窗口的实时大小;
7.拥塞控制,针对网络拥塞,动态控制拥塞窗口的大小,来控制发送的数据量;
11.5.2高效性策略
1.滑动窗口,通过对发送缓冲区的划分进行支持发送一批数据报文,辅助流量控制的实现;
2.快重传,收到3次同样的应答报文,立即进行重传,而不是等待超时,提高了效率;
3.延迟应答,尽量保证发送方接收到的是应答报文的接收窗口大小较大,可以通过应用层来提高接收速度,使得发送效率提高;
4.捎带应答,将数据报文和发送报文合并成一个报文降低发送成本,提高效率;
11.6面向字节流
TCP因为有发送缓冲区和接收缓冲区的存在,所以是全双工的通信;
UDP中发送是以完整的一个报文为单位,读取单位也必须是一个完整的报文,所以是面向数据报的,即发几次就得读几次,必须严格匹配,发快递就是这样;
TCP只认识二进制序列以字节为单位,会不断地将发送缓冲区的数据发送到远端接收缓冲区,这样就实现了类似流的概念;需要应用层进行判断控制来获取一个完整的报文;TCP的首部长度没有包括数据的长度,因为TCP的任务是根据报头,将数据正文进行按序拼接到接收缓冲区,不需要知道正文的大小,由应用层来控制一个完整报文的划分,每次应用层缓冲区读取所以接收缓冲区的内容,然后进行控制分离出来一个完整的有效数据;而UDP16位长度包括了数据正文,即在传输层就保证了接收方获取一个完整的报文,每次上层读取到的是完整的有效数据;
11.7TCP面向字节流引发粘包问题
如果应用层不将字节流数据划分成一个个完成的请求,就会出现多处理或者少处理的情况,这种现象就叫做数据报粘包问题;粘包问题是相对于应用层讲,TCP面向字节流不关心粘包问题;
如果在接收缓冲区边读边解析这种做法会降低滑动窗口的大小,降低传输层IO的速度,所以一般应该交给应用层处理;
通过在应用层定制协议来解决粘包问题;即应用层协议不仅考虑到了序列化和反序列化,还要处理粘包问题;
解决方法:1.定长报文,读取到的长度达到规定长度才进行处理;2.使用特殊字符,读到特殊字符表明读到了一个完整请求;3.使用自描述字段+定长报头(类似UDP在传输层实现了划分);4.使用自描述字段+特殊字符(类似http的实现,空行划分报头和正文,使用len+'\n');
使用UDP方式,会使得应用层读取到的是纯粹的有效数据,可以直接而进行反序列化,而使用TCP方式,上层读取到的可能是纯粹的有效数据(定长报文)也可能是自描述字段+纯粹的有效数据(自描述字段+定长报头或者特殊字符);
总结:解决粘包问题就要明确数据包之间的边界;即使是下层也存在数据报粘包问题,所以也使用了定长报头+自描述字段来进行解决;
11.8TCP连接异常问题
1.进程终止
连接本身和进程是没有直接关系的,而是与文件有关系,而文件的生命周期是随进程的,所以连接是随进程的;进程终止了,文件要挥手,对于连接来说就是要进行释放连接四层挥手;
2.机器重启
机器重启所有的进程杀死,最终还是要将连接释放;
3.机器掉电/网线断开
在客户端机器掉电,内存中所有的痕迹全没了,连接也就没有了;而网线断开本地就会自动断开连接;对于服务端连接还在,这样就会导致通信双方出现连接认知不一致的问题,服务端向客户端发送RST报文,让客户端重新建立连接,服务器直接关闭连接;
如果客户端不发送消息了,直接关闭了,服务器因为保活机制,即在选项里有保活定时器,如果客户端长时间不发送消息,会发送几次带保活选项的报文,如果都没有应答就认为对方关闭连接了,然后自己就关闭连接;
telnet发送的报文往往会将PSH置为1,催促应用层处理接收缓冲区数据;
11.9文件和套接字的关系
struct file结构体对象里存放了 void *private_data指针变量,如果文件是网络文件,该指针就会指向struct socket结构体对象,而struct socket内部也有struct file*指针变量指向文件结构体对象;所以在操作系统用内部可以用文件找到套接字结构;
socket结构里有等待队列,当网络发送和读取条件不就绪时,将进程挂接到等待队列里;
socket结构里还有协议的操作方法,用来进行套接字的相关操作;file结构内部也有对应的方法集;
socket内部有struct sock*指针,指向struct sock结构;sock内部包含了接收和发送队列;
socket内部有相关属性可以区分套接字类型;
struct udp_sock内的第一个成员就是struct sock结构体对象sk,之后是其他相关属性;struct tcp_sock内的第一个成员也是struct sock结构体对象sk,之后是其他相关属性;
在创建tcp套接字时会创建struct tcp_sock结构体对象,第一个属性字段是struct sock字段;
**使用socket通过相关属性识别套接字类型,将内部的sock强转成对应的套接字类型,就可以访问其他属性了;**这种方式的本质也是多态;
当进行网络通信时,文件方法从磁盘相关方法就变成了与网络相关的方法,除了网络方法(对于传输层包括下层的数据交互相关方法和对数据的处理方法)还需要提供对上的相关方法(如write,read,recv,send方法);
sk_buff_head是接收队列或者发送队列结构,使用的是双链表,内部存放的是sk_buff*节点;sk_buff内有head,data,tail,end的指针共同维护一段内存空间;head指向报头起始位置,data指向有效载荷起始位置,tail指向有效载荷结束位置或者报文结束的位置,end指向整个空间的最后位置;
报文的head并不是指向最开始的位置,前面是还有一段空间的;在数据报文流动的过程当中,封装报头本质就是在报文空间的开头添加报头信息,不断向下交付就是不断添加报头,head指针不断的往上移动,最后就移动到了最开始的位置;而解包就是head指针不断向下移动;即整个报文在数据流动中是唯一的,只不过是head指针不断向上或者向下移动,没有频繁地拷贝;