计算机网络-运输层总结

计算机网络-运输层

概述和运输层服务

运输层位于应用层和网络层之间,主要为运行在不同主机上的应用进程提供了逻辑通信。 非常类似于高速公路,高速公路负责把人或者物品从一端运送到另一端,而运输层则负责把报文从一个端系统运送到另一个端系统(手机、网络媒体等)

在运输层运输报文的过程中,会遵循一定的协议规范。比如一次传输的数据显示、选择什么样的运输协议等。运输层实现了让两个互不相关的主机好像直接相连一样,这就是逻辑通信的意义。

上面是一个数据包首部的结构,数据包经过每层后,该层协议都会在数据包附上包首部。那么,在数据传输到运输层后,如果采用的是TCP协议,那么会为其附上TCP首部,首部包含着源端口号,目的端口号

在发送端,运输层将从发送应用程序进程 接收到的报文转化成 运输层分组, 分组在计算机网络中也称为报文段。运输层一般会把报文段进行分割,分割成为较小的块,为每一块加上运输层首部(否则不知道源端口和目的端口)并将其向目的地发送。

前置知识

在TCP/IP 协议中能实现传输层功能的,最具代表性的就是TCP和UDP,它就好像是高速公路上的交通工具。

TCP 叫做 Transmission Control Protocol(传输控制协议) ,通过名称可以大致知道TCP协议有控制传输的功能。TCP为应用层提供了一种可靠、面向连接的 服务,能够将分组可靠的传输到服务端。

UDP 叫做User Datagram Protocol(用户数据报协议) ,UDP 为应用层提供了一种无需建立连接 就可以直接发送数据报的方法。但是传输过程是不可靠的。

在计算机网络中,不同层对数据有不同的描述。上面讲运输层的分组称为报文段,除此之外,还会将TCP中的分组也称为报文段,但是将UDP的分组称为数据报,网络层的分组也称为数据报

但是为了统一,我们这边就统称为报文段

套接字(socket)

在TCP或UDP发送报文信息前,要经过一扇门,也就是套接字(socket),向上连接着应用层,向下连接着网络层。

使用TCP或者UDP通信时,会广泛使用到套接字的API来设置IP地址、端口号,实现数据的发送和接收。

Socket和TCP/IP没有必然联系,但是方便了TCP/IP的使用.比如说:

方法 描述
create() 创建一个socket
bind() 套接字标识,一般用于绑定端口号
listen() 准备接收连接
accept() 准备作为接收者
write() 发送数据
read() 接收数据
close() 关闭连接

套接字类型

  • Datagram sockets: 数据报套接字提供一种无连接的服务,而且不能保证数据传输的可靠性。数据有可能出现丢失或者重复,且无法保证顺序地接收到数据。数据报套接字使用UDP 进行数据的传输。我们需要在程序中作相应的处理才能解决有可能出现的数据丢失的情况。
  • Stream sockets: 流套接字用于面向连接、可靠的数据传输服务。能够保证数据的可靠性、顺序性。 流套接字之所以能够实现可靠的数据服务。员应在于其使用了传输控制协议,即 TCP协议
  • Raw sockets: 原始套接字允许直接发送和接收IP数据报,而无需任何特定于协议的传输层格式,原始套接字可以读写内核没有处理过的IP数据报。

套接字处理过程

在计算机网络中,要想实现通信,必须要两个端系统,至少需要两个套接字

  1. socket 中的API用于创建通信链路中的端点,创建完成后会返回套接字描述符。使用套接字描述符可以用来访问套接字
  2. 当应用程序具有套接字描述符之后,可以将唯一的名字绑定在套接字上,服务器必须绑定一个名称才能在网络中访问
  3. 客户端在为服务端分配了socket 并且使用bind() 将名称绑定到套接字上后,会调用listen() ,表示愿意等待连接的意愿,listen必须在accept 之前使用
  4. 服务器应用程序 使用accept() api 接受客户端连接请求,服务器必须先成功调用 bindlisten 之后,再调用 accept()
  5. 现在,流套接字已经建立,客户端和服务器端可以发起read/writeapi调用了
  6. 当服务器或者客户端要停止操作时,就会调用closeAPI 释放套接字获取的系统资源

端口号

端口号可以理解为Socket 的ID。是一个16位的非负整数,介于 [0-65535] 之间,这个范围会分成3个不同的端口号段:

  • 周知/标准端口号, 范围是 0 - 1023
  • 注册端口号,范围 1024-49151
  • 私有端口号,范围 49152-65535

当到达服务器的两条数据都是同一个端口,但是协议不同,该如何区分这个报文段的传送对象呢?互联网上一般使用源IP地址、目标IP地址、源端口号、目标端口号 来进行区分。 如果其中的某一项不同,就会被认为是不同的报文段。

多路复用和多路分解

当报文段到达主机时,运输层会检查报文段中的目的端口号,并将其定向到相应的套接字,这叫做多路分解。然后报文段中的数据通过套接字进入其所连接的进程。(向上传递)

在源主机从不同的套接字中收集数据块,并为每个数据块封装上首部信息从而生成报文段,然后将报文段传递到网络层,所有这些工作被称为多路复用。(向下传递)

多路复用和多路分解分为两种:无连接的多路复用和多路分解面向连接的多路复用和多路分解

无连接的多路复用和多路分解

如下图所示,加入主机A中的端口 19157 要向服务器B端口46428 发送数据,采用UDP协议。 那么数据在应用层产生之后,会在运输层中加工处理,然后在网络层中将数据封装获得IP数据包,IP数据包通过链路层尽力而为得交付给服务器B,然后主机B会检查报文段中的端口号判断是哪个套接字的。

所以,UDP套接字其实就是个二元组,包含目的IP地址和目的端口号、

所以,如果两个UDP报文段有不同的源IP地址相同的源端口号 ,但是具有相同的目的IP地址目的端口号,那么这两个报文会通过套接字定位到相同的进程

在A到B的报文段中,源端口号作为 “返回地址” 的一部分,即当B需要发回一个报文段给A时,B到A的报文段中的目的端口号便从A到B的报文段的源端口号中取值。

面向连接的多路复用与多路分解

如果说无连接的多路复用和多路分解指的是UDP的话,那么面向连接的多路复用与多路分解指的是TCP了。和UDP的报文结构为一个二元组不同,TCP的报文结构时一个四元组,即源IP地址、目标IP地址、源端口号、目标端口号, 当一个TCP报文段从网络到达一台主机时,这个主机会根据这四个值拆解到对应的套接字上。

上图显示了面向连接的多路复用和多路分解的过程,图中主机C向主机B发起了两个HTTP请求,主机A向主机C发起了一个HTTP请求,主机A,B,C都有自己唯一的IP地址,当主机C发出HTTP请求后,主机B能够进行分解。对于主机A和主机C来说,这两个主机有不同的IP地址,所以对于主机B来说,也能够进行分解。

UDP 无连接运输

UDP 为应用程序提供了一种无需建立连接就可以发送分装的IP数据包的方法。如果应用程序开发人员选择的是UDP而不是TCP的话,那么该应用程序相当于就是直接和IP直接打交道的。

所谓的无需建立连接,就是在使用UDP协议在将数据报传递给目标主机时,发送方和接收方的运输层实体间是没有握手的

UDP 的特点

UDP协议一般是作为流媒体应用、语音交流、视频会议所使用的传输层协议,包括DNS协议的底层也是使用了UDP协议,原因主要是因为以下几点

  • 速度快,采用UDP协议时,只要应用进程将数据传给UDP,UDP就会将此数据打包进UDP报文段并立刻传递给网络层。 但是TCP有拥塞控制的功能,它会在发送前判断互联网拥堵情况,如果互联网极度阻塞,那么就会抑制TCP的发送方。使用UDP的目的就是实时性
  • 无需建立连接:TCP在数据传输前需要经过三次握手的操作,而UDP则无需任何准备可进行数据传输。我们可以做一个比喻:
    • TCP 是一种凡事都要设计好,没设计不会进行开发的工程师,需要把一切因素考虑在内后再开干,所以非常靠谱
    • UDP是上来直接开干,也不管设计也不管技术,这种开发人员非常不靠谱,但是适合快速迭代开发,可以马上上手
      • 但是并不是所有使用UDP协议的应用层都是不可靠的,应用程序可以自己实现可靠的数据传输,通过增加确认和重传机制。

UDP 报文结构

下面来看一下UDP的报文结构,每个UDP报文分为UDP报头和UDP数据区两部分。报头由4个16位长(2字节) 字段组成,分别说明该报文的源端口、目的端口、报文长度、校验值

  • 源端口号(Source Port): 这个字段占据UDP报文头的前16位,通常包含发送数据报的应用程序所使用的UDP端口。接收端的应用程序利用这个字段的值作为发送响应的目的地址。 有时候不会设置源端口号,没有端口号就默认为0, 通常用于不需要返回消息的同信中
  • 目标端口号(Destination Port): 表示接收端口,字段长位16位
  • 长度(Length): 字段占据16位,表示UDP数据报的长度,等于UDP报文头长度+UDP数据长度。因为报文头长度为4*2 = 8 个字节,所以这个值最小为8,最大长度为65535 字节。
  • 校验和(Checksum): UDP 使用校验和来保证数据安全性,UDP的校验和也提供了差错检测功能,差错检测用于校验报文段从源到目标主机的过程中,数据的完整性是否发生了改变。那么校验和是怎么被计算的?
    • 校验和就是将前三个字段的16比特的字相加,如果有溢出,就需要回卷(将溢出的高位加到最低位上去)。最后再取反码。

比如:

这三个 16比特的前两个字和是:

再将上面的和与第三个字相加,得出:

注意到,最后一次的加法是由溢出的,因此,这个溢出的1要回卷到最低位的1出,因此我们看到最低位的1因为这个1而进了一位,倒数第二为变成了1 。最后取反码得到: $1011010100111101$ ,这就是最终的校验和。

在接收方,全部的4个16比特字(包括检验和)加在一起,如果该分组在运输过程中没有出现差错,那么最终的和将是1111111111111111,如果这些比特之一是0,那么我们就知道该分组中已经出现了差错。

为什么UDP 会提供差错检测功能?

其实这是一种端到端的设计原则,这个原则说的是要让传输中各种错误发生的概率降低到一个可以接受的水平。UDP不可靠的原因是因为他虽然提供了差错检测的功能,但是对于差错没有恢复能力更不会有重传机制。

错题与注意点

  • UDP的检验和段是可选的,如果源主机不想计算校验和,该校验和段应全为0
  • UDP数据报的伪首部包含了IP地址信息,目的是通过数据校验保证UDP数据报正确地到达目的主机。该伪首部由源和目的主机仅在校验和计算期间建立,并不发送。
  • 如果数据报在传输过程中被破坏,那么就把它丢弃
  • 传输层提供的是端到端服务,为进程之间提供逻辑通信。不是主机之间的通讯
  • HTTP响应报文可能会具有空的报文体
  • 两个不同的Web页面(例如,www.mit.edu/research.htmlwww.mit.edu/students.html) 可以通过同一个持续连接发送
  • 网络层负责将称为数据报(datagram)的网络层分组从一台主机移动到另一台主机

1

假定在主机C上的一个进程有一个具有端口号6789的UDP套接字。假定主机A和主机B都用目的端口号6789向主机C发送一个UDP报文段。这两台主机的这些报文段在主机C都被描述为相同的套接字吗?如果是这样的话,在主机C的该进程将怎样知道源于两台不同主机的这两个报文段?

答:这两台主机的这些报文段在主机C会被描述为相同的套接字。因为在传输UDP包的时候, 网络层会附带上源和目的的IP地址的, 主机C的程序可以通过不同的源IP地址判别。毕竟主机A和B在选端口的时候不知道彼此具体会选什么, 肯定会有选用一样端口号的情况,主机IP能把它们区分开

2

考虑一个长度为L的分组从端系统A开始,经3段链路传输到目的端系统。令$di,si$和$Ri$表示链路i的长度、传播速度和传输速率$(i= 1,2,3)$。该分组交换机对每个分组的时延为$d{proc}$。假定没有排队时延,用$d_i、s_i、R_i(i= 1,2,3)$和L表示,该分组总的端到端时延是什么?

解答: 首先我们要知道,$D总 = D{trans}+D{prop}+D{proc}$

那么,依次来解决这些时延:

处理时延: 题目说,分组交换机对每个分组的时延为$d{proc}$ ,在这条传输路径上,一共有两个端,两个路由器,因此,处理时延应该为 $2d{proc}$

传播时延: 传播时延指的是数据在链路上的传递时间。这里一共有三段链路,每段链路的传播时延要个计算。为:$d_1/s_1+d_2/s_2+d_3/s_3$

传输时延: 传输时延指的是将分组的信息发到链路的时间,也就是将数据报推出去所花的时间。时延计算公式为:$L/R$, 这里,在每一段链路上都要个自己算,因此传输时延为:$L/R_1+L/R_2+L/R_3$

现在假定该分组是1500字节,在所有3条链路上的传播时延是$2.5 \times 10^8$m/s,所有3条链路的传输速率是2Mbps,分组交换机的处理时延是3ms,第一段链路的长度是5000km,第二段链路的长度是4000km,并且最后一段链路的长度是1000km。对于这些值,该端到端时延为多少?

只要把数据带入公式即可。

3.

UDP和TCP使用反码来计算它们的检验和,结合UDP检验和段的相关知识回答以下问题:

(1)假设你有下面3个8比特字节:$01010011, 01100110, 01110100$。这些8比特字节和的反码是多少?写岀所有计算过程。(注:UDP和TCP使用16比特的字来计算校验和,但对于本题目,考虑8比特和。)

解答:计算字节和:

然后和第二个数相加:

因为溢出了,所以要抹去最高位,并回卷到最低位。最终结果是 $00101110$

取反码得到: $11010001$

(2)在(1)中,UDP为什么要用该和的反码,即为什么不直接使用该和呢?使用该反码方案,接收方如何检测出差错? 1 比特的差错能检测出来吗? 2比特的差错呢?

解答:相比于原码,补码,二进制反码循环移位加法求和具有以下优点:

  • 不依赖系统是大端小端。即无论你是发送方计算机或者接收方检查校验和时,都可直接通过上面的算法得到正确的结果。简单来说,用反码求和时,交换16位数的字节顺序,得到的结果相同,只是字节顺序相应地也交换了;而如果使用原码或者补码求和,得到的结果可能就不同。

    • 比如:针对上面的第二个计算式子,我们将字节顺序调换为:1001 1011 + 0100 0111 = 1110 0010(大端切换成小端), 取反得0001 1101,相比上面的1101 0001只是字节顺序相应的也进行的交换。而如果采用原码的话,1011 1001 + 0111 0100 -> 0010 1101,交换顺序后得:1001 1011 + 0100 0111 -> 1110 0010,结果发生改变。
  • 在接收方,全部的4个8比特字(包括检验和)加在一起。如果该分组中没有引入差错,则显然在接收方处该和将是1111 1111;如果这些比特之一是0, 那么我们就知道该分组中已经出现了差错。

  • 所有的1位错误都会被检测到,但是如果有2位错误就有可能发送忽略,比方说上述第一个字节的最后一位转换为0,第二个字节的最后一位转换为1,此时相加的结果可能就不会有影响(跟(3)类似)。

(3)假定某UDP接收方对接收到的UDP报文段计算因特网检验和,并发现它与承载在检验和字段中的值相匹配。该接收方能够绝对确信没有出现过比特差错吗?试解释之。

解答: 不,接收方不能完全确定没有发生任何位错误。如果包中两个16位字的对应位(相加在一起)是0和1,那么即使这些位分别翻转到1和0,所得的和仍然保持不变。因此,接收方计算的反码也将是相同的。此外,传输错误也有可能导致验证通过。

可靠数据传输原理

这是这一章最难的东西了。在学习TCP之前,我们必须搞懂可靠数据传输原理。

下图是可靠数据传输的框架:为上层实体提供的服务可以理解为:数据可以通过一条可靠信道进行传输。借助于可靠信道,可以实现:

  • 传输数据比特就不会受到损坏或者丢失
  • 所有数据都是按照其发送顺序进行交付

但是在可靠数据传输协议的下层也许是不可靠的,因此,如何把不可靠的变可靠的是一件比较困难的事情:首先应用层把要发送的数据交给传输层的发送端,并调用 rdt_send() 分组以后调用udt_send() 将packet 通过网络层(不可靠的) 发送给 接收方。并通过某种方法让传输的数据在中途不会有损坏或者丢失。再将packet还原成data,并向上抛给接收端的应用层。

接下来我们要使用有限状态机来具象化发送端和接收端。也就是说,可以通过某一个时间是状态机的状态发生改变。

rdt1.0

我们首先来看最简单的情况。在这种情况下,物品,我们将数据的传输信道(也就是上图下方的管道)理想化,视为完全可靠不丢包也不发生bit error(如比特重置) ,在这样的情况下,发送端发送数据,接收端直接接收,并不虑丢包,超时这些问题。

发送者

首先,发送者一直在等待上层应用的rdt_send(data)调用,当收到后会执行三个操作:

  • 调用make_pkt(data),将数据放到packet中
  • 调用udt_send(data),将packet 加上头部信息之后通过传输信道发送给接收端,但是因为这个传输信道是理想化的,所以并不会出现任何差错。

所以说,在rdt1.0的情况下,发送者只有1个状态,并处于一个无限循环当中。

因此,我们把这种机制叫做停等,也就是在发送packet之后一直在等待返回信息。包括在后面的rdt2.x以及rdt3.0 都是在使用停等。

接受者:

在接受者方,也只有一个状态。接收端收到分组以后,将封包解开,取出data,将其发送到上层应用。

rdt2.0

但事实上,错误是不可避免的,上面我们说了有两种方法:比特被重置了或者说是丢包了。现在我们先来看简单的情况——比特被重置了

之前在介绍UDP的时候,谈到了可以用检验和来判断比特是否被重置。现在的问题是发现比特被重置后该怎么恢复?现在是接受者收到数据错误,然后需要发送一个反馈信息告诉发送者,这个信息就是NAK ;同样的,当接受者收到的信息是正确的,那么就发送ACK给发送者。

下面是发送者和接收者的有限状态机FSM

发送者:

  • 发送端等待上层传数据传进来
  • 将数据和检验和打包为分组并将其发送到信道中
  • 发送端进入等待返回信号状态
    • 如果收到NAK则说明发送的数据有误则进行重传。判断条件是:rdt_rcv(rcvpkt)&&isNAK(rcvpkt)
    • 如果接受到ACK则数据无误,回到等待上层调用状态。判断条件是:rdt_rcv(rcvpkt)&&isACK(rcvpkt)

因此这时候发送者的FSM中有两个状态,要注意这两个状态的变换条件。

接收端:

  • 接收端收到资料
  • 当数据分组接收到以后确认无误(判断条件是rdt_rcv(rcvpkt)&&corrupt(rcvpkt)),会把数据提取出来,向上传递并发送ACK给发送方已确定数据无误。
  • 当收到后发现有错误时(判断条件是rdt_rcv(rcvpkt)&&corrupt(rcvpkt)),会传回NAK通知发送端重传。

因此,rdt2.0的接收方只有一个状态,并处于一个循环当中

rdt2.1

现在的新问题是:在rdt2.0 时可能我发送的NAK或者ACK在传输过程中也会发生损坏。这时候接收端就无法区分反馈的信息到底是什么了。这时需要发送端重复发送。

针对rdt2.0中ACK/NAK受损可能会导致重传的问题,rdt2.1加入了序列号机制(sequence number),分组的号码可以:

  • 让发送方知道是否需要重传。
  • 让接受者确认,接收到的packet是否是重新传输的分组

在这里,为了节省bit,该序号在当前协议中只使用 0和1 ,交替排列就可以了。发送第一个包裹的时候编号为0,第二个包裹时编号为1,第三个包裹时编号又变回0。这样一来,发送端和接收端都有了两种序号状态, 0 和 1

发送者

这时候的发送者有4个状态:发送0,停等0;发送1,停等1

现在可能由四种情况:

  1. 什么错误都没发生。 这时候,发送者发送编号为0的包裹,接收者收到以后发现数据正确,那么就会提取数据、向上抛出并返回一个ACK。发送者收到ACK之后,跳到下一个状态,等待编号为1的调用。
  2. 发送过去的数据出现损坏。这时接收方通过判断,返回一个编号为0的NAK,发送端接收到NAK的返回值,落入判断corrupt(rcvpkt)||isNAK(rcvpkt)后面的判断条件,然后重新发送packet
  3. 返回的ACK信号出现损坏。接收端成功接收0号包,返回ACK的同时进入下一个状态等待1号包;这时ACK出现了比特重置——ACK变成了NAK。发送端收到NAK后,会落入判断(corrupt(rcvpkt)||isNAK(rcvpkt)同时满足两个条件,前面这个条件是因为ACK出现重置),会进行编号为0的packet的重传;接收方要等的是编号为1的包,结果却等来一个编号为0的包,那么这时候接收端并不会做任何操作(知道了是上一个ACK出了错误),仍然返回ACK;最后当发送方终于收到ACK之后,会进入状态1,这时接收方和发送发都进入了状态1
  4. 返回的NAK信号出现损坏。接收端收到损坏的0号包,返回NAK并保持在状态0。但是在路上NAK发生了比特翻转,是否会让发送端误判而跑到状态1去呢?这种情况基本不会出现。因为在发送者判断isACK之前还有一个notcorrupt(rcvpkt),这是用校验和来判断收到的包不存在比特重置情况的。因此这时候判定为corrupt(rcvpkt)||isNAK(rcvpkt)的前面那个条件,并重新发送编号为0的packet.

接收端也有状态0和状态1,因为上面已经将所有情况都做了一个梳理,这里就不详细讲了。

rdt2.2

对于rdt2.1需要返回 NAK , ACK 两种状态, 可能太麻烦了,就将其全部改为ACK。只是返回的时候顺便返回序号。 也就是接收端收到包,不管正确与否,都返回 ACK ,同时附上序号,这个序号就是数据包发送过来时的序号。对上一条数据的重复确认就是对当前数据的否认。

还是上面几种情况

  • 什么错误都没发生
  • 发送者发送的数据出现了问题。
    • 接收者收到出错的包之后,落入判定corrupt(rcvpkt)||has_seq1(rcvpkt)的前者。就返回一个编号为1的ACK,并留在状态0;
    • 发送者收到编号为1的ACK之后,落入判定corrupt(rcvpkt)||isACK(rcvpkt,1)的后者,并重发编号为0的包
  • 接收者收到正确的包,并返回(ACK,0)、进入状态1。但是过程中出现了比特重置。
    • 发送者这方收到后,落入判定corrupt(rcvpkt)||has_seq1(rcvpkt) 的前者,因此会重新发编号为0的包。
    • 这时,ACK等待的却是编号为1的包,因此会落入判定(corrupt(rcvpkt)||has_seq0(rcvpkt))的后者,并重新返回一个编号为0的ACK。
  • 接收者收到错误的包,返回编号为1的ACK,但是途图中出现了差错。
    • 发送者收到编号为1的ACK之后,落入判定corrupt(rcvpkt)||isACK(rcvpkt,1)的前者(也有可能是前后者并中),然后重发编号为0的包。

接收方的状态都已在上面说过,因此不再展开

rdt3.0

上面所说的情况,都是在没有丢包的情况下进行的讨论。现在我们要引入丢包的情况了。

如果发生了丢包,那么解决的办法就是重发。那么什么时候重发呢?网络中的延时具有非常大的不确定性, 如果等待足够大的时延才重传分组显然会降低效率 。我们可以引入一个定时器,当反馈信息超过了一定的时间还是没有到达发送者,发送者就会重新发送。

下面是发送者的fsm:

第一个状态,收到上层调用后,发送编号为0的packet的同时启动一个timer,然后变成第二个状态

第二个状态,当超时之后,会重发packet并重启timer、继续等待编号为0的ACK;或者当收到包后却发现包裹损坏或编号为1的ACK。只有当收到包后、包没有损坏且是编号为0的ACK,发送者才会进入下一个状态。等待发送编号为1的packet

接收者的状态和rdt2.2是一样的。

下面四张图描述了四种情况:

  • 数据没有丢失

  • 发送方的packet丢失

  • 接收方返回的ACK丢失

  • 提早超时/姗姗来迟的ACK

这时候问题变得稍稍复杂了些,我们来细致分析:

  1. 首先发送者从第三个状态开始,接收者从第二个状态开始。
  2. 发送者发送了编号为1的包并启动了一个timer,转化为状态4;接收者收到没有损坏的且编号为1的包,并发送一个编号为1的ACK给发送者,状态转为等待来自下层的0
  3. 因为发送者这里超时了,落入timeout->udt_send(sndpkt)逻辑,重新发送编号为1的包并重新启动一个timer
  4. 然而,在这时发送者却受到了来自接收者的ACK1(第一次),于是就落入rdt_rcv(rcvpkt)&&notcorrupt(rcvpkt)&&isAKC(rcvpkt,1)这个判定,并重新回到状态1,等待上层编号为0的调用。上层调用之后,发送编号为0的一个新的包,同时去到状态2 ;
  5. 接收者再次收到编号为1的packet(检测到了重复包),落入corrupty(rcvpkt)||has_seq1(rcvpkt) 判断,这时接收者仍然保持状态1不变,并重新发送编号为1的ACK给发送者。
  6. 发送者这时候再次收到了接收者重新发送的ACK1(第二次),但它现在在状态2,等待的是一个编号为0的ACK,因此,发送者什么都不会做,直到再次超时并重新发送编号为0的包(第二个)。接收者收到了第一个编号为0的新包之后也返回一个编号为0的ACK
  7. 发送者这时候收到了编号为0的ACK了,然后跳到状态三去了,等待上层编号为1的调用。当再次收到编号为0的ACK时,发送者什么也不做就行了。

rdt3.0虽然规避了所有出错的可能,但是还是存在不足,比如:

  • 效率太慢,由于停等方式的存在,一个包没处理好,发送端在大部分时间都是一直等着。
  • 关键我们要设置重发的等待时间,太长会导致通信太慢,用户体验差;太快则会出现发送者重发之后却收到了姗姗来迟的ACK/NAK。

因此我们要考虑 piplined protocols 也就是说能不能像UDP一样,发送端一直在发送(一次性发送多个packet)?但是一直发送的时候由如何保证丢掉的包能被正确重发且顺序一致呢?

因此,下面来讨论两种piplining方案,每次发一批packet,来提高rdt3.0的效率。一个是回退N步的方法,另一种是selective repeat的方法,两者的很大的区别在于,要不要对一些乱序的包进行缓存?

go-Back-N

GBN 的策略是只保留顺序地包裹,剩下的乱序包全部丢弃并重新接受。有以下几个特点

  • 比如发送了123456,接收方只收到了1236,那么这时候就必须从4开始重新发送456三个包。
  • 接收端也只会返回累计确认(cumulative ack),也就是只ack连续的包,像上面,虽然收到了1236,但是也只返回123的ack,并不会返回6的ack
  • 发送端只给最早的没有被确认的包保留一个timer,当超时以后,会重新发送所有没有被确认的包

比如说下图,前面绿色的是已经被确认的了,因此,现在窗口向右移动,到第一个尚未确认的包处。这样能保证窗口左边的包是已确认且按照顺序排列的。黄色的包是发送了,但是还没有收到ack确认;蓝色的包是还没有被发送,但在窗口之内、等待发送的,我们把第一个蓝色的包所在的位置叫做 nextseqnum即下一次待发的序列。同时,为最老的一个未确认的包保持一个timer,当超时的时候,从这个send_base开始,向后重新按顺序发送黄色的包。

sender 的状态机,只有一个状态,始终在等待。

  • 接到发送请求后,开始发包,刚开始nextseqnum和base是在一个位置的,因此发送第一个包之后就会启动计时器。每发一个包之后,nextseqnum就要自增1。 需要发包时要判断,下一个待发的包的序号是否已经超过了窗口大小,否则就拒发
  • 如果超时,那么重开一个timer,并从base开始将已发送但却没确认的所有包都重新发送
  • 如果收到了包裹且没有损坏,那么窗口就要像右边移动一个位置。同时要判断base窗口右端是否已经移动到nextseqnum。如果移到了,说明在未超时的情况下,发送的包全部收到了ack确认,那么就可以停止老的timer了;否则,timer要重新开始计时,因为这时最老的包已被确认,倒数第二老的包变成了最老的未确认的包
  • 如果收到的包是损坏的,那么什么也不用做;如果收到的包是重复的ack,因为base=getacknum(rcvpkt)+1所以也不会有任何变化

receiver 的状态机

在收到包之后,首先判断是否损坏,其次判断是否符合序列,两者满足,才会返回ack确认

如果收到了乱序的包(比如之前123,突然收到了6),那么接收者就会丢弃 ,不将其放入缓存,并同时返回一个最新有序包的序号的ack确认信息(将6丢弃,并返回ack3)

例子:

这个例子可以来描述一下上面讲的状态机的操作。

  • 首先发送者发了0123一共四个包,其中3在运输图中损坏
  • 接收者收到pkt0(符合序列),返回ack0;收到pkt1(符合序列),返回ack1;收到pkt3(不符合序列),返回ack1
  • 发送者收到ack0,窗口右移1位,同时发送pkt4;发送者受到ack1,再右移一位,同时发送pkt5。
  • 接收者收到pkt4(不符合序列),返回ack1;收到pkt5(不符合序列),仍然返回ack1
  • 发送者维持的最古老的计时器这时候超时了,将从base开始(此时是2),连续发送,将已发送但却没确认的所有包都重新发送,这里是重发2345

selective repeat

SR 策略是在接收端对乱序包进行缓存,并告知发送端未收到、需要重传的包。有以下几个特点

  • 比如说发送了123456,接收方只收到了1236,那么接收端就会为1236各自发一个ack确认。并要求发送端重传45
  • 发送端为每一个未被确认的包保留了一个timer,当特定的timer超时后,就发送特定的packet
  • 效率更高但是需要更大的缓存

原理如下图所示

用一个具体的例子来解释:

  • 发送者发送0123,但是2在途中丢失
  • 接收者收到了pkt0,返回ack0;收到pkt1,返回pkt1;收到pkt3,返回pkt3
  • 发送者收到ack0,窗口右移并发送pkt4;收到ack1,继续右移并发送pkt5
  • 接收者收到pkt4,返回ack4;收到pkt5,返回ack5;
  • 这时候发送者这里pkt2的timer超时了,所以重发pkt2;同时收到了ack4和ack5,但是收到的ack345和ack01并不是连续的,因此窗口并不需要移动
  • 等到发送者终于收到ack2的时候,因为ack345都已经收到了,因此窗口会一下子向右移4位

问题来了,现在有一个情况,就是说在发送者和接收者之间,实际上是看不到对方的窗口的,就像隔了一层帘子一样。如下图所示

现在,发送序列为:0123012,窗口大小为3.

发送者所看到的: 自己发送pkt012之后,却没有等来ack012,因此当超时之后,会重新发送pkt0

接收者所看到的: 自己收到了pkt012之后,返回ack012,并且窗口向右移3位,现在接受者的窗口等待的是pkt301;但是现在接收者收到了发送者重新发送的pkt0,因此,接收者便无法判断这个pkt0到底是发送者重发的还是新的pkt0.

现在我们把窗口的大小调整为2,重复刚才的情况:我们发现,现在当ack01丢失的时,发送者再次发送pkt0,但是现在接收者的窗口仅仅移动到了23,因此再次收到pkt0的时候,接收者就会认为这是之前pkt0,因此什么也不用做。

那么,窗口大小和序号长度之间的关系是什么呢?可以发现,应该是发送者窗口大小+接收者窗口大小<= 序号空间大小,又发送者和接收者的窗口大小相同,因此 窗口大小<=序号空间大小/2

但是对于GBN,就不需要满足窗口大小<=序号空间大小/2 。因为GBN的策略是,收到的如果不是我当前在等待的包,我就全都不要。那刚才SR出现错误的情况来说:当接受者收到pkt012之后,返回ack012,并将窗口移动到 301,此时接受者只要pkt3。因此当发送者因为超时重新发送pkt012时,接受者收到了也会直接丢掉,并不会发生歧义。

但是这并不是意味着GBN情况下窗口大小就没有限制,如果我们序列变成012012,窗口仍然为3,那么这时候也会出现上面那样的歧义了。因此在GBN情况下,需要满足窗口大小<=序号空间大小-1

面向连接的运输: TCP

UDP是一种没有复杂的控制、提供无连接通信服务的一种协议,他将部分控制部分交给应用程序去处理,自己只提供作为传输层最基本的功能。

但是,和UDP不同的是,TCP的协议要比UDP的功能多很多

TCP 被称为一种面向连接(connection-oriented) 的协议,这是因为一个应用程序在向另一个应用程序发送数据之前,这两个进程必须先进行握手,握手是一个逻辑连接

一旦主机A和主机B建立了连接,那么进行通信的两个应用程序只使用虚拟的通信线路发送和接收数据就可以保证数据的传输,TCP协议负责控制连接的建立、断开、保持等工作。

  • TCP连接全双工服务(full-duplex service)的,全双工就是指主机A与另外一个主机B存在一条TCP连接,那么应用程序的数据就可以从主机B流向主机A的同时,也从主机A流向主机B
  • TCP只能进行点对点(point-to-point)连接,那么所谓的多播,即一个主机对多个接收方发送消息的情况是不存在的,TCP连接只能连接一对主机
  • 一旦TCP连接建立后,主机之间就可以相互发送应用数据了,客户进程通过套接字传送数据流。一旦数据通过套接字后,它就由客户中的TCP协议所控制

TCP会将数据临时存储到连接的发送缓存中,这个send buffer是三次握手之间设置的缓存之一,然后TCP在合适的时间将发送缓存中的数据发送到目标主机的接收缓存中,实际上每一端都会有发送缓存和接收缓存,如下图所示。

  • 主机之间的发送是用报文段(segment) 进行的,那么什么是报文段呢?
    • TCP会将要传输的数据流分为多个块(chunk),然后将每个chunk中添加tcp标头,这样就形成了一个报文段。每一个报文段可以传输的长度是有限的,不能超过最大数据长度(Maximum Segment Size) ,俗称MSS 。在报文段向下传输的过程中,会经过链路层,连路程有一个最大传输单位(Maximum Transmission Unit),简称MTU,即数据链路层上所能通过最大数据包的大小(通常和通信接口有关)
    • MSS和MTU是在不同分层的不同定义。 MTU可以认为是网络层能够传输的最大IP数据包,MSS可以认为是传输层的概念,也就是TCP数据报能够传输的最大量
    • 从MSS的定义可以看出,MSS字段只是数据字段最大的长度,是不包含任何头部信息的!

TCP报文段结构

TCP 报文段结构比UDP的报文段结构要多了很多内容,但是前两个16比特的字段是一样的。也就是源端口号目标端口号 ,这是用于多路复用和多路分解的。此外,和UDP一样,TCP也包含校验和,除此之外,TCP报文还有:

  • 32 比特 的序号字段(sequence number field) 和32 比特的确认号字段(acknowlegment number field). 这些字段被TCP 发送方和接收方用来实现可靠的数据传输。
  • 4比特的首部字段长度(header length field),这个字段指示了以32比特的字为单位的TCP首部长度。TCP首部的长度是可变的,但是在通常情况下,选项字段为空,所以TCP首部字段的长度为20字节。
  • 16比特的接受窗口字段(receive window field),这个字段用于流量控制。它用于指示接收方能够/愿意接受的字节数量
  • 可变的选项字段(options field),这个字段用于发送方和接收方协商最大报文长度(MSS) 时使用
  • 6 比特 的标志字段
    • ACK 标志用于确认字段中的值是有效的,这个报文段包括一个队已被成功接收报文段的确认
    • RSTSYNFIN 标志用于连接的建立和关闭
    • CWRECE 用于拥塞控制
    • PSH 用于表示立刻将数据交给上层处理
    • URG 标志用来表示数据中存在需要被上层处理的紧急数据
  • 紧急数据指针字段(urgent data point field) 用于指出紧急数据的最后一个字节。一般来说 紧急数据指针字段、PSH和URG 都是不用的

序号、确认号是干嘛的?

TCP 报文段首部中最重要的两个字段就是序号确认号,这两个字段是TCP实现可靠性的基础。首先我们要来看一下这两个字段里面存放的内容:

一个报文段的序号就是数据流的字节编号。 因为TCP会把数据流分割成为一段一段的字节流,因为字节流本身是有序的,所以每一段的字节编号就在标示是哪一段的字节流。比如说,主机A要给主机B发送一条数据,数据经过应用层之后会有一串数据流,数据流经过TCP分割(分割的依据就是MSS)。

假设数据是10000字节,MSS是2000字节,那么TCP就会把数据拆分成 0-1999,2000-3999 …

首字节编号依次是0,2000 …..

然后每个序号会被填入TCP报文段首部的序号字段中,如下图所示

首先,我们要知道TCP是一种全双工的通信协议,如下图所示:

因此,主机A在向主机B发送消息的过程中,也在接收来自主机B的数据。主机A填充进报文段的确认号是期望从主机B收到的下一字节的序号(这可能会比较绕)。代表确认号以前的所有数据都正常收到了, 如下图所示:

再举一个小栗子:

假设主机A通过一条TCP连接向主机B发送两个紧挨着的TCP报文段. 第一个报文段的序号为90, 第二个报文段序号为110.

a. 第一个报文段中有多少数据?
答: 110 - 90 = 20个字节.

b. 假设第一个报文段丢失而第二个报文段到达主机B. 那么在主机B发往主机A的确认报文中, 确认号应该是多少?
答: 90. 因为之前主机B给主机A发送的ACK就是90,说明90以前的报文我都收到了,主机A才会发送90给主机B

延时确认机制

接收方在收到数据后,并不会立即回复ACK,而是延迟一定时间。
一般ACK延迟发送的时间为200ms,但这个200ms并非收到数据后需要
延迟的时间。系统有一个固定的定时器每隔200ms会来检查是否需要发送ACK包。这样做有两个目的。
1、这样做的目的是ACK是可以合并的,也就是指如果连续收到两个TCP包,并不一定需要ACK两次,只要回复最终的ACK就可以了,可以隆低网络流量。
2、如果接收方有数据要发送,那么就会在发送数据的TCP数据包里,带上ACK信息。这样做,可以避免大量的ACK以一个单独的丁CP包发
送,减少了网络流量。

传输可靠性

首先我们来看 TCP 发送者的有限状态机

  • 初始状态时 , SendBase(基础窗口)=NextSeqNum= InitialSeqNum
  • 当TCP收到上层应用传来的数据时,会生成报文段,然后把报文段传给下层网络层。同时,将NextSeqNum加上这次传送数据的长度,得到一个新的NextSeqNum。 如果当前没有启动计时器的话,就启动一个计时器
  • 如果计时器超时,因为是累计确认机制,那么从还没有ack的最小的序号包开始重传并重新计时。
  • 如果收到了ACK,那么就需要判断ACK的值y(等于Next)是否大于SendBase。如果大于,不管是不是等于 NextSeqNum+length(data)(根据累计确认的机制,有可能ACK在途中丢包了,收到的ACK远远大于当前的SendBase,那也不管,说明接收者肯定收到了),那么更新SendBase。 然后,还要判断是否还有未被确认的报文段,如果有的话就重新开一个计时器,如果没有就暂停计时器

重传机制

什么时候回出现重传呢?

  • 如果Ack丢失,那么当sender保留的计时器超时的时候,就会重发原来的包

  • 当出现早超时的情况,发出 seq=92, 8bytes of data 之后,才收到了ACK100和新的ACK120。 那么由于累计确认的机制,说明ACK120之前的所有数据都已经被接收了。因此HostB收到seq=92, 8bytes of data之后仍然返回ACK120。而主机A也并不需要再次等待ack100的信息了,且主机A的SendBase在收到ACK120的时候就已经移动到120了。

    • TCP 通过ACK 来实现可靠的数据传输,当主机A将数据发出之后会等待主机B的响应。如果有ACK,说明数据已经成功到达对端。反之,数据很有可能丢失。

我们就拿下面这个模拟图来说一下累计确认机制(GBN所采用的),顺便也回答一下为什么在sender收到ack后,只要判断y值大于sendBase 而不用等于 NextSeqNum+length(data) 这个问题。

首先主机A发送了两个packet,一个Seq=92, 8 bytes 长;一个Seq=100,20bytes长。 主机B已经收到了这两个包,并返回 ack100和ack120。 然而,这时候ack100的信息却在传输途中丢失了,只有ack120到达了主机A。所以说,如果主机A收到了 大于NextSeqNum+length(data) 的ACK确认信息,这说明说明ACK120之前的所有数据都已经被主机B接收了,因此主机A并不需要傻傻等着ACK=100的消息。只需直接更新sendBase即可。

值得注意的是,在接收端,也是有缓存机制(SR所采用的),也就是收到不连续的包会进行缓存,而不是像GBN那样直接丢掉。因此,TCP可以看做是GBN和SR的一种结合

快速重传

除了刚才说的超时会导致重传, TCP还具有快速重传机制,这是针对 time-out 时间过长导致用户体验变差的情况而设计的。

快速重传就是说,发送者会通过接收到重复的ACK信息来察觉到丢包的情况——如果发送者连续收到3个相同的ack信息,那么就会直接在丢包处进行重发,而不必再等到time-out之后再重发。如下图所示:

流量控制

流量控制就是说,当发送端连续发送太多数据的时候,接收端可能来不及处理那么多的数据,导致溢出。因此我们有时候需要通过接收端来控制发送端的发送速率,来让接收者的buffer可以容纳发送来的数据而不溢出。

接收者的协议栈如下图所示,在IP层收到的报文段会向上传递,存放在 TCP receiver buffer中,上层应用层会从buffer中取出数据。

TCP 缓冲区的内部结构如下图所示,分为已缓存区域和空闲缓存其余。其中,空闲缓存区域可以看做是一个接受窗口rwnd,TCP 接收者在发给TCP发送者的报文中会有 rwnd的大小(16比特的接受窗口字段),因此发送者就可以通过实时的rwnd大小来控制发包的速度。

发送者要控制已发送但还未ackedpkt 的数量小于等于 rwnd ,才能保持不因为溢出而丢包

连接管理

三次握手

  1. 第一步: 客户端TCP向服务器端的TCP发送一个特殊的TCP报文段, 该报文段不包含应用层数据. 报文段首部中的标志位SYN置1, 简称为SYN报文段. 同时客户端随机选取一个初始序列号x, 放置于SYN报文段的序号字段中, 最后把该报文段经下层封装发送给服务器. SYN的意思是: xxx服务器, 我想向你发起TCP连接, 我的初始序号为x.
  2. 第二步: 服务器收到SYN报文段后, 响应一个SYNACK报文段. SYNACK报文段的SYN标志位置1, 确认号字段设置为x + 1, 序号字段由服务器选择自己的初始序号 y. SYNACK报文段的意思是: 我收到了你的SYN报文段, 序号为x, 我同意该连接, 我自己的序号为y. 现在请求你的 x+1的报文段
  3. 第三步: 客户端接收到SYNACK后要告知服务器自己收到了. 于是发送最后一个报文段, SYN标志位置0, 把确认字段设置为y + 1, 并设置自己的序号为x+1. 这个报文意思是: 好的, 我知道你同意了, 我们开始传输数据吧.

为什么握手不能是两次呢?

TCP连接就像男女朋友谈恋爱,如果只有两次握手,女孩子可能就不知道,她的那句我也爱你,男孩子是否收到,恋爱关系就不能愉快展开。

为什么握手不能是四次呢?

因为握手不能是四次呢?因为三次已经够了,三次已经能让双方都知道:你爱我,我也爱你。而四次就多余了。

四次挥手

  1. 首先客户端TCP向服务器发送一个特殊的TCP报文段, 其中FIN标志位被置1,(FIN=1,seq=x), 发送完之后,客户进入 FIN_WAIT_1 状态,等待一个来自服务器的,带有ack的报文段
  2. 服务器收到该报文段后就进入CLOSE_WAIT 状态,并向发送方发送一个确认报文段,(ACK=1,ack=x+1,seq =v),客户端收到后,进入 FIN_WAIT_2状态, 等待一个来自服务器的,FIN 被置为1 的报文段
  3. 然后服务器发送自己的终止报文段, 同样是把FIN位置1, (FIN=1,ACK1,seq=y,ack=x+1).
  4. 最后客户端收到之后,进入TIME_WAIT 状态,并发送一个带有ACK的报文给server, (ACK=1,seq=x+1,ack=y+1)
  5. 在 TIME_WAIT 状态中经过一段时间后,连接就正式关闭,客户端所有资源(包括端口号),都将被释放
  • 两次挥手行不行? 就是客户端提出关闭, 服务器响应后TCP就结束.
  • 答: 不行, 因为客户单方面提出关闭的话, 服务器还是可以向客户端发送数据, 必须双方都提出关闭并得到确认后TCP连接才算关闭.

拥塞控制

有了TCP的窗口控制后,使计算机网络中两个主机之间不再是以单个数据段的形式发送了,而是能够连续发送大量的数据包。然而,大量数据包同时夜伴随着其他问题,比如说网络负载、网络拥堵等问题。TCP因此使用了拥塞控制机制,使得在面临网络拥塞时遏制发送方的数据发送。

拥塞控制主要有两种方法

  • 端到端的拥塞控制: 因为网络层没有为运输层拥塞控制提供显示支持。所以即使网络中存在拥塞情况,端系统也要通过对网络行为的观察来推断。TCP 就是使用了端到端的拥塞控制方式。IP 层不会向端系统提供有关网络拥塞的反馈信息。那么 TCP 如何推断网络拥塞呢?如果超时或者三次冗余确认就被认为是网络拥塞,TCP 会减小窗口的大小,或者增加往返时延来避免
  • 网络辅助的拥塞控制: 在网络辅助的拥塞控制中,路由器会向发送方提供关于网络中拥塞状态的反馈。这种反馈信息就是一个比特信息,它指示链路中的拥塞情况。

TCP拥塞控制

除了序号和确认号之外,TCP的拥塞控制也是实现TCP可靠性的基础。

TCP 所采用的方法是让每一个发送方根据所感知到的网络的拥塞程度来限制发出报文段的速率,如果 TCP 发送方感知到没有什么拥塞,则 TCP 发送方会增加发送速率;如果发送方感知沿着路径有阻塞,那么发送方就会降低发送速率。

但是这种方法有三个问题

  1. TCP 发送方如何限制它向其他连接发送报文段的速率呢?
  2. 一个 TCP 发送方是如何感知到网络拥塞的呢?
  3. 当发送方感知到端到端的拥塞时,采用何种算法来改变其发送速率呢?
问题1

我们先来讨论,TCP发送方如何限制它向其他连接发送报文段的速率呢?

我们知道,TCP是由接收缓存、发送缓存等组成。发送方的TCP拥塞控制机制会跟踪一个变量,即拥塞窗口的变量,拥塞窗口表示为cwnd, 用于限制TCP在接收到ACK之前可以发送到网络的数据量,而接收窗口是用来告诉接收方能够接受的数据量

一般来说,发送方未确认的数据量不得超过 cwnd 和 rwnd 的最小值,也就是

由于每个数据包的往返时间是RTT,我们假设接收端有足够的缓存空间用于接收数据,我们就不用考虑rwnd了了,只用专注于cwnd,那么,该发送方的 发送速率 = cwnd/RTT 字节/秒 . 通过调节cwnd,发送方因此能调整它向连接发送数据的速率。

问题2

一个 TCP 发送方是如何感知到网络拥塞的呢

这个我们上面讨论过,是 TCP 根据超时或者 3 个冗余 ACK(丢包了) 来感知的。

问题3

当发送方感知到端到端的拥塞时,采用何种算法来改变其发送速率呢 ?

这个问题比较复杂,一般来说,TCP 会遵循下面这几种指导性原则

  • 如果在报文段发送过程中丢失,那就意味着网络拥堵,此时需要适当降低 TCP 发送方的速率。
  • 一个确认报文段指示发送方正在向接收方传递报文段,因此,当对先前未确认报文段的确认到达时,能够增加发送方的速率。为啥呢?因为未确认的报文段到达接收方也就表示着网络不拥堵,能够顺利到达,因此发送方拥塞窗口长度会变大,所以发送速率会变快. 此时发送速率就等于 cwnd/rtt
  • 带宽探测,带宽探测说的是 TCP 可以通过调节传输速率来增加/减小 ACK 到达的次数,如果出现丢包事件,就会减小传输速率。因此,为了探测拥塞开始出现的频率, TCP 发送方应该增加它的传输速率。然后慢慢使传输速率降低,进而再次开始探测,看看拥塞开始速率是否发生了变化。

在了解完 TCP 拥塞控制后,下面我们就该聊一下 TCP 的 拥塞控制算法(TCP congestion control algorithm) 了。TCP 拥塞控制算法主要包含三个部分:慢启动、拥塞避免、快速恢复,下面我们依次来看一下

慢启动

当一条 TCP 开始建立连接时,cwnd 的值就会初始化为一个 MSS的较小值。这就使得初始发送速率大概是 MSS/RTT 字节/秒 ,比如要传输 1000 字节的数据,RTT 为 200 ms ,那么得到的初始发送速率大概是 40 kb/s 。实际情况下可用带宽要比这个 MSS/RTT 大得多,因此 TCP 想要找到最佳的发送速率,可以通过 慢启动(slow-start) 的方式,在慢启动的方式中,cwnd 的值会初始化为 1 个 MSS,并且每次传输报文确认后就会增加一个 MSS,cwnd 的值会变为 2 个 MSS,这两个报文段都传输成功后每个报文段 + 1,会变为 4 个 MSS,依此类推,每成功一次 cwnd 的值就会翻倍。如下图所示

发送速率不可能会一直增长,增长总有结束的时候,那么何时结束呢?慢启动通常会使用下面这几种方式结束发送速率的增长。

  • 如果在慢启动的发送过程出现丢包的情况,那么 TCP 会将发送方的 cwnd 设置为 1 并重新开始慢启动的过程,此时会引入一个 ssthresh(慢启动阈值) 的概念,它的初始值就是产生丢包的 cwnd 的值 / 2,即当检测到拥塞时,ssthresh 的值就是窗口值的一半。
  • 第二种方式是直接和 ssthresh 的值相关联,因为当检测到拥塞时,ssthresh 的值就是窗口值的一半,那么当 cwnd > ssthresh 时,每次翻番都可能会出现丢包,所以最好的方式就是 cwnd 的值 = ssthresh ,这样 TCP 就会转为拥塞控制模式,结束慢启动。
  • 慢启动结束的最后一种方式就是如果检测到 3 个冗余 ACK,TCP 就会执行一种快速重传并进入快速恢复状态。

拥塞避免

当 TCP 进入拥塞控制状态后,cwnd 的值就等于拥塞时值的一半,也就是 ssthresh 的值。所以,无法每次报文段到达后都将 cwnd 的值再翻倍。而是采用了一种相对保守的方式,每次cwnd窗口大小的报文都传输完成后只将 cwnd 的值增加一个 MSS,比如收到了 10 个报文段的确认,但是 cwnd 的值只增加一个 MSS。

因此线性增加的公式为:cwnd = cwnd + MSS*(1/窗口数)

这是一种线性增长模式,它也会有增长逾值,它的增长逾值和慢启动一样,如果出现丢包,那么 cwnd 的值就是一个 MSS,ssthresh 的值就等于 cwnd 的一半;或者是收到 3 个冗余的 ACK 响应也能停止 MSS 增长。

如果 TCP 将 cwnd 的值减半后,仍然会收到 3 个冗余 ACK,那么就会将 ssthresh 的值记录为 cwnd 值的一半,进入 快速恢复 状态。

快速恢复

快恢复算法,其过程有以下两个要点:

  1. 当发送方连续收到三个重复确认,就执行“乘法减小”算法,把慢启动门限ssthresh减半。这是为了预防网络发生拥塞。请注意:接下去不执行慢启动算法,而进行快速恢复算法
  2. 由于发送方现在认为网络很可能没有发生拥塞,因此与慢v不同之处是现在不执行慢启动算法(即拥塞窗口cwnd现在不设置为1),而是把cwnd值设置为 慢开始门限ssthresh减半后+3*MSS的数值。 此后,对于使 TCP 进入快速恢复状态缺失的报文段,对于每个收到的冗余 ACK,cwnd 的值都会增加一个 MSS 。直到当丢失报文段的一个 ACK 到达时,TCP 在降低 cwnd 后进入拥塞避免状态。如果在拥塞控制状态后出现超时,那么就会迁移到慢启动状态,cwnd 的值被设置为 1 个 MSS,ssthresh 的值设置为 cwnd 的一半。

这里,快速恢复把开始时的拥塞窗口cwnd值再增大一点,即等于 ssthresh + 3 X MSS 。这样做的理由是:既然发送方收到三个重复的确认,就表明有三个分组已经离开了网络。这三个分组不再消耗网络的资源而是停留在接收方的缓存中。可见现在网络中并不是堆积了分组而是减少了三个分组。因此可以适当把拥塞窗口扩大了些。

整一个TCP拥塞控制的用有限状态机如下:

当 cwnd < ssthresh 时,使用上述的慢开始算法。
当 cwnd > ssthresh 时,停止使用慢开始算法而改用拥塞避免算法。
当 cwnd = ssthresh 时,既可使用慢开始算法,也可使用拥塞控制避免算法。

无论在慢开始阶段还是在拥塞避免阶段,只要发送方判断网络出现拥塞(其根据就是没有收到确认),就要把慢开始门限ssthresh设置为出现拥塞时的发送 方窗口值的一半(但不能小于2)。然后把拥塞窗口cwnd重新设置为1,执行慢开始算法。这样做的目的就是要迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。

整一个过程如下图所示:

例题

1

考虑从主机A向主机B传输L字节的大文件,假设MSS为536字节。

  • 为了使得TCP序号不至于用完,L的最大值是多少?(TCP序号字段为4字节)
  • 对于在上题中得到的L,求出传输此文件需要用多长时间?假设运输层、网络层和数据链路层首部总共为66字节,并加载每个报文段上,然后经155Mbps链路发送得到的分组。忽略流量控制和拥塞控制,使主机A能够一个接一个和连续不断得发送这些报文段

答:TCP是字节流编号的,而TCP序号字一共有4字节。因此可供编码位数为32位,则$L_{\max} = 2^{32}$ byte

对于第二题,我们首先要求要发送的报文总数

$N=\lceil \frac{2^{32}}{536}\rceil = 801299$

则需要发送的总长度为:$2^{32}+801299\times 66~byte = 4.824\cdot10^9$ byte

最后,我们要用总长除以链路传输速率,$t=4.824\cdot 10^9 bytes/155Mbps=249s$

2

比较GBN、SR和TCP (无延时的ACK)。假设对所有3个协议的超时值足够长,使得5个连续的数 据报文段及其对应的ACK能够分别由接收主机(主机B)和发送主机(主机A)收到(如果在信 道中无丢失)。假设主机A向主机B发送5个数据报文段,并且第二个报文段(从A发送)丢失。 最后,所有5个数据报文段已经被主机B正确接收。

a. 主机A总共发送了多少报文段和主机B总共发送了多少ACK?它们的序号是什么?对所有3个 协议回答这个问题。

  • 对于GBN, A发送的报文段顺序为:123452345 (一共9个); B发送的ACK: 11112345 (第二个包裹没收到,第345收到的时候仍然传回ack1,因为是累计确认机制,因此一共8个)
  • 对于SR,A发送的报文段顺序为:123452(一共6个,选择确认),B发送的ACK:13452(一共5个)
  • 对于TCP,A发送的报文段顺序为:123452(一共6个,选择确认),B发送的ACK: 22226(累计确认,发送ack2代表2以前的包都已接收)

b. 如果对所有3个协议超时值比5RTT长得多,则哪个协议在最短的时间间隔中成功地交付所有5 个数据报文段?

对于GBN来说,需要等到最早的计时器(对于2)timeout时,才会重传2345,时间最长

对于SR来说,只有当专属于2的计时器timeout时,才会重传pkt2,因此时间必然长于5rtt

对于TCP来说,因为存在快速重传机制,导致发送端A在收到连续三个相同的ACK时,就会重传2. 因此采用TCP最快

3

考虑仅有一条单一的TCP (Reno)连接使用一条10Mbps链路,且该链路没有缓存任何数据。假设 这条链路是发送主机和接收主机之间的唯一拥塞链路。假定某TCP发送方向接收方有一个大文件要 发送,而接收方的接收缓存比拥塞窗口要大得多。我们也做下列假设:每个TCP报文段长度为1500 字节;该连接的双向传播时延是150ms;并且该TCP连接总是处于拥塞避免阶段,即忽略了慢启动

a.这条TCP连接能够取得的最大窗口长度(以报文段计)是多少?根据发送速率 = cwnd/RTT 字节/秒可知:

最大窗口长度可以由这个公式来计算: 最大窗口长度$\times MSS/ RTT = $链路速度

b. 这条TCP连接的平均窗口长度(以报文段计)和平均吞吐量(以bps计)是多少?

平均窗口长度:拥塞避免阶段所以窗口大小在W/2和W之间变化,所以平均窗口大小为0.75W=125x0.75=94[93.75的上限]

平均吞吐量:94x1500x8/0.15s=7.52Mbps

c. 这条TCP连接在从丢包恢复后,再次到达其最大窗口要经历多长时间?

我们知道拥塞避免算法中cwnd是线性增长的,每次收到ack后cwnd增加一个MSS

丢包之后窗口大小变为W/2=62,62变到125,需要125-62=63个RTT,即63x150ms=9.45s

4

考虑修改TCP的拥塞控制算法。 不使用加性增, 使用乘性增。 A TCP sender increases its window size by a small positive constant whenever it receives a valid ACK.求出丢包率L最大拥塞窗口W之间的函数关系。论证: 对于这种修正的TCP,无论TCP的平均吞吐量如何, 一条TCP连接将其拥塞窗口长度从 $W/2$ 增加到 $W$ ,总是需要相同的时间

也就是说,在拥塞控制之后,每次cwnd包含的分组数都传输完成后,就会乘以 $(1+a)$,假设一共收到n个ack之后,才从 $W/2$增加到 $W$,那么我可以这样求出n

解得: $n=\log{1+a}2$ 这说明,从$W/2$到$W$需要 $\log{1+a}2\cdot RTT$ 秒,这与吞吐量是没有关系的。

现在求 丢包率L最大拥塞窗口W 的函数关系:

首先求从 $W/2$ 到 $W$ 期间一共发送的分组数,也就是每次的拥塞窗口长度相加

这都是因为一个丢包而导致的,因此,在这段时间的丢包率为:

5

考虑⼀种简化的TCP的AIMD算法(加法增大乘法减小算法),其中拥塞窗⼝⻓度⽤报⽂段的数量来度量,⽽不是⽤字节(MSS)度量。在加性增中,每个RTT拥塞窗口长度增加一个报文段;在乘性减中,拥塞窗⼝⻓度减⼩⼀半(如果结果不是一个整数, 向下取整到最近的整数)。假设两条TCP连接C1和C2,它们共享一条速率为每秒30个报文段的单一拥塞链路。 假设C1和C2均处于拥塞避免阶段。 它们具有相同的100ms RTT。在时刻$t_0$, C1的拥塞窗口长度为15个报文段, 而C2的拥塞窗口长度是10个报文段。

a. 在2200ms后, 它们的拥塞窗口长度为多长?
b. 经长时间运行, 这两条连接将取得共享该拥塞链路的相同的带宽吗?
c. 如果这两条连接在相同时间达到它们的最大窗口长度, 并在相同时间达到它们的最小窗口长度,
我们说这两条连接是同步的。 经长时间运行, 这两条连接将最终变得同步吗? 如果是, 它们的最
大窗口长度是多少?
d・这种同步将有助于改善共享链路的利用率吗? 为什么? 给出打破这种同步的某种思路

这里采用的是乘性减方法,也就是当网络层无法承受时,就会缩小一半。又因为每秒最多30个报文段,因此每100ms最多3个报文段。那么,当二者的cwnd都变为1时,这是能通过c1+c2的所有报文,因此会采用加性增,C1和C2都变为2,但是一旦它们变为2后,就会超出每100ms链路能承受的最多报文数量,因此又会采用乘性减。重新变为1。

t/ms C1 cwnd C2 cwnd
0 15 10
100 7 5
200 3 2
300 1 1
400 2 2
500 1 1
600 2 2
700 1 1
800 2 2
900 1 1
1000 2 2
1100 1 1
1200 2 2
1300 1 1
1400 2 2
1500 1 1
1600 2 2
1700 1 1
1800 2 2
1900 1 1
2000 2 2
2100 1 1
2200 2 2

b.
是的

c.
是的,最终都是2

d.
这样的同步不利于改善利用率
因为当 C1 C2 的窗口大小都为 1 时,这条链路无法满载。因此,我们可以增设一个缓冲区,在缓冲区溢出之前随机丢弃一些分组。主动队列管理,随机早期检测等都用到了此种方法。

6

假定在主机C上的一个进程有一个具有端口号6789的UDP套接字.假定主机A和主机B都用目的端口6789向主机C发送一个UDP报文段. 这两台主机的这些报文段在主机C都被描述为相同的套接字吗? 如果是这样的话, 在主机C的该进程将怎样知道源于两台不同主机的这两个报文段?

  • 答: 这两台主机的这些报文段在主机C会被描述为相同的套接字. 因为在传输UDP包的时候, 网络层会附带上源和目的的IP地址的, 主机C的程序可以通过不同的源IP地址判别.
  • 毕竟主机A和B在选端口的时候不知道彼此具体会选什么, 肯定会有选用一样端口号的情况, 主机IP能把它们区分开.
7

假定在主机C端口80上运行一个Web服务器.假定这个Web服务器使用持续连接, 并且正在接收来自两台不同主机A和B的请求. 被发送的所有请求都通过位于主机C的相同套接字吗? 如果它们通过不同的套接字传递, 这两个套接字都具有端口80吗? 讨论和解释之.

答: 这里有个巧妙的关系为题目带来歧义.

A和B的请求会通过80端口找到服务器进程, 就这里而言它们通过为与C的相同套接字, 这个套接字具有端口80.

当它们与服务器进程建立连接的时候, 服务器进程会单独为它们分配套接字, 通过专门的套接字响应客户端的请求. 这两个套接字就不具有80端口了.

-------------本文结束,感谢您的阅读-------------