TCP原理描述一
本篇介绍非具体的可靠协议的原理,并且暂时没有配图(有时间再配图),可以拿笔自己画一画。
TCP可靠原理概述
? TCP可靠性的原理,并不是像我们在日常生活的说的那样「他是一个很可靠的人」这种玄学的东西。初学TCP原理时,我经常会不理解于**为什么既然下层信道是不可靠的,TCP基于这种不可靠的东西还能实现可靠传输。**实际上 这基于互联网提供的网络服务模型的一个特性 : 不保证具有时延上界的确保交付。无论是TCP还是UDP,都不能保证发送的数据报一定会在某个时间点前到达。不过ATM网络体系结构的一种实现是保证这一点的。
? TCP的可靠机制可以用简短的两个字:重传来概括。墨菲定律指出,如果某件事有概率会发生,那么这件事一定会在将来的某个时间点发生。假设有一个猴子坐在电脑前乱敲键盘,那么在未来的某一时刻,它一定可以敲出莎士比亚的名著(假设猴子寿命无限,电脑寿命无限)。对TCP而言,我这次发的数据包没有原封不动的发送到对方,我就一直重发,直至完好无损的发送的发送到对面。
实现可靠传输协议
? 实现可靠传输一般有两种做法,一是数据传输的链路可靠,不丢失,不出错。二是利用一些机制实现可靠传输。第一种自然是不大可行,可靠传输大都指的第二种。下面将逐渐构造出一个抽象的可靠数据传输协议(reliable data transfer protocol,简称rdt)。而并非说实现TCP,TCP也只是在抽象的可靠协议上增加了一些优化的东西。
? 我们假设数据传送有接收方和发送方,实际上数据传送时每一方既可以发送数据,也可以接受数据,这里是做简化。我们先从一个理想的可靠信道逐渐增加不稳定因素,直到信道和实际环境情况一样,这样就实现了可靠协议。
rdt1.0
假设数据传输的信道模型是可靠的(好比借助TCP实现的应用无需关心数据丢失),那么发送方就无需做任何操作,只需发送数据即可,接收方也只管接受数据即可。
发送方和接收方动作的伪代码描述
//发送方动作
while(true) {
if(上层应用调用,开始发送报文)
send_data();
}
//接收方动作
while(true) {
if(收到数据报文)
rcv_data(); // 交给上层应用
}
rdt2.0
现在假设底层的信道模型并非绝对可靠,而是存在传输的bit位出现受损的情况。比如我们对传输的数据设置一个校验和字段,接收方接受到数据后计算校验和发现数据出错。为了收到无损的数据,现在接收方有两种选择:一是尝试利用校验和将出错的比特位恢复过来。二是丢弃这个分组,并告诉发送方,你发的分组有问题,请你重新发送一下。对于第一种应对方式,需要增加太多冗余的字段,而且太过于复杂。一般采用的都是第二种方式,接收方发送一个NAK报文,代表发的报文有问题,告诉发送方重发。如果完好无损的接受到了报文,那么发送一个ACK报文,代表发送方可以继续发送下一个报文了。而发送方发送一个报文后,需要等到收到ACK报文后才能发送下一个报文。基于这样重传机制的可靠数据传输协议被称为自动重传请求协议 ARQ(Automatic Repeat reQuest)
看样子基于这种模型,我们已经实现了可靠传输。实际上接收方发送的NAK/ACK报文也有可能出现受损的情况,由于用于确认的报文数据量不是很大,利用校验和发送也是行得通的。或者发送方如果收到模棱两可的报文段,发送方就无脑发送上一个报文段。如果接收方发送的报文实际上是NAK,发送方的操作就正好就符合要求。如果发送的报文是ACK,接收方就丢弃这个数据报文,并继续发送一个ACK。
这样发送方和接收方根据对方的反馈决定是否要重传自己的报文,基于这样信道模型就实现了。不过还有一点地方要说明:NAK报文的语义实际上可以当作对上一个接收到的完好无损报文的确认。我们可以用ACK0来指代,对于本次发送的报文,我们可以用ACK1来指代。(因为rdt2.0一次只发一个报文,不收到确认是不继续下一报文的发送的)
发送方和接收方动作的伪代码描述
//发送方动作
while(true) {
if(上层应用调用) {
send_data();
while(等待接收方报文) {
if(ACK1)
break;
else
send_data();
}
}
}
//接收方动作
while(true) {
if(接收到报文) {
if(数据无损 && 是当前需要接收的报文) {
rcv_data();
send_ack1();
}else
send_ack0();
}
rdt3.0
? rdt2.0中的信道模型还不足以描述真实的网络环境,除了可能出现bit位差错。还可能出现丢包现象。路由器的输入缓存满时,便会丢掉后来的数据包,有些数据在链路上走着走着也可能丢失不见。如果不应付这种情况,通信双方的进程便会陷入死锁,都在等待对方给它提供反馈。
? 失恋之人常言道:“她未作出任何回应,便是回应了”。同样的,如果发送方愿意等待足够长的时间以便确定分组已经丢失,那么它只需重传该数据分组即可。至于那个分组是否真丢失了,没人在乎。发送方已经发送了一个新的数据包了,接收方收到后经过核对也会将其丢弃。
至于所谓的愿意等待足够长的时间,当然没有傻子一直等待下去。发送方每发送一个数据包,就会启动一个定时器(countdown timer)。如果在定时器归零之前仍然没有收到确认报文,发送方就会重传该报文,并重新设置好定时器等待。如果收到了就会停止当前报文的定时器,并开始进行下一个报文的传送。
看下面的这种情况,发送方发送第一个报文,并启动一个定时器。接收方收到了该报文,并回应一个ACK报文。这个ACK报文经历了一点网络拥塞,在定时器结束之前没有赶到。等到它赶到时,发送方已经重发了报文并重设了定时器。那么对于这个回应报文,发送方该如何处理呢?是不理睬它,继续等待吗。还是停止计时器,发送下一个呢? 答案是停止计时器,准备发送下一个报文。虽然是迟到的回应,可毕竟收到了,这说明发送方的心意已经传答到了。浪费时间去等待是不明智的。
此外,关于这个定时器的的时间应该设置为多少呢?如果设置的时间太短,那么就会大量重传报文,链路上就会有太多冗余的分组,这对网络拥塞的处理不利。如果设置的时间太长,则会影响数据传送双方的传输效率,这对用户的体验显然不大好,而且网络上的链路带宽也没有充分利用。所以这个时间既不能太长,也不能太短。而且还需要是动态变化的,因为网络上的数据发送时具有突发性的。举个例子,白天打游戏的人,比晚上打游戏的人要多。所以,这个超时重传定时器是要能根据具体的情况自适应的,一直以来,针对这个问题,研究学者也发了许多论文来讨论。不过,这个具体实现暂且不论,这在TCP的具体原理里有提及。
发送方和接收方动作的伪代码描述
//发送方
while(true) {
if(上层应用调用) {
send_data();
set_timer(); //设置好计时器
while(等待接收方报文) {
if(time_out()) { //时间超时
send_data(); //重传
set_timer(); //重新设置计时器
}
if(收到回应) {
if(ACK1) {
timer_over(); //关闭计时器
}else { // 重传 重新设置计时器
send_data();
set_timer();
}
}
}
}
}
//接收方
while(true) {
if(接收到报文) {
if(数据无损 && 是当前需要接收的报文) {
rcv_data();
send_ack1();
}else
send_ack0();
}
rdt3.1
? rdt3.0已经可以应对真实的网络环境了,这样就结束了吗? 不,还没有,rdt3.0还是存在一些问题的。前面说过,rdt3.0每发送一个分组就会设置计时器等待反馈(停止等待协议)。发送方发送一个数据包,经过漫长的传播时延,到达了接收方那里。之后再经过漫长的传播时延,返回到发送方。之后才能发送下一个报文。这样对网络链路带宽利用太低了。(这里就不找一些数据带入计算利用率了)
解决这种性能问题的一个方法是:发送方一次发送多个数据包而无需等待确认。这种技术被称作流水线(pipelining) 那么针对这种方式,双方需要增加一些应对措施。
? 1. 分组并不一定按顺序到达,因此需要利用序号来表示分组的身份。rdt3.0中发送的分组只需上一个发送的和当前发送的就可以确定了。而在3.1中却是可能有许多未确认的分组,因此需要增加序号范围。
? 2. 因为要一次发送多个分组,发送方需要增加一个发送缓存,用于存放本次发送了但未确认的分组。因为这些分组是有可能丢失的,为了选择重发,发送方必须准备上备份。对于接收方而言,需要存放那些按序到达的分组,积攒到一定程度再转交给上层应用。
这些准备工作是为了应付丢失,损坏和延时过大的情况的。对于这种差错恢复,又有两种基本方法,回退N步(Go-Back-N)和选择重传(Selective Repeat)。
回退N步(GBN)
回退N步中的N指的是流水线未确认的分组数不能超过某个最大允许数N, 这个N是和流量控制和拥塞控制相关的。
? 先来叙述一下GBN的序号范围代表什么: 假设此时发送方一些数据且其中一部分得到了确认,第base号分组是序号最小且未被确认收到的分组。那么**[0, base - 1]序号范围的分组是已经被确定的了。序号nextseq时最小且未被使用的。那么[base, nextseq - 1]范围的分组则是已经发送了但未被确认的分组。而[nextseq, base+ N - 1]的序号范围则将用于那些将要被发送的分组。至于为什么是base + N - 1**,因为GBN中未确认的分组数不能超过N个。
? 如果让我们直接设计, 直接从rdt3.0跳到3.1: 一次发送多个数据包,接收方对每个分组进行回应。但是实际上接收方对到来的分组逐个回应是存在冗余的。如果第i 号分组和第i + 1 号分组都按序到达了。接收方只需回复一个
a
c
k
i
+
1
ack_{i +1}
acki+1??????代表前i+1 分组都到达了即可,实际上GBN确实是这样处理的。GBN中,接收方对发送方的确认报文就是这样累积确认的形式。
? GBN协议中,接收方的动作是这样的:如果一个序号为n 的分组被正确且按序接收到,即上一次正确按序接收到的分组序号是n-1 。则接受方接收这个分组,并等待一定的时机发送一个ack 。在其他的情况中:分组出错,分组的序号不是按序的。接收方将丢弃这个分组,并为序号最大的且按序接收的分组i 发送一个ack, 代表前i 号分组我已收到,发送方自然明白在那之后的报文没有收到。这样丢弃正确且失序的分组虽然有点任性。丢弃这样的一个分组,之后说不定就出错或者超时了。但是胜在简单。在拥塞不大的网络环境中还是可以的。
? 对于发送方而言,它的计时器并非为每一个发送了且还未收到确认的分组都设置。发送方仅仅设置一个计时器,为最早发送了且还未收到确认的分组设立的。如果收到了一个ACK且此时还有一些仍未确认的分组,则计时器被重新设置,指向最早发送了且未被确认的分组。如果收到了ACK且此时已经没有未被确认的分组,那么计时器就被关闭了。
? 让我们仔细想一下GBN中可能出现的情况,按序接收自然没问题。但是一旦发生问题,代价就有点大了。假设此时接受方最大的按序接收到的分组序号是i 。当接收到一个失序分组时,则发送
a
c
k
i
ack_i
acki???????。如果这个ack到达发送方且无差错,发送方将从第i + 1 号分组开始重发。如果这个ack出了bit错误,则发送方会将之前收到确认的分组之后的分组重传一遍。如果丢失,则等到计时器到时间重传一遍。总而言之,出了问题,后面的所有分组都重传一遍。
虽然讨论一直各种情况有点啰嗦,但是这种针对某种具体情况讨论 对加深理解网络协议时是十分必要的,不妨拿着笔去实际画一画。
现在看一下,通信双方付出了什么代价来维护这个协议。首先是序号范围的增加,这将增加一点传送报文的长度。其次是发送缓冲区和接收缓冲区。发送方还需要维护一些变量:窗口的上下界,以及nextseq的位置,接受方只需维护一个最后按序收到的分组的序号。
最后再实际模拟一下发送方的窗口。由于是累计确认,发送方的缓冲区实际上是被分段的,已经收到回复的,发送还未收到回复的,未发送的。对于已经收到回复的这一部分可以不去理会,不属于窗口部分了,发送方就可以移动一下发送窗口的后沿,使得窗口长度保持为N 。发送方的发送窗口在序号空间向前滑动,因此GBN也被称为滑动窗口协议。
发送方和接收方动作的伪代码描述
//发送方
while(true) {
if(上层应用调用) {
if(发送窗口已满) {
refuse(); //无法发送,通告上层应用
}else {
send_data(); //发送数据
set_timer(); //设置计数器
}
}
if(rev_ack) { // 收到了 ack
if(无法辨认) { //损坏
send_data(); // 重传
set_timer(); //重设
}
if(ack报文序号小于滑动窗口后沿)
no_action(); //无操作,等到 时间到了重发
else
change_window(); //窗口滑动
}
if(timer_over()) {
send_data(); //重发
set_timer(); //重设
}
}
//接收方
while(true) {
if(rev_data()) { //收到了数据
if(无法辨认) {
send_ack_last(); //发送最大有序的ack
discard(); //丢弃该报文
}
if(失序) {
send_ack_last(); // 发送最大有序的ack
discard(); //丢弃收到的这个报文
}
if(完好且不失序) {
change_last(); //改变last值
receive(); //接受
}
}
}
选择重传(SR)
? 前面说到,GBN协议中单个分组的出错就能引起大量分组的重传,而随着信道差错率的增加,流水线就可能被这些大量没必要重传的分组所充斥。而SR协议仅让发送方重传那些出错或丢失的分组,以此避免了没必要的重传(按需重传)。
? 那么来分析一下,SR协议又需要做些什么来满足这些条件呢。
-
为了按需重传,接收方需要逐个确认收到的报文,因此接收方需要对每个报文都发送一个确认ACK。同样的,发送方需要对每个发送的报文都设置一个计时器。 -
SR中同样得按顺序将收到的报文交付给上层应用。对于那些已经收到的且是失序的报文,接收方需要其暂存下来,等到中间的空缺补齐后一并交给上层。
假设发送方的发送窗口中按序确认的ACK报文最大序号是send_base ,接收方接收方按序接收到的最大序号是rcv_base .
? 对接收方而言,那么区间[rcv_base, rcv_base + N - 1] 内的分组将被正确接收。如果一个分组落在该区间内,接收方将发送一个ACK报文给发送方。如果该分组之前没有收到过,接收方则缓存该分组。如果分组序号为rcv_base ,那么从rcv_base 起始的 连续的分组将被转交给上层应用,并修改rcv_base 的值。注意,哪怕是之前收到确认过的分组,也必须发送一个ACK.
? 对发送方而言,区间[send_base,send_base + N - 1] 为其发送窗口,当从上层应用接收到数据后,如果当前发送窗口还有可用的序号(序号落在发送窗口内),则发送方将其发送出去。否则发送方要么将其暂时缓存,要么通告上层,这一点与GBN协议一样。当某一个分组设置的定时器超时时,发送方只需要转发该分组即可。如果收到ACK,如果ACK的序号在发送窗口的区间内,则将那个被确认的分组标记为已接受。如果从send_base 到x 这些连续的分组都已经被接收了,那么send_base ,滑动窗口的后延,将向前移动到x 。
对接收方来说,对于之前确认过的分组,当分组重新到来的时候,也必须重新发送ACK。否则将有可能导致发送窗口无法向前滑动。发送方定时器重传,超时,再重传,超时。由于丢包和延时的存在,发送方和接收方看到的事实是不一定相等的,也意味着发送方和接收方的窗口并非总是一致的。所以,重传很重要。
? 这个也引来了一个新的问题,考虑一下下面的场景。假设此时发送方和接收方的窗口是一致的,[x, x + N - 1] 。发送方发送了一连串报文使得接收方的接收窗口右移了三格,即[x + 3, x + 2 + N] 。而接收方的
A
C
K
x
ACK_x
ACKx???????????????丢失了。等到发送方的计时器到时间之后,x 号报文将重传。这里需要说明一个前面没有提到的知识点,序号空间,即可以使用的序号范围。由于拥塞,流量的控制,以及缓冲区的初始设置,窗口大小是不一定等于序号空间大小的。继续上面的例子。如果序号空间不够大,发送窗口将重复利用前面的序号,这就有可能出现两个分组使用了同一个序号,一个是之前重送的分组,一个是之后的新分组。对于重传的这个分组x 。接收方有可能误以为是之后新发的分组。导致无法区分。因此 **窗口长度必须小于或等于序号空间的一半。**实际上还存在另一种情况:发送方和接收方均已滑出序列号为x 的分组,而该分组却在信道上兜兜转转一大圈子再折返回来。即信道在缓存分组,并在将来的某个时间释放这些分组。为了解决这个问题,或者说,为了确保序号为x 的分组不被重新使用,发送方必须确信之前发送的序号为x的分组都不在网络中为止,即分组的存活时间都不会大于某个最大时间。一般假定分组寿命最长为3分钟。TCP在报文头部使用时间戳选项来避免这个问题。
后话
上面是抽象的可靠协议的实现,TCP的具体原理也只是 在其基础上加上了亿点点细节与优化而已,后继将总结TCP的原理。
?
?
?
|