1 TCP连接的建立和终止
1.1 三路握手
建立一个TCP连接时会发生下述情形。
(1)服务器必须准备好接受外来的连接。这通常是通过调用socket、bind和listen这3个函数来完成,我们称之为被动打开(passive open)。
(2)客户必须通过调用connect发起主动打开(active open)。这导致客户TCP发送一个SYN(同步)分节,它告诉服务器客户将在(待建立的)连接中发送的数据的初始序列号。通常SYN分节不携带数据,其所在IP数据报只含有一个首部、一个TCP首部及可能有的TCP选项。
(3)服务器必须确认(ACK)客户的SYN,同时自己也得发送一个SYB分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器在单个分节中发送SYN和对客户SYN的ACK(确认)。
(4)客户必须确认服务器的SYN。
这种交换至少需要3个分组,因此称之为TCP的三路握手(three-way handshake)。图2-2展示了所交换的3个分节。
图2-2 TCP的三路握手
图2-2给出的客户的初始序列号为j, 服务器的初始序列号为k。ACK中的确认号是发送这个ACK的一端所期待的下一个序列号。因为SYN占据一个字节的序列号空间,所以每一个SYN的ACK中的确认号是该SYN的初始序列号加1。类似地,每一个FIN(表示结束)的ACK中的确认号为该FIN的序列号加1。
1.2 TCP选项
每一个SYN可以含有多个TCP选项。下面是常用的TCP选项。
l MSS选项。
发送SYN的TCP一端使用本选项通告对端它的最大分节大小(maxinum sement size)即MSS,也就是它在本连接的每个TCP分节中愿意接受的最大数据量。发送端TCP使用接收端的MSS值作为所发送分节的最大大小。
l 窗口规模选项。
l 时间戳选项。
1.3 TCP连接终止
TCP建立一个连接需要3个分节,终止一个连接则需要4个分节。
1) 某个应用进程首先调用close,我们称该端执行主动关闭(active close)。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
2) 接收到这个FIN的对端执行被动关闭(passive close)。这个FIN由TCP确认。它的接收也作为一个文件结束符(end-of-file)传递给接收端应用进程(放在已排除等候应用进程接收的任何其他数据之后),因为FIN的接收意味着接收端应用进程在相应连接上再无额外数据可以接收。
3) 一段时间后,接收到这个文件结束符的应用进程调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
4) 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。
既然每个方向上都需要一个FIN和一个ACK,因此通常需要4个分节。我们使用限定词“通常”是因为:某些情形下步骤1的FIN随数据一起发送;另外,步骤2和步骤3发送的分节都出自执行被动关闭那一端,有可能被合并成一个分节。图2-3展示了这些分组。
图2-3 TCP连接关闭时的分组交换
类似SYN,一个FIN也占据1个字节的序列号空间。因此,每个FIN的ACK确认号就是FIN的序列号加1。
当套接字被关闭时,其所在端TCP各自发送了一个FIN。我们在图中指出,这是应用进程调用close而发生的,不过需认识到,当一个Unix进程无论自愿地(调用exit或从main函数返回)还是非自愿地(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN。
图2-3展示了客户端执行主动关闭的情形,不过我们指出,无论是客户还是服务器,任何一端都可以执行主动关闭。通常情况是客户执行主动关闭,但是某些协议(譬如值得注意的HTTP/1.0)却由服务器执行主动关闭。
1.4 TCP状态转换图
TCP涉及连接建立和连接终止的操作可以用状态转换图(state transition diagram)来说明,如图2-4所示。
TCP为一个连接定义了11种状态,并且TCP规则规定如何基于当前状态及在该状态下所接收的分节从一个状态转换到另一个状态,举例来说,当某个应用进程在CLOSED状态下执行主动打开时,TCP将发送一个SYN,且新的状态是SYN_SENT。如果这个TCP接着接收到一个带ACK的SYN,它将发送一个ACK,且新的状态是ESTABLISHE。这个最终状态是绝大多数数据传送发生的状态。
自ESTABLISHED状态引出的两个箭头处理连接的终止。如果某个应用进程在接收到一个FIN之前调用close(主动关闭),那就转换到FIN_WAIT_1状态。但如果某个应用进程在ESTABLISHED状态期间接收到一个FIN(被动关闭),那就转换到CLOSE_WAIT状态。
我们用粗实线表示通常的客户状态转换,用粗虚线表示通常的服务器状态转换。图中还注明存在两个我们未曾讨论的转换:一个为同时打开(simultaneous open),发生在两端几乎同时发送SYN并且这两个SYN在网络中交错的情形下,另一个为同时关闭(simultaneous close),发生在两端几乎同时发送FIN的情形下。它们是可能发生的,不过非常罕见。
展示状态转换图的原因之一是给出11种TCP状态的名称。这些状态可使用netstat显示,它是一个在调试客户/服务器应用时很有用的工具。
图2-4 TCP状态转换图
1.5 观察分组
图2-5展示了一个完整的TCP连接所发生的实际分组交换情况,包括连接建立、数据传送和连接终止3个阶段。图中还展示了每个端点所历经的TCP状态。
本例中的客户通告一个值为536的MSS(表明该客户只实现了最小重组缓冲区大小),服务器通告一个值为1460的MSS(以太网上IPV4的典型值)。不同方向上MSS值不相同不成问题。
一旦建立一个连接,客户就构造一个请求并发送给服务器。这里我们假设该请求适合于单个TCP分节(即请求大小小于服务器通告的值为1460的MSS)。服务器处理该请求并发送一个应答,我们假设应答也适合于单个分节(本例即小于536字节)。图中使用粗箭头来给表示这两个数据分节。注意,服务器对客户请求的确认是伴随其应答发送的。这种做法称为捎带(piggybacking),它通常在服务器处理请求并产生应答的时间少于200ms时发生。如果服务器耗时用更长时间,譬如说1s,那么我们将看到先是确认后是应答。
图中最后展示的是终止连接的4个分节。注意,执行主动关闭的那一端(本例中为客户)进入我们将在下一节中讨论的TIME_WAIT状态。
图2-5中值得注意的是,如果该连接的整个目的仅仅是发送一个单分节的请求和接收一个单分节的应答,那么使用TCP有8个分节的开销。如果改用UDP,那么只需交换两个分组:一个承载请求,一个承载应答。然而从TCP切换到UDP将丧失TCP提供给应用进程的全部可靠性,迫使可靠服务的一大堆细节从传输层(TCP)转移到UDP应用进程。TCP提供的另一个重要特性即拥塞控制也必须由UDP应用进程处理。尽管如此,我们仍然需要知道许多网络应用是使用UDP构建的。因为它们需要交换的数据量较少,而UDP避免了TCP连接建立和终止所需的开销。
1.6 TIME_WAIT状态
毫无疑问,TCP中有关网络编程最不容易理解的是它的TIME_WAIT状态。在图2-4中我们看到执行主动关闭的寻端经历了这个状态。该端点停留在这个状态的持续时间是最长分节生命期(maximum segment lifetime, MSL)的两倍,有时候称这为2MSL。
任何TCP实现都必须为MSL选择一个值。RFC 1122【braden 1989】的建议值是2分钟,不过源自Berkeley的实现传统上改用30秒这个值。这意味着TIME_WATI状态的持续时间在1分钟到4分钟之间。MSL是任何IP数据报能够在因特网中存活的最长时间。我们知道这个时间是有限的,因为每个数据报含有一个称为跳限(hop limit)的8位,它的最大值为255.。尽管这是一个跳数限制而不是真正的时间限制,我们仍然假设:具有最大跳限(255)的分组在网络中存在的时间不可能超过MSL秒。
分组在网络中“迷途”通常是路由异常的结果。某个路由器崩溃或某个两个路由器之间的某个链路断开时,路由协议需要花费数秒钟到数分钟的时间才能稳定并找出另一条通路。在这段时间内在可能发生路由循环(路由器A把分组发送给路由器B,而B再把它们发送回A),我们关心的分组可能就此陷入这样的循环。假设迷途的分组是一个TCP分节,在它迷途期间,发送端TCP超时并重传该分组,而重传的分组却通过某条候选路径到达最终目的地。然而不久后(自迷途的分组开始其起最多MSL秒内)路由循环修复,早先迷失在这个循环中的分组最终也被送到目的地。这个原来的分组称为迷途的重复分组(lost duplicate)或漫游的重复分组(wandering duplicate)。TCP必须正确处理这些重复的分组。
TIME_WAIT状态有两个存在的理由:
(1)可靠地实现TCP全双工连接的终止。
(2)允许老的重复分节在网络中消逝。
第一个理由可以通过查看图2-5并假设最终的ACK丢失了来解释。服务器将重新发送它的最终那个FIN,因此客户必须维护状态信息,以允许它重新发送最终那个ACK。要是客户不维护状态信息,它将响应以一个RST(另外一种类型的TCP分节),该分节将被服务器解释成一个错误。如果TCP打算执行所有必要的工作以彻底终止某个连接上两个方向的数据流(即全双工关闭),那么它必须正确处理连接终止序列4个分节中任何一个分节中任何一个分节丢失的情况。本例子也说明了为什么执行主动关闭的寻一端是牌TIME_WAIT状态的那一端:因为可能不得不重传最终那个ACK的就是那一端。
为理解存在TIME_WAIT状态的第二个理由,我们假设在12.106.32.254的1500端口和206.168.33.22的21端口之间有一个TCP连接。我们关闭这个连接,过一段时间后在相同的IP地址和端口之间建立另一个连接。后一个连接称为前一个连接的化身(incarnation),因为它们的IP地址和端口号都相同。TCP必须防止来自某个连接的老的重复分组在该连接已终止后再现,从而被误解成属于同一连接的某个新的化身。为做到这一点。TCP将不给处于TIME_WAIT状态的连接发起新的化身。既然TIME_WAIT状态的持续时间是MSL的2倍,这就足以让某个方向上的分组最多存活MSL秒即被丢弃,另一个方向上的应答最多存活MSL秒也被丢弃。通过实施这个规则,我们就能保证每成功建立一个TCP连接时,来自该连接先前化身的老的重复分组都已在网络中消逝了。
2. SCTP关联的建立和终止
与TCP一样,SCTP也是面向连接的,因而也有关联的建立与终止的握手过程。不过SCTP的握手过程不同于TCP。
2.1 四路握手
建立一个SCTP关联的时候会发生下述情形(类似于TCP)。
(1)服务器必须准备好接受外来的关联。这通常通过调用socket、bind和listen这3个函数来完成,称为被动打开。
(2)客户通过调用connect或者发送一个隐式打开该关联的消息进行主动打开。这使得客户SCTP发送一个INIT消息(初始化),该消息告诉服务器客户的IP地址清单、初始序列号、用于标识关联中所有分组的起始标记、客户请求的外出流的数目以及客户能够支持的外来流数目。
(3)服务器以一个INIT ACK消息确认客户的INIT消息,其中含有服务器的IP地址清单、初始序列号、起始标记、服务器请求的外出流的数目、服务器能够支持的外来流的数目以及一个状态cookie。状态|cookie包含服务器用于确信本关联有效所需的所有状态,它是数字化签名过的,以确保有效性。
(4)客户以一个COOKIE ECHO消息回射服务器的状态cookie。除COOKIE ECHO外,该消息可能在同一个分组中还捆绑了用户数据。
(5)服务器以一个COOKIE ACK消息确认客户回射的cookie是正确的,本关联于是建立。该消息也可能在同一个分组中还捆绑了用户数据。
以上交换过程至少需要4个分组,因此称之为SCTP的四路握手(four-way handshake).图2-6展示了这4个分节。
SCTP的四路握手在很多方面类似于TCP的三路握手,差别主要在于作为SCTP整体一部分cookie的生成。INIT(随其众多参数一道)承载一个验证标记Ta和一个初始序列号J.在关联的有效期内,验证标记Ta必须在对端发送的每个分组中出现。初始序列号J用作承载用户数据DATA块的起始序列号。对端也在INIT ACK中承载一个验证标记Tz,在关联的有效期内,验证标记Tz也必须在其发送的每个分组中出现。除了验证标记Tz和初始序列号K外,INIT的接收端还在作为响应的INIT ACK中提供一个cookie C。该cookie包含设置本SCTP关联所需的所有状态,这样服务器的SCTP栈就不必保存所关联客户的有关信息。
四路握手过程结束时,两端各自选择一个主目的地(primary destination address)。当不存在网络故障,主目的地址将用作数据要发送的默认目的地。
2.2 关联终止
SCTP不像TCP那样允许“半关闭”的关联。当一端关闭某个关联时,另一端必须停止发送新的数据。关联关闭请求的接收端发送完已经排除的数据(如果有的话)后,完成关联的关闭。图2-7展示了这一交换过程。
SCTP没有类似于TCP的TIME_WAIT状态,因为SCTP使用了验证标记。所有后续块都在捆绑它们的SCTP分组的公共首部标记了初始的INIT块和INIT ACK块中作为起始标记交换的验证标记:由来自旧连接的块通过所在SCTP分组的公共首部间接携带的验证标记对于新连接来说是不正确的。因此,SCTP通过放置验证标记值就避免了TCP在TIME_WAIT状态保持整个连接的做法。
2.3 SCTP状态的转换图
SCTP涉及关联建立和关联终止的操作可以用状态转换图(state transition diagram)来说明。如图2-8所示。
从ESTABLISHED状态引出的两个箭头处理关联的终止。如果某个应用进程在接收到一个SHUTDOWN之前调用close(主动关闭),那就转换到SHUTDOWN-PENDING状态。否则,如果某个应用进程在ESTABLISHED状态期间接收到一个SHUTDOWN-RECEIVED状态。
2.4 观察分组
图2-9展示了一个作为样例 的SCTP关联所发生的实际分组交换情况,包括关联建立、数据传送和关联终止3个阶段。图中还展示了每个端点所历经的SCTP状态。
图2-9 SCTP关联中的分组交换
本例中,客户在COOKIE ECHO块所在分组中捎带了它的第一个DATA块,服务器则在作为应答的COOKIE_ACK块所在分组中捎带了数据。一般而言,当网络应用采用一到多接口式时,COOKIE ECHO通常会捎带一个或多个DATA块。
SCTP分组中信息的单位称为块(chunk)。块是自描述的,包含一个块类型、若干个块标记和一个块长度。这样做方便了多个块的绑缚,只要 把它们简单地组合到一个SCTP外出消息中。
2.5 SCTP选项
SCTP使用参数和块方便增设可选特性。新的特性通过添加这两个 条目之一加以定义,并允许通常的SCTP处理规则汇报未知的参数和未知的块。参数类型字段和块类型字段的高两位指明SCTP接收端如何处置未知的参数或未知的块。
当前如下两个对SCTP的扩展正在开发中。
(1)动态地址扩展,允许协作的SCTP端点从已有的某个关联中动态增删IP地址。
(2)不完全可靠性扩展,允许协作的SCTP端点在应用进程的指导下限制数据的重传。当一个消息变得过于陈旧而无须发送时(按照应用进程的指导),该消息将被跳过而不再发送到对端。这意味着不是所有数据都确保到达关联的另一端。
端口号
任何时候,多个进程可能同时使用TCP、UDP和SCTP这3种传输层协议中的任何一种。这3种协议都使用16位整数的端口号(port number)来区分这些进程。
当一个客户想要跟一个服务器联系时,它必须标识想要与之通信的这个服务器。TCP、UDP、和SCTP定义了一组众所周知的端口(sell-known prot),用于标识众所周知的服务。
另一方面,客户通常使用短期存活的临时端口(ephmeral port)。这些端口号通常由传输层协议自动赋予客户。客户通常不关心其临时端口的具体值,而只需确信该端口在所主机中是唯一的就行。传输协议的代码确保这种唯一性。
套接字对
一个TCP连接的套接字对(socket pair)是一个定义该连接的两个端点的四元组:本地IP地址、本地TCP端口、外地IP地址、外地TCP端口号。套接字对唯一标识一个网络上的每个TCP连接。就SCTP而言,一个关联由一组本地IP地址、一个本地端口、一组外地IP地址、一个外地端口标识。在两个端点均非多宿这一最简单的情形下,SCTP与TCP所用的四元组套接字对一致。然而在某个关联的任何一个端点为多宿的情形下,同一个关联可能需要多个四元组标识(这些四元组的IP地址各不相同,但端口号是一样的)。
标识每个端点的两个值(IP地址和端口号)通常称为一个套接字。
缓冲区大小及限制
下面我们将介绍一些影响IP数据报大小的限制。我们首先介绍这些限制,然后就它们如何影响应用进程能够传送的数据进行综合分析。
l IPv4数据报的最大大小是65535字节,包括IPv4首部。这是因为如图A-1所示其总长度字段占据16位。
l IPv6数据报的最大大小是65575字节,包括40字节的IPv6首部。这是因为如头A-2所示其净荷长度字段占据16位。注意,IPv6的净荷长度字段不包括IPv6首部,而IPv4的总长度字段包括IPv4首部。
IPv6有一个特大净荷(jumbo payload)选项,它把净荷长度扩展到32位,不过这个选 项需要MTU(maxinum transmission unit, 最大传输单元)超过65535的数据链路提供 支持。(这是为主机到主机的内部连接而设计的,譬如HIPPI,它们通常没有内存的 MTU。)
l 许多网络有一个可由硬件规定的MTU.举例来说,以太网的MTU是1500字节,另有一些链路(例如使用PPP协议的点到点的链路)其MTU可以人为配置。较老的SLIP链路通常使用1006字节或296字节的MTU。
IPv4要求的最小链路MTU是68字节。这允许最大的IPv4首部(包括20字节的固定长度部分和最40字节的选项部分)拼接最小的片段(IPv4首部中片段偏移字段以8个字节为单位)。IPv6要求的最小链路MTU为1280字节。IPv6可以运行在MTU小于此最小值的链路上,不过需要特定于链路的分片和重组功能,以使得这些链路看起来具有至少为1280字节的MTU。
l 在两个主机之间的路径中最小的MTU称为路径MTU(path MTU)。1500字节的以太网MTU是当今常见的路径MTU。两个主机之间相反的两个方向上路径的MTU可以不一致,因为在因特网中路由选择往往是不对称的,也就是说从A到B的路径与从B到A的路径可以不相同。
l 当一个IP数据报将从某个接口送出时,如果它的大小超过相应链路的MTU,IPv4T和IPv6都将执行分片(fragmentation)。这些片段在到达最终目的地之前通常不会被重组(reassembling)。IPv4主机对其产生的数据报执行分片,IPv4路由器则对其转发的数据报执行分片。然而IPv6只有主机对其产生的数据报执行分片,IPv6路由器不对其转发的数据报执行分片。
我们必须小心这些术语的使用。一个标记为IPv6路由器的设备可能执行分片,不过只是对于那些由它产生的数据报,而绝不是对于那些由它转发的数据报。当该设备产生IPv6数据报时,它实际上作为主机动作。举例来说,大多数路由器支持Telnet协议,管理 员应用它来配置路由器。由路由器的Telnet服务器产生的IP数据报是由路口 产生的,而不是由路由器转发的。
l IPv4首部的“不分片(don’t fragment)”位(即DF位)若被设置,那么不管是发送这些数据报的主机还是转发它们的路由器,都不允许对它们分片。当路由器接收到一个超过其外出链路MTU大小且设置了DF位的IPv4数据报时,它将产生一个ICMPv4“destination unreachable, fragmentation needed but DF bit set”(目的地不可达,需分片但DF位已设置)出错消息。
既然IPv6路由器不执行分片,每个IPv6数据报于是隐含一个DF位。当IPv6路由器接 收到一个超过其外出链路MTU大小的IPv6数据报时,它将产生一个ICMPv6“packet too big”(分组太大)出错消息。
IPv4的CF位和IPv6的隐含CF位可用于路径MTU发现,举例来说,如果基于IPv4 的TCP使用该技术,那么它将在所改善的所有数据报中设置CF位。如果 某个蹭路由 器返回一个ICMP“destination unreacheable, fragmentation needed but DF bit set”错误, TCP就减小每个数据报的数据量的重传。路径MTU发现对于IPv4是可选的,然而IPv6 的所有实现要么必须支持它,要么必须问题使用最小的MTU发送IPv6数据报。
路径MTU发现在如今的因特网上是有问题的,许多防火墙丢弃所有ICMP消息,包括用于路径MTU发现的上述消息。这意味着TCP永远得不到要求它降低所发送数据量的信号,IETF已经开始尝试定义不依赖于ICMP出错消息的另一种路径MTU发现方法。
l IPv4和IPv6都定义了最小重组缓冲区大小(miinimum reassembly buffer size),它是IPv4或IPv6的任何实现都必须保证支持的最小数据报大小。其值对于IPv4为476字节,对于IPv6为1500字节。例如,就IPv4而言,我们不能判定某个给定目的地能否接受577字节的数据报。为此有许多使用UDP的IPv4网络应用(如DNS、RIP)避免产生大于这个大小的数据报。
l TCP有一个MSS(maximum segment size, 最大分节大小),用于向对端TCP通告对端在每个分节中能发送的最大TCP数据量。在图2-5中我们看到过SYN分节上的MSS选项。MSS的目的是告诉对端其重组缓冲区大小 的实际值,从而试图避免分片。MSS经常设置成MTU减去IP和TCP首部的固定长度。在以太网中使用IPv4 的MSS值为1460,使用IPv6的MSS值为1440(两者的TCP首部都是20字节,但IPv4首部是20字节,IPv6首部却是40字节)。在TCP的MSS选项中,MSS值是一个16位的字段,限定其最大值为65535。这对于IPv4是适合的,因为IPv4数据报中的最大TCP数据量为65495(65535减去IPv4首部的20字节和TCP首部的20字节)。然而对于具有特大净荷选项的IPv6,却需要使用另外一种技巧。首先,没有特大净荷选项的IPv6数据报中的最大TCP数据量为65515(65535减去TCP首部的20字节)。65535这个MSS值于是被视为表示“无限”的一个特殊值。该值只在用到特大净荷选项时才使用,不过这种情况却要求实际的MTU超过65535。其次,如果TCP使用特大净荷选项,并且接收到的对端通行的MSS为65535,那么它所发送数据报的大小限制就是接口MTU。如果这个值太大(也就是说所在路径中某个链路的MTU比较小),那么路径MTU发现功能将确定这个较小值。
l SCTP基于到对端所有地址发现的最小路径MTU保持一个分片点。这个最小MTU大小用于反较大的用户消息分割成较小的能够以单个IP数据报发送的若干片段。SCTP_MAXSEG套接字选项可以影响该值,使得用户能够请求一个更小的分片点。
TCP输出
图2-15展示了某个应用进程写数据到一个TCP套接字时发生的步骤。
每一个TCP套接字有一个发送缓冲区,我们可以使用SO_SNDBUF套接字选项来更改该缓冲区的大小。当某个应用进程调用write时,内核从该应用进程的缓冲区中复制所有数据到所写套接字的发送缓冲区。如果该套接字的发送缓冲区容不下该应用进程的所有数据(或是应用进程 的缓冲区大于套接字的发送缓冲区,或是套接字的发送缓冲区中已有其它数据),该进程将被投入睡眠。这里假设该套接字是阻塞的,它是通常的默认设置。内核将不从write系统调用返回,直到应用进程缓冲区中的所有数据都复制到套接字发送缓冲区。因此,从写一个TCP套接字的write调用成功返回仅仅表示我们可以重新使用原来的进程缓冲区,并不表明对端的TCP或应用进程已接收到数据。
图2-15 应用进程写TCP套接字时涉及的步骤和缓冲区
这一端的TCP提取套接字发送缓冲区中的数据并把它发送给对端TCP,其过程基于TCP数据传送的所有规则。对端必须确认收到的数据,伴随来自对端的ACK的不断到达,本端TCP到此才能从套接字发送缓冲区中丢弃已确认的数据。TCP必须为已发送的数据保留一个副本,直到它被对端确认为止。
本端TCP以MSS大小的或更小的块把数据传递人IP,同时给每个数据块安上一个TCP首部以构成TCP分节,其中MSS或是由对端通行的值,或是536(若对端未发送一个MSS选项)。(536是IPv4最小重组缓冲区字节数576减去IPv4首部字节数20和TCP首部字节数20的结果。)IP给每个TCP分节安上一个IP首部以构成IP数据报,并按照其目的IP地址查找路由表项以确定外出接口,然后把数据报传递给相应的数据链路。IP可能在把数据报传递给数据链路之前将其分片,不过我们已经谈到MSS选项的目的之一就是试图避免分片,较新的实现还使用了路径MTU发现功能。每个数据链路都有一个输出队列,如果该队列已满,那么新到的分组将被丢弃,并沿协议栈向上返回一个错误:从数据链路到IP,再从IP到TCP。TCP将注意到这个错误,并在以后某个时刻重传相应的分节。应用进程并不知道这种暂时的情况。
UDP输出
图2-16展示了某个应用进程写数据到一个UDP套接字中时发生的步骤。
这一次我们以虚线框展示套接字发送缓冲区,因为它实际上并不存在。任何UDP套接字都有发送缓冲区大小(我们可以使用SO_SNDBUF套接字选项更改它),不过它仅仅是可写到该套接字的UDP数据报的大小上限。如果一个应用进程写一个大于套接字缓冲区大小的数据报,内核将返回该进程一个EMSGSIZE错误。既然UDP是不可靠的,它不必保存应用进程数据的一个副本,因此无需一个真正的发送缓冲区。(应用进程的数据在沿协议栈向下传递时,通常被得到某种格式的一个内核缓冲区中,然而当该数据被发送之后,这个副本就被数据链路层丢弃了。)
图2-16 应用进程写UDP套接字时涉及的步骤与缓冲区
这一端的UDP简单地给来自用户的数据安上它的8字节的首部以构成UDP数据报,然后传递给IP。IPv4或IPv6给UDP数据报安上相应的IP首部以构成IP数据报,执行路由操作确定外出接口,然后或者直接把数据报加入数据链路层输出队列(如果适合于MTU),或者分片后再把每个片段加入数据链路层的输出队列。如果某个UDP应用进程发送大量数据报(譬如说2000字节的数据报),那么它们相比TCP应用数据更有可能被分片,因为TCP会把应用数据划分成MSS大小的块,而UDP却没有对待的手段。
从写一个UDP套接字的sendto调用成功返回表示所写的数据报或其所有片段已被加入数据链路层的输出队列。如果该队列没有足够的空间存放数据报或它的某个片段,内核通常会返回一个ENOBUFS错误给它的应用进程。
不幸的是,有些UDP的实现不返回这种错误,这样甚至数据报未经发送就被丢弃的情况应用进程也不知道。
SCTP输出
图2-17展示了某个应用进程写数据到一个SCTP套接字时发生的步骤。
既然SCTP是与TCP类似的可靠协议,它的套接字也有一个发送缓冲区,而且跟TCP一样,我们可以使用SO_SNDBUF套接字选项来更改该缓冲区的大小。当某个应用进程调用write时,内核从该应用进程的缓冲区中复制所有数据到所写套接字的发送缓冲区。如果该套接字的发送缓冲区容不下该应用进程的所有数据(或是应用进程 的缓冲区大于套接字的发送缓冲区,或是套接字的发送缓冲区中已有其它数据),该进程将被投入睡眠。这里假设该套接字是阻塞的,它是通常的默认设置。内核将不从write系统调用返回,直到应用进程缓冲区中的所有数据都复制到套接字发送缓冲区。因此,从写一个SCTP套接字的write调用成功返回仅仅表示我们可以重新使用原来的进程缓冲区,并不表明对端的TCP或应用进程已接收到数据。
图2-17 应用进程写SCTP套接字时涉及的步骤和缓冲区
这一端的TCP提取套接字发送缓冲区中的数据并把它发送给对端TCP,其过程基于TCP数据传送的所有规则。本端SCTP必须等待SACK,在累积确认点超过已发送的数据后,才可以从套接字缓冲区中删除该数据。
小结
UDP是一个简单、不可靠、无连接的协议,而TCP是一个复杂、可靠、面向连接的协议。SCTP组合了这两个协议的一些特性,并提供了TCP所不具备的额外特性。尽管绝大多数因特网应用(Web, FTP和电子邮件)使用TCP,但这3个协议对传输层都是必要的。
TCP使用三路握手建立连接,使用四分组交换序列终止连接。当一个TCP连接被建立时,它从CLOSED状态转换到ESTABLISHED状态:当该连接被终止时,它又回到CLOSED状态。一个TCP连接可处于11种状态之一,其状态转换图给出了从一种状态转换到另一种状态的规则。理解状态转换图是使用netstat命令诊断网络问题的基础,也是理解当某个应用进程调用诸如connect、accept和close等函数时所发生过程的关键。
TCP的TIME_WAIT状态一直是一个造成网络编程人员混淆的来源。存在这一状态是为了实现TCP的全双工连接终止(即处理最终那个ACK丢失的情形),并允许老的重复分节从网络中消逝。
SCTP使用四路握手建立关联:使用三分组交换序列终止关联。当一个SCTP关联被建立时,它从CLOSED状态转换到ESTABLISHED状态;当该关联被终止时,它又回到CLOSED状态。一个SCTP关联可处于8种状态之一,其状态转换图给出从一种状态转换到另一种状态的规则。SCTP不像TCP那样需要TIME_WAIT状态,因为它使用了验证标记。