计算机网络原理:运输层

todo

一、运输层协议概述

1.1、进程之间的通信

从通信和信息处理的角度看,运输层(也翻译为传输层)向它上面的应用层提供端到端通信服务,它属于面向通信部分的最高层,同时也是用户功能中的最低层。当位于网络边缘部分的两台主机使用网络核心部分的功能进行端到端的通信时,只有主机才使用协议栈中的运输层,而网络核心部分的路由器在转发分组时都只用到下三层的功能。

接下来,我们通过下图,说明运输层的作用。

img

既然IP能够将源主机发送出的分组按照首部中的目的地址送交目的主机,那么,为什么还需要再设置一个运输层呢?

从IP层来说,通信的两端是两个主机。IP数据报的首部明确地标志了这两个主机的IP地址。然而,严格地讲,两个主机进行通信实际上是两个主机中的应用进程互相通信。IP虽然能把分组送到目的主机,但是这个分组还停留在主机的网络层而没有交付主机中的应用进程。从运输层的角度看,通信的真正点并不是主机而是主机中的进程。因此,从运输层来看,端到端的通信是应用进程之间的通信。

在一个主机中,经常有多个应用进程同时分别和多个其他主机中的应用进程通信。例如,某用户在使用浏览器查找某网站的信息时,其主机的应用层运行浏览器客户进程。如果用户在浏览网页的同时,还要用电子邮件给网站发送反馈意见,那么主机的应用层就还要运行电子邮件的客户进程。

上图中,主机A的应用进程AP1和主机B的应用进程AP3通信,而与此同时,应用进程AP2也和对方的应用进程AP4通信。因此,运输层的一个很重要的功能就是复用(Multiplexing)和分用(Demultiplexing)。这里的“复用”是指发送方不同的应用进程可以使用同一个运输层协议传送数据(当然需要加上适当的首部),而“分用”是指接收方的运输层在剥去报文的首部后能够把这些数据正确交付目的应用进程。

上图中,两个运输层之间有一个双向粗箭头,写明“运输层提供应用进程间的逻辑通信”。“逻辑通信”的意思是,运输层之间的通信好像是水平传送数据,但事实上这两个运输层之间并没有水平方向的物理连接,要传送的数据是沿着图中的虚线上下多次传送的。

从这里可以看出,网络层和运输层有很大的区别。网络层是为主机之间提供逻辑通信的,而运输层是为应用进程提供端到端的逻辑通信,如下图所示。

img

1.2、互联网的运输层协议

互联网的网络层为主机之间提供的逻辑通信服务是一种尽最大努力交付的数据报服务。也就是说,IP报文在传送过程中有可能出错、丢失或失序。对于电子邮件、文件传输、万维网以及电子银行等很多应用,数据丢失可能会造成灾难性的后果。因此,运输层需要为这类应用提供可靠的数据传输服务。但实时的多媒体应用,如实时音频/视频,则能够承受一定程度的数据丢失。在这些多媒体应用中,丢失少量的数据会对播放的质量产生一些小的影响,但不会造成致命的损伤。为实现可靠数据传输,运输层协议必须增加很多复杂的机制(我们很快就会学到),而这些机制非但不能为这些多媒体应用带来明显的好处,还会带来一些不利因素。总之,单一的运输层服务很难满足所有应用的需求。

根据应用需求的不同,互联网(更准确地说是TCP/IP网络)的运输层为应用层提供了两种不同的运输协议,即面向连接的传输控制协议
(Transmission Control Protocol,TCP)和无连接的用户数据报协议(User Datagram Protocol,UDP)。

img

按照OSI的术语,两个对等运输实体在通信时传送的数据单位叫作运输协议数据单元(Transport Protocol Data Unit,TPDU)。但在互联网中,根据所使用的协议是TCP还是UDP,分别称之为TCP报文段(Segment)和UDP报文(用户数据报)。

UDP在传送数据之前不需要先建立连接。接收方运输层在收到UDP报文后,不需要做出任何确认。虽然UDP不提供可靠交付,但在某些情况下UDP却是一种最有效的工作方式。

TCP则提供面向连接的服务,在传送数据之前必须先建立连接,数据传送结束后要释放连接。TCP不提供广播或多播服务。由于TCP要提供可靠的、面向连接的运输服务,因此不可避免地增加了许多开销,如确认、流量控制、计时器及连接管理等。这不仅使运输协议数据单元的首部增大很多,还要占用许多的处理机资源。

如下图,给出了一些应用和应用层协议主要使用的运输层协议(UDP或TCP)。

img

1.3、运输层的复用与分用

在日常生活中有很多复用和分用的例子。假定一个机关的所有部门向外单位发出的公文都由收发室负责寄出,这相当于各部门都“复用”这个收发室。当收发室收到从外单位寄来的公文时,则要完成“分用”功能,即按照信封上写明的本机关的部门地址正确交付公文。

运输层的复用和分用功能与之类似。应用层所有的应用进程都可以通过运输层再传送到IP层,这就是复用。运输层从IP层收到数据后必须交付指定的应用进程,这就是分用。TCP/IP网络利用软件端口(Port)来实现复用和分用。

端口是应用层与运输层之间接口的抽象,如下图所示。

img

使用运输层协议进行通信的进程,都需要与某个端口关联,端口号标识了应用进程所关联的端口,相当于应用进程的运输层地址。为此,运输协议数据单元(即TCP报文段或UDP报文)的首部必须包含两个字段:源端口号和目的端口号。运输层收到IP层交上来的数据时,要根据其目的端口号来决定应当通过哪一个端口将其上交给目的应用进程。上图中,应用层和运输层之间的小方框就代表端口。

端口的具体实现方法可能有很大的差别,因为这取决于计算机的操作系统。应用层的源进程将数据发送给运输层的某个端口,而应用层的目的进程从端口接收数据。端口用一个16位端口号标识,但端口号只具有本地意义。在互联网上的不同计算机中,相同的端口号是没有联系的,并且TCP和UDP端口号之间也没有必然联系。IP根据IP数据报中的协议字段定位要交付的运输层协议,而相应的运输层协议需要根据运输协议数据单元中的目的端口号来找到要交付的应用进程。16位的端口号共有65535个,这个数目对一台计算机来说是足够用的。

由此可见,两台计算机中的进程要互相通信,不仅要知道对方的IP地址(为了找到对方的计算机),而且要知道对方的端口号(为了找到对方计算机中的应用进程)。应用进程间的通信采用的是客户-服务器通信模式,在应用层中的各种不同的服务器进程不断地监听它们的端口,以便发现是否有某个客户进程要和它通信。客户在发起通信请求时,必须先知道对方服务器的IP地址和端口号,而服务器总是可以从接收到的报文中获得客户的IP地址和端口号。为此,运输层的端口号共分为下面的3类。

  • 熟知端口号(Well-Known Port number),其数值为0~1023。这一类端口号由IANA负责分配给一些常用的应用层程序固定使用,因而所有用户进程都知道。一种新的应用程序出现时若要获得一个熟知端口号,必须向IANA申请。举例如下。

img

  • 登记端口号,其数值为1024~49151。这类端口号IANA不分配也不控制,但可以在IANA注册登记,以防止重复使用。

  • 动态端口号,其数值为49152~65535。这类端口号留给客户进程选作临时端口号。客户进程在发起通信前要先为自己选择一个未用的临时端口号,通信结束后要释放该端口号以便其他客户进程使用。

二、用户数据报协议(UDP)

2.1、UDP概述

用户数据报协议(UDP),在IP的数据报服务上只增加了有限的功能:端口的功能(有了端口,运输层就能进行复用和分用)和差错检测的功能。虽然UDP用户数据报只能提供不可靠的交付,但UDP在某些方面有其特殊的优点。

  • UDP是无连接的,即发送数据之前不需要建立连接(发送数据结束后当然也没有连接可释放),因此减少了开销和发送数据之前的时延。

  • UDP使用尽最大努力交付,即不保证可靠交付,同时也不使用流量控制和拥塞控制,因此主机不需要维持具有许多参数的、复杂的连接状态表。

  • UDP能满足某些应用对实时性的要求。由于UDP没有拥塞控制,因此网络出现的拥塞不会使源主机的发送速率降低。这对某些实时应用是很重要的。很多实时应用(如IP电话、实时视频会议等)要求源主机以恒定的速率发送数据,并且允许在网络发生拥塞时丢失一些数据,但不允许数据有太大的时延。UDP正好适合这种要求。

  • UDP是面向报文的。UDP不会将应用程序交下来的报文划分为若干个分组来发送,也不把收到的若干个报文合并后再交付应用程序。应用程序交给UDP一个报文,UDP就发送这个报文;而UDP收到一个报文,就把它交付应用程序。因此,应用程序必须选择合适大小的报文。若报文太长,UDP把它交给IP层后,IP层在传送时可能要进行分片,这会降低IP层的效率。反之,若报文太短,UDP把它交给IP层后,会使IP数据报的首部相对太大,这也降低了IP层的效率。

  • UDP支持一对一、一对多、多对一和多对多的交互通信。

  • UDP只有8字节的首部开销,比TCP的首部(20字节)要短得多。

虽然某些实时应用需要使用没有拥塞控制的UDP,但当很多源主机同时向网络发送高速率的实时视频流时,网络就有可能发生拥塞,结果大家都无法正常接收。因此UDP不使用拥塞控制功能可能会引起严重的网络拥塞。还有一些使用UDP的实时应用需要对UDP的不可靠的传输进行适当改进,以减少数据的丢失。在这种情况下,应用进程本身可在不影响应用的实时性的前提下增加一些提高可靠性的措施,如采用前向纠错或重传已丢失的报文。

UDP的多路分用模型如下图所示。

img

一个UDP端口与一个报文队列(缓存)关联,UDP根据目的端口号将到达的报文加到对应的队列中。应用进程根据需要从端口对应的队列中读取整个报文。由于UDP没有流量控制功能,如果报文到达的速度长期大于应用进程从队列中读取报文的速度,则会导致队列溢出和报文丢失。这里需要注意的是,端口队列中的所有报文的目的IP地址和目的端口号相同,但源IP地址和源端口号并不一定相同,即不同源而同一目的地的报文会定位到同一队列。

接收方UDP若发现收到的报文中的目的端口号不正确(即不存在对应于该端口号的应用进程),就丢弃该报文,并由网际控制报文协议(ICMP)发送一个终点不可达报文给发送方。

2.2、UDP报文的首部格式

UDP报文由两个部分组成:首部和数据部分。其首部很简单,只有8字节,由四个字段组成,每个字段都是2字节,其示意图如下。

img

各字段意义如下。

  • 源端口:源进程使用的端口号。

  • 目的端口:目的进程使用的端口号。

  • 长度:UDP报文的长度,其最小值是8(仅有首部)。

  • 检验和:差错检验码,防止UDP报文在传输中出错。

UDP报文首部中最重要的字段就是源端口和目的端口,它们用来标识UDP发送方和接收方。实际上,UDP通过二元组(目的IP地址,目的端口号)来定位一个接收方应用进程,而用二元组(源IP地址,源端口号)来标识一个发送方进程。二元组(IP地址,端口号)被称为套接字(Socket)地址。

UDP报文首部中检验和的计算方法有些特殊。在计算检验和时,要在UDP用户数据报之前增加12字节的伪首部。称其为“伪首部”是因为它并不是UDP用户数据报真正的首部,只是在计算检验和时,临时和UDP报文连接在一起,得到一个临时的UDP报文。检验和就是根据这个临时的UDP报文计算出来的。伪首部既不向下传送也不向上递交,仅为计算检验和而存在,防止报文被意外地交付到错误的目的地。上图的最上面给出了伪首部各字段的内容。伪首部第一字段是源IP地址,第一字段是目的IP地址,第三字段是全零,第四字段是IP首部中协议字段的值(对于UDP,此协议字段值为17),第五字段是UDP报文的长度。

UDP的检验和是将首部和数据部分一起检验。发送方先把全零放入检验和字段,再把由伪首部及UDP报文组成的临时UDP报文看成是许多串接起来的16位的字。若UDP报文的数据部分不是偶数个字节,则要填入一个全零字节(但此字节不发送)。然后按二进制反码计算出这些16位字的和。将此和的二进制反码写入检验和字段后,将UDP报文发送出去。接收方把收到的UDP报文和伪首部(以及可能的填充全零字节)放在一起,按二进制反码求这些16位字的和。当无差错时其结果应为全1,否则就表明有差错出现,接收方就应丢弃这个UDP报文(也可以上交应用层,附上出现了差错的警告)。这种简单的差错检验方法的检错能力并不强,但它的好处是简单,处理起来较快。

下图给出了一个计算UDP检验和的例子。这里假定用户数据报的长度是15字节,因此要添加一个全0的字节。

img

三、传输控制协议(TCP)

3.1、TCP概述

TCP是TCP/IP体系中面向连接的运输层协议,它提供全双工的和可靠交付的服务。TCP与UDP最大的区别就是,TCP是面向连接的,而UDP是无连接的。TCP比UDP要复杂得多,除了具有面向连接和可靠传输的特性外,TCP还在运输层使用了流量控制和拥塞控制机制。

TCP是TCP/IP体系中非常复杂的一个协议。下面介绍TCP最主要的几个特点。

  • TCP是面向连接的运输层协议。应用程序在使用TCP提供的服务传送数据之前,必须先建立TCP连接。建立连接的目的是让通信双方为接下来的数据传送做好准备,初始化各种状态变量,分配缓存等资源。在传送数据完毕后,必须释放已建立的TCP连接,即释放相应的资源和变量。这个过程与打电话类似:通话前要先拨号建立连接,通话结束后要挂机释放连接。
  • 每一条TCP连接只能有两个端点,即每一条TCP连接只能是点对点的(一对一)。TCP连接唯一地被通信两端的端点所确定,每个端点由二元组(IP地址,端口号)唯一标识,即一条TCP连接由两个套接字(Socket)地址标识。
  • TCP提供可靠交付的服务。通过TCP连接传送的数据可以无差错、不丢失、不重复,并且按序到达。
  • TCP提供全双工通信。TCP允许通信双方的应用进程在任何时候发送数据。TCP连接的两端都设有发送缓存和接收缓存,用来临时存放双向通信的数据。在发送时,应用程序在把数据传送给TCP的缓存后,就可以做自己的事,而TCP在合适的时候把数据发送出去。在接收时,TCP把收到的数据放入缓存,上层的应用进程在合适的时候读取缓存中的数据。
  • 面向字节流。TCP中的“流”(Stream)指的是流入进程或从进程流出的字节序列。“面向字节流”的含义是,虽然应用程序和TCP的交互是一次一个数据块(大小不等),但TCP把应用程序交下来的数据看成一连串的无结构的字节流。TCP不保证接收方应用程序所收到的数据块和发送方应用程序所发出的数据块具有对应大小。例如,发送方应用程序交给发送方的TCP共10个数据块,而接收方的应用程序是分4次(即4个数据块)从TCP接收方缓存中将数据读取完毕。但接收方应用程序收到的字节流必须和发送方应用程序发出的字节流完全一样。

下图是TCP发送报文段的示意图。为了突出示意图的要点,这里只画出了一个方向的字节流。实际上,只要建立了TCP连接,就能支持同时双向通信的字节流,并且一个TCP报文段可能包含上千字节。还需要注意的是,图中的TCP连接是一条虚连接,而不是一条物理连接。也就是说,TCP连接是一种抽象的逻辑连接。

img

发送方的应用进程按照自己产生数据的规律,不断地把数据块(其长短可能各异)陆续写入TCP的发送缓存。TCP再从发送缓存中取出一定数量的数据,将其组成TCP报文段(Segment)逐个传送给IP层,然后发送出去。图中表示的是在TCP连接上传送一个个TCP报文段,而没有画出IP层或链路层的动作。接收方从IP层收到TCP报文段后,先把它暂存在接收缓存中,然后等待接收方的应用进程从接收缓存中将数据按顺序读取。需要注意的是,接收方应用进程每次从接收缓存中读取数据时,是按应用进程指定的数量读取,而不是一次读取接收缓存中的一个完整的报文段或所有数据。只有当接收缓存中的数据量小于应用进程指定的读取量时,才读取接收缓存中所有的数据。当接收缓存中完全没有数据时,根据读取方式的不同,应用进程可能会一直等待,也可能直接返回。由此可见,TCP的接收方应用进程读取的数据块的边界与发送方应用进程发送的数据块边界毫无关系,也就是说TCP接收方在向上层交付数据时不保证能保持发送方应用进程发送的数据块边界。

TCP报文段首先要传送到IP层,加上IP首部后,再传送到数据链路层,再加上数据链路层的首部和尾部后,才离开主机发送到物理链路。另外,TCP连接仅存在于两个端系统中,而网络核心的中间设备(路由器、交换机等)完全不知道该连接的存在。TCP连接主要包括通信两端主机上的缓存、状态变量。在这两台主机间的路由器和交换机没有为该连接分配任何缓存和变量。

与UDP的端口队列不同的是,TCP的发送缓存和接收缓存都是分配给一个连接的,而不是分配给一个端口的。TCP的一个连接由四元组(源IP地址,源端口号,目的IP地址,目的端口号)标识,即由源/目的套接字地址对标识。也就是说,来自不同源的TCP报文段,即使它们的目的IP地址和目的端口号相同,它们也不可能被交付到同一个TCP接收缓存中,因为它们在不同的TCP“管道”中传输,到达不同“管道”出口的缓存。通常一个TCP服务器进程用一个端口号与不同的客户进程建立多个连接,然后创建多个子进程分别用这些连接与各自的客户进程进行通信。

3.2、TCP报文段的首部格式

TCP虽然是面向字节流的,但TCP传送的数据单元却是报文段。TCP报文段分为首部和数据部分,而TCP的全部功能都体现在它首部中各字段的作用。因此,只有弄清TCP首部各字段的作用才能掌握TCP的工作原理。下图是TCP报文段及其首部的格式。

img

TCP报文段首部的前20字节是固定的,后面4N字节是根据需要而增加的选项(N必须是整数)。因此TCP首部的最小长度是20字节。首部固定部分各字段的意义如下。

  • 源端口和目的端口:各占2字节。与UDP用户数据报一样,该字段定义了在主机中发送和接收该报文段的应用程序的端口号,用于运输层的复用和分用。

  • 序号:占4字节。序号从0开始,到232-1为止,共232(即4 294 967 296)个序号。TCP是面向数据流的。TCP传送的报文段可看成连续的数据流。在一个TCP连接中传送的数据流中的每一个字节都按顺序编号。整个数据的起始序号在连接建立时设置。首部中的序号字段的值则指的是本报文段所发送的数据的第一个字节的序号。例如,一报文段的序号字段值是301,而携带的数据共有100字节。这就表明:本报文段的数据的第一个字节的序号是301,最后一个字节的序号是400。显然,下一个报文段的数据字节序号应当从401开始,因而下一个报文段的序号字段值应为401。

    注:序号字段有32位,可对4 GB的数据进行编号。这可保证在大多数情况下,当序号重复使用时,旧序号的数据早已通过网络到达终点了。

  • 确认号:占4字节,指期望收到的对方下一个报文段的第一个数据字节的序号。TCP提供的是双向通信,一端在发送数据的同时对接收到的对端数据进行确认。例如,B正确收到了A发送过来的一个报文段,其序号字段值是501,而数据长度是200字节,这表明B正确收到了A发送的序号在501~700的数据。因此,B期望收到A的下一个数据字节序号是701,于是B在发送给A的报文段中把确认号置为701,表示对第701个字节之前(不包括第701个字节)的所有字节的确认。TCP采用的是累积确认。

  • 数据偏移:占4位,它指出TCP报文段的数据部分起始处距离TCP报文段的起始处有多远。这实际上就是TCP报文段首部的长度。由于首部长度不固定(因首部中还有长度不确定的选项字段),因此数据偏移字段是必要的。但应注意,“数据偏移”的单位不是字节而是32位字(即以4字节长的字为计算单位)。由于4位二进制数能够表示的最大十进制数是15,因此数据偏移的最大值是60字节,这也是TCP首部的最大长度。

  • 保留:占6位,保留为今后使用,但目前应置为0。

  • 6个标志位标:占6位,说明本报文段的性质。

    • URG (URGent):URG=1表明紧急指针字段有效。它告诉接收方TCP此报文段中有紧急数据,应尽快交付应用程序(相当于高优先级的数据),而不要按序从接收缓存中读取。例如,已经发送了很长的一个程序,要在远地的主机上运行,但后来发现了一些问题,需要取消该程序的运行,因此用户从键盘发出中断命令(Control+C)。如果不作为紧急数据,那么这两个字符将存储在TCP接收缓存的末尾,只有在所有的数据被处理完毕后,这两个字符才被交付接收应用进程。这样做就浪费了许多时间。

      当URG位置1时,相当于告诉发送TCP这两个字符是紧急数据。于是发送TCP就将这两个字符插到报文段的数据部分的最前面,其余的数据都是普通数据。这时URG位要与首部中紧急指针
      (Urgent Pointer)字段配合使用。紧急指针指出在本报文段中的紧急数据共有多少字节。紧急数据到达接收方后,当所有紧急数据都被处理完时,TCP就告诉应用程序恢复正常操作。值得注意的是,即使窗口为零时也可发送紧急数据。URG位在现实中很少被使用。

    • ACK:只有当ACK=1时确认号字段才有效;当ACK=0时,确认号字段无效。

    • PSH(PuSH):出于效率的考虑,TCP可能会延迟发送数据或向应用程序延迟交付数据,这样可以一次处理更多的数据。但是当两个应用进程进行交互式通信时,有时一端的应用进程希望在键入一个命令后立即收到对方的响应。在这种情况下,应用程序可以通知TCP使用推送(push)操作。这时,发送方TCP把PSH位置1,并立即创建一个报文段发送出去,而不需要积累足够多的数据再发送。接收TCP收到PSH位置1的报文段,会尽快交付接收应用进程,而不再等到接收到足够多的数据才向上交付。

      虽然应用程序可以选择推送操作,但现在多数TCP实现都是根据情况自动设置PUSH标志,而不是交由应用程序去处理。

    • RST(ReSeT):RST=1表明TCP连接中出现严重差错(由于主机崩溃或其他原因),必须释放连接,然后重新建立运输连接。RST位置1还用来拒绝一个非法的报文段或拒绝打开一个连接。RST位也可称为重建位或重置位。

    • 同步SYN:用来建立一个连接。SYN=1而ACK=0,表明这是一个连接请求报文段。对方若同意建立连接,则应在响应的报文段中使SYN=1和ACK=1。因此,SYN位置为1就表示这是一个连接请求报文或连接接受报文。

    • 终止FIN(FINal):用来释放一个连接。FIN=1表明此报文段的发送方的数据已发送完毕,要求释放运输连接。

  • 窗口 :占2字节。窗口值指示该报文段发送方的接收窗口 大小,为0到216 -1,用来控制对方 发送的数据量(从确认号开始,允许对方发送的数据量),单位为字节 。窗口字段反映了接收方接收缓存的可用空间大小,计算机网络经常用接收方的接收能力的大小来控制发送方的数据发送量 。例如,设确认号是701,窗口值是1000,这表明允许对方发送数据的序号范围为701~1700。

  • 检验和: 占2字节。检验和字段检验的范围包括首部和数据部分。和UDP用户数据报一样,在计算检验和时,要在TCP报文段的前面加上12字节的伪首部。伪首部的格式与UDP用户数据报的伪首部一样,但应将伪首部第四字段中的17改为6(TCP的协议号是6),将第五字段中的UDP长度改为TCP长度。接收方收到此报文段后,仍要加上这个伪首部来计算检验和。

  • 选项: 长度可变。这里我们只介绍一种选项字段,即最大报文段长度 (Maximum Segment Size,MSS)。MSS告诉对方TCP:“我的缓存所能接收的报文段的数据部分 的最大长度是MSS字节。”当没有使用该选项字段时,TCP的首部长度是20字节。

MSS的选择并不简单。若选择较小的MSS,则网络的利用率较低。设想在极端的情况下,当TCP报文段只含有1字节的数据时,在IP层传输的数据报的开销至少有40字节(包括TCP报文段的首部和IP数据报的首部)。这样,对网络的利用率就不会超过1/41。到了数据链路层还要加上一些开销。但反过来,若TCP报文段非常长,那么其在IP层传输时就有可能要分解成多个短数据报片。目的站要将收到的短数据报片装配成原来的TCP报文段。当传输出错时源站还要进行重传。这些也都会使开销增大。一般认为,MSS应尽可能大些,只要在IP层传输时不需要再分片就行。在连接建立的过程中,双方可以将自己能够支持的MSS写入这一字段。在以后的数据传送阶段,MSS取双方提出的较小的那个数值。若主机未填写这项,则MSS的默认值是536。因此,所有在互联网上的主机都应能接受报文段长度是536+20=556(字节)。

四、连接管理

TCP是面向连接的协议。连接的建立和释放是每一次面向连接的通信中必不可少的过程。因此,TCP连接有三个阶段,即连接建立、数据传送和连接释放。建立连接的目的就是为接下来要进行的通信做好充分的准备,其中最重要的就是分配相应的资源。在通信结束之后显然要释放所占用的资源,即释放连接。

注:TCP的连接是运输层连接,只存在于通信的两个端系统中,而网络核心的路由器完全不知道它的存在。

4.1、连接建立

在连接建立过程中,要解决以下三个问题:

  • 使每一方能够确知对方的存在;

  • 允许双方协商一些参数(如最大报文段长度、初始接收窗口大小、初始序号,以及是否使用某些选项等);

  • 对运输实体资源(如缓存大小、计时器、各状态变量及数据结构等)进行分配和初始化。

连接建立过程又称三次握手(Three-Way Handshake)。接下来,我们以下图为例,说明连接建立的过程。

img

TCP的连接建立采用客户-服务器方式。主动发起连接建立的应用进程叫作客户(Client),而被动等待连接建立的应用进程叫作服务器
(Server)。假设主机B中运行TCP的服务器进程,它先发出一个被动打开(Passive Open)命令,准备接受客户进程的连接请求;然后服务器进程就处于“听”(Listen)的状态,不断检测是否有客户进程发起连接请求,如有,即做出响应。假设客户进程运行在主机A中。它先向其TCP发出主动打开(Active Open)命令,表明要与某个IP地址的某个端口建立运输层连接。主机A和主机B建立连接的过程如下:

  • 主机A的TCP向主机B的TCP发出连接请求报文段,其首部中的SYN位应置1,同时选择一个初始序号seq=x。
  • 主机B的TCP收到连接请求报文段后,如同意,则发回连接请求确认信息。在确认报文段中应把SYN位和ACK位都置1,确认号是ack=x+1(表示对主机A发送的同步报文段的确认),同时也为自己选择一个初始序号seq=y。
  • 主机A的TCP收到B接受连接请求的确认信息后,还要向B给出确认信息,其ACK位置1,确认号ack=y+1,而自己的序号seq=x+1。
  • 运行客户进程的主机A的TCP通知上层应用进程,连接已经建立。运行服务器进程的主机B的TCP收到主机A的确认信息后,会通知其上层应用进程,连接已经建立。

TCP的标准规定,SYN=1的报文段(例如,A发送的第一个报文段)不能携带数据,但要消耗掉一个序号。因此A发送的第二个报文段的序号应当是第一个报文段的序号加1(虽然第一个报文段中并没有数据)。这里需要注意的是,A发送的第二个报文段中SYN位是0而不是1,ACK位必须为1。该报文段是对B的同步报文段的确认,但也是一个普通报文段,可携带数据。若该报文段不携带数据,则按照TCP的规定,确认报文段不消耗序号。

为什么要发送第三个报文段呢?

主要是为了防止已失效的连接请求报文段突然又传送到主机B,导致错误产生。

“已失效的连接请求报文段”是这样产生的。考虑这样一种情况:主机A发出连接请求,但因连接请求报文段丢失而未收到确认信息;主机A于是重传一次,然后收到了确认信息,建立了连接;数据传输完毕后,主机A就释放了连接。主机A共发送了两个连接请求报文段,其中第二个到达了主机B。

现假定出现另一种情况,即主机A发出的第一个连接请求报文段并没有丢失,而是在某些网络结点滞留的时间太长,以致延迟到这次的连接释放以后才传送到主机B。本来这是一个已经失效的报文段,但主机B收到此失效的连接请求报文段后,误认为主机A又发出了一次新的连接请求,于是向主机A发出确认报文段,同意建立连接。由于,主机A并没有要求建立连接,因此不会理睬主机B的确认,也不会向主机B发送数据。但主机B以为运输连接就这样建立了,并一直等待主机A发来数据。主机B的许多资源就这样白白浪费了。

采用三次握手的办法可以防止上述现象的发生。例如,在刚才的情况下,主机A不会向主机B的确认发出确认信息,主机B收不到确认信息,连接就建立不起来。

注:TCP连接两个方向数据的初始序号并非固定为1。如果序号总是从1开始,容易导致前后两次不同连接报文段的混淆。例如,前一个连接的报文段在网络中经历了很长的时延,到达终点时本次连接已终止,并且双方已开始新的连接,若该报文段的序号正好落在接收窗口内,则会被当作新连接的数据而被错误接收。由于很多TCP连接的持续时间都不是很长,如果序号总是从1开始,显然出现以上错误的概率会比较大。为此,前后两个连接的初始序号应该有比较大的间隔。另外,考虑到防范“黑客”的恶意攻击,TCP实现通常随机选择初始序号。

4.2、连接释放

在数据传输结束后,通信双方都可以发出释放连接的请求。在连接释放过程中,双方要释放为该连接分配的所有资源。

接下来,我们以下图为例,说明连接释放的过程。

img

设上图中主机A的应用进程先向其TCP发出连接释放请求,并且不再发送数据。TCP通知对方要释放从A到B这个方向的连接,把发往主机B的报文段首部的FIN位置1,其序号seq=u。由于终止报文段要消耗一个序号,因此序号u等于A前面已传送过的数据的最后一个字节的序号加1。

主机B的TCP收到释放连接通知后即发出确认信息,确认号是ack=u+1(表示对主机A发送的终止报文段的确认),而这个报文段自己的序号假定为v(v等于B前面已传送过的数据的最后一个字节的序号加1)。主机B的TCP这时应通知高层应用进程,见图5-17中的箭头❶。这样,从A到B的连接就释放了,连接处于半关闭(Half-Close)状态,相当于主机A向主机B说:“我已经没有数据要发送了。但你如果还发送数据,我仍可以接收。”

此后,主机B不再接收主机A发来的数据。但主机B若还有一些数据要发往主机A,则可以继续发送(这种情况很少)。主机A只要正确收到数据,仍应向主机B发送确认信息。

若主机B不再向主机A发送数据,其应用进程会通知TCP释放连接,见图5-17中的箭头❷。主机B发出的连接释放报文段必须使FIN=1,其序号为w(若在半关闭状态下B没有发送过数据,则w=ν)。

主机A必须对此进行确认,把ACK置1,确认号ack=w+1(表示对主机B发送的终止报文段的确认)。这样才能把从B到A的反方向连接释放掉。但此时,主机A的TCP并不能马上释放整个连接,还要再等待一个超时重传时间才能将整个连接释放。因为A的确认信息有可能丢失,这时B会重传终止报文段。在这段时间内,若A又收到B重传的终止报文段,A需要再次进行确认。收到A的最后确认信息,B才能最终将整个连接释放。若等待的这段时间内A没有收到B的终止报文段,A的TCP则向其应用进程报告,整个连接已经全部释放。

4.3、有限状态机

前面讨论的是,连接建立和释放过程最简单的情况,在实际运行中有各种可能性,因此TCP连接状态的变化是非常复杂的。

为了更清晰地看出TCP连接的各种状态之间的关系,下图给出了TCP的有限状态机。图中每一个白色方框表示TCP可能具有的状态。状态之间的箭头表示可能发生的状态变迁。箭头旁边的字表明是什么原因引起这种变迁,或表明发生状态变迁后又出现什么动作。图中有三种不同的箭头。粗实线箭头表示对应客户进程的正常变迁(即典型变迁)。粗虚线箭头表示对应服务器进程的正常变迁。细实线箭头表示非典型变迁。

img

一个TCP连接有两个端点。TCP的有限状态机同时表示这两个端点的状态。对这一点,在分析状态变迁时应特别注意。

我们从连接还未建立时的关闭(CLOSED)状态开始。图5-18中的状态较多,最好先顺着粗实线箭头看下去,这是从客户进程开始的变迁过程。设一个主机的客户进程发起连接请求(主动打开),这时本地TCP实体创建TCB,发送一个SYN=1的报文,因而进入SYN_SENT状态。应注意的是,可以有好几个连接代表多个进程同时打开,因此状态是针对每一个连接的。当收到来自进程的SYN和ACK时,TCP就发送出三次握手中的最后的一个ACK,接着就进入连接已经建立(ESTABLISHED)状态。这时双方就可以发送和接收数据了。

当应用进程结束数据传送时,就要释放已建立的连接。设运行客户进程的主机的本地TCP实体发送FIN=1的报文,等待着ACK的到达。这时状态变为FIN_WAIT_1(见主动关闭的虚线方框中左上角)。当运行客户进程的主机收到ACK时,一个方向的连接关闭,状态变为FIN_WAIT_2。

当运行客户进程的主机收到运行服务器进程的主机发送的FIN=1的报文后,就发送ACK。这时另一条连接也关闭了。但是TCP还要等待一段时间(此时间取为报文段在网络中的寿命的两倍)才会删除原来建立的连接记录,返回初始的CLOSED状态。这样做是为了保证原来连接上面的所有分组都从网络中消失了。

现在从服务器进程来分析状态图的变迁(图5-18中粗虚线箭头)。服务器进程发起被动打开,进入听(LISTEN)状态。收到SYN=1的连接请求报文后,服务器进程发送ACK,并使报文中的SYN=1,然后进入SYN_RCVD状态。在收到三次握手中的最后一个ACK时,服务器进程转为ESTABLISHED状态,进入数据传送阶段。

客户进程将数据传送完毕后,就发送FIN=1的报文给服务器进程(见被动关闭的虚线方框),进入CLOSE_WAIT状态。服务器进程发送终止报文段给客户进程,状态变为LAST_ACK。当收到客户进程的ACK时,服务器进程就释放连接,删除连接记录,回到CLOSED状态。

还有一些状态变迁,如连接建立过程中的从LISTEN到SYN_SENT和从SYN_SENT到SYN_RCVD。读者可分析在什么情况下会出现这样的变迁(见习题5-22)。

五、可靠传输

互联网的网络层服务是不可靠的,即通过IP传送的数据可能出现差错、丢失、乱序或重复。TCP在IP的不可靠的尽最大努力服务的基础上实现了一种可靠的数据传输服务,保证数据无差错、无丢失、按序和无重复交付。由于在互联网环境中运输层端到端的时延往往是比较大的(相对于分组的发送时延),因此不能采用在无线局域网中所使用的停止等待协议,而是采用传输效率更高的基于流水线方式的滑动窗口协议。

接下来,我们重点讨论TCP是如何实现可靠传输的。

5.1、数据编号与确认

TCP是面向字节的。TCP把应用层交下来的长报文(可能要划分为多个短报文段)看成是一个个字节组成的数据流,并使每一个字节对应一个序号。在连接建立时,双方TCP要各自确定初始序号。TCP每次发送的报文段的首部中的序号字段数值表示该报文段中首部后面的第一个数据字节的序号。

TCP使用的是累积确认,即确认是对所有按序接收到的数据的确认。接收方返回的确认号是已按序收到的数据的最高序号加1,也就是说,确认号表示接收方期望下次收到的数据中的第一个数据字节的序号。例如,已经收到了1~700号、801~1000号和1201~1500号,而701~800号及1001~1200号的数据还没有收到,那么这时发送的确认号应为701。

由于TCP连接能提供全双工通信,因此通信中的每一方都不必专门发送确认报文段,而可以在传送数据时捎带传送确认信息。为此,TCP采用了一种延迟确认的机制,即接收方在正确接收到数据时可能要等待一小段时间(一般不超过0.5s)再发送确认信息。若这段时间内有数据要发送给对方,则可以捎带确认;或者在这段时间内又有数据到达,则可以同时对这两次到达的数据进行累积确认。这样做可以减少发送完全不带数据的确认报文段,提高TCP的传输效率。

TCP在发送一报文段时,同时会在自己的重传队列中存放一份这个报文段的副本。若收到确认,则删除此副本;若在规定时间内没有收到确认,则重传此报文段的副本。TCP的确认并不保证数据已交付给应用进程,而只是表明接收方的TCP已按序正确收到了对方所发送的报文段。

接收方若收到有差错的报文段就丢弃(不发送否认信息);若收到重复的报文段,也要丢弃,但要立即发回确认信息。这一点是非常重要的。若收到的报文段无差错,只是未按序号顺序到达,那么应如何处理?

TCP对此未做明确规定,而是让TCP的实现者自行确定。TCP实现可以将不按序到达的报文段丢弃,但多数TCP实现是先将其暂存于接收缓存内,待所缺序号的报文段收齐后再一起上交应用层。在互联网环境中,封装TCP报文段的IP数据报不一定是按序到达的,将失序的报文段先缓存起来可以避免不必要的重传。注意,不论采用哪种方法,接收方都要立即对已按序接收到的数据进行确认。

虽然每发送一个报文段就设置一个计时器在概念上最为清楚,但大多数TCP实现为了减少计时器开销,每个连接仅使用一个超时计时器。发送报文段时,若超时计时器未启动则启动它。收到确认时,若还有未被确认的报文段,则重启计时器。若超时计时器超时,仅重传最早未被确认的报文段,并重启计时器。

5.2、以字节为单位的滑动窗口

为了提高报文段的传输效率,TCP采用了滑动窗口协议。TCP发送窗口大小的单位是字节,TCP发送方已发送但未被确认的字节数不能超过发送窗口的大小。

TCP中的窗口概念,可通过下图来理解(这里假设发送窗口的大小为400字节)。落在发送窗口内的是允许发送的字节,落在发送窗口外左侧的是已发送并被确认的字节,落在发送窗口外右侧的是还不能发送的字节。收到确认信息后,发送窗口向右滑动,直到发送窗口的左沿正好包含确认号的字节。

img

上图(a)表示发送窗口大小为400字节,初始序号为1,还没有发送任何字节,可以发送序号为1~400的字节。发送方只要收到了对方的确认信息,发送窗口就可前移。TCP发送方要维护一个指针。每发送一个报文段,指针就向前移动一个报文段的长度。当指针移动到发送窗口的右端(即窗口前沿)时,就不能再发送报文段了。

上图(b)表示发送方已发送了400字节的数据,但只收到对前200字节数据的确认信息。由于窗口右移,现在发送方还可以发送200字节(401~600)。

上图(c)表示发送方收到了对方对前400字节数据的确认信息,发送方最多可再发送400字节的数据(401~800)。

这里需要注意的是,在上图中,我们假设发送窗口大小为400字节,并且一直没有改变。但实际上,TCP的发送窗口是会不断变化的。发送窗口大小的初始值在连接建立时由双方商定,但在通信的过程中,TCP的流量控制和拥塞控制会根据情况动态地调整发送窗口大小的上限值(可增大或减小),从而控制发送数据的平均速率。

在前面,我们的讨论过TCP字节流的概念:发送方的应用程序把字节流写入TCP的发送缓存,接收方的应用程序从TCP的接收缓存中读取字节流。接下来,我们进一步讨论窗口和缓存的关系。

如下图所示,左图是发送方维持的发送缓存和发送窗口,右图是接收方维持的接收缓存和接收窗口。这里需要注意的是:缓存空间和序号空间都是有限的,并且都是循环使用的,所以应该把它们画成圆环形,但这里为了方便,把它们画成了长条形,同时也不考虑循环使用缓存空间和序号空间的问题。

img

我们先看一下上图(a)所示的发送方的情况,发送缓存用来暂时存放:

  • 发送方应用程序传送给发送方TCP准备发送的数据;

  • TCP已发送出去但尚未收到确认信息的数据。

发送窗口通常只是发送缓存的一部分。已被确认的数据应当从发送缓存中删除,因此发送缓存和发送窗口的后沿是重合的。发送方应用程序最后写入发送缓存的字节序号减去最后被确认的字节序号,就是还保留在发送缓存中的被写入的字节数。如果发送方应用程序传送给TCP发送方的速度太快,可能会最终导致发送缓存被填满,这时发送方应用程序必须等待,直到有数据从发送缓存中删除。

再看一下上图(b)所示的接收方的情况,接收缓存用来暂时存放:

  • 按序到达的,但尚未被接收方应用程序读取的数据;
  • 未按序到达的,还不能被接收方应用程序读取的数据。

收到的分组如果被检测出有差错,则被丢弃。如果接收方应用程序来不及读取收到的数据,接收缓存最终就会被填满,使接收窗口大小减小到零。反之,如果接收方应用程序能够及时从接收缓存中读取收到的数据,接收窗口就会增大,但最大不超过接收缓存的大小。上图(b)中还指出了下一个期望收到的字节。这个字节序号也就是接收方给发送方的报文段的首部中的确认号。

5.3、超时重传时间的选择

TCP的发送方在规定的时间内没有收到确认信息就要重传已发送的报文段。这种重传的概念很简单,但如何选择超时重传的时间却是TCP中非常重要也较复杂的一个问题。

由于TCP的下层是互联网环境,发送的报文段可能只经过一个高速率的局域网,也可能经过多个低速率的广域网,并且每个IP数据报所选择的路由还可能不同,不同时间网络拥塞情况也有所不同,因此往返时间是在不断变化的。如下图,是数据链路层和运输层的往返时间分布情况。

img

数据链路层往返时间的方差很小,因此将超时重传时间设置为比下图中的T1大一点的值即可。但运输层往返时间的方差很大。如果把超时重传时间设置得太短(如上图中的T2),则很多报文段会过早超时,引起很多不必要的重传,使网络负荷增大。但如果把超时重传时间设置得过长(如上图中的T3),则大量丢失的报文段不能被及时重传,降低了传输效率。因此,选择超时重传时间在数据链路层并不困难,但在运输层却不那么简单。

那么,运输层的超时计时器的超时重传时间究竟应设置为多大呢?

显然超时重传时间应比当前报文段的往返时间(Round-Trip Time,RTT)要长一些。针对互联网环境中端到端的时延动态变化的特点,TCP采用了一种自适应算法。该算法记录一个报文段发出的时间,以及收到相应的确认报文段的时间。这两个时间之差就是报文段的RTT。在互联网中,实际的RTT测量值变化非常大,因此需要用多个RTT测量值的平均值来估计当前报文段的RTT。由于越近的测量值越能反映网络当前的情况,TCP采用指数加权移动平均的算法对RTT测量值进行加权平均,得出报文段的平均往返时间RTTs(即平滑的往返时间,S表示Smoothed)。每测量到一个新的RTT样本,就按下式重新计算一次RTTs:

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

其中,0α<10 \le \alpha \lt 1。若α\alpha接近于0,则新的RTT样本对计算结果的影响不大,即新算出的平均RTTs和原来的值相比变化不大(RTTs值更新较慢)。若α\alpha接近于1,则加权计算的平均RTTs受新的RTT样本的影响较大(RTTs值更新较快)。典型的α\alpha值为1/8。

显然,计时器设置的超时重传时间(Retransmission Time-Out,RTO)应略大于上面得出的平均往返时间RTTs。由于互联网环境下端到端的往返时间的波动比较大,因此在计算RTO时要考虑实际测量值与平均往返时间的偏差。RFC 2988建议使用下式计算RTO:

RTO=RTTs+4RTTDRTO = RTTs + 4 * RTT_{D}

其中,RTTDRTT_{D}RTTSRTT_{S}和新的RTT样本间偏差的加权平均:

新的RTTD=(1β)(旧的RTTD)+βRTTS新的RTT样本新的RTT_{D} = (1-\beta) * (旧的RTT_{D}) + \beta * |RTT_{S} - 新的RTT样本|

这里的β\beta是小于1的系数,它的推荐值为1/4。

实际往返时间的测量比上面的算法还要复杂一些。试看下面的例子。

如下图所示,发送方发送出一个报文段1。设定的超时重传时间到了,还没有收到确认,于是发送方重传此报文段。经过一段时间后,发送方收到了确认报文段。现在的问题:如何判定此确认报文段是对原来的报文段1的确认,还是对重传的报文段2的确认?由于重传的报文段2和原来的报文段1完全一样,因此源站在收到确认信息后,无法做出正确的判断,而正确的判断对确定RTTS的值非常重要。

img

若收到的确认信息是对重传报文段2的确认,却被源站当成是对原来的报文段1的确认,那么这样计算出的RTTs和超时重传时间就会偏大。若收到的确认信息是对原来的报文段1的确认,但被当成是对重传报文段2的确认,则由此计算出的RTTs和超时重传时间都会偏小。这就必然导致更多报文段的重传,有可能使超时重传时间越来越短。因此,卡恩(Karn)提出了一个算法:在计算RTTs时,不用重传报文段的往返时间样本。这样得出的RTTs和超时重传时间就比较准确。

但是,这又引出了新的问题。设想这样的情况:如果报文段的时延突然增大了很多,则在原来得出的超时重传时间内不会收到确认报文段,于是发送方重传报文段;但根据Karn算法,不考虑重传报文段的往返时间样本,这样,超时重传时间就无法更新,必然导致再次超时和再次重传,并且这种状态会一直持续到RTT变小为止。因此,要对Karn算法进行修正。修正方法是:报文段每重传一次,就将RTO增大一些。典型的做法是将RTO增大一倍(注意:并不增大RTTs)。当不再发生报文段的重传时,才根据式(5-1)和式(5-2)计算超时重传时间。实践证明,这种策略较为合理。

5.4、快速重传

超时触发重传存在的一个问题就是超时重传时间可能相对较长。由于无法精确估计实际的往返时间,RTO往往比实际的往返时间大很多。当一个报文段丢失时,发送方需要等待很长时间才能重传丢失的报文段,因而增加了端到端时延。

幸运的是,有时一个报文段的丢失会使发送方连续收到多个重复的确认信息,发送方通过收到多个重复的确认信息可以快速地判断报文段可能已经丢失,而不必等待超时计时器超时。快速重传就是基于该方法对超时触发重传的补充和改进。

接下来,我们以下图为例,说明快速重传的工作原理。

img

假定发送方发送了M1~M5共五个报文段。接收方收到M1后,发出对M1的确认。假定网络拥塞使M2丢失了。接收方后来收到M3,发现其序号不对,但仍收下放在缓存中,同时发出对最近按序接收的M1的确认(注意,不能对M3确认,因为TCP是累积确认,对M3确认就表示M2也已经收到了)。因为TCP不使用否定确认,所以接收方收到失序报文段时,不能向发送方发回显式的否定确认,而只需对按序接收到的最后一个字节数据进行重复确认。接收方收到M4和M5后,也还要分别发出对M1的重复确认。这样,当发送方一连收到三个重复的确认信息后,就知道可能是网络出现了拥塞,造成分组丢失,或是报文段M2虽未丢失但目前正滞留在网络中的某处,可能还要经过较长的时延才能到达接收方。快速重传算法规定,发送方只要一连收到三个重复的确认信息,就立即重传丢失的报文段M2(注意:重复确认的确认号正是要重传的报文段的序号),而不必继续等待为M2设置的超时计时器超时。

不难看出,快速重传并非取消超时计时器,而是尽早重传丢失的报文段。

5.5、选择确认

TCP报文段的确认字段表示累积确认,即它只通告已收到的最后一个按序到达的字节,而不通告所有已收到的失序到达的字节,虽然这些字节已经被接收方接收并暂存在接收缓存中。这些没有被确认的字节很可能因为超时而被发送方重传。一个可选的功能——选择确认
(Selective ACK,SACK)(RFC 2018)可以用来解决这个问题。选择确认允许接收方通知发送方所有正确接收了但失序的字节块,发送方可以根据这些信息只重传那些接收方还没有收到的字节块。

接下来,我们以下图为例,说明选择确认的工作原理。

img

当接收方TCP接收到失序的字节块时,收到的字节流会形成不连续的字节块。例如在上图中,字节1~1000收到了,字节1001~1500还没有收到,但接下来的字节1501~2000和字节2501~4000已经收到了,而中间的字节2001~2500也没有收到。

接收方要将这些接收到的失序字节块告知对方,只使用一个确认号是办不到的。从上图可以看出,每一个字节块需要用两个边界序号来表示。例如,第一个失序的字节块的左边界L1为1501,右边界R1为2001。这里有两个失序字节块,因此需要用四个边界序号来表示。

但我们知道,TCP报文段的固定首部中没有哪个字段能提供上述这些字节块的边界信息,因此TCP在首部中提供了一个可变长的“SACK选项字段”来存放这些信息。除此之外,要使用选择确认功能,在建立TCP连接时,双方还要分别在同步报文段和同步+确认报文段的首部中都添加“允许SACK选项字段”,表示都支持选择确认功能。之后,才能在数据传输阶段使用SACK选项字段进行选择确认。

当使用选择确认时,TCP首部中的确认号字段的功能和意义并没有改变,实际上选择确认是对累积确认功能的一种补充,并可以和使用累积确认的超时重传与快速重传机制一起工作。目前多数TCP实现都支持选择确认功能。

六、流量控制

TCP为应用程序提供了流量控制(Flow Control)服务,以解决发送方发送数据太快导致接收方来不及接收,使接收缓存溢出的问题。

一条TCP连接的双方主机都为该连接设置了接收缓存。当该TCP连接接收到按序的字节后,它就将数据放入接收缓存。相关联的应用程序会从该缓存中读取数据,但应用程序不一定能马上将数据取走。事实上,接收方的应用程序也许正忙于其他任务,很长时间后才能去读取该数据。如果应用程序读取数据比较慢,而发送方发送数据很快、很多,则很容易使该连接的接收缓存溢出。

流量控制的基本方法就是接收方根据自己的接收能力控制发送方的发送速率。因此,可以说流量控制是一个速度匹配服务,即发送方的发送速率与接收方应用程序的读速率相匹配。利用滑动窗口机制可以很方便地控制发送方的平均发送速率。TCP采用接收方控制发送方发送窗口大小的方法来实现TCP连接上的流量控制。在TCP报文段首部的窗口字段写入的数值就是当前给对方设置的发送窗口大小的上限。这种由接收方控制发送方的做法,在计算机网络中经常使用。

发送窗口大小在连接建立时由双方商定。但在通信的过程中,接收方会根据接收缓存中可用缓存的大小,随时动态地调整对方的发送窗口大小的上限值(可增大或减小)。为此,TCP接收方要维持一个接收窗口变量,其值不能大于可用接收缓存的大小。在TCP报文段首部的窗口字段写入的数值就是当前接收方的接收窗口大小。TCP发送方的发送窗口的大小必须小于该值。发送窗口的大小还受拥塞窗口的限制,在这里我们只考虑流量控制对发送窗口的影响。

接下来,我们以下图为例,说明流量控制的工作原理。

img

这里只考虑主机A向主机B发送数据。假设在连接建立时,B告诉A:“我的接收窗口大小为400字节(win=400,这里win表示TCP报文段首部中窗口字段的值)。”不过这个报文段在上图中省略了。再假设每一个数据报文段所携带的数据都是100字节长,数据报文段序号的初始值为1(见上图中第一个箭头上的seq=1。图中右边的注释可帮助理解整个过程)。这里需要注意的是,上图中大写的ACK表示首部中的ACK位,小写的ack表示确认号字段的值。B向A发送的三个报文段都标注了ACK=1,只有在ACK位置为1时确认号字段才有意义。

从上图可以看出,主机B进行了三次流量控制。第一次把窗口大小减小为300字节,第二次把窗口大小减为100字节,最后把窗口大小减至0,即不允许对方再发送数据了。在第一次调整接收窗口前,B的应用程序从接收缓存中只读取了100字节,因此接收缓存中还有100字节未被读取,可用缓存为300字节。同理,在第二次调整窗口前,应用程序又读取了100字节,而在第三次调整窗口前应用程序没有读取数据,最后没有可用的接收缓存,发送方不能再发送数据。这种暂停状态将持续到主机B的应用程序再次从接收缓存中读取数据。因为当接收方的接收缓存可用空间大小不再为0时,接收方会主动将更新后的窗口大小发送给发送方。从这里可以看出,接收方的应用程序读取数据非常慢,但由于使用流量控制机制控制了发送方的发送速率,因此保证了接收缓存不会溢出。

但这里还存在一个问题:当接收方的接收缓存可用空间大小不再为0时,接收方向发送方发送的窗口大小更新报文段丢失了会引发什么问题?

如果接收方一直没有数据要发送给发送方,则发送方将会永远等下去。为防止接收方发送给发送方的窗口大小更新报文段丢失而导致死锁状态,实际上,当窗口大小变为0时,发送方如果有数据要发送,则会周期性地(例如,每隔60s)发送只包含1字节数据的窗口探测
(Window Probe)报文段,以强制接收方发回确认信息并通告接收窗口大小。如果这时接收窗口大小非零,接收方则会接收这个字节并对该字节进行确认,否则接收方会丢弃该字节并对以前的数据进行重复确认。

这里还有一个问题:如果接收方应用程序发送数据的速率长时间高于接收方应用程序接收数据的速率,在发送方会出现什么情况呢?

从前面的TCP发送缓存与发送窗口间的关系可以看出,这会导致TCP发送方的缓存被填满。这时发送方应用程序必须等待,直到发送缓存有可用的空间。可见,TCP最终实现了发送方应用程序的发送速率与接收方应用程序的接收速率的匹配。

七、拥塞控制

拥塞(Congestion),是指网络中出现太多的分组,网络性能开始下降。拥塞是分组交换网络中一个非常重要的问题。如果网络中的负载(Load),即发送到网络中的数据量超过了网络的容量,即网络中能处理的数据量,那么网络就有可能发生拥塞。所谓拥塞控制(Congestion Control)就是防止过多的数据注入网络,使网络中的路由器或链路不致过载。

拥塞控制和流量控制是有区别的,因为它们都需要控制源点的发送速率,所以容易混淆。

  • 拥塞控制的任务是防止过多的数据注入网络,使网络能够承受现有的网络负载。这是一个全局性的问题,涉及各方面的行为,包括所有的主机、所有的路由器、路由器内部的存储、转发处理过程,以及与降低网络传输性能有关的所有因素。
  • 流量控制只与特定点对点通信的发送方和接收方之间的流量有关。它的任务是,确保一个快速的发送方不会持续地以超过接收方接收能力的速率发送数据,以防止接收方来不及处理数据。流量控制通常涉及的做法是,接收方向发送方提供某种直接的反馈,以抑制发送方的发送速率。

7.1、拥塞的原因与危害

在介绍拥塞控制的基本方法之前,我们稍微深入地分析一下拥塞产生的原因及拥塞带来的危害。

如下图所示,横坐标是输入负载或网络负载,表示单位时间内输入网络的分组数目。纵坐标是吞吐量(Throughput),表示单位时间内从网络输出的数据量。

img

理想情况下,在吞吐量饱和之前,吞吐量应等于输入负载,故吞吐量曲线是45°的斜线。而当输入负载超过网络容量时(由于网络资源的限制),在理想情况下,吞吐量不再增长而保持为水平线,即吞吐量达到饱和。这就表明输入负载中有一部分损失掉了(例如,输入网络的某些分组被路由器丢弃了)。

但是,在实际的网络中,若不采取有效的拥塞控制手段,随着输入负载的增大,网络吞吐量的增长速率会逐渐减小。特别是当输入负载达到某一数值时,网络的吞吐量反而随输入负载的增大而下降,这时网络就进入了拥塞状态。当输入负载继续增大时,网络的吞吐量甚至有可能下降到零,即网络已无法工作。这就是所谓的死锁(Deadlock)。

为什么输入负载达到某一数值后,网络的吞吐量反而随输入负载的增大而下降呢?

下面用一个简单的例子来说明这个问题。如下图所示,A到B的通信和C到D的通信(假定都使用TCP连接)共享路由器R1和R2之间的链路。不难看出,该网络的最大吞吐量受路由器R1和R2之间的链路容量的制约,即100 Mbit/s。

img

先假定没有拥塞控制,A和C都以链路最高速率100 Mbit/s持续地发送数据。由于A和C速率都是100 Mbit/s,因此A和C在R1和R2之间的共享链路上各自获得50 Mbit/s的带宽。但由于R2到D的链路带宽只有10 Mbit/s,C在路由器R2会损失40 Mbit/s的带宽(C发送的分组在路由器R2排队等候向D转发时被丢弃了)。虽然路由器R2到B的带宽有100 Mbit/s,但A却无法充分使用,只能用50 Mbit/s的速率传送数据。网络所能达到的实际吞吐量只有60 Mbit/s。这就出现了网络拥塞所带来的典型问题,即网络性能变差,资源被浪费。出现该问题的本质原因是,C有大量分组在路由器R2处因网络拥塞被丢弃,这些分组不能到达目的地,却白白占用了其所经过链路的资源。这些无用分组占用了资源,使得A无法使用这些资源,导致资源的浪费。

可见,当网络拥塞而丢弃分组时,该分组在其经过路径中所占用的全部资源(如链路带宽)都被白白浪费掉了。

是不是增加某些资源就能彻底解决该问题呢?例如,增大路由器的输入/输出缓存的大小。

增大路由器缓存大小虽然有助于解决突发数据的问题,但有时会使网络性能变得更差。例如,在上图中,大的缓存会推迟分组的丢弃,但由于A和C持续地发送数据,分组还是会被丢弃。而且这些被丢弃的分组占用资源(路由器缓存)的时间变长了,导致排队时延的增加。更糟糕的是,由于时延的增加,可能部分已正确到达目的地的分组因超时而被发送方重传,这些无用的重传又进一步使拥塞状况恶化。

有人可能想到,如果R2和D间的链路带宽增大到100 Mbit/s,则可以解决该问题。这似乎是找到了解决问题的关键,但实际上的网络比这要复杂得多,更重要的是,无论是网络本身,还是网络的用户,又或是输入网络的流量分布,都是在不断变化的。我们在设计时不可能准确预计所有瓶颈。采取“头痛医头、脚痛医脚”的方法,通常不会有很明显的效果,这样的做法往往只是转移了瓶颈而已。当然,在设计网络时,尽量使资源分布均衡、无明显的瓶颈是有好处的,但不能完全解决网络拥塞问题,因为网络实际的通信情况是永远无法准确预测的。

既然网络拥塞是因为发送到网络中的数据量超过了网络的容量,要彻底解决分组交换网络中的拥塞问题,就要想办法限制输入负载,即控制源点的发送速率。例如,在上图中,若A和C的发送速率分别为90 Mbit/s和10 Mbit/s,该网络的吞吐量就能够达到100Mbit/s(A到B为90 Mbit/s,C到D为10 Mbit/s)。

7.2、拥塞控制的基本方法

从控制论的角度出发,拥塞控制可以分为开环控制和闭环控制两大类。开环控制方法试图用良好的设计来解决问题,它的本质是从一开始就保证问题不会发生。一旦系统启动并运行起来了,就不需要中途做修正。相反,闭环控制是一种基于反馈环路的方法,它包括三个部分:

  • 监测网络系统以便检测到拥塞在何时、何地发生;

  • 把拥塞发生的信息传送到可以采取行动的地方;

  • 调整网络系统的运行以解决出现的问题。

当网络系统的流量特征可以准确规定、性能要求可以事先获得时,适于使用开环控制;而当流量特征不能准确描述或者当系统不提供资源预留时,适于使用闭环控制。由于互联网不提供资源预留机制,而且流量的特性不能准确描述,所以在互联网中拥塞控制主要采用闭环控制方法。

根据拥塞反馈信息的形式,又可以将闭环控制算法分为显式反馈算法和隐式反馈算法。

  • 在显式反馈算法中,拥塞点(即路由器)向源点提供关于网络中拥塞状态的显式反馈信息。例如,源点抑制报文就是一种显式反馈信息。当互联网中一个路由器被大量的IP数据报淹没时,它可能丢弃一些数据报,同时可使用ICMP源点抑制报文通告源主机。源站收到后应该降低发送速率。不过当网络拥塞发生时,向网络注入这些额外的分组可能会“火上浇油”,因此该方法现在已不再使用。现在,互联网中的拥塞控制任务主要是在运输层上完成的。更好的显式反馈信息的方法是,在路由器转发的分组中保留一个比特或字段,用该比特或字段的值表示网络的拥塞状态,而不是专门发送一个分组。

  • 在隐式反馈算法中,源点通过对网络行为的观察(如分组丢失与往返时间)来推断网络是否发生了拥塞,无须拥塞点提供显式反馈信息。TCP采用的就是隐式反馈算法。

需要注意的是,拥塞控制并不仅仅是运输层要考虑的问题。显式反馈算法就涉及网络层。虽然一些网络体系结构(如ATM网络)主要在网络层实现拥塞控制,但互联网主要利用隐式反馈算法在运输层实现拥塞控制。

不论采用哪种方法进行拥塞控制都是需要付出代价的。例如,在实施拥塞控制时,可能需要在结点之间交换信息和各种命令,以便选择控制的策略和实施控制。这样会产生额外的开销。有些拥塞控制机制会预留一些资源给特殊用户或特殊情况,这降低了网络资源的共享程度。因此,当网络输入负载不大时,有拥塞控制的系统吞吐量低于无拥塞控制的系统吞吐量。但付出一定的代价是值得的,它会保证网络性能的稳定,不会因输入负载的增长而导致网络性能的恶化甚至崩溃。

7.3、TCP的拥塞控制

在学习了拥塞控制的一般性原理后,我们再来研究TCP具体的拥塞控制机制。

TCP采用的方法是,让每一个发送方根据所感知到的网络拥塞程度,来限制其向连接发送数据的速率。TCP发送方如果感知从它到目的地的路径上没有拥塞,则提高发送速率(充分利用可用带宽);而如果感知该路径上有拥塞,则降低发送速率。该方法具体要解决以下三个问题:

  • 首先,TCP发送方如何限制自己的发送速率;

  • 其次,TCP发送方如何感知从它到目的地的路径上存在拥塞;

  • 最后,当发送方感知到端到端的拥塞时,采用什么算法改变自己的发送速率。

我们先讨论TCP发送方是如何限制自己的发送速率?

为了进行流量控制,TCP的发送方会维持一个叫作接收方窗口(ReceiverWindow)的状态变量rwnd来记录接收到的TCP报文段首部中窗口字段的值(接收方通告的接收窗口大小),并通过该变量限制发送窗口的大小,实现流量控制。另外,为了进行拥塞控制,TCP的发送方还维持了一个叫作拥塞窗口(CongestionWindow)的状态变量cwnd,其大小取决于网络动态变化的拥塞程度。TCP发送方在确定发送速率时,既要考虑接收方的接收能力,又要从全局考虑不要使网络发生拥塞。因此TCP发送方的发送窗口大小取接收方窗口和拥塞窗口中的较小值,即按以下公式确定:

发送窗口的上限值=Min(rwnd,cwnd)发送窗口的上限值=Min(rwnd,cwnd)

当rwnd < cwnd时,是接收方的接收能力限制发送窗口大小的最大值;当rwnd > cwnd时,则是网络的传输能力限制发送窗口大小的最大值。为了简化问题,这里仅考虑拥塞窗口对发送窗口的限制。

TCP发送方又是如何知道网络发生了拥塞呢?

我们知道,当网络发生拥塞时,路由器就要丢弃分组。现在通信线路的传输质量一般都很好,因传输出差错而丢弃分组的概率是很小的(远小于1%)。因此检测到分组丢失就可以认为网络出现了拥塞。这里我们需要知道的是,发送方不一定要通过超时计时器超时来发现分组的丢失,也可以通过接收到三个重复确认来判断有分组丢失。因此,当超时计时器超时或者接收到三个重复确认时,TCP的发送方就认为网络出现了拥塞。

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

1999年公布的互联网建议标准(RFC 2581)定义了三种算法,即慢启动(Slow-Start)、拥塞避免(Congestion Avoidance)和快速恢复(Fast Recovery)。之后,RFC 2582和RFC3390又对这些算法进行了一些改进。

由于TCP的拥塞控制的具体细节非常复杂,接下来仅介绍这些算法的要点和基本原理。

7.3.1、慢启动

主机刚开始发送数据时,完全不知道网络的拥塞情况,如果立即把较大的发送窗口中的全部数据字节都注入网络,就有可能引起网络拥塞。经验证明,较好的方法是,通过试探发现网络的可用带宽,即由小到大逐渐增大发送方的拥塞窗口数值,直到发生拥塞。

通常,在刚刚开始发送报文段时,可先将拥塞窗口cwnd设置为一个MSS。而在每收到一个对新的报文段的确认后,将拥塞窗口增加至多一个MSS。用这样的方法逐步增大发送方的拥塞窗口cwnd,可以使分组注入网络的速率更加合理。这就是慢启动算法。

接下来,我们以下图为例,说明慢启动算法的原理。为了便于说明,我们用MSS作为窗口大小的单位,并且每个报文段的长度都是一个MSS。此外,还假定接收方窗口rwnd足够大,因此发送窗口只受发送方的拥塞窗口的制约。

img

在一开始,发送方先设置cwnd=1,发送第一个报文段M0,接收方收到后确认M0。发送方收到对M0的确认信息后,把cwnd从1增大到2,于是发送方接着发送M1和M2两个报文段。接收方收到后发回对M1和M2的确认信息。发送方每收到一个对新报文段的确认,就将拥塞窗口加1,因此现在发送方的cwnd又从2增大到4,并可发送M3~M6共4个报文段。

在互联网中,通常发送时延远小于往返时间,因此每经过一个轮次(大约一个RTT),发送方的平均发送速率几乎增加一倍,即随时间大约以指数方式增长。可见慢启动的“慢”并不是指cwnd的增长速率慢,而是指一开始发送速率很慢(cwnd=1)。在不清楚网络实际负载的情况下,这样可以避免新的连接突然向网络注入大量分组而导致网络拥塞。这对防止网络出现拥塞是个非常有力的措施。快速增长发送速率的目的是使发送方能迅速获得合适的发送速率。

7.3.2、拥塞避免

在慢启动阶段发送速率以指数方式迅速增长,若发送速率持续以该速度增长,则网络必然很快进入拥塞状态。因此当网络接近拥塞时,应降低发送速率的增长速率。这可以使TCP连接在一段相对长的时间内保持较高的发送速率但又不致网络拥塞。为此,TCP定义了一个状态变量,即慢启动门限ssthresh(即从慢启动阶段进入拥塞避免阶段的阈值)。慢启动门限ssthresh的用法如下:

  • 当cwnd < ssthresh时,使用慢启动算法;

  • 当cwnd>ssthresh时,停止使用慢启动算法而改用拥塞避免算法;

  • 当cwnd=ssthresh时,既可使用慢启动算法,也可使用拥塞避免算法。

在拥塞避免阶段,发送方的拥塞窗口cwnd每经过大约一个RTT就增加一个MSS。实际做法是,每收到一个新的确认信息,将cwnd(这里以字节为单位)增加MSS×(MSS / cwnd)字节。这样,拥塞窗口cwnd按线性规律缓慢增长,比慢启动算法的拥塞窗口增长速率缓慢得多。

无论是在慢启动阶段还是在拥塞避免阶段,发送方只要发现网络出现拥塞(检测到分组丢失),就立即将拥塞窗口cwnd重新设置为1,并执行慢启动算法。这样做的目的就是迅速减少主机发送到网络中的分组数,使得发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。在重新执行慢启动算法的同时,将慢启动门限ssthresh设置为出现拥塞时的发送窗口大小(即接收方窗口和拥塞窗口中数值较小的一个)的一半(但不能小于2)。这样设置的考虑是,这一次在该窗口大小发生拥塞,则下次很有可能在该窗口大小再出现拥塞,因此当下次拥塞窗口又接近该值时,就要降低窗口的增长速率,进入拥塞避免阶段。

下图,说明了上述拥塞控制的具体过程。

img

当TCP连接进行初始化时,将拥塞窗口置为1。前面已说过,为了便于理解,这里窗口单位不使用字节而使用报文段。假设慢启动门限初始值ssthresh=16。我们知道,发送方的发送窗口大小不能超过拥塞窗口cwnd和接收方窗口rwnd中的较小值。现在假定接收方窗口足够大,因此现在发送窗口大小等于拥塞窗口的数值。

在执行慢启动算法时,拥塞窗口cwnd的初始值为1。以后发送方每收到一个对新报文段的确认,就将发送方的拥塞窗口加1,然后开始下一次的传输(图5-22中的横坐标是传输轮次)。一个“轮次”就是把拥塞窗口cwnd所允许发送的报文段都发送出去,并且都收到了对方的确认信息。“轮次”的间隔时间可以近似为一个RTT。因此,拥塞窗口cwnd随着传输轮次按指数规律增长。当拥塞窗口cwnd增长到慢启动门限值ssthresh时(即当cwnd=16时),就改为执行拥塞避免算法,拥塞窗口按线性规律增长。

假定拥塞窗口的数值增长到24时,网络出现拥塞(分组丢失)。更新后的ssthresh值变为12(即发送窗口大小24的一半),拥塞窗口再重新设置为1,并执行慢启动算法。当cwnd=12时改为执行拥塞避免算法,拥塞窗口按线性规律增长,每经过一个RTT就增加一个MSS。

可见,执行拥塞避免算法后,拥塞窗口呈线性增长,发送速率增长比较缓慢,以防止网络过早出现拥塞,并使发送方可以长时间保持一个合理的发送速率。这里要再强调一下,“拥塞避免”并不能避免拥塞,而是说把拥塞窗口控制为按线性规律增长,使网络不容易立即出现拥塞。

7.3.3、快速恢复

实际上TCP检测到分组丢失有两种情况:超时计时器超时和收到连续三个重复的ACK。上面的拥塞控制算法对这两种情况采取了同样的反应,即将拥塞窗口减小为1,然后执行慢启动算法。但实际上这两种情况下网络拥塞程度是不一样的。

当发送方收到连续三个重复的ACK时,虽然有可能丢失了一些分组,但这连续的三个重复ACK同时又表明丢失分组以外的另外三个分组已经被接收方接收了。因此,与发生超时事件的情况不同,网络还有一定的分组交付能力,拥塞情况并不严重。既然网络拥塞情况并不严重,将拥塞窗口直接减小为1就反应过于剧烈了,这会导致发送方经过很长时间才能恢复正常的传输速率。

为此,RFC 2581定义了与快速重传配套使用的快速恢复算法,其具体步骤如下。

  • 发送方收到连续三个重复的ACK时,就重新设置慢启动门限ssthresh,将其设置为当前发送窗口大小的一半。这一点和慢启动算法是一样的。

  • 与慢启动算法的不同之处是拥塞窗口cwnd不是设置为1,而是设置为新设置的慢启动门限ssthresh,然后开始执行拥塞避免算法,使拥塞窗口缓慢地线性增长。

对于超时事件,由于后续的分组都被丢弃了,一直没有收到它们的确认信息导致超时计时器超时(否则已经执行了快速重传而无须等到超时),显然网络存在严重的拥塞。这种情况下重新执行慢启动算法有助于迅速减少主机发送到网络中的分组数,使发生拥塞的路由器有足够时间把队列中积压的分组处理完毕。

下图,是对接收到3个重复ACK和超时事件的不同处理示意图。

img

7.3.4、加性增和乘性减

在TCP拥塞控制的文献中经常可见“加性增”(Additive Increase)和“乘性减”(Multiplicative Decrease)这样的提法。采用快速恢复算法的情况下,长时间的TCP连接在稳定的时候通常处于下面描述的不断重复状态。经过慢启动,发送方迅速进入拥塞避免阶段,在该阶段使拥塞窗口呈线性增长,即“加性增”,发送速率缓慢增长,以防止网络过早出现拥塞。当流量逐渐超过网络可用带宽时会出现拥塞,但由于发送速率增长缓慢,通常仅导致少量分组丢失。这种情况下发送方会收到三个重复ACK并将拥塞窗口减半,即“乘性减”,然后再继续执行“加性增”缓慢增长发送速率,如此重复下去。因此,对于长时间的TCP连接,稳定时拥塞窗口呈锯齿状变化,如下图所示。在这种“加性增、乘性减”的拥塞控制下,发送方的平均发送速率始终保持在较接近网络可用带宽的位置(慢启动门限之上)。

img

最后要说明的是,拥塞控制仍是计算机网络的一个研究热点,TCP及其拥塞控制算法也还在不断发展和变化。在这里我们仅讨论了目前流行版本的基本原理和要点。

八、参考

计算机网络教程(第6版·微课版)

计算机网络原理创新教程

计算机网络原理 自考04741


计算机网络原理:运输层
https://kuberxy.github.io/2024/11/17/计算机网络原理03:运输层/
作者
Mr.x
发布于
2024年11月17日
许可协议