计算机网络自顶向下之可靠传输协议的笔记

前言

此篇文章是计算机网络 自顶向下方法第三章的简略版笔记,这一章非常精彩。个人认为运输层可以说是计算网络中最重要的一层,分别面向应用层以及网络层。众所周知,网络层是无法保证可靠传输的。所以运输层根本的问题在于如何能够在不可靠的网络层之上进行可靠的网络

UDP

UDP几乎没有做事情,仅仅只是把应用层的数据加上报文段,然后交付给网络层,虽然UDP有差错检查,但它也只是丢弃这个包而已。应用程序采用UDP的主要是基于效率的考虑。UDP简单,所以效率高。本文不讨论UDP

可靠数据传输

这一节我们从最简单的情况开始,针对各种情况,慢慢完善,最终形成一个可靠的数据传输协议。TCP是一个全双工的协议,双方可以互相发送信息。目前我们只考虑单向的传输。

假设网络层可靠

最开始我们先假设网络层是可靠的,也就是说我们交付给网络层的包不会丢失且完好无损,同时假设发送和接受双方速率是一样的。这种情况下我们需要考虑什么?什么都不需要考虑。就像UDP一样,打包好交付给网络层即可。相当于在网络层实现了可靠传输。

假设网络层只存在数据损坏的情况

现在我们假设网络层只存在数据损坏的情况,也就是每个包都能到达目标主机,但是数据正确性却不受控制。这种情况我们需要考虑两个问题。

  • 如何判断数据损坏?
  • 如何通知发送方数据损坏?

其实很容易理解,日常对话中,如果我们听清,最常见的就是让对话再说一遍。所以这里需要做三件事差错检查反馈给发送方发送方重传。这里开始是和UDP不同了,UDP进行差错检查后,如果发生错误丢掉这个包就完事了。TCP则会进行重传。

  • 差错检查

    基本原理是用额外的bit来判断数据是否损坏,这里并不多解释。

  • 反馈给发送方

    差错检查发现数据损坏时,我们需要通知发送方。也就是发送ACK数据包,所谓的ACK数据包也就是在报文段把一个标志位改成为1而已。或者发送NAK数据包ACK表示数据发送成功,NAK表示失败。

  • 重传

    当发送方接受到反馈之后,若失败则要进行重传。

现在的情况是这样的,发送方发送数据之后,开始阻塞等待接收方反馈,若得到NAK则进行重传,然后继续阻塞等待反馈。直接接受到ACK则开始发送下一个数据。这里有一个问题就是,当发送一个数据包后,发送方会阻塞,效率很低,这个问题我们下面会解决。

接收方很简单,接收到数据以后进行差错检查,若错误则进行反馈NAK,反之则发送ACK

反馈出错的情况

新的问题,那就是反馈的ACK或者NAK出错!改如何解决,最先想到的就是发送方再问一遍,也就是再发一个包给接收方,问他刚刚说了什么,那如果这个包再出错的?这个方案显然有问题。另一种方法是当收到错误的ACK或者NAK,则直接进行重发。这种方法的问题是接受方不知道新接收的包是重传还是新发送的包。比如说接收方反馈ACK给发送方,但数据损坏发送方直接进行重发,那么接受方会认为这个新的包是全新的数据,而不是重发的,造成冗余分组

解决这种情况一个很巧妙的方法,加入序号。也就是说对需要发送的数据包按顺序编号,那么接受方只需要检查序号即可确实这是否是一个重传的包。即检查新接受的包与上一次正确接受的包的序号进行比对,若相同则是重发,若大与则是新的包。有了序号之后,我们可以不用NAK,只用ACK,也能达到NCK的效果,即重发上一次正确接收的ACK,接收方比对序号可知数据包受损。

网络层发送数据丢失

好,新的问题来了。数据包丢失怎么办?按照之前的讨论,发送方会一直等待接收方进行ACK反馈,若数据包丢失,发送方则会一直阻塞。很自然的就想到采用定时器,等待一定时间之后,若发送方没得到反馈,则自动进行重传。但是如何确定等待时间又是个问题。可以确定的是,等待时间一定大于往返时延加上接受端处理数据包的时间。这是个比较复杂的问题,之后再讨论。

超时处理

还记得前面提到的,当发送方发送数据之后需要阻塞等待接受确认。这种方式叫做停等协议。可想而知效率很低,几乎大部分的时间都用来等待了。根据计算机网络 自顶向下方法上面的描述,采用停等协议的情况下,发送方只有万分之2.7的时间是在真正工作的!

之前的方法是一次发送一个数据包,然后进行阻塞等待。现在解决方法是发送多个数据包,然后再进行阻塞等待。注意这里是有本质的区别的。先前都是发一个等一个。现在是发送n个分组,并一起进行等待确认。区别在于发送的数据包的ACK可以在阻塞之前返回,那么则不需要等待之前发送的分组,可以继续向前移动。就好像铺铁路一样,其实并不需要那么长的铁轨,只需要很短一段铁轨就可以让火车绕世界一圈,只要你铺的够快。下面的图更加形象。

当遇到数据丢失或的情况,有两种方法可以解决。分别是回退N步选择重传

回退N步

发送多个分组且不用等待的数量是有限制的,因为考虑到需要流量控制,所以不能无限制的发送。假设发送N个分组,N通常被称为窗口长度。当N个分组中的序号最小的分组被确认之后,才能继续发送,这就是所谓的滑动窗口协议

我们先来看接收方是如何处理的。接收方采用累积确认的方式,接收方只接收按序到达的数据包。接收方会把接收到的数据包与上次确认接收的数据包序号进行比对,若序号正好是按序到达的,则发送ACK确认接受,若不按序,则发送最后一次确认接收的ACK,并且丢弃这个包。注意这里虽然都是ACK,但是ACK包中带的序号是不同的。

发送方需要维护一个滑动窗口[base,N],我们用nextseq表示下一个可以发送的序号。那么[base, nextseq)表示已经发送但未被确认,[nextseq,N]表示可以发送但是还未发送。base之前的则表示已经发送且确认的。N之后的则表示还不能发送的。若base被确认,则窗口向前移动。回退N步的主要动作在于数据丢失,定时器超时的情况,这个时候会重传所有已经发送但未被确认的数据包。因为上面接收方会把所有未按序到达的数据包丢弃。

选择重传

选择重传解决了回退N步的效率问题,回退N步虽然实现简单,但是效率不高,在某些情况下需要重传大量分组。

选择重传在接收方也维护了滑动窗口来缓存正确到达但是乱序的分组,而不是简单的丢弃。接收方分两种情况,如果是按序到达,则返回ACK确认这个包,并且一起交付与之相连续的之前缓存的数据包,同时滑动窗口向前移动。若未按序到达,则缓存这个数据包,并且返回ACK确认到达。

在发送方,和之前差不多,但是不会重发之前正确发送但是乱序的分组,而是只发送未确认的分组。

TCP

终于轮到TCP了,但核心在上面。TCP用到了上述的所有原理比如差错检查累计确认重传定时器等。TCP是面向连接的全双工运输层协议。也就是说TCP在通信之前,必须要建立一条可靠的连接,通过这条连接来进行通信。这点在使用socket编程的时候,十分明显。当客户端想发送消息给服务端时,必须先调用connect函数。

1
2
self.socket.connect((self.address, self.port))
self.send(self.name)

socket是操作系统对TCP的一层抽象,提供接口让应用更加方便的使用TCP

TCP报文段

  • 源端口号和目标端口号

    各占用16位,源端口号和目标端口号再加上源地址和目的地址构成socket的唯一标识。

  • 顺序号和确认号

    这里就用到了上面的原理,即采用序号编码。顺序号标示当前数据包的序号确认号表示期望收到的下一个数据包的序号

  • 头部长度

    表明TCP首部的长度,因为可选项的存在,所以首部的长度是可变的。

  • 控制位

    设置为1时有效,作用如下图。

  • 窗口大小

    即滑动窗口的长度,用于累积确认以及拥塞控制

  • 校验和

    用于差错检查

  • 紧急指针

    只有控制位的URG设置为1才有用,表示数据要优先处理,代表紧急数据最后一个字节的序号。

  • 选项

    这里可以填MSS之类的数据。

TCP的三次握手

先来讨论下TCP的链接建立,为了建立可靠的通信链接,TCP需要发送三个分组,前两个分组不能携带数据,最后一个是可以携带数据的。也就是所谓的三次握手了。

  • 第一次

    客户端向服务端发送TCP报文段,注意这里是不能包含有效数据的。报文段首部的控制位中SYN位会设置成1,表明这是连接请求报文段。还有就是初始化客户端的起始序号放在顺序位中,一般是随机选的。

  • 第二次

    当服务端收到SYN报文段之后,会回发一个报文段,表示允许链接,并且依旧不能携带数据。同样的SYN控制位设为1,并且初始化服务端的起始序号和缓存空间。另外确认号设置为客户端的起始序号加1,并且ACK的控制位设为1

  • 第三次

    客户端收到服务端的允许连接报文之后,可以开始初始化客户端的缓存空间,并且回发一个报文段,这个报文段可以携带数据。这个报文段的确认号设置为服务端的起始序号加1

到这里不得不谈网上经典的面试题,TCP建立连接为什么需要三次握手?。让我们回到TCP的目的,是为了在不可靠的网络层之上建立可靠的运输协议。那么从这个角度来考虑,如果客户端不确认服务端的允许连接报文会发生什么?假设现在TCP经过服务端允许连接以后,连接就已经建立了。如果现在有一个无效的TCP请求连接发送到服务端,有可能是因为网络阻塞等原因。服务端则会认为这是一个客户端的新的连接请求,因为不需要客户端确认,则直接建立连接,然而这个连接其实是不需要的,白白浪费资源。如果加上第三次,客户端会确认这个请求是失效的,则不会建立连接。

TCP的数据传输

TCP的可靠数据传输基本原理可以说已经包含在上半部分的介绍当中了。主要是三大原理序号累计确认超时重传。但是TCP根据实际情况加入了一些比较有趣的东西,比如快速重传冗余确认等。

简化版的TCP传输

首先初始化序号位,以及sendBase(也就是第一位未被确认的序号或者说下一个需要发送的序号)。比如说发送seq=1seq=2,seq=3都未被确认,那么sendBase就为1,然后之后得到一个seq=3ACK=4,那么则认为seq=1seq=2都被收到了,则sendBase变为4。避免了重传,原因是接收方采用了累积确认

考虑到实际使用中的情况,TCP增加了一些功能。

超时加倍等待时间

这种方法其实很好理解,一旦发生超时事件,其实就说明网络拥堵,那么就加倍定时器的等待时间,让TCP发的慢一点以缓解网络拥塞。

快速重传

快速重传是当TCP连续收到3冗余ACK会触发,即立刻重传未被确认的报文段(不等定时器超时)。所谓的冗余ACK其实也很明了,就是说明之前发送的报文段一直未被客户端收到,所以客户端一直重发期望收到的下一个报文段。

选择重传还是回退N步

答案是混合体!因为是TCP累计确认的,从这一点来看TCP回退N步的。但是TCP不重传已经发送但未被确认的数据,这一点又和选择重传很像。这里有个问题,既然TCP累计确认的,那么如何保证那些已经发送但未被确认的数据已经到达接收方了呢?答案是TCP会缓存这些数据包,但是不会发送确认到达。直到TCP收到那个丢失的包后,会重排序缓存的包,并发送ACK

Reference

《计算机网络 自顶向下方法》