BY 凯寓
[TOC]
传输层位于应用层和网络层之间,其关键功能是将网络层在两个端系统之间的交付拓展到运行在两个不同端系统上的应用层进程之间的交付服务。相比网络层协议为主机之间提供逻辑通信
,传输层协议为运行在不同主机上的应用进程之间提供逻辑通信
,传输层协议是在端系统中实现,而不是在路由器中实现,传输层传送的是报文段(segment)。
一方面,传输层协议提供的服务受制于网络层协议的服务模型:即如果网络层协议无法为主机之间发送的传输层报文段提供时延或带宽的保证,则传输层协议也就无法为进程之间发送的应用层报文提供时延或带宽保证。另一方面,传输层又可提供网络层协议无法提供的服务,如保密性,可靠性。
传输层协议将主机间交付扩展到进程间交付的行为称为传输层多路复用(transport-layer multiplexing)与多路分解(demultiplexing),最有名的传输层协议是TCP和UDP。他们的行为有着鲜明的`两极化``,在一个极端,UDP仅向通信进程提供多路复用/分解的服务,而不提供其他任何服务(因此它非常简单),在另一个极端,TCP向通信进程提供可靠交付、时延保证、带宽保证等一些列服务。具体而言,TCP通过使用流量控制、序号、确认和定时器确保数据正确、按序的从发送进程交付给接受进程,即提供可靠数据传输(reliable data transfer),同时TCP还提供拥塞控制(congestion control)。
在阐述这个问题之前,我们先来简单谈一下不同主机应用程序进程间通信的问题。我们知道不同应用程序之间的交互实际上是进程间的交互,在两个不同端系统上的进程,通过跨越计算机网络交换报文而相互通信。在这个过程中,通信间进程通过一个称为套接字(socket)的软件接口向网络发送和接收报文,套接字是同一台主机内应用层与传输层之间的接口(因此套接字也称为应用程序和网络之间的API)。在网络中,主机地址由IP地址标识,一个发送进程要将报文送往另一台主机的接收进程,除了要知道主机IP地址外,还要知道接收进程的接收套接字,这个接收套接字对应一个目的地端口号(port number)。
对于传输层传输的每个报文,其中都有一些字段,传输层检查这个字段,进而把报文定向到正确的接收套接字。将运输层报文段中的数据交付到正确的套接字的工作就称为多路分解
。与之对应,源主机从不同套接字收集数据块,并为每个数据块封装上首部信息(用于以后分解)从而生成报文段,并将报文段传递到网络层的工作称为多路复用
。
实际中,这些特殊的字段是源端口号字段和目的端口号字段,端口号是一个16bit的数,大小在0—65535之间,其中0—1023范围称为周知端口号(well-known port number),保留给诸如HTTP(80端口)、FTP(控制:21端口,数据:20端口)、telnet(23端口),SMTP(25端口),POP3(110端口),NNTP(119端口)等周知的应用层协议使用。查看周知端口号可前往:www.iana.org
下这张图给出了一个多路复用和分解的例子:
UDP采用无连接的多路复用与分解,一个UDP套接字是由一个二元组(源端口号,目的端口号)来标识的,该二元组包含一个目的IP地址和目的端口号,两个UDP报文只要有相同的IP地址和目的端口号,那么这两个报文就将通过相同的目的套接字被定向到相同的目的进程。在下面的例子中,主机A中的一个进程具有UDP端口号19157,它要发送一个应用程序数据块给位于主机B的另一进程,该进程具有UDP端口号46428。
TCP采用有连接的多路复用与分解,一个TCP套接字是由一个四元组(源IP地址,源端口号,目的IP地址,目的端口号)来标识的,与UDP不同,即使两个TCP报文有相同的目的IP地址和目的端口号,如果它们源IP地址或源端口号不同,就会被定向到两个不同的套接字。例子如下:
UDP(User Datagram Protocol)指用户数据报协议,简单来讲,UDP无非就是对网络层协议增加了多路复用/分解功能而已(还有少量差错检测)。
使用UDP时,在发送报文之前,发送方和接收方的运输层实体间没有握手,因此UDP被称为无连接的。UDP无法提供可靠的数据传输,它有以下特点:
- 只要应用程序将数据传递给UDP,UDP就会将此数据打包进UDP报文并立即将其传递给网络层(相比之下TCP有拥塞控制机制)。由于实时应用通常要求最小发送速率,不希望过分延迟报文段的传送,且能容忍一些数据丢失,因此这些应用可以采用UDP实现。
- 无需建立连接,TCP在开始数据传输之前要经过三次握手,而UDP不需要任何准备即可传输数据,因此UDP不会引入建立连接的时延(正是由于这个原因,DNS使用UDP而不是TCP)。
- 无需维护连接状态,TCP要在端系统间维护连接状态(接收/发送缓存,拥塞控制参数,序号,确认号),而UDP不维护连接状态,因此也不需要这些参数。对于一些应用而言,相比TCP,UDP能支持更多的活跃用户。
- 分组首部开销小,每个TCP报文有20字节的首部开销,而UDP仅有8字节,更节约流量带宽。
- 由于UDP缺乏拥塞控制机制,因此发生拥塞时可能导致发送方和接收方的高丢包率,并会挤垮TCP会话。
UDP定义在RFC 768中,如下图所示,UDP首部只有四个字段(源端口号,目的端口号,长度,检验和),每个字段由两个字节组成。长度字段指示了UDP报文段中的字节数(首部+数据),检验和提供了差错检验功能。
Request For Comments(RFC),是一系列以编号排定的文件。文件收集了有关互联网相关信息,以及UNIX和互联网社区的软件文件。目前RFC文件是由Internet Society(ISOC)赞助发行。基本的互联网通信协议都有在RFC文件内详细说明。
- 源端口:16bits,该数据报的发出端口。发送进程将在此端口发送数据包。
- 目标端口:16bits,该数据报的接收端口。接收进程将在此端口处进行接收。
- 总长度:整个UDP报文的长度,即UDP头部和数据的长度。
- 校验和:一个16位补码,是由伪IP头,UDP头(由于校验和字段本身就在UDP头部中,因此使用UDP头计算时先假设校验和为0),UDP数据形成的。其中,伪IP头的协议号为17。如果要求接收方忽略校验和,发送方不计算校验和,直接把校验和设置为0。对伪IP头的说明见下一小节。
根据RFC 768,检验和的计算方法如下:
图中给出了伪IP头的格式,注意,TCP与UDP计算检验和的方式是一样的。
发送方
对伪IP头,UDP头(校验和为0),UDP数据的所有16bits字的和进行反码运算,求和时遇到的所有溢出都被回卷
(即忽略最高位的进位溢出),得到的结果放在检验和字段。在接收方,将全部字段之和与检验和字段相加,如果没有引入差错,则这个结果应当是全1的,如果有任何>一位为0,则说明传输出现了差错。(参见下图)
这里还有一个问题要说明,虽然UDP提供差错检测,但它对差错的恢复无能为力,对于出错的报文段,UDP的接收方有两种选择:
- 丢弃受损的报文段。
- 将受损的报文段交给应用程序并给出警告。
借助于可靠信道,传输数据比特不会受到损坏或丢失,而且所有数据都是按其顺序进行交付,我们将实现这种服务的抽象称为可靠数据传输协议(reliable data transfer protocol)(注意:可靠数据传输协议的下层协议也许是不可靠的),下面两图给出了一个可靠数据传输协议所提供的服务及其概要实现,其中rdt表示可靠数据传输协议,理解这两张图是理解TCP的基础。
可靠数据传输协议提供的服务和实现:
详细说明:
接下来我们逐步讨论实现可靠数据传输协议的几个要点,在讨论过程中,我们会用到FSM(有限状态机,finite-state machine),其中初始状态用虚线指出,箭头代表从一个状态变化到另一个状态,每个箭头旁会附有引起变迁的事件(横线上方)及事件发生时所采取的行动(横线下方),符号\^表示空集(横线上方表示条件为空/横线下方表示所采取的行动为空)。
为了能更深入的理解TCP,我们先在研究TCP之前改概略的研究以下协议:rdt1.0->rdt2.0->rdt3.0,它们从最理想的网络情况切入,一步步拓展到真实网络环境,其中rdt1.0假设传输信道完全可靠,rdt2.0假设传输信道会有比特差错但不会丢包,rdt3.0假设传输信道不仅会有比特差错,还会丢包(和真实网络情况一致)。
rdt1.0:假设发送双方经完全可靠信道传输,这种情况下我们不必担心任何差错,因为我们假定信道是完全可靠的,且假定接收方接收速率和发送方发送速率一样快。
rdt2.0:假设经有比特差错但不会丢包的信道传输,一个分组在传输、传播、缓存的过程中都可能出现差错,这种情况下,我们需要在rdt1.0的基础上引入以下三种功能来解决。
- 差错检验:利用
检验和
使接收方能够检测到数据分组是否出错。- 接收方反馈:如果没有出错,则返回肯定确认(ACK)给发送方,如果检测到出错,则返回否定确认(NAK),即引入
肯定和否定
分组。- 重传:如果发送方收到NAK,则重传该分组。
由此我们得到rdt2.0如下:
在上图中,只有发送方收到来自接收方的ACK,才会发送下一个分组,如果收到NAK,则重发当前分组。这之中用到了停等协议(stop-and-wait)的思想。但rdt2.0存在以下问题:没有考虑到ACK或NAK在发送过程中受损的问题。如果ACK或NAK受损,rdt2.0的发送方就无法判断该进入哪个状态。解决这一问题的思路是:作为发送方如果不确定收到的是ACK还是NAK,只需重传当前分组(注意:这种方法在信道中引入了冗余分组),但是新的问题来了,如果采取这种方法,作为接收方如何判断自己收到的分组是新的还是一次重传?为了解决这一问题,我们引入序号
字段。即在数据分组中增加一个序号字段,让发送方对其发送的数据进行编号,对于停等协议,这个序号1比特就足够了(使用0、1两个状态轮流标记发送分组),由此我们得到了改进版的rdt2.5,可以看到,此时接收方可以根据序号识别这个一个新的分组还是一次重传。!
rdt3.0:假设经有比特差错且会丢包的信道传输,这是真实网络的情况,为了能处理丢包,我们必须解决以下两个问题:如何检测丢包及发生丢包后该做些什么。第二个问题的答案是显然的,因为其实在引入
检验和
、序号
、肯定和否定分组
及重传
机制后,只要能检测到丢包,我们就可以通过通过重传解决这个问题。
解决第一个问题的办法是引入一个定时器,即选定一个合适的时间值,以判定可能发生了丢包(虽然不能确保是丢包),如果在这个时间内没有收到ACK,则重传该分组。(注意:如果一个分组经历了一个特别大的时延,即使该数据分组及其ACK都没有丢失,发送方也会重传该分组,这在信道中引入了冗余数据分组,但正如rdt2.0中讨论的那样,我们已有的机制可以处理这种冗余,即:可以判断收到的分组是新的还是一次重传。)
如果在协议中,发送方在准备下一个数据项目之前先等待一个肯定的确认,则这样的协议称为ARQ(Automatic Repeat Request,自动重传请求协议)。自动重传请求(Automatic Repeat Request),通过接收方请求发送方重传出错的数据报文来恢复出错的报文,是通信中用于处理信道所带来差错的方法之一,传统自动重传请求分成为三种,即停等协议(stop-and-wait),回退n帧(go-back-n),以及选择性重传(selective repeat)。后两种协议是滑动窗口技术与请求重发技术的结合,由于窗口尺寸开到足够大时,帧在线路上可以连续地流动,因此又称其为流水线ARQ协议。三者的区别在于对于出错的数据报文的处理机制不同。三种ARQ协议中,复杂性递增,效率也递增。接下来停等协议我们已经在之前粗略的提到过,接下来我们分别对这三个协议进行讨论。
我们在上述讨论中提到的rdt3.0也被称为比特交替协议(alternating-bit protocol),尽管它是一个功能正确的协议,但其性能并不令人满意,其性能问题的核心在于它是一个停等协议,下面两张图分别给出了停等协议在无丢包、分组丢失、ACK丢失和过早超时4种情况的过程:
停等协议的性能不佳主要集中表现在它对信道的利用率很低,假设两个距离很远的端系统间的RTT大约为30ms,分组长度为1000字节(8000bits)的数据通过发送速率为1Gbps的信道传输,根据下图的说明和给出的公式计算可知信道利用率只有万分之2.7,停等协议极大的限制了底层网络硬件所能提供的能力,事实上,如果再考虑上发送方与接收方之间路由器的处理时间和排队时延,性能将更加糟糕。
停等协议的性能问题的根源在于每次只能发送一个分组,为了解决其存在的性能问题,我们可以采用流水线技术,即允许发送方一次发送多个分组而无需等待确认,但这也引入一些新的问题:
- 由于每个传输中的分组必须有一个唯一的序号,因此必须
增加序号的范围
。- 发送方和接收方两端必须能
缓存多个分组
,具体而言,发送方最低限度应当能缓存那些已经发送但还没有确认的分组,接收方,而接收方可能需要缓存那些已正确接收的分组(具体取决于采用GBN还是SR)。- 解决流水线下的差错恢复。
解决流水线差错恢复的两种基本方法分别是:GBN和SR。
在GBN中,允许发送方发送多个分组而不需要等待确认,但这里有一个限制,即在流水线中未确认的分组数不能超过某个最大允许数N(N即为窗口长度),下图中,我们定义base为最早的未确认分组号,定义nextseqnum为最小的未使用序号(也即下一个待发分组的序号),则[0,base-1]对应已经发送并被确认的分组,[base,nextseqnum-1]对应已经发送但未被确认的分组,[nextseqnum,base+N-1]内的序号能用于那些要被立即发送的分组,而大于等于base+N的序号是不能使用的。
在未被确认的分组被确认后,窗口向前滑动,因此GBN也被称为滑动窗口协议,分组序号承载在分组首部一个固定长度的字段中(如果分组序号的比特数是k,则序号范围是$[0,2^k-1]$),关于GBN需要理解以下几点:
- 当上层要发送数据时,发送方首先检查发送窗口是否已满,即是否有N个已发送但未被确认的分组,如果窗口未满,则产生一个分组并将其发送,并相应的更新变量,如果窗口已满,发送方缓存这些数据或使用同步机制让上层在仅当窗口不满时才能调用rdt_send()请求发送。
- 在GBN中,对序号为n的分组采取累积确认的方式,接收方丢弃所有收到的失序分组,知道收到期待接收的分组n,然后为分组n返回一个ACK,作为发送方,一旦收到分组n的ACK,就表明n及n之前的所有分组都已被正确接收。
- 发送方仅使用一个定时器,这个定时器是最早的已发送但未被确认的分组适用的定时器。出现超时时,发送方重传所有已发送但还未被确认的分组,即回退n步,并重启计时器。
一个GBN的例子:
GBN的优点是接收缓存简单,缺点是单个分组的差错就可能引起大量本没有必要重传的分组重传。
实现了GBN的rdt4.0:
在GBN中,随着信道差错率的增加,信道可能会被不必要重传的分组所充斥,而SR则可以避免这一问题,选择重传协议通过让发送方重传那些它怀疑在接收方出错的分组而避免不必要的重传,关于SR需要理解以下几点:
- 接收方确认一个正确接收的分组而不管其是否按序,失序的分组将被缓存直到所有序号更小的分组都被收到为止。
- 具体而言有三种情况:如果一个分组的序号落在接收方的窗口内时,如果该分组以前没有收到过,则缓存该分组,回发ACK;如果该分组序号等于接收窗口的基序号(rcv_base),则该分组及其之后缓存的序号连续的分组被交付给上层,而后接收方窗口向后滑动;如果该分组已经被缓存,则也必须回发一个ACK(否则如果上一次回发的该分组的ACK丢失,则发送方将永远无法向后移动窗口)。
一个SR的例子:
SR协议看起来比BNG优秀很多,但它也引入了新的问题,即:由于发送方和接收方窗口间缺乏同步,如果SR接受窗口太大,会带来下图中的问题:接收方在最后收到具有序号0的分组时,无法区分它是第1个分组的重传还是第5个分组的首次传输。
这里要首先补充一点:由于序号空间是一个长度为$2^k$的环(即序号$2^k-1$之后的序号为0)。
为了避免上述问题,通常我们规定窗口长度必须小于序号空间大小的一半。
与UDP不同,TCP是面向连接的,两个端系统的进程在通信前首先要经过三次握手建立连接。TCP提供全双工服务,是点对点的(如果一台主机上的进程A与另一台主机上的进程B存在一条TCP连接,那么应用层数据就可以在从进程B流向A的同时,也从A流向B)。在发送方,当应用层有数据要发送时,客户进程通过套接字传递数据流,TCP将这些数据流引导到发送缓存中(发送缓存在三次握手初期设置),之后TCP会从发送缓存中取出一块数据,加上40字节的首部形成TCP报文段(TCP segment)下传给网络层。当TCP在另一端收到一个报文后,将其存入接收缓存,而后接收方应用程序从此缓存中读取数据流。
这里我们谈到“TCP会从发送缓存中取出一块数据”,所谓一块究竟是多大呢?TCP可从缓存中取出并放入报文段中的数据量受限于最大报文段长度(maximum segment size,MSS),而MSS通常根据最大链路层帧长度,即最大传输单元(maximum transmission unit,MTU)来设置。以太网和PPP链路层协议都具有1500字节的MTU,除去TCP40字节的首部,因此MSS的典型值为1460字节。
TCP定义在RFC 793中,下图显示了TCP报文段的结构:
下面我们逐个分析这些字段:
- 源端口号/目的端口号:各长16bits,分别标识了源端口号和目的端口号。
- 序号:32bits,是该报文段首字节的字节流编号。
- 确认号:32bits,主机A填充进报文段的确认号是主机A期望从主机B收到的下一字节的序号。
- 首部长度:4bits,该字段指示了以32bits的字为单位的TCP首部的长度,由于选项字段的原因,TCP首部长度是可变的,通常选项字段为空,因此TCP首部典型长度为20字节。
- 保留未用:6bits,必须全0。
- 标志(flag):6bits,其中ACK用于确认号字段中的值是有效的,即该报文包括一个对已被成功接收报文段的确认;RST,SYN,FIN比特用于连接的建立和拆除(这一点我们在之后对三次握手的讨论中会详细阐述);PSH比特被置位时,指示接收方应立即将数据交给上层;URG比特用来指示报文段中有被发送端上层实体置位紧急的数据(紧急数据的最后一个字节由16比特的紧急数据指针字段指出,当紧急数据存在且给出紧急数据尾的指针时,TCP必须通知上层实体)。注意,在实际中PSH,URG和紧急数据指针并没有被使用。
- 窗口大小:通告接收缓冲区空闲块大小(确认字节之后还可以发送多少字节)。
- 检验和:一个16位补码,是由伪IP头,TCP头,TCP数据形成的。伪IP头与UDP类似,其中的协议号为6。
- 紧急数据指针:与标志URG一起用来取出紧急数据或指出记录边界。
- 选项:最大数据段长度(Maximum Segment Size,MSS):为TCP有效载荷的最大长度;窗口比例(Scale): 乘以通知窗口大小;允许选择性确认(SACK-Permitted;选择性确认(SeletiveACK,SACK): 已收到数据段的序号集合;时间戳等等。Unix系统的默认值:MSS为536,SACK-Permitted为False。Windows 的默认值MSS为1460,SACK-Permitted为True。
详细说明:
- URG: Urgent Pointer field significant(紧急(Urgent)指针标志,表示本数据段包含紧急数据,位于数据段前部,直到紧急指针指向的位置(从0开始)。可以用来在字节流中指明记录的边界。)
- ACK: Acknowledgment field significant(确认号有效标志。)
- PSH: Push Function(告知接收方发送方执行了推送(Push)操作,接收方需要尽快将这些数据交给接收进程,不要将其放在缓冲区。另外,PSH还实现了段的立即传输,不用等待构建一个大的数据段再发送。当缓冲区中出现回车后,系统会自动设置PSH。因此,PSH在发送端和接收端均起作用。)
- RST: Reset the connection(重置(Reset)连接。因为出现了错误,通知对方立即中止连接并释放与连接有关的资源。)
- SYN: Synchronize sequence numbers(同步(Synchronous)序号标志,用来发起一个TCP连接。)
- FIN: No more data from sender(结束(Finish)标志,表示发送方完成了所有发送任务,要求释放连接。)
(* 所有标志为1有效。)
TCP通过如下方式估计发送方与接收方的往返时间:
我们定义SampleRTT为某报文段被发出到对报文段的确认被收到之间所用的时间,同时TCP维持一个SampleRTT的均值EstimatedRTT。TCP每隔一段时间测量一次SampleRTT,并根据以下公式更新EstimatedRTT。
$$EstimatedRTT = (1- α)EstimatedRTT + αSampleRTT$$
RFC 6298给出α的参考值为0.125
EstimatedRTT是一个加权平均值,这个加权平均值对最近的样本赋予的权值要大于对老样本赋予的权值,因为越近的样本越能反应当前网络的拥塞情况。除了估算RTT外,测量RTT的变化也是有价值的,RFC 6298定义RTT偏差:DevRTT,用于估算SampleRTT偏离EstimatedRTT的程度:
$$DevRTT = (1-β)DevRTT + β|SampleRTT-EstimatedRTT|$$
RFC 6298给出α的参考值为0.25
给出SampleRTT和EstimatedRTT的定义后,我们不妨考虑,TCP超时间隔应该选取什么值呢?我们定义这个值为TimeoutInterval为超时时间间隔。(显然,TimeoutInterval应当大于EstimatedRTT,否则会造成不必要的重传,但也不能比EstimatedRTT大太多,否则报文丢失时将无法很快的重传丢失报文)根据下面公式计算TimeoutInterval:
$$TimeoutInterval = EstimatedRTT + 4*DevRTT$$
RFC 6298推荐的TimeoutInterval初始值为1秒,并在超时后将TimeoutInterval加倍,直到报文收到确认后就使用上面的公式计算TimeoutInterval。
TCP在IP不可靠的尽力而为的服务之上提供了可靠的数据传输服务,确保了一个进程从其接收缓存读取的数据是无损坏,无间隔,非冗余和按序的数据流。接下来我们将具体讨论TCP是如何做到这一点的,我们将会看到,它与我们之前讨论的rdt3.0再很多细节上有所不同。
在之前rdt3.0的讨论中,为了解决丢包的问题,我们实际上为每个分组都引入了一个计时器,一旦计时器超时,就重传该分组,但这样做我们要为计时器的管理付出巨大开销,因此,TCP实际上只采用单一的重传计时器(即使有多个已发送但还未被确认的报文段),这个计时器具体的工作方式我们会在下面逐步给出。
在大多数TCP的实现中,对TimeoutInterval的取值并不总是按照4.3.2中的公式进行计算,而是这样一个过程:
每当超时时间发生时,TCP重传具有最小序号的还未被确认的报文段,并将TimeoutInterval的值设为之前的两倍,而不是使用4.3.2中的公式计算,因此,TimeoutInterval会成指数增长,但是,如果遇到上层应用的数据发送请求或收到来自接收方的ACK事件时,定时器会重新启动,此时启动的定时器将按照4.3.2中的公式计算TimeoutInterval。
之所以这么做是出于以下考虑:如果发生超时,则很可能是由于网络拥塞造成的,而频繁的重传分组会使得拥塞更加严重,因此在没有上层应用的数据发送请求或收到来自接收方的ACK时,TCP使用加倍TimeoutInterval的方式,让重传经过越来越长的时间间隔进行。
如果我们仅依赖是否超时来判断是否丢包,则对于这个真正丢失的分组,发送方必须在超时之后才会重传它,如果超时周期较长,重传会被延迟。为了优化这个问题,TCP引入了快速重传机制。
假设接收方期待接收序号为n的报文段但却收到了序号大于n的报文段,我们就说此时它检测到了数据流的一个间隔,造成间隔的原因可能是报文段丢失或报文段被重新排序,由于TCP不使用否定确认,它会对已经收到的最后一个按序字节数据进行重复确认(即产生一个冗余ACK),作为发送方如果收到对同一个数据的3个冗余ACK(即第4次收到这个数据的ACK),即使当前计时器还没有超时,TCP也立即重传这个分组,这就是快速重传机制。
TCP返回的ACK实际上是它期待(从发送方)接收的下一个分组号,TCP的确认是累积式的,正确接收但失序的报文会被缓存但不会被逐个确认(确认号都是其期待接收的下一个分组号,即前面提到的冗余ACK)。发送方只需要维护已发送但未被确认的字节的最小序号(SendBase)和下一个要发送的字节的序号(NextSeqNum),对于发送方的一组报文1,2,…,N,假设报文段n丢失(n<N),但它之前的全部报文和它之后的全部报文在超时前到达了接收方,则超时后TCP只会重传至多一个报文段(而GBN则会重发n及n之后的分组)。如果对报文段n+1的确认在n超时前到达,即使对n的确认还没有到达,发送方也可以知道接收方已经正确接收报文n了,这是因为TCP采取累积确认,收到对n+1的确认就意味着n+1之前的分组接收方都收到了。
附:RFC 5681对产生TCP ACK的建议:
一条TCP连接每一侧主机都为该连接设置了接收缓存,如果应用程序读取数据相对缓慢,而发送方发送的太多太快,发送的数据就会时接收缓存溢出。TCP为应用程序提供流量控制服务(flow-control service)以消除发送方使接收方缓存溢出的可能性,因此流量控制是一个速度匹配服务。
TCP通过让发送方维护一个称为接收窗口(receive window)的变量来提供流量控制(接收窗口告诉发送方该接收方还有多少缓存空间)。因为TCP是全双工通信,因此在连接两端的发送方都各自维护一个接收窗口。
定义:LastByteRead为主机B上应用程序从缓存读出的数据流的最后一字节的编号,LastByteRcvd为从网络中到达主机B并且已经放入主机B接收缓存中的数据流的最后一个字节的编号。则为了不使缓存溢出,下面式子必须成立:
$$LastByteRcvd - LastByteRead≤RcvBuffer$$
用rwnd表示接收窗口大小,则:
$$rwnd=RcvBuffer-(LastByteRcvd - LastByteRead)$$
为了不使主机B接收缓存溢出,主机A只需轮流跟踪2个变量:LastByteSent和LastByteAcked,注意到LastByteSent - LastByteAcked就是主机A发送到连接中但未被确认的数据量,通过将未被确认的数据量控制在rwnd以内,就可以保证主机B的缓存不会溢出。
在上面的讨论中,如果主机B接收缓存已满,即rwnd=0,在将这一信息告诉主机A后,如果B没有任何数据要发送给A,就会发生死锁,因为此时即使B将缓存中的数据取走,A也无法得知这一事实。为了解决这一问题,我们采用“聪明的发送方/笨拙的接收方”,即发送方定期(Persist Timer)发送一个字节数据,以使接收方响应以获得通知窗口的大小。接收方响应采用下一个期待接收的字节。
建立连接的过程需要三步,称为三次握手:
- 客户端TCP向服务器端TCP发送一个SYN报文段,不含应用层数据,SYN=1,客户端会随机选择一个初始序号x。
- 服务器收到客户端的SYN报文段后,为TCP连接分配缓存和变量,返回一个SYNACK报文段,SYN=1,ACK=1,ACKNum=x+1,同时服务器选择自己的初始序号y。
- 在收到SYNACK报文段后,客户端也分配缓存和变量,并回发一个报文段进行确认,SYN=0,ACK=1,ACKNum=y+1,可以在负载中携带数据。
超时重发:每一步均采用超时重发,多次重发后将放弃。重发次数与间隔时间依系统而不同。
数据字节序号:客户和服务器发送的第一个数据字节的序号分别为x+1和y+1。
选项:头两个数据段给出选项:SACK-Permited,Scale,MSS。
中断连接的过程需要四步,称为四次挥手:
- 客户端TCP向服务器端TCP发送一个FIN报文段,FIN=1,seq=x,客户端进入FIN_WAIT_1状态。
- 服务器收到客户端的FIN报文段后,返回一个ACK报文段,ACK=1,ACKNum=x+1,客户端收到后进入FIN_WAIT_2状态。
- 服务器TCP向客户端TCP发送一个FIN报文段,FIN=1,seq=y。
- 客户端收到服务器的FIN报文段后向服务器发送一个确认,ACK=1,ACKNum=y+1,并进入TIME_WAIT状态,假设ACK丢失,TIME_WAIT状态会使客户端重传最后的报文。
释放连接:先发FIN一方在ACK发送完毕后需要等待2MSL(Maximum Segment Lifetime)的时间才完全关闭。TCP标准中MSL采用60秒,Unix采用30秒。
超时重发:超时未收到确认,则超时自动重发,在若干次重发后依然没有收到确认,则发送RST后强行释放连接。不同的系统重发方法不同。
在三次握手的过程中,如果客户端不发送ACK完成第三步,则服务器将在一段时间内出于半开连接的状态,并在这一时间之后才断开连接并收回资源,如果攻击方发送大量TCP SYN连接但又不完成第三步,就会导致服务器的连接资源最终被耗尽。这种攻击方式被称为SYN Flood Attack,是一种经典的DOS攻击方式,我们可以采用SYN cookie(RFC 4987)来避免它。
TCP发送方法可能因为IP网络的拥塞而被遏制,这种对发送方的控制称为拥塞控制。由于IP层不向端系统提供显式的网络拥塞反馈,因此TCP必须使用端到端的拥塞控制。TCP拥塞控制的方法大致可描述为根据发送方所感知到的网络拥塞程度来限制其能向连接发送流量的速率。如果TCP发送方感知到路径上没有拥塞,则增加发送速率,反之则降低发送速率。
运行在发送方的拥塞控制机制需要跟踪一个额外的变量,即拥塞窗口(congestion window),不妨定义它为cwnd,发送方中未被确认的数据量不能超过cwnd和rwnd中的较小值,即:
$$LastByteRcvd - LastByteRead≤min{cwnd , rwnd}$$
TCP拥塞控制算法主要包括三个部分:慢启动,拥塞避免,快速恢复。其中慢启动和拥塞避免是TCP的强制部分,快速恢复是可选部分。
当一条TCP连接开始时,在慢启动(slow-start)状态,cwnd的值以1个MSS开始,并且每当传输的报文段被首次确认就增加一个MSS,这样每经过一个RTT发送速率就会翻倍,因此,采用慢启动算法,TCP发送速率起始慢,但在慢启动阶段以指数增长。
我们记ssthresh为“慢启动阈值”,在以下三种情况发生时,结束慢启动过程:
- 如果检测到一个由于超时指示的丢包事件,TCP发送方将ssthresh设置为cwnd/2(即当检测到拥塞时将ssthresh值置位拥塞窗口的一半),将cwdn设置为1,并重启慢启动算法。
- 当cwnd等于ssthresh时,结束慢启动算法,转移到拥塞避免模式。
- 如果检测到一个由于3个冗余ACK指示的丢包事件,此时TCP执行快速重传并转入快速恢复状态。
进入拥塞避免状态时,cnwd的值大约是上次遇到拥塞时值的一半,此时TCP采取一种较为保守的方法,每到达一个新的确认,就将cwnd增加一个MSS*(MSS/cwnd)字节,即每个RTT只将cwnd的值增加一个MSS(线性增长)。
在以下两种情况发生时,结束拥塞避免的线性增长:
- 如果检测到一个由于超时指示的丢包事件,将ssthresh更新为cwnd/2,cwnd的值被置位1个MSS,转移到慢启动状态。
- 如果检测到一个由于3个冗余ACK指示的丢包事件,TCP发送方将cwnd的值减半,将ssthresh设置为cwnd/2,进入快速恢复状态。
4.7.3 快速恢复
注意:快速恢复只是TCP推荐但非必须的构件。
在快速恢复中,对于引起TCP进入快速恢复状态的缺失报文段,对收到的每个冗余ACK(最少为3个,但可能不止3个),cwnd的值都增加一个MSS(最少增加3个MSS),不妨设因为冗余ACK而增加的MSS数为k。
在以下两种情况发生时,结束快速恢复:- 当丢失报文段的一个ACK到达时,TCP将cnwd减去k后进入拥塞避免状态。
- 如果检测到一个由于超时指示的丢包事件,将ssthresh更新为cwnd/2,cwnd的值被置位1个MSS,转移到慢启动状态。
一个可能的TCP拥塞窗口变化图如下(图中画出了Reno版TCP与Tahoe版TCP拥塞窗口的变化情况,其中Reno使用了快速恢复而Tahoe没有使用快速恢复):
TCP的拥塞控制是:每个RTT内cwnd线性增加1MSS,每出现3个冗余ACK时cwnd减半,因此TCP拥塞控制也被称为加性增,乘性减(Additive-Increase,Multiplicative-Decrease,AIMD)拥塞控制方式。如下图所示: