本片博文中,我主要谈谈我在学习TCP协议之后,回过头来再总结,对之中的某些知识的一个认识
1.为什么要三次握手才能建立连接?
TCP的三次握手保证了TCP是一个面向连接的传输协议(UDP没有固定的连接),其基本过程如下
如上图所示,要建立一个TCP连接,首先一方(客户端)向服务器发起一个建立连接的带有SYN标志的包,服务器收到后给客户端回一个ack(确认标志的包),用来告诉客户端我收到了你的请求了,此时服务器端已经创建好了用于和客户端收发数据之用的套接字所以当我们的客户端收到服务器给它回的ack之后,客户端其实就已经可以给服务器发送数据了,但是为了保险起见,我们的客户端会继续给服务器会一个ack用于告诉服务器我知道你已经帮我建立好连接了。那么TCP的第三次握手是否多余?因为我们的客户端在收到ack之后服务端已经帮我们建立好连接了,那么我们客户端干嘛还要回一个ack(用来告诉服务器我知道你给我建立连接了)
我个人理解之所以设计成这样有如下俩个理由
1.客户端建立连接时给服务器发个SYN包,服务器收到后创建连接所需的东西(套接字socket),创好之后回个ack给客户端,如果TCP握手没第三次,那么如果回的这个ack在中途丢失了,那么客户端就不敢确定服务器有没有给它创建socket(因为客户端怀疑自己给服务器发的SYN服务器可能没有收到,要不然服务器为什么不给它回ack),既然客户端不能确定服务器有没有给其创建socket,那么客户端也就不能冒然给服务器发数据,这就是引入第三次握手的原因,有了第三次握手,如果服务器给客户端发的ack丢了,那么服务器长时间收不到客户端的第三次响应,服务器就知道它发的ack可能丢失了,于是它就会重新发ack给客户端,我想解释到这里,因该已经把tcp第三次握手存在的必要性讲清楚了把。。。
2.1中我们说明了第三次握手存在的价值,那么由于客户端收到服务器的ack之后客户端已经明确的知道服务器给自己创建好了连接,它给服务器所回的ack也只是让服务器知道您老发的这个包没丢,客户端已经知道你给它创建好连接了。据此我们从理论上可得出在客户端收到服务器的ack之后其实就已经可以发送数据了(因为此时明确知道服务器帮我们建立好连接了)而此时如果发数据的话,它就会有第三次握手的ack携带发给服务器,所以第三次握手的ack是可以携带数据的,这样我们的三次握手的第三次,其实也就不会是做那么一点事(告诉服务器客户端知道它已创建好连接了)。
2.为什么关闭连接要4次挥手
为了讲起来能更加直观,我画了下图作为辅助理解
为什么建立连接需要3次握手,而关闭连接需要4次挥手?
我想只要你看过一些TCP的相关资料一定有遇到此类问题,关于这个问题其实没什么可讲的,因为TCP是个全双工协议(它的读端和写端是相互完全独立的)所以我们要关闭它,就得同时把这俩个端分别关闭,而每一端关闭都需要2次挥手如上图,所以,关闭连接需要4次
记得我在刚开始接触TCP的4次挥手的时候,我所思考的并不是建立要3次,而关闭为啥要四次。我所疑惑的是为什么建立连接的时候我们为了保证双方都知道连接建立成功,我们来了3次握手,那么既然关闭连接需要关闭相互独立的读端与写端,为了保证在关闭每一端时双方都知道连接已关闭不是关闭每一端都得3次挥手(请读者根据握手的三次过程做对比),那么要完整的关闭连接不就得是6次挥手?
其实这个问题并不难回答,对照上图,从上往下客户端先关闭自己的发端,发一个FIN(上图第一步)给服务器,(此时客户端已经把写端关掉了)服务器收到FIN后对应的把自己的读端关闭,然后给客户端发个ack(对应上图第二步),按照我上述的疑惑服务器是不是在给客户端回个ack说我收到你的确认了?其实此时你仔细想想这完全是多余的,因为当客户端收到ack之后,证明它已经知道服务器把自己的读端关闭了,而客户端自己也当然知道自己早已关闭了自己的写端,而服务器在发ack之前当然也早已知道客户端关闭了写端,对应的它也当然知道自己关闭了自己的读端,那么既然双方都在一去一回之后都已完全清楚自己和对方的状态,又何必由客户端在给服务器回个ack呢?
3.tcp协议是每个发出去的报文对面都必须给回个ack么?
这是不必要的,tcp中一方给另一方回复ack时会有一定的延迟等待时间,之所以会有延迟等待,主要有如下3方面的考虑
1.假如某个ack在等待系统限定的延时时间之际,又来了一个确认序号比它大的ack,那么系统就不会发哪个小的ack了,只需要把大的ack给对方发去,对方就知道在这个序列之前的数据对端都已收到,这也就回答了我们的上述问题
2.等待延时之际如果发端有数据要发,那么我们这个ack就可以搭载这这个有数据的数据包发给对方,而不是发一个空包给对方,这样可以节省我们公有的网络资源
3.ack等待延时同时也会加强我们的Nagle算法(下面会提到)
4.关于Nagle算法引发的思考
Nagle算法被用来防止广域网上存在大量的小分组(大小远小于MSS大小的报文段)的
关于该算法的原理,我在此摘抄一段来自TCP/IP详解中对其的一段说明
该算法要求一个TCP连接上最多只能有一个未被确认的未完成的小分组,在该分组的确认到达之前不能发送其他小分组。相反,TCP收集这些少量的分组,并在确认到来之前以一个大分组(不超过MSS)的方式发出去
刚开始看完这个说明后,给我的感觉是这个Nagle算法的效率也太低了吧,因为它每次只允许网络上有一个未被确认的分段,也就意味着,每发一个报文之后,如果它的ack不回来,我就不能发下个报文,如果我发一个大文件时,效率是不是很低?众所周知Nagle所发是默认开启的,这样它的效率就不应该这样低啊,我感觉因该是我理解错了Nagle算法,于是我又重新斟酌上面那段话,终于我发现了,原来它所说的是网络上只允许一个未被确认的小分段存在,注意是小分段!也就是说它对我们一个满足了MSS大小的大分段是没有限制的,所以我上述所说的发大文件时,由于大文件几乎可以装满好多满的MSS所以这些在传输过程中并不会受Nagle算法的作用
其次是Nagle算法的自适应性
由于我们在局域网内的带宽足够,理论上不需要Nagle算法的,那么默认的Nagle算法会不会让我们在局域网内发送数据也由此而变慢呢?
其实这个你大可不必担心,因为局域网内通讯速度快,所以当你发出去一个小数据时,这个数据的ack马上就能回来,你就可以马上再发接下来的数据了,所以Nagle算法对局域网的限制并不大
5.什么是糊涂窗口综合症
所谓的糊涂窗口综合症是指,当发送方的发送速度大于接收方的接受速度时,那么可能会使接受方的接受缓冲区变满,此时接受方给发送方回个ack通告其窗口大小为0,这时发送方就不能再给对方发送数据了,加入对面的接受缓冲区现在空闲出了一个字节大小,此时就立刻给对方通知的话,对方一次只能以40字节大小的头部携带一个字节大小的数据发过来,而此时又会把对方接受缓冲区弄满,然后对面在空闲出一个字节。。。如此循环,网络上充斥这各种1个字节的小包,无意是对网络资源的一种浪费,这就是糊涂窗口综合症,为了解决这种低效率的方式,我们的TCP/IP协议栈规定,当接受缓冲区满了之后,当在一次有超过起码一个MSS大小的空间时才会通知对端
6.慢启动
我们在6中谈了窗口大小,窗口大小是实现接收方的流量控制的策略(接受方告诉发端我最多能接受这么多数据)。那么如果在广域网中传输的话,发送方是不是一次可发送到额数据就可以为接收方给的窗口大小呢?事实是当然不可以,因为在广域网中大家如果都一次发的数据过多,那么某些中间路由可能一下自消化不了这么多数据包(此时就会发生丢包),丢包了其实并不可怕,大不了在重新发一下,关键的问题是,如果你老是以这个比较大的发送量发送数据的话,可能会造成你一直丢包,这样问题就大了。为了解决上述问题,我们得给发送端设置一个网络上可以接受的发送速率,这就得用到我们要讨论的慢启动算法
慢启动算法所要实现的就是新分组进入网络的速度因该与另一端返回确认的速度相同(能返回确认说明没有丢包!)而进行工作的。
那么慢启动算法具体是如何实现的呢?其具体算法说明如下
慢启动会为TCP增加一个拥塞窗口,记为cwnd。当俩台主机初次建立连接时,拥塞窗口初始化为1个报文段,这也就意味着此时我们最多同时能发送一个报文段,之后每收到报文段的一个ack拥塞窗口的大小就增加一个报文段大小,也就意味着我们能同时发送的报文段大小增加一个(当然发送方要去拥塞窗口和通告窗口的最小值最作为上限),根据上述描述,我们可以这样描述慢启动过程,第一次发1个,当收到确认后发2个,再一次收到确认发4个。。。这是一个指数式的增长,所以其实慢启动一点也不慢,照它的这个速度一定会达到某个值而发生丢包,这就是我们一会讨论的问题了
7.拥塞避免算法
拥塞避免算法是用来防止由于发送的数据过多而在网络拥塞的情况下导致丢包的发生,它虽然和慢启动算法的实现目的不同(慢启动是为了使发端和收端的速率匹配),但是要完成此算法得有慢启动算法的配合才可以。拥塞算法的具体实施过程如下
前提:拥塞避免算法和慢启动算法需要对每个连接维持俩个变量:一个拥塞窗口cwnd(前面有说过)和一个慢启动门限ssthresh
1)对一个给定的连接,初始化cwnd为1个报文段,ssthresh为65535字节(约45左右个报文段)
2)执行慢启动,TCP每次可发送的报文段不能超过cwnd和通告窗口俩者中最小的
3)由于慢启动是以一个指数的增长方式在增长,所以很快我们就会发生丢包(针对超时),此时说明拥塞以发生,说明我们当前的发送速率超出了网络负荷,那么我们就该减少我们的发送速率,但是问题的关键是这个速率该杂么减?这个问题谁都回答不出来,我们只能通过调整发送速率的大小,继续试,看哪个速率比较合适。那么拥塞避免算法是如何来调整的呢?它通过将cwnd重设为1,然后再次进行慢启动,当然此时的慢启动的cwnd所能达到的上限为ssthresh(它的大小为上次发生丢包时,窗口大小的一半)
4)当进行慢启动达到ssthresh限制之后,就不在进行慢启动的指数增长了,而是每收到一个ack cwnd增加1/cwnd大小(注意慢启动为每收到一个ack增加1个大小),也就意味着当收到cwnd个ack时(所发所有数据都没有丢失)时cwnd才会增加1个大小报文段大小,这种增长方式为线性增长,相对缓慢,之后每次增长引起拥塞的概率不大,相对稳定,这就是拥塞避免算法的过程。
8.快速重传与快速恢复
1)何种情况下进行快速重传
当我们收到一个失序报文段时,TCP会立即产生一个ack(不被延时的),该重复的ack用来告诉发端,我收到了乱序的报文段(比我想收到的报文段的序列号大),我希望收到的为此ack携带的序列号对应的报文段。当连续收到3个或3个以上的重复报文段时,就说明可能这个重复ack所携带的序列号对应的报文段可能丢失了(因为它后面的好多报文段都已收到,而它还没有到),于是我们此时不需要在等待此报文段的超时器溢出才重新传该报文段,而是直接重传,这就是快速重传算法
2)如何通过快速重传进行快速恢复呢?
.收到第三个重复的ack时,将ssthresh设置成当前拥塞窗口cwnd的一半,重传丢失的报文段。设置cwnd为ssthresh加上3倍的报文段大小(此时cwnd比ssthresh大,所以快速恢复并没有执行慢启动,而是执行的拥塞避免)
.每次收到另一个重复的ack时,cwnd增加一个报文段大小并发送一个分组
.当下一个确认新数据的ack到达时,设置cwnd为sshresh。这个ack因该是在进行重传后的一个往返时间内对步骤1重传的确认。另外这个ack也应该是对丢失的分组和收到的第一个重复的ack之间所有的中间报文的确认。这一步采用的是拥塞避免,因为当分组丢失时我们把当前速率减半.