不容易啊,天气热得厉害,终于到了周末却哪里也去不了,昨晚就特意向老婆申请了一段不长不短的周末时间用来总结近期的工作,也实属不易,如果申请没有获得批准,我也只好利用夜晚了,
因为我几乎是一个不用怎么睡觉,可吃可不吃的人,只要有水,烧酒,就好了...大早上的,热醒了,看来也用不到我申请的时间了。
...此时是早上4点半...
RFC2018描述了TCP SACK的规范,主要是规范了SACK的定义以及在使用该特性的时候,发送端和接收端的行为建议。RFC文档本身非常容易读懂,建议通读一遍,花不了半小时,我之所以还要写这篇文章,一来是为了存档,二来也是为了描述一种自认为更容易让人理解的方式来阐述SACK。行文之前,先给出一幅本文会一直用到的图,描述一下在支持SACK的TCP连接中,一个ACK到来的时候发送队列被分割成的区间:
1.数据发送端对SACK的处理
为了简单又快速的描述发送端对SACK的处理,我们先定义一堆变量,如下所示:
prior UNA:上一次的UNA
UNA:本次的UNA【prior UNA和UNA可能会相等】
S[i]:SKB里面的sack选项里面的每一个sack段
P[j]:传输队列中的每一个数据包
DSACK:记录此次SACK段中是否有DSACK
undo:记录undoable的计数器,每次重传一个包会递增1,每次被DSACK一个包会递减1,如果为0说明可以undo了,重发的都白发了。
reord:记录当前检测到的乱序度
sack_out:所有被SACK的数据包个数
retrans_out:所有已经被重传的数据包个数
lost_out:所有已经丢失的数据包的个数
然后我给出一个简单的数据发送端处理SACK的算法,记为算法A:
【阶段1:判断是否包含DSACK】
if S[0].Left < UNA
DSACK=1【说明是DSACK,DSACK的第一种情况】
else if S[0]被S[1]包含
DSACK=1【说明是DSACK,DSACK的第二种情况】
else
DSACK=0
endif
【阶段2:扫描数据包TCP option中的所有的SACK段】
【阶段2-1:前奏!首先处理区间0(如开篇的图所示),此处没有任何数据包】
if S[0].Right < prior UNA
undo=undo-1;
endif
【阶段2-2:主体】
i=0
for each S[i]; do
j=0
fack=0
【阶段2-1-1:对于每一个S[i]块遍历每一个传输队列中的数据包】
for each P[j]; do
fack=fack+1 【记录当前的P[j]的位置】
if P[j]被S[i]覆盖 && DSACK==1
undo=undo-1
endif
【阶段2-1-1-1:处理区间1(如开篇的图所示)的SACK(DSACK),此处的数据包将要在接下来被删除】
if P[j].Right < UNA || P[j].Right == UNA
if P[j]被重传过
if DSACK==1 && P[j]被S[i]所覆盖 && P[j]曾经被SACK过
【更新乱序度1:重传过,被SACK过,又被SACK了,说明原始包没有丢失,传了两遍,得到两遍SACK,需要记录包位置】
reord=min(fack,reord)
else if ...
【小问题:如果仅仅是重传过,但是没有被SACK过,说明此DSACK是第一次该数据包被确认,是否也可激进地认定为乱序而更新reord值呢??】
endif
else
if fack < 最高的被SACK的位置 && P[j]从没有被SACK过
【更新乱序度2:该P[j]处在一个空洞中】
【这意味着,该P[j]是被推进的ACK确认的,之前从没有被SACK过,说明检测到了乱序】
reord=min(fack,reord)
endif
endif
continue 阶段2-1-1 【此区间1的包将要被clean,继续下一个】
endif
【阶段2-1-1-2:处理区间2(如开篇的图所示),这个处理比较简单,例行的扫描,置SACKed位】
if P[j]没有被S[i]覆盖
continue 阶段2-1-1
endif
if P[j].sack == 0
【新的SACK确认了标记为LOST/RETRANS的段,说明重传成功了】
if P[j]曾经标记为LOST && P[j]被重传过
清除P[j]的LOST和RETRANS标记
lost_out=lost_out-1
retrans_out=retrans_out-1
endif
if P[j]没有被标记为重传过 && fack < 最高的被SACK的位置
【这说明什么,这说明P[j]的SACK是原始数据包引发的,且在最高被SACK的包之后被SACK,赤裸裸的乱序!】
【更新乱序度3:在区间2最高被SACK包之前的空洞被填补(倒序被SACK),注意,该填补并不是重传引起的,而是原始数据包乱序到达引起的】
reord=min(fack,reord)
endif
P[j].sack=1
sack_out=sack_out+1
else
if DSACK == 1 && P[j]被重传过
【更新乱序度4:在区间2被检测出来了DSACK(一个SACK block被另一个block包含或者重合),且当前P[j]处在S[0]的覆盖下】
【P[j]被重传过,且获得了SACK,此时的S[0]携带的DSACK又一次确认了它(同时S[1]也会确认它,因为S[1]包含S[0]),检测到了一个乱序】
reord=min(fack,reord)
retrans_out=retrans_out-1
endif
endif
endfor
DSACK=0 【仅仅在S[0]时处理DSACK】
endfor
请注意,以上是《TCP发送端收到ACK后对传输队列的4次扫描》中的第一次扫描中的第一部分,在第一次扫描中还有以下任务:
a).标记哪些已经重传的包可能丢失
b).更新网络乱序度reordering
并没有包含在算法A当中!
另外值得注意的是,以下算法是可用的,但不是最优的。我们看到算法涉及了两层的遍历,一层针对SACK block,另一层针对传输队列中仍未被清除的数据包,算法中并未对SACK block进行排序和skip优化,这样做的目的在于其简单可读性,你可以最快的速度理解大概,然后再去抠细节。
2.数据接收端对SACK的处理
这个是比较简单的,我的建议是通读RFC2018《TCP Selective Acknowledgment Options》,然而我还是希望可以基于这个RFC,在本文中抽取出一些重要的东西。注意其section 4,描述了接收端的行为,加上其它的标准,归纳起来,就是下面的这些:
2.1.关于第一个SACK block的构造
接收端构造的第一个SACK block必须精确反应刚刚收到的乱序数据包,考虑以下序列:rcv1|rcv2|hole|rcv3|hole|rcv4,如果乱序收到了rcv4和rcv3,rcv4是之前乱序收到的,当前乱序收到了rcv3,那么在构造SACK block的时候,第一个block就是rcv3。
2.2.SACK block要包含哪些数据包
接收端要尽可能多的在SACK block中包含已经乱序收到的数据包,直到达到TCP option的长度限额,这些block的顺序要表示“最近收到了哪些数据包”,越近收到的数据包(措辞不准确,按照TCP标准,应该说是数据段!)应该排在越前面,这个比较类似LRU链表的原理。这样可以帮助数据发送端做精确的决策。
2.3.接收端何时回复携带SACK block的ACK
标准规定,只要接收端的接收队列中存在乱序数据包,就必须在收到任何数据的时候,立即恢复ACK,即便启用了delay ACK也必须立即回复。
3.RFC中的一个例子极其扩展
左边窗口边沿序列号是5000,传输了8个数据包,MSS为500,如下所示:
P0:4500-4999|P1:5000-5499|P2:5500-5999|P3:6000-6499|P4:6500-6999|P5:7000-7499|P6:7500-7999|P7:8000-8499|P8:8500-8999|
现在P2,P4,P6,P8丢失:
|P1:5000-5499|P2:LOST|P3:6000-6499|P4:LOST|P5:7000-7499|P6:LOST|P7:8000-8499|P8:LOST|
P1,P3,P5,P7被成功接收,其中P1是顺序被接收的,因此会推进ACK,而P3,P5,P7是乱序接收的,因此会让针对5500的重复ACK携带SACK,如下图所示:
设想P4被乱序接收时的情景,它会填补P3和P5之间的空洞,将P3-P5连成一片,为了展示“最近被SACK”,P3(6000-6499)-P5(7000-7499)会被置为SACK block的第一个block,即(6000---7500),如下图所示:
P2此时也被乱序接收了(假设它并没有丢失,只是多径延迟乱序到达了),此时ACK可以向前推到7500了,这意味这P6(7500-7999)是下一个要接收的包,同时由于乱序队列中还有一个P7(8000-8499),因此这个ACK到7500的确认包会携带唯一的SACK block(8000-8499)向数据发送端返回,记为ACK7500,如下图所示:
接下来我们看下DSACK的情况,什么时候会处理区间1(如开篇的图所示)的SACK呢?
P0:4500-4999|P1:5000-5499|P2:5500-5999|P3:6000-6499|P4:6500-6999|P5:7000-7499|P6:7500-7999|P7:8000-8499|P8:8500-8999|
此时P0,P1,P2,P4,P6,P8丢失,除了P1与RFC的例子不同以及引入新的P0,其它和RFC的例子一致。考虑重复ACK,即ACK4500已经到达发送端并且已经触发了重传,并且重传了P0,P1,P2,而P2成功到达了接收端,P0,P1则没有到达,此时的SACK block将会包含P2,被发送端收到后会设置P2为SACKed,此时收到了P0,P1,ACK向前推进到6500(P4丢失,P3原本就没有丢失,被SACK),记为ACK6500,然而此时ACK6500丢失了,没有到达发送端,在这个时候,原始的P2也到了,注意P2原本没有丢失,只是可能由于多径而延迟到达了,重传的P2已经收到,并且已经SACK了。此时接收端发现P2在ACK6500左边,而ACK6500早就已经发出了(接收端意识不到ACK丢失),所以会认为这是一个DSACK,因此会构造以下的携带DSACK的ACK6500d:
以上这个ACK6500d到达发送端的时候,执行算法A,此时prior UNA为5500,UNA为6500,而S[0].Right为5500,小于UNA,便会进入区间1的处理,处理详情,参见算法A(注意算法A中提出的小问题)
以上的序列有点复杂,在大多数情况下,这种情况发生的概率比较低,除非网络乱序度非常大,大多数情况是以下两类:
a).重复ACK携带了SACK:此时prior UNA和UNA相等;
b).向前推进的ACK携带了SACK,SACK仅在区间2.
4.更新网络乱序度reordering的一点细节
在第一部分的算法A中,我们记录了一个变量reord,它表示传输队列中的最左边被SACK/DSACK标记的数据包的位置(相对于传输队列头部来说),根据开篇的图所示,prior UNA和UNA将传输队列划分的区间来看,可以分为以下几种情况:
1).DSACK落在了区间0
不更新reordering。因为该区间的所有数据包已经被确认,从传输队列中早已被清除,我们没有办法通过数据包的一些标志位探测它之前的行为,比如是否被重传过,是否被标记为SACK等。
2).DSACK落在了区间1
此时需要考虑两种情况:
2.1).prior UNA与UNA相等
重复ACK的情况。此时区间1坍缩,其前面就是区间0,理由同上面处理区间0的DSACK一样,不会更新reordering。
2.2).prior UNA小于UNA
ACK向前推进UNA的情况。此时会有两种情况下检测到乱序,一种是区间1中有数据包被重传且收到过SACK,当下又收到了DSACK,这说明之前的原始数据包没有丢,这种情况的reordering更新如下图所示:
另一种情况就是发现该区间的一个数据包在本次UNA推进之前始终是一个空洞,既没有被SACK,也没有重传过,且UNA推进之前被SACK的最高的包在它之前,说明它被ACK就是本次UNA推进所为,它之后的数据包都被SACK了,它直到现在才被ACK,说明有乱序,reordering更新如下图所示:
3).DSACK落在了区间2
区间2的乱序检测亦有两种情况。第一种是发现最高的被SACK的数据包前面的一个空洞被填补的情况,该空洞从来没有被重传过,也没有被SACK过,如下图所示:
第二种情况是,发现区间2中有一个数据包已经被重传过,且已经标记为SACK了,说明了重传的成功或者是原始包到达了接收端,然而当下的一个DSACK又一次确认了它,这说明肯定有两个P[j]到达了对端,发送端总共就发了两个,一个原始P[j],一个重传P[j],说明原始数据包没有丢失,乱序了。
5.收到了UNA之前的ACK怎么办
这种情况下,直接丢掉是最直接的办法,但是仔细想想,这种确认包还是可以从中得到一些信息的,比如:
a).可以从其SACK block中获取一些关于DSACK的信息,这种信息可能会让我们发现网络的乱序,递减undo计数器,从而指导我们在拥塞状态下的UNDO操作;
b).可以依照根据RFC3517的section 5中(C)例程刷重传队列,其NextSeg例程可能会返回一系列数据,然后发送之:
The ACK value is considered acceptable only if it is in the range of ((SND.UNA - MAX.SND.WND) <= SEG.ACK <= SND.NXT).
值得注意的是,在Linux的协议栈实现中,tcp_xmit_retransmit_queue并不会实际发送新数据,它仅仅在没有标记为LOST的数据要发送但是有新数据等待发送的时候简单地返回!然后发送新数据的任务就由发送路径在轮到它跑的时候发送了。
但是这样意味着什么你注意到了吗?这样很可能带来安全问题!如果我们看算法A,发现里面有两层循环,其时间复杂度自己算算便知,即便是优化版本也好不到哪去。如果是有恶意的人构造了一些携带SACK的包重放到网络,在传输队列非常大的时候将会使算法A(或者任意的优化版本[无非就是增加一些排序,skip...])消耗巨量的CPU时间,因此收到UNA之前的ACK以期待能为我所用这种想法是利弊兼得的。因此RFC5961里面提到了一种折衷,那就是,在UNA之前一个窗口内的ACK是可以利用的,一个窗口外的数据距离太远了,就会被认为是重放攻击,直接丢弃即可。
6.我是怎么申请到时间的
答案当然是报进度了,要告诉老婆这段时间会获得什么收益,然后她会问,现在情况怎么样,对答之,再问遇到了什么困难,对答之,继续问,什么时候可以完成,对答之,...如果一个没有回答好,基本就不要硬抗了,还是去扫地买菜做饭刷碗吧,也是一种不错的体验,等到周末结束的时候,可以写一道菜谱分享给大家,也算是比较有收获了。
不管怎么样吧,眼里有活儿,心里有上帝,这种新教信仰我觉得就是欧洲文明后起而勃发的根源....有点远了,然而还是要去买菜...
...此时是早上10点...