从TCP三次握手提及——浅析TCP协议中的疑难杂症

声明:本文来自腾讯增值产品部官方公众号小时光茶社,为CSDN原创投稿,未经许可,禁止任何形式的转载。
做者:黄日成,手Q游戏中心后台开发,腾讯高级工程师。从事C++服务后台开发4年多,主要负责手Q游戏中心后台基础系统、复杂业务系统开发,主导过手Q游戏公会、企鹅电竞App-对战系统等项目的后台系统设计,有丰富的后台架构经验。
责编:钱曙光,关注架构和算法领域,寻求报道或者投稿请发邮件qianshg@csdn.net,另有「CSDN 高级架构师群」,内有诸多知名互联网公司的大牛架构师,欢迎架构师加微信qshuguang2008申请入群,备注姓名+公司+职位。html

【引言】前端

说到TCP协议,相信你们都比较熟悉了,对于TCP协议总能说个一二三来,可是TCP协议又是一个很是复杂的协议,其中有很多细节点让人头疼。本文就是来讲说这些头疼点的,浅谈一些TCP的疑难杂症。那么从哪提及呢?固然是从三次握手和四次挥手提及啦,可能你们都知道TCP是三次交互完成链接的创建,四次交互来断开一个链接,那为何是三次握手和四次挥手呢?反过来不行吗?linux

疑症一:TCP的三次握手、四次挥手

下面两图你们再熟悉不过了,TCP的三次握手和四次挥手见下面左边的”TCP创建链接”、”TCP数据传送”、”TCP断开链接”时序图和右边的”TCP协议状态机”web

图片描述

TCP三次握手、四次挥手时序图

图片描述

TCP协议状态机

要弄清TCP创建链接须要几回交互才行,咱们须要弄清创建链接进行初始化的目标是什么。TCP进行握手初始化一个链接的目标是:分配资源、初始化序列号(通知Peer对端个人初始序列号是多少),知道初始化链接的目标,那么要达成这个目标的过程就简单了,握手过程能够简化为下面的四次交互:算法

(1) Client端首先发送一个SYN包告诉Server端个人初始序列号是X;shell

(2) Server端收到SYN包后回复给Client一个ACK确认包,告诉Client说我收到了;数据库

(3) 接着Server端也须要告诉Client端本身的初始序列号,因而Server也发送一个SYN包告诉Client个人初始序列号是Y;缓存

(4) Client收到后,回复Server一个ACK确认包说我知道了。安全

整个过程4次交互便可完成初始化,可是,细心的同窗会发现两个问题:[1] Server发送SYN包是做为发起链接的SYN包,仍是做为响应发起者的SYN包?怎么区分?比较容易引发混淆 ;[2] Server的ACK确认包和接下来的SYN包能够合成一个SYN ACK包一块儿发送的,不必分别单独发送,这样省了一次交互同时也解决了问题[1]。 这样TCP创建一个链接,三次握手在进行最少次交互的状况下完成了Peer两端的资源分配和初始化序列号的交换。服务器

大部分状况下创建链接须要三次握手,也不必定都是三次,有可能出现四次握手来创建链接的。以下图,当Peer两端同时发起SYN来创建链接时,就出现了四次握手来创建链接(对于有些TCP/IP的实现,可能不支持这种同时打开的状况)。

图片描述

在三次握手过程当中,细心的同窗可能会有如下疑问:

  • 初始化序列号X、Y是能够是写死固定的吗,为何不能?
  • 假如Client发送一个SYN包给Server后就挂了或是无论了,这个时候这个链接处于什么状态?会超时吗?为何?

TCP进行断开链接的目标是:回收资源、终止数据传输。因为TCP是全双工的,须要Peer两端分别各自拆除本身通向Peer对端方向的通讯信道。这样须要四次挥手来分别拆除通讯信道,就比较清晰明了了。

(1) Client发送一个FIN包来告诉Server我已经没数据须要发给Server了;

(2) Server收到后回复一个ACK确认包说我知道了;

(3) 而后Server在本身也没数据发送给Client后,Server也发送一个FIN包给Client告诉Client我也已经没数据发给Client了;

(4) Client收到后,就会回复一个ACK确认包说我知道了。

到此,四次挥手,这个TCP链接就能够彻底拆除了。在四次挥手的过程当中,细心的同窗可能会有如下疑问:

  • Client和Server同时发起断开链接的FIN包会怎么样呢,TCP状态是怎么转移的?
  • 左侧图中的四次挥手过程当中,Server端的ACK确认包能不能和接下来的FIN包合并成一个包,这样四次挥手就变成三次挥手了。
  • 四次挥手过程当中,首先断开链接的一端,在回复最后一个ACK后,为何要进行`TIME_
  • 呢(超时设置是 2*MSL,RFC793定义了MSL为2分钟,Linux设置成了30s),在TIME_WAIT的时候又不能释放资源,白白让资源占用那么长时间,可否省去TIME_WAIT`,为何?

疑症二:TCP链接的初始化序列号可否固定

若是初始化序列号(缩写为ISN:Inital Sequence Number)能够固定,咱们来看看会出现什么问题。假设ISN固定是1,Client和Server创建好一条TCP链接后,Client连续给Server发了10个包,这10个包不知怎么被链路上的路由器缓存了(路由器会毫无先兆地缓存或者丢弃任何的数据包),这个时候碰巧Client挂掉了,而后Client用一样的端口号从新连上Server,Client又连续给Server发了几个包,假设这个时候Client的序列号变成了5。接着,以前被路由器缓存的10个数据包所有被路由到Server端了,Server给Client回复确认号10,这个时候,Client整个都很差了,这是什么状况?个人序列号才到5,你怎么给个人确认号是10了,整个都乱了。

RFC793中,建议ISN和一个假的时钟绑在一块儿,这个时钟会在每4微秒对ISN作加一操做,直到超过2^32,又从0开始,这须要4小时才会产生ISN的回绕问题,这几乎能够保证每一个新链接的ISN不会和旧链接的ISN产生冲突。这种递增方式的ISN,很容易让攻击者猜想到TCP链接的ISN,如今的实现大可能是在一个基准值的基础上随机进行的。

疑症三:初始化链接的SYN超时问题

Client发送SYN包给Server后挂了,Server回给Client的SYN-ACK一直没收到Client的ACK确认,此时这个链接既没创建起来,也不能算失败。这就须要一个超时时间让Server将这个链接断开,不然这个链接就会一直占用Server的SYN链接队列中的一个位置,大量这样的链接就会将Server的SYN链接队列耗尽,让正常的链接没法获得处理。目前,Linux下默认会进行5次重发SYN-ACK包,重试的间隔时间从1s开始,下次的重试间隔时间是前一次的双倍,5次的重试时间间隔为1s,2s,4s,8s,16s,总共31s,第5次发出后还要等32s都知道第5次也超时了,因此,总共须要 1s + 2s + 4s+ 8s+ 16s + 32s = 63s,TCP才会断开这个链接。因为,SYN超时须要63秒,那么就给攻击者一个攻击服务器的机会,攻击者在短期内发送大量的SYN包给Server(俗称SYN flood攻击),用于耗尽Server的SYN队列。对于应对SYN过多的问题,Linux提供了几个TCP参数:tcp_syncookiestcp_synack_retriestcp_max_syn_backlogtcp_abort_on_overflow来调整应对。

疑症四:TCP的Peer两端同时断开链接

由上面的”TCP协议状态机 “图能够看出,TCP的Peer端在收到对端的FIN包前发出了FIN包,那么该Peer的状态就变成了FIN_WAIT1,Peer在FIN_WAIT1状态下收到对端Peer对本身FIN包的ACK包的话,那么Peer状态就变成FIN_WAIT2,Peer在FIN_WAIT2下收到对端Peer的FIN包,在确认已经收到了对端Peer所有的Data数据包后,就响应一个ACK给对端Peer,而后本身进入TIME_WAIT状态;可是若是Peer在FIN_WAIT1状态下首先收到对端Peer的FIN包的话,那么该Peer在确认已经收到了对端Peer所有的Data数据包后,就响应一个ACK给对端Peer,而后本身进入CLOSEING状态,Peer在CLOSEING状态下收到本身FIN包的ACK包的话,那么就进入TIME WAIT状态。因而,TCP的Peer两端同时发起FIN包进行断开链接,那么两端Peer可能出现彻底同样的状态转移FIN_WAIT1---->CLOSEING----->TIME_WAIT,Client和Server也就会最后同时进入TIME_WAIT状态。同时关闭链接的状态转移以下图所示:

图片描述

疑症五:四次挥手可否变成三次挥手?

答案是可能的。TCP是全双工通讯,Cliet在本身已经不会再有新的数据要发送给Server后,能够发送FIN信号告知Server,这边已经终止Client到对端Server的数据传输。可是,这个时候对端Server能够继续往Client这边发送数据包。因而,两端数据传输的终止在时序上独立而且可能会相隔比较长的时间,这个时候就必须最少须要2+2=4次挥手来彻底终止这个链接。可是,若是Server在收到Client的FIN包后,再也没数据须要发送给Client了,那么对Client的ACK包和Server本身的FIN包就能够合并成一个包发送过去,这样四次挥手就能够变成三次了(彷佛Linux协议栈就是这样实现的)。

疑症六:TCP的头号疼症TIME_WAIT状态

要说明TIME_WAIT的问题,须要解答如下几个问题:

1. Peer两端,哪一端会进入TIME_WAIT,为何?

相信你们都知道,TCP主动关闭链接的那一方会最后进入TIME_WAIT。那么怎么界定主动关闭方?是否主动关闭是由FIN包的前后决定的,就是在本身没收到对端Peer的FIN包以前本身发出了FIN包,那么本身就是主动关闭链接的那一方。对于疑症四中描述的状况,那么Peer两边都是主动关闭的一方,两边都会进入TIME_WAIT。为何是主动关闭的一方进行TIME_WAIT呢,被动关闭的进入TIME_WAIT能够吗?咱们来看看TCP四次挥手能够简单分为下面三个过程

过程一:主动关闭方发送FIN;
过程二:被动关闭方收到主动关闭方的FIN后发送该FIN的ACK,被动关闭方发送FIN;
过程三:主动关闭方收到被动关闭方的FIN后发送该FIN的ACK,被动关闭方等待本身FIN的ACK

问题就在过程三中,据TCP协议规范,不对ACK进行ACK,若是主动关闭方不进入TIME_WAIT,那么主动关闭方在发送完ACK就走了的话,若是最后发送的ACK在路由过程当中丢掉了,最后没能到被动关闭方,这个时候被动关闭方没收到本身FIN的ACK就不能关闭链接,接着被动关闭方会超时重发FIN包,可是这个时候已经没有对端会给该FIN回ACK,被动关闭方就没法正常关闭链接了,因此主动关闭方须要进入TIME_WAIT以便可以重发丢掉的被动关闭方FIN的ACK。

2. TIME_WAIT状态是用来解决或避免什么问题呢?

TIME_WAIT主要是用来解决如下几个问题:

(1) 上面解释为何主动关闭方须要进入TIME_WAIT状态中提到的:主动关闭方须要进入TIME_WAIT以便可以重发丢掉的被动关闭方FIN包的ACK。若是主动关闭方不进入TIME_WAIT,那么在主动关闭方对被动关闭方FIN包的ACK丢失了的时候,被动关闭方因为没收到本身FIN的ACK,会进行重传FIN包,这个FIN包到主动关闭方后,因为这个链接已经不存在于主动关闭方了,这个时候主动关闭方没法识别这个FIN包,协议栈会认为对方疯了,都还没创建链接你给我来个FIN包?因而回复一个RST包给被动关闭方,被动关闭方就会收到一个错误(咱们见的比较多的:connect reset by peer。这里顺便说下Broken pipe,在收到RST包的时候,还往这个链接写数据,就会收到Broken pipe错误了),本来应该正常关闭的链接,给我来个错误,很难让人接受。

(2) 防止已经断开的链接1中在链路中残留的FIN包终止掉新的链接2[重用了链接1的全部5元素(源IP,目的IP,TCP,源端口,目的端口)],这个几率比较低,由于涉及到一个匹配问题,迟到的FIN分段的序列号必须落在链接2一方的指望序列号范围以内,虽然几率低,可是确实可能发生,由于初始序列号都是随机产生的,而且这个序列号是32位的,会回绕。

(3) 防止链路上已经关闭的链接的残余数据包(a lost duplicate packet or a wandering duplicate packet)干扰正常的数据包,形成数据流不正常。这个问题和(2)相似。

3. TIME_WAIT会带来哪些问题?

TIME_WAIT带来的问题主要是源于:一个链接进入TIME_WAIT状态后须要等待2*MSL(通常是1到4分钟)那么长的时间才能断开链接释放链接占用的资源,会形成如下问题:

(1) 做为服务器,短期内关闭了大量的Client链接,就会形成服务器上出现大量的TIME_WAIT链接,占据大量的tuple,严重消耗着服务器的资源;
(2) 做为客户端,短期内大量的短链接,会大量消耗Client机器的端口,毕竟端口只有65535个,端口被耗尽了,后续就没法再发起新的链接了。

(因为上面两个问题,做为客户端须要连本机的一个服务的时候,首选UNIX域套接字而不是TCP)

TIME_WAIT很使人头疼,不少问题是由TIME_WAIT形成的,但TIME_WAIT又不是多余的,因此不能简单将TIME_WAIT去掉,那么如何来解决或缓解TIME_WAIT问题?能够进行TIME_WAIT的快速回收和重用来缓解TIME_WAIT的问题。是否有一些清掉TIME_WAIT的技巧?

4. TIME_WAIT的快速回收和重用

(1) TIME_WAIT快速回收

Linux下开启TIME_WAIT快速回收须要同时打开tcp_tw_recycletcp_timestamps(默认打开)两选项。Linux下快速回收的时间为3.5*RTO(Retransmission Timeout),而一个RTO时间为200ms至120s。开启快速回收TIME_WAIT,可能会带来问题一中说的三点危险,为了不这些危险,要求同时知足如下三种状况的新链接被拒绝掉。

[1] 来自同一个对端Peer的TCP包携带了时间戳

[2] 以前同一台peer机器(仅仅识别IP地址,由于链接被快速释放了,没了端口信息)的某个TCP数据在MSL秒以内到过本Server

[3] Peer机器新链接的时间戳小于Peer机器上次TCP到来时的时间戳,且差值大于重放窗口戳(TCP_PAWS_WINDOW

初看起来正常的数据包同时知足上面3条几乎不可能,由于机器的时间戳不可能倒流的,出现上述的3点均知足时,必定是老的重复数据包又回来了,丢弃老的SYN包是正常的。到此,彷佛启用快速回收就能很大程度缓解TIME_WAIT带来的问题。可是,这里忽略了一个东西就是NAT——在一个NAT后面的全部Peer机器在Server看来都是一个机器,NAT后面的那么多Peer机器的系统时间戳极可能不一致,有些快,有些慢。这样,在Server关闭了与系统时间戳快的Client的链接后,在这个链接进入快速回收的时候,同一NAT后面的系统时间戳慢的Client向Server发起链接,这就颇有可能同时知足上面的三种状况,形成该链接被Server拒绝掉。因此,在是否开启tcp_tw_recycle须要慎重考虑。

(2) TIME_WAIT重用

Linux上比较完美地实现了TIME_WAIT重用问题。只要知足下面两点中的一点,一个TW状态的四元组(即一个socket链接)能够从新被新到来的SYN链接使用

[1] 新链接SYN告知的初始序列号比TIME_WAIT老链接的末序列号大

[2] 若是开启了tcp_timestamps,而且新到来的链接的时间戳比老链接的时间戳大

要同时开启tcp_tw_reuse选项和tcp_timestamps选项才能够开启TIME_WAIT重用,还有一个条件是:重用TIME_WAIT的条件是收到最后一个包后超过1s。细心的同窗可能发现TIME_WAIT重用对Server端来讲并没解决大量TIME_WAIT形成的资源消耗的问题,由于无论TIME_WAIT链接是否被重用,它依旧占用着系统资源。即使如此,TIME_WAIT重用仍是有些用处的,它解决了整机范围拒绝接入的问题,虽然通常一个单独的Client是不可能在MSL内用同一个端口链接同一个服务的,可是若是Client作了bind端口那就是同一个端口了。时间戳重用TIME_WAIT链接机制的前提是IP地址惟一性,得出新请求发起自同一台机器,可是若是是NAT环境下就不能这样保证了,因而在NAT环境下,TIME_WAIT重用仍是有风险的。

有些同窗可能会混淆tcp_tw_reuseSO_REUSEADDR选项,认为是相关的东西,其实它们是两个彻底不一样的东西,能够说半毛钱关系都没。tcp_tw_reuse是内核选项,而SO_REUSEADDR用户态的选项,使用SO_REUSEADDR是告诉内核,若是端口忙,但TCP状态位于TIME_WAIT,能够重用端口。若是端口忙,而TCP状态位于其它状态,重用端口时依旧获得一个错误信息,指明Address already in use。若是你的服务程序中止后想当即重启,而新套接字依旧使用同一端口,此时SO_REUSEADDR选项很是有用。可是,使用这个选项就会有(问题二)中说的三点危险,虽然发生的几率不大。

5. 清掉TIME_WAIT的奇技怪巧

能够用下面两种方式控制服务器的TIME_WAIT数量:

(1) 修改tcp_max_tw_buckets

tcp_max_tw_buckets控制并发的TIME_WAIT数量,默认值是180000。若是超过默认值,内核会把多的TIME_WAIT链接清掉,而后在日志里打一个警告。官网文档说这个选项只是为了阻止一些简单的DoS攻击,日常不要人为下降它。

(2) 利用RST包从外部清掉TIME_WAIT连接

根据TCP规范,收到任何发送到未侦听端口、已经关闭的链接的数据包、链接处于任何非同步状态(LISTEN, SYS-SENT,SYN-RECEIVED)而且收到的包的ACK在窗口外,或者安全层不匹配,都要回执以RST响应(而收到滑动窗口外的序列号的数据包,都要丢弃这个数据包,并回复一个ACK包),内核收到RST将会产生一个错误并终止该链接。咱们能够利用RST包来终止掉处于TIME_WAIT状态的链接,其实这就是所谓的RST攻击了。为了描述方便:假设Client和Server有个链接Connect1,Server主动关闭链接并进入了TIME_WAIT状态,咱们来描述一下怎么从外部使得Server处于TIME_WAIT状态的链接Connect1提早终止掉。要实现这个RST攻击,首先咱们要知道Client在Connect1中的端口port1(通常这个端口是随机的,比较难猜到,这也是RST攻击较难的一个点),利用IP_TRANSPARENT这个socket选项,它能够bind不属于本地的地址,所以能够从任意机器绑定Client地址以及端口port1,而后向Server发起一个链接,Server收到了窗口外的包因而响应一个ACK,这个ACK包会路由到Client处,这个时候可能99%的Client已经释放链接Connect1了,这个时候Client收到这个ACK包,会发送一个RST包,Server收到RST包而后就释放链接Connect1提早终止TIME_WAIT状态。提早终止TIME_WAIT状态是可能会带来(问题二)中说的三点危害,具体的危害状况能够看下RFC1337。RFC1337中建议,不要用RST过早的结束TIME_WAIT状态。

至此,上面的疑症都解析完毕,然而细心的同窗会有下面的疑问:

  • TCP的可靠传输是确认号来实现的,那么TCP的确认机制是怎样的呢?是收到一个包就立刻确认,仍是能够稍等一下再确认?
  • 假如发送一个包,一直都没收到确认呢?何时重传?超时机制是怎样的?
  • TCP两端Peer的处理能力不对等时,好比发送方处理能力很强,接收方处理能力很弱,这样发送方是否可以无论接收方死活狂发数据?若是不能,流量控制机制是怎样的?
  • TCP是端到端的协议,也就是TCP对端Peer只看到对方,看不到网络上的其余点,那么TCP的两端如何对网络状况作出反映?发生拥塞时,拥塞控制机制是怎样的?

疑症七:TCP的延迟确认机制

按照TCP协议,确认机制是累积的,也就是确认号X确认指示的是全部X以前但不包括X的数据已经收到了。确认号(ACK)自己就是不含数据的分段,所以大量的确认号消耗了大量的带宽,虽然大多数状况下,ACK仍是能够和数据一块儿捎带传输,可是若是没有捎带传输,那么就只能单独回来一个ACK,若是这样的分段太多,网络的利用率就会降低。为缓解这个问题,RFC建议了一种延迟的ACK,也就是说,ACK在收到数据后并不立刻回复,而是延迟一段能够接受的时间,延迟一段时间的目的是看能不能和接收方要发给发送方的数据一块儿回去,由于TCP协议头中老是包含确认号的,若是能的话,就将数据一块儿捎带回去,这样网络利用率就提升了。延迟ACK就算没有数据捎带,那么若是收到了按序的两个包,那么只要对第二包作确认便可,这样也能省去一个ACK消耗。因为TCP协议不对ACK进行ACK,RFC建议最多等待2个包的积累确认,这样可以及时通知对端Peer我这边的接收状况。Linux实现中,有延迟ACK和快速ACK,并根据当前的包的收发状况来在这两种ACK中切换。通常状况下,ACK并不会对网络性能有太大的影响,延迟ACK能减小发送的分段从而节省带宽,而快速ACK能及时通知发送方丢包,避免滑动窗口停等,提高吞吐率。关于ACK分段,有个细节须要说明一下,ACK的确认号,是确认按序收到的最后一个字节序,对于乱序到来的TCP分段,接收端会回复相同的ACK分段,只确认按序到达的最后一个TCP分段。TCP链接的延迟确认时间通常初始化为最小值40ms,随后根据链接的重传超时时间(RTO)、上次收到数据包与本次接收数据包的时间间隔等参数进行不断调整。

疑症八:TCP的重传机制以及重传的超时计算

1. TCP的重传超时计算

TCP交互过程当中,若是发送的包一直没收到ACK确认,是要一直等下去吗?显然不能一直等(若是发送的包在路由过程当中丢失了,对端都没收到又如何给你发送确认呢?),这样协议将不可用,既然不能一直等下去,那么该等多久?等太长时间的话,数据包都丢了好久了才重发,没有效率,性能差;等过短时间的话,可能ACK还在路上快到了,这时候却重传了,形成浪费,同时过多的重传会形成网络拥塞,进一步加重数据的丢失。也是,咱们不能去猜想一个重传超时时间,应该是经过一个算法去计算,而且这个超时时间应该是随着网络情况在变化的。为了使咱们的重传机制更高效,若是咱们可以比较准确知道在当前网络情况下,一个数据包从发出去到回来的时间RTT——Round Trip Time,那么根据这个RTT咱们就能够方便设置TimeOut——RTO(Retransmission TimeOut)了。

为了计算这个RTO,RFC793中定义了一个经典算法,算法以下:

[1] 首先采样计算RTT值

[2] 而后计算平滑的RTT,称为Smoothed Round Trip Time (SRTT),SRTT = ( ALPHA * SRTT ) + ((1-ALPHA) * RTT)

[3] RTO = min[UBOUND,max[LBOUND,(BETA*SRTT)]]

其中:UBOUND是RTO值的上限,例如:能够定义为1分钟;LBOUND是RTO值的下限,例如,能够定义为1秒。ALPHA is a smoothing factor (e.g., .8 to .9), and BETA is a delay variance factor (e.g., 1.3 to 2.0). 然而这个算法有个缺点就是:在算RTT样本的时候,是用第一次发数据的时间和ACK回来的时间作RTT样本值,仍是用重传的时间和ACK回来的时间作RTT样本值?无论怎么选择,总会形成会要么把RTT算过长了,要么把RTT算太短了。以下图:(a)就计算过长了,而(b)就是计算太短了。

图片描述

针对上面经典算法的缺陷,提出Karn/Partridge Algorithm对经典算法进行了改进(算法大特色是——忽略重传,不把重传的RTT作采样),可是这个算法有问题:若是在某一时间,网络闪动,忽然变慢了,产生了比较大的延时,这个延时致使要重转全部的包(由于以前的RTO很小),因而,由于重转不算,因此,RTO就不会被更新,这是一个灾难。因而,为解决上面两个算法的问题,又有人推出来一个新的算法,这个算法叫Jacobson / Karels Algorithm(参看RFC6289),这个算法的核心是:除了考虑每两次测量值的误差以外,其变化率也应该考虑在内,若是变化率过大,则经过以变化率为自变量的函数为主计算RTT(若是陡然增大,则取值为比较大的正数,若是陡然减少,则取值为比较小的负数,而后和平均值加权求和),反之若是变化率很小,则取测量平均值。

公式以下:(其中的DevRTT是Deviation RTT的意思)

SRTT = SRTT + α (RTT – SRTT) —— 计算平滑RTT

DevRTT = (1-β)DevRTT + β(|RTT-SRTT|) ——计算平滑RTT和真实的差距(加权移动平均)

RTO= µ * SRTT + ∂ *DevRTT —— 神同样的公式

(其中:在Linux下,α = 0.125,β = 0.25, μ = 1,∂ = 4 ——这就是算法中的“调得一手好参数”,nobody knows why, it just works…)最后的这个算法被用在今天的TCP协议中并工做很是好。

知道超时怎么计算后,很天然就想到定时器的设计问题。一个简单直观的方案就是为TCP中的每个数据包维护一个定时器,在这个定时器到期前没收到确认,则进行重传。这种在设计理论上是很合理的,可是实现上,这种方案将会有很是多的定时器,会带来巨大内存开销和调度开销。既然不能每一个包一个定时器,那么多少个包一个定时器比较好?这彷佛比较难肯定。能够换个思路,不要以包量来肯定定时器,以链接来肯定定时器是否会比较合理?目前,采起每个TCP链接单一超时定时器的设计则成了一个默认的选择,而且RFC2988给出了每链接单必定时器的设计建议算法规则:

[1] 每一次一个包含数据的包被发送(包括重发),若是还没开启重传定时器,则开启它,使得它在RTO秒以后超时(按照当前的RTO值)。

[2] 当接收到一个ACK确认一个新的数据, 若是全部发出数据都被确认了,关闭重传定时器。

[3] 当接收到一个ACK确认一个新的数据,还有数据在传输,也就是还有没被确认的数据,从新启动重传定时器,使得它在RTO秒以后超时(按照当前的RTO值)。

[4] 当重传定时器超时后,依次作下列3件事情:

[4.1] 重传最先的还没有被TCP接收方ACK的数据包;

[4.2] 从新设置RTO为RTO*2(“还原定时器”),可是新RTO不该该超过RTO的上限(RTO有个上限值,这个上限值最少为60s);

[4.3] 重启重传定时器。

上面的建议算法体现了一个原则:没被确认的包必须能够超时,而且超时的时间不能太长,同时也不要过早重传。规则[1]、[3]、[4.3]共同说明了只要还有数据包没被确认,那么定时器必定会是开启着的(这样知足没被确认的包必须能够超时的原则)。规则[4.2]说明定时器的超时值是有上限的(知足超时的时间不能太长)。规则[3]说明,在一个ACK到来后重置定时器能够保护后发的数据不被过早重传。由于一个ACK到来了,说明后续的ACK极可能会依次到来,也就是说丢失的可能性并不大。规则[4.2]也是在必定程度上避免过早重传,由于,在出现定时器超时后,有多是网络出现拥塞了,这个时候应该延长定时器,避免出现大量的重传进一步加重网络拥塞。

2. TCP的重传机制

经过上面咱们能够知道,TCP的重传是由超时触发的,这会引起一个重传选择问题,假设TCP发送端连续发了一、二、三、四、五、六、七、八、九、10共10包,其中四、六、8这3个包全丢失了,因为TCP的ACK是确认最后连续收到序号,这样发送端只能收到3号包的ACK,这样在TIME_OUT的时候,发送端就面临下面两个重传选择:

(1) 仅重传4号包
(2) 重传3号后面全部的包,也就是重传4~10号包

上面两个选择的优缺点都比较明显。方案(1),优势:按需重传,可以最大程度节省带宽。缺点:重传会比较慢,由于重传4号包后,须要等下一个超时才会重传6号包。方案[2],优势:重传较快,数据可以较快交付给接收端。缺点:重传了不少没必要要重传的包,浪费带宽,在出现丢包的时候,通常是网络拥塞,大量的重传又可能进一步加重拥塞。

上面的问题是因为单纯以时间驱动来进行重传,都必须等待一个超时时间,不能快速对当前网络情况作出响应,若是加入以数据驱动呢?TCP引入了一种叫Fast Retransmit(快速重传)的算法,就是在连续收到3次相同确认号的ACK,就进行重传。这个算法基于这么一个假设:连续收到3个相同的ACK,那么说明当前的网络情况变好了,能够重传丢失的包了。

快速重传解决了timeout的问题,可是没解决重传一个仍是重传多个的问题。出现难以决定是否重传多个包问题的根源在于,发送端不知道那些非连续序号的包已经到达接收端了,可是接收端是知道的,若是接收端告诉一下发送端不就能够解决这个问题吗?因而,RFC2018提出了Selective Acknowledgment(SACK,选择确认)机制,SACK是TCP的扩展选项,包括(1) SACK容许选项(Kind=4,Length=2,选项只容许在有SYN标志的TCP包中),(2) SACK信息选项(Kind=5,Length)。一个SACK的例子以下图,红框说明:接收端收到了0-5500,8000-8500,7000-7500,6000-6500的数据了,这样发送端就能够选择重传丢失的5500-6000,6500-7000,7500-8000的包。

图片描述

SACK依靠接收端的接收状况反馈,解决了重传风暴问题,这样够了吗?接收端可否反馈更多信息?显然是能够的,因而,RFC2883对SACK进行了扩展,提出了D-SACK,也就是利用第一块SACK数据中描述重复接收的不连续数据块的序列号参数,其余SACK数据则描述其余正常接收到的不连续数据。这样发送方利用第一块SACK,能够发现数据段被网络复制、错误重传、ACK丢失引发的重传、重传超时等异常的网络情况,使得发送端能更好调整本身的重传策略。D-SACK,有几个优势:

1)发送端能够判断出,是发包丢失了,仍是接收端的ACK丢失了。(发送方,重传了一个包,发现并无D-SACK那个包,那么就是发送的数据包丢了;不然就是接收端的ACK丢了,或者是发送的包延迟到达了);

2)发送端能够判断本身的RTO是否是有点小了,致使过早重传(若是收到比较多的D-SACK就该怀疑是RTO小了);

3)发送端能够判断本身的数据包是否是被复制了(若是明明没有重传该数据包,可是收到该数据包的D-SACK);

4)发送端能够判断目前网络上是否是出现了有些包被delay了,也就是出现先发的包却后到了。

疑症九:TCP的流量控制

咱们知道TCP的窗口(Window)是一个16bit位字段,它表明的是窗口的字节容量,也就是TCP的标准窗口最大为2^16-1=65535个字节。另外在TCP的选项字段中还包含了一个TCP窗口扩大因子,option-kind为3,option-length为3个字节,option-data取值范围0-14。窗口扩大因子用来扩大TCP窗口,可把原来16bit的窗口,扩大为31bit。这个窗口是接收端告诉发送端本身还有多少缓冲区能够接收数据。因而发送端就能够根据这个接收端的处理能力来发送数据,而不会致使接收端处理不过来。也就是,发送端是根据接收端通知的窗口大小来调整本身的发送速率的,以达到端到端的流量控制。尽管流量控制看起来简单明了,就是发送端根据接收端的限制来控制本身的发送就行了,可是细心的同窗仍是会有些疑问的:

(1) 发送端是怎么作到比较方便知道本身哪些包能够发,哪些包不能发?

(2) 若是接收端通知一个零窗口给发送端,这个时候发送端还能不能发送数据?若是不发数据,那一直等接收端口通知一个非0窗口吗,若是接收端一直不通知呢?

(3) 若是接收端处理能力很慢,这样接收端的窗口很快被填满,而后接收处理完几个字节,腾出几个字节的窗口后,通知发送端,这个时候发送端立刻就发送几个字节给接收端吗?发送的话会不会太浪费了,就像一艘万吨油轮只装上几斤的油就开去目的地同样。对于发送端产生数据的能力很弱也同样,若是发送端慢吞吞产生几个字节的数据要发送,这个时候该不应当即发送?仍是累积多点在发送?

1. 疑问(1)的解决

发送方要知道哪些能够发,哪些不能够发,一个简明的方案就是按照接收方的窗口通告,发送方维护一个同样大小的发送窗口就能够了,在窗口内的能够发,窗口外的不能够发,窗口在发送序列上不断后移,这就是TCP中的滑动窗口。以下图所示,对于TCP发送端其发送缓存内的数据均可以分为4类

[1] 已经发送并获得接收端ACK的;
[2] 已经发送但还未收到接收端ACK的;
[3] 未发送但容许发送的(接收方还有空间);
[4] 未发送且不容许发送(接收方没空间了)。

其中,[2]和[3]两部分合起来称之为发送窗口。

图片描述

下面两图演示窗口的滑动状况,收到36的ACK后,窗口向后滑动5个byte。

图片描述

图片描述

2. 疑问(2)的解决

由问题(1)咱们知道,发送端的发送窗口是由接收端控制的。下图,展现了一个发送端是怎么受接收端控制的。

图片描述

由上图咱们知道,当接收端通知一个Zero窗口时,发送端的发送窗口也变成了0,也就是发送端不能发数据了。若是发送端一直等待,直到接收端通知一个非零窗口在发数据的话,这彷佛太受限于接收端,若是接收端一直不通知新的窗口呢?显然发送端不能干等,起码有一个主动探测的机制。为解决0窗口的问题,TCP使用了Zero Window Probe技术,缩写为ZWP。发送端在窗口变成0后,会发ZWP的包给接收方,来探测目前接收端的窗口大小,通常这个值会设置成3次,每次大约30-60秒(不一样的实现可能会不同)。若是3次事后仍是0的话,有的TCP实现就会发RST关掉这个链接。正若有人的地方就会有商机,那么有等待的地方就颇有可能出现DDoS攻击点。攻击者能够在和Server创建好链接后,就向Server通告一个0窗口,而后Server端就只能等待进行ZWP,因而攻击者会并发大量这样的请求,把Server端的资源耗尽。

3. 疑问点(3)的解决

疑点(3)本质就是一个避免发送大量小包的问题。形成这个问题缘由有二:1) 接收端一直在通知一个小的窗口;2) 发送端自己问题,一直在发送小包。这个问题,TCP中有个术语叫Silly Window Syndrome(糊涂窗口综合症)。解决这个问题的思路有两条:1) 接收端不通知小窗口;2) 发送端积累一下数据再发送。

思路1)是在接收端解决这个问题,David D Clark’s方案,若是收到的数据致使Window Size小于某个值,就ACK一个0窗口,这就阻止发送端再发数据过来。等到接收端处理了一些数据后Windows Size大于等于MSS,或者buffer有一半为空,就能够通告一个非0窗口。思路2)是在发送端解决这个问题,有个著名的Nagle’s algorithm——Nagle算法的规则。

[1]若是包长度达到MSS,则容许发送;
[2]若是该包含有,FIN,则容许发送;
[3]设置了TCP_NODELAY选项,则容许发送;
[4]设置TCP_CORK选项时,若全部发出去的小数据包(包长度小于MSS)均被确认,则容许发送;
[5]上述条件都未知足,但发生了超时(通常为 200ms ),则当即发送。

规则[4]指出TCP链接上最多只能有一个未被确认的小数据包。从规则[4]能够看出Nagle算法并不由止发送小的数据包(超时时间内),而是避免发送大量小的数据包。因为Nagle算法是依赖ACK的,若是ACK很快的话,也会出现一直发小包的状况,形成网络利用率低。TCP_CORK选项则是禁止发送小的数据包(超时时间内),设置该选项后,TCP会尽力把小数据包拼接成一个大的数据包(一个MTU)再发送出去,固然也不会一直等,发生了超时(通常为200ms),也当即发送。Nagle算法和CP_CORK选项提升了网络利用率,但增长延时。从规则[3]能够看出,设置TCP_NODELAY选项,就是彻底禁用Nagle算法了。

这里要说一个小插曲,Nagle算法和延迟确认(Delayed Acknoledgement)一块儿,当出现(write-write-read)时会引起一个40ms的延时问题,这个问题在HTTP svr中体现得比较明显。场景以下:

客户端在请求下载HTTP svr中的一个小文件,通常状况下,HTTP svr都是先发送HTTP响应头部,而后再发送HTTP响应BODY(特别是比较多的实如今发送文件的实施采用的是sendfile系统调用,这就出现write-write-read模式了)。当发送头部的时候,因为头部较小,造成一个小的TCP包发送到客户端,这个时候开始发送body,因为body也较小,这样仍是造成一个小的TCP数据包,根据Nagle算法,HTTP svr已经发送一个小的数据包了,在收到第一个小包的ACK后或等待200ms超时后才能再发小包,HTTP svr不能发送这个body小TCP包;

客户端收到http响应头后,因为这是一个小的TCP包,因而客户端开启延迟确认,客户端在等待Svr的第二个包来再一块儿确认或等待一个超时(通常是40ms)再发送ACK包;这样就出现了你等我、然而我也在等你的死锁状态,因而出现最多的状况是客户端等待一个40ms的超时,而后发送ACK给HTTP svr,HTTP svr收到ACK包后再发送body部分。你们在测HTTP svr的时候就要留意这个问题了。

疑症十:TCP的拥塞控制

谈到拥塞控制,就要先谈谈拥塞的因素和本质。本质上,网络上拥塞的缘由就是你们都想独享整个网络资源,对于TCP,端到端的流量控制必然会致使网络拥堵。这是由于TCP只看到对端的接收空间的大小,而没法知道链路上的容量,只要双方的处理能力很强,那么就能够以很大的速率发包,因而链路很快出现拥堵,进而引发大量的丢包,丢包又引起发送端的重传风暴,进一步加重链路拥塞。另一个拥塞的因素是链路上的转发节点,例如路由器,再好的路由器只要接入网络,老是会拉低网络的总带宽,若是在路由器节点上出现处理瓶颈,那么就很容易出现拥塞。因为TCP看不到网络的情况,那么拥塞控制是必须的而且须要采用试探性的方式来控制拥塞,因而拥塞控制要完成两个任务:(1) 公平性;(2) 拥塞事后的恢复。

TCP发展到如今,拥塞控制方面的算法不少,其中Reno是目前应用最普遍且较为成熟的算法,下面着重介绍一下Reno算法(RFC5681)。介绍该算法前,首先介绍一个概念Duplicate Acknowledgment(冗余ACK、重复ACK)通常状况下一个ACK被称为冗余ACK,要同时知足下面几个条件(对于SACK,那么根据SACK的一些信息来进一步判断):

[1] 接收ACK的那端已经发出了一些还没被ACK的数据包
[2] 该ACK没有捎带data
[3] 该ACK的SYN和FIN位都是off的,也就是既不是SYN包的ACK也不是FIN包的ACK。
[4] 该ACK的确认号等于接收ACK那端已经收到的ACK的最大确认号
[5] 该ACK通知的窗口等接收该ACK的那端上一个收到的ACK的窗口

Reno算法包含4个部分:(1) 慢热启动算法–Slow Start;(2) 拥塞避免算法–Congestion Avoidance;(3) 快速重传-Fast Retransimit;(4) 快速恢复算法–Fast Recovery。TCP的拥塞控制主要原理依赖于一个拥塞窗口(cwnd)来控制,根据前面的讨论,咱们知道有一个接收端通告的接收窗口(rwnd)用于流量控制;加上拥塞控制后,发送端真正的发送窗口=min(rwnd, cwnd)。关于cwnd的单位,在TCP中是以字节来作单位的,咱们假设TCP每次传输都是按照MSS大小来发送数据,所以你能够认为cwnd按照数据包个数来作单位也能够理解,下面若是没有特别说明是字节,那么cwnd增长1也就是至关于字节数增长1个MSS大小。

1. 慢热启动算法–Slow Start:

慢启动体现了一个试探的过程,刚接入网络的时候先发包慢点,探测一下网络状况,而后在慢慢提速。不要一上来就拼命发包,这样很容易形成链路的拥堵,出现拥堵了在想到要降速来缓解拥堵这就有点成本高了,毕竟无数的先例告诫咱们先污染后治理的成本是很高的。慢启动的算法以下(cwnd全称Congestion Window):

1)链接建好的开始先初始化cwnd = N,代表能够传N个MSS大小的数据
2)每当收到一个ACK,++cwnd; 呈线性上升
3)每当过了一个RTT,cwnd = cwnd*2; 呈指数让升
4)还有一个慢启动门限ssthresh(slow start threshold),是一个上限,当cwnd >= ssthresh时,就会进入”拥塞避免算法-Congestion Avoidance”

根据RFC5681,若是MSS > 2190 bytes,则N = 2;若是MSS < 1095 bytes,则N = 4;若是2190 bytes >= MSS >= 1095 bytes,则N = 3。一篇Google的论文《An Argument for Increasing TCP’s Initial Congestion Window》建议把cwnd初始化成了10个MSS。Linux 3.0后采用了这篇论文的建议。

2. 拥塞避免算法–Congestion Avoidance:

慢启动的时候说过,cwnd是指数快速增加的,可是增加是有个门限ssthresh(通常来讲大多数的实现ssthresh的值是65535字节)的,到达门限后进入拥塞避免阶段。在进入拥塞避免阶段后,cwnd值变化算法以下:

(1) 每收到一个ACK,调整cwnd为(cwnd + 1/cwnd)*MSS个字节
(2) 每通过一个RTT的时长,cwnd增长1个MSS大小。

TCP是看不到网络的总体情况的,那么TCP认为网络拥塞的主要依据是它重传了报文段。前面咱们说过TCP的重传分两种状况:

(1) 出现RTO超时,重传数据包。这种状况下,TCP就认为出现拥塞的可能性就很大,因而它反应很是’强烈’:

1) 调整门限ssthresh的值为当前cwnd值的1/2。
2) reset本身的cwnd值为1
3) 而后从新进入慢启动过程。

(2) 在RTO超时前,收到3个duplicate ACK进行重传数据包。这种状况下,收到3个冗余ACK后说明确实有中间的分段丢失,然然后面的分段确实到达了接收端,由于这样才会发送冗余ACK,这通常是路由器故障或者轻度拥塞或者其它不太严重的缘由引发的,所以此时拥塞窗口缩小的幅度就不能太大,此时进入快速重传。

3. 快速重传-Fast Retransimit作的事情有:

(1) 调整门限ssthresh的值为当前cwnd值的1/2;
(2) 将cwnd值设置为新的ssthresh的值;
(3) 从新进入拥塞避免阶段。

在快速重传的时候,通常网络只是轻微拥堵,在进入拥塞避免后,cwnd恢复的比较慢。针对这个,“快速恢复”算法被添加进来,当收到3个冗余ACK时,TCP最后的[3]步骤进入的不是拥塞避免阶段,而是快速恢复阶段。

4. 快速恢复算法–Fast Recovery:

快速恢复的思想是“数据包守恒”原则,即带宽不变的状况下,在网络同一时刻能容纳数据包数量是恒定的。当“老”数据包离开了网络后,就能向网络中发送一个“新”的数据包。既然已经收到了3个冗余ACK,说明有三个数据分段已经到达了接收端,既然三个分段已经离开了网络,那么就是说能够在发送3个分段了。因而只要发送方收到一个冗余的ACK,因而cwnd加1个MSS。快速恢复步骤以下(在进入快速恢复前,cwnd 和 sshthresh已被更新为:sshthresh = cwnd /2,cwnd = sshthresh):

(1) 把cwnd设置为ssthresh的值加3,重传Duplicated ACKs指定的数据包

(2) 若是再收到 duplicated Acks,那么cwnd = cwnd +1

(3) 若是收到新的ACK,而非duplicated Ack,那么将cwnd从新设置为3.中(1)的sshthresh的值。而后进入拥塞避免状态。

细心的同窗可能会发现快速恢复有个比较明显的缺陷就是:它依赖于3个冗余ACK,并假定不少状况下,3个冗余的ACK只表明丢失一个包。可是3个冗余ACK也颇有多是丢失了不少个包,快速恢复只是重传了一个包,而后其余丢失的包就只能等待到RTO超时了。超时会致使ssthresh减半,而且退出了Fast Recovery阶段,多个超时会致使TCP传输速率呈级数降低。出现这个问题的主要缘由是过早退出了Fast Recovery阶段。为解决这个问题,提出了New Reno算法,该算法是在没有SACK的支持下改进Fast Recovery算法(SACK改变TCP的确认机制,把乱序等信息会所有告诉对方,SACK自己携带的信息就能够使得发送方有足够的信息来知道须要重传哪些包,而不须要重传哪些包),具体改进以下:

1) 发送端收到3个冗余ACK后,重传冗余ACK指示可能丢失的那个包segment1,若是segment1的ACK通告接收端已经收到发送端的所有已经发出的数据的话,那么就是只丢失一个包,若是没有,那么就是有多个包丢失了。

2) 发送端根据segment1的ACK判断出有多个包丢失,那么发送端继续重传窗口内未被ACK的第一个包,直到sliding window内发出去的包全被ACK了,才真正退出Fast Recovery阶段。

咱们能够看到,拥塞控制在拥塞避免阶段,cwnd是加性增长的,在判断出现拥塞的时候采起的是指数递减。为何要这样作呢?这是出于公平性的原则,拥塞窗口的增长受惠的只是本身,而拥塞窗口减小受益的是你们。这种指数递减的方式实现了公平性,一旦出现丢包,那么当即减半退避,能够给其余新建的链接腾出足够的带宽空间,从而保证整个的公平性。

至此,TCP的疑难杂症基本介绍完毕了,总的来讲TCP是一个有链接的、可靠的、带流量控制和拥塞控制的端到端的协议。TCP的发送端能发多少数据,由发送端的发送窗口决定(固然发送窗口又被接收端的接收窗口、发送端的拥塞窗口限制)的,那么一个TCP链接的传输稳定状态应该体如今发送端的发送窗口的稳定状态上,这样的话,TCP的发送窗口有哪些稳定状态呢?TCP的发送窗口稳定状态主要有下面三种稳定状态:

【1】接收端拥有大窗口的经典锯齿状

大多数状况下都是处于这样的稳定状态,这是由于,通常状况下机器的处理速度就是比较快,这样TCP的接收端都是拥有较大的窗口,这时发送端的发送窗口就彻底由其拥塞窗口cwnd决定了;网络上拥有成千上万的TCP链接,它们在相互争用网络带宽,TCP的流量控制使得它想要独享整个网络,而拥塞控制又限制其必要时作出牺牲来体现公平性。因而在传输稳定的时候TCP发送端呈现出下面过程的反复:

[1]用慢启动或者拥塞避免方式不断增长其拥塞窗口,直到丢包的发生;
[2]而后将发送窗口将降低到1或者降低一半,进入慢启动或者拥塞避免阶段(要看是因为超时丢包仍是因为冗余ACK丢包),过程以下图:

图片描述

【2】接收端拥有小窗口的直线状态

这种状况下是接收端很是慢速,接收窗口一直很小,这样发送窗口就彻底有接收窗口决定了。因为发送窗口小,发送数据少,网络就不会出现拥塞了,因而发送窗口就一直稳定的等于那个较小的接收窗口,呈直线状态。

【3】两个直连网络端点间的满载状态下的直线状态

这种状况下,Peer两端直连,而且只有位于一个TCP链接,那么这个链接将独享网络带宽,这里不存在拥塞问题,在他们处理能力足够的状况下,TCP的流量控制使得他们可以跑慢整个网络带宽。

经过上面咱们知道,在TCP传输稳定的时候,各个TCP链接会均分网络带宽的。相信你们学生时代常常会发生这样的场景,本身在看视频的时候忽然出现视频卡顿,因而就大叫起来,哪一个开了迅雷,赶忙给我停了。其实简单的下载加速就是开启多个TCP链接来分段下载就达到加速的效果,假设宿舍的带宽是1000K/s,一开始两个在看视频,每人平均网速是500k/s,这速度看起视频来那叫一个顺溜。忽然其中一个同窗打打开迅雷开着99个TCP链接在下载爱情动做片,这个时候平均下来你能分到的带宽就剩下10k/s,这网速下你的视频还不卡成幻灯片。在通讯链路带宽固定(假设为W),多人公用一个网络带宽的状况下,利用TCP协议的拥塞控制的公平性,多开几个TCP链接就能多分到一些带宽(固然要忽略有些用UDP协议带来的影响),然而无论怎么最多也就能把整个带宽抢到,因而在占满整个带宽的状况下,下载一个大小为FS的文件,那么最快须要的时间是FS/W,难道就没办法加速了吗?

答案是有的,这样由于网络是网状的,一个节点是要和不少几点互联的,这就存在多个带宽为W的通讯链路,若是咱们可以将要下载的文件,一半从A通讯链路下载,另一半从B通讯链路下载,这样整个下载时间就减半了为FS/(2W),这就是p2p加速。相信你们学生时代在下载爱情动做片的时候也遇到过这种状况,明明外网速度没这么快的,本身下载的爱情动做片的速度却达到几M/s,那是由于,你的左后或右后的宿友在帮你加速中。咱们都知道P2P模式下载会快,而且越多人下载就越快,那么问题来了,P2P下载加速理论上的加速比是多少呢?

附加题1:P2P理论上的加速比

传统的C/S模式传输文件,在跑满Client带宽的状况下传输一个文件须要耗时FS/BW,若是有n个客户端须要下载文件,那么总耗时是n*(FS/BW),固然啦,这并不必定是串行传输,能够并行来传输的,这样总耗时也就是FS/BW了,可是这须要服务器的带宽是n个client带宽的总和n*BW。C/S模式一个明显的缺点是服务要传输一个文件n次,这样对服务器的性能和带宽带来比较大的压力,我能够换下思路,服务器将文件传给其中一个Client后,让这些互联的Client本身来交互那个文件,那服务器的压力就减小不少了。这就是P2P网络的好处,P2P利用各个节点间的互联,提倡“人人为我,我为人人”。

知道P2P传输的好处后,咱们来谈下理论上的最大加速比,为了简化讨论,一个简单的网络拓扑图以下,有4个相互互联的节点,而且每一个节点间的网络带宽是BW,传输一个大小为FS的文件最快的时间是多少呢?假设节点N1有个大小为FS的文件须要传输给N2,N3,N4节点,一种简单的方式就是:节点N1同时将文件传输给节点N2,N3,N4耗时FS/BW,这样你们都拥有文件FS了。你们能够看出,整个过程只有节点1在发送文件,其余节点都是在接收,彻底违反了P2P的“人人为我,我为人人”的宗旨。那怎么才能让你们都作出贡献了呢?解决方案是切割文件。

(1) 首先,节点N1 文件分红3个片断FS二、FS三、FS4,接着将FS2发送给N2,FS3发送给N3,FS4发送给N4,耗时FS/(3*BW)

(2) 而后,N2,N3,N4执行“人人为我,我为人人”的精神,将本身拥有的F2,F3,F4分别发给没有的其余的节点,这样耗时FS/(3*BW)完成交换。

因而总耗时为2*FS/(3*BW)完成了文件FS的传输,能够看出耗时减小为原来的2/3了,若是有n个节点,那么时间就是原来的2/(n-1),也就是加速比是2/(n-1),这就是加速的理论上限了吗?还没发挥最多能量的,相信你们已经看到分割文件的好处了,上面的文件分割粒度仍是有点大,以致于,在第二阶段(2)传输过程当中,节点N1无所事事。为了最大化发挥你们的做用,咱们须要将FS二、FS三、FS4再进行分割,假设将它们都均分为K等份,这样就有FS21,FS22…FS2K、FS31,FS32…FS3K、FS41,FS42…FS4K,一共3K个分段。因而下面就开始进行加速分发:

[1]节点N1将分段FS21,FS31,FS41分别发送给N2,N3,N4节点。耗时,FS/(3K*BW)

[2]节点N1将分段FS22,FS32,FS42分别发送给N2,N3,N4节点,同时节点N2,N3,N4将阶段[1]收到的分段相互发给没有的节点。耗时,FS/(3K*BW)

……

[K]节点N1将分段FS2K,FS3K,FS4K分别发送给N2,N3,N4节点,同时节点N2,N3,N4将阶段[K-1]收到的分段相互发给没有的节点。耗时,FS/(3K*BW)

[K+1]节点N2,N3,N4将阶段[K]收到的分段相互发给没有的节点。耗时,FS/(3K*BW)

因而总的耗时为(K+1)*(FS/(3K*BW))=FS/(3*BW)+FS/(3K*BW),当K趋于无穷大的时候,文件进行无限细分的时候,耗时变成了FS/(3*BW),也就是当节点是n+1的时候,加速比是n。这就是理论上的最大加速比了,最大加速比是P2P网络节点个数减1。

图片描述

附加题2:系统调用listen()的backlog参数指的是什么

要说明backlog参数的含义,首先须要说一下Linux的协议栈维护的TCP链接的两个链接队列:[1]SYN半链接队列;[2]accept链接队列

[1] SYN半链接队列:Server端收到Client的SYN包并回复SYN,ACK包后,该链接的信息就会被移到一个队列,这个队列就是SYN半链接队列(此时TCP链接处于 非同步状态)

[2] accept链接队列:Server端收到SYN,ACK包的ACK包后,就会将链接信息从[1]中的队列移到另一个队列,这个队列就是accept链接队列(这个时候TCP链接已经创建,三次握手完成了)

用户进程调用accept()系统调用后,该链接信息就会从[2]中的队列中移走。

相信很多同窗就backlog的具体含义进行争论过,有些认为backlog指的是[1]和[2]两个队列的和。而有些则认为是backlog指的是[2]的大小。其实,这两个说法都对,在linux kernel 2.2以前backlog指的是[1]和[2]两个队列的和。而2.2之后,就指的是[2]的大小,那么在kernel 2.2之后,[1]的大小怎么肯定的呢?两个队列的做用分别是什么呢?

1. SYN半链接队列的做用

对于SYN半链接队列的大小是由(/proc/sys/net/ipv4/tcp_max_syn_backlog)这个内核参数控制的,有些内核彷佛也受listen的backlog参数影响,取得是两个值的最小值。当这个队列满了,Server会丢弃新来的SYN包,而Client端在屡次重发SYN包得不到响应而返回(connection time out)错误。可是,当Server端开启了syncookies,那么SYN半链接队列就没有逻辑上的最大值了,而且/proc/sys/net/ipv4/tcp_max_syn_backlog设置的值也会被忽略。

2. accept链接队列

accept链接队列的大小是由backlog参数和(/proc/sys/net/core/somaxconn)内核参数共同决定,取值为两个中的最小值。当accept链接队列满了,协议栈的行为根据(/proc/sys/net/ipv4/tcp_abort_on_overflow)内核参数而定。 若是tcp_abort_on_overflow=1,server在收到SYN_ACK的ACK包后,协议栈会丢弃该链接并回复RST包给对端,这个是Client会出现(connection reset by peer)错误。若是tcp_abort_on_overflow=0,server在收到SYN_ACK的ACK包后,直接丢弃该ACK包。这个时候Client认为链接已经创建了,一直在等Server的数据,直到超时出现read timeout错误。


参考资料


编辑推荐:架构技术实践系列文章(部分):


图片描述