传输控制协议(TCP)

TCP概述

传输控制协议TCP是一个专门为了在不可靠的互联网上提供可靠的端-端字节流通信而设计的传输层协议。TCP的设计目标是能够动态地适应互联网的固有特性(包括互联网不同部分可能存在不同的拓扑、带宽、时延、分组大小和其他参数等),且面对多种差错应有足够的健壮性(robustness)。因此,TCP是一个非常复杂的协议。

TCP的主要特点是:

  • TCP是面向连接的。应用进程之间进行通信必须经历建立连接、数据传输和释放连接三个阶段。
  • ② 应用进程之间的通信是通过TCP连接来进行的。每条TCP连接有两个端点,只能实现点-点通信。
  • TCP提供可靠交付的服务。也就是通过TCP连接传输的数据,不存在差错、丢失和重复现象,能按序到达目的端。
  • TCP提供全双工通信。TCP允许通信双方的应用进程同时发送数据。在TCP连接两端都设有发送缓存和接收缓存,缓存是发送(或接收)数据的临时存放点。
  • TCP是面向字节流的。“面向字节流”的含义是:TCP把应用层下传给传输层的数据块可看成是一串无结构的字节序列流。当然,TCP并不意识所传送的字节流的含义,也不保证接收端应用进程收到的数据块与发送端发出的数据块有着对应大小的关系,但它保证接收端应用进程收到的字节流与发送端发出的字节流是完全一样的。也就是说,TCP创建了一种环境,它使得发送应用进程与接收应用进程之间好像有一条假想的“管道”,而在这条管道上传送的是字节流形式的数据。

以上特点说明,TCP有着与UDP完全不同的传输协议机制。就发送报文的方式而言,TCP传输实体根据接收端给出的窗口大小和当前的拥塞程度来决定一个报文段应包含多少字节。

如果应用进程给出的数据块太大,TCP就有必要把它分片,以便用单独的IP数据报形式发送每一个分片。

另外,由于IP层并不保证数据报一定被正确地传输给接收端,所以TCP必须具有超时判断、重传数据、纠正错序,按序重装等功能。总之,TCP必须提供多数用户所期望的可靠服务,也就是IP层没有提供的功能。

前面提到,每条TCP连接有两个端点。TCP连接的这个端点称为 套接字(socket) 或插口。根据RFC 793的定义:端口号拼接到(contatenated with)IP地址即构成了套接字。套接字的表示方法是在点分十进制的IP地址后面加上端口号,其间用冒号(或逗号)隔开。即:

套接字::=(IP地址:端口号)

例如,IP地址是130.8.16.86,端口号是80,那么得到的套接字就是(130.8.16.86∶80)。每一条TCP连接可用通信两端的两个端点(即两个套接字)来标识,即:

TCP连接::={socket1,socket2}={(IP1:Port1),(IP2,Port2)}

式中,socket1和socket2是这条传输连接的两个套接字,IP1和IP2分别表示两个端点主机的IP地址,而port1和port2分别是两个端点主机中的端口号。

总之,TCP连接是协议软件所提供的一种抽象。为两个进程之间通信而建立的一条TCP连接,其端点是套接字,即 (IP地址:端口号) 。基于传输层具有支持多个进程通信的功能,同一个IP地址可以有多条不同的TCP连接,而同一个端口号也可以出现在多个不同的TCP连接当中。

TCP报文段格式

面指出,TCP把所使用的传输协议数据单元TPDU,称为TCP报文段。一个TCP报文段由首部和数据两部分组成,其格式如下图所示。

image

首部由基本部分(20字节)和选项(4N字节,N为整数)组成。基本部分各字段的含义如下:

  • (1)源端口(16位)和目的端口(16位)。这两个字段分别填入发送该报文段应用程序的源端口号和接收该报文段的应用程序的目的端口号。
  • (2)序列号(32位)。表示本报文段所发送的字节流第一字节的序号(除SYN标志被置位的情况外)。例如,本报文段序列号为101,所携带的数据为200字节,则最后一字节的序列号应是为300。下一个报文段的序列号则应从301开始。在连接建立时,通信双方都使用随机产生器产生初始序列号。
  • (3)确认号(32位)。表示期望收到对方下一个报文段的第一个数据字节的序号(注意:确认号不是已经正确接收到的最后一字节的序号)。

例如,接收端已正确收到一个报文段,其序列号为101,所携带的数据为200字节,这表明接收端正确收到了的序列号在101~300之间的数据。因此,接收端期望收到的下一个报文段的数据应从序列号从301开始,于是接收端在发送的确认报文中将确认号字段置为301。必须指出,TCP常采用捎带技术,往往在发送的数据中捎带对对方数据的确认信息。 序列号和确认号都是 32 位长,可编序号为 0~232-1,共有 232(即 4294967296)个序号。相当于可对4GB的数据字节进行编号。在一般情况下,不会出现序号重复使用。

  • (4)数据偏移(4位)。又称首部长度。它指出首部的长度(以32位为单位),即数据部分离本报文段开始的偏移量。这是因为首部中的选项字段是可变长的,使得整个首部也是可变长的,因此设立数据偏移字段是必要的。由于数据偏移字段为4位,所对应的最大十进制数为15,因此数据偏移的最大值是60字节,这也是TCP首部的最大长度,即选项长度不能超过40字节。
  • (5)保留(6位)。留待后用。
  • (6)标志(6位)。又称控制字段,其中每一位都具有特定的控制意义。标志字段用于TCP的流量控制、连接建立和释放以及数据传送方式等方面。

各位的含义如下:

序号标志位功能描述
紧急URG表示本报文段中数据的紧急程度。URG=1表示后面的紧急指针字段有效,说明本报文具有高优先级,应尽快被发送。接收端TCP收到URG=1的报文段,它就利用紧急指针的值从报文段中提取紧急数据,不再按序地把它交付给应用程序。
确认ACK仅当ACK=1时,确认号字段才有意义。如果ACK=0,则首部中的确认号无效。TCP规定,在连接建立后所有传送的报文段都必须把ACK置为1。
推送PSHPSH=1表示请求接收端TCP将本报文段立即送往其应用层,而不是将它缓存起来直到整个缓冲区被填满后再向上交付。
复位RSTRST=1表示TCP连接中出现了严重错误,必须立即释放传输连接,而后再重建。该位还可用来拒绝一个非法的报文段或拒绝一个连接请求。
同步SYN该位在连接建立时使用,起着序号同步的作用。当SYN=1,而ACK=0时,表示这是一个连接请求报文段。若对方同意建立连接,则在应答报文段中应使SYN=1和ACK=1。可见,SYN被置位,表示该报文段是一个连接请求报文或连接接收报文,然后再用ACK来区分是哪一种报文。
终止FIN该位用来释放一个连接。FIN=1表示欲发送的数据已发送完毕,并要求释放传输连接。
  • (7)窗口(16 位)。该字段用于流控制。窗口值大小为 0~216-1。此窗口值通常由接收端确定,指的是发送本报文段的一方的接收窗口的大小,即从被确认的字节起算,还允许对方发送的字节数,作为发送端设置发送窗口的依据。例如,若确认号为501,窗口为1000,表明从序号 501 起算,接收端还能够接收 1000 字节的数据。这里,窗口值为 0 也是合法的,这相当于接收端现在状态不佳,需等一会儿才能继续接收更多的数据。
  • (8)检验和(16位)。该字段的检验范围是整个报文段(包括首部和数据)。在计算检验和时,要在TCP报文段的前面加上一个伪首部(长度为12字节)。伪首部的格式与图7-4中UDP用户数据报的伪首部一样。但需在伪首部的第4个字段填入TCP协议号6,并把第5字段改为TCP长度。TCP报文段的检验和计算以及出错处理与UDP的一样。必须指出,UDP使用检验和是可选的,而TCP使用检验和则是必选的。
  • (9)紧急指针(16位)。该字段仅当URG=1才有意义,它指出了紧急数据的末尾在报文段中的位置(紧急数据结束后就是普通数据),使得接收端能知道紧急数据的字节数。需要注意的是,即使窗口为零时,也可发送紧急数据。
  • (10)选项与填充(可变)。最长可达40字节。TCP最初只定义一种选项,即最大报文段长度MSS(Maximum Segment Size)。这个MSS指出TCP报文段中的数据部分的最大长度,而不是整个TCP报文段的长度,即TCP报文段长度中减去TCP首部长度才是MSS。若主机未填写MSS,则取其默认值为536字节,此时因特网上主机应能接受的报文长度是556字节(计入首部的固定部分20字节)。随着因特网的发展,TCP又增加了几个选项,如窗口扩大、时间戳、允许选择确认和选择确认等。当选项长度不是32位的整数倍时,填充字段用于填充补齐。

首部后面是数据部分,用于封装上层(应用层)报文,表示所发送TCP报文段的具体内容。

TCP传输控制

由于互联网层只能提供尽最大努力的交付服务(即不可靠的传输服务),传输层所面临的不只是报文段传送出错、丢失,还有可能出现因传送时延的不同而产生失序等情况。因此,TCP实现可靠传输的机制比较复杂。究其复杂的原因,归纳起来有以下4个方面:

  • TCP连接是面向字节流的。因为TCP报文段的长度变化很大,以报文段作为确认的单位显然不够方便。因此TCP采用序号确认机制。同时,为了提高传输效率,通常还采用捎带确认的策略,也就是在自己发送数据时把确认信息一起带上,而不再专门发送确认报文z段。
  • TCP能提供全双工通信,亦即通信双方可同时发送数据。为了实现传输控制,TCP连接的每一端都设有一个发送窗口和一个接收窗口。就全双工通信而言,对一条TCP连接两端的4个不同作用的窗口实施控制,使得TCP传输控制过程显得比较复杂。另外,TCP实现流量控制采用的是滑动窗口机制。
  • TCP允许发送端连续发送多个报文段,而不是采用停等确认策略。发送端可连续发送的字节数取决于当时网络的拥塞程度,以及接收端的接收能力等因素。这些因素都是随着时间而变化的。
  • ④ 数据在传送过程中的传送路径和网络拥塞情况是动态变化的,一条TCP连接的往返时间并不是固定的,因此需要设计特定的算法来估算较合理的重传时间。 在介绍TCP传输控制之前,有必要先阐述可靠传输的基本原理,然后再来讨论TCP传输控制所采用的一些机制。

TCP可靠传输的基本原理

理想的传输条件具有以下两个特点:

①传输信道不会引起差错;

②不管发送端以何种速率发送数据,接收端都来得及处理所接收到的数据。

如果能在这样理想的传输条件下,当然用不着采取任何措施就能实现可靠的数据传输。然而,实际情况并非如此。需要采用一些可靠的传输协议,在出现差错时让发送端重传出现错误的数据,以及在接收端来不及接收数据时,及时告诉发送端降低发送数据的速率。

在计算机网络发展初期,由于通信线路不可靠,在数据链路层采用可靠的通信协议,其中最简单的是停止等待协议(简称停等协议)。

为了讨论问题方便起见,下面仅考虑A向B发送数据,B向A发送应答的情况。因此,设A为发送端,B为接收端。如下图所示。

image

停止等待协议的基本要点如下:

(1)A发送完一个分组后,等待B发回应答。B收到一个分组,如果未检测出传输过程中出现的差错,则向A发回确认应答。A收到确认应答后再发送下一个分组,如图(a)所示。

(2)B收到A发来的一个分组,如果检测出传输过程中出现差错,则丢弃该分组;或者分组在传送过程中丢失,则A通过超时计时器的超时,再重传前面发送过的分组,如图(b)所示。

(3)B收到A发来的一个分组,如果未检测出传输过程中出现差错,则发回一个确认应答,但这个确认应答却在回传过程中丢失了。此时,A在设定的超时重传时间内没有收到确认,也无法知道是自己发送的分组出错或丢失,还是B发回的确认丢失了。因此,只能在A超时计时器超时后重传前面发送过的分组。当B再次收到重传的分组时,应丢弃该重传分组,并向A发送确认,如图(c)所示。

(4)B收到A发来的一个分组,如果未检测出传输过程中出现差错,则发回一个确认应答,但这个确认应答却在回传过程中延误了。此时,A因在设定的超时重传时间内没有收到确认,只得超时重传。当B再次收到重传的分组时,应丢弃该重传分组,并向A发送确认。A收到确认后,即发送下一个分组。A对迟到确认则以丢弃处理,如图(d)所示。

注意

这里需要注意三点:

①A在发送完一个分组后,必须保留该分组的副本直至收到相应的确认后方可清除;

②分组和应答都必须进行编号,以便分辨发送的分组是否得到了相应的应答;

③超时计时器的重传时间应当设定得比分组传输平均往返时间更长一些。

根据上面的阐述,停止等待协议又称自动重传请求ARQ(Automatic Repeat reQuest)。停等协议的可取之处在于简单,但其缺点是信道利用率太低。为了提高信道利用率,发送端可以采用流水线的传输方式,这便是后来的连续ARQ协议。

连续ARQ协议的基本要点是:

(1)A在发送完一个分组后,不是停下来等待应答的到来,而是连续地再发送若干个分组。

(2)B收到A发来的分组,只按序接收没有差错的分组,并给出相应的确认应答,或者只对按序到达的无差错的最后一个分组发送确认应答。对于检测出差错的分组则丢弃。

(3)A在每发完一个分组时都要开启该分组的超时计时器。如果在所设置的超时时间内收到了确认应答,就立即将超时计时器清零。若在设置的超时时间内未收到确认应答,则要重传前面发送过的分组。

(4)如果B检测出传输过程中出现的差错、丢失或延误,其处理方法同停等协议。

连续ARQ协议又称Go-back-N ARQ,意思是当出现差错必须重传时,要往回走N个分组,然后再开始重传。关于连续ARQ协议中用到的滑动窗口概念将在本节后面介绍。下图表示连续ARQ协议的原理示意图。

image

当采用连续ARQ协议时,很有可能出现这样的情况:B接收到的错误分组后面跟随着正确的分组。此时,如从出错分组起都重传,已传送的正确分组将会重复。为此,可采用选择重传策略,也就是只重传出现错误的分组。

利用上述三种协议中的序号、确认和重传机制,就可以在不可靠的链路上实现可靠的通信。虽然在传输层使用的传输控制协议要比上述协议复杂得多,但有了上面介绍的知识,就便于我们深入学习TCP传输控制机制。

TCP序号确认

针对不可靠的网络服务,TCP协议如何来实现传输实体之间的可靠传输呢?TCP采用了序号确认机制。

由于TCP协议是面向字节流的,TCP将所要传送的整个报文视为一个字节组成的数据流,并对每一字节进行按序编号来解决失序问题。通信双方在建立连接时要商定初始序号。TCP将每一次传送的报文段中的第一个数据字节的序号,写入TCP首部的序列号字段中。这种按序编号的方法也用于某些传输协议,如ISO传输协议。不过,TCP所使用的机制略有不同,其差别在于它采用隐式编号,即第一字节的序列号可能为0。

确认是对正确接收发送来的数据所表示的一种形式。TCP的确认是对接收到的数据流的最高序号(即收到的数据流中的最后一字节的序号)表示正确接收,而接收端返回的确认号是已收到的数据的最高序号加1,亦即确认号表示接收端期望下一次收到数据中的第一字节的序号。当报文段按序到达时,接收端传输实体在确认时序上有两种选择:

即时确认(immediate)。指接收端收到的数据正确而被接受,就立即返回一个确认报文。

累积确认(cumulative)。指接收端收到的数据正确而被接受,先将其作为需要确认的报文段记录在案,但不立即发送确认报文,而是等待一个携带有数据的输出报文段,并在其上捎带确认。

显然,即时确认策略很简单,但需要额外地传输用于ACK的无数据报文段(即空报文),这可能会导致更多的网络负荷。由于TCP连接提供全双工通信,通常使用的是累积确认策略,此时通信的每一端都不必专门发送确认报文段,而且尽可能在传送数据时采用捎带确认的方法,这样做有利于提高传输效率。但通信双方都需设置一个窗口计时器来确定报文往返传送的时间间隔。

为了实现可靠传输,TCP对发送、接收过程中出现的下列情况做如下处理:

如发送端在规定的时间内未能接收到确认报文段,则需重新发送未确认的报文段;

如接收端收到的报文段检测出差错,则丢弃该报文段,也不发否认报文段;

如接收端收到重复的报文段,应将其丢弃,但要发回确认报文段。

如接收端收到的报文段虽未检测出差错,但未按序号。

TCP规定:这种情况由TCP实现者自行处理。一般采用的方法是,将不按序的报文段丢弃,或者先将其暂存于接收缓存内,待所缺序号的报文段收齐后再一并上交应用层。后一种方法通常称为选择确认SACK(selective ACK)策略。因为选择确认可使发送端能更好地知道哪些报文段丢失,哪些报文段是失序到达的。这样,发送端就可以仅仅发送那些真正丢失了的报文段,从而提高了传输效率,改善了网络性能。

RFC2018 定义了两个选项:允许 SACKSACK。允许 SACK 选项(2 字节)只用于连接建立阶段。如果发送端在发送 SYN 报文段中有此选项,就说明它能够支持 SACK选项。如果接收端在它的 SYN+ACK 报文段中也包含此选项,则表示通信双方在数据传送阶段都能使用 SACK 选项。具有长度可变的 SACK 选项只能用于数据传送阶段,但需在通信双方事先进行商定。必须指出,由于 TCP 首部中并没有表明丢失报文段的信息,因此该信息只能体现在选项字段当中。每一个报文段都可用左边界Li和右边界Ri (i 为字节块序号)来指明该报文段的首末字节序号。需注意的是,右边界Ri正好与接收端返回的确认号相同,右边界减1才是该报文段末字节的序号。

如前所述,TCP的选项字段大小只有40字节。而指明一个边界要用去个4字节(因为序号为32位,需用4字节表示),因此选项字段中最多只能指明4个报文段的边界信息。这是因为4个报文段共有8个边界,这需要用32字节来描述。另外,为了指明允许SACK还需两字节,一个指明选项种类,另一个指明该选项占用的字节数。显然,如果要通知5个报文段的边界信息,就需用(5×2)×4+2=42字节,显然这已超过选项字段长度的限制。

image

上图为选择确认示意图,接收端收到了5个报文段。这些报文段在传送过程中未出现传送错误,且都在接收窗口之内。只是第1和第2报文段是连续的,而第3、第4和第5报文段是失序的,这种失序体现在第2和第3报文段,以及第4和第5报文段之间。接收端对第1、2报文段可发送一个累积确认,而对失序的报文段可发送一个SACKSACK包括两个块,第一块表示字节3001~5000是失序的,第二块表示字节6001~7000也是失序的。这意味着发送端必须重发已被丢失的字节2001~3000和5001~6000。

TCP流量控制

用户总是希望在网络上快一点传输数据。但是发送端如发送数据过快,接收端就可能来不及接收,从而造成数据丢失。流量控制旨在让发送端的发送速率不要过快,一定要使接收端来得及接收。在传输层实现流量控制比较复杂,主要有两个原因:一是传输实体之间的传输时延通常都比较长,也就是流量控制信息的通信存在相当可观的时延;二是传输层是在网络上操作的,其传输时延是随时变化的,这使得超时重传机制难以做到高的效率。

下面介绍TCP实现流量控制的机制。

(1)滑动窗口的概念

TCP的滑动窗口是以字节为单位的。为了说明滑动窗口的工作原理,首先,我们假设数据只在一个方向上进行,即A发送数据,B接收数据并给出应答。这样讨论仅涉及两个窗口:A的发送窗口和B的接收窗口。其次,我们把传送的字节数取得较少,便于图示。这样做既能简化问题,又不影响问题的实质。

假定A接收到B发来的确认报文段,其中窗口字段值为20,而确认号为21。按照报文段格式中提供的这两个数据,A就可构建出自己的发送窗口情况,如下图所示。此时A的发送状态可用三个指针P1、P2和P3来加以描述:

P1指向发送窗口内接收端期望收到的字节序号,即可发送的首字节序号;

P2指向发送窗口内允许发送而尚未发送的字节序号;

P3指向发送窗口外不允许发送的字节序号。

此时,因为A尚未发送数据,所以指针P1和P2是重合的。

下图描述了根据对方给出的数据构建己方的窗口过程:

image

在图中,发送窗口WT用来对发送端进行流量控制。WT的大小表示A在没有收到B的确认的情况下,最多还可连续发送的字节数。显然,在接收端来得及进行接收处理的情况下,WT越大,允许发送端在未收到确认之前可连续发送的数据也越多,从而获得更高的传输效率。考虑到超时重传的需要,凡是已经发送的数据,在未收到确认之前都必须暂时保留着,以备后用。

发送窗口的大小由窗口的前沿和后沿来确定。发送窗口后沿的变化有两种可能:不动(未收到新的确认)和前移(收到了新的确认)。发送窗口后沿不会后移,因为不能撤销已收到的确认。发送窗口前沿通常是不断地向前移动,但也有两种保持不动的可能:一是没有收到新的确认;二是收到了新的确认但接收端通知发送窗口要缩小,使得发送窗口前沿正好不动。当然,发送窗口前沿也可能向后退缩,这可能发生在接收端要求缩小发送窗口的时候,但TCP不支持这样做,因为发送数据后又收到缩小窗口的通知,会产生错误。由此可见,发送窗口后沿外面的数据表示已发送且已收到了确认,这些数据无须再作保留。发送窗口前沿外面的数据表示不允许发送的数据,因为接收端并没有为这些数据预留存储空间。

下图展示了A发送了12字节数据的窗口状况(a)和B的接收窗口状况(b):

imageimage

假定A已发送了序号为21~32的数据。上图(a)表示A现在的发送窗口状态。发送窗口WT内左边的12字节(21~32)表示已发送但未收到确认,而右边的8字节(33~40)是允许发送而尚未发送的。

假定B接收窗口WR为20。上图(b)表示B现在的接收窗口状态。在接收窗口左边的数据是已经得到确认的,并已交付主机,所以B不必再保存这些数据。接收窗口内的序号为21~40是允许接收的数据。此时B的接收状态可用两个指针Q1和Q2来描述:

Q1 指向接收窗口内允许接收,但尚未发送确认的字节序号;

Q2 指向接收窗口外不允许接收的字节序号。

发送窗口和接收窗口的滑动示例:

image

现在假定B收到的数据未按序到达,只收到序号为22~23,而没有收到序号21,此时B仍然只能对最高序号给出确认,即确认报文段中的确认号为21,而不是23和24。因此Q1和Q2都不能移动。

若B后来收到了序号21的数据,B就可将21及原先收到的22~23一起交付给主机,并删除这些数据。接着,B就可把接收窗口WR前移3个序号,如图(b)所示,同时给A发送确认。此时的接收窗口WR仍为20,而确认号为24。A在收到B的确认后,把发送窗口前移3个序号,但指针P2未动,如图图(a)所示。另外,B因没有收到序号24,且收到了序号25~32,未能实现按序到达,因此这些数据只能暂时存放在接收窗口中。由图可见,现在A的有效窗口已改变,可发送的序号范围是24~43。

接着A继续发送完序号33~43的数据,指针P2前移到与P3重合,如图所示。

image

此时发送窗口内允许发送的序号已用完(即A的有效窗口已减小到零),虽还没有收到确认,但必须停止发送。应注意的是,在A没有收到确认之前,只能认为B还没有收到这些数据。当A所设置的超时计时器超时时,A就进行重传并重置超时计时器,直到收到B的确认为止。如果A收到的确认号落在发送窗口之内,A就将发送窗口前移,并继续发送新的数据。

注意

最后还需强调:

①尽管 A 的发送窗口WT是依据 B 的接收窗口WR来设置的,但两者大小不一样。这是因为窗口值通过网络传送要经历一定的时延,且这个时延是不确定的。其次,A 还可能根据网络当时的拥塞情况减小自己的窗口值;

②对于不按序到达的数据,TCP没有明确的处理规定。通常把不按序到达的数据暂存于接收窗口中,等待尚缺序号到达,再一并按序交付主机。

③为了减少传输开销,TCP要求接收端具有累积确认的功能。

(2)利用滑动窗口实现流量控制

下面我们通过一个例子来说明如何利用滑动窗口在TCP连接上实现流量控制。

通信双方的发送和接收的过程,如图所示。

image

假设A向B发送数据,每一个报文段为1024字节。在建立连接时,接收端B设有4KB字节的缓冲区。

首先,A发送2个报文段,序号分别为SN=0和SN=1024。B正确收到后给出确认报文段,确认号为AN=2048,窗口WIN=2048,表示自己的缓冲空间只有2KB。

A又发送2个报文段,序号分别为SN=2048和SN=3072。B正确收到后给出确认报文段,确认号为AN=4096,窗口WIN=0,表示自己的缓冲区已满。此时A必须停止下来,等待接收端主机上的应用程序取走一些数据。

若接收端主机从接收缓冲区读取2K数据后,B发一确认报文段,确认号仍为AN=4096,窗口WIN=2048,表示自己的缓冲空间尚有2KB。 A又发送1个报文段,序号为SN=4096。B正确收到后给出确认报文段,确认号为AN=5120,窗口WIN=1024,表示自己的缓冲空间只有1KB。此时接收端主机从接收缓冲区读取2K数据后,B发一确认报文段,确认号仍为AN=5120,窗口WIN=3072,表示自己的缓冲空间尚有3KB。

A又发送1个报文段,序号为SN=5120。如此等等。

当窗口为0时,原则上发送端是不能再正常地发送报文段了,但有两种意外情况:

① 紧急数据仍可以发送。如要求用户中止远程主机上运行的某一个进程;

② 在图7-14中,B向A发送了窗口WIN=0的确认报文段后,因应用程序读取了数据,B的接收缓冲区又有了存储空间。于是向A发送窗口WIN≠0的确认报文段。但是,如果这个报文段在传送过程中丢失,A将一直等待接收B发送的非零的窗口通知,这样,就会造成相互等待的死锁现象。

为了解决这个问题,TCP为每一个连接设置一个持续计时器(persistence timer)。当TCP连接的一方收到对方的零窗口确认报文段,就启动持续计时器。若持续计时器超时,就发送一个零窗口的探询报文段(仅携带1字节的数据),以便让接收端重新发送下一个期望的字节号和窗口大小。对方在确认这个探询报文段时应给出现有的允许窗口值。如果窗口值仍然为零,那么收到这个报文段的一方就重置持续计时器。否则,死锁僵局结束。

TCP发送控制

用程序把数据写入TCP发送缓冲区后,发送这些数据的任务是由TCP来完成的。RFC 793建议中仅指出:“TCP在它方便时以报文段为单位发送数据”,但并没有指明发送数据的具体时机。

其实,TCP控制报文段的发送可以有三种机制:

TCP维持一个变量,它等于最大报文长度MSS。只要缓冲区中存放的数据达到MSS字节时,就组装成一个报文段发送出去。

② 由发送端的应用进程指明要求发送报文段,利用TCP报文段格式中控制字段的推送操作位(push)的作用。接收端TCP收到此报文段后,就尽快地交付给接收端的应用进程,而不再等到整个缓冲区都填满了以后再向上交付。

③ 发送端设置一个计时器,待计时器所设置的时间一到,就把当前缓冲区中的数据装入报文段(长度不超过MSS)发送出去。但问题在于如何控制TCP发送报文段的时机?

例如,用户考虑使用一条telnet连接,该交互式编辑器对用户的每次击键动作都做出响应。假设用户只发一个字符,这个字符到达发送端的TCP实体的时候,TCP就创建一个21字节的TCP报文段(其中首部20字节,数据部分1字节),并将它交给IP作为一个41字节的IP数据报(含IP首部20字节)发送出去。在接收端,TCP立即发送一个40字节的确认报文段(仅TCP首部20字节和IP首部20字节)。如果用户要求远端主机回送这个字符,则又要发回41字节的IP数据报和40字节确认IP数据报。这样,用户每发送一个字符,就需要传送4个报文段(共162字节)。这对于带宽紧缺的场合,显然不是一种合适的处理方法,因为此法的传送效率太低。针对这种情况,TCP采用了捎带确认的方法。

尽管采用捎带确认可使报文段个数和所用的带宽减少一半,但发送端的工作方式是低效的。因为发送41字节的报文段中只包含1字节的数据,其利用率只有1/41。所以,在TCP实现中,广泛采用Nagle算法。该算法指出:应用进程把要发送的数据以一个字节的方式送入发送缓冲区,发送端只先发送第1字节,把后面到达的字节缓存起来,直到收到对第1字节的确认为止。然后将缓冲区中的所有数据组装成一个报文段发送出去,并继续对随后到达的数据进行缓存,直到前面发送出去的报文段被确认,再继续发送下一个报文段。如果数据到达较快而网络速率较慢,用这种方法可以大大减少所用的网络带宽。Nagle算法还规定:当到达的数据填满发送窗口的一半或达到报文段的最大长度时,也允许发送一个报文段。

另一个可使TCP性能退化的问题值得考虑。当数据以大块的形式被传送给发送端的TCP实体时,在接收端缓冲区已满的情况下,如果接收端的交互式应用每次仅读1字节数据,然后向发送端发送确认(此时窗口值仅为1字节);接着发送端又发来1字节的数据(实际上发来的是41字节的IP数据报),接收端再发回确认(窗口值仍为1字节),那么这个过程可能会永久地持续下去,使得网络效率十分低下。这种现象称为愚蠢窗口综合征(silly windows syndrome)。

愚蠢窗口综合征症状如下所示:

image

为了解决愚蠢窗口综合征的症状,Clark提出的解决方案是禁止接收端发送只有1字节的窗口更新报文段,而是等待一段时间,直到有了一定数量的缓冲区空间之后再通知发送端。特别是,当接收端能够处理它在建立连接时通信双方协商好的最大报文长度MSS的大小,或者它的缓冲区已有一半空出时,它就应该发送窗口更新报文段。

上述Nagle算法和Clark针对愚蠢窗口综合征的解决方案是互补的。Nagle试图解决由于发送端应用进程每次向TCP传送一字节而引起的问题,而Clark则试图解决由于接收端应用进程每次从TCP字节流中读取一字节所带来的问题。这两种策略均有效,可以配合使用。这使得在发送端不要发送太小的报文段的同时,接收端也不要在接收缓冲区刚刚有一点空间时就匆忙地把这个很小的可用窗口通知给发送端。

TCP重传控制

在传送报文段过程中,凡出现下列两种情况都必须将该报文段重新传送。第一,报文段在传输过程中受损,但仍能到达接收端。该报文段被接收端检验发现差错,接收实体便将其丢弃。此后,发送端等待应答超时,必须重传原来发送过的报文段;第二,报文段在传送过程中被丢失,没能到达接收端目的站,这纯属偶然事件。对这种情况,由于发送端的传输实体并不知道这个报文段的传输已经失败,因此也要重传原来发送过的报文段。

为了控制丢弃的或丢失的报文段,TCP使用了重传计时器(retransmission timer)。该计时器用来处理报文段的确认与等待重传的时间。当TCP发送报文段时,就要创建该报文段的重传计时器,并设定超时重传时间RTO(Retransmission Time-Out)。在此之后可能会发生两种情况:一是如果在计时器设定的RTO时间内收到了对该报文段的确认,则将该计时器撤销。二是如果在报文段被确认之前重传计时器超时,那么发送端就要重传这一报文段,并重置重传计时器。但是,超时重传时间的选择却是个非常复杂的问题,因为TCP面临的是互联网环境。如果把超时重传时间设置得太短,就会引起很多报文段不必要的重传,从而增大网络负荷;若把超时重传时间设置得过长,则又使网络的空闲时间延长,降低了传输效率。

解决这一问题的方案是使用一种自适应的算法,它根据网络性能的连续测量情况,动态地调整超时重传的时间。这种算法记录着传送报文段的往返时间RTTTCP保留RTT的一个加权平均往返时间RTTS(又称平滑往返时间,S是Smoothed)。当第一次测量到RTT样本时,RTTS值就取为所测量到的RTT样本值。但以后每测量至一个新的RTT样本,就按下式重新计算:

新的RTTs=(1-α)*旧的RTTs + α*新的RTT样本

式中,0≤α<1。显然,当α接近于 0,表示新的 RTTS值和旧的 RTTS值相比变化不大,亦即新的RTT样本影响不大,RTT值更新缓慢。当α接近于1,则表示新的RTTS值受新的RTT样本影响较大,RTT值更新较快。RFC 2988推荐的α值为1/8(即0.125)。

显然,超时计时器的**超时重传时间RTO(Retransmission Time-Out)**应略大于上面得出的加权平均往返时间RTTS。RFC 2988建议RTO可按下式计算:

RTO=RTTs + 4*RTTd

式中,RTTDRTT的偏差的加权平均值,它与RTTS和新的RTT样本之差有关。RFC 2988建议:当第一次测量时,RTTD值取为被测量到的RTT样本值的一半,在以后的测量中,则用下式来计算加权平均的RTTD:

新的RTTd=(1-β)*旧的RTTd | β* |RTTs-新的RTT样本|

式中,β是一个小于1的系数,其推荐值为1/4(即0.25)。

由此可见,自适应算法是基于新的RTT样本的正确测定。但在有重传的情况下,新的RTT样本是很难测定的。因为若未重传的确认报文段在报文重传之后才收到,就无法断定确认报文段的真正归属。为此,Karn提出了一个新的测定平均往返时间的Karn算法:在计算加权平均RTTS时,只要报文段重传了,就不采用其往返时间样本。这样得出的加权平均RTTS和重传时间就较准确。但是,Karn算法 避开重传,只测定无重传报文的平均往返时间,这与实际情况不甚相符。

后来对初始的Karn算法作了修正,其方法是:把重传确认考虑在内,每重发一次报文段,就把重传时间增大一些。典型的做法是取新的重传时间为旧的重传时间的2倍。而在无重传情况下,超时重传时间仍按公式如下计算。

RTO=RTTs + 4*RTTd

实践证明,经改进的Karn算法非常有效。

TCP拥塞控制

TCP拥塞控制基本原理

计算机网络中拥有许多资源(如带宽、缓存、路由器、主机等),当对网络中某个(或多个)资源的需求超过了该资源所能提供的能力,而导致整个网络性能下降的的现象,称为拥塞(congestion)。网络出现拥塞的条件可表示为:

对资源的需求之和 > 可用资源

下图表示网络吞吐量与输入负荷的关系曲线:

image

上图表示网络吞吐量与输入负荷的关系曲线。此曲线的横坐标表示提供给网络的输入负荷,即单位时间内输入网络的分组数目。纵坐标表示吞吐量,即单位时间内从网络输出的分组数目。对于具有理想拥塞控制的网络,在吞吐量饱和之前,网络吞吐量与提供的输入负荷是成正比的,即吞吐量曲线呈45°斜线。当输入负荷超过某一数值后,由于网络资源有限,吞吐量就不再增长而保持水平线,表明此时吞吐量已达到饱和,所提供的输入负荷中有一部分被某些结点丢失了。

实际的情况则不同。在没有拥塞控制的情况下,随着输入负荷的增加,网络吞吐量的增长速率会逐渐减慢。也就是说,在吞吐量尚未达到饱和之前,就已经有一部分输入分组被丢失了。吞吐量明显小于理想的吞吐量表明网络已进入轻度拥塞的状态。更值得注意的是,当输入负荷达到一定数值时,吞吐量反而随着输入负荷的增大而下降,此时网络进入了拥塞状态。当输入负荷继续增大到某一数值时,吞吐量就会下降为零,说明网络已无法运行。这种现象称为死锁(deadlock)。为了改善网络的性能,实际上都要采取一定的拥塞控制措施,此时的曲线情况就界于理想的和无拥塞控制之间。

从前面介绍的出现网络拥塞的条件来看,造成网络拥塞的原因,表面上看似乎是资源短缺,只要增大网络的某些可用资源,或者减少一些用户对某些资源的需求,就可解决网络的拥塞问题。其实不然,因为造成网络拥塞涉及许多因素,并不是单纯地通过增加网络资源就能得到解决的。例如,增大结点的缓存空间是有利于存放到达该结点的分组数目,但输出链路和主机的处理速率并未提高,这只能使分组在结点中的排队时间大大增加,因传送超时上层软件只得重传分组,这使问题更加严重。又如,提高主机的处理速度可能会将问题转移到其他地方,而造成新的瓶颈。可见,只有改善整个网络的匹配状况,使得各部分保持平衡,问题才能解决。

实施拥塞控制有两种机制:一种是开环拥塞控制,它是在拥塞发生之前采用一些策略(如重传、确认和丢弃)以免网络进入拥塞状态的“预防”机制,防止过多的分组输入网络以免网络过载。在此机制中,拥塞控制既可在源端也可在目的端进行。另一种是闭环拥塞控制,它是试图在拥塞发生后使网络从拥塞状态中摆脱出来的“恢复”机制。不同的协议采用不同的“恢复”机制(如反压、阻流点、发送隐式或显式信号等)。显然,这两种机制都必须了解网络内部的流量分布状况。同时,实施拥塞控制还必须在结点之间传送命令和信息,这些都是额外的开销。拥塞控制有时还需要将一些资源(如带宽、缓存等)分配给个别用户单独使用,这更会造成资源的短缺。这些都说明进行拥塞控制是要付出代价的。

拥塞控制与流量控制存在着一定的关系。流量控制是一个局部性的问题,属于点-点的通信量控制,其目的在于抑制发送端发送数据的速率,以便接收端来得及接收。拥塞控制却是一个全局性的问题,它涉及所有的主机、路由器和链路等网络资源,以及与降低网络性能有关的所有因素。不过,实现拥塞控制时也要向发送端发送控制报文,请求发送端降低发送速率,这一举措两者是相同的。

实践证明,实现拥塞控制并不容易。因为这是一个属于动态控制的问题。从控制理论来看拥塞控制,包括开环控制和闭环控制两种方法。前者就是在设计网络时必须周密地考虑产生拥塞的各种因素。后者则基于反馈环路,通过监测系统发现拥塞、发送拥塞信息、调整运行状态等来达到拥塞控制的目的。不过,过于频繁地采取拥塞措施将使网络处于不稳定的振荡状态,迟缓地采取行动又不具有任何价值,因此只能选择某种折中措施。

在因特网中,尽管网络层也试图进行拥塞控制,但真正解决网络拥塞是由传输层TCP来完成的,降低发送数据速率是解决网络拥塞的有效措施。下面介绍TCP采用的一些拥塞控制方法。

TCP采用的拥塞控制方法

为了进行拥塞控制,TCP曾出现过多种版本。1999年公布的因特网建议RFC 2581定义了4种拥塞控制算法,它们是慢启动(slow-start)拥塞避免(congestion avoidance)快重传(fast retransmit)快恢复(fast recovery)。后来,RFC 2582和RFC 3390又对这些算法进行了改进。

为了简化拥塞控制的讨论,这里作出两个假设:

①数据传送是单向的,即一端发送数据,另一端接收数据,并发送确认;

②接收端拥有足够大的缓冲空间,发送窗口的大小由网络拥塞程度来决定。

下面介绍这4种算法的基本思想。

(1)慢启动和拥塞避免

如前所述,源端使用的发送窗口越大,它在等待确认之前可发送的报文段就越多。这在初次建立连接时可能会出现问题,因为发送传输实体如将整个窗口中的报文段都发送出去,就会造成网络中的流量过大。要避免这种情况,可采用如下策略:让源端从某个相对较大的窗口而不是最大窗口开始发送,然后在发送过程中逐渐逼近连接最终提供的窗口大小。

对于每一个TCP连接,需要设定两个窗口变量:

① 接收端窗口rwnd(receiver window)。又称通知窗口,指接收端根据其接收能力许诺的当前允许发送的最新窗口值(以报文段为单位)。接收端将此窗口值放入TCP报文首部的“窗口”字段传送给发送端。这是来自接收端的流控制。

② 拥塞窗口cwnd(congestion window)。指发送端根据网络的拥塞情况而设定的窗口值(以报文段为单位)。这是来自发送端的流控制。

发送端确定拥塞窗口的原则是:以不出现网络拥塞为前提,只要没有出现拥塞,就增大此拥塞窗口,否则就将其减小。那么,发送端又是如何知道网络发生拥塞现象呢?由于网络发生拥塞时,发送端就不能按时收到应当到达的确认报文,于是就可以此作为判断网络是否发生拥塞的条件。

在任何时候,发送端的发送窗口可按下式确定:

发送窗口=min[rwnd,cwnd]

此式表明,发送窗口选取“接收端窗口”和“拥塞窗口”中的较小者。同时还告诉我们,当rwnd<cwnd时,是接收端的接收能力限制了发送窗口的最大值。但当rwnd>cwnd时,则是网络的拥塞限制发送窗口的最大值。也就是说,以rwnd和cwnd中较小的一个来控制发送速率。

慢启动算法的基本思想是:当创建或打开一个连接时,传输实体将“拥塞窗口”初始化为1(注:此数字“1”表示一个最大报文段MSS的数值)。这就是说,只允许发送一个报文段,并在传送第二个报文段之前等待确认应答的到来。以后每收到一个对新的报文段的确认,就将拥塞窗口的值增加1。以此逐渐增大拥塞窗口,使得注入网络的分组速率更加合理。当出现超时,拥塞窗口降低到1。慢启动算法是由Van Jacobson提出的。

“慢启动”这个术语其实有点词不达意,因为实际的拥塞窗口是以指数规律增长的,也即发送端发送的分组数是按指数递增的。使用慢启动算法可使发送端在开始发送时向网络注入的分组数大为减少,这对防止网络出现拥塞是一个非常有力的措施。

在初始化连接时,慢启动算法对防止网络发生拥塞很有成效,它使发送端尽快决定本次连接窗口的合理值。那么,在网络出现拥塞迹象时,如何动态调整发送窗口的大小呢?解决这个问题最简单的方法是将拥塞窗口复位到1,并重新开始慢启动过程。Van Jacobson指出,“让网络进入饱和状态很容易,而让网络从饱和状态中恢复却很难”。这句话的意思是,一旦拥塞发生了,要消除拥塞就要花很长的时间。因此,Van Jacobson提出在连接初期使用慢启动。当发生一次超时,则按如下规则动态调整窗口大小:

① 设慢启动的门限值ssthresh为出现拥塞时发送窗口值的一半。

② 设置cwnd=1,并执行慢启动过程,直到cwnd=ssthresh。

③ 当cwnd≥ssthresh时,则每过一个往返时延RTT就对cwnd加1,使得cwnd按线性规律增长。

下图表示慢启动和拥塞避免算法示意图。其具体过程如下:

image

① 当建立TCP连接时,设cwnd=1,ssthresh=16。此时发送窗口不能超过rwnd和cwnd中的最小值。再假设rwnd足够大,则发送窗口值等于拥塞窗口值cwnd。

② 在执行慢启动算法时,cwnd的初始值为1。以后发送端每收到一个对新报文段的确认ACK,就把cwnd加1,再开始下一次的传输。此时,cwnd是按传输次数的指数规律增长的。当cwnd增长到ssthresh时(即当cwnd=16时),就改为执行拥塞避免算法,cwnd按线性规律增长。

③ 假定cwnd增长到24时,网络出现超时(表明出现了网络拥塞)。更新慢启动门限值ssthresh为12(即发送窗口值24的一半),cwnd再重新设置为1,并执行慢开始算法。当cwnd=12时,改为执行拥塞避免算法,cwnd按线性规律增长,每经过一个往返时延就增加1。

使用慢启动,并在拥塞时动态调整窗口大小,使得发生网络拥塞的时间由4次往返时延延长到11次往返时延。这说明防止发生网络拥塞的性能得到了明显的改善。必须指出,“拥塞避免”并未完全能够避免拥塞,只是使网络不容易出现拥塞而已。

最后指出,在TCP拥塞控制文献中还经常出现“乘法减小(multiplicative decrease)”和“加法增大(additive increase)”的提法。所谓“乘法减小”是指在慢开始阶段或拥塞避免阶段,只要出现一次超时(即出现一次网络拥塞),就把ssthresh值减半。这样,当网络频繁出现拥塞时,ssthresh值就下降得很快,从而大大减少输入网络的分组数。而“加法增大”是指执行拥塞避免算法后,当收到对报文段的确认就将cwnd加1,以使拥塞窗口慢慢增大,防止网络过早出现拥塞。

(2)快重传和快恢复

前面两种算法是TCP最早使用的拥塞控制算法。后来人们发现还可以对它们进行改进,因为我们的目的只是为了尽快判明是否真正出现了拥塞,而不必因等待重传计时器的超时而浪费较长的时间。为此又增加了两种新的拥塞控制算法——快重传和快恢复。

快重传算法的基本思想是:要求接收端每收到一个报文后就立即发出确认,而不要等待自己发送数据时才进行捎带确认;发送端只要接连收到三个重复的确认报文,就认为该报文段已经丢失,但不一定是由于网络发生了严重的拥塞所致,于是就立即重传丢失的报文段,而不是继续等待相应的重传计时器的超时。实践证明,采用快重传可以使整个网络的吞吐量增加约20%。

快重传并未取消重传计时器,而是尽早地重传未被确认的报文段。与快重传配合使用的还有快恢复算法。在不使用快恢复算法时,发送端发现网络出现拥塞就把cwnd重新设置为1,然后执行慢启动算法。这样做的缺点是网络不能很快地恢复到正常工作状态。快恢复算法的要点是:

① 当发送端收到三个重复的某确认报文段时,就按照“乘法减小”把慢启动门限值ssthresh减半。

② 由于发送端并不认为现在网络已发生拥塞,就把cwnd设置为慢启动门限值ssthresh减半后的数值,并开始执行拥塞避免算法继续发送报文段,使拥塞窗口缓慢地线性增大。

下图给出了“TCP Reno版本”的快重传和快恢复算法的示意图,这是目前使用得最为广泛的一种版本。

image

不过,也有的快重传算法是把开始时的拥塞窗口 cwnd 值再增大一些,设置为ssthresh+3×MSS。其理由是既然发送端收到了三个重复的确认,就表明有三个报文段是停留在接收端的缓存中,而不是堆积在网络里,因而可以适当地增大拥塞窗口。

显然,采用快恢复算法将使TCP拥塞控制性能得到明显的提高。同时,不难看出,在采用快重传和快恢复算法时,慢启动算法只在TCP连接建立或网络出现超时的情况下才使用。

TCP连接管理

TCP是面向连接的协议。传输连接的建立和释放是每一次面向连接通信中两个不可缺少的过程。传输连接管理就是使传输连接的建立和释放均能正常地进行。

建立连接时要解决三个问题:

①使通信双方都能够确知对方的存在;

②允许通信双方协商可选参数(如最大报文段长度、最大窗口、服务质量等);

③对传输实体资源(如缓存空间、连接表的表项等)进行分配。

TCP连接的建立采用客户/服务器方式。主动发起连接建立的应用进程为客户(client),而被动等待连接建立的应用进程为服务器(server)。

TCP连接的建立

若A是运行TCP客户程序的客户机,而B为运行服务器程序的服务器。两者的最初状态都处于CLOSED状态。

TCP连接过程是由服务器开始的。B运行服务器程序的进程首先创建传输控制块TCB(Transmission Control Block),准备接受来自客户进程的连接请求。然后进入LINTEN状态(即“听”的状态),不断检测是否有客户进程发出的连接请求。如有,则立即予以响应。

A运行客户程序的进程也创建传输控制块TCB,并向其TCP发出主动打开(active open)命令,表示要向某个IP地址的指定服务器建立传输连接。

TCP连接的建立过程如下:

① A客户进程向B服务器发出连接请求报文段,其首部中的SYN=1,同时选择一个序列号SN=i,这表明在即将传送的数据的第一字节的序号为i。TCP的标准规定:对SYN=1的报文段要赋予一个序列号,即便这个报文段中没有数据。此时,TCP客户进程进入 SYN-SENT状态。

② B服务器收到A的连接请求报文段后,如同意连接,则回答确认报文段。确认报文首部中的SYN=1,ACK=1,其序列号SN=j,确认号AN=i+1。此时TCP服务器进程进入SYN-RCVD状态。

③ A客户进程收到确认报文段后,还要向B回送确认。确认报文段首部中的ACK=1,确认号为AN=j+1,而序列号为SN=i+1。此时,运行客户进程的A告知上层应用进程连接已建立(或打开),进入ESTABLISHED状态。而运行服务器进程的B收到A的确认后,也通知上层应用进程,同样也进入ESTABLISHED状态。

下图表示TCP建立连接的过程。

image

以上连接建立的过程,通常称为三次握手(three-way handshake)。其实,三次握手是指在连接建立过程中通信双方交互3 个报文(请求—确认—再确认)的过程。那么,为什么建立连接时需要发送第三个报文段(即第三次握手)呢?这主要是为了防止已失效的连接请求报文段又传送到B而产生差错。

在正常情况下,假设A向B发出连接请求报文,B应返回确认报文以证实这条连接。此时可能会出现两种错误:一种是A的连接请求报文丢失,另一种是B的确认应答报文丢失。这两种情况都可以通过重传计时器超时来处理。一旦重传SYN计时器超时,A便重发一个连接请求报文,待收到B的确认,就建立了连接。数据传输完毕后,再释放本次连接。

不过,建立连接必须考虑到网络服务的不可靠性。如果出现了这样的异常情况,假设A向B发出的第一个连接请求报文并没有丢失,而是滞留在网络中的时间过长,以致在连接释放之后才传送到B。其实,这已是一个失效的连接请求报文,但B却把这个失效的连接请求报文误认为是A发出的一个新的连接请求。于是B就向A发出确认报文,同意建立连接,但实际上A并没有建立连接的要求,因此就不理会B的确认,也不向B传送数据,而B却以为连接已经建立,并一直苦等A发送数据,从而白白地浪费了B的许多资源。针对这种异常情况,采用三次握手的策略,A就不会对B发出的确认给予再确认,而B也因收不到A的确认,就知道A并没有建立连接的要求。这样,就防止了已失效的连接请求报文又传送到B而出现错误。

顺便指出,在连接建立阶段,一条连接的源端口与目的端口是唯一的。因此,任何时候一对端口之间仅存在一条TCP连接。当然,一个给定的端口允许支持多个连接,但这条连接的另一端的端口号肯定是不同的。

TCP连接的释放

TCP连接建立起来后,接着是进行数据传输。数据传输结束后,A和B均处于ESTABLISHED状态。此时,通信的任何一方都可以发出释放连接的请求,要求终止本次连接。连接释放过程和连接建立时的三次握手本质上是一致的,但比较复杂。

TCP连接的释放过程如下:

① 假设由A向B发出连接释放报文段,其首部中的FIN=1,同时选择一个序列号SN=u,它是前面已传送过的数据的最后一字节的序号加1,表示发送数据已告结束,主动关闭TCP连接。此时A进入FIN-WAIT-1状态,等待来自B的确认。

② B收到释放连接报文后,如同意连接,则回答确认报文段。确认报文段首部中的SYN=1,ACK=1,其序列号SN=v,确认号AN=u+1。然后B进入CLOSED-WAIT状态,同时通知高层应用进程。这样,从A到B的连接就释放了,连接处于半关闭(half-close)状态,这相当于A告诉B:“我已没有数据要发送,但你仍可向我发送数据”。A收到来自B的确认报文段后,就进入FIN-WAIT-2状态,等待B再发来连接释放报文段。

此后,B不再接收来自A的数据,但B若有数据要发往A,仍可继续发送。若B向A的数据发送完毕后,就向A发出连接释放报文段。在此报文段中应将FIN置成1。同时置SN=v,它是前面已传送过的数据的最后一字节的序号加1。另外,必须重复上次已发送过的确认号AN=u+1。此时B进入LAST-ACK状态,等待A发来的确认报文段。

③ A收到连接释放报文段后,必须对此发出确认,其确认号为AN=v+1,而序列号SN=u+1。然后进入到TIME-WAIT状态。B收到了来自A的确认报文段后,就进入CLOSED状态,并销销相应的传输控制块TCB,就结束了本次的TCP连接。

必须注意的是,进入到TIME-WAIT状态后,本次TCP连接还没有完全释放掉,必须再经过时间等待计时器设置的时间(=2MSL)后,A才进入到CLOSED状态,此时整个连接才全部释放。

有的读者可能会问:为什么要设置TIME-WAIT状态并使A在该状态下等待2MSL时间呢?这主要出于两方面的考虑:①为了防止“已失效的连接请求报文”出现在本次连接当中。当A发送完最后一个确认报文段后,再经过足够的时间间隔2MSL,就可避免“已失效的连接请求报文”出现在新的连接中。②为了保证A发送的最后一个确认报文段能够到达B(上述步骤③)。因为这个最后的确认报文段可能丢失,使得处于LAST-ACK状态的B还以为它所应答的FIN+ACK报文段丢失了,于是B就会因超时而重传FIN+ACK报文段。设置时间等待计时器就是为了在2MSL时间内能够收到这个重传的FIN+ACK报文段。这样,A收到此报文后重传一次确认,并重启时间等待计时器。最后,A和B都进入CLOSED状态。如果A在TIME-WAIT状态不等待2MSL时间,而提前关闭本次连接,那么A就永远收不到B重传的FIN+ACK报文段,也不会再发送确认报文段,这样,B就无法进入CLOSED状态关闭本次连接。

下图表示TCP连接的释放过程。

image

时间等待计时器所设置的时间为**最长报文段寿命MSL(Maximum Segment Lifetime)**的两倍。RFC 793建议这个时间为2min,但工程上可根据用户具体情况加以调整,如设为4min,那么要经过4min,A才能进入CLOSED状态,这样就可以确保本次连接上创建的所有报文段都已消失,然后才允许开始建立新的连接。

除时间等待计时器外,TCP还设有一个保活计时器(keepalive timer)。这是为防止建立本次连接的一端出现故障而设置的。当一个连接空闲了较长时间之后,保活计时器可能超时,从而促使客户端(或服务器端)发送探询报文段查看服务器端(或客户端)是否仍然存在。如果另一端没有应答,则本次连接关闭。

TCP连接管理模型

在因特网的管理中心设有管理信息库MIB(Management Information Base),该库中存放着各主机的TCP连接表,表中对每一个连接都记录着连接信息,其中包括本地的和远地的IP地址、端口号,以及每一个连接所处的状态。

TCP连接的建立和释放过程可用一个有限状态机来描述。该状态机有11种可能的状态,如下表所列。

image

每一种状态中都存在一些合法事件。发生合法事件时可能要采取某个动作,而发生其他事件时,则应报告一个错误。

TCP连接管理有限状态机描述了所有连接可能处于的状态及其变迁,如下图所示。

image

图中方框表示TCP当时所处的状态,状态名称用大写英文字符串表示。各方框(即状态)间的带箭头线条表示可能发生的状态变迁。箭头线旁边的标注文字说明状态变迁的原因或发生状态变迁后出现的动作。箭头线上注有斜线隔开的两个字符串。前者表示TCP收到的输入,后者表示TCP发出的输出。请注意,图中粗实线表示客户进程的正常状态变迁,粗虚线表示服务器进程的正常状态变迁,而细线表示异常的状态变迁。在未建立连接时,系统处于关闭状态CLOSED(图中起始点)。

为了更好地理解图中的内容,我们先从粗实线的客户路径开始来分析状态变迁的情况。假设主机上的客户进程发出连接请求(主动打开),本地TCP传输实体就创建一条连接记录,并发送一个SYN=1的报文段,而进入SYN_SENT状态。应注意,一台机器上可以有多个进程同时打开多条连接,而状态是针对每一条连接的,每条连接的状态都被记录在相应的连接记录当中。当收到SYN+ACK时,TCP发送三次握手中的最后一个ACK,进入连接建立状态ESTABLISHED,便可进行发送和接收数据操作。

当应用进程结束数据传输时,可释放已建立的连接。此时运行客户进程的主机的本地TCP传输实体发送一个FIN=1的报文段,并等待确认ACK的到来。此时的状态变为FIN_WAIT_1(在表示主动关闭的左边虚线框内)。当收到ACK时,则一个方向的连接现已被关闭,状态变为FIN_WAIT_2。

当运行客户进程的主机收到运行服务器进程的主机发送的FIN=1的报文后,并回答确认ACK,此时另一方向的连接也被关闭,即双方都已关闭了连接。但是,TCP要等待一段时间(可取报文段在网络中的生存时间的两倍),确保该连接所有报文段都已经消失,TCP才删除该连接的记录,返回到起始点CLOSED状态。

现在再从服务器进程的角度,沿着粗虚线来分析状态变迁的情况。服务器进程执行被动打开,进入“监听”状态LISTEN。当收到SYN=1的连接请求报文段后,即发送一个SYN=1的确认报文段ACK,服务器进入SYN_RCVD状态。当收到三次握手中的最后一个确认ACK时,服务器进程就进入连接建立状态ESTABLISHED,以后双方便可开始数据传送操作。

当客户进程的数据已经传送完毕,就发送FIN=1的报文段给服务器进程(在表示被动关闭的右边虚线框内),此时的状态变为CLOSED_WAIT。然后,服务器进程发送FIN=1的报文段给客户进程,状态变为LAST_ACK。当收到客户进程的确认ACK时,服务器就释放该连接,随即删除该连接的记录,重新返回到起始状态CLOSED。

在状态变迁图中还有一些其他的状态变迁,这里不再作更多的解释,读者可自行思考分析。