Linux下的TCP滑动窗口
1.UDP报文解析
我们的UDP进行通信的时候,有报头和数据2个部分,我们需要对他们进行认识和理解帮助我们理解通信的本质。
首先,任意一个协议进行通信都要解决的问题是,报头和有效载荷的分离,如何交给上层。
UDP中,它有一个16位UDP长度,我们通过这个数据知道整个UDP报文的大小,然后我们协议又约定了我们的UDP报头的部分固定是8个字节,这样通过这2个数据,我们用报文的总长度减去我们的报头就得到了我们的有效载荷的大小。这样我们就解决了我们的报头和有效载荷的分离。
那么我们又该如何解决如何交给上层的问题呢?
答案是我们的报头中有我们的16位源端口号和我们的16位目的端口号,通过这2个数据我们可以知道我们的报文从哪里来,到哪里去,交给上层哪个协议我们也就知道了。
实际上我们的报头在内核中的结构体就是这个样子
下面我们总说报文报文,我们要理解一下报文,把理解建立在具体的事物上,而非抽象的概念上
我们的主机进行通信,肯定同时有很多个正在通信的报文,可以肯定的是我们的主机中肯定存在大量的报文,那么我们的操作系统要不要把这些报文管理起来呢?要的,如何管理先描述再组织。
我们就有描述报文的结构体和我们管理这些结构体的数据结构。
描述报文的结构体就是sk_buff
下面我们来看一下我们的进程PCB和我们的套接字的关系,我们一个进程有一个PCB,里面有我们进程的属性,然后它有文件描述符表,里面有我们这个进程打开的文件,套接字也是一个文件,我们通过我们的下标拿到这个文件,套接字里有一个指针指向我们一个结构体,该结构体里面有我们的发送缓冲区和接受缓冲区,缓冲区里就有我们的报文。
我们下图中的发送缓冲区和接受缓冲区就可以通过我们进程打开的文件,文件里的套接字文件找到我们的缓冲区,缓冲区就是一个队列,队列里面有我们的发送的报文,和接受的报文,至此我们的系统和我们的网络进行了串联!!!
2.TCP数据报解析
当我们的TCP在进行传输的时候,它有报头和数据2个部分,我们需要一个一个来认识这些,从而才能更深刻的理解TCP为什么是天才的造物!它兼顾了稳定和高效。
我们说过任意一个协议都要解决报头和有效载荷分离的问题以及如何交付给上层的协议。
要解决第一个问题,我们就要了解一下什么是4位首部长度了,它的范围是【0000,1111】,弄成十进制就是0-15,单位是4字节,所以我们的范围是【0,60】字节,然后我们的报头固定20字节,多出来的就是我们的选项了,所以我们的选项就是20-40字节大小了,所以我们的4位首部长度表示的是我们的报头+选项的大小,那么如何分离我们的报头和有效载荷呢?没有总长度如何分离呢?答案是TCP的分离根本不需要总长度,这是因为我们的TCP是面向字节流进行传输的,我们只需要拿到我们报头+选项的长度,其余部分就是我们的有效载荷了。
TCP在传输过程中,当我们应用层把数据交付到它的发送缓冲区内,它就能决定什么时候发,发多少,可能会把我们多次写入的数据合并成一个进行发送,也可能把我们一次写入的数据分成多个包进行发送,所以TCP只负责把我们的数据安全可靠的传送到对端,并不负责有效载荷数据的分离。
第二个问题,如何向上交付呢?通过16位目的端口进行交付。进程内部有套接字,套接字绑定了IP和端口,知道目的端口就知道交付给谁了。
下面是TCP的报头在我们结构体中的具体。
3.TCP的可靠性和效率
我们知道我们TCP是可靠的数据传输,那么TCP是如何保证可靠性的呢?
我们问一个问题是?存在百分之一百不丢包的协议吗?不存在的,所以我们TCP为了保证可靠做出的办法是发出去的请求需要应答,收到应答,历史报文就可以确定收到了,最新发出去的消息,永远不能确定是否收到,所以确认应答机制给了我们一个可靠性的保证。如果对端收到了请求,就会给我们发送一个应答,被应答的报文,就是百分之百收到了。
有一些细节需要注意,我们发送的永远是报文,请求和应答都是以报文的形式发送出去的。
消息不等于应答,消息包含了我的数据,应答单纯就是一个应答
我们要保证的是消息的可靠性,不是应答的可靠性。
应答方,不确定应答是否收到,但是发送方,有没有收到对方的应答是确认的,所以我们如果发送方没有收到应答就会重复发送报文给对方。
收到应答的本质是为了保证厉害消息的可靠性。
我们知道为了保证我们的消息送达,对我发送的消息,对端得给我个应答,但是这样我发送一个请求,对端就得给我一个应答,等收到应答,我才可以发送下一条消息,这样我们的执行就是串行的,效率低下。
为了提高效率,我们引入了确认应答机制,我们客户端发送消息可以连续发送,不必等待对方的应答到来才能发送下一条,等我发送完了,对方再给我一条一条应答,但是这种发送不一定是顺序的,打个比方我们在东北,我们要去北京,难道最先出发的一定是最先到达的吗?不一定吧,那按这个道理来推算,活的久的人一定更有智慧啊,也不一定吧,韩愈就说过,弟子不必不如师,孔子不就比他老师的学问大吗?所以我们为了保证数据的顺序,我们引入了序号,我们发送的时候在报头会给我们的数据报带上序号,这样对端收到数据即使是乱序的,也能根据序号把它们按顺序排列。
上面说的这个过程,都是发生在内核的,对我们用户来说就是完全透明的!
又有问题来了,假如我发送的4个报文里面,有一个丢失了怎么办?报文丢失了,我们就收不到应答,收不到应答就需要重发。
而我们的报文里的报头除了有我们的序号保证发送的报文顺序之外,还有我们的确认序号,确认序号的意思是,该序号之前的报文我已经全部收到了,下次发送,请从确认序号开始发送,这个不是对一个报文的确认,这个是对历史所有报文的确认,比如我发送1000-5000,1000-2000,3000-5000全部正确发送,但是我们的2000-3000丢包了,那么我们发送过来的应答就一直是2001,表面2001序号之前的报文全部收到了,当我们的客户端连续接收到一样的确认序号之后,就会对我们的2000-3000数据进行补发。
我们必须明确的是,我们发送的请求和应答都是以我们报文的形式发送的,而不是什么概念的请求
我们在我们TCP通信过程中会出现捎带应答的现象,比如我的服务端正好对端要发数据,这个时候也有应答要发送,我们就自动把请求和应答合并为一个报文发了过去。这个就叫做捎带应答,顺手的事情。
流量控制;
我们的TCP报头里面还有16位窗口大小,这个数据的含义是我的缓冲区里还有多少空间。
我们的缓冲区都是有限的,我发送报文的时候如果不知道对方的缓冲区大小,就无法根据大小动态调整,所以我们得知道对端的缓冲区大小,对方的缓冲区空间大,我多发一点,小的话我少发一点,不然的话可能会导致发送过去的报文被丢弃或者速度过慢。应答是以报文的形式进行的,所以每次发送数据都会把自己的缓冲区大小交给对端。这就是我们所谓的流量控制。
这样我们既保证了可靠性,不让报文大量丢包,也保证了效率,不让速度过慢。
我们要知道我们的报文是有类型的,有的报文是建立连接的,有的报文是断开连接的,那么用什么来表示这些类型呢?就是用我们的标志位来代表我们的类型。
0无效,1有效。
有哪些标志位呢?
ACK表示该报文是一个确认报文。
SYN表示是建立连接的报文。
FIN是结束连接的报文
建立连接的三次握手就是有对应的标志位。
其实是四次握手,只不过有一次捎带应答,这样建立了建立连接的共识。
就好比结婚的时候需要2个人都同意才可以。
四次挥手的过程:
通过4次挥手统一了断开连接的共识,就好比结婚的时候需要2个人都同意才可以。
PSH标志位代表着这个数据需要快速被应用层取走。
RST标志位代表着RESET就是重置连接,用来处理连接异常的时候,让对方进行连接重置,我们实际的通信过程中,会存在各种各样的连接问题,当连接异常的时候,我们通过这个让对方重置连接。
我们再来理解一下三次握手,对于客户端来说当它收发送请求和收到应答的时候客户端就认为连接就已经建立成功了,但是如果客户端给对方的应答丢失了呢?这个对客户端来说是不知道的。
这个时候客户端以为连接已经建立好了,但是当它发给对方时,对方是没有建立好的,所以对端会给他发送一个带RST的报文进行告诉客户端,连接没好,你进行重置吧。
对服务端来说,它的内存里一定存在着很多连接,那么它要不要对这些连接进行管理?需要,怎么管理,先描述再组织。
下面我们再来了解一下TCP的紧急指针,在我们日常生活中,我们在迅雷上下载东西,如果这个时候我要终止下载,但是TCP是按序到达的,按这个来说我就得等资源下载完才能终止,这个不符合我们的需求呀,所以我们有一个紧急指针,让它完成插队。它与URG搭配使用完成我们的插队,但是不常用。
紧急指针指向的是一个有效载荷里面需要紧急处理的数据。
URG代表内部有紧急数据,紧急指针有偏移量,紧急数据只能有1个字节,所以我们就可以通过这两个搭配实现我们的插队。
我们的TCP设计相当灵活,序号保证了按序到达,URG和紧急指针又能实现插队。保证可靠的同时又提高效率,天才的造物。
下面来详谈我们的TCP为了可靠和效率的策略:
TCP滑动窗口
我们的TCP通信为了有一个更大的窗口,会选择延时应答,发出去的请求不会立即应答,而是等待200ms后再进行应答,这是因为我们的服务端上层可能在这期间会消费数据,给我们返回一个更大的缓存区,同时如果这个时候服务端有请求也可以进行捎带应答。
由于TCP是面向字节流的,当我们把多个数据放入我们的发送缓冲区,它可能为了效率会把我们的多个数据合并成一个数据报发送给对面或者把我们的一个包拆分成多个小包,然后按照我们的序号在对端的接收缓冲区里按序号排好,站在应用层的角度看到的就是一堆字符串,它不知道如何划分我们的字符串边界,我们把这个问题叫做TCP的粘包问题。
我们是如何解决这个问题呢?
一个是我们的定长,就是我们固定每个消息都是100字节,每次读取100个字节,100个字节就是一个消息。
分隔符,我们在我们一个消息结束之后加上我们的特殊符号,识别到我们的特殊符号就是我们的这个消息结束了。
长度前缀法,在我们的消息前面加上一个这个消息的长度,根据我们的消息长度读到它的消息大小。
我们的UDP是没有这个问题的,这是因为我们的UDP每次发送都是一个完整的消息,它不会对我们的消息进行拆分合并,每个消息之间天然就有边界,我们只需要每次读取一个UDP报文,我们就可以拿到我们一个完整的消息。
TCP异常
我们的TCP在发送和接受消息的时候可能会出现异常,比如我们的进程关闭,我们在学习系统的时候知道,当我们的进程关闭的时候,我们的fd会被释放,如果我们的文件的引用计数归为0,它也会被释放, 所以进程关闭会自动释放我们的网络文件,正常的调用四次挥手。
电脑关机,在电脑关机之前,会把我们的进程全部干掉,才会关机,所以和我们的进程终止是类似的。
机器断点/网线被拔掉,这个就比较特殊了,比如我们的客户端把网线拔掉, 检测到网卡的状态变化,我们客户端的主机会触发硬件中断,然后操作系统根据中断向量表把我们的连接释放掉,如果在网线拔掉之前,我们的客户端向我们的服务端发送了一个请求,然后我们的客户端断网,这个时候当我们的服务端向我们的客户端发送应答的时候,我们的连接已经被关掉了,如果这个时候客户端再插上网线,由于我们的连接已经关闭,它会向我们的服务器发送RST,进行重连,但是我们的操作系统检测到我们的服务器向一个不存在的连接里进行写入,我们的操作系统会给我们的进程发送信号,干掉这个进程,但是我们的服务器不能被随便干掉, 所以我们需要把我们的SIGPIPE信号屏蔽掉,防止我们的服务器被干掉。
这个时候再让我们梳理一下,我们的客户端在断网前给我们的服务端发送一个请求,然后它就挂掉了,我们的服务端接到我们的客户端请求之后,会给它一个应答,这个应答肯定发送不到,然后我们的服务端还需要给我们的请求进行回复,回复的时候需要收到客户端的应答,我们的服务端进行多次的重传之后发现,客户端还是没有反应,我们的服务端判定连接失效。
还有一种情况就是我们的客户端断网前发送一个请求,然后它断网,断网后它又恢复了,这个时候我们的客户端会接收到我们的服务端的回复,但是这个时候连接是没有建立的,在连接没有建立的情况下,我们的客户端会给我们的服务端发送RST,然后我们的操作系统检测到我们的服务端在向一个错误的连接里面进行写入,会发送信号干掉我们的服务端,为了防止被干掉,我们捕捉到这个信号。
那么如果我的客户端不发送消息呢?那服务端怎么知道连接已经失效的,这是因为我们的TCP还有一个保活机制,隔一定的时间会向客户端发送一个报文,如果收到回复,判定客户端还在,如果收不到回复,会进行超时重发,然后一定的次数之后,会判断连接失效。如果客户端断网后又恢复,会向我们的服务端发送RST。连接失效,进行重连。