admin管理员组

文章数量:814268

【2021

计算机网络

知 识 梳 理

(第一版)

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


链接:
提取码:0000

三 运输层

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

在第4章和第5章将学习网络层,这里只简要介绍。Internet网络层的协议称为Internet协议(IP,网际协议)。
前面讲过,运输层为进程间的通信提供服务,而网络层负责在主机之间传输分组。IP的服务模型是尽力而为交付服务(best-effort delivery service):IP虽然尽最大的努力在通信的主机之间交付报文段,但并不提供任何保证:它不确保报文段的成功交付,也不确保报文段的按序交付,更不保证报文段的数据完整性。因此,IP是不可靠服务(unreliable service)。此外,每台主机至少有一个网络层地址,即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。这些端口的使用是受限的,因为它们要留给HTTP(端口80)和FTP(端口21)等常用应用层协议。开发一个新的需要访问网络的应用程序时,必须为其分配一个端口号:可以自动分配,也可以手动指定。

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

UDP套接字是由一个2元组 <目标IP地址,目标端口号> 标识的。如果两个UDP报文段具有不同的源IP地址和 / 或源端口,但具有相同的目标IP地址和目标端口,那么这两个报文段将通过相同的目标套接字被定向到相同的目标进程。
TCP套接字是由一个4元组 <源IP地址,源端口号,目标IP地址,目标端口号> 来标识的。因此,当TCP报文段到达主机时,该主机使用全部4个值来将报文段传递给相应的套接字。同一主机接收到的两个具有不同源IP地址或源端口号的TCP报文段,总是被定向到两个不同的套接字,除非TCP报文段携带了初始连接创建请求。顺带一提,socket这个词,也可能表示别的意思。例如:
(1)允许应用程序访问网络的API,即运输层和应用层之间的一种接口,称为socket API,有的资料可能将其简称为socket。
(2)在socket API中使用的一个函数名也叫做socket。
(3)调用socket函数的端点称为socket,如“创建一个数据报socket”。
(4)调用socket函数时,其返回值称为socket描述符,可简称为socket。
(5)在操作系统内核中连网协议的Berkeley实现,称为socket实现。
上面的这些“socket”的意思,都和本章所引用的RFC 793定义的socket不同。请读者加以注意。一个服务器进程会打开一些端口等待客户的请求。某些端口为常用应用(Web、FTP、DNS和SMTP服务器等)所预留;按照惯例,还有其它端口由热门的应用程序(例如,SQL Server 2000在UDP端口1434上监听请求)使用。因此,如果我们确定一台主机上打开了一个端口,就能确定该主机正在运行一个特定的应用程序。这对系统管理员非常有用:他们通常希望知晓有哪些网络应用程序运行在他们的网络的主机上。攻击者为了寻找突破口,也需要知道在目标主机上有哪些端口打开。如果发现一台主机正在运行具有已知漏洞的应用程序(例如,在端口1434上监听的SQL Server不免疫缓冲区溢出攻击,使远程用户能执行任意代码,这是一种由Slammer蠕虫利用的缺陷),那么攻击该主机的条件就已经成熟。
确定哪个应用程序正在监听哪些端口很容易。端口扫描器做的正是这种事。使用最广泛的工具之一是nmap,可在,并且它已集成在大多数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服务模型并不特别适合这些应用。这些应用可以使用UDP,并在应用层实现UDP不提供但程序又需要的额外功能。
·无需建立连接。TCP在传输之前需要三次握手;UDP无需任何准备即可开始传输,没有建立连接的时延。这是DNS运行在UDP而不是TCP上的主要原因:若运行在TCP上,则DNS会慢得多。HTTP使用TCP而非UDP,因为对具有文本的Web网页来说,可靠性至关重要。但是,TCP连接建立时延对于下载Web文档的影响比较大。Chrome浏览器中的QUIC(参见3.8节)协议将UDP作为运输协议,并在应用层实现可靠性。
·无需追踪连接状态。TCP需要在端系统中维护连接状态,包括接收和发送缓存、拥塞控制参数以及序号与确认号(见后文)的参数。要实现TCP的可靠数据传输并提供拥塞控制,这些状态信息是必要的。UDP不维护连接状态,也不跟踪这些参数。因此,当应用程序运行在UDP而不是TCP上时,服务器就能同时服务更多客户端。
·分组首部开销小。每个TCP报文段至少有20字节的首部开销,而UDP仅有8字节的开销。此外,再提一下UDP的其它特点:
·UDP尽最大努力交付。即:不保证可靠交付,因此主机不需要维持复杂的连接状态(这里面有许多参数)。
·UDP是面向报文的。发送方UDP对应用程序交下来的报文,在添加报头后,就向下交付IP层。UDP对应用层交下来的报文,既不合并,也不拆分,而是保留这些报文的边界。这就是说,应用层交给UDP多长的报文,UDP就照样发送:一次发送一份完整的报文。在接收方的UDP,对IP层交上来的用户数据报,在去除首部后,就原封不动地交付上层的应用进程:一次交付一份完整的报文。因此,应用程序必须选择合适大小的报文。若报文太长,UDP把它交给IP层后,IP层在传送时就要分片(见4.3节),这会降低IP层的效率。反之,若报文太短,UDP把它交给IP层后,会使IP数据报报头的相对长度太大,这也降低了IP层的效率。
·UDP支持一对一、一对多、多对一和多对多的交互通信。
·UDP没有拥塞控制。网络出现的拥塞不会使源主机的发送速率降低,很适合多媒体通信等场景。2.1节给出了常见Internet应用使用的协议。邮件、TELNET、Web及文件传输都运行在TCP上,因为它们都要求传输可靠。有很多重要的应用运行在UDP而不是TCP上。例如,简单网络管理协议(SNMP,5.7节)。在这种场合下,UDP要优于TCP。因为网络管理程序通常在网络处于重压状态时运行。这时候,如果控制与管理仍要额外花费较多的网络流量,它们也就失去了意义。DNS运行在UDP之上,从而避免了TCP的额外开销。
UDP和TCP都用于多媒体应用,如Internet电话、实时视频会议、流式影音。这些应用都能容忍少量的分组丢失,因此可靠数据传输不太重要。此外,TCP拥塞控制会导致Internet电话、视频会议之类的实时应用的性能变得很差。因此,多媒体应用开发人员通常将这些应用运行在UDP上。出于安全原因(参见第8章),某些机构会禁止UDP流量。对于流式媒体传输来说,TCP变得越来越有吸引力了。在UDP上运行多媒体应用依然存在争议。UDP没有拥塞控制。但面对诸如大量用户同时传输高码率视频等情况,若不使用任何拥塞控制,则会使路由器出现大量的分组溢出,以至于只能成功传送非常少的UDP分组。并且,由无控制的UDP发送的大量数据将引起TCP发送方(TCP遇到拥塞将降低发送速率)大大减小它们的速率。因此,缺乏拥塞控制导致了UDP的高丢包率,并挤垮TCP会话,这是一个潜在的严重问题。很多研究人员提出了新机制,促使所有的数据源(含UDP源)执行自适应的拥塞控制。
使用UDP的应用也可以实现可靠数据传输:这要通过应用程序自身建立可靠性机制来完成(例如,增加确认与重传机制)。前面说过,Chrome使用QUIC协议在UDP之上的应用层协议中实现了可靠性。但这样开发的工作量很大。无论如何,这种做法能令鱼与熊掌兼得。也就是说:应用进程可以进行可靠通信,同时无须受制于TCP拥塞控制强加的传输速率限制。UDP报文段结构如图所示,由RFC 768定义。应用层数据位于数据字段。对于DNS,数据字段是一份查询或响应报文。对于流式音频应用,音频片段填充到数据字段。UDP报头只有4个字段,每个2字节:首先是源端口号与目标端口号。当无需对方回信时,源端口可以填零。其次是长度字段,指示了UDP报文段的字节数(报头加数据,最小值为8,即仅有报头)。数据字段的长度可以不同,所以需要专门表示出来。然后是校验和(checksum),接收方使用它检查传输是否出错。
源端口号的用途,是作为返回地址的一部分。目标主机需要回发一个报文段给源主机时,可以从已经接收的报文直接得到源主机的地址。UDP具有一定的差错检测功能。
校验和有16位,其计算方法是:先将校验和字段填零,然后将报头与数据以16位为单位累加起来。每当最高位有进位,就加到最低位。最后取反。校验结果由发送方填入报头的校验和字段。 
计算检验和时,要在用户数据报前增加12字节的伪报头。伪报头并不是用户数据报真正的报头,只是在计算检验和时,临时添加在用户数据报前面,得到一份临时的用户数据报。检验和就是按照临时的用户数据报来计算的。伪报头既不向下传送也不向上递交,而仅仅是为了计算检验和。下图给出了伪报头各字段的内容。若用户数据报的数据部分不是偶数个字节,则要填入1个全零字节(此字节也不发送)。
接收方将伪报头、报头(包括发送方给出的校验和)和数据(包括补齐为偶数字节使用的全零字节)累加。在数据没有出错时,相加结果一定是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协议中,有3种机制处理位错误:
·错误检测。将传输的数据划分成若干等份,每份增加额外的字节,称为校验码或校验和,再一并发送出去。
·接收反馈。接收方成功接收后,发送确认(acknowledge,ACK)消息给发送方,确认该段数据已接收。
在未能成功接收时,也可以发送否定确认(negative acknowledge,NAK)消息。不过,由于这样处理会使协议复杂化,许多可靠传输协议都不使用否认报文了。
·重传。接收方收到有差错的分组时,发送方将重传该分组。
等待接收方的ACK消息时,发送方一般不发送新的数据——这样的协议称为停止-等待(stop-and-wait)协议。当然,现在的可靠传输协议远远不止这么简单了。

ACK或NAK消息也有可能受损或丢失。几乎所有现有的数据传输协议中,都通过为发送数据编号来避免此问题。在数据分组中添加新字段,发送方对其分组编号:将发送数据分组的序号(sequence number)放在该字段。于是,接收方只需要检查序号,即可确定哪些分组对应哪些ACK或NAK分组。ACK或NAK分组本身没有编号。
对每个发送出去的数据包,发送方都等待一段时间。超过一定时间还未接收到对应的ACK信息时,将其重发。如果一个协议实现了上述机制,那么它的可靠性已经比较高了。但是,它在性能上存在重大问题。如果总是在收到一个包的ACK消息以后才发送下一个包,那么信道的利用率(有效吞吐量与理论吞吐量之比)会非常低。即

U=v_e/v_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, v_t=1 Gbps, d_trans=L/v_t
解得:有效吞吐率仅为v_e≈267 “kbps” ,利用率低至U=0.027 %。这对这条1 Gbps的链路是极大的浪费。如果还考虑排队延迟、处理延迟和低层协议的开销等因素,信道利用率只会更低。
为了提高利用率,引入流水线(pipelining)技术:在等待某个包的ACK消息时,发送方就可以先行发送后续的数据包,而无需等待每条ACK消息经过长长的链路返回到发送方之后,再发送下一个包。显然,这极大减少了传播时延的影响。
不使用流水线传输时,甚至可以只用1位来为一段时间内的分组编号。但使用流水线传输以后,很明显,假如仍然只使用1位,那么编号冲突几乎变成必然。这就需要扩大编号占用的位数。发送方也需要准备一定的缓存,用于临时存储未收到ACK消息的分组。当然,此种情况下,接收方或许也需要缓存分组,这将在下面讨论。

回退N步(Go-back-N)协议中,允许发送方发送多个分组,而不需等待之前的分组的ACK消息。但这受限于在流水线中未确认的分组数,它不能超过某个最大允许的值N。
如图,N称为发送窗口(通知窗口)大小。GBN协议是一种滑动窗口协议(sliding-window protocol)。蓝色部分是已确认的分组,浅蓝色部分是已发送但未确认的分组,灰色部分是可以开始发送的分组,白色部分是还不允许先行发送的分组。若窗口内的N个分组未发送,则可以提前发送,而无需等待之前的分组的ACK;若已经发送,则继续等待其ACK消息。窗口总是从左向右(时间流逝方向)滑动(因为不允许撤销已经收到的确认)。一旦已发送而未确认接收的包超时,这个包就需要重发。由于各种干扰,分组常常不会按原有顺序到达接收方。
发送窗口的前沿(右侧)也可能向后收缩。这发生在对方提供的接收窗口缩小了的情况。但TCP标准强烈不赞成这样做。因为发送方在收到这个通知以前,很可能已经发送了窗口中的许多数据,现在又要收缩窗口,不让发送这些数据,这样就会产生一些错误。后续的讨论中,不考虑发送窗口不移动而其前端往时间流逝的反方向收缩的情形。
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及n+1最终会被发送方重传。于是,接收方只需丢弃分组n+1即可。
这种方法的优点是:接收缓存简单,无需缓存任何失序分组;并且,即使ACK信息丢失,通常也不会引起发送方重发数据:只要之后发送方及时收到一条ACK消息,就能一次性确认之前没有确认的分组了。
当然,此方法的缺点是:如有分组丢失或出错,往往需要更多的重传。例如,如果发送方发送了前5个分组,而中间的第3个分组丢失了。这时接收方只能对前2个分组发出确认。发送方无法知道后3个分组的下落,而只好把这3个分组都重传一次。当通信线路质量不好时,这会带来显著的负面影响。GBN协议是使用流水线机制的。如果窗口长度或带宽时延积较大,就会令链路上存在许多尚未接收的分组。这时,更容易因为单个分组出错而引起大量分组重新发送。解决此问题需要选择重传(Selective Repeat,SR)协议。这要求接收方为每个正确接收的分组都发送一条ACK消息。接收方还需要缓存正确接收的分组,以确保乱序到来的分组原序交给上层。失序的分组将被缓存,直到所有丢失分组(序号更小)皆被收到为止。如果接收应用程序能够及时从接收缓存中读取收到的数据,接收窗口(可用窗口)就可以增大,但最大不能超过接收缓存的大小。总的来说,发送方需要应对的情况有:
1、从上层接收到数据。SR发送方检查下一个可用于该分组的序号。如果序号位于发送窗口内,则将数据打包并发送;否则就像在GBN中一样,要么将数据缓存,要么将其返回给上层,以后再传输。
2、超时。每个分组必须拥有单独的定时器,因为要确保超时不引起其它已经确认接收的分组一同重新发送。可以使用单个专门的硬件定时器模拟多个逻辑定时器的操作。
3、收到ACK。若该分组序号在窗口内,则发送方将此分组记为已接收。如序号还位于窗口最左侧,则窗口前(右)移,使最左侧与最小序号的未确认分组重合。窗口移动后,如有未发送的分组进入窗口,就将其发送。接收方使用接收窗口来决定哪些包允许被接收。接收方需要应对的情况有:
1、窗口内的若干个分组被正确接收。此时需要准备相应的ACK回应发送方,然后缓存该分组(若已经缓存过,则直接丢包)。若窗口最左侧的未接收分组被正确接收,则窗口前移,同时按序向上层交付被移出窗口的分组。
2、在当前窗口位置左侧一倍窗口大小的范围内的N个编号的包被接收到(这些包已经被正确接收过,因此新收到的副本也被丢弃),则需要重新产生ACK并传回给发送方。
3、其它情形,直接丢包。第2步是必要的,因为在接收方的窗口最左侧的未接收分组被正确接收后,窗口就会移动,这可能会导致接收方的窗口比发送方的窗口提前移动。这时候,如果发送方意外没有收到之前的包的ACK,就会发生重传。如果接收方再次接收到这些包后不重新发送ACK给发送方,发送方的窗口就会一直无法向前移动。
先讨论发送窗口大小=接收窗口大小的情况。
不难证明:发送窗口不会超前接收窗口,即发送窗口最左侧对应的编号不大于接收窗口最左侧对应的编号。在传输开始时,发送、接收窗口都在起始位置,前面所说的两个编号相同。这之后,因为对任何一个包,永远都是接收方先接收到,再有发送方收到ACK,所以接收窗口一定先于发送窗口移动。
同样不难证明:不会出现接收窗口最左侧与发送窗口最左侧的距离超过窗口大小N的情形。假设某个时刻接收方的窗口先移动了。按照最坏情况考虑:接收窗口全部收到了N个包,但这N个包的ACK全丢,于是接收窗口比发送窗口超前了N。但这时,发送方会重传的包对应接收窗口移动之前的N个位置,而接收方现在准备接收的新的N个包都不在发送窗口以内,于是接收窗口就无法再向前移动了。窗口的移动是单向的(不能撤销已经收到的确认),所以命题得证。因此步骤3是合理的。
因为接收方最左侧与发送方最左侧的距离不超过窗口大小N,所以:窗口长度≤序号空间大小的一半。如果超过一半,在刚才所说的极端情况中,就必定存在不同的包被分配到相同序号的可能性,导致最终传输的数据出错或部分丢失。即:过期的数据因为编号冲突,对最新的数据造成干扰。
不难验证:当发送窗口大小≠接收窗口大小时,刚才证明的第一个结论(加粗+下划线部分)仍然成立;不过,
“不会出现接收窗口最左侧与发送窗口最左侧的距离超过窗口大小N的情形”此时应更改为“不会出现接收窗口最左侧与发送窗口最左侧的距离超过发送窗口大小N_s的情形”;
“窗口长度≤序号空间大小的一半”此时应更改为“发送窗口与接收窗口的长度之和≤序号空间大小”。
虽然发送窗口宽于接收窗口时,也不影响证明的结论;但是,还是需要指出,出于流量控制(见后文)的考虑,发送方的发送窗口大小,应当不超过对方给出的接收方窗口大小。上面说的情况,换种说法是:在窗口长度超过序号总数的一半时,即使中间的链路完全没有导致数据包乱序到达,也可以引发编号冲突;而实际上,即便不超过,中间的传输链路也有可能导致数据包的乱序,从而引发问题。在发送方与接收方由单段物理线路相连的情况下,双方之间的链路几乎不会重排各个包;但如果两端之间是一个庞大而复杂的网络,数据包的顺序被打乱的概率就不低了。结合上面的例子,假如发送方在向接收方重传未确认的编号为x的分组时,一段时间之前滞留在网络中的编号也为x的分组先到达了接收方并被确认。之后,双方的窗口都会移走,后来发送方重发的包将因已确认而被丢弃。两个包虽然编号相同,但数据部分不同。结果,因为编号冲突,之前的数据覆盖了后来的数据,导致传输出错。为了尽可能将此情况的发生率降低到零,可以为每个分组指定存活时间。比如,在高性能TCP扩展(RFC 7323)中,规定了分组的最长寿命约为3分钟。也有新的使用序号的方法被提出,它们能够完全避免这种重新排序问题。

3.5 面向连接的运输:TCP
前面说过,TCP是面向连接的(connection-oriented):发送数据之前,通信的两个进程必须先相互握手,即相互发送某些预备报文段,准备数据传输的参数。作为建立连接的一部分,连接双方初始化许多TCP状态变量。在传送数据完毕后,必须释放已经建立的TCP连接。
TCP提供可靠交付的服务。通过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共 10个数据块,但接收方的TCP可能只用了4个数据块,就把收到的字节流交付上层的应用程序)。然而,接收方应用程序收到的字节流,必须和发送方应用程序发出的字节流完全一样。

数据从发送端的套接字来到运输层后,就由发送端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报文段由首部字段和数据字段组成。数据字段包含需要传输的数据。如前所述,MSS限制了数据长度。当TCP需要发送较多数据时,将数据划分成长为MSS的若干块(最后一块通常短于MSS)。然而,交互式应用通常传送短于MSS的数据块。例如,对于Telnet,其TCP段的数据部分经常只有1字节。由于TCP首部一般是20字节(比UDP首部多12字节),所以该报文段只有21字节长。上图显示了TCP报文段的结构。与UDP一样,段头包括源端口号和目标端口号,它被用于多路复用 / 分用来自或送到上层应用的数据。TCP段段头还包含:
·各32位的序号(sequence number)字段和确认号(acknowledgment number)字段。TCP收发双方用它们实现可靠数据传输服务,讨论见后。
·4位的段头长度(header length)字段,指示以32位的字为单位的TCP段头长度。由于存在TCP选项字段,段头长度是可变的。通常选项字段为空,所以TCP段头的典型长度是20字节,也是最小长度。这固定的20字节包括源端口号到紧急数据指针这部分。段头长度最大为60字节,此时该字段的值为0Fh,即1111b。
·6位暂未使用(unused)。目前应置零。
·8位的标志(flag)字段:
(1)在成功接收的段的确认报文中,ACK位为有效。
(2)RST、SYN和FIN位用于连接建立和删除。RST = 1表明TCP连接出现严重差错(由于主机崩溃、网络中断等),必须释放并重新建立连接;还用来拒绝非法报文段或拒绝打开连接。当FIN = 1时, 表明此报文段的发送方的数据已发送完毕,并要求释放运输连接。
(3)显式拥塞通知使用CWR和ECE位,见3.7节。
(4)当PSH(push)位置位时,指示接收方应立即将数据交给上层,而不等到缓存填满以后再向上交付。
(5)URG位指示报文段存在被发送端上层标记为“紧急”的数据。紧急数据的最后1字节由16位的紧急数据指针(urgent data pointer)指出。当URG置位并给出指向紧急数据的尾指针时,代表段中有紧急数据,应尽快传送,而不要排队。例如,突然发现严重问题,需要立刻取消远程主机上的某程序的运行,因此键入Ctrl+C。如果不标记紧急,那么这2个字符将存储在接收TCP的缓存末尾。只有之前数据被递交完毕,这2个字符才被交付接收方的应用进程。这就浪费了许多时间。传送紧急数据时,发送方TCP把紧急数据插入到本报文段数据的最前面;紧急数据之后的数据仍为普通数据。实际中,PSH和URG一般不使用。
·16位的接收窗口(receive window)字段,用于流量控制。该字段指示接收方愿意接收的字节数。注意:窗口指的是发送本报文段的一方的接收窗口,而不是其发送窗口。窗口值告诉对方:从本报文段段头的确认号算起,接收方目前允许对方发送的数据量(单位:Byte)。之所以要有这个限制,是因为接收方的缓存是有限的。总之,窗口值作为接收方让发送方设置其发送窗口的依据。
·16位的校验和。检验的范围包括首部和数据这两部分。和UDP一样,在计算检验和时,要在TCP报文段的前面加上12字节的伪首部。伪首部的格式与UDP的伪首部一样,但应把伪首部第4字段中的17改为6(TCP的协议号),第5字段解释为TCP长度。接收方收到此报文段后,仍要加上这个伪首部来计算检验和。若使用IPv6,则相应的伪首部也要改变。
·可选、变长的选项字段(options field),最长40字节。用于在高速网络环境中作为窗口调节因子,等等。
TCP最初只规定了一种选项:最大报文段长度(MSS)。为何要确定MSS呢?MSS与接收窗口没有关系,毕竟接收到的数据可以缓存起来。我们知道,TCP段的数据部分,至少要加上40字节的首部(不含选项时,TCP首部20字节和IP首部20字节),才能组装成一份IP数据报。若选择较小的MSS,网络的利用率就会降低。极端情况下,当TCP段只含1字节的数据时,IP数据报的额外开销至少有40字节。这样,网络利用率就不超过1 / 41。到了数据链路层还要加上别的开销。若TCP段过长,那么在IP层传输时,就可能要分解成多个短数据报片(第4章)。在终点要把收到的片装配成原来的段。当传输出错时还要进行重传。这些也都会使开销增大,进而使传输效率降低。
因此,MSS应尽可能大些,只要IP层不需分片就行。IP数据报经过的路径是动态变化的,因此,在这条路径上确定的不需要分片的MSS,如果改走另一条路径就可能需要分片。所以,最佳的MSS是很难确定的。在连接建立的过程中,双方都把自己支持的MSS写入这一字段,以后就按照此值传送数据。两个传送方向可以有不同的MSS值。若未填写此项,则默认值为536字节。互联网上的主机都应接受的MSS是536 + 20(固定首部长度)= 556字节。
RFC 879指出,流行的一种说法是:在TCP连接建立阶段“双方协商MSS值”,但这是错误的,因为这里并不存在任何的协商,而只是一方把MSS值设定好以后通知另一方而已。
可见,可以用不同的机制控制TCP报文段的发送时机。例如,只要缓存中存放的数据达到MSS字节,就组装成一个TCP段发送出去。或者,发送方的应用进程主动要求发送报文段,即推送(push)操作。又或者,发送方的一个计时器到期,就把已有的缓存数据装入报文段(长度不能超过MSS)发送出去。
随着互联网的发展,又陆续增加了几个选项,如:窗口扩大、时间戳等(RFC 7323)。以后又增加了有关选择确认选项(RFC 2018)。具体细节参见RFC 854和RFC 1323,这里只作简单说明。
窗口扩大选项是为了扩大窗口。TCP首部的窗口字段长16位,因此最大窗口大小为64 KB。这对早期的网络是足够的。但对于包含卫星信道的网络,传播时延和带宽都很大。要获得高吞吐率,需要更大的窗口大小。
窗口扩大选项占3字节,其中1字节表示移位值S。新的窗口值等于TCP首部中的窗口位数从16增大到(16 + S)。移位值允许使用的最大值是14,相当于窗口最大值增大到216+14 – 1 = 230 – 1。
窗口扩大选项可以在建立TCP连接时协商。若不再需要扩大窗口,可发送S = 0的选项,使窗口大小复原。
时间戳(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字节(序号需要4字节表示),因此,在选项中最多只能指明4个字节块的边界信息。这是因为4个字节块共有8个边界,因而需要32字节来描述。另外还需要2字节:1字节用来指明是SACK选项,1字节指明这个选项要占用多少字节。如果报告5个字节块的边界信息,那么需要42字节,超过了40字节的上限。RFC 2018对边界信息的格式做出了明确规定。
然而,SACK文档并没有指明发送方应当怎样响应SACK。所以,大多数实现还是重传所有未被确认的数据块。此外,如果TCP段段头的长度不是4字节的整数倍,则需要在段头的最后填充。TCP会把要发送的数据的每个字节都编号(可从任意自然数开始),并分成若干段;每个TCP段也编号,并存储在序号字段中。一个报文段的序号,就是该段的数据字段的第一个字节的序号。
在TCP中,一方填入报文段的确认号,是期望从另一方收到的下一个字节的编号。数据传输完毕后,接收方还会再给发送方发送一份不带数据的报文。这时,确认号依然为期望从发送方收到的下一个字节的编号。可以看出,这属于累积确认机制。
TCP段的到达顺序不一定与发送顺序相同。TCP RFC没有规定应对方法。开发者有两种选择:
【1】发现编号更大的报文段先到达,直接丢弃。
【2】先缓存起来,等待前面缺失的段。实践中,多采用本方法,因为它更节省带宽。很明显,本方法配合SACK使用。
实际应用中,可能会遇到这样的情况:两台主机虽然已经终止TCP连接,但网络上仍有该连接的报文(没来得及到达接收方);随后,这两台主机又建立了一个端口号不变的新TCP连接。这时,旧连接的报文到达了,它被误认为是新连接的报文。TCP连接双方在对每个TCP段编号时,初始序号常常随机选择,将此情形的概率降到最低。TCP采用超时-重传(timeout-retransmission)机制解决报文段的丢失:对于发送的段,若一定的时间后仍未收到ACK,则重新传输该段。超时必须大于该连接的往返时间(RTT),即:从一个报文段发出,到发送方收到接收方确认的时间。否则,会造成大量不必要的重传。
大多数时候,TCP仅在少数时刻做一次测量,得到的结果称为样本RTT(SRTT,S = sample),而不为每份发送的报文测量RTT。比如,只对每个当前仍未被确认的TCP段测量RTT,并从不为重传的段测量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秒(RFC 6298)。对于超时的计入,使用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认为报文段或其确认报文丢失或受损时,重传这些报文段。
RFC 6298推荐使用单一的定时器为每个未确认段计时:假如为每个未确认的TCP段单独计时,开销可能巨大。TCP要求接收方必须有累积确认的功能,减小传输开销。接收方可以在合适的时候发送确认,也可以在自己有数据要发送时把确认信息顺便捎带上。但请注意两点:一是接收方不应过分推迟确认,否则会导致不必要的重传,反而浪费网络资源。TCP标准规定,确认推迟的时间不应超过0.5秒。若收到一连串具有最大长度的报文段,则必须每隔一个报文段就发送一个确认。二是捎带确认实际上并不经常发生,因为多数应用程序很少同时双向发送数据。有些版本的TCP采用隐式NAK(implicit NAK)机制:收到对一个特定报文段的3个重复ACK,就作为对后续报文段的一个隐式NAK:即便该报文段实际上没有超时,也会触发对它的重传。可见,这种情况下,TCP认为该后续报文段和(或)其确认报文丢失了。TCP为每个报文段都添加了序号,以便接收方能识别丢失或重复的报文段。
冗余ACK(duplicate ACK)就是再次确认某个报文段的ACK。当某个报文段收到之前,编号比它更大的报文段先被收到,那么TCP会重新发送包含最后一个按序收到的TCP段的编号的ACK。一旦在收到某报文段的ACK后,再收到3次对同一报文段的冗余ACK,TCP就执行快速重传(fast retransmission),在定时器过期前重传丢失的段。
3 DUP ACK是总结于经验,表明网络是连通的(能收到另一端的ACK),但有数据包迟迟未被确认,很可能是堵在了路上。在收到2条DUP ACK时,很大概率是因为乱序而非丢失,此时还没有必要快速重传。TCP还使用流水线方法传输,使吞吐量显著提升。发送方能够拥有的未确认段数量,是由流量控制和拥塞控制机制决定的。TCP流量控制将在本节讨论;TCP拥塞控制将在3.7节讨论。此时只需知道TCP发送方使用了流水线。TCP提供了流量控制(flow control)服务,消除发送方使接收方缓存溢出的可能性。可见,流量控制是一个速率匹配服务,通过调节接收窗口的大小,令发送速率与接收方应用程序的读取速率相匹配,既要让接收方来得及接收,也不要使网络拥塞。流量控制和拥塞控制的动作非常相似,但是它们显然是针对完全不同的原因采取的措施。
接收方给发送方回应的报文中,指示了缓冲区的剩余空间。
有一个问题需要注意:接收方接到某个TCP段以后,缓冲区满了,接收窗口的大小减为零,而这之后发送的ACK报文又全部丢失。当接收方的应用读取了缓冲区,缓冲区重新拥有一定的空闲空间以后,发送方并不会继续收到通知。于是发送方被阻塞,不能再发送新的数据。如果没有其它措施,这种互相等待的死锁局面将一直延续下去。
为了解决这个问题,TCP为每个连接设有一个persistence timer。只要一方收到对方的零窗口通知,就启动这个计时器。计时器到期后,发送一个只有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 →
在命令行下,输入netstat,可以查看所有TCP连接的状态。

TCP通过三次握手(3-way handshake,三路握手,或称三报文握手,注意handshake没有s)建立连接。服务器进程事先创建好传输控制块(transmission control block,TCB),准备接收客户端的连接请求。然后,服务器进程从CLOSED状态进入LISTEN状态,等待客户的连接请求。如有,即做出响应。
·第一步:客户端的TCP创建传输控制块。在打算建立连接时,向服务器端的TCP发送一个SYN报文段。
该段不包含应用层数据,但是段头的SYN(同步序列编号,Synchronize Sequence Numbers)位置1。另外,客户会随机选择一个初始序号(记为s),放置于该起始的TCP SYN报文段的序号字段中。报文段被封装进IP数据报并发送。为了避免某些攻击,这个初始序号是随机分配的。TCP规定,SYN段(SYN = 1的报文段)不能携带数据,但要消耗掉一个序号。
这一步,客户进程从CLOSED状态进入SYN_SENT状态。
·第二步:一旦包含TCP SYN报文段的IP数据报到达服务器主机,服务器便提取出TCP SYN报文段,为该TCP连接分配TCP缓存和变量,并向客户TCP发送允许连接的报文段(SYN ACK报文段)。
这个允许连接的报文段也不包含应用层数据。但是,其首部却包含了重要的信息:
首先,SYN位和ACK位置位。其次,该TCP报文段首部的确认号字段被置为s+1(这个报文段也不能携带数据,但同样要消耗一个序号)。最后,服务器选择自己的初始序号(记为t),并放到TCP段头的序号字段中。
这一步,服务器进程进入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一共发送了2个连接请求报文段,第1个丢失,第2个到达了服务器B,没有“已失效的连接请求报文段”。
现出现一种异常情况:A发出的第1个连接请求报文段实际上并没有丢失,而是在某些结点滞留了,延误到连接释放后的某个时间才到达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状态。注意:如果在半关闭状态服务器没有发送额外的数据,那么挥手的报文只有3份。
·第四步:客户对服务器的终止报文段确认,ACK置1,序号为u+1,确认号为v+1,进入TIME_WAIT状态。如果这是释放连接过程中的第3份报文(在半关闭状态服务器没有发送数据),则确认号为第2步发送的报文的序号加1。这份报文表明服务器发来的确认报文没有丢失。TIME_WAIT的持续时间与实现有关,典型值为30秒、1分钟或2分钟。
请注意,现在TCP连接还没有释放掉。必须经过TIME-WAIT计时器设置的时间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小时。若2小时没有收到客户的数据,服务器就发送一个探测报文段,以后每75秒发送一次。若发送10个探测段后仍无响应,服务器就认为客户端故障,关闭这个连接。如果服务器收到TCP段后,发现段的目标端口与服务器已开启(监听)的端口都不匹配,则服务器向源回复一个特殊的报文段,该段将RST标志位置位。如果服务器收到目标端口与已开启的端口均不匹配的UDP段,则发送一份特殊的ICMP数据报(见第5章)。nmap端口扫描工具可以向目标主机的指定端口发送TCP SYN报文段。源主机可能面临3种可能的结果:
• 从目标主机接收到一个TCP SYN ACK报文段。目标主机有应用程序使用该TCP端口,nmap返回“打开”。
• 从目标主机接收到一个TCP RST报文段。这意味着该SYN段到达了目标主机,但目标主机没有使用该TCP端口的应用程序。不过,攻击者至少知道发向该主机指定端口的报文段没有被中途的任何防火墙(firewall)所阻挡。
• 什么也没有收到。这很可能表明:该SYN报文段被中间的防火墙所阻挡,无法到达目标主机。
nmap是一个功能强大的工具。它不仅能侦察打开的TCP端口,也能侦察打开的UDP端口,还能侦察防火墙及其配置,甚至能侦察应用程序的版本和操作系统版本。其中的大多数都能通过操作TCP连接管理报文段完成。在三次握手中,服务器为了响应一个收到的SYN,分配并初始化连接变量和缓存。然后,服务器发送一个SYN ACK进行响应,并等待来自客户的ACK报文段。如果客户不发送ACK完成该三次握手的第三步,最终(通常1分钟或更久之后)服务器将终止该半开连接并回收资源。
这种TCP连接管理协议为经典的DoS攻击——SYN洪水攻击(SYN flood attack)提供了条件。在这种攻击中,攻击者发送大量的TCP SYN报文段,而不完成第三次握手。随着SYN段纷至沓来,服务器不断为这些半开连接分配和释放资源,导致资源消耗殆尽。这种SYN洪泛攻击是被记载的众多DoS攻击中的第一种。幸运的是,现在有一种有效的防御系统,称为SYN cookies,它们被部署在大多数主流操作系统中。SYN cookies以下列方式工作:
·当服务器收到一个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的实现中,广泛使用Nagle算法:若发送应用进程把要发送的数据逐个字节地送到发送缓存,则发送方把第1字节先发送出去,后面的字节都缓存起来。发送方收到对第1个字节的确认后,再把发送缓存中的所有数据组装成一个报文段发送出去,同时继续缓存随后的数据。只有收到对前一个报文段的确认后,才继续发送下一个报文段。当数据到达较快而网速较慢时,这种方法可以明显减少消耗的带宽。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)拥塞控制中,路由器显式地通知发送方,它(路由器)能在输出链路上支持的最大主机发送速率。Internet中,默认版本的IP和TCP采用端到端拥塞控制方法。然而,IP和TCP也能选择性实现网络辅助拥塞控制。
对于网络辅助的拥塞控制,拥塞信息从网络反馈到发送方通常有两种方式。直接反馈信息可以由路由器发给发送方。这种方式的通知通常采用阻塞分组(choke packet)的形式。更为通用的第二种形式是,路由器标记从发送方流向接收方的分组中的某个字段来指示拥塞的产生。一旦收到一个标记的分组,接收方就向发送方通知网络发生了拥塞。后一种形式至少要消耗一倍完整的RTT。拥塞控制需要代价。首先要获得网络内部的流量分布信息。实施拥塞控制时,还需要在结点之间交换信息和命令,以选择控制策略并实施控制。这就产生了额外开销。拥塞控制有时需要将一些资源(缓存、带宽等)分配给个别用户(或若干类用户)单独使用,使网络资源不能更好地共享。很明显,设计拥塞控制策略时,必须全面衡量得失。
实践证明,拥塞控制是很难设计的,因为它是一个动态的而不是静态的问题。当前,网络正朝着高速化的方向发展,这很容易出现缓存不够大而造成分组的丢失。但分组丢失是拥塞的征兆而不是原因。在许多情况下,甚至正是拥塞控制机制本身,成为引起网络性能恶化甚至死锁的原因。这点应特别引起重视。

3.7 TCP拥塞控制
运行在发送方的TCP拥塞控制机制维护一个额外的变量,即拥塞窗口(congestion window)。发送方中未被确认的数据量不超过拥塞窗口和接收窗口的最小值。即:接收窗口长度 < 拥塞窗口长度时,接收方的接收能力限制发送窗口长度的最大值。而接收窗口长度 > 拥塞窗口长度时,网络的拥塞限制发送窗口长度的最大值。
如果发送方与目的地之间的路径上出现了拥塞,中间链路丢弃了数据报(包含TCP段),那么会因为数据丢失而引发超时或收到3个冗余ACK。发送方据此可以判别拥塞程度。现在,通信线路的传输质量一般都很好,因传输出差错而丢弃分组的概率是很小的。因此,发送方在超时重传计时器启动时,就可以判断网络出现了拥塞。
如果TCP正常收到ACK消息,那么拥塞窗口的大小会增加。收到正确的ACK的速率越高,窗口扩大越快。TCP使用确认来触发(trigger,或计时,clock)增大拥塞窗口长度,因此我们说TCP是自计时(self-clocking)的。
TCP拥塞控制中,网络里没有明确的拥塞状态信令,ACK和丢包事件充当隐式信号;发送方也各自控制速率。

TCP拥塞控制算法(TCP congestion-control algorithm)载于RFC 5681。算法包括3部分:慢启动、拥塞避免与快速恢复。慢启动和拥塞避免是强制部分,两者的差异在于:对收到的ACK反应时,增加拥塞窗口长度的方式。慢启动比拥塞避免能更快增加窗口大小(不要被名称所迷惑)。快速恢复是推荐部分,但对TCP发送方不是必需的。1、慢开始(slow-start)。
当一条TCP连接开始时,拥塞窗口(cwnd)大小通常置为1倍SMSS(发送方最大报文段大小)的较小值,初始发送速率按SMSS / RTT估计。还需要另一个状态变量——慢开始阈值SST。在初始状态下,它通常比拥塞窗口大小cwnd大得多,例如64 KB。
在旧标准中,拥塞窗口大小一般为1到2倍SMSS。新的RFC 5681标准把初始拥塞窗口设置为不超过2至4倍SMSS的数值。具体的规定如下:
(1)若SMSS > 2190字节,则cwnd = 2×SMSS字节,且不得超过2个报文段。
(2)若 (SMSS > 1095字节) 且 (SMSS≤2190字节) ,则cwnd = 3×SMSS字节,且不超过3个报文段。
(3)若SMSS≤1095字节,则cwnd = 4×SMSS字节,且不得超过4个报文段。
可见,这个规定就是限制初始拥塞窗口的字节数。
对TCP发送方而言,可用带宽可能比初始限速大得多,TCP发送方希望迅速确定可用带宽。因此,在慢启动状态,拥塞窗口cwnd的值以1个SMSS开始;并且,每当传输的报文段首次被确认,就增加1个SMSS;同时,如有已记录的DUP ACK数量,则将其清零。于是,第一个RTT(或:第一个传输轮次)后,如果未拥塞,则允许一次性发送2倍SMSS的数据量。又一个RTT后,如果未拥塞,则允许一次性发送4倍SMSS的数据量(收到的2个ACK各贡献1倍SMSS)。于是,每过一个RTT,发送速率就翻番。
此外,cwnd的增加也受到原先未被确认的、但现在刚被收到的确认报文段所确认的字节数N的影响。即

Δcwnd=min⁡(N, SMSS)
但是,何时结束这种指数增长呢?慢开始对这个问题提供了几种答案。
【1】如果出现一次超时,发送方将慢开始阈值SST,设为拥塞窗口长度cwnd的一半;然后将拥塞窗口长度cwnd设为1倍SMSS;如有已累计的DUP ACK数量,则将其清零。然后,重新开始慢启动过程。
【2】当cwnd≥SST时,慢开始结束,TCP进入拥塞避免模式。之后,TCP更为谨慎地扩大拥塞窗口。
这是因为:当拥塞窗口大小不小于慢开始阈值SST时,继续使其翻倍可能有些鲁莽。
【3】如检测到3个冗余ACK,TCP快速重传并进入第三部分,即快速恢复状态。后续将讨论相关内容。
有时,个别报文段会在网络中丢失,但实际上网络并未拥塞。如果发送方迟迟收不到确认,就会产生超时,误认为网络发生了拥塞。这就导致发送方错误地启动慢开始,把拥塞窗口cwnd又设置为1×SMSS,降低了传输效率。
采用快速重传,可以让发送方尽早知道发生了个别报文段的丢失。快重传首先要求:接收方不要等待自己发送数据时才进行捎带确认,而是要立即发送确认;即使收到了失序的报文段,也要立即发出对最后一个按序报文段的重复确认。算法规定,发送方只要一连收到3个重复确认,就判定接收方没有收到相应的报文,因而应立即重传,这样就不会出现超时,发送方也不会误认为出现了网络拥塞。使用快重传可以使整个网络的吞吐量提高约20%。

2、拥塞避免。
一旦进入拥塞避免状态,拥塞窗口大小大约是原先的一半。TCP将不会每过一个RTT再将窗口大小翻番,而是采用较为保守的方法:每个RTT只将窗口大小增加1个SMSS。这能够以几种方式完成。
一种通用的方法是:TCP发送方无论何时收到一个新的确认,都将拥塞窗口大小增加(SMSS / cwnd)倍SMSS。例如,如果SMSS是1460字节,cwnd是14600字节,则在一个RTT内发送10个报文段,每个到达ACK(假定每个报文段一个ACK)增加1 / 10 SMSS的窗口长度。因此,在收到对所有10个报文段的确认后,拥塞窗口的值增加了一个SMSS。此外,DUP ACK计数也要清零。
但是何时结束这种线性增长(每RTT一个SMSS)呢?
【1】当超时以后,与慢启动的情况一样:慢启动阈值SST被更新为原cwnd的一半;cwnd设为1倍SMSS;然后,回到慢启动状态。
【2】然而,丢包事件也能由3次冗余ACK触发。这时,TCP的反应不那么剧烈:将慢启动阈值SST记为cwnd的一半;然后,将cwnd砍半再加上3个SMSS。接下来进入快速恢复状态。这样做的理由是:既然发送方收到3个重复的确认,就表明有3个分组已经离开了网络。这3个分组不再消耗网络的资源,而是停留在接收方的缓存中(接收方能成功送达3个DUP ACK就证明了这个事实)。可见,网络中并不是一直在堆积分组,而是至少减少了3个分组,因此可以适当把拥塞窗口扩大。
不过,必须强调,“拥塞避免”并非指完全避免拥塞。利用以上的措施,要完全避免网络拥塞,还是不可能的。3、快速恢复。
在快速恢复中,对收到的引起TCP进入快速恢复状态的缺失报文段的每个冗余ACK,cwnd增加1倍SMSS。最终,当对丢失报文段的一个ACK到达时,TCP在降低拥塞窗口大小cwnd为慢开始阈值SST后,将DUP ACK计数清零,并进入拥塞避免状态。如果出现超时,快速恢复在执行与慢启动和拥塞避免中相同的动作后,迁移到慢启动状态:慢启动阈值设为cwnd的一半;cwnd的值被设置为1个SMSS;DUP ACK计数清零。
快速恢复是TCP推荐的而非必需的构件。TCP Tahoe作为TCP早期版本,不管是发生超时还是收到3个冗余ACK,都无条件地将其拥塞窗口减至1个SMSS,并进入慢启动阶段。较新的TCP Reno则引入了快速恢复。从上图可以看出,TCP认为,相比3 DUP ACK,超时意味着相当大的概率已发生拥塞,且拥堵状况要更加严重,因此TCP发送端会直接被打回慢开始状态;而3 DUP ACK代表不那么严重的网络拥挤,因此进入快速恢复状态,降速不会过于严重(差不多从零开始重新提速),而是尽量保持在降速前的速率的大约50%。如果网络相对不拥塞,那么传输速率线性上升,直到达到网卡或瓶颈链路等的传输速率的瓶颈。诸如搜索、电子邮件和社交网络等云服务,传输和处理的数据量非常大。而用户又经常位于远离数据中心的地方,这些数据中心负责为云服务关联的动态内容提供服务。实际上,如果端系统远离数据中心,那么RTT将会很大,TCP慢启动将潜在地导致较长的响应时间。
以搜索为例。有资料表明,典型地,服务器在慢启动期间交付响应要求3个TCP窗口。所以,从某端系统发起一条TCP连接,到它收到该响应的最后一个分组的时间,大致是4RTT(用于建立TCP连接的1个RTT,加上用于3个数据窗口的3个RTT),再加上在数据中心处理的时间。对于大量的查询来说,这些RTT导致返回搜索结果的过程中的显而易见的时延。此外,在接入网中可能有较大的丢包率,导致重传和更大的时延。
缓解这个问题和改善用户体验的一个途径是:
①部署邻近用户的前端服务器;
②在该前端服务器使用TCP Splitting。
借助TCP Splitting,客户向邻近前端连接一条TCP连接,并且该前端与数据中心维持一条拥塞窗口非常大的TCP连接。使用这种方法,响应时间大致变为

4RTTFE+RTTBE+处理时间
RTTFE是客户与前端服务器之间的往返时间,RTTBE是前端服务器与数据中心(后端服务器)之间的往返时间。如果前端服务器邻近客户,则该响应时间大约变为RTT加上处理时间,因为RTTFE小得微不足道,且RTTBE约等于RTT。总而言之,TCP Splitting大约能够将网络时延从4RTT减少到RTT,极大地改善用户体验,对于远离数据中心的用户更是如此。TCP Splitting也有助于减少因接入网络丢包引起的TCP重传。Google和Akamai在接入网中广泛为它们的云服务使用TCP Splitting。

使用TCP拥塞控制机制时,拥塞窗口常常线性扩大,然后减半,再线性扩大,再线性减半(有的TCP版本实现的是减半再加3)……因此,TCP拥塞控制被称为加性增、乘性减(Additive-Increase,Multiplicative-Decrease,AIMD)拥塞控制。将窗口大小对时间画成图表,曲线的大多数部分都呈锯齿状。TCP AIMD算法基于大量的工程见解和拥塞控制经验而开发。在TCP研发后的十年,理论分析显示,TCP的拥塞控制算法作为一种分布式异步优化算法,使得用户和网络性能的几个重要方面都同时得到了提升。此后,拥塞控制的理论得到了长足的发展。有的时候,直接将传输速率砍半还是过于保守了。2008年,TCP Cubic(RFC 8312)问世。它与TCP Reno的不同仅仅在于:拥塞窗口只在收到ACK以后扩大,而慢开始和快速恢复阶段未作改变。具体而言,在拥塞避免阶段:
·设W_"max" 为上次探测到丢包以后的TCP拥塞控制窗口大小,K代表在之后重新达到W_"max" 的时间点(假设这期间不再丢包)。K由几个可调整的参数决定。
·拥塞窗口大小按照关于当前时间t和K的三次函数增长。因此,当t≪K时,拥塞窗口扩大得比t→K时快得多。当t>K后,一开始,拥塞窗口大小仍然不会太快扩大,这在导致丢包的链路状态还未好转的时候表现比较好。但是,很快,拥塞窗口就会迅速扩大,以便在链路状态变好后,尽快找到一个尽量高的速率。如果使用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必须针对高速环境予以改进。

另一种避免拥塞的方法是:基于延迟的拥塞控制(delay-based congestion control)。它允许路由器提前将拥塞指示位置位,以便在缓冲区满导致丢包之前就令发送方降低发送速率,以此避免丢包和重新传输。
TCP Vegas算法试图在维持较好的吞吐量的同时避免拥塞。Vegas的基本思想是:①在分组丢失发生之前,在源与目标之间检测路由器中的拥塞;②当检测出快要发生的分组丢失时,线性降低发送速率。快要发生的分组丢失是通过观察RTT来预测的。分组的RTT越长,路由器中的拥塞越严重。2015年年底,Ubuntu的TCP实现默认提供了慢启动、拥塞避免、快速恢复、快速重传和SACK,也提供了诸如TCP Vegas和BIC等其它拥塞控制算法。
在TCP Vegas中,发送方为每个已确认的包测量源到目的地的RTT。记RTT_"min" 为测出的最小值。如果TCP Vegas的拥塞窗口大小为cwnd,则未拥塞时的吞吐量为cwnd/RTT_"min" 。假设发送方测定的实际发送速率接近此值,代表发送速率可以继续提升,因为路径未拥塞;然而,假如测出的吞吐量远小于此值,意味着路径有拥塞,将降低发送速率。
BBR拥塞控制协议根据TCP Vegas的思想设计,并包含与不采用BBR的TCP发送方公平竞争的机制。2016年,Google在其私有的B4网络(与Google数据中心互连)中开始为全部TCP流量使用BBR,代替CUBIC。BBR也正在部署于Google和YouTube Web服务器。其它基于延迟的TCP拥塞控制协议包括针对数据中心网络的TIMELY、复合TCP(Compound TCP)和针对高速长距离网络的FAST。IP和TCP的扩展方案RFC 3168已经提出并部署。该方案允许网络明确向TCP发送方和接收方发出拥塞信号。这种形式的网络辅助拥塞控制称为显式拥塞通知(Explicit Congestion Notification,ECN),涉及TCP和IP协议。
在网络层,IP数据报首部的服务类型字段中的2位(参见4.3节)用于ECN。路由器使用ECN位指示它正面临拥塞。该拥塞指示由被标记的IP数据报所携带,送给目标主机,再由目标主机通知发送主机。方案标准没有提供路由器拥塞的定义:该判断可能由路由器厂商配置,并由网络操作员决定。发送主机也可以在ECN位(向路由器)表明:收发双方是ECN兼容的,可以对ECN指示的拥塞采取行动。
当接收主机中的TCP接收到的数据报包含ECN拥塞指示时,它通过在接收方到发送方的TCP ACK报文段中设置ECE(显式拥塞回显)位,通知发送主机中的TCP,当前网络拥塞。接下来,发送方减半拥塞窗口大小,也可以快速重传,并在下一个将要发送的报文段首部中,将CWR(拥塞窗口已缩小)位置位。
其它运输层协议也可以利用网络层发送的ECN。数据报拥塞控制协议(Datagram Congestion Control Protocol,DCCP)提供了一种低开销、可控制拥塞的、类似UDP的不可靠服务,该协议利用了ECN。DCTCP(数据中心TCP)和DCQCN(数据中心量化的拥塞通知)专门为数据中心网络设计,也利用了ECN。近年的数据显示,ECN已经应用于越来越多的服务器和到达这些服务器的路径中的路由器。TCP的AIMD算法公平吗?尤其是假定可在不同时间启动,并因此在某些时刻可能具有不同的窗口长度情况下,对这些不同的TCP连接,还是公平的吗?TCP趋于在竞争的多条TCP连接之间,提供对瓶颈链路带宽的平等分享。
考虑有两条TCP连接共享一段传输速率为R的链路的简单例子,如下图。假设这两条连接有相同的SMSS和RTT(如果它们具有相同的拥塞窗口长度,就具有相同的吞吐量),它们有大量的数据要发送,且没有其它TCP连接或UDP数据报穿越该段共享链路。同样,忽略慢启动阶段,并假设TCP连接一直按拥塞避免模式(AIMD)运行。
参见图线。横轴和纵轴分别代表连接1和2的吞吐量。如果点落在射线r:   y=x,   x>0上,就意味着两个连接都分配到了相等的正吞吐量。如果在某个时刻,点没有落在射线r上,即两个连接分到的吞吐量不等,那么:
【1】如果它们的总吞吐量小于瓶颈链路的总带宽R,即:点在线段l:   y=-x+R,   x,y>0下方,那么,连接1和2的吞吐量都将按照每个RTT一倍SMSS的速率增长。由于Δx=Δy,所以点会沿平行于直线y=x的方向逐渐移到线段上方。这意味着,两个连接的总速率已经超过了瓶颈链路允许的速率,将发生丢包。
【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应用的链路,每个应用使用1条TCP连接。如果1个新的应用加入了,也使用1条TCP连接,则每个应用得到差不多相同的传输速率R/10。但是,如果这个新应用使用了11个并行TCP连接,那么它就不公平地分到超过一半的带宽。Web流量在Internet中是非常普遍的,所以多条并行连接并非不常见。

3.8 运输层功能的演化
TCP和UDP伴随计算机网络走过的三十多年告诉我们,它们都无法很好地适配实际情况。因此,新的运输层功能的设计还在继续。
除了之前已经提到过的TCP版本,还有多得多的TCP版本,包括针对无线链路,或高带宽高RTT路径,或出现分组重排的路径,或数据中心内的较短路径等情形的TCP;也包括为在瓶颈链路中争抢带宽的TCP连接设置不同的优先级,或报文段将通往不同类型的源到目的地的路径的并行TCP连接设计的TCP;还包括以完全不同的方式处理分组确认、TCP会话的建立和终止的TCP。它们仅有的共性,大概就是都使用相同的TCP报文段格式,以及都需要公平处理网络拥塞吧。

快速UDP Internet连接(Quick UDP Internet Connections,QUIC)是一种新型的应用层协议,用于为安全HTTP改进运输层服务的性能。QUIC已经广泛部署,虽然它还在被标准化为RFC的进程中。Google已将QUIC应用于许多面向公众的Web服务器、YouTube APP、Chrome浏览器和Android的Google搜索APP。2017年的一份资料显示,多于7%的网络流量是QUIC。
QUIC的主要特性包括:
·面向连接且安全。建立QUIC连接状态需要端点握手。连接状态包括源和目标连接ID。所有QUIC分组都是加密的。建立连接和身份验证与加密需要的握手被合并到了一起,因此建立连接更快。若使用TCP实现类似功能,则需要先建立TCP连接,再在TCP连接之上建立TLS连接。
·流。QUIC允许不同的应用级“流”通过单一的QUIC连接复用。QUIC连接建立后,还可以添加新的流。流是在QUIC端点之间可靠、按序、双向传递数据的一种抽象。在HTTP/3中,Web页面中的每个对象都与不同的流关联。每个连接具有一个连接ID,每个流也具有流ID。这些ID都包含在QUIC分组首部中。流的数据可以包括一个QUIC报文段,由UDP传输。流控制传输协议(Steam Control Transmission Protocol,SCTP)是更早的、面向报文的协议,是在单一的SCTP连接上复用多条应用级“流”的先驱。SCTP具有TCP和UDP的共同优点,可支持一些新的应用,如IP电话。限于篇幅,这里不再介绍。
·可靠的、TCP友好的拥塞控制下的数据传输。QUIC为每个QUIC流分别提供可靠的数据传输。相比之下,HTTP/1.1通过单一的TCP连接发送多个HTTP请求。TCP提供可靠的按序交付,所有HTTP请求必须有序递交给目标HTTP服务器。因此,如果其中一个HTTP请求有数据丢失,剩下的HTTP请求就不能继续发送,直到确认已经正确地重新传输了丢失的数据。这就带来了线路头端阻塞(HOL)。而QUIC为每个流提供可靠按序交付,一个UDP段的丢失只会影响到包含该报文段中的丢失的数据的那些流。其它流中的HTTP报文仍然可以继续收发,不受影响。QUIC使用与TCP类似的确认机制提供可靠数据传输。
QUIC的拥塞控制基于TCP NewReno——这是一个TCP Reno的轻微修改版本。
再次强调,QUIC是提供可靠的、拥塞控制下的数据传输的应用层协议。QUIC的作者着重指出,QUIC的更新能够做到“应用更新的时间尺度”(application-update timescales),也就是说,远远快于TCP和UDP的更新。

本文标签: 2021