【2021/1/17修订】【梳理】计算机网络:自顶向下方法 第三章 运输层(docx)

计算机网络

知 识 梳 理

(第一版)

建议先修课程:数据结构。
配套教材:
Computer Networking - A Top Down Approach, 7th edition James F. Kurose, Keith W. Ross
参考书目:
1、计算机网络(第7版) 谢希仁 编著 高等教育出版社


链接:https://pan.baidu.com/s/19VSIzeL10VUMse7-jG5yXA
提取码:0000


三 运输层

3.1 运输层服务
先来回忆一下TCP和UDP。UDP为调用它的应用程序提供了一种不可靠、无连接的服务;而TCP为调用它的应用程序提供了一种可靠的、面向连接的服务。
运输层的数据包(分组)称为运输层报文段,简称报文段(segment)。有的资料可能将TCP和UDP的运输层分组分别称为报文段和数据报(datagram)。但日后将要讲到的网络层的分组也称为数据报。在这里,有时我们将TCP和UDP的分组都称为报文段;有时会说得具体一些,对UDP数据包,也称其为用户数据报。

在第4和第5章将学习网络层,这里只作简要介绍。网络层的协议称为Internet协议(IP),也称网际协议。
前面我们讲过,运输层为进程间的通信提供服务,而网络层负责在主机之间传输分组。IP的服务模型是尽力而为交付服务(best-effort delivery service),这意味着IP虽然尽最大的努力在通信的主机之间交付报文段,但它并不提供任何保证:它不确保报文段的成功交付,也不确保报文段的按序交付,更不保证报文段的数据完整性。因此,IP是不可靠服务(unreliable service)。此外,每台主机至少有一个网络层地址,即IP地址。在本章,我们只需对网络层了解这些。

运输层最近又增加了第三种协议,即流控制传输协议SCTP(Stream Control Transmission Protocol)。它具有TCP和UDP协议的共同优点,可支持一些新的应用,如IP电话。限于篇幅,这里不再介绍。

从通信和信息处理的角度看,运输层向它上面的应用层提供通信服务,它属于面向通信部分的最高层,同时也是用户功能中的最低层。当网络的边缘部分中的两台主机使用网络的核心部分的功能进行端到端通信时,一般只有主机的协议栈才有运输层,而网络核心部分中的路由器在转发分组时都只用到下三层(网络层、链路层和物理层)的功能。
3.2 多路复用与多路分用
有了IP,我们已经可以将分组从一台主机传送到另一台主机了。那么,为什么还需要运输层呢?
从IP层来说,通信的两端是两台主机。IP数据报的报头记录了两台主机的IP地址。但真正进行通信的实体,是两台主机中的正在交换数据(通信)的进程。因此严格地讲,两台主机进行通信,就是两台主机中的应用进程互相通信。IP虽然能把分组送到目标主机,但是这个分组还停留在主机的网络层而没有交给主机中的应用进程。从运输层的角度看,通信的真正端点并不是主机而是主机中的进程。也就是说,端到端的通信是应用进程之间的通信。
总之,网络层和运输层有明显的区别。网络层为主机之间提供逻辑通信,而运输层为应用进程之间提供端到端的逻辑通信。网络层和运输层的任务的不同,在1.5节已经强调过了。

在一台主机中经常有多个应用进程同时分别和另一台主机中的多个应用进程通信。例如,某用户在使用浏览器查找某网站的信息时,其主机的应用层运行浏览器客户进程。如果在浏览网页的同时还发邮件,那么主机的应用层还运行电子邮件的客户进程。这表明运输层有两个很重要的功能:复用和分用。
运输层段具有若干字段。在接收端,运输层从这些字段提取出接收套接字,将报文段定向到该套接字。将运输层段中的数据交付到正确的套接字的工作称为分用(demultiplexing)。在源主机从不同套接字中收集数据块,并为每个数据块封装上首部信息(用于分用)从而生成报文段,然后将报文段传递到网络层,所有这些工作称为复用(multiplexing)。如果不理解,可以想一想数字逻辑电路中的数据选择器(multiplexer)和数据分配器(demultiplexer)。

在目标主机,运输层从紧邻其下的网络层接收报文段,并将这些报文段中的数据交付给在主机上运行的相应进程。
一个进程(作为网络应用的一部分)有一个或多个套接字,它是从网络向进程传递数据和从进程向网络传递数据的闸门。因此,在接收主机中的运输层实际上并没有直接将数据交付给进程,而是将数据交给了位于运输层和应用层之间的套接字。每个套接字都有唯一的标识符。标识符的格式取决于它是UDP还是TCP套接字。

为了正确将数据送到相应的进程,就需要对每台主机的每个进程进行标识。
我们知道,一台计算机中的进程是用一个不大的整数——进程标识符(PID)标识的。但在互联网环境下,用操作系统指派的PID来标识应用层的各种进程则是不行的。这是因为互联网上的计算机的操作系统很多,不同的操作系统往往使用不同格式的PID。为了使运行不同操作系统的计算机的应用进程能够互相通信,就必须用统一的方法(与操作系统无关)对TCP/IP体系的应用进程进行标识。
但是,把一个特定机器上运行的特定进程,指定为互联网通信的最后终点,还是不可行的。这是因为进程的创建和撤销都是动态的,通信的一方难以识别对方机器上的进程。另外,我们往往需要利用目标主机提供的功能来识别终点,而不需要知道具体实现这个功能的进程是哪一个。例如,要和互联网上的某个邮件服务器联系,不一定要知道这个服务器功能是由目标主机上的哪个进程实现的。

解决这个问题的方法就是在运输层使用协议端口号(protocol port number),简称端口(port)。这就是说,虽然通信的终点是应用进程,但只要把所传送的报文交到目标主机的某个合适的目标端口,剩下的工作(即最后交付目的进程)就由TCP或UDP来完成。
运输层段中的特殊字段包括源端口号和目标端口号,它们由发送端的运输层给出。端口号是一个16位的整数,它只有本地的意义。在互联网上不同的计算机中,相同的端口号通常是没有直接关联的。16位的端口号可允许有65535个不同的端口,对一台计算机来说是足够的。
请注意,这种在协议栈层间的抽象的协议端口是软件端口,和路由器或交换机等硬件上的硬件端口是完全不同的概念。硬件端口是不同硬件设备交互的接口,而软件端口是应用层的各种协议进程与运输实体进行层间交互的一种地址。不同的计算机实现端口的方法可以是不同的,取决于操作系统。

运输层端口号分为两类:服务器使用的端口号、客户端使用的端口号。
服务器使用的端口号又分两类:一类是熟知端口号(well-known port number)或系统端口号,范围为0 ~ 1023。0 ~ 1023端口的使用是受限的,因为它们要留给HTTP(端口80)和FTP(端口21)之类的常用应用层协议。开发一个新的需要访问网络的应用程序时,必须为其分配一个端口号。端口号可以自动分配,也可以手动指定。

而另一类叫做登记端口号,数值为1024 ~ 49151。这类端口号是为没有熟知端口号的应用程序使用的。使用这类端口号必须在互联网编配号码管理局(Internet Assigned Numbers Authority,IANA)按照规定的手续登记,以防止重复。
第二类是客户端使用的端口号,范围为49152 ~ 65535(共16384个)。这类端口号仅在客户进程运行时才动态选择,因此又叫做短暂端口号(ephemeral port number)。这类端口号留给客户进程选择使用。当服务器进程收到客户进程的报文时,就知道了客户进程使用的端口号,因而可以把数据发送给客户进程。通信结束后,刚才使用的客户端口号就被释放,这个端口号就可以供其它客户进程使用。
短暂端口表示这种端口的存在时间是短期的。客户进程并不在意操作系统给它分配的是哪一个端口号,因为客户进程之所以必须有一个端口号(在本地主机中必须是唯一的),是为了让运输层的实体能够找到自己。这和熟知端口不同。为了让网上所有的客户程序都能找到服务器程序,服务器程序使用的端口就必须是固定的,并且是众所周知的。

一个UDP套接字是由一个二元组标识的。二元组的元素有:目标IP地址和目标端口号。如果两个UDP报文段有不同的源IP地址和 / 或源端口号,但具有相同的目标IP地址和目标端口号,那么这两个报文段将通过相同的目标套接字被定向到相同的目标进程。
与UDP套接字不同,TCP套接字是由一个四元组 <源IP地址,源端口号,目标IP地址,目标端口号> 来标识的。因此,当TCP报文段到达主机时,该主机使用全部4个值来将报文段传递给相应的套接字。同一主机接收到的两个具有不同源IP地址或源端口号的TCP报文段将被定向到两个不同的套接字,除非TCP报文段携带了初始连接创建请求。

一个服务器进程会打开一些端口等待客户的请求。某些端口为常用应用(例如Web、FTP、DNS和SMTP服务器)所预留;按照惯例,还有其它端口由热门的应用程序(例如SQL Server 2000在UDP端口1434上监听请求)使用。因此,如果我们确定一台主机上打开了一个端口,就能确定该主机正在运行一个特定的应用程序。这对于系统管理员非常有用,管理员通常希望知晓有哪些网络应用程序运行在他们的网络主机上。攻击者为了寻找突破口,也要知道在目标主机上有哪些端口打开。如果发现一台主机正在运行具有已知漏洞的应用程序(例如,在端口1434上监听的SQL Server不免疫缓冲区溢出攻击,使远程用户能执行任意代码,这是一种由Slammer蠕虫所利用的缺陷),那么攻击该主机的条件就已经成熟。
确定哪个应用程序正在监听哪些端口很容易。有许多公共域程序(称为端口扫描器)做的正是这种事。使用最广泛的工具之一是nmap,可在http://nmap.org上免费获取,并且它已集成在大多数Linux发行版中。对于TCP,nmap顺序地扫描端口,寻找能够接受TCP连接的端口。对于UDP,nmap也顺序地扫描端口,寻找对传输的UDP报文段进行响应的UDP端口。nmap返回打开的、关闭的或不可达的端口列表。运行nmap的主机能够尝试扫描Internet中任何地方的主机。

不同的主机的相同的源端口与同一服务器的相同的端口同时进行TCP通信,并不会引起问题。在没有IP冲突的情况下,不同的主机具有不同的IP地址,服务器仍然可以区分不同的会话。

现在,套接字与进程之间并非总是一一对应。当今的高性能Web服务器提供Web服务时通常只使用一个进程,但为每个新的客户连接创建一个具有新的连接套接字的新线程。于是,任意时刻都可能有(不同标识的)许多套接字连接同一进程。
如果客户与服务器使用持久HTTP,则在连接持续期间,客户与服务器之间经由同一个服务器套接字交换HTTP报文。然而,如果客户与服务器使用非持久HTTP,则对每一对请求 / 响应都创建一个新的TCP连接并在随后关闭,因此对每一对请求 / 响应也会创建一个新的套接字并在随后关闭。套接字的频繁创建和关闭会严重降低Web服务器的性能,尽管有许多操作系统层面的技巧可以用来减轻影响。
3.3 无连接运输:UDP
UDP只是做了运输协议能够做的最少工作。除了复用、分用功能及少量的差错检测外,它几乎没有为IP带来别的东西。如果开发人员选择UDP而不是TCP,则做出来的程序差不多就是直接与IP打交道。UDP从应用进程得到数据,附加几个小字段,将形成的用户数据报交给网络层。网络层将该运输层段封装到一个IP数据报中,尽量将其交付给接收主机。如果报文段到达接收主机,UDP使用目标端口号将报文段中的数据交付给正确的进程。使用UDP发送报文段之前,双方的运输层实体之间没有握手。所以UDP是无连接的。

DNS运行在UDP上。当需要进行DNS查询时,DNS应用程序构造一份DNS查询报文交给UDP,而无须与任何运行在目的端系统中的UDP实体进行握手。主机端的UDP为此报文添加报头字段,将形成的报文段交给网络层。网络层将此UDP报文段封装进IP数据报,将其发送给DNS服务器。发起查询的DNS应用程序则等待响应。如果它没有收到响应,则要么试图向另一台DNS服务器发送该查询,要么通知调用的应用程序它不能获得响应。

为什么有时候开发人员宁愿在UDP上构建应用而不是TCP?TCP提供了可靠数据传输服务,而UDP不能,那么为何不将TCP作为首选?因为许多应用更适合用UDP,原因主要有:
·应用层可以更精细地控制发送什么数据以及何时发送。采用UDP时,只要将数据传递给UDP,UDP就会将其打包进UDP报文段并立即传递给网络层。而TCP具有拥塞控制机制,当源和目标主机间的链路堵塞时,会降低TCP发送方的传输速率。如果迟迟无法收到目标主机确认已收到报文的应答,TCP总是会重发报文,而不考虑耗时。因为多数实时应用不希望过分延迟报文段的传送,且能容忍一些数据丢失,TCP服务模型并不特别适合这些应用。这些应用可以使用UDP,并在应用层实现UDP不提供但程序又需要的额外功能。
·无需建立连接。TCP在开始传输之前需要三次握手;UDP却无需任何准备即可开始传输。因此UDP没有建立连接的时延。这是DNS运行在UDP而不是TCP上的主要原因:如果运行在TCP上,则DNS会慢得多。HTTP使用TCP而不是UDP,因为对具有文本的Web网页来说,可靠性至关重要。但是,HTTP中的TCP连接建立时延对于下载Web文档的影响比较大。Chrome浏览器中的QUIC协议(快速UDP Internet连接)将UDP作为运输协议,并在应用层协议中实现可靠性。
·无需追踪连接状态。TCP需要在端系统中维护连接状态,包括接收和发送缓存、拥塞控制参数以及序号与确认号的参数。要实现TCP的可靠数据传输并提供拥塞控制,这些状态信息是必要的。UDP不维护连接状态,也不跟踪这些参数。因此,当应用程序运行在UDP而不是TCP上时,服务器就能同时服务更多客户端。
·分组首部开销小。每个TCP报文段至少有20字节的首部开销,而UDP仅有8字节的开销。

此外,再提一下UDP的另外两个特点:
·UDP是面向报文的。发送方UDP对应用程序交下来的报文,在添加报头后就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。这就是说,应用层交给UDP多长的报文,UDP就照样发送,即一次发送一份完整的报文。在接收方的UDP,对IP层交上来的用户数据报,在去除首部后就原封不动地交付上层的应用进程:UDP一次交付一份完整的报文。因此,应用程序必须选择合适大小的报文。若报文太长,UDP把它交给IP层后,IP层在传送时就要分片(见4.3节),这会降低IP层的效率。反之,若报文太短,UDP把它交给IP层后,会使IP数据报报头的相对长度太大,这也降低了IP层的效率。
·UDP支持一对一、一对多、多对一和多对多的交互通信。
·UDP没有拥塞控制。网络出现的拥塞不会使源主机的发送速率降低,很适合多媒体通信等场景。

下表给出了常见Internet应用使用的协议。邮件、TELNET、Web及文件传输都运行在TCP上,因为它们都需要可靠数据传输服务。有很多重要的应用是运行在UDP上而不是TCP上。例如,简单网络管理协议(SNMP,见5.7节)。在这种场合下,UDP要优于TCP。因为网络管理程序通常在该网络处于重压状态时运行。这时候如果控制与管理仍要额外花费较多的网络流量,它们也就失去了意义。DNS运行在UDP之上,从而避免了TCP的额外开销。
UDP和TCP都用于多媒体应用,如Internet电话、实时视频会议、流式影音。这些应用都能容忍少量的分组丢失,因此可靠数据传输不太重要。此外,TCP的拥塞控制会导致Internet电话、视频会议之类的实时应用的性能变得很差。因此,多媒体应用开发人员通常将这些应用运行在UDP上。出于安全原因,某些机构会禁止UDP流量。对于流式媒体传输来说,TCP变得越来越有吸引力了。

在UDP之上运行多媒体应用依然存在争议。如前所述,UDP没有拥塞控制。在拥塞状态中可做的有用工作非常少。但面对诸如大量用户同时在传输高比特率的视频等情况,若不使用任何拥塞控制,则会使路由器中出现大量的分组溢出,以至于只能成功传送非常少的UDP分组。并且,由无控制的UDP发送的大量数据将引起TCP发送方(TCP遇到拥塞将降低发送速率)大大减小它们的速率。因此,UDP缺乏拥塞控制,导致了UDP的高丢包率,并挤垮TCP会话,这是一个潜在的严重问题。很多研究人员已提出了新机制,以促使所有的数据源(包括UDP源)执行自适应的拥塞控制。
使用UDP的应用也可以实现可靠数据传输:这要通过应用程序自身建立可靠性机制来完成(例如,可通过增加确认与重传机制来实现)。前面说过,Chrome浏览器中所使用的QUIC协议在UDP之上的应用层协议中实现了可靠性。但开发这样的程序的工作量很大。但无论如何,这种做法能令鱼与熊掌兼得。也就是说应用进程可以进行可靠通信,同时无须受制于TCP拥塞控制机制强加的传输速率限制。

UDP报文段结构如图所示,它由RFC 768定义。应用层数据位于数据字段。对于DNS应用,数据字段是一份查询或响应报文。对于流式音频应用,音频片段填充到数据字段。UDP报头只有4个字段,每个字段2字节:首先是源端口号与目标端口号。其次是长度字段,指示了UDP报文段的字节数(首部加数据,最小值为8,即仅有首部)。因为数据字段的长度可以不同,所以需要专门表示出来。然后是校验和(checksum),接收方使用它检查传输是否出错。
源端口号的用途是作为返回地址的一部分,即当目标主机需要回发一个报文段给源主机时,就可以从已经接收的报文直接知道源主机的地址。

UDP具有一定的差错检测功能。
校验和有16位,其计算方法是:先将校验和字段填零,然后将报头和数据以16位为单位累加起来,将最高位的进位加到最低位,最后取反。校验结果由发送方填入报头的校验和字段。
在计算检验和时,要在用户数据报之前增加12个字节的伪报头。伪报头并不是用户数据报真正的报头,只是在计算检验和时,临时添加在用户数据报前面,得到一份临时的用户数据报。检验和就是按照临时的用户数据报来计算的。伪报头既不向下传送也不向上递交,而仅仅是为了计算检验和。下图给出了伪报头各字段的内容。
接收方将伪报头、报头(包括发送方给出的校验和)和数据累加。在数据没有出错时,相加结果一定是FFFFh,即16个1(二进制下)。虽然检验结果为FFFFh时不一定能严格保证没有出错,但当检验结果不为FFFFh时,一定意味着该分组出了错。这种差错检验方法的检错能力并不强,但好处是简单,处理起来较快。
当校验失败时,UDP会直接丢包(也可以上交给应用层,但附上出现了差错的警告)。如果接收方UDP发现收到的报文中的目标端口号不正确(即不存在对应于该端口号的应用进程),就丢弃该报文,并由ICMP(5.6节)发送“端口不可达”(port unreachable)差错报文给发送方。

UDP为何会提供基本的检错手段?其原因是:不能保证源和目的地之间的所有链路都提供差错检测;此外,即使报文段经链路正确地传输,当报文段存储在路由器的内存中时,也可能出错。在既无法确保逐链路的可靠性,又无法确保内存能检错的情况下,如果端到端数据传输服务需要差错检测,UDP就必须在运输层提供差错检测。这是系统设计的基本原则之一——端到端原则(end-end principle)的例子,该原则的一种表述为:如果一种机制能在端系统实现,那么就不应将其在网络核心中实现;网络核心应当尽可能提供通用的服务,而具体应用相关的功能应避免在网络核心出现。
端到端原则旨在让上层承担网络应用的开发和创新,而让网络本身保持相对简单。这种相对简单的核心网络模型也是网络能够在上层变换实现不同应用的技术基础,确保网络能够被位于边缘的用户较为容易地扩展新的功能。端到端原则的显著好处是保持了Internet的伸缩性、通用性和开放性。具体而言:其一,核心网的复杂性得以降低。其二,网络容易支持新应用程序。其三,增加了网络应用的可靠性。
端到端原则指导了Internet的体系结构设计。因为网络应用能够迅速在边缘网上开发、运行、产生效益,无需对核心网进行改动,所以Internet上不断创新的应用能够应运而生,发展了资源共享、通信、游戏、信息发布等各种各样丰富多彩的应用模式。这也是Internet得以被普通用户广泛使用的根本原因。
3.4 可靠数据传输的原理
可靠数据传输需要向上层保证:传输的任何位都不会翻转、丢失,并且任何位的相对位置不会改变。两台终端之间的部分,即TCP下面的层和物理链路,一般都被视为不可靠。
自动重传请求(Automatic repeat request,ARQ)协议令接收方发送额外的控制报文通知发送方,哪些内容已经正确接收,哪些内容有误需要重发。ARQ协议中,有三种机制处理位错误:
·错误检测。将传输的数据划分成若干等份,每份增加额外的字节,称为校验码或校验和,之后再一并发送出去。
·接收反馈。接收方成功接收后,发送确认(acknowledge,ACK)消息给发送方,确认该段数据已接收。
在未能成功接收时,也可以发送否定确认(negative acknowledge,NAK)消息。不过由于这样处理会使协议复杂化,现在实用的可靠传输协议都不使用否认报文了。
·重传。接收方收到有差错的分组时,发送方将重传该分组。
等待接收方的ACK消息时,发送方一般不发送新的数据。具备这种行为的协议称为停止-等待(stop-and-wait)协议。

ACK或NAK消息也有可能受损或丢失。几乎所有现有的数据传输协议中,都采用了为发送数据编号的方法来避免此问题。在数据分组中添加一新字段,让发送方对其数据分组编号,即将发送数据分组的序号(sequence number)放在该字段。于是,接收方只需要检查序号即可确定哪些分组对应哪些ACK或NAK分组。ACK或NAK分组本身没有编号。
对每个发送出去的数据包,发送方都等待一段时间。超过时间还未接收到对应的ACK信息时,则将其重发。

如果一个协议能够实现上述机制,那么这个协议的可靠性已经比较高了。但是,它在性能上存在重大问题。如果总是等到收到一个包的ACK消息以后才发送下一个包,那么信道的利用率(utilization)会非常低。信道的利用率是有效吞吐量与理论吞吐量之比,即:
U=r_e/r_t =(L/(d_proc+d_queue+d_trans+d_prop ))/(L/d_trans )=(d_proc+d_queue+d_trans+d_prop)/d_trans =d/d_trans
假设从美国西海岸到东海岸的一条链路的总时延d=RTT=30 ms。如果每个分组长1000字节(8000位),该链路的传输速率为1 Gbps,并且忽略处理延迟和排队延迟。向上式代入
L=1000 Byte, r_t=1 Gbps, d_trans=L/r_t
解得有效吞吐率仅为267 kbps,利用率低至0.027 %。这对这条1 Gbps的链路是极大的浪费。如果还考虑排队延迟和处理延迟,信道利用率只会更低。
为了提高利用率,引入流水线(pipelining)技术:在等待某个包的ACK消息时,发送方就可以先行发送后续的数据包,而无需等待每条ACK消息经过长长的链路返回到发送方之后,再发送下一个包。显然,这极大减少了传播时延的影响。
不使用流水线传输时,甚至可以只用1位来为一段时间内的分组编号。但使用流水线传输以后,很明显,假如仍然只使用1位,那么编号冲突的概率将变得极高。这就需要扩大编号占用的位数。发送方也需要准备一定的缓存,用于临时存储未收到ACK消息的分组。当然,此种情况下,接收方或许也需要缓存分组,这将在下面讨论。

回退N步(Go-back-N)协议中,允许发送方发送多个分组而不需等待之前的分组的ACK消息。但这受限于在流水线中未确认的分组数,它不能超过某个最大允许的值N。
如图,N称为发送窗口(通知窗口)大小。GBN协议是一种滑动窗口协议(sliding-window protocol)。蓝色部分是已确认的分组,浅蓝色部分是已发送但未确认的分组,灰色部分是可以开始发送的分组,白色部分是还不允许先行发送的分组。对于窗口内的N个分组,如果未发送,则可以提前发送,而无需等待之前的分组的ACK;如果已经发送,则可以继续等待其ACK消息。窗口从左向右(即时间流逝方向)滑动。一旦已发送但未确认接收的包超时,这个包就需要重发。由于各种干扰,分组常常不会按原有顺序到达接收方。
N也决定了分组序号字段的长度。k位的序号允许的最大窗口大小为2^k。TCP的序号字段长为32位,但TCP不为每个报文依次编号,而是将该报文段的数据字段的第一个字节作为编号。详见3.5节。

在GBN协议中,当发送方要求发送数据时,首先检查窗口是否已满,即是否有N个已发送但未被确认的分组。如果窗口未满,则产生新的分组并发送,并相应地更新变量。否则,发送方只需将数据返回给上层,提示窗口已满。上层可以选择过一会儿再试。在实际实现中,发送方更可能缓存而不立刻发送这些数据,或者使用同步机制(如一个信号量(semaphore,参见操作系统相关教程)或标志变量)允许上层在仅当窗口不满时才发出发送请求。
发送窗口通常只是发送缓存的一部分。已被确认的数据应当从发送缓存中删除,因此发送缓存和发送窗口的后沿(左侧)是重合的。发送应用程序最后写入发送缓存的字节减去最后被确认的字节,就是还保留在发送缓存中的被写入的字节数。发送应用程序必须控制写入缓存的速率,不能太快,否则发送缓存很快就会用尽。

如果超时(timeout),发送方重传所有已发送但还未被确认过的分组。一般来说,发送方仅使用一个定时器,为最早的已发送但未确认的包计时。如果收到一个ACK,但仍有已发送但未被确认的分组,则定时器被重新启动。如果没有已发送但未被确认的分组,则停止该定时器。

在GBN中,接收方的动作也很简单。如果一个序号为n的分组被正确接收,并且有序(即上次收到的是序号为n-1的分组),则接收方为分组n发送一个ACK,并将该分组中的数据部分交付到上层。即:收到若干个分组后,对按序到达的最后一个分组发送确认。
其它情况下,接收方丢弃该分组。因此,如果分组k已接收并交付,则所有序号比k小的分组也已经交付。可见,使用累积确认(cumulative acknowledgement)是GBN一个自然的选择。累积确认机制一般也为接收方常用。

尽管丢弃一个正确接收但乱序的分组有点愚蠢和浪费,但这样做是有理由的。接收方必须按序将数据交付给上层。假定现在期望接收分组n而分组n+1却先到了。因为数据必须按序交付,接收方可以选择缓存分组n+1,然后,在它收到并交付分组n后,再将该分组交付到上层。然而,如果分组n丢失,因为GBN使用累积确认的方式发送ACK分组,所以无法先行发送n+1分组的ACK信息,因此该分组及分组n+1最终会被发送方重传。因此,接收方只需丢弃分组n+1即可。
这种方法的优点是接收缓存简单,即接收方不需要缓存任何失序分组;并且,即使ACK信息丢失,通常也不会引起发送方重发数据,因为只要之后发送方及时收到一条ACK消息,就能一次性确认之前没有确认的分组了。
当然,此方法的缺点是:如有分组丢失或出错,往往需要更多的重传。例如,如果发送方发送了前5个分组,而中间的第3个分组丢失了。这时接收方只能对前两个分组发出确认。发送方无法知道后面三个分组的下落,而只好把后面的三个分组都再重传一次。当通信线路质量不好时,这会带来显著的负面影响。

GBN协议是使用流水线机制的。如果窗口长度或时延带宽积较大,就会令链路上存在许多尚未接收的分组。这时候就更容易因为单个分组出错而引起大量分组重新发送。为了解决此问题,就需要选择重传(Selective Repeat,SR)协议。这要求接收方为每个正确接收的分组都发送一条ACK消息。接收方还需要缓存正确接收的分组,以确保乱序到来的分组被按原有顺序交给上层。失序的分组将被缓存直到所有丢失分组(即序号更小的分组)皆被收到为止。如果接收应用程序能够及时从接收缓存中读取收到的数据,接收窗口(可用窗口)就可以增大,但最大不能超过接收缓存的大小。

总的来说,发送方需要应对的情况有:
1、从上层接收到数据。SR发送方检查下一个可用于该分组的序号。如果序号位于发送窗口内,则将数据打包并发送;否则就像在GBN中一样,要么将数据缓存,要么将其返回给上层以便以后传输。
2、超时。定时器再次被用来防止丢失分组。现在每个分组必须拥有单独的定时器,因为要确保超时不至于引起其它已经确认接收的分组一同重新发送。可以使用单个专门的硬件定时器模拟多个逻辑定时器的操作。
3、收到ACK。倘若该分组序号在窗口内,则发送方将那个被确认的分组标记为已接收。如果该分组的序号位于窗口最左侧,则窗口向前(向右)移动,使得其最左侧与具有最小序号的未确认分组重合。窗口移动后,如果新的窗口范围内存在未发送的分组,就将其发送。

接收方使用接收窗口来决定哪些包允许被接收。接收方需要应对的情况有:
1、窗口内的一个或多个分组被正确接收。此时需要准备相应的ACK回应发送方,然后缓存该分组(如果已经缓存过,则直接丢包)。如果窗口最左侧的未接收分组被正确接收,则窗口向前移动,同时按序向上层交付被移出窗口的分组。
2、在当前窗口位置左侧一倍窗口大小的范围内的N个编号的包被接收到(这些包已经被正确接收过,因此新收到的副本也被丢弃),则需要重新产生ACK并传回给发送方。
3、其它情形,直接丢包。

第二步是必要的,因为在接收方的窗口最左侧的未接收分组被正确接收后,窗口就会移动,这可能会导致接收方的窗口比发送方的窗口提前移动,下图就是一个例子。这时候,如果发送方意外没有收到之前的包的ACK,就会发生重传。如果接收方再次接收到这些包后不重新发送ACK给发送方,发送方的窗口就会一直无法向前移动。

因为发送窗口大于接收窗口部分的包可能不能被接收,所以:发送窗口大小≤接收窗口大小。

不难证明:发送窗口不会超前接收窗口,即发送窗口最左侧对应的编号不大于接收窗口最左侧对应的编号。在传输开始时,发送、接收窗口都在起始位置,前面所说的两个编号相同。这之后,因为对任何一个包,永远都是接收方先接收到,再有发送方接收到ACK,所以接收窗口肯定先于发送窗口移动。

同样不难证明:不会出现接收窗口最左侧与发送窗口最左侧的距离超过窗口大小N的情形。假设某个时刻接收方的窗口先移动了。按照最坏情况考虑:接收窗口全部收到了N个包,但这N个包的ACK全丢,于是接收窗口比发送窗口超前了N。但这时,发送方会重传的包对应接收窗口移动之前的N个位置,而接收方现在准备接收的新的N个包都不在发送窗口的范围内,于是接收窗口就无法向前移动了。窗口的移动是单向的(不能撤销已经收到的确认),所以命题得证。因此步骤3是合理的。
因为接收方最左侧与发送方最左侧的距离不超过窗口大小N,所以我们可以得出一个结论:窗口长度必须小于或等于序号空间大小的一半。如果超过一半,在刚才所说的这种极端情况中,就必定存在不同的包被分配到相同序号的可能性,导致最终传输的数据出错或部分丢失。即:过期的数据因为编号冲突,对最新的数据造成干扰。
不难验证:当发送窗口大小小于接收窗口大小时,刚才证明的第一个结论(加粗部分)仍然成立;不过,
“不会出现接收窗口最左侧与发送窗口最左侧的距离超过窗口大小N的情形”此时应更改为
“不会出现接收窗口最左侧与发送窗口最左侧的距离超过发送窗口大小N的情形”;
“窗口长度必须小于或等于序号空间大小的一半”应更改为“发送窗口与接收窗口的长度之和不超过序号空间大小”。

上面说的情况,换种说法是:在窗口长度超过序号总数的一半时,即使中间的链路完全没有导致数据包乱序到达,也可以引发编号冲突;而实际上,即使窗口长度不超过序号总数的一半,也有可能会因为中间的传输链路导致数据包的乱序从而引发问题。在发送方与接收方由单段物理线路相连的情况下,发送方和接收方之间的链路几乎不会重排各个包;但如果两端之间是一个非常庞大而复杂的网络,数据包的顺序被打乱的概率就不低了。结合上面的例子,假如发送方在向接收方重传未确认接收的编号为x的分组时,一段时间之前滞留在网络中的编号也为x的分组先到达了接收方并被确认(注意:这时候双方的窗口早已移走了,因为后来发送方重发的相同数据的包已经被确认接收),它们虽然具有相同的编号,但数据部分不同。结果就是因为编号冲突,之前的数据覆盖了后来的数据,导致传输出错(后来的数据所在的包被丢弃了)。为了尽可能将此情况的发生率降低到零,可以为每个分组指定存活时间。比如在高性能TCP扩展(RFC 1323)中,规定了分组的最长寿命约为3分钟。也有新的使用序号的方法被提出,它能够完全避免这种重新排序问题。
3.5 面向连接的运输:TCP
前面说过,TCP是面向连接的(connection-oriented):一个进程在向另一个进程发送数据之前,这两个进程必须先相互握手,即相互发送某些预备报文段,准备数据传输的参数。作为建立连接的一部分,连接双方初始化许多TCP状态变量。
TCP连接不是一条像在电路交换网络中的端到端TDM或FDM电路。相反,它是一条逻辑连接,其共同状态仅保留在两个通信终端的TCP程序中。由于TCP只在端系统中运行,而不在中间的网络元素(路由器和链路层交换机)中运行,所以中间的网络元素不维持TCP连接状态。事实上,中间路由器对TCP连接完全视而不见,它们只能看到数据报,而不是连接。

TCP连接是全双工(full-duplex)的:连接双方可以同时收发数据。TCP连接也是点对点(point-to-point)的:仅在单个发送方与单个接收方之间连接。TCP无法多播(multicasting,4.3节):只建立一个TCP连接时,一个发送方无法将数据同时传送给多个接收方。

TCP允许通信双方的应用进程在任何时候都能传输数据。TCP连接的两端都设有发送缓存和接收缓存,用来临时存放通信数据。在发送时,应用程序在把数据传送给TCP的缓存后,就可以做自己的事,而TCP在合适的时候把数据发送出去。在接收时,TCP把收到的数据放入缓存,上层的应用进程在合适的时候可以读取到缓存中的数据。

TCP不关心应用进程一次把多长的报文发送到TCP的缓存中,而是根据对方给出的窗口值和当前网络拥塞的程度来决定一个报文段应包含多少字节(UDP发送的报文长度是应用进程给出的)。如果应用进程传送到TCP缓存的数据太长,TCP可以把它划分短一些再传送。如果应用进程一次只发来1字节,TCP也可以在积累足够多的字节后再构造报文段发送出去。

TCP是面向字节流(byte-stream-oriented)的。TCP中的流(stream)指的是流入或流出进程的字节序列。“面向字节流”的含义是:TCP把应用程序交下来的数据仅仅看成一连串无结构的字节流。

数据从发送端的套接字来到运输层后,就由发送端运行的TCP控制了。TCP将这些数据引导到该连接的发送缓存里,发送缓存是三次握手期间设置的缓存之一。接下来TCP不时从发送缓存里取出一块数据,传递到网络层。TCP规范RFC 793没有提及TCP应何时发送缓存里的数据,只规定了“TCP应在方便的时候以报文段的形式发送数据”。TCP可从缓存中取出并放入报文段中的数据量受限于最大报文段长度(Maximum Segment Size,MSS)。MSS通常根据最初确定的由发送主机发送的单个链路层帧允许装入的数据长度的上限(最大传输单元,Maximum Transmission Unit,MTU)来设置。MSS的值要保证一个TCP段(当封装在一份IP数据报中)加上TCP/IP首部长度(通常40字节,20 + 20)能放入单个链路层帧。以太网和PPP(点对点协议,以后会学到)都具有1500字节的MTU,因此MSS的典型值为1460字节。注意:MSS是指在报文段里应用层数据的最大长度,而不是指包括首部的TCP报文段的最大长度。
TCP为每块要传输的数据配上首部,从而形成多个TCP段。这些段被下传给网络层,网络层将其分别封装在IP数据报中,发送到网络。当另一端接收到TCP段后,该段的数据就被放入该TCP连接的接收缓存。应用程序从此缓存读取数据流。

TCP报文段由首部字段和数据字段组成。数据字段包含需要传输的数据。如前所述,MSS限制了数据字段的长度。当TCP需要发送较多数据时,需将数据划分成长度为MSS的若干块(最后一块通常短于MSS)。然而,交互式应用通常传送长度小于MSS的数据块。例如,对于Telnet,其TCP报文段的数据字段经常只有1字节。由于TCP的首部一般是20字节(比UDP首部多12字节),所以该报文段只有21字节长。

上图显示了TCP报文段的结构。与UDP一样,段头包括源端口号和目标端口号,它被用于多路复用 / 分用来自或送到上层应用的数据。同UDP一样,TCP段头也包括校验和字段(checksum field)。TCP段段头还包含下列字段:

·32位的序号字段(sequence number field)和32位的确认号字段(acknowledgment number field)。TCP收发双方用它们实现可靠数据传输服务,讨论见后。

·4位的段头长度字段(header length field),指示以32位的字为单位的TCP段头长度。由于存在TCP选项字段,段头长度是可变的。通常选项字段为空,所以TCP段头的典型长度是20字节,也是最小长度。这固定的20字节包括源端口号到紧急数据指针这部分。

·6位暂未使用(unused)。目前应置零。

·8位的标志字段(flag field):
·在成功接收的段中,ACK位为有效。
·RST、SYN和FIN位用于连接建立和删除。RST = 1表明TCP连接出现严重差错(由于主机崩溃、网络中断或其它原因),必须释放连接,再重新建立连接。RST置1还用来拒绝一个非法的报文段或拒绝打开一个连接。RST也可称为重建位或重置位。更多细节将在之后讨论。
·在显式拥塞通知中会使用CWR和ECE位,见3.7节。
·当PSH(push)位被置位时,指示接收方应立即将数据交给上层。接收方TCP收到PSH = 1的报文段,就尽快地(即“推送”向前)交付接收应用进程,而不再等到整个缓存都填满了后再向上交付。
·URG位指示报文段存在被发送端上层标记为“紧急”的数据。紧急数据的最后一个字节由16位的紧急数据指针字段(urgent data pointer field)指出。当紧急数据存在并给出指向紧急数据的尾指针时,代表文段中有紧急数据,应尽快传送,而不要按原来的排队顺序传送。例如,已经发送了很长的一个程序在远程主机上运行。但后来突然发现了严重问题,需要立刻取消该程序的运行。因此用户键入Ctrl+C。如果不使用紧急数据,那么这两个字符将存储在接收TCP的缓存末尾。只有在所有的数据被处理完毕后这两个字符才被交付接收方的应用进程。这样做就浪费了许多时间。传送紧急数据时,发送方TCP把紧急数据插入到本报文段数据的最前面;而紧急数据后面的数据仍旧为普通数据。

·16位的接收窗口字段(receive window field),用于流量控制。该字段用于指示接收方愿意接收的字节数。注意:窗口指的是发送本报文段的一方的接收窗口,而不是自己的发送窗口。窗口值告诉对方:从本报文段段头的确认号算起,接收方目前允许对方发送的数据量(单位:Byte)。之所以要有这个限制,是因为接收方的缓存是有限的。总之,窗口值作为接收方让发送方设置其发送窗口的依据。

·16位的校验和。检验的范围包括首部和数据这两部分。和UDP一样,在计算检验和时,要在TCP报文段的前面加上12字节的伪首部。伪首部的格式与UDP的伪首部一样,但应把伪首部第4字段中的17改为6(TCP的协议号),第5字段解释为TCP长度。接收方收到此报文段后,仍要加上这个伪首部来计算检验和。若使用IPv6,则相应的伪首部也要改变。

·可选、变长的选项字段(options field),最长可达40字节。用于设定最大报文段长度(MSS),或在高速网络环境下用作窗口调节因子使用。
为何要确定最大报文段长度呢?这并不是考虑接收方的接收缓存可能放不下TCP段中的数据。实际上,MSS与接收窗口值没有关系。我们知道,TCP报文段的数据部分,至少要加上40字节的首部(TCP首部20字节和IP首部20字节,这里都还没有考虑首部中的选项部分),才能组装成一份IP数据报。若选择较小的MSS,网络的利用率就会降低。极端情况下,当TCP段只含1字节的数据时,IP数据报的额外开销至少有40字节(包括TCP段段头和IP数据报报头)。这样,对网络的利用率就不会超过1 / 41。到了数据链路层还要加上一些开销。但反过来,若TCP段非常长,那么在IP层传输时就有可能要分解成多个短数据报片。在终点要把收到的短数据报片装配成原来的TCP段。当传输出错时还要进行重传。这些也都会使开销增大,进而使传输效率降低。
因此,MSS应尽可能大些,只要在IP层传输时不需分片就行。由于IP数据报所经历的路径是动态变化的,因此在这条路径上确定的不需要分片的MSS,如果改走另一条路径就可能需要分片。因此最佳的MSS是很难确定的。在连接建立的过程中,双方都把自己能够支持的MSS写入这一字段,以后就按照这个数值传送数据,两个传送方向可以有不同的MSS值。若主机未填写此项,则默认值是536字节。因此,所有在互联网上的主机都应能接受的报文段长度是536 + 20(固定首部长度)= 556字节。
RFC 879指出,流行的一种说法是:在TCP连接建立阶段“双方协商MSS值”,但这是错误的,因为这里并不存在任何的协商,而只是一方把MSS值设定好以后通知另一方而已。

随着互联网的发展,又陆续增加了几个选项,如窗口扩大选项、时间戳选项等(RFC 7323)。以后又增加了有关选择确认选项(RFC 2018)。这些选项的位置都在选项字段中。
首部字段还定义了一个时间戳选项,具体细节参见RFC 854和RFC 1323,这里只作简单说明。

窗口扩大选项是为了扩大窗口。TCP首部中窗口字段长度是16位,因此最大窗口大小为64 KB。这对早期的网络是足够用的,但对于包含卫星信道的网络,传播时延和带宽都很大,要获得高吞吐率需要更大的窗口大小。
窗口扩大选项占3字节,其中有一个字节表示移位值S。新的窗口值等于TCP首部中的窗口位数从16增大到(16 + S)。移位值允许使用的最大值是14,相当于窗口最大值增大到216+14 – 1 = 230 – 1。
窗口扩大选项可以在双方初始建立TCP连接时进行协商。如果连接的某一端实现了窗口扩大,当它不再需要扩大其窗口时,可发送S = 0的选项,使窗口大小回到16。

时间戳(timestamp)选项占10字节,其中最主要的字段是时间戳值字段(4字节)和时间戳回送回答字段(4字节)。时间戳选项有以下两个功能:
第一,用来计算往返时间(RTT)。发送方在发送报文段时把当前时钟的时间值放入时间戳字段,接收方在确认该报文段时把时间戳字段值复制到时间戳回送回答字段。因此,发送方在收到确认报文后,可以准确计算RTT。
第二,用于处理TCP序号≥232的情况,这又称为防止序号绕回(Protect Against Wrapped Sequence numbers,PAWS)。TCP段的序号只有32位,编号不小于232后,就会重复使用原来用过的序号。当使用高速网络时,在一次TCP连接的数据传送中序号很可能会被重复使用:使用1.5 Mbps的速率发送报文段时,序号重复要6小时以上;但若用2.5 Gbps的速率发送报文段,则不到14秒钟序号就会重复。为了使接收方能够把新的报文段和迟到很久的报文段区分开,可以在报文段中加上这种时间戳。序号冲突导致数据出错在3.4节已经讲解过了。

选择确认(SACK)表明只传送缺少的数据而不重传已经正确到达接收方的数据。其原理已经在3.4节中的选择重传(SR)协议中讲过。
如果要使用SACK,那么在建立TCP连接时,就要在选项中加上“允许SACK”,而双方必须都事先商定好。如果使用选择确认,那么原来段头中的“确认号字段”的用法仍然不变,只是以后在TCP报文段段头中都增加了SACK选项,以便报告收到的不连续的字节块的边界。由于首部选项的长度最多只有40字节,而指明一个边界就要用掉4字节(因为序号有32位,需要4字节表示),因此在选项中最多只能指明4个字节块的边界信息。这是因为4个字节块共有8个边界,因而需要用32个字节来描述。另外还需要2字节:1字节用来指明是SACK选项,1字节指明这个选项要占用多少字节。如果要报告五个字节块的边界信息,那么至少需要42个字节,超过了40字节的上限。互联网建议标准RFC 2018还对报告这些边界信息的格式都做出了非常明确的规定,这里从略。
然而,SACK文档并没有指明发送方应当怎样响应SACK。因此大多数的实现还是重传所有未被确认的数据块。

此外,如果TCP段段头的长度不是4字节的整数倍,则需要在段头的最后填充。

TCP会把要发送的数据的每个字节都编号(可从任意自然数开始编号),并分成若干段;每个TCP段也编号,段编号存储在序号字段中。一个报文段的序号就是该报文段的数据字段的第一个字节的序号。
在TCP中,一方填入报文段的确认号是期望从另一方收到的下一个字节的编号。数据传输完毕后,接收方还会再给发送方发送一个数据字段为空的报文,这时确认号依然为期望从发送方收到的下一个字节的编号。
在网络中,TCP段的到达顺序不一定与发送顺序相同。TCP RFC没有规定应对方法。开发者有两种选择:【1】发现编号更大的报文段先到达,直接丢弃。【2】先缓存起来,等待前面缺失的段。实践中,一般采用第二种方法,因为它更省带宽。
实际应用中,可能会遇到这样的情况:两台主机虽然已经终止TCP连接,但网络上仍有该连接的报文(还没来得及到达接收方);随后这两台主机又建立了一个端口号不变的新TCP连接。这时候旧连接的报文到达了,它被误认为是新连接传输的报文。TCP连接的双方在对每个TCP段编号时,初始序号常常随机选择,这样可以将此种情况的可能性降到最低。

TCP采用超时-重传(timeout-retransmission)机制来解决报文段的丢失问题:对于一个发送出去的TCP段,如果在一定时间内未收到ACK,则重新传输该TCP段。超时必须大于该连接的往返时间(RTT),即从一个报文段发出到发送方收到接收方确认接收的回复的时间,否则会造成大量不必要的重传。
大多数时候,TCP仅在少数时刻做一次测量,得到的结果称为样本RTT(SRTT,S = sample),而不为每份发送的报文测量RTT。SRTT用于估计一段时间内的最佳超时。估计的RTT(ERTT,E = estimated)通过如下公式更新:
ERTT=(1-α)ERTT+αSRTT
RFC 6298推荐取α=0.125,于是上式变为
ERTT=7/8 ERTT+1/8 SRTT
很明显,新采样的SRTT在用于估计RTT时的比重比原有的样本要更大。这种统计方法称为指数加权移动平均(Exponential Weighted Moving Average,EWMA)。“指数”是指一个给定的SRTT的权重在更新的过程中指数衰减。

有时候,需要测量RTT的波动程度DRTT(SRTT与ERTT的偏离程度,D = deviation):
DRTT=(1-β)DRTT+β|SRTT-ERTT|
同样,DRTT是一个SRTT与ERTT之间差值的EWMA。推荐取β=0.25。

有了ERTT与DRTT之后,通过下述公式来确定超时重传时间(Retransmission Timeout):
RTO=ERTT+4DRTT
问题来了:如何判定确认报文是对先发送的报文段的确认,还是对后来重传的报文段的确认?由于重传的报文段和原来的报文段完全一样,因此源主机收到确认后,无法做出正确的判断,这对ERTT与实际的接近程度的影响很大。
在初始状态下,超时为1秒。对于超时的计入,使用Karn算法的改进算法(Karn算法不考虑重传报文段的RTT样本,使得时延突然大幅增加时超时无法更新):如果有TCP段传输超时,则
RTO×=γ
典型值γ=2,以免即将被确认的后继报文段过早出现超时。当有报文段成功接收后,再按照上述给出的公式变更超时。实践证明,此策略较为合理。
这是一种简单的拥塞控制。超时大多数由于网络拥塞引起,即发送端与接收端之间的链路中,某些结点累积了太多的包,导致很长的排队延迟或丢包。在拥塞的时候,如果源持续重传分组,会使拥塞更加严重。相反,TCP使用更文雅的方式,每个发送方的重传都是经过越来越长的时间间隔后进行的。

如图,取α=0.125时,从gaia.cs.umass.edu(Amherst,MA,US)到fantasia.eurecom.fr(Sophia Antipolis,Alpes-Maritimes,FR)的SRTT与ERTT的图线如下:

TCP通过使用确认接收与定时器机制来提供可靠数据传输。TCP的可靠数据传输服务确保一个进程从其接收缓存中读出的数据流是无损坏、无间隙、非冗余和有序的:该字节流与发送端发出的字节流完全相同。
只有收到ACK消息时,TCP才将该TCP段的数据提取出来,传递给应用层。当TCP认为报文段或其确认报文丢失或受损时,TCP会重传这些报文段。
TCP使用单一的定时器为每个未被确认的报文段计时,这是因为如果为每个未确认的TCP段单独计时,开销非常大。

有些版本的TCP采用隐式NAK机制:收到对一个特定报文段的3个ACK就可作为对后面报文段的一个隐式NAK,即便这个后续的报文段实际上没有超时,也会触发对该报文段的重传。可见,这种情况下,TCP认为该后续报文段和(或)其确认报文丢失了。TCP为每个报文段都添加了序号,以便接收方能识别丢失或重复的报文段。
冗余ACK(duplicate ACK)就是再次确认某个报文段的ACK。当某个报文段收到之前,编号比它更大的报文段先被收到,那么TCP会重新发送一条ACK消息,消息包含最后一个按序收到的TCP段的编号。一旦在收到某报文段的ACK后收到3个对同一报文段的冗余ACK,TCP就执行快速重传(fast retransmission),在该报文段的定时器过期前重传丢失的段。

总的来说,为了传输可靠,3.4节的许多思想,TCP都使用了,包括校验和、ACK信息、定时器、分组编号和滑动窗口。

TCP还使用流水线方法传输,使吞吐量显著提升。发送方能够具有的未被确认报文段的数量是由TCP的流量控制和拥塞控制机制决定的。TCP流量控制将在本节讨论;TCP拥塞控制将在3.7节讨论。此时只需知道TCP发送方使用了流水线。

TCP要求接收方必须有累积确认的功能,减小传输开销。接收方可以在合适的时候发送确认,也可以在自己有数据要发送时把确认信息顺便捎带上。但请注意两点:一是接收方不应过分推迟发送确认,否则会导致发送方不必要的重传,这反而浪费了网络的资源。TCP标准规定,确认推迟的时间不应超过0.5秒。若收到一连串具有最大长度的报文段,则必须每隔一个报文段就发送一个确认。二是捎带确认实际上并不经常发生,因为大多数应用程序很少同时在两个方向上发送数据。

TCP为应用程序提供了流量控制服务(flow-control service)以消除发送方使接收方缓存溢出的可能性。可见,流量控制是一个速度匹配服务,即通过调节接收窗口的大小,令发送方的发送速率与接收方应用程序的读取速率相匹配,既要让接收方来得及接收,也不要使网络发生拥塞。流量控制和拥塞控制采取的动作非常相似,但是它们显然是针对完全不同的原因而采取的措施。
接收方给发送方回应的报文中,指示了缓冲区的剩余空间。
有一个问题需要注意:接收方接到某个TCP段以后,缓冲区满了,接收窗口的大小减为零,而这之后发送的ACK报文又全部丢失。当接收方的应用读取了缓冲区,缓冲区重新拥有一定的空闲空间以后,发送方并不会继续收到通知。于是发送方被阻塞,一直不能再发送新的数据。如果没有其它措施,这种互相等待的死锁局面将一直延续下去。
TCP规范中要求:发送方收到接收方缓冲区满的消息后,等待一定时间,发送一个只有1字节数据的探测报文段。这些报
文段将会被接收方确认。之后,如果等待超时,就继续发送零窗口探测报文段;缓存开始具有空闲时,接收方发送的ACK消息里会指示缓冲区未满。
TCP规定,即使设置为零窗口,也必须接收以下几种报文段:零窗口探测报文段、确认报文段和携带紧急数据的报文段。
UDP并不提供流量控制,收到的用户数据报直接放在缓冲区中。如果缓冲区溢出,就会有数据丢失。

在TCP连接的生命周期内,每台主机中的TCP在各种TCP状态(TCP state)之间变化。客户端可以具有的TCP状态是:
CLOSED → SYN_SENT → ESTABLISHED → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT →
而服务器端可以具有的TCP状态是:
CLOSED → LISTEN → SYN_RCVD → ESTABLISHED → CLOSE_WAIT → LAST_ACK →

TCP通过三次握手(3-way handshake,三路握手,或称三报文握手,注意handshake没有s)建立连接。服务器进程事先创建好传输控制块(transmission control block,TCB),准备接收客户端发来的连接请求。然后服务器进程就从CLOSED状态进入LISTEN状态,等待客户的连接请求。如有,即做出响应。

·第一步:客户端的TCP首先创建传输控制块,在打算建立连接时,向服务器端的TCP发送一个特殊的TCP段。
该段不包含应用层数据,但是段头的SYN(同步序列编号,Synchronize Sequence Numbers)标志位置1。这个特殊报文段称为SYN报文段。另外,客户会随机选择一个初始序号(记为s),放置于该起始的TCP SYN报文段的序号字段中。报文段被封装进IP数据报并发送。为了避免某些攻击,这个初始序号是随机分配的。
这一步,客户进程从CLOSED状态进入SYN_SENT状态。

·第二步:一旦包含TCP SYN报文段的IP数据报到达服务器主机,服务器便提取出TCP SYN报文段,为该TCP连接分配TCP缓存和变量,并向客户TCP发送允许连接的报文段。这个允许连接的报文段也不包含应用层数据。但是,其首部却包含了重要的信息:
首先,SYN位和ACK位置位。其次,该TCP报文段首部的确认号字段被置为s+1。最后,服务器选择自己的初始序号(记为t),并放到TCP报文段首部的序号字段中。该报文段被称为SYN ACK报文段。
这一步,服务器进程进入SYN_RCVD状态。
发回给客户端的报文可以拆成两份:一个确认报文段(ACK = 1,确认号=s+1)和一个同步报文段(SYN = 1,
顺序号=t)。拆分后,三报文握手变为四报文握手,效果是一样的。

·第三步:在收到SYN ACK报文段后,客户也要给该连接分配缓存和变量。客户主机则向服务器发送另外一个报文段,用于对服务器的允许连接的报文段进行确认:
通过将值t+1放置到TCP报文段首部的确认字段中来完成此项工作。因为连接已经建立了,所以SYN位置0。这是握手的第三阶段,可以在报文段中携带客户到服务器的数据;但如果不携带数据则不消耗序号,在这种情况下,下一个数据报文段的序号仍是s+1。
这一步,客户端进入ESTABLISHED状态。服务器收到客户端的确认后,也进入ESTABLISHED状态。
一旦完成这3个步骤,客户和服务器主机就可以相互发送数据了。在以后每一个报文段中,SYN位都为0。

为什么客户端最后还要发送一次确认呢?这主要是为了防止已失效的连接请求报文段突然又传送到服务器,产生错误。
所谓“已失效的连接请求报文段”是这样产生的。考虑一种比较正常的情况:客户端A发出连接请求,但请求报文丢失而未收到确认。于是A再重传一次连接请求。后来收到了确认,建立了连接。数据传输完毕后,就释放了连接。A共发送了两个连接请求报文段,其中第一个丢失,第二个到达了服务器B,没有“已失效的连接请求报文段”。
现假定出现一种异常情况:A发出的第一个连接请求报文段并没有丢失,而是在某些网络结点长时间滞留了,以致延误到连接释放以后的某个时间才到达B。本来这是一个早已失效的报文段。但B收到此失效的连接请求报文段后,就误认为是A又发出一次新的连接请求。于是就向A发出确认报文段,同意建立连接。假定不采用报文握手,那么只要B发出确认,新的连接就建立了。
由于现在A并没有发出建立连接的请求,因此不会理睬B的确认,也不会向B发送数据。但B却以为新的运输连接已经建立了,并一直等待A发来数据。B的许多资源就这样白白浪费了。

TCP关闭连接时,客户端和服务器之间至少需要发送4个数据包,该过程称为四次挥手或四报文挥手。
·第一步:客户应用进程发出关闭连接命令。之后,客户TCP向服务器进程发送一个特殊的TCP段,进入FIN_WAIT_1状态。段首部的FIN标志位置l,序号记为u,等于前面已经传送过的数据的最后一个字节的编号加上1。请注意,TCP规定,FIN报文段即使不携带数据,它也消耗掉一个序号。

·第二步:服务器收到该段后,就回送一个确认报文段,确认号是u+1,并进入CLOSE_WAIT状态。TCP服务器进程这时应通知高层应用进程,因而从客户端到服务器这个方向的连接就释放了,这时的TCP连接处于半关闭(half-close)状态,即客户端已经没有数据要发送了;但服务器若发送数据,客户端仍要接收。也就是说,从服务器到客户端这个方向的连接并未关闭,这个状态可能会持续一段时间。客户端收到服务器的确认报文后,进入FIN_WAIT_2状态。

·第三步:若服务器已经没有要向客户端发送的数据,其应用进程就通知TCP释放连接。服务器发送它自己的终止报文段,其FIN标志位置1。服务器端给的序号记为v(在半关闭状态服务器可能又发送了一些数据)。服务器还必须重复上次已经发送过的确认号u+1。服务器进入LAST_ACK状态。注意:如果在半关闭状态服务器没有发送额外的数据,那么挥手的报文只有三份。

·第四步:客户对服务器的终止报文段确认,ACK置1,序号为u+1,确认号为v+1,进入TIME_WAIT状态。如果这是释放连接过程中的第3份报文(在半关闭状态服务器没有发送数据),则确认号为第二部中发送的报文的序号加1。
请注意,现在TCP连接还没有释放掉。必须经过时间等待计时器(TIME-WAIT timer)设置的时间2MSL后,客户端才进入到CLOSED状态。MSL叫做最长报文段寿命(Maximum Segment Lifetime),RFC 793建议设为2分钟。对于现在的网络,这可能太长了一些。因此TCP允许不同的实现可根据具体情况使用更小的MSL值。所以,从客户端进入到TIME_WAIT状态后,要经过2倍的MSL才能进入到CLOSED状态,才能开始建立下一个新的连接。当A撤销相应的传输控制块TCB后,就结束了这次的TCP连接。

为什么客户端在TIME_WAIT状态必须等待2MSL的时间呢?这有两个理由。
第一,为了保证客户端发送的最后一个ACK报文段能够到达服务器。这个ACK报文段有可能丢失,因而使处在LAST_ACK状态的服务器收不到对已发送的FIN + ACK报文段的确认。服务器在超时后重传这个FIN + ACK报文段,而客户端就能在2MSL时间内收到这个重传的FIN + ACK报文段。接着客户端重传一次确认,重新启动2MSL计时器。最后,两端都正常进入到CLOSED状态。如果客户端在TIME_WAIT状态不等待一段时间,而是在发送完ACK报文段后立即释放连接,那么就无法收到服务器重传的FIN + ACK报文段,因而也不会再发送一次确认报文段。这样,服务器就无法按照正常步骤进入CLOSED状态。
第二,防止之前提到的“已失效的连接请求报文段”出现在本连接中。客户端在发送完最后一个ACK报文段后,再经过2MSL的时间,就可以使本连接持续的时间内所产生的所有报文段都从网络中消失。这样就可以使下一个新的连接中不会出现这种旧的连接请求报文段。
服务器只要收到了客户端发出的确认,就进入CLOSED状态。同样,服务器在撤销相应的传输控制块TCB后,就结束了这次的TCP连接。因此,服务器结束TCP连接的时间要比客户端早一些。

除时间等待计时器外,TCP还设有一个保活计时器(keepalive timer)。设想有这样的情况:客户已主动与服务器建立了TCP连接,但后来客户端的主机突发故障。显然,服务器以后就不能再收到客户发来的数据。因此,应当有措施使服务器不要再白白等待下去。这就是使用保活计时器。服务器每收到一次客户的数据,就重新设置保活计时器,时间通常是2小时。若两小时没有收到客户的数据,服务器就发送一个探测报文段,以后每隔75秒钟发送一次。若一连发送10个探测报文段后仍无客户的响应,服务器就认为客户端出了故障,接着就关闭这个连接。

如果服务器收到TCP段后,发现段的目标端口与服务器已开启(监听)的端口都不匹配,则服务器向源回复一个特殊的报文段,该段将RST标志位置位。如果服务器收到目标端口与已开启的端口均不匹配的UDP段,则发送一个特殊的ICMP数据报(见第4章)。

nmap端口扫描工具可以向目标主机的指定端口发送TCP SYN报文段。源主机可能面临3种可能的结果:
• 从目标主机接收到一个TCP SYN ACK报文段。目标主机上的一个应用程序使用该TCP端口,nmap返回“打开”。
• 从目标主机接收到一个TCP RST报文段。这意味着该SYN报文段到达了目标主机,但目标主机没有运行一个使用该TCP端口的应用程序。不过,攻击者至少知道发向该主机指定端口的报文段没有被中途的任何防火墙(firewall)所阻挡。
• 什么也没有收到。这很可能表明该SYN报文段被中间的防火墙所阻挡,无法到达目标主机。
nmap是一个功能强大的工具。它不仅能侦察打开的TCP端口,也能侦察打开的UDP端口,还能侦察防火墙及其配置,甚至能侦察应用程序的版本和操作系统版本。其中的大多数都能通过操作TCP连接管理报文段完成。

在三次握手中,服务器为了响应一个收到的SYN,分配并初始化连接变量和缓存。然后服务器发送一个SYN ACK进行响应,并等待来自客户的ACK报文段。如果客户不发送ACK来完成该三次握手的第三步,最终(通常在一分多钟之后)服务器将终止该半开连接并回收资源。这种TCP连接管理协议为经典的DoS攻击——SYN洪水攻击(SYN flood attack)提供了条件。在这种攻击中,攻击者发送大量的TCP SYN报文段,而不完成第三次握手的步骤。随着这种SYN报文段纷至沓来,服务器不断为这些半开连接分配和释放资源,导致服务器的资源被消耗殆尽。这种SYN洪泛攻击是被记载的众多DoS攻击中的第一种。幸运的是,现在有一种有效的防御系统,称为SYN cookie,它们被部署在大多数主流操作系统中。SYN cookie以下列方式工作:
·当服务器收到一个SYN报文段时,它并不知道该报文段是来自一个合法的用户,还是一个SYN洪水攻击。因此服务器不会为该报文段生成一个半开连接。相反,服务器生成一个初始TCP序列号,该序列号是SYN报文段的源和目的IP地址与端口号和一个仅由服务器持有的一个秘密数字的一个复杂函数(散列函数)。这种精心制作的初始序列号被称为“cookie”。服务器则发送具有这种特殊初始序列号的SYN ACK分组。服务器并不记忆该cookie或任何对应于SYN的其他状态信息。
·如果客户是合法的,则它将返回一个ACK报文段。当服务器收到该ACK,需要验证该ACK与前面发送的某些SYN相对应。服务器借助cookie来做到这一点。前面讲过对于一个合法的ACK,在确认字段中的值等于在SYN ACK字段(此时为cookie值)中的值加1。服务器则将使用在SYN ACK报文段中的源和目的地IP地址与端口号(它们与初始的SYN中的相同)以及秘密数运行相同的散列函数。如果该函数的结果加1与在客户的SYN ACK中的确认(cookie)值相同的话,服务器认为该ACK对应于之前的SYN报文段,因此它是合法的。服务器则生成一个具有套接字的全开的连接。
·在另一方面,如果客户没有返回一个ACK报文段,则初始的SYN并没有对服务器产生什么影响,因为服务器没有为它分配任何资源。

可以用不同的机制来控制TCP报文段的发送时机。例如,第一种机制是TCP维持一个变量,它等于MSS。只要缓存中存放的数据达到MSS字节时,就组装成一个TCP段发送出去。第二种机制是由发送方的应用进程指明要求发送报文段,即TCP支持的推送(push)操作。第三种机制是发送方的一个计时器期限到了,这时就把当前已有的缓存数据装入报文段(但长度不能超过MSS)发送出去。
但是,如何控制TCP发送报文段的时机仍然是一个较为复杂的问题。
例如,一个交互式用户使用一条TELNET连接(运输层为TCP协议)。假设用户只发1个字符,加上20字节的首部后,得到21字节长的TCP报文段。再加上20字节的IP首部,形成41字节长的IP数据报。在接收方TCP立即发出确认,构成的数据报是40字节长(假定没有数据发送)。若用户要求远地主机回送这一字符,则又要发回41字节长的IP数据报和40字节长的确认IP数据报。这样,用户仅发1个字符时,线路上就需传送总长度为162字节共4个报文段。当线路带宽并不富裕时,这种传送方法的效率的确不高。因此应适当推迟发回确认报文,并尽量使用捎带确认的方法。

在TCP的实现中广泛使用Nagle算法:若发送应用进程把要发送的数据逐个字节地送到发送缓存,则发送方把第一个字节先发送出去,把后面的字节都缓存起来。当发送方收到对第一个字节的确认后,再把发送缓存中的所有数据组装成一个报文段发送出去,同时继续对随后到达的数据进行缓存。只有在收到对前一个报文段的确认后才继续发送下一个报文段。当数据到达较快而网络速率较慢时,用这样的方法可明显地减少所用的网络带宽。Nagle算法还规定,当到达的数据已达到发送窗口大小的一半或已达到报文段的最大长度时,就立即发送一个报文段。这样做,就可以有效地提高网络的吞吐量。

除了上述的发送方糊涂窗口综合征(silly window syndrome,SWS)以外,还有接收方糊涂窗口综合征,是TCP流量控制实现不良导致的一种计算机网络问题,有时也会使TCP的性能变坏。设想:TCP接收方的缓存已满,而交互式的应用进程一次只从接收缓存中读取1个字节(这样就使接收缓存空间仅腾出1个字节),然后向发送方发送确认,并把窗口设置为1个字节(但发送的数据报是40字节长)。接着,发送方又发来1个字节的数据(请注意,发送方发送的IP数据报是41字节长)。接收方发回确认,仍然将窗口设置为1个字节。这样进行下去,网络的效率很低。
要解决这个问题,可以让接收方等待一段时间,使得或者接收缓存已有足够空间容纳一个最长的报文段,或者等到接收缓存已有一半空闲的空间。只要出现这两种情况之一,接收方再发出确认报文,并向发送方通知当前的窗口大小。此外,也可以令发送方也不要发送太小的报文段,而是把数据积累成足够大的报文段,或达到接收方缓存的空间的一半大小。上述两种方法可配合使用,使得在发送方不发送很小的报文段的同时,接收方也不要在缓存刚刚有了一点小的空间就急忙把这个很小的窗口大小信息通知给发送方。
3.6 拥塞控制原理
若对网络中某一资源的需求超过了该资源所能提供的可用部分,网络的性能就要变坏。这种情况叫做拥塞(congestion)。
有人可能会说:“只要任意增加一些资源,例如把结点缓存扩大,或把链路速率提高,或把结点处理机速度提高,就可以解决拥塞。”其实不然。网络拥塞是一个非常复杂的问题。简单地采用上述做法,在许多情况下,不但不能解决拥塞问题,还会使网络的性能更坏。
网络拥塞往往是由许多因素引起的。例如,当某个结点缓存的容量太小时,到达该结点的分组因缓存已满而不得不被丢弃。加入将该结点的缓存扩展得非常大,于是到达该结点的分组均可在结点的缓存队列中排队,不受任何限制。由于输出链路的容量和处理机的速度并未提高,因此队列中的绝大多数分组的排队等待时间依然会大大增加,结果上层软件只好把它们进行重传(因为早就超时了)。由此可见,简单地扩大缓存的存储空间同样会造成网络资源的严重浪费,因而解决不了网络拥塞的问题。
又如,处理机处理的速率太慢可能引起网络的拥塞。将处理机速率提高,可能会使情况缓解,但往往又会将瓶颈转移到别处。问题的实质往往是整个系统的各个部分不匹配。只有所有的部分都配合好了,问题才会得到解决。
拥塞常常趋于恶化。如果一个路由器没有足够的缓存,它就会丢弃一些新到的分组。但当分组被丢弃时,发送这一分组的源点就会重传这一分组,甚至重传多次。这会引起更多的分组流入网络和被网络中的路由器丢弃。可见拥塞引起的重传并不会缓解网络的拥塞,反而会加剧网络的拥塞。
拥塞控制与流量控制的关系密切,它们之间也存在着一些差别。所谓拥塞控制就是防止过多的数据注入到网络中,这样可以使网络中的路由器或链路不致过载。拥塞控制遵照一个前提工作:网络能够承受现有的负荷。拥塞控制是一个全局性的过程,涉及到所有的主机、所有的路由器,以及与降低网络传输性能有关的所有因素。TCP连接的端点只要迟迟不能收到对方的ACK,就猜想在网络中的某处发生了拥塞,但无法知道拥塞到底发生在网络的何处,也无法知道发生拥塞的具体原因。是访问某个服务器的通信量过大?还是在某个地区出现自然灾害?发送端和接收端都得不到答案。
相反,流量控制往往是指点对点通信量的控制,是个端到端的问题(接收端控制发送端)。流量控制所要做的就是抑制发送端发送数据的速率,以便使接收端来得及接收。
拥塞控制和流量控制之所以常常被弄混,是因为某些拥塞控制算法是向发送端发送控制报文,并告诉发送端,网络已出现麻烦,必须放慢发送速率。这点又和流量控制是很相似的。

我们先来看拥塞的常见原因及其代价。
原因1:中间链路速率瓶颈,或者说,发送方的发送速率过高。
从发送端到接收端的整条链路的速率等于最慢的那一段链路的速率。如果发送方的速率高于瓶颈链路的速率,数据包就会积压在瓶颈链路指向的路由器中。如果发送方的传输速率始终高于瓶颈链路的速率,那么这台路由器就会积累越来越多的包,造成大量的排队延迟。
原因2:丢包导致重传,增大了整条链路的流量。
如果中间某个路由器的缓冲区不够大,那么就有可能导致其缓冲区溢出,于是有部分数据就丢失了。发送方侦测到部分包丢失后,就会重发丢失的包,于是整条链路的流量就更大了。
并且,发送方判为已丢失的包实际上可能堵在中间的某个路由器的缓冲区中,这种误判会导致接收方接收到丢失的包的额外副本,而接收方只会保留其中一份数据,其余的冗余分组都会被丢弃。这种情况下,重传造成了额外的浪费。
原因3:不同链路间会争抢路由器的资源。
同一台路由器通常处理多条链路。对经过路由器的某条链路,如果其速率被之前的链路拖慢,那么其余的链路把数据传输给路由器的速率就相对更快,于是就会分到该路由器的更多资源,包括出站链路的速率和缓冲区容量。在这时候,如果速率较低的链路积压在路由器中的数据因为缓冲区溢出而丢失,那么前面的传输也就白费了。

有很多的方法可用来监测网络的拥塞。主要的一些指标是:由于缺少缓存空间而被丢弃的分组的百分数(丢包率)、平均队列长度、超时重传的分组数、平均分组时延、分组时延的标准差,等等。上述这些指标的上升都标志着拥塞的增长。

实践中,拥塞控制方法常分为两类:
•端到端拥塞控制。此类方法中,网络层没有为运输层拥塞控制提供显式支持。即使存在拥塞,端系统也必须通过对网络行为的观察(如分组丢失与时延)来推断。在3.7节将看到,TCP采用端到端的方法解决拥塞控制,因为IP层不会向端系统提供有关网络拥塞的反馈信息。TCP报文段的丢失(超时或冗余ACK)被认为是拥塞的迹象,TCP将减小窗口长度。关于TCP拥塞控制的一些最新建议包括:使用增加的RTT值作为网络拥塞程度增加的指示。
•网络辅助的拥塞控制。此类方法中,路由器向发送方提供关于拥塞的显式反馈信息。这种反馈可以仅用1位指示链路是否拥塞。该方法在早期常常被采用。更复杂的网络反馈也是可以的。例如,在ATM可用比特率(Available Bit Rate,ABR)拥塞控制中,路由器显式地通知发送方它(路由器)能在输出链路上支持的最大主机发送速率。默认版本的IP和TCP采用端到端拥塞控制方法。然而,IP和TCP也能选择性实现网络辅助拥塞控制。
对于网络辅助的拥塞控制,拥塞信息从网络反馈到发送方通常有两种方式。直接反馈信息可以由路由器发给发送方。这种方式的通知通常采用阻塞分组(choke packet)的形式。更为通用的第二种形式是,路由器标记从发送方流向接收方的分组中的某个字段来指示拥塞的产生。一旦收到一个标记的分组,接收方就向发送方通知网络发生了拥塞。后一种形式至少要消耗一倍完整的RTT。

进行拥塞控制需要付出代价。首先需要获得网络内部流量分布的信息。在实施拥塞控制时,还需要在结点之间交换信息和各种命令,以便选择控制策略并实施控制。这样就产生了额外开销。拥塞控制有时需要将一些资源(如缓存、带宽等)分配给个别用户(或一些类别的用户)单独使用,这样就使得网络资源不能更好地实现共享。十分明显,在设计拥塞控制策略时,必须全面衡量得失。
实践证明,拥塞控制是很难设计的,因为它是一个动态的而不是静态的问题。当前网络正朝着高速化的方向发展,这很容易出现缓存不够大而造成分组的丢失。但分组的丢失是网络发生拥塞的征兆而不是原因。在许多情况下,甚至正是拥塞控制机制本身成为引起网络性能恶化甚至发生死锁的原因。这点应特别引起重视。
3.7 TCP拥塞控制
运行在发送方的TCP拥塞控制机制维护一个额外的变量,即拥塞窗口(congestion window)。发送方中未被确认的数据量不超过拥塞窗口和接收窗口的最小值。即:接收窗口长度 < 拥塞窗口长度时,接收方的接收能力限制发送窗口的最大值。而接收窗口长度 > 拥塞窗口长度时,网络的拥塞限制发送窗口的最大值。
如果发送方与目的地之间的路径上出现了拥塞,中间链路丢弃了数据报(包含TCP段),那么会因为数据丢失而引发超时或收到三个冗余ACK。发送方据此可以判别拥塞程度。
如果TCP正常收到ACK消息,那么拥塞窗口的大小会增加。收到正确的ACK的速率越高,窗口扩大越快。TCP使用确认
来触发(trigger,或计时,clock)增大它的拥塞窗口长度,因此我们说TCP是自计时(self-clocking)的。
TCP拥塞控制中,网络里没有明确的拥塞状态信令,ACK和丢包事件充当了隐式信号,且每个TCP发送方各自控制速率。

TCP拥塞控制算法(TCP congestion-control algorithm)已经写入RFC 5681。该算法包括3部分:①慢启动;②拥塞避免;③快速恢复。慢启动和拥塞避免是TCP的强制部分,两者的差异在于对收到的ACK做出反应时增加拥塞窗口长度的方式。慢启动比拥塞避免能更快增加窗口大小(不要被名称所迷惑!)。快速恢复是推荐部分,对TCP发送方不是必需的。

1、慢开始(slow-start)。
当一条TCP连接开始时,拥塞窗口大小通常置为一倍SMSS(发送方最大报文段大小)的较小值,初始发送速率可以按MSS / RTT估计。
在旧标准中,拥塞窗口大小一般为1到2倍SMSS。新的RFC 5681标准把初始拥塞窗口(cwnd)设置为不超过2至4个SMSS的数值。具体的规定如下:
若SMSS > 2190字节,则设置初始拥塞窗口cwnd = 2×SMSS字节,且不得超过2个报文段。
若 (SMSS > 1095字节) 且 (SMSS≤2190字节) ,则设置初始拥塞窗口cwnd = 3×SMSS字节,且不得超过3个报文段。
若SMSS≤1095字节,则设置初始拥塞窗口cwnd = 4×SMSS字节,且不得超过4个报文段。
可见这个规定就是限制初始拥塞窗口的字节数。

对TCP发送方而言,可用带宽可能比初始限速大得多,TCP发送方希望迅速找到可用带宽的数量。因此,在慢启动状态,拥塞窗口cwnd的值以1个SMSS开始;并且,每当传输的报文段首次被确认,就增加1个SMSS。第一个RTT(或者说:第一个传输轮次)后,如果未拥塞,则允许一次性发送2倍SMSS的数据量。又一个RTT后,如果未拥塞,则允许一次性发送4倍SMSS的数据量(收到的2个ACK各贡献1倍SMSS)。于是每过一个RTT,发送速率就翻番。此外,拥塞窗口cwnd的增加也受到原先未被确认的、但现在被刚收到的确认报文段所确认的字节数N的影响。即
Δcwnd=min⁡(N, SMSS)
但是,何时结束这种指数增长呢?慢启动对这个问题提供了几种答案。
首先,如果出现一次超时,TCP发送方将另一状态变量——慢启动阈值SST设为拥塞窗口长度cwnd的一半,然后将拥塞窗口长度cwnd设为单倍SMSS,并重新开始慢启动过程。
第二种方式是直接与预设的慢启动阈值SST相关联。因为当检测到拥塞时,慢启动阈值SST设为原拥塞窗口大小cwnd的一半,当拥塞窗口大小到达或超过慢启动阈值时,继续使其翻倍可能有些鲁莽。因此,当cwnd = SST时,慢启动结束,TCP转移到拥塞避免模式。进入拥塞避免模式后,TCP更为谨慎地增加拥塞窗口大小。
最后一种结束慢启动的方式是,如果检测到3个冗余ACK,TCP快速重传并进入第三部分,即快速恢复状态。后面将讨论相关内容。

有时,个别报文段会在网络中丢失,但实际上网络并未发生拥塞。如果发送方迟迟收不到确认,就会产生超时,就会误认为网络发生了拥塞。这就导致发送方错误地启动慢开始,把拥塞窗口cwnd又设置为1×SMSS,因而降低了传输效率。
采用快速重传可以让发送方尽早知道发生了个别报文段的丢失。快重传首先要求接收方不要等待自己发送数据时才进行捎带确认,而是要立即发送确认,即使收到了失序的报文段也要立即发出对最后一个按序报文段的重复确认。算法规定,发送方只要一连收到3个重复确认,就判定接收方没有收到相应的报文,因而应立即重传,这样就不会出现超时,发送方也不会误认为出现了网络拥塞。使用快重传可以使整个网络的吞吐量提高约20%。

2、拥塞避免。
一旦进入拥塞避免状态,拥塞窗口大小大约是原先的一半。因此,TCP将不会每过一个RTT再将窗口大小翻番,而是采用较为保守的方法,每个RTT只将窗口大小增加一个SMSS。这能够以几种方式完成。
一种通用的方法是:TCP发送方无论何时收到一个新的确认,都将拥塞窗口大小增加(SMSS / cwnd)倍SMSS。例如,如果SMSS是1460字节,cwnd是14600字节,则在一个RTT内发送10个报文段,每个到达ACK(假定每个报文段一个ACK)增加1 / 10 SMSS的窗口长度,因此在收到对所有10个报文段的确认后,拥塞窗口的值增加了一个SMSS。
但是何时结束这种线性增长(每RTT一个SMSS)呢?
当超时以后,与慢启动的情况一样,cwnd设为1倍SMSS,慢启动阈值SST被更新为原cwnd的一半;然后,回到慢启动状态。
然而,丢包事件也能由三次冗余ACK触发。在这种情况下,TCP的反应不那么剧烈:TCP将慢启动阈值SST记为cwnd的一半,然后将cwnd砍半再加上3个SMSS。接下来进入快速恢复状态。这样做的理由是:既然发送方收到3个重复的确认,就表明有3个分组已经离开了网络。这3个分组不再消耗网络的资源而是停留在接收方的缓存中(接收方发送出3个重复的确认就证明了这个事实)。可见网络中并不是堆积了分组而是减少了3个分组,因此可以适当把拥塞窗口扩大。
不过,必须强调,“拥塞避免”并非指完全能够避免了拥塞。利用以上的措施要完全避免网络拥塞还是不可能的。

3、快速恢复。
在快速恢复中,对收到的每个引起TCP进入快速恢复状态的缺失报文段的冗余ACK,cwnd的值增加1倍SMSS。最终,当对丢失报文段的一个ACK到达时,TCP在降低cwnd后进入拥塞避免状态。如果出现超时,快速恢复在执行与慢启动和拥塞避免中相同的动作后,迁移到慢启动状态:当丢包事件出现时,cwnd的值被设置为1个SMSS,并且慢启动阈值设为cwnd的一半。
快速恢复是TCP推荐的而非必需的构件。TCP Tahoe作为TCP早期版本,不管是发生超时指示的丢包事件,还是发生3个冗余ACK指示的丢包事件,都无条件地将其拥塞窗口减至1个SMSS,并进入慢启动阶段。TCP的较新版本TCP Reno,则引入了快速恢复。

使用TCP拥塞控制机制时,拥塞窗口常常线性扩大,然后减半,再线性扩大,再线性减半(有的TCP版本实现的是减半再加3)……因此TCP拥塞控制被称为加性增、乘性减(Additive-Increase,Multiplicative-Decrease,AIMD)拥塞控制。将窗口大小对时间画成图表,曲线的大多数部分都呈锯齿状。

Reno算法的许多变种也层出不穷。TCP Vegas算法试图在维持较好的吞吐量的同时避免拥塞。Vegas的基本思想是:①在分组丢失发生之前,在源与目标之间检测路由器中的拥塞;②当检测出快要发生的分组丢失时,线性降低发送速率。快要发生的分组丢失是通过观察RTT来预测的。分组的RTT越长,路由器中的拥塞越严重。2015年年底,Ubuntu的TCP实现默认提供了慢启动、拥塞避免、快速恢复、快速重传和SACK,也提供了诸如TCP Vegas和BIC等其它拥塞控制算法。

TCP AIMD算法基于大量的工程见解和拥塞控制经验而开发。在TCP研发后的十年,理论分析显示,TCP的拥塞控制算法作为一种分布式异步优化算法,使得用户和网络性能的几个重要方面都同时得到了提升。此后,拥塞控制的理论得到了长足的发展。

如果使用TCP进行长时间的传输,比如将一张UHD BD备份到NAS(网络附属存储)或网盘,那么我们来简单估计一下平均的传输速率。
因为慢启动阶段相对很短(传输速率指数增长后很快离开此状态),因此我们忽略它。如果拥塞窗口长度为w,往返时间为RTT,那么传输速率可以按w/RTT估计。回想一下锯齿状变化的传输速率曲线,如果w=W时发生丢包导致拥塞窗口长度减半,那么容易得出:速率从W/RTT骤降到W/(2·RTT) ,再线性上升,如此往复。所以一条TCP连接的平均吞吐量的估计值是:
3W/(4·RTT)
除了这个极其简单的估计,还有许多根据经验建立且与实际数据符合得很好的模型。由于篇幅所限,这里略去。

TCP拥塞控制已经演化了多年并仍在继续演化。我们来看一个在高带宽场景下使用TCP的例子。
假设每个TCP报文段的长度为1500字节,某条10 Gbps链路的RTT为100 ms。为了取得10 Gbps吞吐量,平均拥塞窗口长度至少不低于83333个报文段。对如此大量的报文,如果出现丢包,会怎样呢?或者以另一种方式说,这些传输的报文段最多能丢失多少,使得上述TCP拥塞控制算法仍能取得所希望的10 Gbps速率?在课后习题中,要求你推导出一条TCP连接的吞吐量公式:(丢包率(P)、往返时间(RTT)和最大报文段长度(MSS))
Average Throughput=(1.22·MSS)/(RTT√P)
通过此公式可得:为了保证10 Gbps的吞吐量,拥塞控制算法必须将丢包率控制在约2×10^(-10)以内。这要求TCP必须针对高速环境予以改进。

TCP的AIMD算法公平吗?尤其是假定可在不同时间启动并因此在某些时刻可能具有不同的窗口长度情况下,对这些不同的TCP连接还是公平的吗?TCP趋于在竞争的多条TCP连接之间提供对瓶颈链路带宽的平等分享。
我们考虑有两条TCP连接共享一段传输速率为R的链路的简单例子,如下图。假设这两条连接有相同的SMSS和RTT(这样如果它们有相同的拥塞窗口长度,就会有相同的吞吐量),它们有大量的数据要发送,且没有其它TCP连接或UDP数据报穿越该段共享链路。同样忽略TCP的慢启动阶段,并假设TCP连接一直按CA模式(AIMD)运行。
参见图线。横轴和纵轴分别代表连接1好2的吞吐量。如果点落在射线r: y=x, x>0上,就意味着两个连接都分配到了相等的正吞吐量。如果某个时刻点没有落在射线r上,即两个连接分到的吞吐量不等,那么:
【1】如果它们的总吞吐量小于瓶颈链路的总带宽R,即点在线段l: y=-x+R, x,y>0下方,那么连接1和2的吞吐量都将按照每个RTT一倍SMSS的速率增长。由于Δx=Δy,所以点会沿平行于的方向逐渐移到线段上方。此时意味着两个连接的总速率已经超过了瓶颈链路允许的速率,将发生丢包。
【2】一旦它们的总吞吐量达到或大于瓶颈链路的总带宽R,即点在线段l上或其上方,那么拥塞控制算法会即刻将两个连接的吞吐量都减半,即从当前点向原点移动一半的距离。
经过数次【1】【2】的交替,点将越来越接近射线r: y=x, x>0,即两个连接的吞吐量之差将越来越小。可见,TCP拥塞控制算法是尽量确保公平性的。

当然,这只是一种非常理想化的情况。实践中,每条TCP连接的各个参数都可以不一样,上面的情况就不成立了。当多条连接共享一条的瓶颈链路时,具有较小RTT的连接能够在链路空闲时更快抢到可用带宽(较快扩大其拥塞窗口),因而将比那些具有较大RTT的连接享用更高的吞吐量。

我们已经看到TCP拥塞控制是如何通过拥塞窗口调节传输速率的。许多多媒体应用如Internet电话和视频会议,经常因为这一点而不在TCP上运行,因为它们不想传输速率被扼制,即使在网络非常拥堵时。相反,这些应用宁可在UDP上运行,UDP是没有内置的拥塞控制的。当运行在UDP上时,这些应用能够以恒定的速率将其音频和视频数据注入网络并偶尔丢失分组,而不在拥塞时将其发送速率降至“公平”级别并不丢失任何分组。从TCP的角度看,运行在UDP上的多媒体应用是不公平的,因为它们不与其它连接合作,也不适时调整传输速率。TCP拥塞控制在面临拥塞增加(丢包)时,将降低其传输速率,而UDP源则不这样做,这就导致UDP源可能压制TCP流量。当今的一个主要研究领域就是开发一种Internet中的拥塞控制机制,用于阻止UDP流量不断压制直至中断其它Internet流量的情况。

即使我们迫使UDP流量具有公平的行为,公平性问题也仍未完全解决,因为没有办法阻止基于TCP的应用使用并行连接。例如,Web浏览器通常使用多个并行TCP连接来传送一个Web页中的多个对象(连接数目一般可在浏览器中进行配置)。当一个应用使用多条并行连接时,它占用了一条拥塞链路中较大比例的带宽。举例来说,有一段速率为R且支持9个在线C-S应用的链路,每个应用使用一条TCP连接。如果一个新的应用加入了,也使用一条TCP连接,则每个应用得到差不多相同的传输速率R/10。但是如果这个新应用使用了11个并行TCP连接,则这个新应用就不公平地分到超过一半的带宽。Web流量在Internet中是非常普遍的,所以多条并行连接并非不常见。

对于IP和TCP的扩展方案RFC 3168已经提出并已实现和部署,该方案允许网络明确向TCP发送方和接收方发出拥塞信号。这种形式的网络辅助拥塞控制称为显式拥塞通知(Explicit Congestion Notification,ECN),涉及TCP和IP协议。
在网络层,IP数据报首部的服务类型字段中的两个比特用于ECN。ECN位可以用于指示该路由器正面临拥塞。该拥塞指示由被标记的IP数据报所携带,送给目标主机,再由目标主机通知发送主机。方案标准没有提供路由器拥塞的定义:该判断由路由器厂商所做的配置选择,并由网络操作员决定。然而,标准推荐仅当拥塞持续时才设置ECN位。发送主机也可以在ECN位(向路由器)表明:收发双方是ECN兼容的,可以对于ECN指示的网络拥塞采取行动。
当接收主机中的TCP接收到的数据报包含ECN拥塞指示时,它通过在接收方到发送方的TCP ACK报文段中设置ECE(显式拥塞通知回显)位,通知发送主机中的TCP,当前网络拥塞。接下来,发送方减半拥塞窗口大小,并在下一个将要发送的报文首部中将CWR(拥塞窗口缩减)位置位。其它运输层协议也可以利用网络层发送ECN信号。数据报拥塞控制协议(Datagram Congestion Control Protocol,DCCP)提供了一种低开销、可控制拥塞的、类似于UDP的不可靠服务,该协议利用了ECN。DCTCP(数据中心TCP)是一种专门为数据中心网络设计的TCP版本,也利用了ECN。

诸如搜索、电子邮件和社交网络等云服务,传输和处理的数据量非常大。而用户又经常位于远离数据中心的地方,这些数据中心负责为云服务关联的动态内容提供服务。实际上,如果端系统远离数据中心,那么RTT将会很大,TCP慢启动将潜在地导致较长的响应时间。
以搜索为例。通常,服务器在慢启动期间交付响应要求三个TCP窗口。所以从某端系统发起一条TCP连接到它收到该响应的最后一个分组的时间粗略是4RTT(用于建立TCP连接的1个RTT加上用于3个数据窗口的3个RTT),再加上在数据中心处理的时间。对于大量的查询来说,这些RTT导致其返回搜索结果中显而易见的时延。此外,在接入网中可能有较大的丢包率,导致重传和更大的时延。
缓解这个问题和改善用户体验的一个途径是:①部署邻近用户的前端服务器;②在该前端服务器利用TCP分岔(TCP splitting)来分裂TCP连接。借助于TCP分岔,客户向邻近前端连接一条TCP连接,并且该前端以非常大的窗口向数据中心维护一条TCP连接。使用这种方法,响应时间大致变为
4RTTFE+RTTBE+处理时间
RTTFE是客户与前端服务器之间的往返时间,RTTBE是前端服务器与数据中心(后端服务器)之间的往返时间。如果前端服务器邻近客户,则该响应时间大约变为RTTBE加上处理时间,因为RTTFE小得微不足道并且RTTBE约为RTT。总而言之,TCP分岔大约能够将网络时延从4RTT减少到RTT,极大地改善用户体验,对于远离数据中心的用户更是如此。TCP分岔也有助于减少因接入网丢包引起的TCP重传。Google和Akamai在接入网中广泛为它们的云服务使用TCP分岔。

猜你喜欢

转载自blog.csdn.net/COFACTOR/article/details/109574058
今日推荐