运输层协议为运行在不同主机上的应用进程之间提供了逻辑通信,从应用程序的角度看通过这玩意运行不同进程的主机好像直接相连一样(而实际上它们可能位于地球的两端并且中间隔了很多路由器和链路)。
运输层协议是在端系统中而不是在路由器中实现的。在发送端,运输层将从发送应用程序接收到的报文转化为运输层分组,也叫运输层报文段,实现的方法(可能)是将应用报文划分为较小的块,并为每块加上一个首部成为一个运输层报文段,然后传送到网络层,网络层将其分装为网络层分组(数据报)并向目的地发送。请注意:路由器仅作用于该数据报的网络层字段,即它们不检查封装在里面的运输层报文段的字段。网络应用程序可以使用多种运输层协议,例如因特网有两种协议:TCP和UDP。
网络层提供了主机之间的逻辑通信,而运输层则为运行在不同主机的进程之间提供了逻辑通信。运输层协议只工作在端系统上,而且运输层所提供的服务常常受制于底层网络层协议的服务模型,如果网络层协议无法为主机之间发送的运输层报文段提供时延或带宽保证的话,运输层协议也就无法为进程之间发送的应用程序报文提供时延或带宽保证。(Ann和Bill分发邮件的例子)
然而,即使底层网络协议不能在网络层提供相应的服务,运输层协议也能提供某些服务。例如即使底层网络协议是不可靠的,也就是说网络层协议会使分组丢失、篡改和冗余,运输协议也能为应用程序提供可靠的数据传输服务。另一个例子是即使网络层不能保证运输层报文段机密性,运输协议也能使用加密来确保应用程序报文不被入侵者读取。
UDP:用户数据报协议,它为调试它的应用程序提供了一种不可靠、无连接的服务。
TCP:传输控制协议,它为调试它的应用程序提供了一种可靠、面向连接的服务。
某应用程序的开发人员必须选用这两个中的一个,在生成套接字时必须指定是UDP还是TCP。
在这里我们将TCP和UDP分组称为报文段,数据报名称保留给网络层分组。
先来简单介绍一下网络层,网络层协议有一个名字叫IP,即网际协议。IP为主机之间提供了逻辑通信。IP的服务模型是尽力而为交付服务,这意味着IP尽它最大的努力在通信的主机之间交付报文段,但他却不做任何确保,特别是不确保报文段的交付,不保证其按序交付,不保证报文段中数据的完整性。正因如此IP被称为不可靠服务,每台主机至少有一个网络层地址,即所谓的IP地址,在此我们只需要记住每台主机有一个IP地址。
UDP和TCP最基本的责任就是将两个端系统之间的IP交付服务扩展为在两个端系统上运行的两个进程之间的交付服务,这种扩展被称为运输层的多路复用与多路分解。这哥俩还可以通过在其报文段首部中包括差错检查字段而提供完整性检查。进程到进程的数据交付和差错检查是两种最低限度的运输层服务,也是UDP所能仅提供的两种服务。特别的,UDP和IP一样也是一种不可靠服务。
另一方面TCP为应用程序提供了几种附加服务,首先它提供可靠数据传输。通过使用流量控制、序号、确认和定时器,TCP确保正确、按序地将数据将数据从发送进程交付给接收进程,这样TCP就将两个端系统之间的不可靠IP服务转换成了一种进程间的可靠数据传输服务。TCP还提供拥塞控制,这是一种给整个因特网带来好处的通用服务,不太严格的说TCP拥塞控制防止任何一条TCP连接用过多流量来淹没通讯主机间的链路和交换设备。TCP力求为每个通过一条拥塞网络链路的连接平等的共享网络带宽,这可以通过调节TCP连接的发送端发送进网络的流量速率来做到。另一方面UDP流量是不可调节的,使用UDP传输的应用程序愿意发多快就发多快。
这两种服务是所有计算机网络都需要的。
在目的主机,运输层从紧邻向下的网络层接收报文段。运输层负责将这些报文段的数据交付给在主机上运行的适当应用程序进程。一个进程有一个或者多个套接字,它相当于从网络向进程传递数据和从进程向网络传递数据的门户,在接收主机中的运输层实际上并没有直接将数据交付给进程,而是将数据交付给了中间的一个套接字。由于在任一时刻,在接收主机可能不只有一个套接字,所以每个套接字都有唯一的标识符,标识符的格式取决于它是UDP还是TCP套接字。
每个运输层报文中有几个字段。在接收端,运输层检查这些字段,标识出接收套接字,进而将报文段定向到该套接字。将运输层报文段中的数据交付到正确的套接字工作称为多路分解。在源主机从不同套接字收集数据块,并为每个数据块封装上首部信息(这将在以后用于分解)从而生成报文段,然后将报文段传递到网络层,所有这些工作称为多路复用。
运输层多路复用要求:①套接字有唯一标识符②每个报文段有特殊字段来指示该报文段所要交付到的套接字这些特殊字段是源端口号字段和目的端口号字段,端口号是一个16比特的数,其大小在065535之间。01023范围的端口号称为周知端口号,是受限制的,这是指它们保留给诸如http(使用80)和FTP(使用21)之类的周知应用层协议来使用。当我们开发一个新的应用程序时必须为其分配一个端口号。
现在应该清楚运输层是怎样能够实现分解服务的了:在主机的每个套接字能够分配一个端口号,当报文段到达主机时,运输层检查报文段中的目的端口号,并将其定向到相应的套接字,然后报文段中的数据通过套接字进入其所连接的进程,UDP大体上是这样做的,TCP则更为复杂。
1.无连接的多路复用与多路分解
在主机上运行的py程序使用下面一行代码创建了一个UDP套接字:
clientSocket = socket(AF_INET , SOCK_DGRAM)
当用这种方式创建一个UDP套接字时运输层自动的为该套接字提供一个端口号,从1024-65535内分配并且在当前未被该主机中任何其他UDP端口使用的号。另一种方法是在创建一个套接字后,增加一行代码,通过套接字bind()为这个UDP套接字关联一个特定的端口号。如19157。
clientSocket.bind((’’,19157))
如果应用程序开发者所编写的代码实现的是一个“周知协议”的服务器端,那么开发者必须为其分配一个相应的周知端口号。通常引用程序的客户端让运输层自动的(并且是透明的)分配端口号,而服务器端则分配一个特定的端口号。假定主机A中的一个进程具有UDP端口19157,他要发送一个应用程序数据块给位于主机B中的另一进程,该进程具有UDP端口46428。主机A中的运输层创建一个运输层报文段,其中包括应用程序数据、源端口号(19157)、目的端口号(46428)和两个其他值。然后运输层将得到的报文段传递到网络层。网络层将该报文封装到一个IP数据报中,并尽力而为的将报文段交付给接收主机。如果该报文段到达接收主机B,接收主机运输层就检查该报文中的目的端口号(46428)并将该报文交付给端口号46428所标识的套接字。主机B能够同时运行多个进程每个进程有自己的UDP套接字与相应的端口号。当UDP报文段从网络到达时,主机B通过检查该报文段中的目的端口号,将每个报文段定向(分解)到相应的套接字。
一个UDP套接字是由一个二元组全面标识的,该二元组包含一个目的IP地址和一个目的端口号。因此如果有两个UDP报文段有不同的源IP地址和/或源端口号,但具有相同的目的IP地址和目的端口号,那么这两个报文段将通过相同的目的套接字被定向到相同的进程。
源端口号的用途是被用于“返回地址”的一部分。
2.面向连接的多路复用与多路分解
TCP套接字和UDP套接字之间的一个细微差别是TCP套接字是由一个四元组(源IP地址,源端口号,目的IP地址,目的端口号)来标识的。当一个TCP报文段到达主机时,该主机用全部的四个值来将报文段定向到相应的套接字,与UDP不同的是两个具有不同源IP地址或源端口号的到达TCP报文段讲将定向到不同的套接字,除非TCP报文段携带了初始创建连接的请求。
3.web服务器与TCP
比如一个正在80端口运行Apache Web的主机,当客户向该服务器发送报文段时所有报文段的目的端口都将为80,特别是初始连接建立报文段和承载HTTP请求的报文段都有80的目的端口,该服务器能够根据源IP地址和源端口号来区分来自不同客户的报文段。
每个进程都有自己的连接套接字,但是进程与套接字之间并不总是一一对应的关系。如今高性能的Web服务器通常只使用一个进程,但是为每个新的用户连接创建一个具有新连接套接字的新线程(线程可以看作一个轻量级的子进程),对于这样一台服务器,在任意给定的时间都可能有(具有不同标识的)许多连接套接字连接到相同的进程。
如果客户与服务器使用持续HTTP,则在整条连接持续期间,客户与服务器之间经由同一个服务器套接字交换HTTP报文。但是如果客户与服务器使用非持续HTTP则对每一对请求/响应都创建一个新的TCP连接并在随后关闭,因此对每一对创建一个新的套接字并在随后关闭。这种套接字的频繁创建与关闭会严重影响一个繁忙的Web服务器的性能。(尽管有许多操作系统的技巧可以用来减轻这个问题的影响)
由【RFC368】定义的UDP只是做了运输协议能够做的最少工作,除了复用/分解功能及少量的差错检测外,几乎没有对IP增加别的东西。使用UDP时在发送报文段之前,发送方和接收方的运输层实体之间并没有握手,正因如此,UDP被称为无连接的。
DNS是一个通常使用UDP的应用层协议的例子,当一台主机中DNS应用程序想要进行一次查询时,它构造了一个查询报文并将其交给UDP,无需执行任何和目的端系统UDP实体间的握手,主机端的UDP为此报文添加首部字段然后将形成的报文段交给网络层,网络层将其封装到一个IP数据报中然后将其发送给一个名字服务器,在查询主机的应用程序则等待对该查询的响应,如果他没有收到响应(由于底层网络丢失了查询或响应),要么试图向另一个名字服务器发送该查询,要么通知调用的应用程序它不能获得响应。
UDP这么菜还要用它的原因:
采用UDP时只要应用进程将数据传递给UDP,UDP就会将此数据打包进UDP报文段并立即将其传递给网络层。另一方面由于TCP的拥塞控制机制。实时应用通常需要最小的发送速率,不希望过分的延迟报文段的传送,且能容忍一些数据丢失。这些应用并不适合采用TCP而可以使用UDP并作为应用的一部分来实现所需的、超出UDP的不提供不必要的报文段交付服务之外的额外功能。
UDP不会引入建立连接的时延。这可能是DNS运行在UDP而不是TCP上的原因(否则将会慢很多)。HTTP使用TCP的原因是作为Web网页来说可靠性是至关重要的。但是HTTP中的TCP连接建立时的延时对于与下载Web文档相关的时延来说是一个重要因素。用于Chrome浏览器中的**QUIC协议(快速UDP因特网连接)**将UDP作为其支撑运输协议并在UDP之上的应用层协议中实现可靠性。
TCP需要在端系统中维护连接状态,包括接收和发送缓存、拥塞控制参数以及序号和确认号的参数,这些状态是必要的。另一方面UDP不维护连接状态,也不跟踪这些参数。因此,某些专门用于某种特定应用的服务器当应用程序运行在UDP之上而不是运行在TCP上时,一般都能支持更多的活跃客户。
每个TCP报文段都有20字节的首部开销,UDP只有8个。
电子邮件、远程终端访问、WEB及文件传输都运行在TCP之上,但是依然有很多应用运行在UDP上,例如网络管理数据(SNMP),在这种情况下UDP要优于TCP,因为网络管理应用程序通常必须在该网络处与重压时运行,而这个时候可靠的、拥塞受控的数据传输难以实现。此外如前所述,DNS运行在UDP之上从而避免了TCP的连接创建时延。
使用UDP是可能实现可靠数据传输的,这可通过在应用程序自身中建立可靠性机制来完成(例如可通过增加确认与重传机制来实现)。
谷歌Chrome浏览器所使用的QUIC协议在UDP之上的应用层中实现了可靠性。
将可靠性直接构建于应用程序可以使其“左右逢源”,也就是说应用进程可以可以进行可靠通信,而无须受制于TCP的拥塞控制强加的传输速率限制。
|-------------------------------------------------------------------------------32比特----------------------------------------------------------------------------------|
应用层数据占用UDP报文段的数据字段。
通过端口号可以使目的主机将应用数据交给运行在目的端系统中的相应进程(即分解功能)。
长度字段指示了在UDP报文段中的字节数(首部+数据),因为数据字段的长度在一个UDP段中不同于在另一个段中,故需要一个明确的长度,接收方使用检验和来检查在该报文段中是否出现了差错。
实际上计算检验和时,除了UDP报文段以外还包括了IP首部的一些字段,但现在我们暂时忽略它。长度字段指明了包括首部在内的UDP报文段长度。
UDP检验和提供了差错检测功能,用于确定当UDP报文段从源到达目的地移动时其中的比特是否发生了改变(比如链路中的噪声干扰或者存储在路由器中时引入问题)发送方的UDP对报文段中所有16比特字的和进行反码运算,求和时遇到的任何溢出都将被回卷,得到的结果被放在UDP报文段中的检验和字段。在接收方,全部的16比特字(包括检验和)加在一起,如果该分组中没有引入差错,则显然在接收方的和将是1111111111111111,如果这些之一是0,那么该分组中就出现了差错。
提供检验和的原因是不能保证源和目的之间的所有链路都提供差错检测,也就是说这些链路之中可能会有一条或者几条链路可能没有使用差错检测的协议。即使报文经链路正确的传输,当报文段存储在某台路由器中的内存时,也可能引入比特差错,在这两者都无法保证的情况下如果端到端数据传输服务要提供差错检测,UDP就必须在端到端基础上在运输层提供差错检测。这是一个在系统设计中被称颂为端到端原则的例子,该原则表述为因为某种功能(在此处为差错检测)必须基于端到端实现:“与在较高级别提供这些功能的代价相比,在较低级别上设置的功能可能是冗余的或者几乎没有价值的。”
因为假定IP是可以运行在任何第二层协议之上的,运输层提供差错检测作为一种保险措施是非常有必要的。虽然UDP提供差错检测,但他对差错恢复无能为力。UDP的某种实现只是丢弃受损的报文段,其他实现是将受损的报文段交给应用程序并给出警告。
这个问题不仅在运输层出现,也在链路层及应用层出现,这里只讨论它的一般性。
数据可以通过一条可靠的信道进行传输,借助于可靠信道,传输数据比特就不会受到损坏或者丢失,而且所有的数据都是按照其发送顺序进行交付,这恰好就是TCP向调用它的因特网应用所提供的服务模型。
实现这种服务的抽象是可靠数据传输协议的责任,由于它的下层协议也许是不可靠的,因此这非常困难。例如TCP在不可靠的(IP)端到端网络层之上实现的可靠数据传输协议。就我们目的而言,我们可将较低层直接视为不可靠的点对点信道。在此节中贯穿始终的一个假设是分组将以它们发送的次序进行交付,某些分组可能会丢失;也就是说底层信道将不会对分组重排序。
通过调用rdt_send()函数,上层可以调用数据传输协议的发送方。它将要发送的数据交付给位于接收方的较高层。(这里的rdt表示可靠数据传输协议,_send指示rdt的发送端正在被调用)在接收端,当分组从信道的接收端到达时,将调用rdt_rcv()。当rdt协议想要向较高层交付数据时,将通过调用deliver_data()来完成。
本节中我们仅考虑单向数据传输。
现在我们一步步的研究一系列协议,它们一个比一个更为复杂,最后得到一个完美的、可靠的数据传输协议
这个1.0协议的发送方和接收方的**有限状态机(FSM)**的定义操作如下:
图中发送方和接收方的FSM每个都只有一个状态,箭头指示了协议从一个状态变迁到另一个状态,引起变迁的事件表示在横线的上方,事件发生时所采取的动作显示在横线下方。如果对一个事件没有动作,或没有就事件发生而采取了一个动作,我们将在横线上方或者下方使用符号Λ,以分别明确的表示缺少动作或事件。FSM的初始状态用虚线表示。rdt的发送端只通过rdt_send(data)事件接受来自较高层的数据,产生一个包含该数据的分组(经由make_pkt(data)动作),并将分组发送到信道中。实际上,rdt_send(data)事件是由较高层应用的过程调用产生的(例如,rdt_send())。
在接收端,rdt通过rdt_rcv(packet)事件从底层信道接收一个分组,从分组中取出数据(经由extract(packet,data)动作),并将数据上传给较高层(通过deliver_data(data)动作)。实际上,rdt_rcv(packet)事件是由较低层协议的过程调用产生的(例如rdt_rcv())。
在这个简单协议中,一个单元数据与一个分组没差别,而且所有分组是从发送方流向接收;有了完全可信的信道,接收端就不需要提供任何反馈信息给发送方,因为不必担心出现差错!注意到我们已经假定了接收方接收数据的速率能够与发送方发送数据的速率一样快,因此接收方没必要请求发送方慢一点。
实际中分组中的比特是可能受损的,这种比特差错通常会出现在网络的物理部件中。我们眼下还假定发送的分组将按其发送的顺序被接收。肯定确认(ACK)与否定确认(NAK)这种控制报文使得接收方可以让发送方知道哪些内容被正确接收,哪些内容接收有误并因此需要重复,在计算机网络环境中,基于这样重传机制的可靠数据传输协议称为自动重传请求协议(ARQ)。
重要的是,ARQ协议中还需要另外三种协议功能来处理比特差错的情况:
如图rdt2.0的FSM,该数据传输协议采用了差错检测、肯定确认和否定确认。
rdt2.0的发送端有两个状态,在左边的状态中,发送端协议正等待来自上层传下来的数据,当rdt_send(data)事件出现时,发送方将产生一个包含待发送数据的分组(sndpkt),带有检验和,然后经由udt_send(sndpkt)操作发送该分组。在右边的状态中发送方协议等待来自接收方的ACK或NAK分组。如果收到一个ACK分组,则发送方知道最近发送的分组已经被正确接收,因此协议返回到等待来自上层数据的状态。如果收到一个NAK分组,该协议重传上一个分组并等待接收方为响应重传分组而回送的ACK和NAK。注意到下列事实很重要:当发送方处于等待ACK或NAK的状态时,它不能从上层获取更多的数据。这就是说,rdt_send()事件不可能出现,仅当接收到ACK并离开该状态时才能发生这样的事件,因此,发送方将不会发送一块新数据,除非发送方确信接收方已正确接收当前分组。正因如此,rdt2.0这样的协议被称为停等协议。
rdt2.0的接收方的FSM仍然只有单一状态。当分组到达时,接收方要么回答一个ACK要么回答一个NAK,这取决于收到的分组是否受损。
rdt2.0协议看起来似乎可以运行了,但遗憾的是,它存在一个致命的缺陷,尤其我们没有考虑到ACK或者NAK分组受损的可能性!至少我们需要在ACK/NAK分组中添加检验和比特以检测这样的差错,更难的问题是协议应该怎样纠正ACK或NAK分组中的差错,这里的难点在于,如果一个ACK或者NAK分组受损,发送方无法知道接收方是否正确接收了上一块发送的数据。
考虑处理受损ACK和NAK时的三种可能性:
解决这个新问题的一个简单方法(几乎所有的现有的数据传输协议中,包括TCP都采用了这种方法)是在数据分组中添加一新字段,让发送方对其数据分组编号,即将发送数据分组的序号放在该字段,于是接收方只需要检查序号即可确定收到的分组是否一次重传,对于停等协议这种简单情况,1比特序号就足够了,因为它可以让接收方知道发送方是否正在重传前一个分组(接收到的分组序号与最近收到的分组序号相同),或是一个新分组(序号变化了,用模2变化“向前”移动),因为目前我们确定信道不丢失分组,所以ACK和NAK不需要指明它们要确认的分组序号。发送方知道所接收到的ACK和NAK分组(无论是否是含糊不清的)是为响应其最近发送的数据分组而生成的。如图给出rdt2.1(rdt2.0的加强版)
rdt2.1的发送方和接收方FSM的状态数都是以前的2倍,这是因为协议状态必须反映出目前(由发送方)正发送端分组或(在接收方)希望接受的分组的序号是0还是1,值得注意的是发送或期望接收0号分组的状态的动作与1号分组是相似的,唯一不同的是序号处理方法。
rdt2.1使用了从接收方到发送方的肯定确认与否定确认,当接收方收到一个失序的分组时,接收方对所接收的分组发送一个肯定确认。如果收到受损的分组,则接收方将发送一个否定确认,如果不发送NAK而是对上次正确接收的分组发送一个ACK也是与NAK一样的。发送方接收到对同一个分组两个ACK之后(冗余ACK)就知道接收方没有接收到被确认两次分组后面的分组。
rdt2.2是在有比特差错信道上实现的一个无NAK的可靠数据传输协议,如图
rdt2.2是在有比特差错信道上实现的一个无NAK的可靠数据传输协议,和2.1相比,接收方此时必须包括一个ACK报文所确认的分组序号(这可以通过在接收方FSM中在make _pkt()中包括参数ACK0或ACK1来实现),发送方此时必须检查收到的ACK报文中被确认的分组序号(这可通过在发送方FSM中,在is_ACK()中包括参数1或0来实现)
现在除了假定的比特差错外,丢包也并不罕见。协议现在必须关注:怎样检测丢包以及丢包之后应该做些什么,2.2中的检验和、序号、ACK分组、重传等可以解决后一个问题,为解决前一个问题需要引入一种新的机制。
在这里我们让发送方负责检测和恢复丢包的工作,无论是分组数据还是针对该分组的ACK发生丢包,发送方都收不到来自接收方的消息,所以让发送方等会来确定丢包,那么重传就完事了。
那么发送方应该等多长时间呢?很明显至少是一次往返时延和接收方处理一个分组所需要的时间。在该过程中无论是分组丢失,ACK丢失或者只是它们经历了较长的时延,发送方都会干同一件事:重传,所以在后者的情况下,信道中可能会出现冗余数据分组,还好rdt2.2中已经有了足够的功能(即序号)来处理这件事。
发送方为了实现基于时间的重传机制需要一个倒计数定时器,在一个给定的时间量过期后可以中断发送方,因此发送方需要能做到:
①每次发送一个分组(包括第一个分组和重传分组)时,便启动一个定时器
②响应定时器中断并采取相应措施
③终止定时器
因为分组序号在0和1之间交替,因此rdt3.0有时被称为比特交替协议。
rdt3.0虽然功能正确但是性能却差强人意,核心问题在于它是一个停等协议,解决方法也很简单,允许发送方发送多个分组而无需等待确认,如果发送方可以在等待确认之前发送三个报文,其利用率也基本上提高了三倍,这种技术被称为流水线技术
那么协议必须要增加一些东西:
在回退N步(GBN)协议中允许发送方发送多个分组而不需等待确认,但是受限于在流水线中未确认的分组数不能超过每个最大数N
如图那些已经被发送但是还未被确认的分组的许可序号范围被看成是一个长度未N的窗口,随着协议的运行该窗口在序号空间向前滑动,因此N常被称为窗口长度,GBN协议也常被称为滑动窗口协议。
为什么是N而不是无限制的发送呢?3.5节将讲到流量控制是原因之一,3.7学习TCP拥塞控制时分析另一个原因
称该FSM为扩展FSM,是因为我们增加了变量base和nextseqnum,还增加了对这些变量的操作以及与这些变量有关的条件动作。
GBN发送方必须响应三种类型的事件:
在GBN中,接收方的动作为,如果一个分组n被正确、按序接收到(上次交付给上层的数据是序号为n-1的分组),那么接收方为该分组发送一个ACK。在所有其他情况下接收方丢弃该分组并且为最近一个按序接收的分组重新发送ACK。注意在这个模式下如果序号为k的分组被正确交付,那么所有序号比k小的分组也已经正确交付,所以这个模型为累计确认。
在GBN协议中虽然接收方丢弃所以失序分组看起来有点浪费,但是却接收缓存简单,发送需要维护窗口的上下边界以及nextseqnum在该窗口中的位置,但是接收方只需要维护下一个按序接收分组的序号即可。该值保存在expectedseqnum中,当然丢弃一个正确接收的分组的缺点是随后对该分组的重传也许会丢失或者出错,甚至需要更多重传。
在GBN协议中也存在着很多性能问题,尤其是当窗口长度和带宽时延都很大的时候,单个分组的错误就会导致大量的分组重传,信道中可能被大量这种分组充斥,导致性能严重下降。
选择重传(SR)协议通过让发送方仅重新发送那些它怀疑接收方出错的分组而避免了不必要的重传。SR接收方将确认一个正确接收的分组而不管它是否按序,失序的分组将被缓存直到丢失所有分组(即序号更小的分组)都被接收为止,这时才能给一批分组按序交付给上层。
注意接收方需要重新确认已收到过的那些序号小于当前窗口基序号的分组,否则发送方的窗口永远不能向前滑动。这说明了和很多其他协议一样SR协议中哪些分组被正确接收,哪些没有,发送方和接收方并不总是一样的。
当面对有限序号范围的现实时,发送方和接收方窗口间缺乏同步会产生严重的后果。在一些情况下,接收方无法知道一个分组是发送方重传的分组(由于ACK的丢失)还是一个新分组,所以对于SE协议而言,窗口长度必须小于或等于序号空间大小的一半。
TCP被称为是面向连接的,这是因为在一个应用进程向另一个应用进程发送数据之前,两个进程必须相互“握手”,即必须先发送某些预备报文段,以建立确保数据传输的参数,作为TCP建立的一部分,连接的双方都将初始化与TCP连接相关的许多TCP状态变量。
这种TCP连接是一种“逻辑”连接,其共同状态仅保留在两个通信端系统的TCP程序中。所以中间的网络元素不会维持TCP连接状态,事实上中间路由器完全看不到这种连接,只能看到数据报。
TCP连接提供的是全双工服务:如果一台主机上的进程A与另一台主机的进程B存在一条TCP连接,那么应用层数据就可以同时在这两个进程之间传输。TCP连接也总是点对点的,即单个发送方和接收方之间的连接。
TCP连接是怎样连接的:
客户应用进程首先通知客户运输层,它想与服务器上的一个进程建立一个连接:
一个py客户程序通过发出这条命令来实现:clientSocket.connect((serverName,serverPort))
其中serverName是服务器名字,serverPort标识了服务器上的进程,客户与服务器上的TCP便开始建立一条TCP连接,大概过程为:客户先发送一个特殊的TCP报文段,服务器用一个特殊的TCP报文段来响应,最后,客户再用第三个特殊报文段作为响应,前两个报文段不承载“有效载荷”,也就是不包含应用层数据;而第三个报文段可以承载有效载荷,因为整个过程有三个报文段,所以这个过程被称为“三次握手”。
一旦建立起一个TCP连接,两个应用进程之间就可以相互发送数据了,数据一旦通过套接字就由客户中运行的TCP控制了,TCP将引导这些数据到该连接的发送缓存中,发送缓存是发起三次握手期间设置的缓存之一。接下来TCP就会时不时从发送缓存中取出一段数据传递到网络层。
TCP可从缓存中取出并放入报文段中的数据数量受限于最大报文段长度(MSS),它通常根据最初确定的由本地发送主机发送的最大链路层帧长度(即所谓的最大传输单元(MTU))来设置。设置该MSS要保证一个TCP报文段(当封装在一个IP数据报中)加上TCP/IP首部长度(通常40字节)将适合单个链路层帧。以太网和PPP链路层协议都具有1500字节的MTU,因此MSS的典型值为1460字节。
已经提出了多种发现路径MTU的方法,并基于路径MTU值设置MSS(路径MTU是指能从源到目的地所有链路上发送的最大链路层帧)。注意到MSS是指在报文段中应用层数据的最大长度,而不是包括首部的TCP报文段的最大长度。
TCP为每块客户数据配上一个TCP首部,从而形成多个TCP报文段,这些报文段被下传给网络层,网络层将其分别封装在网络层IP数据报中,然后这些IP数据报被发送到网络中,当TCP在另一端接收到一个报文段后,该报文段的数据便被放入该TCP连接的接收缓存中,应用程序从此缓存中读取数据流,该连接的每一端都有各自的发送缓存和接收缓存。
综上所述:TCP连接的组成包括两台主机上的缓存、变量和与进程连接的套接字;在这两台主机之间的网络元素(路由器、交换机和中继器)中,没有为该连接分配任何缓存和变量。
MSS限制了报文段数据字段的最大长度,当TCP发送一个大文件,TCP通常是将文件划分为MSS的若干块(最后一块除外,它通常小于MSS),然而,交互式应用通常传送长度小于MSS的数据块。
与UDP一样,首部包括源端口号和目的端口号,它被用于多路复用/分解来自或送到上层应用的数据,同UDP一样TCP首部也包括检验和字段。
TCP首部还包括下列字段:
首部中最重要的两个是序号字段和确认号字段,那么这两个字段中放了什么呢?
TCP把数据看成一个无结构的、有序的字节流。TCP对序号的使用,序号是建立在传送的字节流之上而不是传送的报文段序列之上,一个报文段的序号因此是该报文段首字节的字节流编号。
举个栗子,主机A的一个进程想通过TCP想主机B的一个进程传输数据,主机A的TCP将隐式的对数据流中的每一个字节编号,假定数据流由一个包含500 000字节的文件组成,其MSS为1000字节,数据流的首字节编号为0。所以TCP将数据流构建500个报文段,给第一个报文段分配序号0,给第二个报文段分配序号1000,第三个2000…每一个序号被填入到响应TCP报文段首部的序号字段中。
接下来看确认号。TCP是全双工的,A向B发送数据的同时也许正在接收来自B的数据(都是同一条TCP连接的一部分),从主机B到达的每个报文段中都有一个序号用于从B流向A的数据。主机A填充进报文段中的确认号是主机A期望从主机B收到的下一字节的序号。
举个栗子,假设主机A已经收到了来自主机B的编号为0~535的所有字节,同时假设它打算发送一个报文段给主机B,主机A等待主机B的数据流字节536及之后的所有字节,所以主机A就会在它发往主机B的报文段的确认号字段中填上536.
再举个栗子,假设主机A已经收到了来自主机B的编号为0535的所有字节,以及另一个包含字节9001000的报文段,由于某种原因,主机A还没有收到字节536~899的报文段,那么主机A为了重新构建主机B的数据流,仍然等待字节536及其之后的字节,因此A到B的下一个报文段将在确认号字段中包含536,因为TCP只确认该流中至第一个丢失的字节为止的字节,所以TCP被称为提供累积确认。
比如刚才所说,主机收到了失序报文段怎么办呢,TCP并没有对此进行规定,而是留给了实现TCP的编程人员进行处理,要么把失序的直接仍了,这样可以简化接收方的设计,要么保留失序字节,等待缺少的字节填补间隔。后一种对网络带宽来讲更有效,实践中一般采用。
在上面的假设中,初始序号为0,事实上,一条TCP连接的双方均可随机的选择初始序号,以免碰巧使用了相同的端口号并因此产生报文段的混淆。
它是一个远程登陆的流行应用层协议,运行在TCP之上,被设计成可以在任意一对主机之间工作,它与第二章讨论的批量数据传输应用不同,它是一个交互式应用,我们注意到许多用户更愿意采用SSH协议这是因为Telnet连接中发送的数据(包括口令!)是没有加密的,所以更易遭遇窃听攻击。
假设主机A发起一个与主机B的Telnet会话,因为是主机A发起该会话,因此他被标记为客户,主机B被标记为服务器,在客户端的用户键入每个字符都会被发送至远程主机 ,远程主机将回复每个字符的副本给用户,并将这些字符显示在Telnet用户的屏幕上,这种“回显”用于确保由Telnet用户发送的字符已经被远程主机收到并在远程站点上得到处理,因此在从用户击键到字符被显示在用户屏幕上这段时间内,每个字符在网络中被传输了两次。
现在假设用户输入了一个字符,用户和服务器的初始序号分别是42和79。一个报文段的序号就是该报文段数据字段首字节的序号,因此客户发送的第一个报文段的序号是42,服务器则是79。确认号就是主机正在等待的数据的下一个字节序号,在TCP连接建立后但没有发送任何数据之前该客户等待字节79,而服务器等待字节42。
共发送三个报文段,第一个由客户发给服务器,在数据字段里包含一字节的C的ASCII码。序号字段是42,由于服务器p也没发所以确认号字段是79。
第二个报文段由服务器发给客户,有两个目的:1、服务器告诉通过在确认号中填入43来告诉客户,它已经收到了42及其之前的所有字节,现在正期待着字节43的出现(搓手手);2、回显字符C,因此第二个报文段的数据字段填入到是字符C的ASCII码,第二个报文段序号是79,是该TCP连接上从服务器到客户的数据流的起始序号,也正是服务器第一次给客户发数据。值得注意的是,对客户到服务器的数据的确认被装载一个承载服务器到客户的数据的报文段中,这种确认被称为是捎带在服务器到客户的数据报文段中的。
第三个报文段是从客户发往服务器的,它的唯一目的是让客户告诉服务器确认已从服务器端收到数据,该报文段中的数据字段为空(即确认信息没有被任何从客户到服务器的数据所捎带)。该报文段的确认号字段填入的是80,因为客户已经收到字节流中序号为79以及以前的字节,正等待着字节80的出现(搓手手)。
TCP和前面所讲的rdt协议一样,采用超时/重传机制来处理报文段的丢失问题,尽管这在概念上简单,但是当在TCP这样的实际协议中实现超时/重传机制还是会产生许多微妙的问题,也许最明显的一个问题就是超时间隔长度的设置,显然超时间隔必须大于该连接的往返时间(RTT),即一个报文段发出到它被确认的时间,但是具体情况是怎么样的呢?
我们开始学习TCP定时器的管理问题,要考虑一下TCP是如何估计发送方与接收方之间的往返时间的。报文段的样本RTT(表示为SampleRTT)就是从某报文段被发出(即交给IP)到对报文段的确认被收到之间的时间量,大多数TCP的实现仅在某个时刻做一次SampleRTT测量,而不是为每个发送的报文段都测量一次,这就是说,在任意时刻,仅为一个已发送但目前尚未被确认的报文段估计SampleRTT,从而产生一个接近每个RTT的新的SampleRTT的值。另外TCP绝不为已被重传的报文段计算SampleRTT;它仅为传输一次的报文段测量SampleRTT。
显然,由于路由器的拥塞和端系统负载的变化,这些报文段的SampleRTT值会随之波动,由于这种波动,任何给定的SampleRTT的值都是非典型的,因此为了估计一个典型的RTT,自然要采取某种对SampleRTT取平均的办法。TCP维持一个SampleRTT均值(称为EstimatedRTT),一旦获得一个新的SampleRTT时,TCP就会根据下列公式来更新EstimatedRTT:
EstimatedRTT=(1-α)×EstimatedRTT+α×SampleRTT
在RFC 6298中给出的α推荐值是0.125
值得注意的是,EstimatedRTT是SampleRTT值的加权平均值,这个加权平均对最近的样本赋予的权值要大于对旧样本赋予的权值,这是很自然的,譬如你问爸爸某条路路况咋样,他上午刚开车去过的建议总要好于他前两天去过,越近的样本越能反映出网络的当前拥塞状况,这种平均被称为指数加权移动平均(EWMA)。
除了估算RTT外,测量RTT的变化也是有价值的。RFC 6298定义了RTT偏差DevRTT,用于估算SampleRTT一般会偏离EstimatedRTT的程度:
DevRTT=(1-β)×DevRTT+β×|SampleRTT-EstimatedRTT|
注意到DevRTT是一个EstimatedRTT与SampleRTT之间差值的EWMA,如果SampleRTT值波动较小,那么DevRTT的值就会很小,β推荐值为0.25。
TCP超时间隔应该大于等于EstimatedRTT,但也不能大太多,当SampleRTT值波动较大时,这个余量应该大些;相反则小些,DevRTT则在这里发挥作用:
TimeoutInterval = EstimatedRTT+4×DevRTT
推荐初始TimeoutInterval值为1秒,超时后将加倍。
网络层服务(IP服务)是不可靠的,不保证数据的按序交付,也不保证完整性,由于运输层报文段是被IP数据报携带者在网路中传输的,所以运输层的报文也会遇到这些问题。
TCP在IP的尽力而为模型上创建了一种可靠数据传输服务。TCP提供可靠数据传输的方法确保了一个进程从其从其接收缓存中读出的数据流是无损坏、无间隙、非冗余和按序的数据流,即与发送端是完全相同的。
举个栗子,A单向B发送一个大文件,A作为发送方有三个主要事件:从上层应用程序接收数据;定时器超时和收到ACK。一旦第一个事件发生TCP从应用程序得到数据封装在一个报文段中并且交给IP,每个报文段包括一个序号(如前所述这个序号为当前报文段第一个字节的字节流序号);如果定时器没有为其他报文段而运行,则当报文段被传给IP时定时器则启动(想象成定时器与最早的未被确认的报文段相关联),过期间隔是TimeoutInterval。
第二个事件是超时,TCP通过重传引起超时的报文段来响应超时事件。然后TCP重启定时器。
第三个主要事件是处理到达的来自接收方的包含有效ACK字段值的报文段,发生时,TCP将ACK的值y与它的变量SendBase进行比较,SendBase是最早未被确认的字节的序号(因此SendBase-1是指接收方已正确按序接收到的数据的最后一个字节的序号。)TCP采取累积确认,所以y确认了字节编号在y之前的所有字节都已经收到,如果y大于SendBase,则该ACK是在确认一个或多个先前未被确认的报文段,因此发送方更新SendBase变量;如果还有未被确认的,TCP重启定时器。
值得注意的是,超时间隔加倍这个东西,定时器过期很有可能是源与目的地之间的太多分组卡在路径上的一台或者多台路由器上,造成分组丢失或者长时间的排队时延,在拥塞的时候如果源持续重传分组会使拥塞更加严重。但是TCP采用的策略是,每次重传都经过了越来越长的时间。
冗余ACK和快速重传:
冗余ACK是发送方已经接收到的确认的报文段,对这个报文段的ACK进行再次确认,如果接收方接收到的报文段的序号大于下一个期望的、按序的报文段,那么就是说存在报文段的丢失,可能是因为网络中报文段的丢失或者重新排序造成的。
因为发送方经常一个接一个的发送大量的报文段,如果一个报文段丢失 ,就可能引起许多一个接一个的冗余ACK,一旦发送方接收到针对同一个数据的三个ACK,就执行快速重传,即在该报文段定时器过期之前就重传丢失报文段。
接收方是允许接收缓存这个东西存在的,到达的分组不必一到达就立马被读出,接收方可能在忙别的事,数据可能需要很长时间之后才被读出,而如果发送方一直在不停的发出数据,那就很容易导致接收方的接收缓存溢出。所以TCP为它的引用程序提供了流量控制服务,这使得发送方的发送速率与接收方的读取速率相匹配。前面提到过,TCP发送方有可能因为IP网络的拥塞而被遏制,这种形式的发送方的控制被称为拥塞控制,虽然这两个对发送方的遏制非常相似,但是是两个完全不同的概念!!!!不要搞混!!!
这里我们假设TCP的接收方丢弃失序的报文段。
TCP通过让发送方维护一个称为接收窗口的变量来提供流量控制,这个变量指示了接受方还有多少缓存空间可用。由于TCP是全双工通信,所以发送接收两方各维护一个接收窗口变量。假设主机A通过一条TCP连接向主机B发送一个大文件,主机B为该连接分配了一个接收缓存,并用RcvBuffer来表示其大小,主机B的应用程序不时从该缓存中读取数据,定义以下变量:
由于TCP不允许分配的缓存溢出,所以:
LastByteRcvd - LastByteRead ≤ RcvBuffer
接收窗口用rwnd表示,根据缓存可用空间的数量来设置,所以是动态的:
rwnd = RcvBuffer -(LastByteRcvd - LastByteRead)
主机B把rwnd填入要发送给A的报文段的接收窗口字段中来告诉A还剩多少缓存空间可用,开始时则设定rwnd = RcvBuffer,要实现这个功能需要B跟踪几个连接有关变量。
主机A轮流跟踪两个变量,LastByteSent和LastByteAcked,这两个变量之差就是已经发送但还未被确认的数据量,只要保证这个值在rwnd之内就可以了。
LastByteSent - LastByteAcked ≤ rwnd
还有一个小小的问题,假设主机B的缓存,满了!rwnd,成0了!B还没有数据要给A,也没有确认要发给A。这时候B的应用程序清空缓存,但是A不知道啊,A接收的最后一个rwnd是0啊,这就导致A被阻塞不能再发送数据。为了解决这个事,TCP规范要求,当主机B的接收窗口为0时,主机A继续发送只有一个字节数据的报文段,这些报文段将会被接收方确认,然后缓存被清空,确认报文里将会包含一个非0的rwnd的值。
(在这里我有一个疑问,报文段肯定是要按序的,那么大概率下一个报文段的大小不是正好只有一个字节,那么是从下一个报文段硬拆出一个字节还是造出一个新的特殊报文段,此报文段大小只有一字节且专门用于发送方向接收方“打听”还有没有缓存空间呢?如果是造出一个新的特殊报文段的话那就相当于在正常的数据流之中插入了一个“奇怪的东西”原本不属于这个数据流,不会对数据传输造成影响吗)
(还有,如果某时刻rwnd小于即将发送的下一个报文段,那么发送方是发还是不发呢…?)
请注意,UDP是不提供流量控制服务的,报文段由于缓存溢出可能在接收方丢失。
假设一个客户进程想与一个服务器进程建立一条连接,客户应用进程首先通知客户TCP,它想建立一个与服务器上某个进程之间的连接,遵循以下建立方式:
一旦完成这三个步骤客户和服务器就可以相互发送包括数据的报文段了,在以后的每一个报文段中,SYN比特都是0,这个建立的过程因为互相发送了三次报文段所以被称为“三次握手”。
参与TCP连接的两个进程中的任意一个都能终止该连接,连接结束后,主机中的资源(变量和缓存)将被释放。比如现在客户应用进程想关闭这个连接所以发出一个关闭连接命令,发送出一个特殊报文段,首部的一个标志位(FIN比特)被置为0,当服务器接收该报文段后就向发送方回送一个确认报文段,然后服务器它自己的终止报文段,其FIN比特被置为1,最后客户对这个终止报文段确认。此时两台主机上用于该连接的资源都被释放了。
在一个TCP连接的生命周期内,运行在每台主机中的TCP协议在各种TCP状态之间变迁,下图说明了客户TCP会经历的一系列典型TCP状态。
客户TCP开始时处于CLOSED状态,客户的应用程序发起一个新的TCP连接(比如第二章Python的栗子创建一个Socket对象),这引起客户中的TCP向服务器中的TCP发送一个SYN报文段,在发送过SYN报文段后,客户TCP进入了SYN_SENT的状态,等待来自服务器TCP的对客户所发报文段进行确认且SYN比特被置为1的一个报文段,收到这样一个报文段之后,客户TCP进入ESTABLISHED(已建立)状态,此时客户就可以发送和接收包含有效载荷数据的TCP报文段了。
假设客户应用程序要关闭该TCP连接(服务器也能)。这引起客户TCP发送一个带有FIN比特被置为1的TCP报文段,并进入FIN_WAIT_1的状态,等待一个来自服务器的带有确认的报文段,收到该报文段后进入FIN_WAIT_2状态,并等待一个来自服务器的FIN比特被置为1的报文段,收到后进行确认然后进入TIME_WAIT状态,假定ACK丢失TIME_WAIT状态使客户重传最后的确认报文,这个状态所消耗的时间是与具体实现有关的,典型的值是30s、1min或者2min。经过等待后连接正式关闭,客户所有资源包括缓存、变量和端口号将被释放。
下图显示了服务器端的TCP通常要经历的一系列状态:
其中假设客户连接拆除;
当一台主机接受一个TCP报文段,其端口号或源IP地址与该主机上进行中的套接字都不匹配的情况:比如一台主机接受了具有目的端口80的一个TCP SYN分组,但是该主机在端口80不接受连接(即它不在端口80上运行Web服务器),则该主机将向源发送一个特殊重置报文段。该TCP报文段将RST标志位(参见3.5.2)置为1,意为告诉源“我么有这个报文段的套接字,你憋发了”。当一台主机接受一个UDP分组,它的目的端口与进行中的UDP套接字不匹配,该主机发送一个特殊的ICMP数据报(将在第四章中讨论)
来更为详细的回顾一下nmap的原理,为了探索目标主机上的一个特定的TCP端口,如端口6789,nmap将对那台主机的目的端口6789发送一个特殊的TCP SYN报文段,有三种可能的输出:
来自主机A和主机B的分组通过一台路由器,在一段容量为R的共享式输出链路上传输,该路由器带有缓存,可用于当分组到达速率超过该输出链路的容量时存储“入分组”。在此第一种情况下,我们假设路由器有无限大的缓存空间。
(无线大的缓存空间和容量R是什么关系呢。。。。?)
当发送速率在0~R/2之间时,接收方的吞吐量等于发送方的发送速率(经有限时延后)。当超过时,每链接吞吐量(接收方每秒接收的字节数)只能达到R/2,这个上限是由两条连接之间共享链路容量造成的。链路完全不能以超过R/2的稳定状态速率向接收方交付分组。
看起来每连接R/2吞吐量挺好,因为链路被充分利用了,但是当发送数据越来越接近R/2时,平均时延就会越来越大,发送速率超过R/2时,路由器中的平均排队分组数就会无限增长,源与目的地之间的平均时延就会变成无穷大(假设这些连接以此发送速率运行无限长时间并且有无限量的缓存可用)甚至在这种极端理想化的情况中,我们已经发现了拥塞网络的一种代价,即当分组的到达速率接近链路容量时,分组经历巨大的排队时延。
与上一种情况不同的是,当分组到达一个已满的缓存时会被丢弃,其次我们假设每条连接都是可靠的,也就意味着如果一个包含有运输层报文段的分组在路由器中被丢弃,将会被发送方重传。我们将会以λin表示应用程序将初始数据发送到套接字中的速率,运输层向网络中发送报文段(含有初始数据或重传数据)的速率用λin’表示,有时也被称为网络的供给载荷。
此时性能强烈依赖于重传的方式,这里假设一种不真实的情况,主机A能够以某种方式确定路由器中的缓存是否空闲,因而仅当缓存空闲时才发送一个分组,这时不会产生丢包,λin和λin’相等,吞吐量就等于λin。发送速率还是不能超过R/2,因为假定分组不考虑丢失。
接下来考虑一种更为真实的情况:发送方仅当确定了一个分组已经丢失才重传,当供给载荷为R/2时,数据被交付给接收方的应用程序的速率是R/3,因此平均来看,在所发送的0.5R数据中,0.333R是初始数据,0.166R是重传数据。在此看到了另一种网络拥塞的代价,即发送方必须执行重传以补偿因为缓存溢出而丢失的分组。
最后考虑这种情况:发送方也许会提前发生超时并重传在队列中已经被推迟但是还未被丢弃的分组。在这种情况下,初始数据和重传数据都可能到达接收方,但是问题在于接收方要一份就够了,多余的同样数据会被丢弃,这就造成了链路的浪费,因为本可以用这些资源去传输新的分组。这里我们又看到了网络拥塞的另一种代价,即发送方在遇到大的时延所进行的不必要的重传会引起路由器利用其链路带宽来转发不必要的分组副本。当平均每个分组被路由器转发两次时,供给载荷接近R/2时,其吞吐量接近R/4。
有4台主机发送分组,每台都通过交叠的两跳路径传输,每台都采用超时/重传机制,所有主机都有相同的λin值,路由器的链路容量都是R字节/秒。
考虑主机A到主机C的连接,该连接经过路由器R1和R2,A - C连接与D - B连接共享路由器R1,并与B - D连接共享路由器R2,对于极小的λin,路由器缓存溢出是很少见的,吞吐量大致接近供给载荷,稍大一点的λin,吞吐量也会变大,但是依然有很少溢出。因此对于较小的λin,λin的增大会导致λout的增大。
当λin很大的时候,考虑路由器R2,无论λin的值为多大,到达路由器R2的A - C流量是经过R1转发的,所有至多是R。如果λin’是极大的值,那么在R2上,B - D流量的到达速率要比A - C流量的到达速率大得多,因为这两个连接必须要在路由器R2上为有限的缓存空间竞争,所以当供给载荷增大时,A - C连接成功通过R2的流量(未因为路由器缓存溢出而丢失)会越来越少,极端情况下当供给载荷无穷大时,A - C流量p也过不去,R2的空闲缓存立马就全是B - D连接的分组。这就引发了供给载荷和吞吐量的权衡。
由于拥塞,一旦一个分组经过一个路由器被丢弃时,那么这个分组刚才经过的所有路由器就全白忙活了,所以可以采取一些措施比如优先发送哪些刚才经历过较多上游路由器的分组。
对于后一种,拥塞信息从网络反馈到发送方通常有两种方式。
1.直接反馈信息,由网络路由器发给发送方“我拥塞了!!”,通常采用一种阻塞分组的形式。
2.更为通用的是路由器标记或者更新从发送方流向接收方分组中的某个字段来指示拥塞的产生,接收方收到了之后一看,呀呵!拥塞了!告诉发送方!(这种通知形式至少要经过一个完整的往返时间)
TCP必须使用端到端拥塞控制而不是使用网络辅助的拥塞控制,因为IP层步向端系统提供显式的网络拥塞反馈。TCP采用的方法是感知它与目的地之间的网络的拥塞程度来调整它的发送速率。那么这样就存在三个问题:
首先考虑第一个:TCP连接的每一端都是由一个接收缓存、一个发送缓存和几个变量组成,运行在发送方的TCP拥塞控制机制跟踪一个额外的变量,即拥塞窗口。表示为cwnd,对一个TCP发送方向网络中发送流量的速率进行了限制,特别的,在一个发送方中未被确认的数据量不会超过cwnd与rwnd中的最小值:
LastByteSent - LastByteAcked ≤ min { cwnd, rwnd }
为了关注拥塞控制(与流量控制形成对比),假设TCP接收缓存足够大以至于忽略rwnd的夏至,所以发送方中未被确认的数据量只受限于cwnd,所以在一个丢包和发送时延忽略不计的连接中,在每个RTT的起始点,上面的限制条件允许发送方发送cwnd的护具,在RTT结束时收到确认报文。因此发送方的速率大概是cwnd/RTT字节/秒,通过调整cwnd的值来调整发送速率。
接下来考虑TCP是如何感知的。如果将“丢包事件”定义为TCP发送方超时或者收到三个冗余ACK,那么当过度拥塞时,这条路径上的一台或者多台路由器缓存溢出造成丢包事件,发送方就会认为路径拥塞。乐观的情况是,如果没有网路拥塞,那么确认到达发送方的速率将会直接影响到拥塞窗口增加的速率,所以我们说TCP是自计时的。所以TCP如何维持合适的发送速率使得既不会浪费带宽又使得网络不会拥塞呢?
现在考虑TCP拥塞控制算法,该算法主要包含三个部分:①慢启动②拥塞避免③快速恢复,前两者是TCP的强制部分,两者的差异在于对收到的ACK作出反应时增加cwnd的方式,慢启动能比拥塞避免更快的增加cwnd。快速回复不必须。
当一条TCP连接开始时,cwnd的值通常初始置为一个MSS的较小值,这就使得初始发送速率大约为MSS/RTT,这显然比带宽小的多,TCP发送方希望尽快找到一个合适的速率,因此在慢启动状态,拥塞窗口一开始是一个MSS,然后一个变俩,俩变四个。。。
但是什么时候结束呢?
首先,丢包了!拥塞了!那就会将cwnd置为1并重新开始慢启动过程,还会将ssthresh(慢启动阈值)设置为cwnd/2。
或者当发送速率到达或超过ssthresh的值时,再将cwnd翻番可能多少有一丢丢草率,那么此时就会结束慢启动而进入拥塞避免模式模式,此后会更加谨慎的增加cwnd。
再或者,检测到三个冗余ACK,此时TCP执行一种快速重传并进入快速恢复状态。
一旦进入拥塞避免状态,cwnd的值大约是上次遇到拥塞时的一半,距离拥塞可能并不遥远。所以采取一种更为保守的方法:每个RTT只将cwnd的值增加一个MSS,一种通用的方法是对于TCP发送方无论何时到达一个新的确认,就将cwnd增加一个MSS的长度。
那么何时结束呢?出现超时时,与慢启动一样,cwnd被设置为1个MSS,丢包事件出现时ssthresh的值被更新为cwnd值的一半,然鹅丢包事件也能由三个冗余ACK引发,这种条件下网络继续从发送方向接收方交付报文段,所以相对于超时,TCP发送方的反应不那么剧烈:TCP将cwnd的值减半,并且当收到三个冗余ACK,将ssthresh的值记录为cwnd的值的一半,然后进入快速恢复状态。
对于引起TCP进入快速恢复状态的确实报文段,对收到的每个冗余的ACK,cwnd的值增加一个MSS。最终当对丢失报文段的一个ACK到达时,TCP在降低cwnd后进入拥塞避免状态。如果出现超时事件,快速回复在执行如同在慢启动和拥塞避免中相同的动作后迁移到慢启动状态:当丢包事件出现时,cwnd的值被设置为1个MSS,并且ssthresh的值设置为cwnd值的一半。
忽略一条连接开始时初始的慢启动阶段,假定丢包由3个冗余的ACK而不是超时指示,TCP的拥塞控制是:每个RTT内cwnd线性(加性)增加1MSS,然后出现3个冗余ACK事件时cwnd减半(乘性),因此TCP拥塞控制常常被称为加性增,乘性减(AIMD)。(锯齿状)
分析过程中忽略慢启动阶段(因为这个时间很短,指数增长使得其会很快离开该阶段)。在一个特定的往返时间内,TCP发送数据的速率是拥塞窗口与当前RTT的函数。窗口长度为w字节,,且当前往返时间是RTT秒时,则TCP的发送速率大约是w/RTT。TCP通过每经过1个RTT将w增加1个MSS探测出额外的带宽,直到一个丢包事件发生为止,那么当丢包事件发生时,用W表示w的值,假设在持续连接期间RTT和W几乎不变,那么TCP的传输速率在W/(2xRTT)到W/RTT之间变化。
这个模型中到达W/RTT之后发送速率减半,然后继续1个MSS、1个MSS地加直到再次达到W/RTT,不断重复,因为TCP吞吐量在两个极值之间线性增长,所以我们有:
一条连接的平均吞吐量 = (0.75xW)/ RTT
吞吐量公式:
一条连接的平均吞吐量 = (1.22 x MSS)/(RTTx根号L)
其中L为丢包率,也就是说为了取得10Gbps的吞吐量,今天的TCP拥塞控制算法仅能容忍2 x 10 (-10)次方的报文段丢失概率,也就是说每5000000000个报文段才能丢一个,是不是很蛋疼。。。
K条路径,都经过一段传输速率为R bps的瓶颈线路,无UDP流量,其他段链路都是充足的,如果每条连接的平均传输速率接近R/K,则认为该拥塞控制机制是公平的。
在如上图所示的理想状态下(两条连接具有相同的MSS和RTT,所以它们拥有相同的拥塞窗口长度就会有相同的吞吐量,也没有其他CTP或者UDP共享这些线路,也忽略慢启动阶段并且假设它一直按照AIMD运行),两条连接实现的带宽最终将沿着平等带宽共享曲线波动。(因为在共同链路消耗小于R时,每经过一个RTT都要将窗口加一,最终两条共同消耗的带宽将超过R,然后丢包然后按1/2减小,如此往复)
在实际中却不满足这些理想条件,事实表明当多条连接共享一个共同的瓶颈链路时,哪些具有较小的RTT连接能够在链路空闲时更快的抢到可用带宽,进而享用更高的吞吐量。
一些应用比如因特网电话或者视频会议更倾向于使用UDP而不是TCP,因为它们想把数据尽快的注入到网络中而不愿遵守什么拥塞机制,它们也可以接受一定程度的丢包。那人家TCP也不乐意啊,你UDP不跟其它连接合作也不适时的调整发送速率,你这不是玩赖吗。
面临拥塞时,UDP会压制TCP的传输速率,目前正在研究一种让双方都满意的拥塞机制,用于阻止UDP流量不断压制直至中断因特网吞吐量的情况。
即使我们能够迫使UDP流量具有公平的行为,公平性的问题仍然没有完全解决,因为有可能一个应用基于TCP使用多个并行连接,比如一个浏览器可以使用多个并行连接传输同一个web页中的多个对象(具体几个浏览器可以配置)
最近对于IP和TCP的扩展方案已经提出和实现部署[RFC3168],允许网络明确向TCP发送方和接收方发出拥塞信号。这种形式的网络辅助拥塞控制称为明确拥塞通告。RFC3168推荐仅当拥塞持续不断时才设置ECN比特,它在IP数据报首部占两个字节,指示被标示的数据报携带,送给目的主机,再由目的主机通知发送主机,RFC3168却没有提供拥塞时的定义,这有路由器厂商所配置,发送主机使用另一种ECN比特设置通知路由器发送方和接收方和ECN使能的,然后对于ECN指示的网络拥塞采取行动。
接收主机的TCP通过一个接收到的数据报收到了一个ECN拥塞指示,接受主机的TCP通过在接收方到发送方的TCP ACK报文段中设置ECE(明确拥塞通告回显)比特,通知发送方的TCP。然后TCP发送方通过减半拥塞窗口对一个具有ECE拥塞指示的ACK作出反应,并且在下一个传输的TCP发送方到接收方的报文段首部对CWR(拥塞窗口缩减)比特进行设置。