转自https://www.doraemonext.com/archives/653.html
这篇笔记对应了《计算机网络 自顶向下方法》第六版中的 3.4 节内容,因为该节内容比较重要且略繁琐,所以提炼整理了一下。由于内容过长,分上、下两篇发布。
文章目录 [隐藏]
在实际的网络传输中,信道是不可靠的,在其上传输的分组可能会损坏或丢失,甚至相对次序都不能保证。
在这种情况下,应用层的程序迫切需要运输层提供一个可靠的数据传输服务,可以保证无论在实际的物理传输中发生了什么,数据都可以无损按序地交付给接收端。这就是可靠数据传输协议的作用,也是 TCP 向调用它的应用所提供的服务模型。
下图表明了可靠数据传输的服务模型与服务实现:
可以看到,在上图中,应用层的程序可以非常肯定的认为自己所发送和接收的数据都是通过一条可靠信道进行传输的,数据不会丢失、损坏、乱序。而可靠数据传输的服务实现则是在一个不可靠的信道上面通过自己的各种机制来抽象出一个可靠信道供上层调用。
注:上图中的 udt
表示不可靠的数据传输。
现在我们考虑最简单的情况下如何构造一个可靠数据传输协议。
假设底层信道是完全可靠的,不必担心数据损坏和丢失,而且也假定接收方接收数据的速率与发送端发送数据的速率一样快。
下图表示了在底层信道完全可靠的情况下,分别对应于发送方和接收方的有限状态机定义:
如果你对有限状态机不熟悉,这里是对有限状态机的一些解释:
上图中的
Waiting for call from above
和Waiting for call from bellow
分别表示发送方和接收方的状态,这里只是简单的一个,后面会有更多更复杂的状态图。上图中的箭头指示了协议从一个状态变迁到另一个状态(可以从自己变迁到自己),引起变迁的事件显示在表示变迁的横线上方,事件发生时所采取的动作显示在横线下方。如果一个事件没有动作,或没有就事件发生而采取一个动作,将在横线上方或下方使用符号
∧
。有限状态机的初始状态用虚线表示。
可以看到,rdt 的发送端只通过 rdt_send(data)
事件接收来自较高层的数据发送请求。在完成一次数据发送请求中需要两个动作:
make_pkt(data)
产生)udt_send(packet)
发送到信道中完成这两个动作后,重新返回原始状态,继续等待来自较高层的数据发送请求。
而在接收端,rdt 通过 rdt_rcv(packet)
事件从底层信道接收一个分组。在一次数据接收过程中同样需要两个动作:
extract(packet, data)
产生)deliver_data(data)
动作)和发送端一样,接收端完成这两个动作后也重新返回原始状态,继续等待从底层信道接收分组。
需要注意的是,在发送端,引起状态变迁的事件是由较高层应用的过程调用产生的;而在接收端,引起状态变迁的事件是由较低层协议的过程调用产生的。
现在我们就构造出了适用于可靠信道的可靠数据传输协议 rdt 1.0 ,因为信道可靠,接收方也不需要提供任何反馈信息给发送方,不必担心出现差错。而且因为假定了接收方接收数据的速率能够与发送方发送数据的速率一样快,所以接收方也没有必要请求发送方慢一点发送。
现在我们可以假设在信道传输过程中分组中的比特可能受损了,在这种比特可能受损的情况下,来看一下如何构造可靠数据传输协议 rdt 2.0。注意现在仍然假定所有发送的分组(即使受损)将按照其发送的顺序被接收,同时信道中的分组也不会丢失。
首先需要明确的一点是:如果发送方知道了哪些分组发送出去后接收方并没有收到,那么发送方就需要重传这些分组。基于这样的重传机制的可靠数据传输协议称为自动重传请求(Automatic Repeat Request, ARQ)协议 。
ARQ 协议使用以下三种方法来处理存在比特差错的情况:
下面来看一下 rdt 2.0 的有限状态机描述图,现在该数据传输协议(自动重传请求协议)采用了差错检测、肯定确认与否定确认。
rdt 2.0 的发送端有两个状态。在最左边的初始状态中,发送端协议正等待来自较高层传下来的数据。当触发 rdt_send(data)
事件时:
sndpkt = make_pkt(data, checksum)
产生一个包含待发送数据且带有校验和的分组udt_send(sndpkt)
发送到信道中执行完上述的两个动作后,发送端的状态变迁为“等待接收接收端的 ACK 或 NAK 分组”。接下来根据接收端的响应不同会有不同的变迁方案:
rdt_rcv(rcvpkt) && isACK(rcvpkt)
),那么发送端知道接收端已经成功接收到了刚才发送出去的分组,发送端状态回到初始状态,继续等待下一次由较高层传下来的数据发送请求rdt_rcv(rcvpkt) && isNAK(rcvpkt)
),那么发送端知道接收端接收到的分组是受损的,所以调用 udt_send(sndpkt)
重新发送该分组,然后状态不变,继续等待接收接收端的 ACK 或 NAK 分组由于 rdt 2.0 的发送端拥有这个特性,所以 rdt 2.0 这样的协议被称为停等(stop-and-wait)协议。
rdt 2.0 的接收端仍然只有一个状态。状态变迁取决于收到的分组是否受损,有两种方式:
rdt_rcv(rcvpkt) && corrupt(rcvpkt)
,则返回 NAK 分组rdt_rcv(rcvpkt) && notcorrupt(rcvpkt)
,则返回 ACK 分组处理完后仍然返回自身这个状态,继续等待下一次从底层接收分组并处理。
现在我们得到了一个似乎是可以在有比特差错信道上正常工作的可靠数据传输协议了,但仔细想想,我们没有考虑 ACK 或 NAK 分组受损的情况。如果 ACK 或 NAK 分组受损的时候,我们应该怎么做?
解决这个问题比较简单的一个方法是在数据分组中添加一个新的字段,然后让发送端对其数据分组编号,将发送数据分组的序号放在该字段中。于是,接收端只需要检查序号就可以确定收到的分组是否是一次重新传送的分组。因为 rdt 2.0 是一个简单的停等协议,1 比特序号就足够了。
在这里再次提醒一下我们在 rdt 2.0 开始的地方所做的假设:假设信道不丢分组,而且不会存在分组乱序的情况。所以发送端知道所接收到的 ACK 和 NAK 分组(无论是否受损)都是为响应其最近发送的数据分组而生成的。
完善了对 ACK 和 NAK 分组受损的情况的处理机制后,我们把完善后的协议称为 rdt 2.1,下面是 rdt 2.1 发送端的有限状态机描述图:
下面是 rdt 2.1 接收端的有限状态机描述图:
现在的状态数是以前的两倍,是因为协议的状态必须反映出目前(由发送端)正发送的分组或(在接收端)希望接受的分组序号是 0 还是 1。看起来这个描述图很复杂,其实发送或期望接收 0 号分组的状态中的动作与发送或期望接收 1 号分组的状态中的动作是相似的,唯一不同的是序号处理的方法不同。
这里我按照上图来描述一下 rdt 2.1 协议发送端的状态变迁过程:
rdt_send(data)
事件,通过 sndpkt = make_pkt(0, data, checksum)
产生一个序号为 0,包含待发送数据且带有校验和的分组,接着通过 udt_send(sndpkt)
将其发送到信道中,然后状态变迁为“等待接收接收端的 ACK 或 NAK 0”udt_send(sndpkt)
重新传送刚才的序号为 0 的分组到信道中rdt_send(data)
事件,通过 sndpkt = make_pkt(1, data, checksum)
产生一个序号为 1,包含待发送数据且带有校验和的分组,接着通过 udt_send(sndpkt)
将其发送到信道中,然后状态变迁为“等待接收接收端的 ACK 或 NAK 1”udt_send(sndpkt)
重新传送刚才的序号为 1 的分组到信道中接着再来描述一下 rdt 2.1 协议接收端的状态变迁过程:
rdt_rcv(rcvpkt)
从底层信道接收了一个分组数据:
rdt_crv(rcvpkt) && corrupt(rcvpkt)
),那么由 sndpkt = make_pkt(NAK, checksum)
产生一个附带校验和的 NAK 分组,接着由 udt_send(sndpkt)
发送回发送端rdt_rcv(rcvpkt) && notcorrupt(rcvpkt) && has_seq1(rcvpkt)
),那么由 sndpkt = make_pkt(ACK, checksum)
产生一个附带校验和的 ACK 分组,接着由 udt_send(sndpkt)
发送回发送端rdt_rcv(rcvpkt) && notcorrupt(rcvpkt) && has_seq0(rcvpkt)
),那么通过 extract(rcvpkt, data)
和 deliver_data(data)
将分组数据上传给较高层程序。接着,由 sndpkt = make_pkt(ACK, checksum)
产生一个附带校验和的 ACK 分组,由 udt_send(sndpkt)
发送回发送端这里顺便解释一下接收端接收到失序分组的原因:假设发送端发送序号为 0 的分组,接收端收到并回复 ACK,接着接收端就开始等待接收序号为 1 的分组,但是这个接收端返回的 ACK 分组由于在传输过程中受损,发送端并不知道序号为 0 的分组已经发送成功,所以仍然重复发送序号为 0 的分组,这样,就造成了接收端在等待接收序号为 1 的分组的时候,却接收到了序号为 0 的失序分组。
为什么当接收端接收到分组失序时要返回 ACK 分组呢?因为按照上面的假设,信道不会丢失分组,也不会乱序,所以收到失序的分组的唯一原因就是上面解释的这种,那么在这种情况下,只需要告诉发送端:我确实已经收到了你刚才一直重发的分组,可以发新的了。所以接收端回应 ACK 分组即可。
其实上面的 rdt 2.1 协议在上述假设的底层信道模型中已经工作的不错了,但是我们还可以再简化一下,实现一个无 NAK 的可靠数据传输协议,我们称它为 rdt 2.2。
rdt 2.1 和 rdt 2.2 之间的细微变化在于,接收端此时必须包括由一个 ACK 报文所确认的分组序号(可以通过在接收端有限状态机中,在 make_pkt()
中包括参数 ACK 0 或 ACK 1 来实现),发送端此时必须检查接收到的 ACK 报文中被确认的分组序号(可通过在发送端有限状态机中,在 isACK()
中包括参数 0 或 1 来实现)。
下图是 rdt 2.2 协议发送端的有限状态机描述图:
下图是接收端的有限状态机描述图:
考虑在 rdt 2.1 协议中,如果接收端收到了一个受损的分组则会返回 NAK 分组。但是如果不发送 NAK,而是对上次正确接收的分组发送一个 ACK,也能实现与发送 NAK 一样的效果。发送端接收到对同一个分组的两个 ACK(即接收冗余ACK)后,就知道接收端没有正确接收到跟在被确认两次的分组后面的分组。这就是 rdt 2.2 可以取消 NAK 分组的原因。
具体 rdt 2.2 的流程因为和 rdt 2.1 基本类似,故不赘述。
现在我们终于可以回到现实世界了,在现实世界中,除了比特受损外,底层信道还会丢包。这时我们应该如何设计协议以保证可靠数据传输呢?
有很多可能的方法用于解决丢包问题,在这里,我们让发送端负责检测和回复丢包工作。假定发送端传输一个数据分组,该分组或者接收端对该分组的 ACK 发生了丢失。在这两种情况下,发送端都收不到应当到来的接收端的响应。所以,如果发送端愿意等待足够长的时间以确定该分组缺失已丢失,则它只需要重传该数据分组即可。
但是等待多长时间合适呢?很明显发送端至少需要等待:发送端与接收端之间的往返时延(可能会包括在中间路由器的缓冲时延)加上接收端处理一个分组所需的时间。但这个时间是很难估算的。在 RFC 1323 中,这个时间被假定为 3 分钟。
在实践中,发送端明智地选择一个时间值,以判定可能发生了丢包(尽管不能确定)。如果在这个时间内没有收到 ACK,则重传该分组。注意到如果一个分组经历了特别大的时延,发送端可能会重传该分组,即使该数据分组及其 ACK 都没有丢失。这就在发送端到接收端的信道中引入了冗余数据分组的可能性。不过上面的 rdt 2.2 协议已经有足够的功能(即序号)来处理冗余分组情况。
从发送端的观点来看,重传是万灵药。发送端不知道是一个数据分组丢失,还是一个 ACK 丢失,或者只是该分组或 ACK 过低延时。在所有这些情况下,发送端执行的动作都是重传。
为了实现基于时间的重传机制,需要一个倒计时计时器,在一个给定的时间量过期后,中断发送端。因此发送端需要能做到:
下图是 rdt 3.0 的发送端有限状态机描述图:
因为在 rdt 2.2 协议中的接收端已经可以处理冗余分组,而 rdt 3.0 协议的发送端无非是加大了发送冗余分组的可能性,所以 rdt 2.2 协议中的接收端有限状态机描述图仍然适用于 rdt 3.0 协议。
下面我仍然用文字来简要描述一下上图中的发送端发送分组流程:
rdt_send(data)
事件,通过 sndpkt = make_pkt(0, data, checksum)
产生一个序号为 0,包含待发送数据且带有校验和的分组,接着通过 udt_send(sndpkt)
将其发送到信道中并启动定时器,然后状态变迁为“等待接收接收端的 ACK 0”corrupt(rcvpkt)
)或者收到了 ACK 1(即 isACK(rcvpkt, 1)
,也就是收到了自己发送的上一个分组的 ACK),则直接忽略udt_send(sndpkt)
重新发送该分组并重新启动定时器现在 rdt 3.0 已经是一个功能正确的协议,但因为它的本质仍然是停等协议,所以效率实在捉急。在本笔记的下篇中,将介绍流水线可靠数据传输协议、回退 N 步协议以及选择重传协议,最终将会得到一个可靠并且效率较高的协议实现方法。