TCP协议
TCP全称为 “传输控制协议(Transmission Control Protocol”)。它最重要的是解决在传输层通信的过程中,解决网络通信过程中可靠性的问题。当然,很多人在理解TCP的时候,往往只知道帮我们解决可靠性,但是,同时呢,还帮我们解决我们所对应的效率问题。
》传输控制协议呢,我们要对数据进行详细的控制的话呢,那么它的报文格式也是在应用层交付到传输层的时候呢,从上往下交付的时候,跟UDP一样,也是要添加TCP报头的。TCP报文格式呢就是上图的样子。下面呢,来认识一下报文的格式。
》其中呢,我们的报文整体宽度是0~31位,20字节。一行就是4字节,一共有5行,5行呢总共20字节,是TCP的标准长度。TCP还有一个字段就是选项,TCP里面有些选项呢,诸如:保活、安全控制的一些字段,但是我们暂时不考虑,我们重点是将其前20个字节搞定。
》其中呢,添加报头,还有一个“数据”字段,这个数据呢,就是你从应用层交付给传输层,其中是把自己的数据拷贝到内核当中的,其中这里的数据呢,就是应用层交给TCP协议的数据,在TCP协议这里,我们叫做有效载荷的问题。
》那么其中对我们来讲呢,作为一个TCP协议,其中它的字段呢,有几个字段是一看就明白的,第一行呢,就有源端口字段和目的端口号字段。关于源端口号和目的端口号我们就不解释了,在TCP这里,它叫做传输层协议。它对上层提供的系统调用接口呢,是可以帮助我们来进行网络TCP socket通信的。所以,我们当时写套接字的时候,我们的服务端需要绑定IP和端口,说白了就是套接字。客户端在进行连接的时候呢,也是需要conet连接目标主机的,在连接过程中呢,操作系统会自动给我们指定IP和端口号的。换句话说呢,在端口号这件事情上,传输层协议的报文必须携带端口号,因为这个端口号解决的,尤其是这个目的端口号解决的是我们将来的报文被分包之后如何交付给上层的问题,这个问题和UDP是一摸一样的,不说了。
》第二个,对我们的TCP来讲呢,它里面还有很多很多的字段,大家可以看看,如果我们今天想对我们的UDP来进行,叫做封装和解包话,我们未来呢就是封装的时候添加字段就行了。未来一定要解决的一个问题叫做,如何将报头和有效载荷分离的问题,你只有能够把它能分开,怎么组合,你就能够理解。能分开,你才能理解,数据发到对方之后,对方的一个处理流程是什么,所以,我们首先不谈里面的其他细节,先来谈一谈我们所对应的TCP你报头和有效载荷,如何分离的问题。
》如何分用,就是如何向上交付给进程呢?和我们前面UDP是一样的,根据目的端口号向上交付就可以,现在的问题是,怎么分离呢? 我们看哪个字段呢?有人说,这不是标准长度20字节嘛,直接读取一大堆的信息,直接将前20字节读完,光读前20个字节不够,还有一个报头可选择选项,所以TCP报头在百分之90情况是标准的,剩下呢,有一定可能性呢,它的报头是超过20字节的,换句话说呢,我们TCP报头是一个变长的,是变长的话,我该怎么去确认报头呢?因为你必须把报头确定清楚,将报头和有效载荷进行分离,怎么分呢?
》在20字节里面的,有一个字段是4位首部长度字段,注意是4位,这是其一;其二,4位呢是首部长度,什么叫做长度呢?长度呢,就是最少你是0吧,一个数字是长度,不可能是负数,它的取值范围就是0~15,也就是0000-1111,其中就代表了首部长度的最小和最大。但实际上呢,我们的TCP有标准长度是20字节,所以这个数字呢有自己的最小范围,可能是从某一个值到1111.可是有同学算了一下,发现也不对,4位首部长度呢,你是4位,从0000~1111,换句话说呢,你的首部长度最长也就是15字节,那你怎么表示标准的20字节呢?更别谈,你还想把选项带上。
》所以这里想要告诉大家,这里的4位首部长度,不是字面上的0~15字节概念,这个首部长度是有单位的,单位呢,是4字节!换句话说呢,如果这里的首部长度转化成15,也就最大的1111,实际上表示的长度是15 * 4 = 60字节。也就是说TCP最长字节是60字节。就是我们的4位首部长度呢,取值范围就是0~15,但并不代表长度就是0~15字节,太小了,它是由基本单位的,是4字节。
》所以,现在如果我们不考虑选项,或者换句话说,选项最多有40字节选项。现在我们不考虑选项,单纯的考虑,如果TCP是20字节,那么这里的4位首部长度是多少?现在呢,就是我们4位首部长度不考虑其他的,报文就是标准的20字节长度,那么4位首部长度应该是怎么样表示的呢?假设这里的4位首部长度的值呢为X,你标准的TCP报文长度是20字节,所以X*4 = 20,那么我们这个地方的值X是多少呢? 是不是就是5呀,那么4位首部长度的表示就是0101,所以我们TCP报文标准长度是20字节,那么4位首部长度里面,填的2进制序列就是0101。
》但是,我们TCP报文的标准长度是20字节,就直接决定了,这里的4位首部长度至少是0101,也就是说这个值呢,是不会从0开始,一般至少是从5开始的,所以取值范围是5~15,然后✖️我们的单位4,就是20~60。所以,我们现在呢,能正确的区分TCP报头的长度了。那么现在回答我一个问题,TCP协议是如何做到解包的呢?实际上做法很简单,大家可以看看,这个设计和我们以前学协议当中http协议有很大的相似性。
》什么意思呢,就是你收到了一大串TCP数据,那么我现在要读取它,我首先无脑的将20个字节读取出来,我不管你有没有这个选项字段有没有,我都先将前20个字节读取出来。在前20个字节呢,各字段的位置是标准的,标准的话,也就意味着前20个字节当中,我再去提取4位首部长度,是一定能够拿到该字段上面的数据。道理就好比,我们在学习HTTP协议的时候,我们要读取http协议呢,是按行读取,直到读到空行,如果读到空行呢,我就讲http协议的报头读完了 ,至于你的有效载荷是多少呢,我可以通过Content-Len来知道你有效载荷是多少字节。所以这里同样的道理,它读取前20字节是不错的,是固定的,至少是20字节,然后再提取4位bit位,将4个bit为读到之后呢,然后再✖️单位4,就知道了报文的长度。
》如果计算完是30,也就意味着,报文的完整长度是30字节,那么这30字节是包含了报头的标准20字节,所以30-20=10字节,所以,再连续读10字节,就将选项读出来了,剩下的都是数据,所以对我们来讲呢,我们收到TCP是可以根据它的报头字段来将它的报文来进行解包的。
》关于TCP的解包和分用的问题呢,我们就算是搞定了。也就是说呢,报文的属性当中呢,有目的端口号,所以当我们的操作系统收到了TCP报文,向上应用层交付我们对应的数据时,知道给谁交,根据目的端口号来决定。说白了就是,根据目的端口号来查哈希表,找到进程,再根据进程结合你对应的文件描述符,找到对应的文件,将数据拷贝到文件的缓冲区里面,最后你就跟读文件一样,将数据拿到了,这就是一套策略。现在呢,解包也好理解了,就是将报头去掉,剩下的就是数据了。如何去掉报头呢?我直接读取前20字节,读取到之后,再读取到4位首部长度字段,然后再✖️单位4,再减去20字节,如果为0,说明报头读完了,如果不为0,那么就直接将选项读取出来了。反正我们是可以根据特定的字段来操作的。相当于标准长度是包含了整个报文的长度的字段的。
》然后呢,这里TCP报头究竟是什么呢?和刚刚一样,如果封装呢?所谓的封装是什么呢?TCP报头的字段呢,实际上是在Linux内核当中实现的 ,它呢,我们可以称作TCPhandler类,它里面也是位段,它里面的每一个位段呢,都按照TCP报头的格式设计好。设计好之后呢,你添加报头,无非就是,定义一个TCP_handler对象,将对象的属性一填,填完之后,将对象拷贝到原始数据的前20字节上,至此就完成了封装,它的封装过程和UDP也是一样的。
》这里有一个细节,有同学会问,以前我们在定制应用层协议的时候,我们没有用传结构体,而你今天讲的是传结构体,而且你更夸张的是,不仅是结构体,传的还是位段。相当于你拷贝了2进制,拷贝之后发送给对方,这样可以吗?答案是:可以的。但是,一般呢,会有各种复杂的问题,编译器问题,包括大小端的问题,实际上Linux源码当中会添加各种预处理,条件编译选项,在源码当中直接识别你的大小端类型,然后在要求编译的时候,按照什么方式去编译,这都是定制好的。所以,在内核当中它确实传的是2进制结构体对象,但是呢,它底层做了大量的优化工作,大量的处理工作,不像我们应用层,因为内核这东西,尤其是和网络协议栈相关,写好了之后,一般是不会大改,你需求再怎么变,它一般稳定之后,就不敢轻易去动了,但是应用层不一样,你应用层各种各样的网络服务器,可能随随便便的,需求一直在变化,所以,我们使用最基本的字符串风格序列化和反序列化方案,也是为了方便我们快速扩展,这一点要注意,并不是说传结构体对象不行。
》我相信同学还发现了一个问题,这个TCP报头当中,它呢是有首部长度,他不像UDP,UDP可是有报文长度的,首部就是标准的8字节,但是TCP这里对不起,它没有报文长度,它只有首部长度,那么我们的问题是,我怎么知道你数据有多长呢?答案是:你不需要知道!你TCP不需要知道数据有多少字节,因为跟你没有关系,你只要将你的报头去掉了,然后把你的数据拷贝到接收缓冲区里面,按照顺序一个个,来10个拷10下,放在缓冲区里面,放一起,你的任务就完了。报文当中,你的数据有效载荷是多少,你不用关心,因为TCP是面向字节流的,这个关心的工作呢,是由应用层去关心的,所以它的报头里面没有设计报文总长度,你只需要把数据放在对应的缓冲区就完了,其他的,你不用管,所以TCP叫做面向字节流。随着我们学习的深入呢,大家会对其越来越理解,我们慢慢的基于不同的场景再来谈。
》下面,我们再来谈一组问题,如果说,上面的一组问题是说了TCP,乃至UDP的一些共性问题,帮助同学们解决了两个共性问题,如何解包与分用,说白了就是认识目的端口和4位首部长度。接下来我们要讨论的问题就比较大了,这个问题就叫做,TCP是如何保证可靠性,以及它上面的那些字段分别代表什么含义,所以要给大家加一点东西了。
》首先,我们先来谈一个可靠性问题,可靠性问题呢,我想给大家递进式的去给大家去讲,递进式讲呢,就是想把最核心的,在报头当中体现出来的可靠性,顺便给大家说了,因为不讲也没办法,要不然报头说不清楚。那么,TCP的可靠性呢有一部分是体现在报头的字段上的,也就是有一些字段首先是根可靠性有关的,所以我们把相关的先一谈,然后,后面我们在往后看的时候,有一些报头里面没有体现出来的,我们再后面学习课件当中的内容。
》1.什么是不可靠:丢包、乱序、数据报校验失败…当然呢,还有很多很多的问题,但是呢,我们先把这几个单独的列出来。一般呢,最典型的,最重要的不可靠问题就是丢包问题。
》我想问一下大家,你怎么知道,比如说,我一丢包为例,我要确定一个报文丢失了,问题是我将报文发出去了,我怎么确认一个报文丢了还是没丢呢?两台机器相隔可是千里万里的距离,一个数据报发出去之后,你怎么知道这个报文是丢了还是没丢呢?我们讨论的这个话题,我个人认为是TCP或者是理解TCP最重要的一个知识点,没有之一,虽然很小,但是我认为它是TCP当中最重要的内容。
》我们要对可靠性有一个正确的理解的话,这个问题必须想明白,就是一个报文丢了,你再怎么知道它是丢了还是没丢呢?一个报文发出去了,你怎么知道丢了还是没丢呢。 比如说,今天我们两个人站在一座桥的两边,我对你讲,吃了吗?当我把这句话说完的时候,我能不能确认,你收到了没有呢?答案是:我不能确认。因为我看不到你的脸,更看不到你的各种微表情,到底收到没。但是,当我说,你吃了没?其中,你给我回了一条消息,我吃了,吃的饺子。当我听到这句话的时候,给我透露的信息是两条,第一条就是字面意思,你吃的是饺子 ;但是背后有一个更重大的意义在于,我确认, 我刚刚给你发的消息,你收到了。
》所以,我们刚刚发出去的消息,我们如何得知对方是否收到呢?只要得到应答就意味着我刚刚发的消息,你100%收到了。
》换句话说呢,我给你发的消息,我怎么知道,我先不考虑丢还是没丢,我怎么知道你收到了,只要我收到了你的应答,我能确认的是,我虽然收到了你的消息,是你给我发的,但是我能确认的是,我刚刚给你发的消息,你100%收到了。反过来,站在你的角度,你给我说,吃了,我吃的是饺子。 那么站在你的角度,能不能确认,你刚给我说的话,我收到了吗?你能确认吗?答案是:不能确认。那么,我后来又对你喊了一声,吃饺子挺好的。所以,当你收到了这条消息之后,你也收到了两类信息,第一类,就是我给你发的字面信息;第二类,更重要的是,你收到我给你回的消息之后,也意味着,你刚刚给我发的话,我也收到了。所以,从你给我发话的方向上,我也收到了。换而言之,一个消息要真正的长距离传输,我作为发送方要确认消息被对方收到 ,必须得让对方给我进行应答,我才能根据应答,确认上一条发的消息,是被对方100%收到了,所以,我们能够理解。
》有人说,我给对方说,吃饺子挺好的。那我又怎么确认,这条消息被对方收到了吗?那么根据我前面说的,那就是要对方给我们进行应答。那么问题来了,一旦我收到应答,我确认对方收到了我的消息,那对方怎么确认我收到了没有呢?那么我们会发现一个问题,在我们长距离交互的时候,永远有一条最新的数据是没有应答的!那么换句话说,我们最新的数据没有应答,也就是说,最新的数据,是否对方收到,我是不确定的,只要不确定,它的可靠性就不是100%。所以,换句话说,世界上存不存在100%可靠的协议呢?对不起,在整体看来,是不存在100%可靠的协议的。因为,**在我们长距离交互的时候,永远有一条最新的数据是没有应答的。**只要最新的数据没有应答,那么发送方就无法确认这个数据是否被对方收到。所以没有100%可靠的协议。
》但是,我们也得到了100%局部可靠的协议,只要有发送的,有对应的应答,我们就认为我们发送的消息,对方是收到了。换句话说,虽然我无法确认你是否收到,但是我能确认之前的消息是被对方收到的。换而言之呢,我们在通信的时候,我们把我们最核心的数据放在较前面的位置,不太核心的数据放在后面,我们也能够保证我们的消息被对方100%收到了,这个就是可靠性的根本思想。
》我认为这个思路呢,是TCP最核心的思路。当然这个观点呢,在任何一本教材里面都是能够找到的,但是并不意味着其他教材就能够说清楚。我想说的是,我们经常说的可靠性,指的是被确认过的报文,我们能够保证他的可靠性。换而言之,没被确认的报文,我们是无法保证可靠性。所以回过头看最开始的问题,就是,你怎么确认一个报文丢没丢呢?答案是:如果我们收到了应答,我们确认是没丢,否则:就是不确定(不一定就是丢了,因为最新的数据不可能立刻得到回应)。没得到应答的,丢了还是没丢,我不知道,我也没办法知道,甚至我也不care,你丢还是没丢我根本不关心。那么未来呢,管你丢没丢,反正按照我的要求你没有到达,没给我应答,我大不了再重传。
》所以,我们要记住了,一旦发出去的报文没有得到应答,对方到底收没收到,我们完全是无法确定的,除非有专门的协议,一般在裸的TCP这里是无法知道。那么,我没有收到应答,就无法确认它的报文是否收到,所以是不确定的。但我能够确定的是,只要我收到应答了,那么它就是没丢的。所以,对我们来讲呢,这个策略就叫做,确认应答机制。确认应答机制其实是解决,不是解决确定是什么原因丢的,而是解决我能够保证我的数据被对方收到的问题,这叫做确认应答机制。
》我现在知道了,根据你讲的网络相关的一些细节呢,反正我发出去的数据呢,如果没有确认的话,那我就是不知道丢没丢。只有我收到了应答了,我才能确定对方收到没。
》现在的问题是,你发出去的报文,可能有很多,你怎么知道这个确认是对你发的哪一个报文的确认呢?比如说,我今天给大家发消息的时候,我不问你吃了吗?而是连续的问,吃了吗,吃的什么呀,味道怎么样?我连续的给你发了很多报文。那么你在给我响应的时候,此时呢,你可能每个都响应。那么,我问了你三个问题,你就得给我三个答案,吃了,吃的饺子,味道不错。在传输过程后,我收到的可能是,吃的饺子,吃了,味道不错。那么相当于,对我来讲呢,我必须得保证,我刚刚问的问题,或者我发出去的消息,确认应答时,要确认你给我的应答,是和我发出去的报文,哪一些是对应的,必须得对应起来,不对应就很难去确认,哪些消息被收到了,对不对。
》所以,怎么办呢?我们的TCP,它对应的报头当中呢,是包含了一个重要的字段,叫做“序号字段”和“确认序号字段”!这里有两个字段了。
》我们先不谈为什么有两个的问题,我们先把它们两个分别是什么说一下。在谈之前呢,和大家建立一个共识:当我说,基于TCP进行通信的时候,哪怕说,只发送了1个字节,还是2个字节,**绝对不要忘记,发送出去的报文一定会携带TCP报头的!比如说呢,客户端和服务器基于TCP通信,客户端发了一个,“你好”。当举这个例子的时候,当看到你好的时候,你在今天TCP场景里面,你永远要记住,当我们发送你好的时候,默认是会在报文头部位置携带报头!并且,报头里面的字段也默认填好了。这个共识先给大家提一提,因为,我们后面在讲3次握手的时候,会将模型简化,不管你发了什么数据,你发的都是一个TCP报头,一个完整的报头,这个很重要。很多教材就只给你写ACK+SN之类的,但是,我们今天要强调一下,只要双方在通信它不可能把bit位扔上来,而是要完整的TCP报头,报文+报头,有效载荷+报头都必须要有。所以,当我说,你吃了吗?这是有效载荷要传递的内容,但实际上这个信息里面也是包含了报头的。同样的双方交互的时候一定都是携带报头的。
》这样就有一个概念就是,32位序号和32位确认序号。接下来给大家举一个例子,其中,因为我们报头里面携带了序号,所以,今天我发一个消息,“你好”。这个消息,我给他的编号序号是从10开始,发送了一个报文,序号是10,对方再给我ACK的时候,他给我ACK的时候发了一个,“嗯”。我怎么知道,它给我回的这个,嗯。因为我之前给他说了很多话,那我怎么知道,它发的这个,“嗯”,是对应我们的,“你好”这个报文呢?所以此时,TCP里面呢,不一定非得带上“嗯”。有时候呢,我客户端给服务器发消息,服务器就是没有数据要给客户端,什么意思呢,就是服务端不想理客户端,客户端有数据给服务器,服务器没有数据给客户端。所以,当你发消息的时候,服务器可能给你发一个空报头,可以吗?可以,这个空报头呢,是纯纯正正的确认应答。如果我们有数据也要发,那么将数据也带上。所以,当发“嗯”的时候呢,我们的发的“你好”的序号是10,那么“嗯”的确认序号就是11。
》换而言之呢,在请求的时候,填的是“32序号”10,然后“嗯”响应的时候,填的是“32位确认序号”11。所以,当我收到这个报文的时候,可以根据 确认序号,提取11,只要我收到了11,意思就是说,作为我来讲,对方想告诉我的消息是,11之前的报文,我已经全部收到了,你下次再发的时候,从序号为11的报文开始向我发送。所以,对我们来讲呢,我们经过我们对应的序号机制呢,就可以确认我们的11号之前的报文全部被收到了。
》这和我们想象的有点不一样,有人说,我给你发了10个报文,你在 确认序号 这里填一个10,那么你发的是10号报文,我这里收到了10号报文,那我 确认序号 填10不就更简单吗?你发一个,我确认一个不就更简单吗?首先呢,这个方案一定是可以的,只不过TCP没有采用这个策略,因为确认也有可能丢,我们后面会讲,确认也有可能会丢。
》我们如果序号的含义规定成 ,比如说,我发了1、2、3…10,这10个报文,那么你在给我确认的时候,你确认了2、3、4、5、11,当我收到的时候有可能确认丢了,但我收到了11号序号,即便2、3、4、5全丢了,只要我收到了11,哪怕只收到11这么一个报文,我也知道前10个报文1、2、3、4…10你是收到了的。所以TCP就是采用的这种策略呢,来确认这个序号之前的报文,我是全部都收到了的。
》所以“32位序号”用来标定一个我们对应的报文序号,而我们“确认序号”用来确认的是特定,我所收到的报文,之前的特定报文,我全部收到。如果我服务端收到的是1、2、3、5、6、7的报文,那么请问我的服务端给响应的时候,“32位确认序号”应该填几呢?填的是 4!确认序号的概念是,一旦我们收到若干报文,需要确认,确认序号的含义是告诉发送方,特定序号之前的,我已经全部收到了。所以,在若干个报文当中,我收到了7号报文,但是我前面的报文呢,1、2、3、5、6我少了4,没有全部收到,所以,我只能回复4之前的,我们全部收到了,4我没有收到,所以对方再重新发的时候就只能从4开始发了,所以这就叫做确认序号的含义。
》我们先不着急,我知道大家在序号这里有问题。我们看到一个TCP报文是有确认序号。那么就挑出来的这个发送方的序号,那么响应方就会有确认序号,是不是就不能保证从左到右,客户端到服务端,数据的可靠性呢?什么意思呢,就是客户端给服务器发消息,最终呢,只要我们发了消息,服务器呢,都会尽可能的将它收到的报文,都会确认,确认序号呢,只要收到了,就可以根据确认序号,来确定哪些报文已经收到了,哪些没有收到,所以我能够保证从客户端到服务端的可靠性问题。
》我现在的问题是,为什么TCP报文有两组序号呢?我现在TCP里面,不是有序号、确认序号,为什么要搞成两个呢?意思就是说,我用一组序号不就行了吗?我们客户端发过去的消息,不要你这里的32位确认序号,不要了,你发的时候,就只要填你的序号,你服务器响应的时候,我们也不要什么确认序号了,我们只要将序号填为11 ,我们两个用一个序号,就可以实现从客户端到服务端,也能够进行响应了呀,那为什么又要有两组序号呢?为什么TCP里面既有序号,又有确认序号。这个问题,就要给大家说一说了,很简单。 原因就在于,因为TCP协议是全双工的,我在给你发消息的同时,我也可以收消息。
》比如说,我发了一个消息,发了一消息呢,序号填的10, 服务端现在呢,想给客户端发送一个应答,服务端想给客户端发送应答的同时,也想在自己的应答里面携带自己想要发送的消息。就好比,以前我们在沟通的时候,客户端给服务器说,“吃了吗”。服务端回一个,“嗯”。没有有效信息,就是一个确认应答。 但是如果服务端发,“我吃了,你吃了吗?”,所以,这个消息有两层含义,第一,我给你说这句话的时候,一方面是对你消息的确认,另一方面,我也给你发了新的消息。所以,我如果想要给你发消息,并且也同时给你做应答呢?**如果想要应答,就必须填充,确认序号。我给你应答,是不是要把你曾经发来的消息给你做应答,我是不是就得填充确认序号。第二个,如果我想给你发消息,TCP是保证可靠性,它不仅仅要保证从客户端到服务端到可靠性,它是不是还要保证从服务端到客户端的可靠性呀,换而言之,你不能只保证你给我发消息,客户端给服务器发消息,从左向右的可靠性,从右向左的可靠性,是不是也得保证呀。所以服务端想给你应答,也同时想给你发消息呢?那么我们的服务端是不是要也要客户端给我响应呀。所以服务端里面也携带自己的序号,换而言之,服务端发来的消息,既有对你的应答,又有想给你发的消息,那么这个服务端既有可能同时需要设置序号和确认序号,这两个必须得同时设置。如果想要同时设置,你用一个序号够吗?当然不够,所以需要两个独立的序号。客户端用序号和对方的确认序号构成从左向右的可靠性,那么服务器用自己的序号和客户端的确认序号,构建从右向左的可靠性,那么其中,我们就可以采用序号的机制保证双向的全双工的确认应答机制。
》实际上,TCP在通信的时候,客户端和服务器双方,我给你发消息的同时,我给你确认的时候,也有可能我给你发消息,这种策略呢,我们也会说,叫做捎带应答,也就是说,我给你确认,也在给你发消息。现实生活中,我们人在沟通也会出现这种情况,你问朋友说,“你的作业完成没”?你朋友说,“没做完”。这叫做确认了消息,还捎带了自己的消息。这种,在大部分客户端和服务器通信呢,序号填的是自己的,确认序号是对方的。对于客户端和服务端双方都是如此。在这点上,TCP协议在这点上,双方的地位是对等的。这就叫做序号和确认序号的问题,所以TCP的这两个字段我们就搞定了。
》接下来总结一下就是,序号和确认序号就是用来保证可靠性的,它核心的可靠性叫做确认应答,序号和确认序号呢就是为了支持确认应答而诞生的。序号是为了让对方确认的,确认序号是让对方给我确认的。所以,最终呢,我们要根据确认序号,来确定对方已经收到了我所发出去的多少报文,这是其一;其二呢,我们要记住,没有100%的可靠性协议,但是有局部100%可靠的协议,虽然,最新的一条消息永远没有应答,但是之前的消息,我们可以做有应答。只要有应答,我收到了应答,我就认为我刚发的消息,对方100%收到。如果你现在非得追求,我们最新的消息也得可靠,这种在我们人类目前的自然世界当中呢,你可以认为是不可能存在的。
》有人就会想,序号和确认序号,序号这个数字是怎么来的呢?比如我是一个网络黑客,你这个序号的产生,是有一定的规律,那我是不是就可以去攻击你的服务器的时候呢,发送一些正确序号,但是为错误的内容,是不是就可以干扰你TCP正常通信了呀。所以一般呢,这个序号的起始序号都是随便生成的,其二呢,这个起始序号随机生成之后,往后在递增的时候,是与报文是有关系的。比如说,你的报文有1000个字节,其实序号是123,所以我第一个报文序号是123,那么第二个序号就是1123,通过这个方式呢,就可以按字节来确认哪些报文收到了。
》有人说,双方的报文序号不断递增,发了特别大的数据,经过长时间距离传输,发的时间越长,那么序号就会递增,递增有可能会出现溢出呀,溢出怎么办呢?你也不用担心,序号这个东西,和我们讲的进程PID一样,它一旦出现了溢出,是会回绕的。
TCP是既有发送缓冲区,又有接收缓冲区的。这个缓冲区呢,在操作系统是由内核维护的。而我们呢,记住了,我们在应用层调用的接口呢,一个叫做write/send,还有read/recv接口,其中当我们进行,大家可以思考想一想,我们调用write的时候,是需要你传入一个你想写入的数据的缓冲区函数参数的,就是buf,还有对应的发送的count字节数。换句话说呢,就相当于是,我们要把buf和count相关的数据呢,进行拷贝到对应的,相当于buf这个缓冲区是你自己定的,要想办法把数据发送出去。与此同时,我们还有read()函数,recv()一样的哈,它们也有办法,它们也有传入的自己定义的缓冲区buf,从网络中将数据读取到缓冲区buf里面。
》反正呢就是,我们用户需要有自己的缓冲区,保存数据(write()),或者空缓冲区用来读取数据(read())。所以write、send这样的接口本质并不是把数据发送到网络里面,而是将数据从应用层,从我们的用户态,从应用层拷贝到我们对应的TCP发送缓冲区里面。我们一般在进行读取的时候呢,所谓的read、recv其实是从TCP的接收缓冲区里面,读取到用户定义好的接收缓冲区里面。所以,我们刚刚最开始的时候就说了,我们所学过的IO类函数,本质其实都是拷贝函数。所谓的拷贝函数,本质相当于什么呢?就相当于,我们这一批函数呢,其实是从用户拷贝到内核,从内核拷贝到用户。如果,结合我们曾经谈过的虚拟地址空间这样一个概念,0~3G是属于用户空间的,3~4G是属于内核的,这里的拷贝是什么呢,无非就是将内核的数据拷贝到用户区。这个用户缓冲区里面呢,自己可能定义的,就是在栈上定义的数组,也有可能是堆上malloc的一段空间。说白了,拷贝的过程呢,在我们的层状结构里面,但是在进程角度呢,其实就是在自己的地址空间内,把数据从内核空间拷贝到用户空间,这就叫做拷贝功能。
》所以,我们想告诉大家,在TCP这里,什么时候发,发多少,并不是由用户决定,而是由操作系统内,TCP协议自主决定,UDP也是类似。所以,**数据什么时候发,发多少,出错了怎么办,要不要添加提高效率的策略,**这些都不是由我们应用层程序员决定的,是由我们对应的OS内的TCP自主决定的! 凭什么呢?凭的就是,你用户只是将数据拷贝到操作系统内TCP的发送缓冲区里面,至于发送缓冲区,什么时候发,怎么发,发多少完全由操作系统内,TCP自主决定,所以,我们将TCP协议称之为,传输控制协议。你想想,我们自己程序员在套接字那里调用了write、send,直接就把数据一次从软件、操作系统,一直干到驱动,再干到硬件上,直接让硬件发出去,第一,对我这个程序员来说,效率太低,我要贯穿整个体系结构从上面一直走到硬件上,太慢了,但是,我有了缓冲区,我缓冲区好了之后立马返回,这就是缓冲区存在的意义,节省了发送函数编写的一个效率问题,这是其一,还不是最重要的,最重要的是,我把数据交给发送缓冲区,然后接下来呢,数据是什么时候发,怎么发,完全是由TCP自己决定。发多少,怎么发,完全是由TCP自己决定这句话,就决定了,TCP对于数据发送是有,传输控制的权利和能力,这才叫做TCP协议!
》你再仔细想一想,我们以前没有学网络,就是学的操作系统,我们当时讲文件系统的时候, 我们说过,我们把数据写到文件里面,当时在讲C语言接口的时候说,我们把数据拷贝到行缓冲区里面,我说过,那是由我们C语言提供的缓冲区,这个是语言的,然后呢,语言级的缓冲区里面数据,被拷贝到操作系统里面,经过write接口写到文件内部,写到文件内部,其实不一定就写到了磁盘上,而是这个数据在操作系统内部,是从用户空间拷贝到内核里面的,至于数据什么时候刷新到我们对应的磁盘的文件,进行落盘,完完全全是由操作系统自主决定。所以,你以为你调用了write,你就能真的将数据发出去了吗,实际上对不起,并没有。你能确定的就是,你调用write接口是将数据交给了操作系统,至于操作系统什么时候处理,和你没有关系。所以,我们操作系统呢,为了让用户,给用户带来更大的确定性,操作系统也有类似fync()这样的接口,将内核缓冲区的数据刷新到磁盘这样的接口。同样的道理,在网络这里也是类似的,而且在网络这里,它更需要!因为你如果要把数据write,真的要把数据发出去的时候,甚至收到确认应答,才算你发送完成的话,那么我们的write()接口就太慢了!
》当我们明白了TCP有接收和发送缓冲区之外,我们再来一个。如果现在有两台之间要通信,本质是什么呢?本质不就是两个TCP协议在互相通信嘛,所以,我们要理解它的话,我们需要做的是,要理解清楚的话,就相当于,比如说有客户端C,有服务端S,两个要通信,实际上我们发送消息的时候,发送的时候是,服务端发送到TCP发送缓冲区,TCP再把缓冲区刷新,经过网络推送到对端服务器的TCP接收缓冲区,所以我的发送缓冲区和对方的接收缓冲区是一对。然后呢,我们服务器S也想要给客户端C发送,每个TCP都有接收和发送缓冲区,所以,实际上我们在通信的时候本质呢,就是相当于用户把数据拷贝到TCP发送缓冲区,经过网络到达对方的接收缓冲区,对方再调用read接口在进行读取,如果缓冲区里面没有数据,那么read、recv就会被阻塞。
》所以呢,因为我们是有两对我们对应的,成对发送和接收缓冲区,所以TCP通信的时候,是全双工!也就是说,TCP协议,我们也经常也说他是全双工协议,那么凭什么呢?根本原因就在于,TCP在通信的时候,我发送的时候,将数据拷贝到发送缓冲区里面,我在拷贝的时候,并不妨碍对方在给我发消息,因为我在拷贝数据的时候,我把数据从用户到内核,但并不影响,我网卡把数据通过操作系统放到我的接收缓冲区里面,所以,我呢是可以并行的,所以,同一个文件描述符既可以读,又可以写,这样的行为!如果你愿意,你可以用多线程,对一个文件描述符,同时,一个读,一个写,两个线程是可以同时进行的,而且是互补干扰的!
》你怎会突然给我谈这块的内容呢?这块内容和我们刚刚所讲的报头又有什么关系呢?关系可大了!先不着急,下面给大家再来说一下,所以,同学们,我们以前写的网络版本计算器,包括我们之前写的简洁版的http,我们把我们构建好的报文,说白了就是字符串,无论是序列化了,还是构建http协议报头的那样的请求request,我们最后调用sendto()接口,或者write()把数据发给对方了,你以为你发了吗?其实我们以前在应用层,就是调这些接口把数据拷贝到发送缓冲区里面,仅此而已,剩下呢,我们的TCP会在合适的时候帮我们进行发送的,包括序号的起始,报文丢包了怎么办,我们全都没管过,这就叫做TCP。TCP就跟我们应用层的管家一样,你将数据交给他,它帮你全部进行发送,这就是传输层他的意义,以及代码结藕的好处。
》当我们谈到这点之后呢,我们拿出一个朝向,也就是我们假设从客户端到服务端发消息,下面我们将图简化一下,其中我们有一个clinet,有一个sevrver,考虑从左向右,clinet向server发送数据的过程。所以clinet我们挑出它的发送缓冲区,挑出server的接收缓冲区。我们client呢,可以调用send()接口,server调用recv接口,client发送消息,把数据发送到发送缓冲区里面,把自己的数据呢再交给对方的接收缓冲区,对方的接收缓冲区再通过让上层调用recv()将数据读到应用层就拿到数据了。现在的问题就是,我们的clinet和server相隔的距离可是千里万里,clinet在发消息的时候,既然是缓冲区,就一定有大小,即便是你们用的STL自动扩容,它都是有大小的,只要是缓冲区,它就有大小,更何况TCP、UDP缓冲区大小都是固定的,虽然可以调,但是固定的,调好了就不能变。所以,TCP发送和接收缓冲区是有上限的,有上限的话,clinet现在给server端发消息的时候,那么**如果发送的太快了,导致server来不及接收,怎么办?**从server到clinet也是同样的道理,我们以一个方向来进行研究。
》现在就是,客户端给服务器发送消息的时候,发送的特别快,导致server来不及接收,说白了就是把server的接收缓冲区打满了,打满了之后,我clinet一意孤行,继续给server发消息,那么最终再发过来的消息,server缓冲区已经装不下了,剩下的消息或者报文,不好意思了,只能被丢弃了。这个跟UDP也是一样的。可是呢,一旦丢包了,丢弃了,不就丢包了嘛,丢包了就会有同学说,不用担心,因为我们后面会知道TCP在应对丢包这件事情上,它是有自己的策略,大不了重传嘛!道理是这么个道理,如果这样子去做,也不是不可以,但是你得有理由,就好比,我报文错了吗?校验和出错了吗?还是我报文发错了?还是我犯了什么错?凭什么,我来的晚就应该被丢弃呢?更何况,我已经到了你的server,已经被你的server收到,就意味着,我在路上已经就消耗了大量的网络资源,无论是带宽,还是路由器,还是CPU内存资源,整个网络的资源,我都已经消耗了,到了server端,你告诉我是因为你来不及接收,来将我丢弃了,是你的问题,凭什么让我来承担结果。换句话说,它可以,但是直接丢包,以重传的方式重传,他可以,但是不合理。方案肯定都一样,一道题有四五种解法,但是我们总要走最优的解法。所以,对我们来讲呢,我们其实是不想看到这样的结果的,就相当于你server来不及接收,就跟我说嘛,我给你发慢点,或者不发,那不就完了吗!所以,我们不能忍受数据发送到对方,因为你作为接收方,缓冲区被写满,而导致报文丢弃,你要让我重传,我是不能接受的。
》所以,怎么办呢?当我们在进行收发报文的时候,每一个报文,都是会携带TCP的,所以,我现在不能忍,那怎么办呢?我们怎么来保证客户端发送的消息,发送的不要太快,让它更智能的进行,根据对方的接收能力来动态的调整自己的发送速度呢?如果让你设计,你会怎么设计呢?毫无疑问,这件事情的关键争点在于,**需要让clinet知道server接收能力!我们必须想办法让clinet知道我们的server他本身的接收能力。只要clinet知道了server的接收能力,那么clinet是不是就可以根据server的接收能力来进行动态的调整自己的策略呀!你告诉我说,你还能接收1000个字节,那我就给你发1000字节之后,我就发了。这样 是不是对大家都好。
》所以,现在的问题是,首先server的接收能力,由哪一个指标表示呢?**很简单,今天一顿饭,本来能吃三碗米饭,然后你已经吃了两碗了,然后问你,你的接收能力是多大。然后你自己想着,我还能吃一碗饭,为什么呢?因为你的接收能力,由你的胃还剩下的大小决定的,对不对。所以,server的指标由哪一个决定呢?—接收缓冲区剩余空间的大小!
》换句话说,我们现在要动态调整,那动态调整,不要让我们的clinet向server端发消息发的太快了,所以,我们不要让clinet给server发太快了的话,那么我们就得知道server的接收能力。那么接收能力又是由谁来决定呢?我们可以称之为,server的接收缓冲区的剩余空间大小,不是你缓冲区的大小,而是剩余空间的大小。 缓冲区大小是一个数字,那么剩余空间就一定也是一个数字,那么第二个问题就来了。我现在server可以自己通过计算,计算自己缓冲区剩余空间大小,这个很好算,它也能算出来,现在的问题是,**clinet怎么知道呢?**你server随便算,你算出来之后,我发送方怎么知道你的接收能力呢?答案是:**clinet能知道!为什么?因为所有的发送,都会有应答,而应答本质就要包含TCP报头,tcp报头可以有保存server接收能力的属性字段:“16位窗口大小”!
》换句话说,我作为clinet,我怎么知道server目前的接收能力呢?很简单,我给他发的消息,它是会给我应答的,它的应答报文里面是会携带,我们所对应的窗口大小的。而这里的窗口大小指的就是,我们叫做,server自己接收缓冲区的剩余空间大小。
》当我们现在呢,已经知道了,现在的问题是,如果server给clinet发报文,填充的窗口大小,填充的是自己的,还是对方的?答案是:只能填充自己的!为什么呢?因为报文是发给对方的,填充自己的大小,发送给对方,对方不就知道了吗。
》刚刚我们研究的是clinet向server端来进行发送,那么同样的server向clinet端发送的时候,是不是server也可能会将clinet端的接收缓冲区打满呀,所以我们clinet端也同样如此!也就是说,我们现在要server向clinet发消息,我们clinet给应答的时候,就要把自己的窗口大小告诉server,双方基于两个缓冲区,基于得知对方接收能力的前提之下,进行数据通信,我们就可以保证给对方发消息的时候,对方也不断的在给我应答,应答的时候就每一次都会给我更新它的窗口大小,那么所以,我就可以根据它的窗口大小来定期的向他发送合适的大小,我们的这种策略,我们称之为,流量控制!而流量控制是双向的!**也就是说,我们给server发,会有流量控制,server给clinet发也是会有流量控制。所以呢,我们通过这样的策略,来让client给server发消息的时候,用流量控制的方式,我们就可以保证,当前呢,我们两个双方在进行数据发送的时候,我们不会出现,你都接收不了了,我还使劲给你发消息的这样的情况!
》现在我抛出问题,如果我们是第一次发消息,我饿怎确定对方的接收能力呢?这个我们在讲三次握手的时候会揭晓答案。什么意思呢?意思就是,按照你刚刚给我说的,clinet给server发消息,只有当clinet收到了应答,它才能拿到clinet对应的窗口大小,如果极端情况下,client直接给server塞了一个大数据,这样的情况下,不就是有bug吗?它不就是相当于对方来不及接收,出现了数据报丢失的问题呢!那怎么办呢?你怎么保证你第一次给对方发数据的时候,对方就能接收?这个问题呢,我们后面说。
》我们目前已经把TCP的源/目的端口号、4位首部长度、序号、确认序号、窗口大小都已经讲完了,保留6位,就不用说了,用处不大,我们后面还要谈剩下的几个字段,其中16位校验和我们就不谈了,然后我们就可以学习TCP的其他策略了,现在已经将TCP的核心策略都谈了,我认为最重要的是,如何封装和解包,怎么理解它,然后还有一个就是确认应答机制的含义和意义是什么,另外就是流量控制和接收缓冲区和发送缓冲区这样概念的建立。
我们几乎惊奇的发现TCP在报头里面没有体现出数据的大小,我们也是说了,不是没有体现出来,而是TCP不需要,因为TCP是面向字节流的,你只需要保证把数据按序给我放到缓冲区里面,至于对于TCP的解释是由谁解释,你根本就不用操心,是你的TCP根本不用操心,因为这个工作是由你应用层要做的,这也就是为什么,我们之前在写套接字的时候,在UDP那里写聊天的时候,就直接send、sendto、recv就能够读到完整报文了,而我们为什么在写对应的TCP,尤其是当时写网络版本计算器的时候,我们一直在强调,我们read读取的时候呢,是需要不断的进行recv读取,你怎么保证你把对应的报头读到了,你怎么知道正文有多长,当时为什么要添加我们的报头,为什么要序列化和反序列化,序列化和反序列化发出去之后,我怎么知道有多长呢?我们当时也添加了有效载荷的长度,自定义协议为什么要这么做呢?因为是TCP不负责数据有多长的话题,数据有多长的话题是由你应用层决定的,TCP给你提供的就是基于流式的套接字服务,它只负责服务把数据呢,客户端给你发过来是什么东西,我就按照顺序给你放好,至于,这个TCP怎么给你解释,你自己决定,这就叫做流式套接。
》那么其中呢,我们关于流的概念呢,大家也应该能够感受到,第一,我们曾经在学习文件的时候,你也会发现,文件打开之后,读取多少个字节,大家面临的最难的,最恶心的问题是,如果我想读取指定的,我必须得有严格的我们对应的格式控制,比如说,你必须得按行读取,或者,我干脆用循环把文件内容全部读出来,这是第一。
》第二呢,比如说,我们以前学管道,它也是文件,其中呢,我们客户端给我们发来一条消息,我读一条消息,当客户端如果给我塞了10条、20条消息,我可能一次全部读出来了,可是我就想读一条消息,对不起,管道做不到,因为你写多少,管道只负责将数据放到缓冲区里面,至于你将来怎么读,是你的事情,这就意味着,我们之前无论是文件还是管道,它提供的也是流式服务,这也正是同学们在学习文件的时候,经常有老师告诉大家,我们把文件打开,也可以把它称之为打开文件流的原因。
》那么其中呢,我们想正常的,在我们对应的文件当中按照我的期望读取,比如说,我写了10份数据,我想按一份一份,一次一次的去读,那你自己其实在你的文件内部也要定协议,很简单,说白了,其实就是你也要做序列化和反序列化,你也要给他整一个字符串,设定你的报文长度,至少和我们网络版本计算器一样,只不过,你以前往网络里面写,现在往文件里面写,读取的时候也要按照读取网络的方式来读取文件。所以这就是序列化和反序列化,以及自定义协议呢,在流式当中,在流式的协议当中,经常采用,也是因为它重要的根本原因。这也同样的看出来,我们的网络,就是在网络的上下文当中,我们还是把我们网络当作文件来看待的原因,彼此网络与系统是完全适配的,你写出来的网络代码,把网络底层代码换成文件代码,它也照样能够工作,这就是我们学习systemV进程间通信的时候,建立共享内存,什么乱七八糟的去讲。包括我们消息队列和信号量,当时我们说那部分内容不再作为重点去讲,原因就在于那部分内容跟文件的兼容性根本就不是很好,原因都在这里。
》如上就是关于为什么TCP不需要告诉你数据大小的概念和流式的再次讲解。将TCP讲完,我们还有一个话题就是,如何理解面向字节流的,以及如何理解数据报粘包问题,都是跟现在要说的字节流是有关的。
下面我们就来看一下,TCP当中常见的6个标记位,我们一个一个来。我们曾经在给大家讲套接字的时候,曾经铺垫过一个概念, 叫做TCP三次握手,我们曾经讲呢,并没有花太多时间谈它,而只是告诉大家,如果我们两个主机想通过我们对应的PCB来通信,那么要正常通信必须得先进行3次握手,3次握手一帮由客户端主动发起第一次握手,客户端发起完毕之后呢,第二次服务端给它进行响应,只有第三次成功之后,我们才完成我们的三次握手。
》**只有完成了3次握手,才算建立链接成功,只有链接建立成功,才能正式通信。**换而言之,双方在进行通信时,就如上节课给大家讲窗口的时候,给大家说过,我clinet给server发消息,server进行响应,我说的这个是已经属于3次握手建立成功的之后,clinet给server再发消息,这才算是一个正常通信过程。
》所以呢,我们得先进行3次握手,最后当我们把我们所对应的数据发送完毕了,那么我们曾经也提过一个概念,叫做发送完毕呢,双方要断开链接,又要经历叫做4次挥手,这个地方当时也没有作为细节给大家去讲,但是这件事情应该是知道的。
》所以,每一次我们进行对应的叫做数据通信的时候呢,必须得先建立,保证链接的建立成功,然后才是正常通信,然后,当我们在进行我们的通信完毕,再4次挥手,这才是TCP通信的完整的基本整体结构。
》关于3次握手和4次挥手的话题呢,我在给大家把报头介绍完毕,估计也到不了那块,一会在讲RST标记位的时候,就顺便给大家讲完了。现在我们依旧把它的过程来说一下。这个过程大家当然都知道,但是呢,我今天不想再说他了,我想说的是,大家站在server的角度,它收到的TCP报文,有的是用来建立链接的,有的是正常的数据报文,有的是用来断开链接的。所以,我们要清楚一点的就叫做,**报文也是有类别的!**这个非常重要,就好比作为我们的server端,我作为一个服务器,我在和客户端A正常通信的时候,也同样有客户端B、C给我再发消息,或者在向我发起建立链接的请求,或者在跟我断开链接,我作为一个服务器,我一瞬间,可能一段时间内,收到了大量的报文,不同的报文呢,它有不同的诉求,有的是想跟我建立链接的,有的是想给我发消息的,有的是想给我断开链接的,那么作为server来讲,那么它要不要来区分TCP,我收到的TCP报文都属于哪些类别呢?答案是:必须得有!
》好,我说的这个呢,就是想告诉大家,不要对于server来讲,认为server会把所有的报文呢,会以同样的方式去处理。我收到一个报文是建立链接的,那我server就应该进入到建立链接的逻辑,如果是给我发消息的,那我的server就应该进入到接收数据,然后解包分用的逻辑。如果是给我断开链接的报文,那我就应该释放链接,然后进行将我们没有发送完的数据,进行快速推送的逻辑。所以,不同的报文是有不同的类别和不同的处理方式,这是一个常识,往往被很多的教材所忽视,教材只会告诉你这个标志位是什么,但是我刚刚说的这个,报文是有类别的,站在我们的server和clinet是一样的,对于双方来讲呢,报文是有类别的,也是在回答,为什么会有若干个标记位的原因,下面,我们再来谈它是什么。
》所以为了正确区分,server收到一个报文,你这个报文是想给我发消息,还是想给我进行建立链接等,所以我们就有了第一个标记位SYN标记位!SYN:只要报文是建立链接的请求,那么SYN同步标志位需要被设置成1!
》换而言之呢,还是我前面说的,我客户端给server端发消息,建立链接的请求,当我说建立链接的请求时,你大脑里面立马要想到,你给我说建立链接的请求,说人话就是,你的客户端要给服务器发一个TCP报头 ,可以不携带有效载荷,但是一定要有搞头,要不然怎么证明你是TCP呢?所以,clinet发过来的报头当中,只要将SYN标记位置为1,就证明server一收到,它会直接先报头一读取,根据4位报头首部长度,进行分离,做完之后呢,它还不着急做,而是先检测这6个标记位,如果发现SYN标记位被设置成1,就证明这一次是一个链接请求的报文,所以server要思考,到底要不要给你建立链接,那么就到了我们3次握手的阶段了,这点我们要注意,稍后我们在讲3次握手的时候再谈它更多的细节。所以,它叫做SYN同步标记位,它就是用来标识一个报文是一个链接请求报文。
》第二个呢,第二个就更好理解了,我能知道你的报文是跟我建立链接的,那么久还有一个就是FIN。**FIN:该报文是一个断开链接的请求报文。**如果我们最终呢,SYN和FIN呢,尤其是FIN这个标记位被设置, server收到了,那么server就知道客户端想跟我断开链接,这就是FIN的意义,从SYN和FIN的意义上看也知道,这两个标志位不会被同事设置的,要么你两都别设置,要么就是舍SYN,要么就是设置FIN,不可能既是SYN,又是FIN,说白了就是两个bit位不会被同时设置。
》那么第三个,还有就是,我们server端除了你的建立链接还有断开链接,还有没有其他的,就比如说,我们正常通信的时候,正常通信的时候,我怎么证明,比如说,你给我说的,会有确认应答。确认应答的时候,客户端给server发消息,server给客户端确认,那么server如何识别到clinet给它发来的是一个链接请求报文呢,还是就是一个正常的确认呢?所以,我们还有一个标记位,就是ACK标记位。
》**ACK:标记位是一个确认标记位,表示该报文是对历史报文的确认(不仅仅)。什么意思呢?比如说,我clinet给server正式发了一个数据,序号编号为10,那么server给clinet回消息的时候,就会把自己ACK标记位置1,当clinet收到了ACK标记位为1的报文呢, 它就立马想到了,ACK置1,证明这个报文是对我历史报文的确认,所以clinet就直接就去,在报头当中去找确认序号,然后就知道了server目前收到了那些报文,它是这个意思。大家发现,我()里面写了一个“不仅仅”,什么叫做不仅仅呢?意思就是说,当你设置了ACK,这个报文100%是对历史报文的确认,但不仅仅只是对历史报文的确认,它也可能携带了另一方向给我发的消息。也就是说任何一个确认,那么,这个确认的标记位被置为1了,并不排斥,给这个确认报文携带上他想给我发的消息。本来是它先给我确认,确认之后,才再给我发消息。但是TCP呢,它优化策略就是,我给你确认的同时,我也可以给你携带上数据,毕竟发报文,怎么发不是发呢,我发两次就有两个报头,我发一个报头不就挺好的嘛,所以,它可以捎带式的给你也在发消息,这个呢,是TCP的最常见的工作方式。
》我们现在很显然呢,根据ACK的理解呢,大家现在也基本能确认一件事情,叫做,一般在大部分正式通信的情况下,ACK都是1!因为报文里面既携带了数据,又有确认。即便是不携带数据, 它的ACK也必须得置为1,那么TCP的响应就不存在意义。只要你响应,只要是正常通信的阶段,ACK都是1。
》所以呢,我们就根据TCP报文的类别呢,就回答了前三个点标志位。接下来,我们要来谈一谈,剩下的三个,剩下的三个呢是不太好理解的,但是不用担心,我会一个一个给大家都会揭晓答案。
》下面,我们再谈下一个标记位,就是PSH! PSH:它叫做数据推送标记位,提示接收端应用程序立刻从TCP缓冲区把数据读走。 PSH标记位是什么意思呢,下面给大家举一个例子。我们依旧是clinet给server发消息了,client有TCP的发送缓冲区,那么server呢就有TCP的接收缓冲区。我们以前在讲窗口大小的时候,给大家说过,当我们正式在clinet和server在进行通信的时候,我们的servr端或者clinet端都会互相的给对方确认。确认的本质就是TCP完整报头,报头里面携带了窗口大小,就可以让双方在交互的时候,得知对方的接收能力。比如说,客户端给server端发送我们对应的消息,发送的时候呢,如果此时客户端给server端发了消息,发的这个消息呢,有几种极端情况,我们也写过服务器,你服务器端肯定都是调用的read()。大家能理解的一个东西叫做,server端就可以根据我们所对应的调用read()来读取我们的数据,到我们的应用层,这个是能理解的,那如果接收缓冲区里面,没有数据呢?没有数据的话,当前我们read()会怎么办呢?各位同学都想到的是,反正我知道,比如说,我们建立好链接了,我客户端不发消息,那么server最终就是读取read或者recv()的时候,它是会阻塞在那里的。阻塞在那里是什么意思呢?意味着,接收缓冲区里面没有数据,没有数据的话,当前我们在读的时候,读不到数据就只能阻塞在那里,直到有数据来了。后来客户端发来了一个,“你好”。那么最终呢,我们就发现,我们read()就返回,把数据就拿到了。
》那么对我们来讲呢,这个过程其实也并不陌生,但有一个基本事实 ,什么事实呢?就是,我们的读取条件如果不满足,我们的readI()就会被阻塞!就是在应用层,如果读取条件不满足,我们read会被阻塞。这个呢,如果写过套接字,那么肯定知道,就是当我们在读取的时候呢,应用层在读,如果底层条件不满足就会被阻塞。那么什么叫做条件满足呢?我们在今天来看的话,这里的所谓的,条件满足和不满足是什么意思呢?同学们会说,对于条件满足和不满足,不就意味着,以读取为例,就意味着,接收缓冲区里面现在是有还是没有数据,如果是有,那么是满足的,那么read()就会读取返回,如果没有数据,那么此时呢,当前条件就不满足,也就会阻塞。没问题,道理是这么个道理,但是呢,尤其是在我们高并发,高IO下呢,即便是缓冲区里面有数据了,我们对应的read()呢,它的消息,那么可能你也并不一定知道它是不是满足条件的。因为今天我们学到的接口呢,是你自己调了read,阻塞式的调了read,然后你才把数据拿出来,但是未来呢,我们在学习的时候呢,是想变成,当你准备好了,你再来让我调用。
》我再说一遍,目前我们学到的所有接口全部都是主动的由进程自己去轮训检测。无论是我们自己,现在非阻塞IO还没说,但是我们以前还讲过进程等待嘛。我们自己去检测条件满足还是不满足,我们是自己调用read()接口的,是你自己做的。read()接口,它看起来好像叫做read,叫做拷贝,把数据从内核缓冲区拷贝到用户缓冲区,但是它还有一个潜在的功能,那就是识别缓冲区是否有数据的条件是否满足的。那么在未来呢,我们现在,当底层有没有数据,我不知道,我必须得通过读才能做,为什么呢?因为,你只有通过读,才能检测到你的缓冲区有没有数据,这个是你自己主动检测的。在未来呢,我们还要学习一种IO方式,是 当你底层的数据就绪准备好了,你来通知我,我再也不主动调用read()了,是你有数据,你来告诉我,那么这种类似于回调的策略,才是更高效的IO策略。
》我先说这种理解方式,如果理解不了,我就再换一个角度,让大家去理解。
》换句话说呢,那么我们接收缓冲区里面有数据了,那么我们呢,就可以让上层通过一定的方式得知我有数据了,所以呢,我们操作系统它是可以给我们提供一整套的,叫做就绪事件通知的策略。也就是说呢,如果有数据,我们操作系统可以通知你。
》那么所以,PSH的意思就是说:我给你发的消息,以前呢,我的缓冲区里面,比如说,以前缓冲区里面只要有数据,那么我们呢,或者呢,可以给大家这么说,我们可以理解成,接收缓冲区,它呢可以理解成了,有自己对应的接收数据的最小范围。比如说,我的缓冲区里面有100个字节,我只要接收数据超过了20以上的字节,我才会向你上层来通知,这个呢,我们称之为,接收数据的低水位线或者高水位线,我们后面都会说。现在的问题是,如果底层它收到了一条消息或者某条数据,收到了一条数据之后呢,那么其中呢,我们呢,如果发送的TCP报文,当中携带了PHS,那么它就会影响你本地的server的操作系统,让操作系统尽快通知上层,数据已经有了,赶紧有时间就来读取吧!这就叫做PSH状态标记位的含义。
》换句话说呢,缓冲区有数据,你怎么知道?目前是因为你阻塞式的去调用了这个read(),那么如果有一天你无法直接阻塞式的调用read(),那么你就需要有人尽快的,或者使用操作系统默认的策略呢,来当数据有了,或者超过水位线就来通知你。如果我现在不想让操作系统默认,而是想立即让上层赶进读,那么我们就可以携带PSH的标记位,在我们的TCP报头里面添加设置这么一个对应的PSH标记位,此时这个数据呢,就会被我们对应的server端收到之后,然后放到缓冲区里面,更多的做一个工作,通知上层来读取。
》如果这种讲法你不太理解,我也能够理解你的,那我就从另一个角度,让大家也可以接受的一个概念。我们换一种说法,上面的说法是最正确的,现在理解不了是正常的,我换一种说法。
》比如说,我们所谓的客户端,它给服务器发消息,服务器给我ACK,或者给我响应它的窗口大小。如果,窗口大小为0,那么其中意味着server端,把缓冲区里面的数据给写满了。如果我们server给的ACK当中,窗口大小为0,那我们对应的客户端给server发过去的消息之后,服务端响应窗口大小为0,此时,我clinet认识到server现在到接收缓冲区写满了,来不及接收了,这是第一个。第二个,假设我们的应用层特别忙,那么它就没有时间去直接检测当前的缓冲区里面有没有数据,它可能忙着做其他事情,那么其中呢,我们的客户端发过去的消息,上层来不及取走,那么请问我客户端该怎么做呢?客户端说,“嗯,它给我发来窗口大小为0了,这可怎么办呢?”这个时候,我们最理想的情况呢,就是,你还能怎么办呢,你只能等了,所以客户端你就只能等,等什么呢?你是不是必须得等server端上层的应用层尽快的把接收缓冲区的数据快点取走。然后你等了一会儿之后呢,你又给对方发一个不携带数据的,只发一个报头,类似于侦测报文,对方给你响应窗口大小还是为0。那么作为客户端来讲,你只能继续等,为什么呢?因为当前server呢,不把数据取走,它的缓冲区里面已经写满了,你客户端数据再怎么发,也是发不出去的,那这是不是有点尴尬。所以呢,如果我们客户端发了一大堆消息,将server的缓冲区打满,它等了好长时间,发现server上层就是不读取,clinet就着急了,就立马给对方推送了一个报文,报头的PSH标记位被置1,给他推送过去了。推送之后呢,那么这个PSH标记位被TCP协议层收到之后,TCP协议层意识到,对方催促我尽快将我的缓冲区的数据交付给上层,那我得尽快的让我上层来读取了,所以,我们操作系统直接给上层呢,告知上层,客户端已经给我们发来了催促报文了,让我们尽快将数据取走,你赶紧抽时间过来取数据吧!这就是PSH标记位的作用!
》换而言之,就相当于呢,如果我的客户端给server端发消息,缓冲区写满了,然后呢,相当于客户端想给对方发消息,可是对方一直不取数据,最后呢,我一直想发,对方一直不读取数据,我总不能一直等啊吧,所以,我可以催一催他,那么我就发送报头里面的PSH标记位置1的报文,然后,让他的操作系统尽快通知他上层,底层的数据有了,赶紧来读,所以,应用层他必须在合适的时候来读取。
》那如果应用层不听呢?我应用层就是不听你的,你操作系统说有数据了,我就是不读。有人喜欢抬杠,说server不听话呢? server端就是这里的应用层不听话,那不就是应用层有bug嘛,操作系统告诉你数据有了,那你应用层还等什么呢?还慢慢悠悠的去忙你自己的事情,还不赶紧去读数据,就说明你写的代码有问题。所以,一般数据都会被应用层去读取。当然,对于PSH的理解呢,我们前面的第一种讲法是最准确的;第二种讲法是给大家举了一个特别极端的情况,就是不断的去催促他,让应用层去读取数据。这就完了吗?还没有。
》所以,对于接收方来讲,我们客户端发消息的过程就是往缓冲区里面放数据的过程,那么我们接收方的应用层会从缓冲区里面把数据拿走。一个放数据,一个拿数据,这个过程的本质,其实就是生产者和消费者模型呀。而生产者和消费者模型我们讲了,如果生产满了,我们是不是尽快让消费者来进行我们对应的读取,让消费者尽快进行读取,那是不是得尽快通知我们的消费者尽快取走数据,这个过程,我们称之为同步的过程。所以,PSH从网络的角度,实际上是尽快的告知应用层,底层的缓冲区数据已经就绪了,但是在我们的系统层面上,这不就相当于,我们曾经讲过的条件变量吗?来通知上层,数据已经就绪了。通知你,就好比,我给你的条件变量发,条件已经就绪了,如果你还是不读,那就是你的问题了,反正我告诉你了。所以,这就叫做PSH标记位,当然,它和条件变量没有半毛钱关系,只是想通过网络和我们系统的概念进行类比,再去理解这个概念就很好理解了。
》我们在课堂上的实验是没法做的,但是也不用担心,我们后面在讲最后一个多路转接的话题的时候,我们会重新谈IO,重新谈时间就绪,我们会重新去理解read()、write()这样的接口,到时候再谈的时候,我们就慢慢的理解了。
》下一个再来一个URG标记位。URG代表的就是紧急指针,什么意思呢?还是一样,在谈这个话题的时候,也得绕一个圈子。
》请问为什么TCP在正常通信的时候要有“序号”字段呢?这个问题,你不是讲过吗?我们携带了序号,就意味着,我们是不是可以根据序号来定向的进行确认应答呀,那么我就可以得知,哪些序号之前的报文,已经被我们的接收方全部接收到了。所以,序号呢,是我们确认应答的基石,有问题吗?没有问题。那是我们当时在讲确认应答,所以我们就只谈序号在确认应答方面的功能,但是序号并不仅仅只有这么一个功能。
》什么意思呢?意思是,如果我今天给你发的报文的序号已经编好了,是1~10一共10个报文,然后我把这10个报文给你server端发过去了,发过去之后,你这个接收方一定接收到的数据,就严格的按照1~10号报文全部接收到呢?假设是在不丢包的情况下,我客户端给你1、2、3、4…10这10个报文到server端,你server端要给我ACK,没问题,我现在的问题是,server你先别调用ACK,你server端收这10个报文的时候,是不是严格按照10个报文,按顺序收呢?我发可是按顺序发的。答案是:不一定。因为这个问题呢,我们很早之前就提过,当我们的10个报文发出去的时候,我们的每一个报文在网络当中,经过的路由器节点,经过的路径选择,不同设备的效率,路径的长短,都可能影响每一个报文,在网络当中传送的时常问题。所以,你有10个报文,对不起,可能我server端先收到的就是,我们对应的3或者7,虽然你1号最早发,但是可能是最晚到。
》那么我呢,想通过这样的例子告诉大家,就是报文在发送的时候,是可能乱序到达的。**乱序到达呢,其实是有很大的问题,如果乱序到达了,最终呢,你在向上层交付的时候,你能想象一下,你发的一个http报文,你的空行,是在你的请求之前行之前吗?你能想到,你收到的一个http应答,它的正文是在你的http状态行的前面吗?这些都是不能忍的!你给我发是怎么样子的,我收就必须是什么样子的,所以,我们要能够让他按序到达。所以反过来就意味着,乱序到达,是不可靠的一种!所以,我们为了能够让他不要乱序,所以, 我们需要让我们的报文按序到达,这里就有一个问题就是,如何做到?
》换而言之呢,TCP要保证可靠性嘛,所以数据报乱序了,那你怎么保证你的数据是按序的呢? 所以怎么做呢?很简单,规则就在“序号”字段这里。首先呢,序号是递增的,所以我们其实是完全可以根据序号来进行,收到了10个报文,我们按序号排序就可以了。我们按序号进行排序,那么乱序的报文最终相当于,在我们TCP缓冲区内部对数据做了调整。调整完就能做到,按序到达,所以序号的策略呢,能保证按序到达。
》所以,TCP协议在进行收取报文的时候,它不是把数据收到就往上交的,它也要自己做很多的处理,比如说,它要根据序号来保证我们的报文,按顺序,一旦给你从小到大,比如从小到大排好序,排好序之后,你哪些丢了,哪些没丢,最大收到的报文是多少,它一目了然,所以这个时候再去设置ACK,设置它的应答,这个时候就不会出问题了,所以这就是序号的作用。
》我们呢,你怎么又突然给我们去讲了一个叫做,按序到达的知识点呢?这里就要说一下了,如果数据是必须在TCP中进行按序到达的话,这是他的优点,也是保证可靠性的一种策略,那么潜台词,也就是如果有一些数据优先级更高,但是序号比较晚,这样的报文应该让它按序到达呢,还是应该让它插队呢?如果严格让它按照,按序到达的话,对应的数据呢,此时也就没有办法让它插队了,没办法让它插队,那它就是一个常规报文,和我们对应的,让你这个报文优先背上层读取,那么就不可能存在这样的情况了。
》可是我们就是有一些优先级更高的数据呢,我们想让它尽快被读取,但是它序号比较晚。所以,如果我们想让这样的报文优先处理,那就出问题了?到底听谁的?听你的,听TCP的还是, 还是听你优先级更高的呢?怎么办呢?所以面对这样的问题的时候, 如果TCP必须按序到达的话,也就是说,如果有些优先级更高,但序号更晚的话,严格按照TCP这样的方式来按序到达的话,我们就无法做到数据被优先紧急处理!
》但是,这前提是你得仅仅就很死板,必须得按序到达。所以,TCP呢有一些场景,有可能有些数据呢,是要让我们的server端优先去读取的,那么让server端优先去读取,此时我们就可以使用,给TCP报头当中的URG置1,那么就代表该数据可以直接忽略它的信号,被我们的上层直接进行读取处理,那么这样的报文就叫做,我们对应的紧急指针报文。所以,一旦我们的TCP里面是常规的,那就按照序号的机制来排队,按序到达就行了,如果携带了URG,那么就意味着这个报文可以被优先读取!如果上层不想优先读取,那么就正常的一个个读取,如果此时你想优先读取,你可以优先读取它。那么在我们的read()当中有些标记位是可以直接读取URG的。
》现在我的问题就是,同学们,可是呢,我们的URG标记位被设置了,那么将来呢,我们收到这个TCP报文呢,它最终是要直接被放到我们对应的缓冲区里面的,那么最终呢,到达缓冲区内部,那么请问,哪些数据是紧急数据呢?我上层在读取数据的时候,我读取的数据,不是你标记位,而是数据字段,即有效载荷。所以,我上层怎么知道数据在哪里呢?所以我们就又有了“16位紧急指针”的字段!
》“16位紧急指针”呢,他这个指针,它代表的就是我们呢,要读取到的紧急数据呢,最终在接收缓冲区的偏移量的位置。就相当于,16位紧急指针就标定了我们数据最终在我们整个报文当中的偏移量处。比如说,为5的话,就是偏移5个字节,即第5个字节,就代表的是我们紧急数据。有同学又说了,按照你的说法,那我的数据如果有100个字节,那么紧急指针呢就代表在100个字节当中的偏移量,那么这个指针指向的偏移量是哪个直接,就代表是,这个位置的数据呢,是我们紧急数据。你这句话呢,说了两个问题。
》第一个,我们的这个整个数据呢,携带的数据不是所有的数据都是紧急数据是不是,因为你有紧急指针呀!只有,你紧急指针所指向的数据是紧急数据。
》第二个,那你的意思就是说,从我们的紧急指针指向的位置,那么我应该读取多少字节呢?答案是:只能有1个字节!换而言之呢,紧急指针,你所发送的紧急指针的数据,只能发送1个字节。也就是说呢,紧急指针的位置,最终在我们的“数据”字段中呢,偏移量后指向的位置,1个字节就是我们将来要读取的紧急数据。也就是说呢,只要你URG标记位被置1,代表你的有效载荷里面携带了紧急数据,那么紧急数据在哪里呢?以16位紧急指针,通过偏移量找到这个数据,而且该数据只有1个字节!这就叫做,紧急指针和紧急报文标记位所包含的概念。
》我知道,光这样去讲,还是不够的,不要着急。听你这么说,意思就是说,如果客户端给server端塞了大量数据,server端处理数据可能非常慢,然后呢,客户端也如果想有一些更高优先级的工作呢,让server端来处理,所以呢,客户端就可以让server端发送紧急指针,也可能携带其他有效载荷,然后呢,其中这个数据里面的特定的某些数据呢,通过紧急指针,携带在我们的数据内容当中,用指针来指向它的位置。所以呢,我们就可以在server端以优先级较高的,不要让这一个字节的数据呢,严格按照序号去做,而是优先被读取,那么这样做,可以吗?答案是:是的,可以的,没问题。
》然后呢,什么情况下会存在这样的应用场景呢?你说的挺好的,你把该说的,也说清楚了,但是有一个问题就是,什么情况下会这么用呢?我来给大家说一些场景,当然,99.9%的情况下,我们的紧急指针都是用不到的,这是第一个;第二个,我们的紧急指针呢,一般呢,它传过去的大部分情况下,不再是我们对应的数据内容本身了,一般用紧急指针传的呢,都是一些具有额外含义的数据, 举一个例子:比如说呢,今天客户端给server端发消息,发出去的各种消息呢,server端也有响应,但是客户端总是把数据发不过去,甚至呢,我们的server端出现卡死、假死这样的情况。就相当于,比如说,我们经常用xshell来连接我们的服务器,连的时候呢,我们服务器没有反应,没反应的话,有很多情况,要么服务器挂了,要么就是我们对应的server端呢,可能出现了一些,因为你发过去的所有东西都是数据嘛,那么server端现在可能压力很大,它读取你指令各方面呢,在你的缓冲区里面积压了很多东西,积压了很多东西,导致你现在输入很多东西的时候,它没有反应。然后,这个时候,我们想知道,你server怎么了,如果我想知道server怎么了,我们客户端就可以给server端发一个,比如说,紧急数据。有人说,正常发一个报文不就行了吗?但是,正常的报文,还得在接收缓冲区里面排队,前面的数据被处理完了,才能处理到你这个紧急报文,到那个时候黄花菜都凉了,我现在为什么问你这个server呢,就是因为我前面给你发数据,你为什么不给我任何反应呢?我现在还给你发消息,它还是在缓冲区里面排队,那我是不是也得等好长时间,这种做法呢,让它严格按序到达,是不太好的。所以呢,客户端可以给他一个数据量,你也就能理解紧急指针为什么不能太大。紧急指针指向的有效数据就只有1个字节,而且紧急指针呢,可以被我们serve端优先读取的,所以呢,当clinet端一时发了消息没有反应,可能是主机挂了还是怎么回事,我不管,我就提前在clinet端就给server发送一个紧急指针,然后呢,紧急指针所标定的数据呢,只有一个字节,我设为1,我server端呢,曾经有一部分预先写好的逻辑,就是如果对应的服务出现挂了的话,此时这个服务呢,尽管不能读取我们接收缓冲区的里面数据,或者读取的比较慢,如果有紧急指针的数据,我们也要让他去读,它里面有读取紧急指针的逻辑,比如起一个线程,然后呢,客户端发了一个紧急指针只有1个字节,这个字节呢,server端立马就能读到,为什么呢?因为它是紧急数据,它可以插队,虽然我接收缓冲区里面读的比较慢,但是只要你给我发了紧急指针,那么这个数据就可以优先背读取。比如说clinet给server端发了一个1,server读到了,它立马就意识到,clinet在询问我的状态,然后我呢,就给客户端也以紧急指针的方式返回给客户端,server端给clinet返回一个20,我们也约定好,如果我的服务端呢,因为内存资源不足的情况下,你来问我呢,我就告诉你我给你返回的状态码是20。所以当clinet以紧急指针的方式询问server端,然后server端也以紧急指针的方式返回给client,那么clinet就收到了20的信号,所以clinet呢也就查到了服务端挂掉的原因,server端返回的是20,那clinet端就知道了,是server端端内存资源不足了,那么此时呢,clinet端呢通过某种方式告诉工作人员去看看机器怎么了。
》所以,一般紧急指针呢,我们会发现,即便是我们两个通信很困难了,缓冲区已经接近被打满,我们很难进行处理数据了,但我们两个依旧可以以非常小的字节数来进行,在超脱缓冲区之外来进行很少字节的通信,它的目的主要是用来,未来获取主机或服务的状态的。所以,我们把这种URG标记位所表示的数据呢,将其称之为,代外数据。什么叫做代外数据呢?同一个TCP链接,不走你的接收缓冲区,而是被上层优先处理,这就是代外数据,而代外数据呢,通常可以被进行检测某些已经毫无反应的机器的状态,如果我们机器挂掉了,那就算再怎么代外,也没用。如果,没挂,就是现在的服务很慢,可以通过代外数据来获取它的状态,这就是紧急指针在机房当中的应用场景。目前见到的场景就这个了,能见到的场景是比较少见的, 你可以理解成在公司里面有很多机器,有些机器是挂掉的,有些机器是单纯的慢,有些机器是正常工作,所以,我们要长年累月的去监测某些机器的状态,要将很慢的机器和很快的机器区分出来,要不然你怎么去保证和甄别是什么原因呢。所以URG标记位呢,通常是我们的状态询问的。
TCP中大部分的报文字段我们都讲过了,还差最后一个属性是 RST。这个reset标记位也不好理解。reset标记位我们真正的要去理解它,我们也得像我们之前说URG、窗口大小一样,也是要给大家绕一下圈的。在谈RST之前呢,我们也是先来谈第一个话题:TCP三次握手。只有将TCP三次握手搞清楚了,然后呢,我们再想办法,以TCP三次握手这个知识点理解呢,尝试回过头去理解RST标记位,那就会更好理解了。我们现在学习的协议叫做TCP- IP协议,如果之前学http/https那是具体的协议还好,我们现在学的是TCP—IP协议,但是大家都知道,整个网络协议栈可不止这两个协议,但是就以TCP-IP协议命名了,整个网络协议栈就叫做TCP-IP协议栈,它就用这两个协议将整个网络协议栈命名,所以,你觉得这个协议重要吗,答案是:非常重要!TCP报头曾经在面试问到,请帮我绘制一下TCP报头,虽然有点变态,但确实有人问。我们学网络呢,可能各种协议都要学,但是最重要的呢就三个,应用层http,传输层TCP和网络层IP,就这三个,所以这三个协议一定要好好的去学习。
》下面我们来看TCP三次握手,TCP三次握手呢,其中的过程呢就是,客户端要想给服务端链接消息,第一得先给服务端发送SYN。当说到SYN就不要只想到SYN,而是想到TCP报文,只不过SYN的标记位被置1了。然后服务端识别到之后呢,服务端就会给客户端发回SYN+ACK,SYN+ACK就代表的第一次握手,第二次呢就相当于给客户端发SYN+ACK就证明我们握手的请求,我已经收到了,允许你跟我我握手。然后呢,客户端呢,再进行ACK,确认该链接被建立好。其中呢,它们两个的报文协商呢,基本上就是将我们对应的特定标记位SYN、ACK置1,至此三次握手就完成。
》同时呢,在我们客户端和服务器端呢,在建立链接的时候,客户端在发送SYN之后,只要发出去,客户端状态就叫做SYN_SENT同步发送。然后呢,服务器收到SYN之后,紧接着他立马发送SYN+ACK之后,它的状态就立马变成SYN_RCVD同步收到。当我们客户端呢,一旦收到服务端发来的SYN+ACK之后,客户端再进行ACK,此时客户端的状态就变成了ESTABUSHED链接建立。所以呢,我们对应的TCP链接保护/维护呢,双方是基于状态变化的,所以呢,这就是TCP的三次握手。
》下面值得讲的东西非常多,我们一个个来。第一个:大家可以看到,在网上或者市面上随便找一个关于TCP握手或者通信的图呢,都是这样画的。尤其是它在画数据从左向右流动,从右向左流动的时候,线都是斜着画的,不是横着画的。其中有一个非常重要的信息,非常容易被忽略,这叫做,时间。换而言之呢,我们在客户端发送SYN的时候,它认为自己的状态叫做SYN_SENT,并不是因为对方给我SYN+ACK了,我的状态才是SYN_SEND。而是,我只要将SYN发出去了,我的状态就变成SYN_SEND。其中,线是斜的,告诉我们,在数据报从左向右流动的时候,它呢,是需要花时间的。所以,在某时候发的时候是3点,当对方收到之后可能是3点1秒,所以他们两点时间呢,是有时间差的,这是第一点,是个细节。
》第二点呢,就是,我们三次握手这里呢,它基于状态变化呢,没有问题,但是呢,我们比较关注点在于,TCP为什么要三次握手呢?因为TCP是面向连接的,拿什么叫做面向连接的呢?根本原因就在于,我们在通信之前就要先建立链接, 所谓建立连接,就是通过三次握手来完成的,而建立链接的时候呢,三次握手完成之后,我们才能能够进行数据通信,当然这句话也不太准确,我们一会儿再纠正。换句话说呢,我们既然是面向连接的,那么我们就必须得先建立链接。但是问题,不在这,在于,面向连接的,那么如何理解链接呢?
》首先作为服务器可能随时随地都要被我们的客户端来连接,也就是说呢,我这个服务器有1000个客户端来连我,这1000个主机都给我发三次握手,那么此时我的服务端呢在链接建立成功之后呢,我们就要把我们这1000个建立好的链接管理起来,大家想想是不是这个道理,要不然都给你连,你怎么知道跟谁连的,什么时候连的?像这样的问题呢,我们在服务端是不是应该把所有已经建立好的链接都要管理起来。我们的链接本身呢,是在操作系统内,TCP协议栈当中帮我们去维护的,那么问题就来了,说白了就是在内核当中给我们维护的。所以,操作系统是如何管理链接的呢?那么,我们呢,就有一个非常重要的话题,server会收到大量的链接,那么OS就需要管理这些链接,那么OS如何管理这些链接呢?? 先描述,在组织!所以呢,在Linux内核当中,一旦建立好链接,链接双方一定要为了维护该链接创建对应的数据结构。而要创建数据结构,说白了,链接就是一个结构体,一个连接成功建立了,那么三次握手完成,双方就要在自己的内核的当中维护链接结构体,链接结构体包含了链接的所有的属性。包括我们链接建立的时间,双方协商好的起始序号,再包括我们双方在建立链接之后呢,我们对应的各种缓冲区数据和文件的关联等等都要全部给我们维护好,换而言之,创建链接本质就是一个连接对象,就需要花时间和花空间!所以,我们双方建立好链接之后,我们UDP是不需要这个过程,而我们TCP就需要维护链接,创建双方要维护的数据结构,和我们之前讲进程、讲文件、文件系统、讲信号是完全一样的,所以这就是我们之前为什么要花那么大的功夫去讲这个的原因,只要这个只是理解清楚了,那么我们也就能够理解了。所以,操作系统建立好链接,那么双方就要维护链接结构体。链接结构体是一个对象,那么说白了,它就是要花空间的,花空间,你要申请吗?你要初始化吗?你要维护各种结构体关系吗?是要的,所以需要花时间。换而言之,我们维护链接是有成本的,这是我们如何理解链接的最终结论。
》我们再回到我们一开始的问题,服务器是可以被客户端大量连接的。也就是说,服务器可能是1对1000、1对5000等,你说,你给我过来1个链接,我服务器消耗一点一点资源,如果成百上千的链接过来了,我的服务器上一定会存在大量的链接,也就意味着会有大量的成本。这就叫做,聚少成多,所以,我们也会经常说,服务器在高压力的情况下呢,有大量的IO来了,最后服务器就挂掉了,原因就在这。因为我们每一个链接来了,都是要消耗那么一点资源,你来一口,我来一口,最后服务器资源没了, 最后操作系统,以及应用程序的活动空间越来越小,最后服务器就挂掉了,俗称荡机了。
》当我们体会到这一点之后,那和三次握手又有什么关系呢?关系很简单,三次握手成功,双方就为了维护链接,就要各自付出一定的数据结构的成本,这就是要说的第一个结论,这是其一;其二呢,如何理解三次握手呢,我们就不得不面对第二个问题,叫做,为什么要三次握手?那么你理解一个东西,理解三次握手,你要真正理解它,对我们来讲呢,我们首先要理解的不仅仅是三次握手,而是要再理解两次或者四次等有什么缺点?然后才能慢慢等接受这个三次握手。
》首先,在三次握手期间,它的握手过程叫做SYN、SYN+ACK、ACK。而其中,大家很明显的发现,我们最后一次的ACK是没有应答的!就是在这,这个客户端最后一次发的ACK是没有应答的,那么其中前面都是有应答的,前面两个因为都有应答,所以我能100%保证前面两个报文对方一定能收到,但是,第三个对不起,因为没有应答,所以是否被对方收到,我们是不确定的,所以,第一个概念叫做三次握手一定能成功吗?答案是:不一定!所以,你得首先承认建立链接,它并不是100%能建立成功的,虽然TCP是保证可靠性的,它在正式通信的时候,能保证要发出去的数据,被你100%收到,但并不代表我们的三次握手一定能够让你握手成功。这是第一个认识。
》为什么是三次握手呢?为什么不是两次、四次呢?我们先来谈谈,一次行不行。什么叫做一次呢,就是客户端给服务端发个SYN,双方链接就算建立好了,那么双方链接就算建立好了,我们继续就正式通信了,就相当于我们不需要确认,确认就是一次握手。所以客户端只要给服务端发一个SYN,服务端就认为双方可以正式通信了。首先,一次是肯定不行的!为什么呢?因为,如果是一次的话,最简单的理由就是,这一次握手呢,当然和三次握手一样,也可能失败也可能成功,但是从成功率来讲呢,一次握手和三次握手的成功率其实差不多,因为都是最后一个报文被确认就算建立连接成功。但是呢,有一个什么问题呢?一次握手成功,非常容易受到攻击,现在一次握手成功,意思就是,我只要给你发一个SYN就可以,你现在是服务器对不对,我现在拿着我的笔记本循环式的给你发送SYN,发完SYN我就不管了,我只给你发SYN,什么也不管,也不给你发数据,因为是一次握手,只要给你发SYN,那么服务端就认为链接建立好了,同学们,你们认为链接建立好了,会带来什么结果?是不是会带来,我们服务端为了维护这个链接,就要创建对应的数据结构来描述它呀!所以,一台主机,拼命给我们发送SYN,或者构造假的SYN请求,最终就一台主机就将我们的server当中的有效资源完全打满,进而让正常的链接无法建立链接,这种攻击方式,叫做,SYN洪水。所以如果一次链接的话,那就只能被人攻击了, 一个理由就能很说明这个问题了。因为我一发个SYN,你就认为链接建立好了,那你就建立结构体描述对象吧 ,一个占几十kB,我给你发上万个,呢服务器立马挂掉。
》那么,两次握手为什么不行呢?我客户端给你发一个SYN,你服务端鼻血得ACK,一旦你ACK,一来一回,这才叫做握手。这两次握手行不行呢?答案是:也不行。有同学会说,这个两次握手为什么不行呢?这个两次握手和我们的一次握手其实是类似的。如果你是两次握手呢,其中最后一次,你服务端只要发出ACK,也就意味着,服务端就认为链接建立好了,同样的就要做维护链接的工作。所以,我今天是一个客户端,我也继续给你发送大量的叫做SYN,给你发送大量的SYN,你怎么办呢?你发送SYN,服务器不会认为链接建立好了,但是,你服务器对我客户端发出第二个报文ACK,只要一发出,就是两次握手,此时服务端就意识到链接建立好了。那我作为客户端,只给你发大量的SYN,你服务端给我发过来的第二次ACK报文,我客户端直接丢弃,我照样的给你发大量的SYN,你是不是服务端最终也要维护大量的链接呀!是不是一次和两次的效果都是类似的。所以你是两次也一样,我也无脑给你发大量SYN,因为站在你服务端的角度,只要第二次握手发出去了,那么此时你就认为链接建立好了,你是不管客户端是否收到了,反正你认为链接建立好了,此时我客户端给你发大量的SYN,你服务端给我发过来的第二次报文,我直接丢弃掉,反正你也不知道,我也不需要给你应答了。此时照样是SYN洪水,照样能够耗尽你服务端掉资源,所以也是明显有问题的!
》那三次握手行不行呢?三次握手,我们可以发现,一次不行,两次不行,根本原因就在于,我们的链接建立的时候,每一次都是让sever端先认为链接已经建立好了,而三次握手之后呢,它可以把最后一次确认的机会交给server端,因为只有三次握手,最后一次握手成功之后,你的server端收到最后一个报文,由服务端最终来结束三次握手,那么只有当服务端最后一次收到确认了,其中注意,客户端三次握手时,最后一次ACK发出去,客户端根本就不知道最后一个报文是否被服务端收到,因为我们前面讲的第一次、第二次都有应答,只有第三次没有应答,所以第三次丢失,是三次握手最害怕的事情,但是这个并不影响我服务端!因为我服务端是最后一个确认ACK的人,那么当你客户端对应发出最后一个ACK的时候,虽然最后一次握手有概率丢失了,但是影响的是客户端,跟我服务器没有关系,也就是说呢,在正常情况下,你再给我发大量的SYN,那我服务端给你ACK,那么你客户端必须得给我ACK然后我才认为链接建立好了。那么换句话说呢,只要服务端和客户端建立好了链接,只要服务端有了维护链接的结构体,你客户端必定维护,所以你想拿一台机器对我发送大量的SYN攻击,我的服务端也要把你拉下水,你给我挂多少链接,我也要给你挂多少链接,你单主机想要攻击我是很困难的,因为你的资源在不断减少,你必须得像正常的三次握手一样维护链接,我服务器的资源当然比你多。
》所以,如果最后的ACK报文丢了,它并不会影响服务端,因为服务端认为,我三次握手并没有成功,最多我服务端给你维护一个短暂的半链接,我不会用全链接的方式,把我们对应的链接结构体维护好,因为我没有收到ACK。而你的客户端就不一样了,你客户端只要发出去了ACK,你就是链接建立好了,不管未来,只要你客户端发出去了对应ACK,无论你ACK是否被我服务端收到,你的客户端一定都要维护链接。所以呢,三次握手,这种奇数次握手,最后一个报文丢失的成本嫁接给了客户端,因为客户端面向端客户群体比较小,所以它出现一些闲置的或者非法的链接呢,并不影响。如果服务端如果出现大量的链接,问题就大了。所以,为什么要三次握手呢?因为可以把最后一个报文丢失的成本嫁接给客户端,这就是我们第二个结论。
》那么就有人说了,我们的三次握手是不是就不会收到SYN洪水呢?答案是:并不是,三次握手呢是以最小的成本,较少的握手次数来尽可能的避免直接的SYN洪水攻击,因为它并不是网络安全方面的话题,它就是一个正常的前提,所以呢,我们如果是三次握手的情况下,我们的服务器能不能受到洪水攻击呢?答案是:照样会。但是我们能够保证,我们不会让一台机器随随便便就搞垮。你要攻击我,你就必须得建立连接成功,即便是你要跟我建立连接成功,前提条件呢,我也要把你拉下水。所以,一般你想要攻击我服务器就不会拿一台机器做,而是用很多机器,同时来对我发起攻击。
》TCP三次握手有一个特点,它以最小成本方式,对于客户端来讲,客户端有发送数据的过程,客户端也有接收过程。能理解吗?客户端能发一次,它也能接收一次,所以客户端呢就验证了自己的IO。服务端呢,也有发送的过程和接收的过程,所以呢, 我们三次握手就分别验证了,客户端和服务端的输入和输出是否正常,我们俗称验证全双工。为什么是三次握手呢?因为,它用了最小成本的方式来验证了全双工。
》所以,最后的结论呢,为什么是三次握手,我给大家了两个理由: 第一个,用奇数次握手,能够将最后一个报文丢失的成本呢,嫁接给客户端,最后一次的确认由服务端来做,意味着服务端是最后一个建立链接的,必须得保证你客户端建立好,我才建立好。第二个呢,就是验证全双工。
》为什么不是四次呢?四次的话很显然,最后一个报文的丢失成本又交给了服务端,那就不太合理了。五次为什么不行呢?就这样告诉大家,你哪怕8、10次呢,总是会不安全的,总是会有各种各样的情况,因为双方在建立链接,只要你服务器为了维护链接成本,你哪怕握手上百次,黑客呢,照样劫持一大批机器,你要握手几次就几次,我照样可以攻击你,所以握手过程有安全的考量,但它并不能很好的去解决问题。所以,三次握手呢,只要以较小的成本不要那么容易受到攻击,单纯的站在三次握手的角度;然后能够验证双方的信息通路是能够连通的,就可以了。基本上三次就OK了,5次啥的没必要,因为你再增加握手次数,并不能解决实际问题,而且还只会徒增握手数据交互的频次,进而导致握手出现效率降低的问题,所以三次握手是一个比较合适的方案。如上就是为什么要三次握手。
》下面我要来回答两个问题了,第一个问题:按照你的说法,最后一个报文ACK丢失,我想问一下你,如果我们的客户端发了一个链接的请求,又收到了一个链接的响应,一来一回,就两次握手了。客户端呢,只要把最后的ACK发出去,客户端是不是立马就认为链接建立成功了呀。 可能过了一秒钟之后,服务端才收到ACK,它才建立连接成功,这都没问题,因为呢,我们最后的ACK都没确认,所以,搏的概率呢,就是最后一个ACK能够被对方收到,这就是搏概率。我的问题是,如果客户端它把最后一个ACK发出去了,但很不幸,ACK丢失了,因为这个ACK是没有应答的,所以客户端也就无法得知ACK丢失了。但是客户端依旧认为,链接已经建立好了,因为它把ACK发出去了,此时客户端链接建立好,维护对应的空间链接。然后,此时服务器在干什么呢?因为它没有收到所谓的ACK,所以服务端认为依旧没有建立成功,那么这里就有一个,当然我们后续肯定还有,比如服务端发出ACK,没有收到ACK,也就意味着它发出去的第二个报文没有被确认,服务器呢,会进行超时重传,但是这不重要,关键是你在超时重传的时间段内,有一个时间窗口,客户端认为链接建立好了,服务端认为链接没建立好。你要超时重传还得再等一等,那么我现在的问题是, 如果客户端一把ACK发出去,就认为链接建立好,但是报已经丢了,服务器认为链接根本就没有建立好,那么请问,接下来客户端会干什么呢?就在这么一个时间窗口内,双方的链接建立成功与否,并没有达成共识,那么此时客户端会干什么?客户端呢,认为我费这么大劲将链接建立好,建立好之后,我还不赶紧给服务器发消息吗?所以客户端呢,立马给我们服务器发消息,我们先暂时不考虑ACK的问题,一旦再发消息,此时服务器就会遇到这样一个东西,不是说好的,必须三次握手成功,你才能给我发消息吗?怎么我三次握手还没完成你就给我发消息呢?所以,服务器立马就意识到,可能链接建立是有问题的。所以,此时服务端就立马给客户端发来的数据进行ACK响应,响应的时候, 将响应报文的RST置1,代表的就是,告知客户端,将你的链接进行重置!所以reset标记位,客户端收到TCP报文当中的reset标记位被置1,代表的就是需要关闭链接,进行重新链接,所以叫做reset!
》所以,我们reset标记位代表的含义就是:双方在进行链接建立的时候,倘若出现了链接有问题这样的情况呢,我们需要重新建立链接,或者叫做链接复位,那我们就需要设置RST标记位为1,这叫做reset。
》当然实际上,我刚刚举的这一个例子呢,那么其实并不是特别常见,也并不是特别准确,但是它很好理解。大部分情况下呢,比如说,我今天访问一个网站或者登陆一个账号,这个网站呢,我跟他把链接建立好了,但是从此往后我再也不动他,时间一久呢,操作系统呢就会将客户端的链接关了,服务器呢,也可能把他的链接关了。你想想,我们两个在进行建立链接的时候,把链接建立好了,你不动我也不动,我俩都别动,不动之后呢,对于双方的操作系统,它不可能把你的链接一直保持下去,因为你们两个把链接建立好了,你们都不聊天,都不沟通,所以不就是资源白白浪费嘛,所以操作系统就要做链接的相关管理工作。比如说,里面有各种的定时器,去检测你里面的链接是否是正常的链接,或者你多长时间没有访问了,你过了一个小时还是两个小时,这个链接都没动,那就会被释放掉了。释放掉了,但是双方在关闭链接的时候,就直接无脑关了,关了之后呢,此时就会出现什么问题呢?一方认为有,一方认为没有,所以双方在正常通信的时候,比如说,我服务端收到一个没有建立链接就发过来的数据,那么我呢就立马会给对方发送链接重置,此时就会进行重新建立链接,说白了就是让你重新连。比如说,你那浏览器,访问某些网站的时候,它就会告诉你这个链接被重置了,是的就是这个。一般在很多情况都会遇到这个,还有就是丢包率比较高的场景,还有就是目标服务器压力过大,你访问,它也访问,最后呢,你认为你建立好了,但是服务端还没来得及处理,最后呢就出现刚刚说的那些问题。
》总之呢,我们的reset呢,是一个底线标记位,一旦出问题,我们就使用reset来进行链接重置,一般都是让客户端重连的。如上就是三次握手和TCP报头的6个标记位,我们全部讲完!至此呢,我们就把TCP的报头全部讲完了。
》我们再来进行从上往下,把我们刚刚讲的东西给大家说一下。有些没讲的,我在这里给大家讲。
》TCP的序号问题,我再给大家说一下,TCP是有发送缓冲区的,TCP是面向字节流的,字节是单位,1个字节就是1个字节,实际上呢,缓冲区呢,我们不要把它当成一个整数、float这样的缓冲区,我就把它当成char类型的数组,一块缓冲区不就是一块内存吗?这块内存呢,我把它当作char类型的数组,所以上层把数据拷贝到TCP发送缓冲区的时候,那么每一段数据就具备了天然的在缓冲区数组这样的下标!那么我们就可以使用这个下标来充当字节流数据,每一个字节的序号,所以呢,你要发1~1000的时候,我们拿最大的来充当我们报文的序号就行了,所以我们的TCP呢,可以将每一个字节都进行编号,编好之后呢,你发到了多少,我们用最大的序号呢,作为这次报文的序号值,那么发送给我们的接收方,接收方ACK的时候,就相当于我们发的序号是1000,它给我ACK确认序号是1001,下次发的时候,我直接拿着数组的下标定到1001,直接就可以向对方再发数据了。所以,我们就以这样的方式来进行我们的编号。
》所以这个确认应答机制当中的关于序号的问题,其实我们也是很好理解。发送缓冲区不就是一段内存嘛,把内存当作char类型的数组,上层拷下来的数据,每一个数据不都有下标嘛,每一个下标不都是序号嘛,那么我就可以根据这个下标来进行发送。
》下一个。如果我们几天主机A给主机B发消息的时候,主机A给主机B一旦发消息,只要发数据发出去了,就有两种情况,第一种情况呢,就是数据丢了,丢包了怎么办呢?TCP其中呢,是要给我们进行超时重传的。它此时,当你把数据发出去,发出去之后呢,因为每一个数据都必须要有应答,如果我在一段时间内没有收到直接或间接的确认应答的话,我就认为这个报文发出去就是丢了,至于是不是真的丢了,我们不管了,反正我们在一段时间内,没有给我们应答,我就认为真的丢了,丢了之后我就再重新发送,这就叫做超时重传机制。关于重传我觉得并不难理解,但是呢,这里有很多的潜台词。是不是就意味着,你把数据发出去了,在没有收到应答之前,这个已经发送出去的数据还必须得先暂时在发送端主机保留起来,要不然你后面,万一丢了,该怎么重传呢?那么这里,衍生再想呢,就是这个数据被保留在哪里呢?这就是我们后面要讲的,知识都是有联系的!
》主机A发送出去的数据,如果没有收到应答,好吧,它要超时重传,没有问题。假设超时重传的时间是1秒,1秒期间,它的数据不能在发送方主机立马清掉,因为你要支持超时重传。如果你重传了好多次,就意味着,一直在这段重传的时间段内你必须把他也要一直保存下来,这就是我们后面的问题,现在不管,反正就是丢包就重传,那么重传的话呢,这是一种情况,我主机A发消息,数据真的丢了。
》除了这种情况,还有一种情况就是,主机A给主机B发消息,可能并不是我们数据丢了,而是主机B给主机A发送确认应答的时候,应答丢了,在主机A看来不就是一回事嘛,主机A看到主机B发过来的应答丢了,那么主机A并不知道呀,它只知道自己没有收到应答,所以实际上这个数据已经被收到了,但是应答丢了,主机A依旧认为是数据丢了,数据丢了怎么办呢?那么就超时重传呗,超时重传就再发一次出去,然后B再给ACK,至此发送完毕。那么这个也没问题,但是我们同学最担心的问题,就来了,我在进行网络长距离传送的时候,除了担心我数据从主机A发送到主机B,除了担心丢包的问题,除了担心乱序的问题。丢包有重传,乱序的话,有序号来进行按序到达。可是像这种情况怎么破解呢?相当于主机B收到了两个一摸一样报文呀!同学们,那你说怎么办?主机B难道把这两份一样的数据都交给上层嘛?答案是:不行。两个一摸一样的数据被收到了,这种情况是存在的,但是我们不允许它再向上进行传递了。怎么办呢?TCP 还要具备可靠性机制,叫做,去重!
》那么问题又来了,请问你怎么去重呢?那么这里又得谈谈序号的第三个作用了。因为报文有序号,你发过来的报文呢,我在历史上收到过,你再给我发一份,我可以根据我们的序号来进行说,这个报文我们已经收到了。那么根据序号呢,我们就能够很明显的做到去重的功能。所以,**序号不仅仅是为了确认应答,更不仅仅是为了按序到达,它还可以做到去重!**所以呢,我们不用担心主机B收到重复报文,一般收到重复报文,那么这个报文就会被丢弃了。
》所以呢,我们超时重传的机制呢,我们丢包的情况就只有这两种。
》那么接下来还有一个话题是值得研究的。那你说主机A给主机B发消息,你要在特定的时间间隔内,要进行超时重传。超时重传我没有意见,我的问题是,那么这个超时的时间是多长呢?我想给大家说的是,首先网络的情况,主机A给主机B跨网络传送的时候,它们之间所经历的网络状况是变化的,这个大家要能意识到,就好比呢,今天12点吃饭的时候呢,你们的校园网一定是非常拥堵的,上网的人非常多,但如果是半夜12点,那么访问的一定很少,所以,网络的状况呢,是随着入网的人的增多而变得拥堵,而随着人下线的越多而变得通畅,所以网络的情况是浮动的!如果网络的情况是浮动的,那么网好的情况,数据就一定能够很快到达B,反之,花的时间就更多了。倘若,网络非常好,你这个超时重传的间隔设置的又长,那你不就是在浪费资源嘛?我网络状况明明非常好,主机A到主机B可以很快将数据发出,你非得把时间间隔设置的很长,就会导致效率很低。另外,如果网络拥堵,你的时间间隔又非常短,那是不是我把数据发出去,我的报文还在路上走呢,你这边又超时了,又得重发,所以就很容易误触我们的重发策略。
》所以,我们基于这样的分析就可以得出来,这个特定的时间间隔呢,它网络的情况是变化的,所以这个超时重传的时间间隔必须是变化的,不能是一个确定的值,它必须随着网络的情况而随时调整,如果网络状况非常好,那么时间间隔就可以很短,如果网络状况拥堵,时间间隔就应该长一点。所以呢,基于这样的指导思想呢,Linux下是这样的:
》超时以500ms为一个单位进行控制, 每次判定超时重发的超时时间都是500ms的整数倍
·如果重发一次之后, 仍然得不到应答, 等待 2500ms 后再进行重传.
·如果仍然得不到应答, 等待 4500ms 进行重传. 依次类推, 以指数形式递增.
·累计到一定的重传次数, TCP认为网络或者对端主机出现异常, 强制关闭连接。也就是反正我服务端呢,给你发消息你也收不到了,什么三次握手,四次挥手,做了也没用,你也没办法给我直接连接嘛,所以我就强制断开连接,换句话说呢,就是彻底断开链接。
》所以,重传时间间隔是浮动的,这里也验证了,我们前面讲的一个观点,如果你是认为对端主机出现异常,如果单纯的对方主机没有出现异常,单纯的就是网络出问题了,那么你现在服务端强制关闭了连接,那么这个时候是不是出现了我们刚开始的问题:服务端认为链接已经关了,客户端认为链接好着呢。所以双方再通信,那么服务端就会给客户端发送reset标记位的报文,那么这个时候链接就会被重置了。
》这就是我们对应的超时重传机制。
下一个我们要谈的话题呢,就是TCP的链接管理机制。链接管理呢,我们已经讲了一半了,还剩下的一半呢,下面接着说。
》连接管理我们重点要说,三次握手已经在前面讲了,下面呢,在三次握手成功之后呢,紧急着就是正常的数据通信了,我们发数据,然后呢对端再进行ACK,不过在我们正式谈它之前呢,我们要回答一开始遗留的问题,这个问题叫做,我们发送方给接收方发消息,可是呢,我们担心发太多,所以他必须给我们通报它的一个接收能力,所以对方在给我相应的时候,报文里面有一个16位窗口大小的字段,能够知道它的接收能力。可是第一次怎么办呢?不要忘了,在我们正常发送数据之前,双方在前两次握手的时候,就已经有了两次的数据交互了。所以,在三次握手这里,不要简单的认为就只是在进行三次握手,除了三次握手之外,双方还有很多的协商工作,比如说,互相通告双方的数据接收能力,只有通告了数据的接收能力之后呢,我们才能正式发送数据的时候,我们其实已经能够在握手期间得知对方的接收能力了,所以我们后续第一次发的时候,不担心对方来不及接收的问题。
》后面呢,我们在三次握手这里还要再加一些三次握手要做的工作,后面再说,相信大家现在是能够理解了。还有一个细节呢,就是一般呢,在进行三次握手的时候呢,有可能在很多的教材里面告诉你,最后一次握手的ACK也是可以携带数据的,那么是可以的。
》再下来,正常的进行数据通信呢,就是我们之前写套接字调用的read()、write()接口,其实就是在进行正常的IO了。
》当数据通信的时候呢,我们最后通信完了,那么此时我们就进入到了下一个话题,叫做,close(),也就是关闭我们对应的文件描述符。双方在关闭文件描述符的时候呢,你会发现客户端要管,服务端也要关,每一个人关一次就意味着呢,我们最后呢,就相当于,你close一下,我close一下,我们双方在进行close的时候呢,一个close对应两次挥手,下面呢,我们要谈的就是四次挥手的问题。
当我们想断开链接的时候,TCP连接是面向连接的,建立是需要三次握手,断开是需要四次挥手。四次挥手怎么挥呢?主动断开连接的一方,比如说客户端,它想要断开连接的时候,它需要给服务端发送FIN,然后服务端再对他进行ACK,一来一回就是两次挥手。代表的从左向右发送FIN的时候,代表的就是,只要我给对方发了FIN,代表的就是我要和你断开连接,相当于呢,客户端给服务端说呢,我不想再给你发消息了,我想和你断开连接,此时客户端就和服务器断开了。注意如果客户端给服务器断开连接了 ,因为四次挥手分别由客户和服务端各自主动出发一次,如果我们当前客户端它发送了断开连接,但是我们的服务端还没有发送FIN,所以此时我们的客户端呢,就无法再向服务端发送正常的数据,确认是可以发,但是一般的数据就无法发送了。但是呢,我们服务端依旧能够向客户端发送消息,只有服务端把消息也发完了,再发送FIN,客户端回应ACK,至此双方的连接就断开了。这就叫做四次挥手。
》话说来,如果只有一方断开,就意味着我不想给你发消息了,如果你想给我发消息也可以,可能你还有消息没给我发完,你发你的,所以四次挥手当中呢,第一次和第二次,就是一个FIN和ACK挥手报文对呢,和下一个报文对呢,可能中间还有一部数据发送的场景,但我们今天不考虑它,只要你想断开链接,我也就可以跟你断开连接。
》为什么是四次挥手呢,原因很简单,其实作为客户端呢,我想给服务端断开连接,其实我只要发一个FIN就够了,就是我告诉你我要和你断开连接了, 服务端想给客户端发,其实也是服务端给客户端说,我想跟你断开连接,然后也发一个FIN。其实,理论上想要断开连接的话,只要两次挥手就可以了,但是为什么是四次呢?根本原因就是,客户端发的FIN,怎么保证服务端是收到的。只要你想发FIN,你自己肯定是知道,但对方怎么知道呢?你怎么知道你发出去的FIN,100%被对方收到呢?所以FIN必须得有ACK,为什么四次挥手的根本原因就在于,你其实就想FIN,只不过需要得到确认,那就会有ACK了,这里也就产生了四次挥手。
》当然,我直接个对方发一个FIN,对方再给我发FIN的时候,不也是相当于确认码?可以这么理解,但是,我想跟你断开连接,并不代表对方也想和你断开连接,可能我想和你断开连接的时候,对方还有数据没有发完,那么势必就导致了,你发FIN的时候,可能我并不想发FIN,我们两个发送的FIN可能有时间差,所以,基于两次挥手绝对是不可能的。如果刚好你想跟我断开连接,我也想和你断开连接,其中我FIN一发过去,你呢,FIN+ACK也发过来了,然后我再ACK,这样可以吗?可以的。所以在很多的教材里面,官方的都叫做四次挥手,实际上,如果在很巧合的情况下呢,你想和我断开,我也想和你断开,在这种情况下呢,我们也是可以三次挥手,相当于一方是ACK+FIN,三次挥手也是可以的。不过呢,这是特殊情况,我们要学习,肯定是要学习普遍情况,所以四次挥手,FIN、ACK、FIN、ACK。总结一下,原因就是双方就像告知对方,告知对方呢,也得站在我的角度上,保证100%被对方收到,所以,只要我给他发送FIN,它必然要给我ACK,对方也同样如此,这样的话,我想告诉对方,对方也得告诉我,至少要两报文, 两个报文都要被确认,所以总共要四次报文。这就是四次挥手。
》当我们搞清楚四次挥手之后呢,我们接下来再来研究左边和右边两侧的状态变化。一般呢,我们是主动断开连接的一方,只要我发送出去FIN,我的状态呢,就是FIN_WAIT_1。只要我发出去了,对方收到了,对方给我发送ACK,那么只要对方给我一发送ACK,它只要一发出去了,它的状态就变成了CLOSE_WAIT,此时主动要求断开的一方收到ACK,那么此时客户端的状态就是FIN_WAIT_2,接下来客户端什么都不做了,剩下就是由我们对应的服务端再来,把它从右向左的连接也关掉 ,所以它呢,由CLOSE_WAIT呢,也想调用close,所以,它再发送FIN,此时它的状态就变成了LAST_ACK,此时呢,我们的客户端收到了断开连接的请求,再发送ACK确认,此时客户端状态变成TIME_WAIT,发送给对方之后,至此两方链接断开,就进入了CLOSED状态。这两边的状态不需要记,也不用担心,下面把他们再说一下。
》其中呢, 我们把单方向断开链接呢,我们把进入CLOSE_WAIT状态呢,我们其实就是属于一种半关闭链接的状态。说白了就是,我们客户端给服务端发消息,服务端ACK之后,客户端不再给服务端发消息了,此时服务端的状态就是一种半关闭链接的状态,这就是CLOSE_WAIT状态。下面的问题就是,对我们现在而言,所有的FIN的发送,都是由调用了close()发送的,而无论是三次握手还是四次挥手,上层接口呢,也就是一个函数,而三次握手和四次挥手呢,是由TCP协议自主自动的去完成的,这是其一;其二呢,我们双方在进行断开链接的时候,如果我的发送方,比如说断开链接的一方关闭了, 但另一方并不着急关闭,比如说客户端呢,它此时跟我把链接关了,我服务端不想关,我不想调用close(),那么此时我们的服务端呢,势必会挂上大量的CLOSE_WAIT状态。CLOSE_WAIT是半关闭状态,说白了,就是没关。这个没关的链接呢,依旧还要占用我们的资源,因为服务端有可能还要像我们的客户端发消息,所以这样的状态呢,链接还是要维持的。第三点呢,主动要关闭链接的一方呢,最重要进入一个状态,叫做TIME_WAIT状态,注意,我现在讲是以close()为例,如果以服务器断开为例,同样是这样子的。主动断开链接的一方,谁先断开链接,谁最后就要进入TIME_WAIT状态。TIME_WAIT他这个特点呢,就是发送出去最后一个ACK之后,我们呢理论上客户端已经完成了四次挥手,可以关闭链接了,但是主动断开链接的一方,并不会立马释放,而是要等一段时间,然后才会进入CLOSED状态。我们一会儿要从代码角度去验证两种状态,一种是CLOSE_WAIT,一种是TIME_WAIT。
》所以,四次挥手呢,是双方在建立好链接之后,是我们断开链接的常规方式,既然是常规方式呢,就是正常的大部分情况,也有少数的情况呢,强制关闭的情况呢,我们也不用担心,因为强制关闭呢,我们有reset,我们可以进行链接重置,这也不影响。所以,正常通信的情况呢,它是四次挥手的,为什么四次呢?根本原因就在于TCP双方的地位是对等的,你要跟我断开链接,我也要跟你断开链接,但是呢,这个理由呢,是有一点点牵强的,再具体一点呢,双方地位的对等呢,最主要的是体现在,TCP呢,它维护链接的话,双方都是要维护的, 而且TCP是全双工通信的,所以全双工指的是什么意思呢?指的是,客户端是全双工的,服务端也是全双工的。所以呢,我们在断开链接的时候呢,我如果给你断开链接了,其实这句话的潜台词是,关闭全双工当中的发送能力,就是我不再给你发送消息,但是并没有完成,我们所对应的关闭接收能力,只有当,我再接收到对方给我发来的第三次挥手,第四ACK之后,双方才把各自的IO能力关掉,这叫做真正的关闭链接,这就是四次挥手。
》整个的过程呢,我们是完全没有感知的,因为我们在应用层呢,调用read、write这样的接口进行IO,其他的细节呢,我们其实并不清楚,具体工作呢,是由我们TCP自己完成的。所以呢,想发送SYN的时候,就相当于我们客户端进行connet(),那么connet就是构建一个SYN报文,三次握手自动成功了,我们再去调用accept(,就会直接返回。再下来呢,我们调用read()、write()就是正常通信 。我给他发报文,这个ACK从来就感觉没有收到过,我们在read()的时候也没有读到过ACK,这个ACK是在TCP内部自己维护的。那么四次挥手就更直白了,你关闭链接,我也关闭链接,你关了就完了,你应用层并不知道底层在做什么,但现在你知道了,你一关闭,一个close(),对应一对两次挥手,两个close就对应四次。如上就是四次挥手,其他的呢,状态的变化,能记住最好,记不住的话也没办法,也不算重要,虽然报头大家要记住,但是状态忘记了,可以再去查一下嘛,它毕竟是官方的一些资料,我们忘记也没有关系。
》下面呢,我们重点来谈两个状态,一个是CLOSE_WAIT,一个叫做TIME_WAIT。按照你的说法,如果此时,我想进行一下验证我的CLOSE_WAIT状态,我应该怎么验证呢? 它是TCP底层的状态,链接一旦建立好,我们让一方主动断开连接,比如让客户端断开,但是服务端不断开链接,服务端不关闭,它不发送FIN,那么服务端的状态是不是会一直维持在CLOSE_WAIT状态下呀,所以,我想看一下CLOSE_WAIT状态,我们是能够验证的。
》就是你连我,我不获取你的链接,我先证明,如果我不accept,这个链接最终能不能建立成功;第二个,你关闭你的,我不关闭 ,那么这个链接我不close,我不close的话,最终我想看到的是,我们的状态呢就会有一个CLOSE_WAIT状态,然后后来呢,我想主动关闭,如果双方挥手完成,我也想看到TIME_WAIT状态。
》我们的服务器一旦创建好套接字,别人来进行绑定我们的套接字信息,设置监听,我们先不accept你,看你能不能连上我,为了后面测试方便呢,我要把listen()参数里面有一个数字,我们从来没有讲过,我们说过,后面会说,我们把数字改小一点,比如说改成2,然后就是循环了,我们什么都不做,这是seve.hpp代码。我们将我们的服务器启动起来了,我服务器创建好套接字,什么都不做,没有调用accept(),listen()的第二个参数,我们故意改成了2,下面就是我想让别人来连我。我们用另一台服务器来连了,我们用#netstat - nltp,注意l选项只差listen()的,我们今天是一旦我和你客户端一旦建立好了链接那么此时呢,我们应该看到的不仅仅是你listen状态,应该是插刀ESTABUSHED状态,所以把l选项就去掉了,#netstat-ntp。所以第一个结论就来了,如果我们今天启动一个服务,这个服务呢,不accept(),那么此时这个链接能建立码?答案是:能!换句话说,我今天呢,accpet()并不参与三次握手,也就是我们上层调用的accept(),你调不调用它,它底层自动会三次握手成功,你accpet()仅仅是把底层已经三次握手成功的链接拿上来,仅此而已,这是第一!此时呢,我们再用客户端的服务器,再进行连接我们的服务器,我们可以查看到服务器#netstat-ntp,可以看到同样连上了,状态时ESTABUSHED;我们再利用同一台服务器去练服务器,可以看到成功了,状态是ESTABUSHED;我们继续用同一台服务器去连接,但是发现状态是SYN_RECV!
我们发现我们有了三个对应的已经成功建立的连接,换句话说,就是ESTABLISHED,但是有一个,第四次的时候,出现了SYN_RECV,就是说,我们这一台被连接的阿里云机器呢,收到了一个SYN,但是呢,它并不着急的给你SYN+ACK,而仅仅说,我说到了这个请求,第二次握手的报文,它不发,它的状态一直处于SYN_RECV状态,换而言之呢,我们当前的连接呢还没有完成,注意连接没被建立好的端口是45170,我们将第一个客户端关掉,我们看到45170端口号的那个客户端不见了,是因为我们在关掉第一个客户端的时间太久了,因为是半链接,所以半链接的时候,你长时间连接不成功的话,它会将你的链接关掉了。
》但是我们发现的是,我们连第四次的时候,此时就无法成功的进行握手了,第五次、第六次,对不起无法再成功的三次握手了,你客户端给我发来的SYN,我记着,但是我当前不跟你建立链接了,我想验证的是,如果之前有一个链接退出了,这个链接在成功握手,这个可以试试。
》现在的问题是,最重要的是,为什么3个正常的链接,第四个就不让你建立链接了呢? 现在就要正式的说一下listen()的第二个参数! listen()的第二个参数,叫做底层的全连接队列的长度。换而言之呢,我们今天写的代码当中,没有accpet(),将来我们的服务器有没有可能非常忙,一瞬间成百上千个连接,来不及accpet(),那么其中呢,我们底层的连接就会在操作系统层面上进行排队,**listen()的第二个参数,叫做底层的全连接队列的长度,算法就是,让我们用户传入的值n+1,表示在不accept的情况下,你最多能够维护多少个连接。**换而言之呢,因为我传入的是n=2,我们服务器呢,在底层呢,自动给我们建立好的全连接队列的长度就是3,如果超过3个,那么TCP不再进行三次握手,而是说呢,TCP收到你的请求,暂时以半链接的方式存着,后续有老的链接退出了,我再把你这个链接建立成功。
》我们后面会谈一个,全连接维护的意义!就是,为什么你TCP不大度一点,我给你发了很多连接,你来不急accept,那你直接让我排队不就完了,排的越长不是挺好的吗?为什么,要让我们的用户去维护一个参数,来设定自己全连接队列元素的个数呢?我们后面会说。
》现在重要的是,另一个知识点,我们下面继续验证。我们继续查一下#netstat ntp,是有两个链接还是建立着的,我们前面关过一个,关的那一个状态变成了CLOSE_WAIT。我们再去关一个客户端,我们可以看到关的那个客户端,状态就变成了CLOSE_WAIT。我们腾讯云服务器呢相当于客户端,它客户端主动关闭的话,就相当于断开链接,此时断开链接的一方呢,它此时要进入一个TIME_WAIT状态,但是我们还没有查看。现在的问题是,你客户端作为主动断开链接的一方,你给我断开链接了,发送了FIN,我也发送了ACK了,此时我的服务端,链接建立好了,可是我服务端呢,一,没有获取你这个链接;二、我更加没有关闭链接。怎么关闭呢?你得accept上,然后close()才算关闭,现在你没有关它,所以就看到了一个客户端关闭之后呈现的是CLOSE_WAIT状态。CLOSE_WAIT是谁的状态呢?是我们服务端8080号端口服务有两个链接是处于CLOSE_WAIT状态。什么意思?意思是,这两个客户端走了,可是我的服务端没有调用close(),所以最终呢,就出现我们看到的CLOSE_WAIT状态。
所以,我们就知道了,如果我们服务端呢,不关闭对应的文件描述符,此时呢它进入的一个状态就叫做CLOSE_WAIT状态!
》然后呢,我们将例子再变一变,我们此时服务器主动将自己关闭掉了。大家知道,文件生命周期是随进程的,服务器我自己关掉了,我们再去看的的时候#netstat-ntp,什么都没有了,但是呢,我们在客户端腾讯云机器上再查一查,#netstat-ntp,可惜没看到我们要的现象。我们在做一下实验。
》我们将accept()也加入,来一个链接,我就将你拿上来,拿上来之后呢,打印一条消息,那么链接我就拿到了,拿到之后怎么做呢?我们还是不关闭套接字,即不调用close(),我们也将listen的第二个参数改成1,方便我们测试。我们让服务器启动起来,然后用腾讯云机器充当客户端去连接,然后我们在服务器机器上查看,#netstat -ntp可以看到有一个链接拿上来了,状态是ESTABLISHED;当然还可以再让一个客户端连接成功,所以这应该也是意料之中,然后我们连第三个客户端的时候,状态也是ESTABLISHED,因为我们调用了accept(),我们将链接拿上来了,所以,不要大惊小怪哈。现在呢,我们将第一个客户端关掉,我们再去服务器机器上查一下,我们服务器相当于将链接accept()上来了,但是我们并没有去close(),客户端发送来FIN,我服务端给他ACK之后呢,我的状态呢就是CLOSE_WAIT,因为当前没有调用对应的close(),所以我服务器这边维持链接的状态呢,就是CLOSE_WAIT。这个证明了,一个建立好的链接,只要客户端退出了, 无论你有没有accept他,只要你没有调用close(),那么当前的网络文件描诉符所处的状态呢,一直都是CLOSE_WAIT状态。
》下面呢,我们再做一个实验,因为我们服务端是一个正常的telnet应用,服务端是将连接保持着,如果我将服务端杀掉,那么服务端是不是变成了主动断开链接的一方了呀! 那么他在底层呢,要一瞬间的能够成功四次挥手,那么主动断开链接的一方是要处于TIME_WAIT的状态,我们来看看是不是呢?我们来查一下服务端机器上的链接#netstat -ntp,我们可以看到链接是TIME_WAIT状态。
换而言之呢,只要走到TIME_WAIT,一定意味着,主动断开链接的一方呢,是立即将四次挥手的工作做完了。但是TIME_WAIT时间呢,是会保持上一段时间的。
》其中呢,我们刚刚验证了三个知识点,第一个,是关于listen的第二个参数含义,它的第二个参数呢,是代表我们底层全连接队列的长度,你设为n,最终就是n+1长度; 第二个,我们服务端呢,别人连上我了,对方跟我通信的时候,对方主动关闭链接,我收到了关闭链接的请求,但是我没有调用close()关掉连接,那么我服务端会进入CLOSE_WAIT状态;第三个,在服务端和客户端连接建立成功后,双方完成四次挥手,主动断开链接的一方是要进入TIME_WAIT状态的。
》关于CLOSE_WAIT你要记住了,服务端和客户端建立好链接了,CLOSE_WAIT一直在你的服务器上存在的话,它也是要消耗你的资源,如果未来你发现你自己的写的服务器,差的时候#netstat,你的服务器上挂满了大量的CLOSE_WAIT,基本只有一种可能性,就是你写的网络服务器呢,你将文件描诉符获取上来了(链接),但是你应该没有调用close(),你没将文件描述符关掉,所以它的状态无法主动的四次挥手成功,所以最终就出现了这个问题。往后发现自己服务器越来越卡,你可以#netstat,看一下是不是挂满了大量的CLOSE_WAIT状态的链接。
》至于下一个,TIME_WAIT状态怎么解决?以及为什么要有它?还有listen()第二个参数,全连接队列的这么一个概念呢,我们下面再展开。
我们在很早之前写过一个TCP服务器,我们写的很简单,创建套接字,绑定,再进行监听没什么好说的,再下来就是进入循环,循环的时候,我们accept()获取新链接,将链接拿上来了,然后你可以连我。但是呢,我们说过,主动断开链接的一方要进入一个状态,叫做TIME_WAIT状态。我们用阿里云充当服务端,然后腾讯云充当客户端去连接,我们在服务端再开一窗口可以查看当前服务器的连接#netstat -ntp,是可以找到建立好的连接,是腾讯云连的。 我们说过,主动断开连接的一方呢会处于TIME_WAIT状态。我们现在是让服务端直接挂掉,也就是服务端主动断开链接,然后客户端呢,就发出了coonection closde也就是服务端关掉了。服务端关掉了呢,我们赶紧来查一下服务端的链接情况,我们可以看到客户端刚刚连接的状态呢就是TIME_WAIT,要等一会儿,不同系统有不同时长,但是在等的时候呢,我们是看到主动断开连接的一方是进入到了TIME_WAIT状态。
》然后此时呢,我们再启动我们的服务器./server 8080,启动失败,然后得到的退出码是2,我们查看源代码查到,是在bind()的地方出现失败。换而言之就相当于呢,在我们TIME_WAIT期间呢,你服务端主动关闭了,你在重启的时候呢,是无法立即重启的!你必须得等,等什么呢?我们再来查一下服务端的连接状况,#netstat -ntp,我们发现原先的客户端连接不见了,我们再来启动我们的服务器./server 8080,我们发现启动成功了!
》我们先来分析这里的现象。我们刚刚呢,是先让我们的服务端关闭,因为文件描诉符的生命周期是随进程的。虽然在代码里面没有close()但是我将进程杀掉了,那么操作系统终止杀掉进程时,底层会自动实现握手过程。就好比我们以前学文件,你把文件打开了,最后呢,你文件本来是想关掉的,但是你没关,但是你最后进程退出了,你操作系统会自动帮你关这个文件的,那么关闭的时候,就会自动帮我们发送FIN的,然后客户端是telnet,意识到你服务端关闭了,那么客户端也会关。但是呢我们是服务端先退出的,我们服务端维护的连接呢,最后进入到了TIME_WAIT状态,进入到TIME_WAIT状态呢,这就是我们TCP验证它状态的一个过程,我们确实也看到了这个现象。
》第二个就是,为什么要有TIME_WAIT状态呢?如果我们有CLOSE_WAIT状态呢,我也能理解,因为,你关了的话,我不关,那么这个链接一直要处于CLOSE_WAIT状态,这个能理解。但是TIME_WAIT呢,我主动断开,第一对挥手完成,然后对方给我FIN挥手的时候,我再ACK吗,我发出去ACK的时候,我已经完成了4次挥手,那么就是主动断开链接的一方已经完成了4次挥手,换句话说,4次挥手之后链接应该释放了呀,为什么要保持一个TIME_WAIT状态呢?主要原因还是一样,最后一个ACK是否被对方收到,我们是不确定的,如果最后一个ACK发出去了,势必会导致一个问题,如果这个ACK丢了呢 ?那么此时大家能够理解的一个点呢,就是,主动断开链接的一方,它发送出去了ACK,它认为自己四次挥手完了,但是ACK在路上还要花时间,服务端照样四次挥手没有完成,而我们的客户端可能会主动的先进入断开链接的环节,就是自己将链接释放了,释放了之后呢,对于服务端来讲呢,服务端没有收到ACK,那它是不是四次挥手没有完成呀,就要保持一段时间的链接保持的一段情况。这对于我们不太好,就相当于,我们断开链接时候,一旦ACK丢了,就要以异常情况下关闭我们的链接,这不太好,这是其一;其二,我们怎么去解决这个问题呢?我们让主动发起断开链接的一方,你在发送最后一个报文的时候,你先不要着急从TIME_WAIT到CLOSED关闭状态,你先等一等。你得尽量保证,这个ACK被对方收到,三次握手不是100%成功的,那么四次挥手也同样不是100%成的,所以呢,最后一个ACK被对方收到没,我们不确定,但是,如果在通常情况下,这个ACK,无论最终有没有丢失,我们只要等一会儿,我们就可以以间接的方式,得到它是否被对方收到的一种情况。什么间接方式呢?如果我今天的ACK丢了,那么对于接收方来讲,主动断开一方为什么要发送ACK呢,一定是对方曾经发送过FIN,你接收方在等的时候呢,ACK丢了接收方说,我给你发了个FIN,怎么这么长的时间没有对应的ACK呢?所以,对方大概率会进行FIN的超时重传。所以,在我们发送ACK一方的时间段内,如果我们时间设置合理,在特定的TIME_WAIT时间段内,如果我们认为,我们没有收到来自对方发过来的重传FIN,我就认为我的ACK被对方收到了。当然,有没有例外的情况呢?当然有,它把这种异常情况出现可能性大大减小了。这是其中一个理由,换而言之呢,我们在进行TIME_WAIT等待的时候,此时呢,没有消息就是最好的消息,如果在TIME_WAIT时间段内,我们又收到了FIN,就意识到,给对方的ACK丢了,那我就重传,否则在TIME_WAIT阶段没有消息就是最好的消息。
》其实在更宏观的角度呢,我们的网络在进行通信的时候,当你准备挥手的时候呢,有没有可能上层,曾经已经发出去的报文呢,也可能正在路上路由。我们举一个简单的例子,我们把刚刚的FIN和ACK呢,在路上的场景再扩展一下。比如说,我刚发完,我立马就close(),此时呢,就会出现,正常数据和FIN同时就在网络上存在了,尽管我们的TCP有按序到达,但是总是会出现FIN先被对方收到的情况 ,所以,在我们彻底关闭链接的情况下呢,我们一定要保证从左向右,从右向左两个方向上,曾经的历史数据在网络当中进行消散,说白了就是被对方尽可能的收到,当然,正常情况下,数据早就被对方收到了,都只是考虑到一些极端的情况。就如同刚刚的ACK丢失了,我们在TIME_WAIT状态下等,同样的,历史上在网络当中,我们历史发过来的正常的交互数据也可能在网络当中,所以,我等一段时间呢,就可以保证链接没有在彻底关闭的时候,保证历史数据也被双方TCP正常收到,这也是TIME_WAIT的意义。
》第三点,就是TIME_WAIT的时长问题,而TIME_WAIT时长呢,和我们之前的超时重传一样,网络的情况不一样,那么势必就决定了,一方传到另一方花费的时间一定是浮动的,后面我们还会讲流量控制和拥塞控制这样的概念,大家感受就会更深,所以对我们来讲呢,我们的数据从主机A到主机B,他们两个花费的时间肯定是浮动的,所以我们要有一个这样的时间,叫做,从我们A到B,或者B到A,其中我们要花费的最大时间,我们将其称之为MSL。也就是一个报文,从左向右,从右向左,它最大花费的一个时间叫做,MSL。比如说,我们所有的报文都花费一秒传送给对方,但是呢,我们经过一段时间发现,总有那么几个报文是两秒发过来的,这个两秒就是MSL最大传送时间。所以呢,我们的TIME_WAIT一般在等的时候呢,等待的时间基本都是2MSL,所以从左向右,从右向左,一来一回,也就是能够保证至少一个FIN和一个ACK,也就是至少能够保证从左向右,从右向左两个方向的数据能够尽可能的消散,所以呢,我们的TIME_WAIT时间,一般在设置的时候呢,都会被设置成2MSL。
》当然,这个2MSL大家可以理解成呢,一个是TCP,还有一个就是我们操作系统也能够去设置超时时间,虽然我们说是2MSL,但是一般的操作系统都会有一个自己的配置文件,它里面是会包含我们的TIME_WAIT时间的。大家明显可以看到一个问题,一来一回的时间呢,如果说是按照传送的时间设定的话,会比较短,基本上是ms毫秒级别,我们TCP呢,再网络当中设定呢,如果一来一回的时间是OK的,这样是最好的,但是一般配置文件不会选择这个时间,而是也有自己的配置时间,这个配置时间呢,一般都是在s秒级别的。
》为什么是2MSL呢,因为要保证双方的方向的数据都已经消散了,否则你服务器万一重启会收到一些迟到的数据,那么我们就有可能会reset对方的链接,所以,我们尽量的不要保证出现这个问题,第二个呢,就是保证ACK到达。这个是等待的问题。
》这个等待的时间呢,主动关闭的一方,要等待两个MSL,才能回到CLOSED状态。大家可以想象一下,最大段生成时间MSL呢,就是你在网络上存活的时间,你在网络上面存活的时间都是从左向右,从右向左能够发送的一个来回的时间,只不过呢,时间不好定,有他自己浮动的,也有系统自己配置的,一般我们都是直接用系统配置的。
》下面最重要的不是这个,我们已经看到TIME_WAIT的现象,TIME_WAIT存在的意义,我们也看到了,保证最后一个握手尽可能成功,第二个呢,网络当中存在的双方数据尽可能消散。
》还有一个什么问题呢,我们还发现了,如果我们此时是出于TIME_WAIT状态,那么我们在启动的时候就有可能会出现绑定bind()失败的问题。因为,当你主动断开链接的时候,我们的服务器最重要进入一个TIME_WAIT状态,一旦进入TIME_WAIT状态,虽然链接已经名存实亡,但是它依旧还是存在!所以,我们在绑定的时候呢,也就意味着,你要绑定IP和端口依旧要被占用,所以,此时一个不会再被使用的链接,依旧持有你的IP和port端口号,你的其他进程想绑定,那么系统就不允许你绑定了,这是我们操作系统的默认行为。
》所以这样又没有问题呢?答案是:有问答题的!大家可以想一下,假设双11淘宝服务器上面有10万个链接,再来了一个链接,前10万个链接呢都是正常连接的,来的这一个呢,成了压死骆驼的最后一根稻草,导致我们服务器直接崩溃,服务器崩溃本质是进程退出,你这最后一个来的链接不影响,影响的是前面10万个链接,前10万个链接,客户端没有退出,是你服务器先崩溃了,然后服务器就要进入主动关闭链接的流程,因为服务器崩溃了,对于前面10万个链接呢,相当于主动断开了,所以服务器上面会存在大量的TIME_WAIT状态。那么服务器崩溃了就崩溃了,赶紧重启嘛,那么你就要立即重启,服务端一旦崩溃就要立即重启,如果重启失败了,那就麻烦大了,当我们想立即重启的时候,我们面临这么一个问题,因为服务端挂满了大量的TIME_WAIT状态的链接,你想重启,对不起,bind()绑定失败,不让你重启,难道你给客户说,等1分钟就好吗?客户是不能忍的,在这一分钟内是没有交易的,会造成很大的经济损失。
》换而言之呢,我们面临的事实就是,我们对应的进行我们绑定bind()时候呢,我们自己崩溃了,确实是bind()绑定失败嘛,因为有TIME_WAIT状态的链接,那么我们怎么办呢?我们必须得让操作系统接受一件事情,即便我有TIME_WAIT,反正我们这端口也不会被使用了,因为他已经进入到了挥手环节了,我允许你在TIME_WAIT状态的一个已经准备退出链接,所对应的端口号呢,允许它被其他的进程所绑定bind(),从而达到让服务器可以立即重启的目的!所以,操作系统也提供了这样的接口,这个接口,我们叫做setsockopt()接口。
》sersockopt()接口很简单,说白了就是一个接口问题,服务问题。那怎么办呢?我们在这里认识一下接口。第一个参数,就是你要设置哪一个套接字的属性;第二个呢,这个level呢,我们一般设置成SOL_SOCKET;第三个参数iotname,就设置成SO_REUSEADDR;第四个参数,就是你想设置的值;第五个参数,就是你想要设置的长度。就这么一个函数,就可以完成,我们服务器在崩溃的时候,就能立马重启的功能。
》我们往后在写套接字的时候,把套接字创建好,把serscokopt()参数的选项呢,你也带上,参数设置好之后呢,就可以保证服务器出现异常的时候,可以立即重启。其实就是在操作系统底层设置一个判断嘛,它让不让你绑定,就是你当前端口有没有被占用嘛,所以这个函数的参数选项无非就是告诉他不需要判断嘛。
》我们还有一个话题呢,在我们继续往下谈之前呢,我觉得是时候给大家揭晓答案了。
我们经过前面的实验呢,我们也发现了,如果我们给listen()的第二个参数呢,设置为2,在不accept(),我们不要accept()的话,你只是将套接字创建好,让别人来连你,这个道理其实也说明一个问题,一个服务器创建好了,只要他设置套接字,然后设置sertsockopt()地址复用,然后bind(),监听listen(),那么其实这个服务器就已经可以被别人连接了,被别人连接的时候呢,即便你没有调用accept,底层的握手也已经完成了,所以,我们前面的到的结论呢就是,accpet并不参与三次握手。最重要的呢,是我们底层会自动给我们维护已经建立好的链接。所以,你经常会听到accept()获取链接,那它到底在干什么呢?它所谓的获取链接,是将底层已经建立好的任务呢,拿到我们的上层,让用户能够看到他,这就是accept()。
》我们前面也说了,如果我们设置成2,可以直接连接的套接字个数就是2+1=3个,即n+1个。listen()的第二个参数就是backlog:如果上层不进行accept,底层建立好的链接数是有上限的,backlog+1。那么现在的问题是,我们前面也说了,如果服务端来了很多的链接,我们服务端不进行accept(),那么操作系统呢,会在底层为我们维护一个链接队列,你调用的accept()呢,是从链接队列里面将链接拿到我们的上层,来进行处理的。我们当时也说backlog,代表的就是全连接队列的长度,把这个链接称为全连接,也就是处于我们对应的ESTABLISHED状态的链接个数,它叫做全连接。当然,大家也看到了,我们在做的时候呢,如果再来超过backlog个数的链接呢,那么该链接所处的状态呢,会是对应的SYN_RCVD代表的就是,处于一种,我已经收到了你的SYN,但是我不想做处理,我不想进入链接建立成功的状态,你在这里再等一等,这个链接呢,我们叫做半链接,我们后面会说。半链接,我们在全连接讲完之后,就了解一下。
》所谓的全连接呢,根据我们前面所讲,操作系统为了维护我们通信的过程呢,服务器端有操作系统要维护的各种链接,如果要想维护,那么就必须得先描述,再组织。所以,一个一个的链接呢,最终都是一个结构体对象,所以,你所谓的全连接对象呢,就相当于一个结构体对象,里面有状态变量,链接建立成功呢,状态就被设置成ESTABLISHED。所以,我们凡是ESTABLISHED状态的链接呢,把它的结构体对象呢,放到队列当中,就相当于让其进行排队了。下面的问题是,**为什么要有backlog呢?**也就是为什么要有这个链接数的问题,这是第一个。
》第二个,我们曾经在谈listen()的时候说过,listen()的第二个参数呢,一般不要太大,根据不同的场景呢进行不同的设定。有的设置成5,有的设置成10,当然不同的应用软件呢,它的backlog值是可以设置成不一样的,这也就是为什么将这个参数暴露出来的原因。
》现在呢,我们知道,backlog,一、不能没有,你必须得有;二、就是不能太长。现在的问题就是,为什么它不能没有,又不能太长呢?我们要从计算机角度理解,不太好理解,下面给大家举一个生活当中的例子来帮助大家理解一下这个链接的问题blacklog。
》不知道大家有没吃过和见过海底捞,海底捞生意特别好的时候呢,会进行排队。那么海底捞为什么要用户去排队呢?用户呢在放店门口,工作人员说,我们饭店内部呢,已经满载了,没有桌子了,你们要吃饭吗?用户说,是的,我要吃饭。如果工作人员这样说,对不起,我们的桌子已经满了,你们去别家吃吧。那么此时用户肯定就走了。当用户带着几十个朋友走了,走了之后呢,这个餐厅可能有段时间没人进入餐厅吃饭了,但是又有4、5桌客人离桌了,但是因为没有外部及时补充上来的人呢,就可能导致离桌的这些客人当中,剩下的桌子,本来是可以让客户去吃饭的,但是现在呢,桌子就出现了闲置的状态。虽然没做过生意,但大家知道,一家餐厅的桌子使用率是100%,那么生意是最好的。如果去了5次,桌子有几次没人,那么说明生意不怎么样。如果工作人员呢,在自己的店门口,不让用户进行排队,那么此时店内有离桌的情况呢,那就无法立马补充上新的人,那么就会造成生意损失。所以,海底捞工作人员很聪明,它说我现在的生意火爆,我店内总是爆满,门外又有客人,不想让他们走,所以工作人员呢,在门外摆了很多的桌椅,然后给想来店里吃饭的人说,对不起,人已经满了,不过您可以在店门口排队。有没有不愿意等的,有呀,但一定会有愿意的。所以,最终就会出现在店门口有一大堆排队的人。这些排队的人呢,最大的意义就在于,如果里面有人离桌了,那么我们的工作人员呢,就可以在排队的人里面大喊一声,15号该你们用餐了,那么15号的人就可以进行吃饭了。这是不是带来了非常重要的好处,那么是什么好处呢?
》我们先回答第一个问题,**为什么需要排队?可以让我们的服务器在有闲置的情况下,上层从底层去拿链接,进行连接处理。**我们的第一个问题很好回答,就是为什么要有这个对应的全连接呢?很简单,我们应用层是要将底层的链接accpet()上去的,那么,当你的服务器上面有闲置的资源了,已经开始休眠了, 那么此时,你的accept(),底层没有链接的时候,那你服务器就只能干等,但是当你处理服务的时候呢,同时又来了链接,而且你的服务器没有让链接流失,而是将其留下来,当我们上层一旦有我们对应的,我们称之为,任务处理完了,那么accept()立马就能从底层直接获取链接,直接获取链接呢,就能让我们的资源呢拿到上层进行处理,说白了,排队的过程呢,本质上就是一种池化的技术。我们把链接呢,也进行池化,叫做链接池,当你紧急着上层有对应的任务处理完,就能立马拿到新的链接,立马进行处理。所以,我们回答了,为什么要排队的问题。
》接下来进入第二层理机,为什么不能太长?有同学一听,这挺好的,我现在也能理解为什么要排队了,因为,服务器处理业务很火爆的时候,再来新的链接,就会被拒绝,你总不能一直拒绝客户,万一拒绝多了,很长时间,客户不来,你将业务处理完,你不火爆了,你想获取链接,对不起,底层没有链接,你就得等,一等,服务器资源不就浪费了嘛。互联网公司恨不得服务器随时随地被爆满。我们知道,这样就只能排队,那么有人说,既然要排队,那么我海底捞将桌子绕商场一周,排到二环、三环等,把桌椅板凳排满整个商场,你要吃发,那你排队吧。那么,换而言之,我将队列长度设置的非常长,对我海底捞有影响吗?事实上,目前看起来不影响,因为,你该服务还是服务。这个队列可以设置的很长吗?通过我刚刚的极端例子想想,你把桌椅排到马路上,人家愿意吗?商场愿意吗?如果别人不愿意,你还要摆,是不是要给别人钱,花更多的成本,这是其一;其二,本来买20张桌椅是小钱,但是买2000张桌子就是大钱了,这是其二;换而言之,我们摆了这么多桌椅,最终一定意味着海底涝店一定要付出更多的成本。更重要的是,当我作为,一个吃饭的客户,我看到前面排了1000多人,我还会不会排队呢?答案是:我根本就不会,因为我知道,等轮到我已经饿的差不多了。所以,无论是从服务端角度,还是客户端角度,我们可以得到这样的结论,队列太长会严重影响客户体验,那么我登上某些服务,超过一定时间的时候,客户是不愿意等的,一般人说等个7、8s秒,但是短视频刷多了,基本上3-5秒都不相等,链接就直接关了。所以,链接的队列不能设置的太长!更重要的理由,从系统的角度上,我整个计算机,整个服务器,可用的硬件资源是确定的,多大内存,多大CPU是确定,你现在把队列维护的那么长,让用户去那里排队,为什么不把你的队列设置短一点呢?让节省出来的资源,尽快对外提供服务呢。这个道理,就好比,你这个海底捞,你花了五十万买了好几千张桌椅,你为什么不拿着这50万,把你的店面扩大一下呢?让你的店面具有更大的吞吐能力呢。所以呢,我们的队列不能太长,根本原因,你如果太长的话,会占用更多的资源,在服务器效率并没有怎么显著提升的前提条件下,让用户的体验反而变得越来越不好,所以,这个链接呢,我们尽量不太长,原因就在于,把节省出来的长链接资源呢,让我们的服务器内部可以去申请,可以去使用,今儿增大我们服务器的效率,换而言之,如果链接队列太长,一定会影响我们服务器的本身服务能力。**所以为什么不能太长呢?太长影响客户体验,太长归于占用我们的系统资源,导致服务器的效率低下。**所以,它维护不怎么长,也不怎么短的队列,对我们的服务端呢,影响不大,并且呢,维护这个队列的时候,维护的,可以理解成,维护的都是我们忠实粉丝。 所以呢,有时候我们连某些网站,有时候连的的上,有时候连不上,容易连上的,一定是那些一直连的人。我队列长度就只维护20个,你上层来不及accept(),那么我底层最多维护20个,你再多来的链接,我不处理,我拒绝你,我不担心,因为我上层一旦处理完了,我可以从底层当中20个里面拿到上层。在我正在处理这20个当中的某些链接的时候,后续还有不断的再来,所以,我就维护这么一个小小的缓冲池,就可以保证我们服务器在满载的情况下,一直满载,这就是listen的第二个参数。
》我们来总结一下,然后就进入下一个话题,滑动窗口。至此呢,我们把三次握手喝四次挥手,CLOSE_WAIT状态、TIME_WAIT状态都搞定,并且我们还了解了listend()的第二个参数。listen()第二个参数,维护的是我们全连接队列的长度,该队列不能太长,也不能没有。不能没有,原因在于,我们必须得保证,服务器想要获取链接的时候,立马就能有链接,不要让服务器出现想要获取链接,却没有对应的情况,不要让服务器出现闲置的情况,这是第一。当然你服务器访问量不大,你维护不维护问题都不大。第二个,为什么不能太长呢?太长,一、影响用户体验;二、太长的话没有意义,因为你维护太长的链接,你维护的成本,占用的内存资源,倒不如腾出来给我服务器有更多的资源对外提供服务,增大服务器的吞吐量。所以,不能太长,也不能没有。
下面我们来谈下一个话题,关于滑动窗口的谈法呢,我们需要的储备相关知识,其实我们都已经有了,只不过呢,我们先把课件里面的东西简单的说一下,然后我们再来谈一谈,TCP的缓冲区问题。下面我们来谈谈TCP的滑动窗口。
》我们要理解滑动窗口呢,我们首先得从确认应答作为切入点来理解。一般而言,我们主机A给主机B发消息,我们主机B收到一个报文之后,就得对主机A进行ACK确认应答。那么,换句话说呢,如果我们按照之前的认知呢,确认应答就相当于,主机A给主机B发个消息,主机B就给一个应答,发一个,给一个应答,那么整个发送过程呢,只能是串型的。也就是主机A发一个呢,不发第二个,收到第一个报文的应答之后,然后才开始发第二个,这样一来一回,一来一回,我们就能保证100%的从主机A到主机B,主机A到主机B的一个通信的可靠性。但是呢,这个做法非常机械,而且呢从我们现在理解上来看的话,它的效率非常非常低,因为主机A每次发送报文的过程,都是串型的发送。那么,实际上TCP是不是采用这种方式呢?答案是:TCP是用了确认应答的机制的思想,但是并没有采用,发一个然后必须不能发第二个,得等到第一个ACK收到了,才能发第二个。并不是这干的,而是怎么做的呢?
》既然上面一发一收的方式呢,性能太低了,那么我们呢,其实一次可以给我们主机B,塞满大量的报文,给他多塞一点数据,那么此时,我们的效率不就提高了吗。因为以前要发送4个报文,时间是串型的,现在我要发四个报文,那么时间就是并行的,相当于它们并行的使用网络的功能,发送到对方。那么对方接收的话呢,也能按照接受一批,响应一批的方式,这样不就可以了吗。它多个发送数据的时间段呢,确认应答时间都重叠了,那么效率不就高了嘛。此时TCP真正发送方式呢,采用的是这用策略。换而言之,实际上我们发送的时候,是可以发一批的。
》当我说,主机A可以给主机B发送一批数据的时候呢, 我们应该立即能够想到的是,我主机A怎么知道主机B的接收能力的呢?如果,主机A给主机B发送的数据量太大了,你不是一次发一批吗,我一批的上限是多少,如果我一次给你发送10G,主机B扛不住了怎么办呢?所以,我们这里在说的时候呢,主机A给主机B发送大量的数据,前提条件是,我们要保证主机B来得及接收,能做到吗?能!因为我们TCP是有流量控制,主机B会通告接收能力。所以主机A呢,可以根据主机B的接收能力,来向主机B直接发送数据。暂时,我们不考虑主机B来不及接收数据的问题,也不会存在这样的问题。
》我们现在面临的问题是,主机A可以向主机B一次发送一批数据了,所以,你现在再看主机A发的每一个报文都需要携带序号有多么的重要,一旦我们可以一次发送很多的数据,必须得每一个报文都带上序号,要不然主机B无法进行区分,无法进行常规的ACK了。当然,从图上可以看到,主机A呢,发送了4个报文,其实主机B应该也是要发送4个ACK的,所以,理论上每一个报文都会有一个ACK,这里要注意。但是,我们一会儿讲的时候,会发现ACK部分丢失也会没问题哈。
》下面呢,我们找一个切入点,说一下主机A的发送过程。当我们明白刚刚所说的之后呢,发送方的发送缓冲区,接收方的接收缓冲区是一对,那么客户端,一次就可以给对方发送大量的数据了。根据,我们以前所讲,你发送的数据呢,其实是应用层给你拷贝下来的,是由TCP决定,什么时候发,给你发多少,出错了怎么办的问题,所以TCP叫做传输控制协议。现在的问题是,我们的客户端一次可以给每一个报文带上序号之后,可以给对方发消息了。我们曾经谈过一个话题,如果一个报文丢失了怎么办?如果两个报文丢失了怎么办?如果全部丢失了怎么办?你这次不是发一个报文了,你发一个报文丢失了,我没有ACK,你再超时重传,再把这一个报文发给你不就行了。可是,你现在可是一次发一批,一批可能是10个。有2个或者5个丢了, 有这么多的报文丢了,那你怎么办呀?还能怎么办,那就超时重传呗。因为,我给你发的所有报文,你都得给我ACK,我一定可以通过确认序号,判定哪些报文丢了,或者从哪个开始丢的,那我就一定可以超时重传对不对。
》好嘛。现在就存在一个问题,我们把数据已经发出,在你得知他已经丢包的时候,这段时间内,有一个检测它丢包的超时重传的窗口内,超时了的话,你是能够重传,那么在这窗口之内,意味着你把数据一发出,那么对不起,你在发送缓冲区对应的数据,不能把它立马清除,而是要暂时将其保存起来。不知道有没有听懂我的问题,我再说一遍,当你把数据发出去的时候,因为你要支持超时重传,什么意思呢,你发出去的一个报文,在未来的一个时间点可能丢,那么也就意味着,在你识别到,收到对方的确认之前,或者确认它丢包之前,你得一直把数据保存在特定的内存区域当中,那么以支持我们超时重传,这个太重要了。我把数据发出去了,有可能丢包,我只有把数据发出去了, 过一段时间,我们才能得知是否发送成功,要么发送ACK成功了,要么超时了。但是,在我还没有收到结果的时候,这批数据得暂时保存起来,以支持我们超时重传,这个链路呢,应该是很好理解的,我们早就之前说过的。
》那么下面的问题就是,你已经发出,但是还没有得到确切答案,你这个数据被临时保存在缓冲区里面,那是保存在哪里了呢?以前我们学习窗口,学习接收缓冲区,一个报头当中的窗口大小,谈的是对方的接收缓冲区,那么我们今天要谈的就是发送方的发送缓冲区。那么,其中,我们的数据呢,必须得暂存在,发送方的发送缓冲区。我们说一下,**发送出去的数据,在没有得到“答案”的情况下,必须被保留,以便支持超时重传。那么这里的问题来了,你这里说的“答案”是什么呢?你把数据发出去了,在没有得到的时候,数据必须得被保存起来,以便于支持超时重传,那么“答案”是什么呢?1.发送成功;2.发送失败。**发送成功了,说白了,就是收到ACK了,另一个发送失败了,那么也就意味着,这个数据丢包了,你得重传一下。
》换句话说呢,那么其中,我们把数据暂时保留起来了,在得到结果的情况下,根据发送成功和发送失败,来决定是否将这个数据丢弃,还是将数据重新发送。
》下一个,那么保留,**又保留到哪里呢?我们要将有可能重传的数据保留在发送缓冲区中。**也就是说,我们的数据呢,是从应用层拷贝下来的数据,放到了缓冲区里面,发送缓冲区里面呢,那么一些已经发送,但是还没有收到ACK的报文,得暂时保留起来。
》所以,根据我现在给大家分析的结论,所以我们想看一看,发送方的发送缓冲区,它应该至少有几部分构成呢?其中呢,我们可以定义一段我们的数据区域,这一批区域的内部,代表的是什么呢?叫做,已经发送,但是还没有得到响应结果的区域。就是允许你直接发送,但是我们最终还没有对应的响应结果,就是,有没有发成功,我们还不清楚,这是一部分区域。第二部分区域呢,代表的就是,已经发送&&收到确认应答。也就是说,这一部分区域就是已经发送,并且收到了确认应答的区域。还有一部分区域呢,当然可能缓冲区里面还有,没有数据的区域,但是我们现在不考虑,没有数据的区域。所以,剩下的一部分区域,就是,待发送区域。也就说呢,我们根据上面所说呢,你是需要在发送缓冲区里面,要把已经发出去的数据暂时保留的,那么我们要清楚的知道呢,在我们的发送缓冲区里面,一定要有一段区域呢,把已经发送但是还没有收到应答的数据呢保存在某一部分区域。但同时也就意味着,还有当时我们曾经已经得到确认的和还没有发送的区域。这就是我们一个关于发送缓冲区的内存布局的一个问题。说白了,我可什么也没有说,滑动窗口啥的,我还没有说呢,只是想告诉大家,我们当前呢,发送缓冲区的结构,一定是类似于这种结构的。
接下来我们要做的下一个工作就是正式介绍我们的一个概念,就是,滑动窗口。
》为了能够更好的支持,第一个,高性能的发送,能够一次性发送大量的数据;第二个,当数据发送不成功的时候,要支持我们超时重传,还有各种的延迟应答等其他策略,所以,我们TCP呢,有一种策略,叫做滑动窗口。
·操作系统内核为了维护这个滑动窗口, 需要开辟 发送缓冲区 来记录当前还有哪些数据没有应答; 只有确
认应答过的数据, 才能从缓冲区删掉;
》说白了就是,我们的缓冲区可以被设计成一种结构,其中左侧代表已经发送且收到应答,最右侧是准备发送的数据,当然还有一点是没有被占满的空间,中间的白色格子部分是暂时不需要应答,可以立马发送的数据区域,或者说,可以没有收到ACK的情况下,直接发送的区域,也就是说,作为发送方,这部分区域可以以数据报的方式给对方发送过去。
》接下来,我们将白格子的这部分区域发出去了,一旦这部分数据全部发出去,那么这部分数据呢,就是我们已经发送但是还没有得到响应结果的区域。当我们后续,不断的收到报文的时候,比如我发的报文,第一个序号是2000,第二个是3000…,那么对方给我响应的话呢,对应的序号呢就是2001、3001…因为发送方给了ACK是2001,那就证明你发送方发的1000-2000的报文我收到了,收到之后怎么办呢? 那么我们就可以让我们当前的窗口呢,向右移动。我们依次呢,就可以不断确认,不断向右移动窗口,我们将这种窗口就叫做滑动窗口。概念是这么个概念,当然,里面有相当多的问题,我们一个个来。
》第一个问题,比如说现在发送的是一个1001-2000的报文,刚好是1000个字节,所以序号是1001-2000,主机A发送了4个报文,我们挑出1001-2000这么一个报文为例子,然后接收方进行确认,确认应答给的编号呢,就是2001。2001的话,我们发送方就收到了1001-2000的确认,所以将窗口呢,进行向右移动,代表的就是,空白格的区域依旧是可以发送数据的,空白区域左侧的部分代表的是,已经发送且收到确认应答的数据。截止到目前,这个也是大部分书本讲的概念。因为,如果想要彻底的弄清楚,是要理解各种各样的情况。图片和书上,只能这么给你一讲,还有很多我们没有想明白的地方。
》其实说白了就是,我们发送数据的时候,我们的发送缓冲区,有一个部分区域呢,是允许你直接把数据发送到对方,是没有收到ACK确认,可以直接发送的内容。我将这批数据全部扔出去,那么对端就会给我应答,只要给我应答,符合1001-2000,我就将这部分区域的窗口向右侧移动,收到ACK我就向右移动一次,这个倒没有什么太难的理解,我们也能够理解窗口向右移动。
》下面我的第一个问题就是,如果就这么讲呢,没多大意思,**1.如何理解缓冲区和滑动窗口?**你说这里的窗口向右移动就移动吗?凭什么呢?你这个缓冲区究竟是什么,你怎么做到使其向右滑动这样的功能呢?如何理解它呢?有同学会有这样的疑问,就按照你这样的说法,就向右移动,那么你这缓冲区是有大小的,你一直向右移动,但到最后不会出现溢出的问题吗?最后溢出的话,怎么办呢?相当于你滑动窗口到了发送缓冲区之外的区域了,不就溢出和越界了吗,但到没问题吗?
》首先回答第一个问题,缓冲区的理解。同学们,不要人家画一个抽象的图,就按照抽象去理解,这个缓冲区,大家将其当成char类型的大数组,char sendBuffer[161024];反正我不管,我将你当成一个大数组。所以呢,就有了,我们拷贝到缓冲区里面的数据呢,都是字节流,按顺序拷贝的,并且每一个字节呢,都天然的带有一个编号,每一个字节都有,这个编号呢,我们就称作数组的下标,那我们是不是很容易的去理解它了,它就是一个char类型的缓冲区,这是第一层理解;第二层理解,既然它是一个缓冲区,那我又如何限定,哪些区域是什么,另一些区域又是什么呢?很简单,我们可以定义两个指针,int start_index;int end_index;那么在我们的数组当中呢,就有这么两个指针指向我们发送数据,但没有收到ACK的这么一个区域。我这里将其叫做指针,你们也知道,指针式char类型,但是今天呢,这里就是相当于是数组的下标嘛,用下标指向的位置,我们也叫做指针了。其中呢,无外乎你所给我说的,滑动窗口的这个窗口,本质上就是由两整数维护的一个起始位置和结束位置,这部分由start和end限定的区域,就称之为滑动窗口,所以滑动窗口就是由两个整数维护,这是第二层理解。第三层理解,所谓的滑动窗口整体向右移动的本质,就是让我们的start和end进行+=某些值,即start+=x;end+=x;所以,所谓的滑动窗口向右滑动,代表的就是两个下标,两个指针递增的过程,这就是滑动窗口进行右移的过程。所以不要担心这个滑动窗口有多复杂,我就将你当作一个char类型的数组,所以就叫做字节流嘛,从左向右移动,所以,你上面说的将缓冲区划分成这么多的区域,本质上呢,滑动窗口就是发送缓冲区由一个起始位置和结束位置维护,不就可以了嘛。
》当滑动窗口整体向右移动呢,你这个线性的数组,是不是最终就有可能会出现,万一你的滑动窗口出现所谓的越界情况,那么是不是就搞不定了,所以,滑动窗口会不会越界呢?我们要说一下,TCP的发送缓冲区其实是被设计成为环状结构的!请问,一个数组,是怎么被设计成为环状结构呀?说白了,就是当它的start和end下标不断向右移动的时候,我们可以通过取模运算保证,它的窗口信息,在逻辑上是不越界的。所以,发到最后呢,start和end会出现,start在右边,end在左边的情况,但是,因为它在逻辑上是被设计成环状的结构,所以,它也不会出现,因为不断向右移动,出现缓冲区越界和溢出的问题,所以这个不用担心。
》我们下面再来谈第二组问题,**2.滑动窗口,一定会向右移动吗?就好比所有的教材都会告诉你,当我们发送数据的时候,窗口里面数据,可以不收到ACK,就可以直接给对方发送,当我们收到对应的ACK报文的时候,窗口就可以向右滑动了,这是真的吗?这是其一;其二,是滑动窗口固定大小吗?可以变大吗?可以缩小吗?难道一定是向右移动吗?有人说,会不会向左移动呢,那肯定不会的,不会向左移动的,因为左边的都是已经发送且收到ACK的。
》首先,我们要回答这些问题,我们就不得不面临下一个问题,就是这个滑动窗口的大小由谁决定呢?**你先别考虑向右滑动和变大变小的问题,你先告诉我,假设图中灰色部分的区域都是上层拷贝下来的数据,那么我的滑动窗口要给对方发送数据,这个滑动窗口呢,是暂时不用收到ACK确认,可以立马直接发送给对方的数据。那么,我的问题是,这个滑动窗口的大小由谁来决定呢?我再给对方发消息的时候,我一次可以对方发多少消息,就决定了,滑动窗口的大小。比如说,我的发送缓冲区有1M数据,可能对方的接收能力只有1Kb,所以,即便你的发送缓冲区拷贝了1Mb数据,你也最多一次只能给对方发送1Kb的数据。想一想,滑动窗口是为了提高我们发送效率的问题,但是唯效率论的,也就是说呢,它发送出去的数据呢,前提条件是保证对端主机能够来得及接收,我一次给你发那么多数据,你都瘦不下,那么我的滑动窗口还有什么意义呢?所以,滑动窗口的总大小,**目前呢,我们可以理解,它的大小一般是由对方的接收能力决定的!对方的接收能力是什么呢?是我收到的TCP数据报头中的16位窗口大小字段!!换而言之呢,目前我窗口大小是由对方接收能力决定的,当我给对方发消息的时候,对方给我ACK,它会通告我,它的TCP当中能够告诉我窗口大小,那么是不是就是告诉我们的它的接收能力!理想情况下,我一次可以给对方发送多少,是不是由对方的接收能力决定的。也就是说呢,我发送方有很多的数据,而发送缓冲区意味着,我一次可以向对方塞多少的数据,那么我窗口的大小是不是最多是对方接收缓冲区的剩余大小呀,那么它决定了我们滑动窗口的大小。
》所以,对端的现在的接收能力是4Kb,我发送方的滑动窗口大小也是4Kb,然后我一次给对方送了3Kb,对不起,对方的上层不取数据,所以我的滑动窗口在不断的给对方消息的时候,我们发送方收到的应答当中,我们刚刚发了3Kb,那么应答的时候就告诉我们发送方,它的接收能力只剩下1Kb了呀,那么请问,我们的滑动窗口会不会向右移动呢?再说一遍,什么意思呢?意思就是,我们就基于对方的接收能力,我们来看一看,其中,对我们来讲呢,我们滑动窗口大小呢是4Kb,对方现在接收能力大小也是4Kb,我将例子推向极端,我给对方一次发送了4Kb数据,但是对端的上层根本就不取数据,缓冲区被打满了,所以,对端给我ACK的报文中携带的接收能力就是0了呀,那么接收方给ACK的时候,我们发送方的滑动窗口是怎么移动的呢?窗口会不会向右移动呢?你想想,当我们进行我们对应的数据发送的时候,我们可以想象一下,我给对方发送了4Kb数据,但是对方上层就是不取,它给我通告它的接收缓冲区剩余大小是0,是0Kb的话,说白了就是给我们发送方说,你别给我发数据了,你发过来的数据,我还没来及处理呢,所以此时我们的窗口滑动是怎么滑动的呢?它是这么滑动的,它的右侧指针end_index根本就不动,它是收到端一个ACK确认,就将窗口向右移动,即start_index移动,如果,接收方一次给的是4Kb确认,那么直接就是start_index向右移动到end_index,代表的就是发送窗口为0,那么也就意味着,我们无法向对方再直接发送消息了,这就叫做,停止发送,而间接的就是根本没有向右滑动,所以,我们的滑动窗口一定会向右滑动吗?答案是:不一定!因为发送缓冲区的滑动窗口此时是衡量对方的接收能力的话,如果对方的接收能力没有增大,反而越来越小,那么其中只有我们左侧的start_index下标位置不断的进行确认,而end_index位置已没有移动,这样的情况存在吗?答案是:存在!
》大家要记住,我们滑动窗口的大小,目前是由对方的接收能力决定的,我们收到了TCP数据报头当中的窗口大小,如果对方一直说,自己的接收能力为0,此时你就没办法发了。我们举一下课件里面的例子,假设我给对方发送了4个报文,编号分别为1000-2000;2000-3000…4000-5000我给对方一次塞过去了,对方给我的ACK是这个样子的,第一个ACK是2001,它通告我的窗口大小,本来是4000个字节,但是它给我通告的窗口大小是3000,所以,换而言之呢,我们的start_index位置向右移动,因为告诉我们的窗口大小是3000,所以,我右侧的end-index不动,此时接收方再来ACK的话,是3001,那么左侧的start_index指针向右移动到3001的位置,并且告诉我们接收能力时2000,那么是不是end_index也还是不需要移动呀,此后接收方继续给我们ACK,但是对方的上层一直不取数据,那么最后搞诉我们它的窗口大小是0了,那么此时我们的start_index和end_index重合了,他们指向的同一个位置,代表的就是窗口大小为0,不能发送了。过了一会儿,接收方的上层一次取走了8Kb,接收方给我们来了一个报文,通告它的接收能力时8Kb。那么,当我们的发送方收到了这个报文呢,我该怎么办呢?那我立马就想到,这货能收数据了, 此时,我就拿着我的end+=8Kb,就扩展出来了一个8Kb大小的滑动窗口呀!就可以再进行发送了。所以,如果在进行通信的时候,接收方一开始说接收能力是4Kb,我发送方给它发送了4Kb大小的报文,那么接收方给我们更新通告它的接收能力是16Kb,那么此时我们的发送方滑动窗口就变成了,不一定向右移动,它可以变大,也可以变小!
》也就是说呢,这个滑动窗口呢,正常情况下向右移动,这个向右移动呢,指的是对方的接收能力一直比较稳定,我再给对方发,对方也一直在取,但是如果对方不取了,那么我们的滑动窗口呢,不一定会向右滑动,有可能滑动窗口会减小,也可能滑动窗口直接增大。所谓的变小呢,就是我们的start_index向后移动,end_start不变;变大的话,就是end_index向右移动。 下面的问题再继续,我们知道了原理,往后一步,我们要考虑的就是,我们知道了对方的接收能力呢,是跟缓冲区各方面有关系的,那么我们写一个伪代码。
》请问,当我们实际上向右滑动的时候,我们会收到各种ACK报文,请问start_index是如何向右移动的,start看什么数据,end看什么数据?我们后续滑动窗口会接收到对应的TCP报文的时候,它一定会接收到各种各样的TCP的各种ACK确认,这个确认,说白了不就是收到TCP报文的内容嘛。现在的问题就是,我们的报头当中的哪些字段会影响我们的start,哪些又会影响我们的end呢?也就是说呢,我们现在已经知道,滑动窗口向右进行滑动,其实说白了就是,我们的start和end两指针向后移动,也就是下标在向后移动,移动到末尾,通过模运算,保证是环状结构就行了。再下来,它一定会向右滑动吗?不一定,和对方的接收能力有关,有可能对方的接收大小为0的时候呢,那么对不起,我没办法再接收了,对应的滑动窗口大小就是0了。当我滑动窗口增大的时候,你又变的end指针一定是要向右移动的。所以,滑动窗口可以变大,也可以变小,不一定非得向右移动。当然只要滑动窗口变大,肯定是得向左,因为,我们无法向左滑动。
》所以,当我们知道了,滑动窗口大小由谁来决定和滑动窗口的特点之后呢,不仅可以向右滑动,还可以变大变小。下面的问题是,那么我作为TCP发送方,我收到了一个报文,请问这里的start和end下标是怎么去更新的呢?看哪些字段呢?
》我们先考虑正常情况,接下来我们要进行我们对应的,你可以理解成接收到报文的时候,这个start和end下标怎么去更新呢?其实人家做的肯定更完善,我们只是写一些伪代码来进行理解。比如说,你现在发了一个报文,你这里有若干个报文同时发出去了,发出之后,我们应该怎么去设置start和end呢?很简单,这里的start呢对应的,因为每一个报文都有自己的序号,代表的是你下次从哪里发,下次从哪发不就已经告诉你答案了吗,那么当你收到ACK的时候,start_index在更新的时候,只能够start_index=确认序号。本来,我们的start=1000,当它收到的ACK是2000 的时候,那么这个start=确认序号,就直接指向2000的位置了,这是第一个;第二个,我们的end怎么做呢?end和start之间呢,代表的是窗口大小,对方的接收能力是多少呢?不就是16位窗口大小的字段得出嘛,那么就是新的滑动窗口大小呢,就是要我们以对端的16位窗口大小为基准,那么,我们的end_index = start_index + 16位窗口大小。其中收到报文的时候,就可以根据这么简单的算法呢,将自己的下标不断的向后移动。如果收到的ACK报文呢,正常情况下,当然可能会有丢包的情况,我们先不考虑,现在的问题是,正常情况下,收到的确认序号不断的是递增,也就意味着start不断的向后移动,然后,如果通告的窗口大小呢,它也在不断增大呢,那么end也就在不断增大,如果接收方的接收能力为0了,那么滑动窗口就是start = end了嘛,此时两个指向同一个位置,代表的就是滑动窗口为0,那么就是不发了。后面接收方再来ACK,再同步过来接收方的接收能力,确认序号肯定还是start指向的位置,然后根据发来的16位窗口大小,就更新end_index的位置,那么接着可以发送数据了。当然正儿八经做的时候,肯定不是这么简单的,这样讲很好理解罢了。
》下来再来个大家回答一个,你们肯定会有一个问题。你说的挺好,我们发送报文的时候呢, 我们在学习滑动窗口的时候,势必会面临一个问题,你一次发送了4个报文,理想情况就是4次ACK,然后滑动窗口依次向右移动,start下标增大,右侧的end根据ACK报文携带的16位窗口大小来决定向后移动,这个没问题,也很好理解,但如果中间的报文丢了呢?比如说,1000-2000, 2000-3000,3000-4000,发送了4个报文,一次1000个字节,最后ACK,对不起,2000-3000的报文丢了,1000-2000,3000-4000收到了。仔细听,我作为发送方主机A,我收到确认应答,我们的第一个和最后一个报文收到了,但是中间的报文丢失,也就是没有得到中间的ACK确认,那么怎么办呢?答案是,我们直接将窗口移动到4000!为什么呢?因为确认序号的含义就是,只要我们保证收到了4001的确认序号,那么就是代表的4001之前的所有数据,接收方已经全部收到了,尽管没有收到或者收不到2000-3000报文的ACK,但我可以确认,4001之前的全部收到了!因为,这个确认序号字段,是我们曾经规定好的协议,只有主机B真的收到了1、2、3、4这4个报文的时候,它才会响应确认序号是4001,所以,确认当中,丢包不丢包主要是看,有没有确认ACK嘛,2000-3000报文的ACK没有收到,没关系,我收到4001的确认序号,我照样一次将start向后移动到4001的位置。有人又说了,行,按照你的说法,我ACK的时候,2000-3000没收到,40001我收到了,行我还是正常情况start向右移动,全部越过,然后按照新的序号,从4001往后发送。那如果我是1001没收到,2001、3001、4001我收到了,那么1001没收到也不影响,只要收到2、3、4都是依旧可以向后移动。有同学又说了,如果我发的数据报,如果2000-3000的报文真的丢了,那么接收方在ACK的时候,只会给你ACK2001,即便收到了3000-4000的报文。也就是说,如果我们中间真的有一个报文丢失了,比如2000-3000的报文真丢了,最后呢,也不会存在ACK为3001的确认序号,此时我们的主机B即便收到了3000-4000,我的接收方也只会给你响应ACK的确认序号是2001。你以为确认序号是随便有的吗,这个确认序号的用处特别大,确认序号的设计也特别好。不管是响应还是真正的报文,只要接受方主机B真的收到了数据,它就会给你应答,应答当中呢,哪些ACK丢失不影响,我们就以最大的ACK的确认序号向右移动,如果是真的中间的2000-3000的报文真的丢了,即便主机B接收到2001之前的,3000-4000的,此时呢主机B也会只给你返回ACK的确认序号是2001。
》那么这个时候就进入到一个过程,主机A发送方就会发现,我给你发的4个报文,可是你主机B,为什么只给我ACK到2001呢?所以主机A的滑动窗口的start下标滑动到2001的位置,然后没有收到确认的就进入到超时重传的策略。什么意思呢,意思就是说,主机A给主机B发数据的时候,如果我发送的四个数据,哪怕是2000-3000的数据丢了,丢了之后呢,我们发送方的滑动窗口只会移动到接收方发来ACK确认序号2001位置,然后2001后面的数据就被留下来了,因为没收到确认所以滑动窗口要包含它们且不会移动,然后呢,我发送方会等,等超时,超时之后呢,我会把2001后面的数据再重发,所以这就是,为什么你把数据发出去了,不会立即给你清理掉,而是要等结果,收到ACK是一个结果,start_index下标右移,如果此时呢,中间有报文丢失了,因为ACK序号的原因,这个报文以及后续的ACK最多就只会ACK到2001,所以,我们在进行等待的时候呢,2001-3000的报文只能是在这里等待超时重传。超时重传成功与否呢,成功了的话,我们会收到ACK3001,然后再进行向右滑动,如果超时重传收不到ACK呢?那么此时就不是再发送的问题了,而是,如果重传了若干次,对方都没有给我响应,就证明对方崩溃或者出问题了,那么主机A发送方就要异常终止我们的链接。所以同学们,你不要担心丢包。再想想,你还担心那些报文丢失呢?有同学又说了,我担心的是2000-3000、3000-4000的报文丢失了,如果真没收到,主机B只收到1000-2000,2000-3000,那么怎么办呢?那么主机B给你的ACK只会写到1001,那么主机A立马就意识到,2001之后的报文都可能丢了,那么主机A就要对数据做重传,现阶段,我们只能够理解成,它会对2001后面的数据做重传,一会儿我们后面会补充的一个知识点呢,可以告诉大家,它其实可以定向的去重传。总之呢,因为序号的定义呢,只要收到了特定的报文,序号之前的全部收到了,才会ACK最大的确认序号+1返回给发送方。如果零零散散的收到后续的报文,不要担心,因为只要前面的报文收到了,就会ACK前面的报文,那么start下标就会移动,中间零零散散的没有收到,那么滑动窗口也不会向后移动的,只有收到了真正的ACK才会向右移动。
》我们现在所学这些很成熟的知识,最大的好处就是,如果我有疑问肯定是我没想明白,它这个协议呢,人家一定有标准答案,所以我们不用怀疑机制问题,一定是我们自己没想明白,所以,我们可以假设各种场景。如果,说我发了这么多的报文,如果我第一个报文丢了呢?那么主机B给我们ACK确认应答是多少呢?主机B就相当于一个也没有收到,即便收到了2000、3000、4000,最直白的就是,它给你ACK的确认序号还是你第一个报文之前的序号,或者他给你ACK1001,那么此时主机A就意识到,我给你发的是1001-2000往后的呀,你为什么给我ACK1001老的序号呢,那么就会意识到1001-2000数据丢了,总之有策略。最大的亮点就在于我们的“确认序号”字段上。如果我们ACK确认序号报文丢了,其实不怕,因为每一个报文都有ACK。数据丢失,只会确认最小的序号,最大的序号呢,我们一会儿会有方式帮助大家理解。稍后呢,我们还会基于滑动窗口以及数据呢,会再谈一下重传机制,到时候大家就会理解,它是怎么知道我们的那一个报文丢了。
》我们有了滑动窗口的理解之后呢,我们再来看,如果我们的数据报丢了, 如果是确认ACK数据报丢了的话,我们一点也不担心,有可能会有较大报文的确认序号被收到了,虽然较大之前的确认序号没有收到,但是根据确认序号的定义,它代表该序号之前的报文都收到了,然后主机A就知道了,对方数据全收到了,只不过之前的若干个确认序号丢了,那么它可以直接进行后续的操作。那么有人说,前面报文的确认序号收到了,后面报文的确认序号丢了呢?如果你只收到最大的确认序号是4001,那么就将start_index移到4001,然后后面的超时重传呗。
》现在呢,主机A给主机B发了一大堆消息,1-1000,1001-2000…6001-7000,一共7个报文,每个报文1000个字节,然后一次全部扔出去。扔出去之后呢,对不起,这次的丢的和上面我们说的丢的不一样了,上面丢的是ACK确认报文,这里丢的是发送方的数据报。如果数据报真的丢了的话,我们主机B会给主机A进行应答,应答的时候会填充,主机B收到的是1-1000,那么就会发送确认序号是1001,那么主机A就知道1001之前的都收到了。但是呢,主机A接下来发送出1001-2000,2001-3000的报文,主机B在进行响应的时候呢,那么此时给我们填充的确认序号依旧是1001,此时我们对应的主机B,3001-4000也是收到了,但是它不会给你确认4001,它只会给你填的确认序号依旧是1001。所以,主机A发送大量报文的时候,它会收到若干个确认序号相同的报文,主机A立马就意识到了,是我们的1001之后的报文有丢失,那么主机A呢,就开始补发1001-2000,再进行ACK,那么对端主机收到了1001-2000补发的数据报,那么数据齐了,那么接收方给你ACK报文中填的确认序号直接是7001,那么此时我们的主机A就可以继续发从7001往后的数据报了。所以,在你发送的众多的报文当中,有一个报文丢失了,后续报文呢,是会向对方连续填充上丢失报文的序号,然后让主机A尽快得知已经有一个报文丢失了,此时主机A就立即意识到是1001-2000的报文丢失了,那么就会立马补发,
》有同学又说了,如果我是丢失了两个,不是丢一个了,我是1001-2000、5001-6000丢了,那么此时怎么办呢?你也不要担心,此时主机B呢,再给对方响应,丢了1001-2000的数据报,该数据报前面的数据报ACK,包括该数据报后面的数据报ACK确认序号,填的全部是1001,在补发的时候,因为5001-6000丢了,在把1001-2000确认之后,根据我们的规则,它会把连续收到的报文,比如说,我们5001-6000丢了,那么就意味着,4001-5000收到了,所以主机A将1001-2000补发之后,那么确认序号就递增到5001,立马就能意识到我不是给你发到7000了吗,你怎么给我发来的ACK是5001呢,所以我们主机A可以对你剩下的报文做补发,总之,我们的主机A和主机B可以有方式,对我们的丢失报文进行甄别,乃至重传。
》有的同学很死板,他是这么想的,你图上文字写的是,主机A到主机B,收到3个同样的确认应答,即确认序号相同时则则进行重发。那行,此时1001-2000丢了,你后面还有很多的报文,那么此时2001之后的报文不断的给发送方主机A进行ACK嘛,如果2001之后的报文发过去的ACK没有满足三个填的是相同的确认序号2001呢?如果我就总共发送了3个报文,有一个报文丢了,只给你ACK了两次相同的确认序号 怎么办呢?不能怎么办,这个时候,超时重传策略就降级成为了普通的超时重传,就相当于主机A给主机B发送大量的数据,如果此时收到连续三个确认序号相同的ACK报文呢,我们就可以从三个以上相同的确认序号ACK报文来甄别出哪个报文丢失了,再补发该报文。但是呢,没有收到三个或以上的,没关系,我们降级成普通的超时重传就行了,就是将确认序号之后的报文都再重发一遍就行了,而不是单独发一个丢失的报文。另外,网络在通信的时候,它一定有很多预料之外的情况,所以,在网络当中呢,我们更多的是在概率上,我们主机A和主机B通过这样的发送尽快的提高我们的发送效率,并不代表每一次,可能收到两个ACK报文的确认序号相同的,没有收到3个或以上的,只能普通的超时重传,这个也不影响,但是你主机A和主机B通信的时候,报文不会是这么一点,我们实际上发过去的话会是十几二十个和上百个报文发送出去。一,在现在的网络里面,丢包的概率本来就不高;二,即便是丢包了,我们也能够很快的甄别出来,因为和我们一块儿发的报文本来就非常多。即便你丢了很多,反正大不了超时重传嘛。
》我们把数据报真正的丢失时,基于我们的滑动窗口,连续发送多个报文,然后当我们的主机B收到的时候,如果中间的报文丢了,接收方后续收到了非连续的报文呢,它进行ACK报文填充时,会填充上被丢失报文的序号,即确认序号,当然肯定是填充最小的那个报文的序号,当我们主机A收到ACK报文后呢,就会意识到时1001-2000的这么一个报文丢失了,那么就会立马补发这么一个报文。另外,在我补发的过程当中呢,我也可能发送后续的其他报文,课件虽然没画出来,只是不想把逻辑搞复杂,实际上滑动窗口特别大,你丢了1001-2000这部分,没关系,你再丢一个5000-6001也没关系,因为你这个时候,在你进行处理的时候呢,后续可能还在发报文,所以当你,5001-6000报文也丢了,在你该报文5001-6000之后的报文,需要发送ACK的时候,会填充上5001这么一个确认序号,则就让主机A甄别到5001-6000的报文丢失了。所以,不要静态想象成图上面就这么7个报文的数据,而是想象成一次发很多很多的数据报,因为对方的上层也在不断的取数据,所以,也就决定了,我们的滑动窗口可以一直发。
》我们把连续收到3个或以上的确认序号相同的报文,让发送方进行重传丢失报文的机制,我们称之为“快重传”!也叫做,“高速重发机制”。说白了,就是当你的数据丢失时,你一下子会发若干个报文,那么你发送方会收到若干个报文的ACK确认报文,那么发送方收到ACK确认报文,就可以进行,比如说ACK报文的确认序号写的是1001,那么会对我们的1001-2000进行重传的。因为重传之后呢,使用的我们3个连序报文来确认的,所以一般会被我们超时时间短很多,所以,它会提高效率,这一点要注意。
·当某一段报文段丢失之后, 发送端会一直收到 1001 这样的ACK, 就像是在提醒发送端 “我想要的是 1001”
一样;
·如果发送端主机连续三次收到了同样一个 “1001” 这样的应答, 就会将对应的数据 1001 - 2000 重新发送;
·这个时候接收端收到了 1001 之后, 再次返回的ACK就是7001了(因为2001 - 7000)接收端其实之前就已
经收到了, 被放到了接收端操作系统内核的接收缓冲区中。
》这种机制被称为 “高速重发控制”(也叫 “快重传”)。
》我们的滑动窗口全部讲完,基于滑动窗口,有这样一个概念之后,我们的主机A可以连续的向主机B发送大量的数据了,发送的上限是对方的接收能力,目前我们是认为,滑动窗口的窗口大小,代表的就是对方接收能力,一会儿还要再加一个,一会儿再说。现在我们也知道,一次并行给对方发送大量的数据,可能会出现丢包,但不影响,ACK丢失,会有更大的ACK确认序号能够帮助我们确认。如果真正的数据报中间丢失,我们滑动窗口不会跳过一些没有被确认的报文,而向后滑动,因为ACK确认序号规定了,没法跳过一些没有确认的序号。
》如果我们在数据传送的时候,有大量的数据过去的时候,出现丢失,历史上丢失的报文,会被该报文序号的后续报文,进行给发送方响应ACK报文的时候,填上丢失报文的序号作为确认序号,比如说1001-2000丢了,我们收到1-1000的ACK报文还有2001-3000之后的报文的ACK报文,它们的ACK报文的确认序号都会是1001,这就相当于给发送方响应大量的相同确认序号的ACK报文,那么发送方就会意识到要重发1001-2000的这个报文。会不会存在若干个不连续的报文丢失呢?1、2、3…7,丢的是2、4、6报文,那么可以吗?可以,要超时重传的就超时重传,满足快传条件的,就快传。因为,我们的滑动窗口保证了不会掠过没有得到确认的数据,那么最后每一个数据都有自己发送的超时时间,所以我们没有得到确认成功的数据,会暂时保存在滑动窗口当中。
》最后回答一个问题,我们如何理解,发送缓冲区发送完毕数据呢?**毫无疑问,现在我们清楚了,在我们滑动窗口当中呢,如果我们真的把数据发完了,此时我们的滑动窗口的左侧start_index向后移动,向后滑动的时候,也就意味着,这部分数据自然的就被淘汰了,就如同计算机传统的删除数据一般,它并不会对缓冲区的数据做清空,而是只要收到了ACK,就说明被对方,那么缓冲区的数据便就无意义了。无意义怎么办,滑动窗口向右移动,此时窗口的左侧数据代表废弃的数据,是可以被上层拷贝的数据进行覆盖,这就叫做,我们发送完数据之后,清空数据的本质就是让start和end向后移动,而不用对数据做任何的清空操作。
》如上就是我们关于滑动窗口、快重传的理解。
》我们暂停一下,我们在写套接字的时候,在上层我们通过调用一个send(),我们就认为我们把数据发出去了,今天你回过头,你在send()的时候,你以为你就发出了嘛?实际上我们也说,你在调用send()和write()的时候,其实就是把数据拷贝到你的发送缓冲区,说白了就是拷贝到没有被发送到内存区域,然后你再想一想,接下来,你将工作交给操作系统的时候,TCP为我们做了多少的工作,它既要考虑到数据报丢失的问题,还要考虑到乱序问题,还要考虑解决数据重复的问题,要能够支持重传,要能够知道对方的接收能力,你想一想,你就简简单单的send()了一下,拷贝给TCP之后,TCP为我们做了这么多的工作,所以,你学网络套接字的时候,你就光光的send()出去了,你就以为你很厉害了,实际上它究竟底层是怎么做的,能不能说清楚呢?在面试的时候,如果碰到真的懂行的面试官,那么就会问你这些问题。 问你这些问题的时候,你怎么去回答了,所以要注意了。
下面我们再来谈下一个话题,流量控制。我们再把上面的报头谈完,滑动窗口谈完,那么流量控制就非常简单了。所谓的流量控制呢,说白了就是,主机A给主机B发送消息时,我们不能给对方发送数据太多太快,而导致主机B上层来不及拿取,那怎么办呢?主机A给主机B发消息的时候呢,主机B要告诉主机A能够接受的量,所以,主机A和主机B在进行握手期间,就交互双方的,我们称之为窗口大小,也就是各自接收缓冲区剩余空间大小。主机A把自己的接收能力发给B,同样的主机B把自己的接收能力发给主机A,双方就能够进行正常的通信了。当然实际情况会稍微复杂一点,还有一些情况要说一下。因为这部分内容,原理上能理解了,我们就简单的过一下课件。
》接收端处理数据的速度是有限的. 如果发送端发的太快, 导致接收端的缓冲区被打满, 这个时候如果发送端继续发送,就会造成丢包, 继而引起丢包重传等等一系列连锁反应。
》因此TCP支持根据接收端的处理能力, 来决定发送端的发送速度. 这个机制就叫做流量控制(Flow Control);
·接收端将自己可以接收的缓冲区剩余大小放入 TCP 首部中的 “窗口大小” 字段, 通过ACK端通知发送端;
窗口大小字段越大, 说明网络的吞吐量越高;
·接收端一旦发现自己的缓冲区快满了, 就会将窗口大小设置成一个更小的值通知给发送端,其实就是0;
·发送端接受到这个窗口之后, 就会减慢自己的发送速度;
·如果接收端缓冲区满了, 就会将窗口置为0; 这时发送方不再发送数据, 但是需要定期发送一个窗口探测数据段, 使接收端把窗口大小告诉发送端。所谓的窗口探测呢,其实就是没有正文的,没有有效载荷的TCP报文,因为我给你发了一个报文,那么你主机B就要给我响应,那么你就会携带你的窗口大小,所以我发一个报文呢,你就要给我一个ACK响应报文,就通告我你的窗口大小,会发若干次,这是第一种策略;同时主机B也会定期去做,和主机A轮询的去发窗口探测是一样的,主机B的上层一旦取走数据,缓冲区剩余大小更新了,就会给主机A发送窗口更新通知,所以他们两个一旦出现接收缓冲区大小为0,它们两个是一个双向奔赴的策略,不是主机A单方面的去问,也不是主机B更新了才回去通知A,而是主机A会去问,然后主机B更新了,也会去说,这样的话,双方就能一最小时间成本,达成新的共识,然后继续进行发送。
》如上就是我们的流量控制。其实呢,流量控制是最简单,因为只要里面有字段,报头里面有属性,就很好理解。
下面就要进入第二个大的挑战,就是拥塞控制。要学TCP必学三大机制,分别叫做,滑动窗口、流量控制和拥塞控制。这三个话题是必学的。
》我们再来分析一下,当前我们具有什么问题?问你们一个问题,现在发送方有流量控制,能够保证向对方发送数据的时候不会出现所谓的,因为有流量控制呢,能够保证对方主机,按照你的接收能力给你定向的发送数据,不会给你多发,通过你告诉我你的窗口大小,我就知道你的接收能力,双方都是如此。
》第二个呢,我们还有确认应答,超时重传,序号。我们的确认应答能够保证数据能够100%被你收到,潜台词就是我能够确认被你收到了还是没有收到。没收到,我给你超时重传,收到了,你给我ACK。
》另外呢,因为有序号的存在可以给我们确认应答,可以支持我们的按序到达,也能够支持去重。
》我们上面的滑动窗口呢,就可以在流量控制的前提下,也就是你的接收能力前提下,设定我的滑动窗口的大小,同时在你的接收能力范围之内,尽快的,尽多的给你发送更多数据。所以,丢包问题,乱序问题,还有重传策略和效率等相关问题都解决了,
》所以,从我们的主机到主机的双方策略基本上都齐了,但是,你有没有发现你漏掉了一个非常重要的角色。你发送是考虑了对方的接收能力,你考虑到了给对方发报文丢包的问题,你考虑了数据报乱序到达的问题,你也考虑了效率问题,你也考虑了又重复报文要进行去重的问题,所以对方主机呢,可以很舒服的状态来进行接收各种数据了,可是呢,那你有没有发现你忽略了最重要的角色,你把数据给对方主机,其实并不是你把数据给对方主机,或者说,你不是直接把数据给对方主机,而是你把数据先交给网络。所以,你其实所有的这些机制在考虑的时候,好想这些策略直接都是考虑对方的感受,那我网络的感受谁来管呢?
》你看,一会儿流量控制,一会儿确认应答,一会儿超时重传,一会儿序号,一会儿去重,一会儿滑动窗口,既保证效率,还保证可靠性,保证你开开心心将数据拿到,你要是没有了空间接收数据了,我还不能给你发,我们一直都在考虑对方接收的问题,对方的情况,但是,你并不是将数据直接给对方呀,而是你把数据给了网络!难道,你在考虑对方的问题的时候,就不考虑网络的感受吗?这里是不是就有问题了?万一根本就不是对方的问题,而是网络的问题呢?那你这TCP的一对机制就没有什么用了呢?
》所以,我们TCP设计的好,就耗在这里,它不光考虑了对方的问题,还考虑到了中间路上的问题,也就是网络的问题。我们下面正式介绍一个概念,以一个例子为我们的切入点。有人会觉得很奇怪,你作为主机还要考虑网络,网络这么大,是你一个主机该考虑的问题吗?有人很奇怪,TCP为什么要考虑网络呢?我就是一台小小机器,网络里面有上千万台机器,我这么一个发送方还要操心网络的问题,想不明白,我们一会儿会说。
》下面我们举一个小例子。比如说,我们考C语言,在座的同学有100人,然后考试成绩出来了,有两名同学挂科了,这是谁的问题呢?100人考试,2个人挂科,这个比例肯定就是学生的问题了。那么今天去考C++,最后考下来,98个人都挂科了,这是谁的问题呢?同样的人去考试,C语言挂科是学生的问题,C++有98个人挂科,是谁的问题呢?很显然,挂两个人是学生的问题,98个人挂科了,那就是出卷老师的问题。你会发现,考试不合格的是你,但是根据数量的多少来决定锅是谁来背的,这是一个例子。我们下面再举一个例子:我今天发送方主机给对方发送了1000个报文,其中有两三个报文丢了,是谁的问题呢?是谁的问题不重要,从目前来看呢,是我们发送方或者接收方的问题,要么就是你接收方来不及接收就丢了。如果有1000个报文发出去,一两个报文丢了,不影响,因为作为我们的发送方呢觉得很正常,我们暂且认为是报文的错,大不了我进行快重传或者超时重传呗。但是,我今天在流量控制和滑动窗口、超时重传、确认序号、确认应答各种机制下,1000个报文里面,有999个报文丢了,这是谁的问题呢?毫无疑问,不是发送方和接收方的问题,我们发送方和接收方的策略能够保证可靠性,首先我发的报文不会超过对方的接收能力,因为有流量控制,但是为什么发1000个有999个丢了呢,那就只能是路上出了问题。所以,你发100个数据,丢一两个,那么网络没有问题,但是如果丢了大量的报文,那么我们就怀疑是网络的问题。
》换而言之,我们第一个问题先不谈,我们接下来要讲什么,我们要谈的**第一个问题就是如何确认网络出现问题!**因为,两端通信的主机,它们的可靠性机制相当完善,该做的都做了,包括我们没有讲的,其实更复杂。但是呢,我很清楚,尤其是我给对方发消息,它一定是可以来得及接收的,是不会给他乱发的,所以给对端发消息,出现极个别报文消失,我只会认为网络是正常的,也会认为对方是正常的,因为这是属于可靠控制范围之内,大不了块传或者超时重传嘛。但是出现了大面积的丢包,我们毫无疑问,有理由怀疑是网络出现了问题,所以,少量丢包,我们可以是正常现象,大量丢包的话,就认为是网络出问题。所以,我们可以根据网络出问题的一些指标,来衡量网络当前的健康状态。此时TCP为了能够更加方便的处理这方面的问题呢,就引入了,“拥塞控制”。网络出现问题,然后怎么办呢?
》就相当于,当我们出现丢包的情况呢,大量丢包了,那么就是网络出问题了,丢一两个,大不了重传呗,出现大量丢包,我们该怎么办呢?**能不能重传呢?答案毫无疑问,肯定是不能重传。我们现在的问题是,我们为什么不能立马重传?**以及,怎么办呢?
》有一个基本事实,网络这个东西和对端主机不一样,网络这个东西特别重要,因为接收数据的主机可不仅仅是只有一台。如果网络出现问题,我们先不考虑出什么问题,反正就是导致我丢包了,这是第一个;;第二,接入到这么多的主机,无论是手机、笔记本等,只要你用操作系统,无论是windows还是Linux等操作系统,你都要遵守我TCP/IP协议!如果网络出问题,你这个主机发了1000个报文,你识别到,你丢失了999个。此时别的主机也在发送数据,它们是不是从概率上讲一定也会有很多主机识别到网络出问题了,也就会出现丢包问题。因为网络被大家所共享,我发出现问题,那么其他的主机发数据也会出现问题,当然凡事不是绝对,有的主机不发,有的主机比较慢没有更新到和你完全一致,但是从概率上讲一定是相当大的主机和你遇到同样的问题。假设极端情况下,大量的主机都在向服务器发送数据,那么一旦网络出现问题,那么我们的报文就都丢了,如果我们现在遵守超时重传,所有的主机都进入超时重传的流程,时间一到,各自都补发,此时一瞬间网络当中又出现大量的数据,网络本来已经压力很大了,出现丢包问题了,已经很拥挤了,你们每一台主机又同时向网络当中塞数据,只会加重网络问题, 随意不能立即超时重传。所以,不要仅仅站在一台主机的角度去理解重传,如果某一台主机发送1000个报文不能重传,因为你在重传的时候,要将900多个报文重传一下,最后会导致网络压力变大,大家会非常奇怪 ,一台主机就1000个报文,丢了900多个,大不了将他们重传呗,900多个报文能有多大?话不能这么说,我们要有宏观的认识,一旦涉及到网络,那么发送方主机就不止一台了,所以,网络出现问题,那么大家都可能识别到了,重传的时候,可不是仅仅只有你一个主机在重传哦,如果TCP协议设置成,丢包的时候立即重传,只会加重网络的瘫痪程度,进而谁也传不了,怎么办呢?
》如果要解决的话,思路上很好理解。首先网络出现问题,是硬件上出现问题,比如说,路由器挂了一大批,那么你那些主机再怎么玩,对不起,都没用,你再怎么重传都不行,就跟挖掘机将网线挖断了,你再怎么重传都是无济于事。所以呢,我们要能够缓解这个问题呢,前提条件是,网络出现问题呢,仅仅是软件上出现问题,什么叫做软件出问题呢?就是网络当中已经出现了大量的报文,中间有些路由器积压了大量的数据,导致转发压力太大了,网络压力大也可能造成超时丢包的问题,有可能你的报文压根没有丢掉,只不过在特定的路由器 下排队呢,只不过呢,因为网络压力太大了,来不及转发导致超时,所以,我们把这种情况,只考虑网络软件方面的问题,TCP可能会判定网络发生了,“网络拥塞”问题。
》当我们知道了为什么,不能重传之外,我们接下来要解决的就是,如果这么多的主机出现了网络拥塞问题,那么正确的做法该是什么呢?我们虽然没有接触网络拥塞相关的算法。不发或者少发对不对,本来丢了9000多报文,网络出现问题了,你得网络一点时间,让网络积压的数据赶紧推送到不同的服务器当中。当网络出现拥塞的时候,这些主机呢,它要做的工作呢,最合理的是什么呢,是少发数据,让我们的网络缓一缓,让我们已经在网络当中的数据排一些出去,网络拥塞问题恢复了,我们再进行重新发送就可以了,所以这个时候我们要做的合法的工作呢,不是重发,而是减少发送的数据。不是不要你重传,而是将你要重传的900多个报文一次塞上一两个,然后慢慢增多,这样会好些。
》我们现在讨论的问题是,假设现在网络已经出现了拥塞,然后,怎么办的问题。TCP呢,虽然有滑动窗口这样的大杀器,用高校可靠方式,主要是用滑动窗口+流量控制这两个,组合呢,使得能够在安全的情况下,向对方发送大量的数据。如果,发送的时候出现了大量的丢包问题呢,我们服务器就判定出现了网络拥塞的问题,一旦出现网络拥塞呢,这是TCP协议识别到了,你能识别到,别的机器也能识别到,所以在一个共识的时间点内呢,不能立即全部重传,否则只会导致网络更加严重,那么怎么办呢?现在假设网络已经出现了拥塞,那么我们在不清楚网络当前的情况,贸然的发送大量的数据,只会雪上加霜,所以TCP,引入了“慢启动”机制,说白了就是,识别到网络拥塞,所有的主机不要超时重传,而是大家都开始发送少量的数据,比如发一个报文探探路,如果一个报文发出去,一点反应都没有,那我就再等等,过一会儿我就再发一个数据,只要我发了一个数据,它给我响应了,然后,我再尝试发两个数据,再给我相应,我们就逐渐递增数据量,慢慢的就能达到我们发送的正常要求了。有人说,你一个人这样有什么用呢?你这台机器一旦识别到了网络拥塞了,网络当中的其他主机都能识别到,所以,不是你一个主机慢慢的这样发送,而是网络内的所有主机都是这样的共识,都慢慢发,所以网络一旦出现拥塞,那么网络入口流量呢,就会瞬间减少,给了我们网络充足的时间,进行它所对应的数据推送和数据路由,将数据推送到指定的服务器端,这样就能让网络缓一口气,大家发送的数据是慢慢的增多多,所以,网络也就慢慢的缓口气了。这样就是我们,发送少量数据,慢慢增多的做法。
》我们把此处,出现拥塞之后呢,解决此问题的做法叫做,“慢启动”机制。也就是,一旦出现了网络拥塞,那么所有的TCP都会进行慢启动机制,然后进行数据发送。此处呢,我们就要在引入一个概念了,这个概念称作,“拥塞窗口”。拥塞窗口没有体现在报头当中,它相当于TCP链接当中的一个属性字段,这个拥塞窗口代表的含义就是,这个窗口发送数据量以内,不会出现拥塞,超过了,可能会导致网络拥塞问题,这就叫做,拥塞窗口。
·此处引入一个概念程为拥塞窗口
·发送开始的时候, 定义拥塞窗口大小为1;
·每次收到一个ACK应答, 拥塞窗口加1;
·每次发送数据包的时候, 将拥塞窗口和接收端主机反馈的窗口大小做比较, 取较小的值作为实际发送的窗
口;
》拥塞窗口,就是主机A和主机B在网络当中发送数据的时候,超过窗口大小就可能发生拥塞,在大小以内就不会出现拥塞,这是从概率上讲的,谁知道主机有没有问题,而导致我们出现问题呢。问一个问题,所以,我们发送方一次向对方发送数据的时候,一次最多能发送多少数据呀?我们今天发送数据的时候,考虑的可不那么简单了,不仅仅是对端的接收能力,我们对应的发送方呢,发送的时候,一次最多可以向网络里面发送多少数据呢?从我们目前可以得知,发送的时候,一次向目标主机发送的数据量=min(对方的接收能力(这是我们没有考虑问题),拥塞窗口大小),其中一次向目标主机发送的数据量不就是滑动窗口的大小嘛,所以滑动窗口大小=min(接收方的窗口大小,拥塞窗口)。所以,我们有三个窗口的概念,一个是滑动窗口、拥塞窗口、接收端的窗口大小。所以,修正一下我们的end_Inde,end_index = start_index + min(窗口大小,拥塞窗口);这样既很好的控制了对方的接收能力,有很好的控制了网络的情况,相当于,网络特别好的时候,拥塞窗口就特别大,就以对方的接收能力为主;网络特别差的时候,就以网络为主。
》更重要的是,每一个主机都要遵守这样的规则,所以一旦网络出现拥塞问题了,都要以网络为主,都要触发TCP的慢启动机制。根据我们现在所谈呢,我们又有一个问题,你刚刚说过,当网络出现问题,主机A向主机B发消息,一旦出现了网络拥塞问题,你说过,此时发送数据的量是,一旦收到了ACK,就发送刚刚数据的2倍,再收到ACK,又是前面数据的2倍,好吧,你好的慢启动呢?你不说是指数级增长吗?指数级增长,你能说它是,慢吗?下面给大家讲一个故事,帮助大家理解一下;以前有加农户,欠了地主很多钱,地主想要整一整农户,你的房租地租我不要了,你第一天给我1粒米,天数的不同呢,就是前一天粒数的2倍就可以了,农户觉得看起来不多,也就答应了,可是过了一个月之后,基本上就是还不起了。我们关注的是,为什么农奴会答应呢?
》指数增长的前期是非常慢的。其实我觉得设计拥塞控制的人,是一个非常牛逼的人,虽然这个很简单,但是很有效。因为指数增长的前期是非常慢的,所以叫做慢启动,但是随着长期增长,报文数就变得越来越多,势必会导致下一次的网络拥塞,难道我们网络就是在不断的拥塞和恢复过程当中吗?那这个网络是不是太震荡了,所以,我们并不是这样的,所以,我们就又定义了一个新的东西,前期慢启动是为了一个两个慢慢的发报文,因为指数级增长的前期特别慢,一旦过了某一个极点就会增长的特别快,但是呢,我们既想用指数级增长的前期慢特点,我又想要用它高增长性,为什么?稍后再说,现在为了不让它增长那么快,不能只是一味的让拥塞窗口加倍,其实单纯的加倍也不怕,为社么呢?因为发送主机除了受拥塞窗口大小影响外,还是会受到对方主机接收能力的影响,换句话说,你的拥塞窗口变得特别大,但是对方接收能力也会约束我不会发特别多,但是能想一下吗,拥塞窗口设置的特别大,那么就是形同虚设,我们在全网主机向目标服务器发送,都是全量发送的,这是最理想的情况,但是网络资源同主机相比,永远都是稀缺资源。总之呢,我们不想让拥塞窗口单纯的加倍,此处呢,我们就引入了,慢启动的阈值,就是设定了一个上限值,当拥塞窗口以指数增长,超过了这个阈值时,就不能指数增长了,变成线性增长方式。就好比,第一次出现网络拥塞后,我们的指数增长变成线性增长的阈值设定为16,随着后面的线性增长,不考虑对方接收能力,再多的数据,对方都能收,16变成了24,此时又出现了网络拥塞,那么我们就要再进行慢启动,说白了就是又恢复成从发送1个报文然后指数增长,更重要的是,当我们发生了一次网络拥塞,我们还做了一个工作,就是重新计算新的停止指数增长的阈值,是上次拥塞窗口的一半,然后变成线性增长。所以,出现网络拥塞,做两个工作,首先将发送数据的量变成起始位置1,然后指数级增长,但是呢,从指数级变成线性增长的阈值需要重新计算,是上一次指数到线性的阈值的一半。所以对我们来讲,先是指数增长,然后阈值是上一次阈值的一半,然后线性增长。我们将这个算法呢,叫做“拥塞控制”算法。
》现在解决一些困惑。第一个,为什么要用指数增长呢?指数增长前期很慢,意味着前期都可以发送少量的数据,符合慢启动机制;还有一个理由,过了某一临界点,增长速度快。想一想,网络出现拥塞,你最想做什么呢?我们想做的,当然是尽快恢复!所以呢,除了你前期非常慢,你发送少量的数据是为了探探路,检测一下网络,如果我前期发送的一个、二个、四个等对方都给我ACK了,我们现在要考虑的就是,要尽快的进行网络恢复。我已经,探过路了,一个、二个、四个、八个等都给我成功的ACK数据了,说明我们的网络没有问题了,此时我就应该尽快恢复,所以,我们会发现一个问题,指数增长一方面要考虑,让我们的网络缓一缓,另一方面还要考虑,当你缓过来了,我们要尽快的恢复网络通信的正常速度。所以,指数级增长刚好前期增长慢,而它的增长曲率又非常快,当我们前期慢点阶段一过,我们就能尽快的让我们的网络恢复到正常通信水准,所以采用了指数急增长!我们也能够理解,为什么不能一直指数级增长呢?一直指数级增长,让我么的拥塞窗口变得太大,也就没有意义了,我们要让其始终处于一个合理的范围之内,所以此时,当他指数级增长达到某一个阈值的时候,就让其进行正常的线性增长。
》拥塞控制完了吗?还没有,还差一点点,有同学会说,我们在进行传输的时候,难道我们对应的拥塞窗口是一直增大吗,即便是线性增长也是在增大呀。如果你网络的设计者,拥塞窗口一直在增大,你此时会怎么考虑?大家一定要记住,理论上我们进行网络通信的时候,如果你传输的轮次变得越来越多,你每一次能够发送更多的报文的时候,是不是网络变得越来越好了,那么拥塞窗口增大是不 是变得合理,但是从此往后,我恢复了正常通信,我给对方发送数据的时候,已经趋于稳定,我给对方一次性发送数据大小也就16KB,那么网络拥塞的窗口还需要增大吗?其实是不用的,所以大家不要担心拥塞窗口一直增大的问题,增大到一定程度,在未来的时间点呢,要么再出现拥塞情况,然后重新增大;要么,增大到一定程度么,在发数据的时候,不再由拥塞窗口决定了,而是由对方接收能力决定了,那么就不用再增大了,即便是一直增大也没关系,因为出现网络拥塞,它会自己调整。
》以上就是网络拥塞的内容
下面我们进入“延迟应答”话题。当我嫩进行数据报发送的时候,发送方主机向我们的接收方主机发送消息,因为,我一次能像对方发送多少数据,是由我的滑动窗口决定的,换而言之,如果我的滑动窗口越大,意味着发送的效率就越高,也就是能够发送的数据量越大。因为网络问题,我们作为单主机控制不了,所以,暂时不考虑网络拥塞的问题。在不考虑网络拥塞问题的下,我们的滑动窗口越大,那是不是我们的发送效率越高,再往本质去说呢,就是我们对端主机接收能力越强,我们的发送效率越高,因为对端主机接收能力越强,剩余空间大小非长度,那么我们发送方可以根据对方剩余空间能力设置窗口大小,所以对方接收能力越强,我发送越多,效率也就越高。我们如何保证让对方的接收能力越强呢?就变成了,如何让接收方的剩余空间大小变得更大的问题,怎么做呢?
》因为不断有人在打消息,同样,就有不断的有人在取数据。我们接收方有自己的接收缓冲区,发送方发数据,网络拿到数据交给接收方的缓冲区,我们也说了,如果我的滑动窗口越大,那么我的发送效率就越高,滑动窗口大小又由对端接收能力决定,换而言之,将问题转化成了,如何让我们的接收方具有更大的剩余空间,怎么做呢?我作为发送方不断的向缓冲区里面放数据,只会让缓冲区的剩余空间越来越小,而我们上层是拿数据的。也就是我们以前自己写的TCP套接字,自己调用的read()、recv()接口读取数据的时候,说白了就是你把数据取走,你把数据取走呢,就是腾出了更多的剩余空间,换而言之,我们网络的整体发送效率问题呢,在根上就变成了上层能不能尽快取走数据的问题。
》这里有一个完整的理解链的。我们要发送的更多,是由滑动窗口决定的,滑动窗口是由对方的接收能力决定,对方的接收能力,说白了就是剩余空间的大小,说白了就是,上层要把数据快速取走,那么我们的缓冲区剩余空间就会变大,只要让上层尽快取走,那么我们进行发送的时候,就可以更新出更大的滑动窗口进行发送数据了。现在的问题是,你如何让上层尽快的取走数据呢?
》我们接下来看一个策略,叫做,”延迟应答“。如果我们作为接收端的主机,收到了一些报文,立即给出ACK应答,那么这时候,我们返回的窗口会比较小,但是呢,如果我接收方收到了数据,先不着急给你ACK,我等一会儿,我在等什么呢?我在等上层可能有一定的概率,在这个时候,把数据取走,所以我再给对方ACK的时候,我填的窗口大小也就能够更大了,我们通过这样的等一等的策略,就可能让我们的TCP发送效率变得比之前更高。
·假设接收端缓冲区为1M. 一次收到了500K的数据; 如果立刻应答, 返回的窗口就是500K;
·但实际上可能处理端处理的速度很快, 10ms之内就把500K数据从缓冲区消费掉了;
·在这种情况下, 接收端处理还远没有达到自己的极限, 即使窗口再放大一些, 也能处理过来;
·如果接收端稍微等一会再应答, 比如等待200ms再应答, 那么这个时候返回的窗口大小就是1M。对比之前我给对方说,我的窗口大小是500K,那么对方的发送策略可能就不一样了,对方可能之前给你发500K,现在就变成了1M了。
》所以,我们就可以通过等一等,收到一个报文,不着急给对方立即确认,这种策略,我们称之为延迟应答。所以,我们窗口越大,传输数据越多,那么传送效率也就越高,所以呢,在保证我们网络不拥堵的情况下,尽快的提高传输效率。我相信大家也能理解的一个比较重要的点是,我们在发送数据的时候,也要考虑网络拥塞的问题呀,但是,无论你有没有延迟应答,你都要考虑网络拥塞问题,只不过有了延迟应答,在一定的概率上,可以在正常通信的时候,可以一次让对方向我发送更多的数据。网络也是IO,如果今天要向磁盘写10M数据,分10次去写,进行10次IO,和一次写完,进行1次IO,它们的效率相差是特别大的。磁盘已经很慢了,网络比磁盘更慢,所以,一次IO就能够把大块数据传过来,速度是肉眼可见的提升,这一点要注意!
》现在的问题变成了第二点,我已经知道要等一会儿再给他延迟,那么我必然能够提高效率,但是这里就有下一个问题,我应该等多长时间呢?延迟时间太短了,达不到延迟应答想要的效率。太长的话,就会导致对方认为数据报丢失,从而误认为要进行重传,反而得不偿失,那么怎么办呢?所以,我们的延迟应答必须得设计的比较合理 ,如何设计呢?通常有两种策略,第一种,我们每个N个报文进行应答一次,一般是2个报文哈。比如说,我收到了一个报文,我不嫌着急给你ACK,当我收到了两个报文的时候,我才给你进行ACK,我甚至ACK的时候,将这两个报文的ACK合成一次,因为你的确认序号就代表了我的ACK情况,所以之前有一个报文,我没有给ACK也没有关系,每两个报文,给你一次ACK,这就是一种变相的延迟;第二个呢,就是时间限制,一旦超过最大的延迟时间呢,我们就应答一次,但是这个时间呢,完完全全的跟操作系统有关,但是我们可以理解的就是呢,这个最大的延迟时间不会超过我们最大的超时重传的时间,这点要注意。我们操作系统一般用的是数量的方式,这种方式更加简单,一般数量N取2.
下一个就是TCP为了提高效率,它还有一种做法,但是这种做法呢,我在以前都直接和间接的都给大家说过了,但是今天呢,我们正式提出来。
》比如说呢,我们以前发报文是怎么发的呢,我们是发一个数据,对方给我一个确认。发一个数据呢,我们可以理解成,携带了报头和有效载荷的数据,然后收方主机B给发送方确认,它只是确认报文,也就是它只给我发了有用的报头,即ACK标记位置1,我们发一个数据,对方给我一个确认,发一个,对方给我一个确认。但是呢,这种方式呢,对方给我们响应的时候,只对上一个报文做确认,它没错,但是响应本身没有携带有效信息,也会导致我们的效率太低。
》所以,在正常通信的时候,TCP不是这样通信的,你给我发了消息,我在给你做确认的时候呢,我也可以给你携带我想发给你的数据。所以,当我们发送一个数据,对方在给我ACK的时候,也可以给ACK报文本身携带数据。这种策略呢,就叫做捎带应答,捎带应答呢是数据通信的真相,并不是说发一个ACK就完了,而是在将ACK标记置1的时候,也可以携带有效载荷。首先,在TCP当中能做到吗?能,因为一个ACK,确认报文,其实说白了TCP报头,就将ACK标记位置1,当然确认序号也要填,如果我们携带数据了,我们既然携带了有效载荷,我们的序号也用上,这样我们的通信效率也就更高了。
》如上就是我们的TCP工作流程,我们花一点时间,把我们目前学到的TCP策略呢复盘一下,将之前的内容一总结,回过头看一下TCP常见的三个问题。
我们的TCP特性呢,无非就是两套特性,第一套呢,叫做可靠性,第二套呢,叫做效率性。所以,我们经常会说,TCP是为了解决可靠性的,那是因为,它的主要特征是为了解决可靠性,但是往往被人们忽略的是它的效率提升的策略。那么,TCP为了保证可靠性和提高效率呢,做了哪些工作呢,我们一个个说一下。
》我们先来谈一谈可靠性。TCP的报头是有校验和字段的,当然校验和我们没有作为重点去讲,知道有校验和就行了。一旦校验和失败呢,那么数据报就直接被丢弃了,被丢弃怕不怕呢?我们也不怕,我们一旦丢包了,我们大不了就重传嘛。校验和是保证数据不会出现偏差,比如bit位翻转的问题。
》第二个呢,就是序号机制,有了序号机制呢,我们就能够按序到达,也能够进行一定程度的重传,它也是我们确认应答的基础。
》第三个呢,是确认应答,这个是可靠性机制最核心的。只有你理解了确认应答,以及理解了长距离传输可靠性的方式,才算是TCP入了门。TCO的确认应答机制,对于确认来讲呢,没有任何意义,但是对收到的人呢,对它的最大意义就是,我能保证该序号之前的报文,对方都收到了。确认应答是一个非常可靠的策略。
》第四个呢,就是超时重传。当丢包了,我们可以超时重传,超时重传我们也知道,我们后面为了提高效率呢,为了更高效的进行我们可靠性的保证呢,我们也有所谓的快重传,但是有人会说,既然有了快重传,但是为什么又有超时重传呢?因为,你不用担心,快重传是有要求的,就是收到连续三个或以上的同确认序号的确认报文,这样的话,如果只收到了一个或两个呢?那么,我们的超时重传是为了兜底的。在我们无法判定是否丢包的情况呢,我们根据超时的时间问题来进行对报文进行重传,所以有超时重传完全不会错。
》第五个,连接管理。TCP在通信的时候,要进行三次握手和四次挥手。三次握手呢,SYN、ACK+SYN、ACK,双方状态也会发生变化,通信完毕,再进行四次挥手。
》第六个,流量控制呢,属于可靠性范畴。首先,没有流量控制呢,我们是可以向对方发送大量的数据,而有了流量控制,就可以动态的让我们去发送数据量,不要超过对方的接收能力,进而导致丢包问题。当然,流量控制呢,在你发多了会控制你,在你发少了,也会控制你,所以流量控制呢,也会帮助我们提高效率的。
》在下来呢,就是拥塞控制。它能够保证我们的数据报不在网络当中丢失。
》所以,你看到的可靠性的常见几种机制,再下来还有一些机制呢,是来提高效率的。,当然也会有一定程度上横跨可靠性,但是我们将其划到提高效率的方面。
》比如说,滑动窗口。它的存在呢,它可以让我们并发式的向对方发送大量的数据,而不用收到ACK。
》下一个就是,快重传,就是丢包之后,收到三个或以上的确认序号相同的报文,然后将丢失的报文进行重传。
》再下来就是延迟应答和捎带应答。
》所以TCP当中,上面的如此之多的策略呢,对我们来讲呢,它也就是相当于,在我们的TCP内部自主实现的。有些特征是体现在报头的,有些没有体现在报头,当然还有其他的没讲。
》如上就是我们的TCP内容,下面给大家解决下一个问题,叫做,面向字节流。
面向字节流的问题呢,就要给大家重点谈一谈,关于字节流的理解问题。字节流的理解问题呢,我们也写过TCP的套接字代码,我们在创建套接字的时候,我们是基于我们所对应的sock_string流式套接来进行通信的。我们以前呢,也确确实实在说这个字节流,我们也没有正式去谈过,下面我们正式说了。
》因为之前有一定的讲解,甚至在代码里面呢,也有一定的体现,所以,再给大家讲的时候,我们先将课件里面的文字念完。然后我们通过字节流问题,引出下一个问题。
》创建一个TCP的socket, 同时在内核中创建一个 发送缓冲区 和一个 接收缓冲区;
·调用write时, 数据会先写入发送缓冲区中;也就是,你调用write()的时候,你是将数据拷贝到发送缓冲区的,你并没有将数据发出去,send()也一样的。之前也说了,send()和write()不叫做发送函数,而是拷贝函数。
·如果发送的字节数太长, 会被拆分成多个TCP的数据包发出;这句话,很耐人寻味,这个拆分是由谁来做呢?是用户来做吗?是我应用层调用拷贝的人来做吗?你以前做过没?你没有做过,你也不会做这个工作。你只需要将你的数据能拷就拷下来,你根本就不关心数据什么时候发,发多少的问题。所以,用户拷贝进来的数据太大,怎么去拆,拆多少,拆成若干个报文后,最后怎么发,丢包怎么办,这些问题全部都是由TCP自主决定的。关于把TCP报文拆解的问题呢,我们要具体结合下层协议才能看,但是现在呢,常识告诉我们,你现在拷贝了1M的数据到发送缓冲区里面,我TCP不一定能够立马将数据发出去,因为我要考虑对方的接收能力,以及路上的网络拥塞问题。所以,你把数据给我,发多少由我来决定,这就是TCP的作用。
·如果发送的字节数太短, 就会先在缓冲区里等待,延迟应答一下, 等到缓冲区长度差不多了, 或者其他合适的时机发送出去;因为一次构建一个报文,要花20个字节报头,但是发1字节的有效载荷,是不是效率太低了,我等一会儿,等上层积累了足够多的数据,然后发送缓冲区的数据差不多了,我再把我们的数据刷新到网络当中。这点和我们之前讲的缓冲区和文件,有异曲同工之妙。
·接收数据的时候, 数据也是从网卡驱动程序到达内核的接收缓冲区;五秒钟思考一下,发送数据时,一定是用户把数据拷贝给操作系统,TCP再根据网卡驱动,把数据交给网卡,然后再由网卡把数据发出去。那么接收的时候,又是谁先接收到的呢?我目前直播,会被采集很多的画面,头像等等,你在家里面呢,收到了我的声音,这个收到我声音数据的,是谁先收到呢?答案是:一定是先被你机器的网卡收到。为什么一定是被你的网卡先收到呢?原因很简单,因为底层一定是硬件先工作,这是其一;其二,冯诺伊曼体系结构规定,外设/输入设备先收到数据,然后再通过一定的方式,让操作系统把数据拷贝到内存,拷贝到操作系统内部的接收缓冲区当中。
》第二个问题给大家说一下,我的网卡当中收到了数据,它又是怎么让操作系统知道,网卡里面有数据了呢?从而让操作系统把数据从网卡硬件,通过网卡驱动程序,拿到操作系统的TCP接收缓冲区呢?是如何做到的呢?其实就如同,你今天在你的电脑旁边,你按键盘的时候,那是怎么知道你按键盘了呢?又怎么知道你按的是几呢?然后把你的数据拷贝到可执行程序当中,怎么做到的呢?答案是:中断。也就是硬件中断!当网卡收到数据时,网卡是可以通过和CPU直接相连的中断,发送中断信号,然后CPU识别到外设有就绪,然后就可以强制让CPU执行中断的上下文程序,然后调度操作系统的拷贝功能,将数据从底层拷贝上来,和我们对应的键盘是一摸一样的原理。中断会涉及到外设、针脚等,我们要只知道的就是,我们硬件收到数据,是可以让操作系统收到的。所以,当我们接收的时候呢,我们的数据可以通过硬件中断的方式,让我们的操作系统知道了,立即从底层读取数据,然后就可以解包了,然后向上交付,到达TCP。
·然后应用程序可以调用read从接收缓冲区拿数据;
·另一方面, TCP的一个连接, 既有发送缓冲区, 也有接收缓冲区, 那么对于这一个连接, 既可以读数据, 也可
以写数据. 这个概念叫做 全双工。
》我们把工作流程谈完了,下面我们要谈的是,TCP面向字节流该如何理解呢?这个就很好理解了。当发送方,从上层向发送缓冲区拷贝1字节或多字节时,可能TCP没有发,也可能TCP立马发,你应用层尽管拷,我TCP呢,按照自己的节奏发,我可能把100字节的数据给你封装成100个TCP报文,每一个报文只携带1字节有效数据发出去;我也可能100字节,一次一个报文给你发出去。也就是说呢,我上层所谓的调用write(),把write()调用1次和100次,每次只写一个1字节,向我们的缓冲区里面拷,此时呢,就叫做,写入的时候与写入的格式没有关系。而当我们读取的时候呢,底层给我们收到了若干个字节,你读的时候,可以一次读一个字节,也可以一次读两个字节,一次10个字节等,那么这与格式也毫不相关,这就叫做,读取时是面向字节流的。换而言之,在网络通信,基于TCP通信的时候,发送,怎么发?接收,怎么收呢?发送的时候,write()调用几次,接收的时候,read()函数调用几次,两个毫无关系,你写你的,我读我的,这就叫做面向字节流。
》以我们现在的能力去理解,面向字节流,成本并不高,因为曾经在写网络版本计算器的时候,我们在读取的时候,我们单独弄了一个自定义协议,其中呢,一开始的部分代表的是有效载荷的长度,然后是/r/n再是有效载荷,我们当时为什么要这么做呢?因为,我们得先读取出报头的有效长度字段,然后根据长度,读取有效载荷,我们为什么要这么干呢?主要原因是,TCP没有一个完整的报文边界,读取呢,是由我们读取方自由读取的,所以,你必须得保证读取出正确数据。那么这里呢,必须得衍生出另一个问题,TCP是面向字节流,它不关心数据的格式,它发随便发,我收,按照我自己的节奏去收。但是,我们发过来的,可能是网络版本计算器协议字段,可能是一个http/https的报文,可能是两个、十个等,虽然TCP不关心报文和报文之间的边界,但是应用层必须得关心。
》所以,我们得带出一句话,**TCP是面向字节流的,它不关心任何的数据格式,**也就是说,它收到的任何数据,在他看来,全都是二进制或者就是以字节为单位的字节流,不关心任何格式,**但是要正确使用这个数据,必须得有特定的格式!什么叫做特定的格式呢?就好比,我收到了一批数据,我就是不读取,那么接收缓冲区里面会积压很多的数据,但是TCP根本不关心数据是由什么构成的,但是,发过来的数据呢,可能都是一条一条的,对应的我们的网络版本计算器当中,发来的数据,特定的格式。TCP不关心,但是你用户将来还得提取操作数、操作符号是什么,所以特定的数据格式必须得有,那么这个必须得有特定的数据格式的话呢,那么谁来解释这个格式呢?**因为TCP不处理,只能是应用层进行处理!
》就好比,我们的接收缓冲区收到,发来的大量的数据,有可能发来的数据是两个完整报文,这是站在上帝视角,但是呢,我们站在读取端,站在TCP端,TCP它不管,它不关心你有几个完整报文和还有不完整的报文,它不关心,它只关心多少字节,然后我们上层呢,调用系统调用,比如说,recv()、write()从我们对应的接收缓冲区读取的时候,把数据读到我们对应的应用层的时候,TCP不关心,但是应用层必须得保证,它自己可以把数据全部读取出来,并且要至少保证能读取到一个完整的报文,然后将这个完整的报文进行处理,处理完之后再继续至少读到一个完整报文,所以,我们应用层要解决数据的特定格式问题。
》也就是说,TCP面向字节流给我们的感觉就是,好像挺不负责任呀,数据发过来怎么办呢?这个问题呢,必须得由应用层去解决。这也就是为什么,我们在写网络版本计算器的时候,我们要给每个报文携带报头,要有encode、decode,报头是一个有效载荷的长度字符串,表明有效载荷是多长,我们读取的时候先读取长度,然后再提取有效载荷,再通过同样的方式,重复的进行读取下一个报文。主要是协议,报文的处理问题呢,是由应用层自己去做的,TCP不关心,这就叫做面向字节流。
》将其搞定,我们紧接着进入下一个问题,如果我今天在进行数据处理的时候,如果应用层不负责任,它调用read()、recv(),定义一个缓冲区,char buffer[1024],直接从流式空间里面读的话,它可能恰好读到了一个完整报文,运气不好,就只读到了半个报文,再甚至呢,它读了一个半上去,那么是不是将一个完整的报文进行破坏了呀,相当于,你正常读上来一个,应用层认识出了一个特定格式的报文,读上来,应用层去处理就行了,但是,如果应用层只读上来半个呢?一个半呢?这个时候,应用层来处理就特别恶心了呀,它在处理的时候,就得保证读到一个完整的报文,那么我们既不能多读,也不能少读,必须严格按照应用层定制的协议要求,将数据读取出来,如果我们此时没有协议,或者我们没有按照严格的方式去读的话,那么就有可能出现多读或者少读的问题,这个情况,我们就叫做,TCP粘包问题。
》就如同,我们家里过年,每家都会蒸馒头或者蒸包子,如果包子蒸好后直接去拿的话,就有可能包子蒸好后可能挤在一起,此时你去拿,就有可能拿上来半个包子,或者你将旁边的包子皮拿上来了一部分,将另一个包子给破坏了。所以,包子和包子之间粘在一起,你再去拿的话,就很难拿了,多拿或者少拿都叫做粘包问题,所以在做包子的时候,都会将包子分开,你再去拿的话就只会拿上来一个。我们把包子提上来会多拿或者少拿,这种也叫做数据粘包问题。
所以,我们就要谈一个,数据粘包问题,我们该如何去解决呢?其实主体思想非常简单:明确报文和报文之间的边界,比如说,http报文和报文之间,它们的边界特别明显,它的报头呢,以空格为分隔符,你只要行距,当我们收到底层的流式信息,上层呢,只需要循环式的从接收缓冲区按行读,读到了空行,那么接下来就认为报头读完了,报头读完后,从报头里读取conet-lenth,根据conet-lenth再读取有效载荷,将这个工作做完之后,就认为已经把一个完整的报文读完了,把报文读完之后,再读取下一个,重复这样的工作,那么就不会出现粘包问题。它明确报文和报文之间的边界呢,是采用特殊的字符,比如说,空行,以及自描诉字段,比如,content-lenth来表明有效载荷的长度,通过这样的策略,来明确它的报文和报文之间的边界,这就叫做定制协议。再比如说呢,我们之前写的网络版本计算器,我们定制的协议呢,就是你要计算的字符串,其中字符串长度呢是被当作报头的,所以,当我们读取的时候,我们是依旧是按行读取上来,那么长度就知道了,再进行读取,就能将我的有效载荷读取出来了,只要我将一个报文读取上来,我下次再读,只要发送方依旧遵照我的协议时,我第二次再读取,依旧能够再次正确读取我们的数据了,我们刚刚无论是http还是网络版本计算器,其中,明确边界的任务,全部都是由我们应用层自己做的,这就是我们解决数据报粘包问题!
》还有其他策略吗?我们刚刚都是用特殊字符➕自描诉,还有其他策略就是,规定定长报文,我规定报文的大小必须是1024的,你不够,哪怕你自己添加一些废弃数据,你也得把1024字节给凑齐,我们每一个报文都是1024的话,我们上层就按照1024读取,这样也能解决报文和报文之间问题,这也就叫做我们数据包粘包问题。
》所以,对于现阶段而言,我们在进行数据通信的时候,在TCP当中,我们的协议得自己去定了, 这就是我之前说,我们聊天那些代码并不太对,因为你发的消息,对方收的时候,你可能发了四五次,对方一次就有可能全部收上去了,但是,你得明确报文和报文之间的界限,以保证不出问题。
》那么UDP有没有粘包问题呢?按照你上面讲的TCP有粘包的问题,那么UDP有没有呢?UDP的报文当中的报头里面是包含了,它的报头是定长的8字节,然后报头里面有16位长度字段,当我们的传输层收到了一个一个的UDP时,每一个UDP的有效载荷是多长,其实我们的上层或者UDP是可以甄别出来的,UDP在向上层交付的时候,它直接交付的就是完整报文,你发一个报文,我一定会收一个报文,当我们收到之后,UDP标定了有效载荷长度,报头是定长的,有效载荷就是我们要的长度,我们呢将上层交付就完了。所以,UDP在应用层上是不存在粘包问题,你不会读到半个UDP,也不会读到一个半,因为UDP底层是不会这样给你项上层交付的,当他收到了一个半报文,它就直接丢弃了,它给你的一定是完整报文,我们UDP呢在进行向上层交付的时候,它不存在粘包问题,只有我们的TCP才存在粘包问题。
》所以,在我们未来进行网络编程的时候,UDP只需要考虑一个问题,就是如何序列化和反序列化问题,而我们的TCP除了要考虑序列化和反序列化问题,还需要考虑数据报粘包问题,也就是协议定制的时候,如何读到完整报文的问题。所以,你觉得谁更简单呢?当然是UDP呀。
》在最后一点,当我们明白这一点之后呢,在TCP这里的粘包问题呢,TCP是面向字节流的,TCP它根本就不关心数据格式,也就是客户你应用层给我拷贝啥,我就把数据按照自己节奏发送到对端,所以,TCP根本不需要有效载荷的长度,因为它自己也不知道TCP报文有多长,所以TCP报头里面没有有效载荷的长度字段,它只有报头长度。当TCP收到报文,将报头一去掉,把数据直接按照序号放到缓冲区里面,那么它的任务就做完了,这个报文是什么意思,是什么格式,需不需要我TCP关心呢?答案是:不需要!所以TCP是纯纯的基于流式的服务。
》类比于现实生活中呢,我们在收快递的时候,这个快递就等同于UDP,就有点像UDP,你UDP要么收到一个报文,不可能收到一个半,如果一个快递员给你发一个半或者半个,你就得找快递员麻烦了,所以UDP这种事面向数据报。而这里的TCP呢,就有点像家里面的自来水,你家里面从自来水公司将自来水接到家里面,你接多少,完全取决你用什么来接,你用盘子、杯子、桶,根据不同的协议定制,根据不同的需求,接不同的水,但是,水在自来水公司流的时候,根本就不关心这个用户将来要接多少水,在我们自来水管道中呢,都叫做自来水流。如上就是我们粘包问题和面向字节流问题,我们全部讲完。
》这一张变,现在大家应该是能够看懂,就是OSI的七层模型。网络层、数据链路也没学,我们从传输层往上看。
》传输层的作用呢,是管理两个节点之间的数据传输,负责可靠的传输。很明显,这句话是传输层以TCP为主的传输协议。下一个会话层,它主要是负责建立和断开通信连接,这个工作我们没怎么做,但是有一个工作我们是做了,就是当我们在通信之前,我们TCP应用层需要调用connet()连上服务器,连上之后,服务器才能够跟我正常通话,这就是会话。
》我们要认识的是上面两个表示层和应用层。我们现在再来理解表示层是什么意思呢?它是固有数据格式和网络标准数据格式的转化。这里的固有格式是什么意思呢?就是你客户端和服务器内部所采用的格式,也叫做结构化数据,然后呢,网络标准数据格式转化什么意思呢?相当于你要进行序列化,你要将你的数据变成我们网络当中可以发送的数据,这个表示层表述的其实相当于 序列化,同时它也会涉及到你的数据发送给对方,然后你还要保证对方能够读取到完整报文,所以你光序列化还不够,你还得添加报头,因为你序列化的是有效载荷,你还要携带报头,这个报头就要保证你发过去的有效数据能够被对方完整接收,这就是表示层。
》应用层呢,就是指定协议。比如说呢,我们网络版本计算器,你的code退出码为1、2、3、4等是什么意思,这就是应用层定约定的。
》只要你应用层定制好了,然后表示层呢,再把结构化的数据转化成序列化再添加报头,然后转化成网络可以发的数据,然后通过我们的连接由会话层发送出去,然后交给操作系统,所以OSI定制的非常好。为什么变成四层呢?原因在于,比如我们的序列化和反序列化,我们没办法让用户在协议栈当中实现,因为不同用户有不同的网络通信需求。你应用层协议字段是什么也没有办法直接定好。包括你的连接什么时候建立和断开都完全是由用户决定的,底层很难决定。所以,它制定的多么细,只是将下面三层:传输层、网络层、数据链路层实现了。
》现在又有一个问题,如果我在和对方通信,连接出问题了呢?现在我能够把数据完整的交付给上层了,因为TCP当中可以采用自定义协议将特定格式的数据交付给上层,这也没问题;我们也解决了粘包问题,无非就是添加报头,保证对方收到完整的数据,我们也可以反序列化方式,将我们字符串风格的数据格式话成结构化数据,让我们上层的应用层直接读取,然后再编写我们数据处理逻辑就可以。现在的问题是,如果连接出问题了呢?
》连接出问题,比如说,两个主机正在通信,服务端突然终止了,被认为kill或者压力太大而被终止了,怎么办呢?这个问题好回答,当我们在网络通信的时候,我们双方经常有一个或两个终止了,我们也知道,网络也是文件,严格上来讲,打开的文件生命周期是随进程的,这个我们在操作系统学过,所以,进程终止时,会自动关闭我们曾经所打开的文件 。在自动关闭所打开的文件呢,在网络这里就是自动进行四次挥手,三次握手也是属于TCP做的,TCP属于操作系统。进程终止时,我们压根不用担心,因为底层操作系统会自动帮我们完成FIN、FIN+ACK这样的逻辑,和正常的四次挥手没什么区别。
》那下一个,如果我们今天连接建立着呢,我将我的机器关机,此时连接会怎么样呢?大家都知道,如果你今天在windows下重启,你在关机的时候呢,如果你的应用没有退出,它会提示你,当前有正在运行的进程,是否退出,你点击,是,它才会自动关机,当然他也要时间。如果你点击,否,那么他就取消它的关机过程,所以,这就告诉我们,在操作系统关机之前,我们操作系统是需要关闭我们曾经打开的应用,包括打开的客户端或服务器。所以,相当于在关机之前,本地文件就直接关了,如果涉及到网络,它也要进行四次挥手,其实和我们正常终止的情况也是一样的。
》所以理论上讲,一台时联网,连着远端的服务,比如登着QQ或者微信,和你的一台Windows什么软件都没启动, 在关机的时候,连接网络的那台主机,关机的时间会更久,因为他要正常的去关闭,除了本地软件之外,还要关闭联网应用。
》如果主机正在和远端服务器或者客户端正常通信的时候,你将网线拔了或者电池扣了,此时我们双方的连接会怎么办呢?这种情况肯定是属于意外情况,首先可以肯定的是,如果你拔的是客户端的网线或者客户端的电池,那么你操作系统相当于一瞬间断网了,断网之后呢,我们不可能把断开链接的请求发送给服务方,所以你客户端断网的时候,服务器压根就不知道断网了,它还认为你好着呢,但是呢,你可能已经不行了,所以呢,会出现,一方认为链接建立好,一方认为链接没有了。按照现在,如果我们的操作系统出现断开的情况,不就是网卡不能正常工作了嘛,操作系统识别到,那它就会觉得软件就没必要工作了,所以,一旦出了问题,在你拔网线之后呢,操作系统识别到网络断开了,曾经建立好的链接呢,把IP地址和Port保留下来,但依旧会将链接全部释放掉,当你回复网络之后呢,有可能操作系统会自动帮你重连,这就是操作系统帮我们去做的。