**前言:**本节内容是TCP的可靠性策略。 讲解可靠性策略中的连接管理、滑动窗口、拥塞控制。其实TCP的可靠性策略还有很多, 博主上一节已经讲过一部分(检验和、序列号、确认应答、超时重传) ,有兴趣的友友们可以看一下。 linux网络 | TCP报头之六个标记位与部分可靠性策略 -CSDN博客
ps:友友们在看本节内容之前最好熟悉一下TCP的报文哦。
目录
链接管理机制
什么是连接管理机制, 其实就是三次握手和四次挥手。
TCP在进行通信之前,要进行三次握手。第一次就是客户端向服务单发送请求连接SYN,然后服务端确认应答ACK + 请求连接SYN,最后客户端确认应答ACK。 此时客户端发送ACK后就认为握手成功,服务端当收到最后一个ACK就认为握手成功。
在这,我们要重新认识一下connect系统调用。 connect,其实只是创建一个SYN报文,推送给服务端,也就是connect只负责发起三次握手。 当connect发起请求后,就可以当成connect此时处于阻塞状态,服务端返回ACK + SYN后, established后,connect再返回。然后是accept,accept不参与连接,它是把底层已经建立好的数据拿上来。
然后就是数据的正常通信过程, 其实本质就是write将数据写到内核缓冲区, tcp协议
传输控制。read将内核缓冲区读取。
最后是断开协议。断开协议就是客户端先请求断开连接,发送一个FIN请求。然后服务端就确认应答ACK。一这是客户端方向上断开链接了。 然后服务端也请求断开连接,客户端就确认应答ACK。------这是服务端方向上断开连接了。这就是四次挥手。
为什么握手是三次,而挥手是四次?
这是因为握手和挥手都应该是四次,只不过握手的三次其实是挥手其中的两次合起来了。握手的第二次和第三次是可以合起来的,但是挥手的第二次和第三次是不可以合起来的。
因为在建立连接的时候,双方都是想要建立连接的。所以服务器在应答的时候同时发送一个建立连接请求是合理的。而挥手的时候可能客户端想要挥手而服务器不想要挥手。这个时候,就先客户端进行断开连接,服务器要先同意,然后再发送一些自己想要发送的信息,然后再发送断开请求。 这就是四次挥手。
为什么要三次握手
其实,三次握手和四次挥手,本质其实就是一来一回的可靠性。
客户端第一次请求和服务端的捎带应答。 这里面客户端有着一次发和一次收。服务端有着一次发一次收。意义是什么? 意义就是可靠的验证全双工是否通畅。这是第一个原因。
如果是一次握手,那么只要客户端发送了请求,我们就认为连接建立成功了。 如果此时我们的客户端一直发送建立连接的请求,那么服务端和客户端这里就都要一直消耗内存资源。这个就叫做SYN洪水。
两次握手呢,如果是两次握手,其实是一样的,只要客户端发送了请求,服务器收到了请求,那么服务器就一定要进行应答。 进行应答后,服务器就要消耗内存资源创建连接结构体。 这个时候其实是和一次握手情况一样。如果客户端不停发送SYN请求,都会造成SYN洪水的情况。这其实就是一个嫁接的原理, 如果两次握手,第一次握手我们不怕,因为这个时候谁都不会建立连接。 但是一旦到了第二次握手,这个时候服务端就要建立连接,消耗资源。然后哪怕建立连接失败,这个时候服务端的资源已经消耗了,这就导致了建立连接失败的成本嫁接到了服务端。
如果是三次握手,我们不怕第一次丢,第二次丢。因为第一次,第二次丢双方连接稍微建立完成。 连接成本不是特别高。 如果第三次丢,那么对于客户端讲,他认为他的链接建立成功了,服务器没有认为建立成功。 此时建立的成本就到了客户端的身上。 这样,失败的成本就转移到了我们的客户端,维护了服务器的稳定性。
TCP状态介绍
全连接
只要我们客户端connet上服务端,那么服务端和客户端就会有一条tcp连接。这个tcp链接,状态是ESTABLSH(完成),说明建立成功。也就是说,连接建立成功,和上层有没有accept没有关系。三次握手是双方操作系统自动完成的。
那么accept和这个链接有什么关系?
我们刚说客户端和服务端进行三次握手成功后,在服务端这边就会生成一个tcp链接的结构体对象。 客户端有很多个,所以服务端这边生成的结构体链接就有很多。然后这些链接就要被管理起来,管理的方式就是队列。也就是说这些结构体是用队列组织起来的。然后accept的时候,就会从队列里面拿到一个链接,放到文件管理里面。这样,以后进程就可以使用文件fd了。
backlog
listen的第二个参数我们以前说过是叫做backlog,这个backog + 就是表示底层建立好的连接队列的最大长度,我们把整个的队列叫做全链接队列。
如果此时队列已经满了,却还有客户端在发起连接请求,这个时候服务端就不进行回应了,这个时候的服务端和客户端的连接状态就是SYN.RECV,就是下图的这个状态:
相反, 如果此时队列没满, 那么就到了这个状态:
半链接
我们如何理解这个SYN.RECV?就是对于服务端和客户端来说,它们想要从一个状态到达另一个状态,必须要接收到对应的请求。那么对于服务端来说,如果客户端发送了一个连接请求SYN,那么服务端接收到请求后就变成了SYNRECV状态。此时服务端进行挡带应答。客户端接收到服务端的报文后就变成了ESTABLSH状态。但是当客户端再给服务端发送最后一次ACK的时候,服务端因为队列满了,所以就只能把ACK丢掉,不接受。所以就一直处于了SYN RECV状态。
SYN.RECV状态,被称为半链接。同时SYN.RECV也有自己的维护的队列。这个队列叫做半链接队列。同时,半链接的节点,不会长时间的维护。所以我们的服务端,如果有SYN_RECV状态的tcp连接。可能过一会儿这个连接就不复存在了。 (半链接长度不关心这里)
当我们的服务端的SYN.RECV半链接释放掉,客户端那边还有一个ESTABLSH呢。这个时候,服务端这边有,但是客户端那边没有,这就造成了cient和server 链接建立不一致的问题。
这个半链接和全连接合作,就类似于我们平时在学校报英语四六级。我们报英语四六级的时候,是不是经常遇到点开网页,然后服务器很繁忙的状态。这个状态显然不是服务器挂掉了,而是里面的全队列被打满了,服务器不能给我们响应,没有建立链接导致的。甚至可能半链接都被打满了,所以就造成了根本挤不进去的情况。而有时候我们可能等了一会就进去了,就是我们正好挤进去了半销接,并目从半链接拿到了全连接里面,这个时候就成功链接到服务器了。
这个listen的第二个参数为什么不能太长,为什么不能没有?
因为会造成有些链接来不及被上层处理,但事实还在系统中长时间维持的情况。就比如当我们的服务器非常忙的时候,上层没有时间将全队列里面的数据拿到上面去。但是全队列里面还非常满。这个时候就是全队列空占着资源但是不创造价值。所以就没必要将backlog设置太长。
为什么不能没有,这是因为如果上层空出位置来的时候,如果没有一个维护tcp的队列,那么此时上面就空出来了,资源闲置,就减少效益了。相反呢,如果有这个全队列,那么每当上层有位置的时候,都会拿着全队列里面的链接放到上层处理,这样效益就高。
四次挥手
主动断开连接的一方,在四次挥手完成之后,要进入time_wait状态,等待若干时常,之后会自动释放。有的时候,我们的服务器断开后,我们再重启服务器的时候,如果刚断开就重启,并且端口号还是一样的,这个时候就会绑定失败。为什么会这样呢?
为什么会这样呢?主要因为,如果我们主动断开的一方是服务方,那么服务器方就要主动进入time wat状态。这个tme wait状态,就是连接没有彻底断开,ip和port正在被使用。所以,我们如果想要重启服务器,但是此时端口号正在被上一个服务器time wait状态占用呢,就不能使用。
我们要知道,未来我们如果是一个大型服务器,几乎每秒产生的消费数字都是非常大的。所以这个时候如果一个服务器挂掉了,还不能重启,造成的影响就非常大,所以为了能够立刻就重启,就可以使用 setsockopt这个函数,来设置地址复用,这样就可以立即重启了。
为什么客户端不需要担心time_wat的影响呢?因为客户端每次重启的端口号都不一样。而服务端的端口号必须一样。
TIME_WAIT,为什么要有,并且,为什么要等待这么长时间 (TIME_WAIT要等待两个MSL)?
首先我们要知道这个MSL(即最大存在时常)的意思是什么? 首先我们要知道,一个报文从发出去到收到。这个时间窗口内,数据包都是在网络里面,数据包在网络里面的这个时间的最长时常,就叫做MSL。这个MSL一般是秒级别的。
那么,TIME_WAIT,为什么要有?我们在进行断开连接的时候,我们历史上可能发送了很多数据,不管是命令还是数据。当我们断开的时候,因为我们发送过很多数据,那么这些数据就有可能不存在于网络当中,所以等待的TME WAT就是为了让这些数据在双方信道中进行消散。------即,让通信双方历史数据得以消散。
还有一个为什么要等待这么长时间呢? 就是客户端双方进行四次挥手的时候, 如果三次回收后,这个时候客户端最后发送一个ACK, 直接就退出了。如果这个时候ACK丢失了,那么服务端就一直处于LAST_ACK的状态了。然后服务端即便向客户端再次请求FIN,客户端也不会响应,因为客户端已经释放了。------即,让我们断开连接,4次挥手,具有较好的容错性。
但是,这里有一个问题了,MSL为什么是秒级别的? 我们的网络收发一次,有可能是正常的报文,有可能这个报文就阻塞了。如果是正常的报文,那么它在网络中的最大传送时常和最大存在时常是相同的。但是如果是不正常的报文,这个报文阻塞住了,那么这个报文的最大存在市场就应该更长,因为它在一直阻塞着呢。如果是最大传送时常,那么就是毫秒级别的,如果是最大存在时常,那么就多了,并且不稳定,因为路由器有时候卡的话一直阻塞,可能阻塞很长时间。所以,一般建议这个是秒级别的。
流量控制
TCP根据报文里面的十六位端口号来判别接收端的接受能力,来决定发送端的发送速度,这个过程就叫做流量控制。 这个博主在之前在讲解十六位窗口大小的时候已经谈过。
这里有几个问题就是:
- 第一次的时候,怎么保证发送的数据是合理的呢? 这个就是三次握手的工作,三次握手不只是三次握手,三次握手也交换了报文!!!已经协商了双方的接受能力!!!
- 2、第三次握手的时候,是可以携带数据的。(本质上就是捎带应答)
- 3、流量控制,属于可靠性,还是效率。答案是既属于可靠性,又属于效率。
滑动窗口
滑动窗口三个结论
第一个问题,在这些多条报文中,其中有许多没有收到应的报文。这些报文我们之前说过要被tcp暂时保存起来,那么保存到哪里呢?
第二个问题,在这些报文中,有许多已经发出去,但是没有收到应答的报文,这些报文是不是有非常多个?
这两个问题能合成一个问题,就是这些报文中,已经发出去,但是暂时没有收到应答的多个报文,会被保存到哪里呢?
其实就是保存到发送缓冲区中。发送缓冲区分为三个板块,一个是已发送已确认,一个是已发送末确认,一个是待发送。那些发送出去,未收到应答的报文,就是在已发送未确认报文里面。这个已发送未确认报文,就是滑动窗口,大小是可变的!!!
所以第一个结论,滑动窗口在哪里?------就是我们发送缓冲区的一部分!!!
我们的通信双方,正是因为有着滑动窗口的存在,才可以一次性向对方发送大量的报文。所以第二个结论一-滑动窗口的大小,是对方接受窗口。
滑动窗口,我们说发送缓冲区是一块连续的控件,这个滑动窗口是怎么做到的呢?
这个就类似于那个紧急报文,就是利用两个整型变量,指向两个字节位置。 然后分隔成了三个部分。 第一个部分就是已发送已确认,第二个部分就是已发送未确认,第三个部分就是待发送。
所以第三个结论-如何理解区域划分,就是通过指针/下标来进行区分!
丢包时如何理解滑动窗口
目前认为,至少滑动窗口的大小,不能超过对方的接收缓冲区的剩余空间的大小,即应答报文的窗口大小。
现在有几个问题:
问题1、如果丢包了,怎么理解滑动窗口?
先讨论ACK丢了。
我们之前说过,我们对于确认序号的定义:确认序号是x,x之前的报文我们全部收到了!也就是允许少量的ACK丢失。所以,就像上图,即便1001 ~4000的报文没有对应的应答,但是有一个应答的确认序号为5001,那么就认为5001之前的报文全部都被收到了!
那么滑动窗口向前滑动,就要滑动到5001的位置!而不会卡在1001,即便1001没有应答!!!
然后讨论数据的丢失:
假如我们的1001收到了,2001收到了,4001收到了,5001收到了,但是3001没收到。对于数据,没有收到不能装作收到。所以确认序号最多就到2001。也就是说,即便是4001那里,5001那里,填写的确认序号也是2001。到时候滑动窗口等待左指针,最多就向右滑动到2001。 所以,我们就不用怕滑动窗口直接越过3001的问题。
所以,确认序号的定义,就保证了滑动窗口,线性的连续的向后更新不会出现跳跃的情况。
就是上边的情况,一开始1~1000的数据发送了,然后1001确认应答。然后呢, 1001~2000的报文丢失了。 那么就不能确认应答了。并且之后的报文像2001~ 3000、3001~ 4000等这些报文,确认应答都是1001,不能是3001或者4001。1001~2000的报文丢失了,那么就要等,等到判定为超时的时候,就进行超时重传。 但是这样等待的时间就太长了。 所以就规定,一旦有三次重复的确认应答, 那么就立刻进行超时重传。 然后进行确认应答。这就叫做快重传。
问题是,已经有了快充穿,为什么还要有超时重传呢?主要是因为快重传是有条件的,只有超过三次重复的确认应答时,才会出现快重传。所以,快充穿的本质是来提高效率的。即便有了快重传也不能摒弃超时重传,超时重传是用来兜底的。
如何理解滑动窗口的变化
问题2:如何理解滑动窗口的变化?
滑动窗口会不会向左移动,会不会向右移动,移动的时候大小会变化吗? 怎么变化呢?会变成0吗?
首先不会向左移动,因为序号和确认序号只会增加。会向右移动。移动的时候,窗口的大小是变化的。动态变化,那么就涉及到三个,变大,变小、不变。
所以针对性的,我们就要提出来向右移动的三种方式:右不变,左移动、左右都移动,范围变大(主机A一边向主机B发送数据,主机B一边向上取数据)、左不变,右移动。
start和end是标已窗口大小起始和结尾的招针。是int类型,start就是确认序号。 根据确认序号来设置start的值。即,start等于确认序号。end指针就等于确认序号 +窗口大小,这个窗口大小就是对方给我们的ACK中一定也存在窗口大小,到时候end就直接等于stat + ACK中的窗口大小即可。
滑动窗口会越界吗
TCP采用了类似环状算法。就是那个基于数组的环形队列。所以就不需要担心溢出的问题。
拥塞控制
其实当我们数据通信的时候,如果发送数据,出现问题,不仅仅是对方主机出现问题,也可能是网络出现了问题。
如果通信双方出现了大量的数据包问题(少量可能是因为常规性质的问题),tcp会判断网络出问题了(网络拥塞) 。
这个时候我们发送方,应该怎么办?------我们不能对报文进行超时重发!!-为什么?因为会加重网络的拥塞。所以,应该怎么办呢?比如等一等,比如发送少量的。
为什么等一等就可以了呢?因为一个局域网当中的主机非常多。所以,到时候一个主机少一些报文,那么一群主机一起减少。就可以减少大量的报文。
拥塞控制控制策略
TCP引入慢启动机制,先发送少量的数据,探探路。摸清楚拥堵的状态,再决定按照多大的速度传输数据。
拥塞窗口
滑动窗口的大小 = min(窗口大小,拥塞窗口),窗口大小考虑的是对方主机的接受能力。拥塞窗口考虑的是动态的,网络的接受能力。
那么拥塞窗口是什么呢?就是主机判断网络健康程度的指标,超过拥塞窗口,会引发网络拥塞,否则不会。拥塞窗口本来就是用来评估网络状态的,网络是动态的,所以拥塞窗口肯定是动态的。
慢启动
TCP虽然有了滑动窗口,能够提高很大的效率。但是如果一开始就发送大量的报文,还是有很大的问题。就比如网络上很多计算机在不知道现在网络状态的情况下,直接一股脑发送大量的数据,就有可能引起网络的更加拥堵。
所以TCP引入了慢启动的方式,就是先发送少量的数据,探探路,摸清楚当前的网络拥堵的状态,再决定按照多大的速度传输数据。
就比如上面的图,这里面就用到了拥塞窗口,因为拥塞窗口可以一开始为1,然后指数上升,当拥塞窗口的大小小于滑动窗口的大小的时候,发送的数据量就按照拥塞窗口来控制。
就是一开始定义拥塞窗口大小为1。每次收到一个ACK应答,拥塞窗口加1。每次发送数据包,就将拥塞窗口和滑动窗口作比较,取小的那个作为实际的发送的窗口。
上面这就叫做慢启动。为了不增长那么快,所以不能让拥塞窗口单纯的加倍,此处就引入了一个叫做慢启动的闽值,当拥塞窗口超过这个闻值的时候,不再按照指数方式增长,而是按照线性方式增长。
网络出现拥塞,发送少量的报文,如果都ok,就说明网络已经趋于健康了。应该尽快恢复正常通信了。
那么实际机器发送的数据量,会一直指数级别增长吗?答案是不会, 原因就是因为:当拥塞窗口超过这个闻值的时候,不再按照指数方式增长,而是按照线性方式增长。
慢启动的阈值:最近一次发生网络拥塞时,拥塞窗口大小/2;一旦网络发送拥塞,此时拥塞多大,然后下一次闯值就是拥塞窗口的大小/2。
随着拥塞窗口越来越大,说明拥塞的改率越来越高。 最后一定会导致拥塞,但是实际上在发送数据的时候,不仅仅拥塞窗口限制我们,对方的接受能力也能限制我们。所以发送网络拥塞并不是一定会发生的。
------------------以上就是本节全部内容哦, 如果对友友们有帮助的话可以关注博主, 方便学习更多知识哦!!!