概述
传输层主要包括:TCP、UDP、SCTP(流控制传输协议)!
绝大多数客户端/服务器网络应用都使用TCP/UDP。SCTP是一个较新的协议,最初设计用于跨因特网传输电话信令。
这些传输协议都转而使用网络协议IP:或是IPv4,或是IPv6。尽管可以绕过传输层直接使用IPv4或IPv6,但这种技术(往往称为原始套接字)却极少使用。
UDP是一个简单的、不可靠的数据报协议,而TCP是一个复杂、可靠的字节流协议。SCTP与TCP类似之处在于它也是一个可靠的传输协议,但它还提供消息边界、传输级别多宿支持以及将头端阻塞减少到最小的一种方法。
必须了解这些传输层协议提供给应用进程的服务,才能弄清这些协议处理什么,应用进程中又需要处理什么。
下面主要讲述:TCP的三次握手、TCP的连接终止序列和TCP的TIME_WAIT状态,SCTP的四次握手和SCTP的连接终止,加上由套接字层提供的TCP、UDP和SCTP缓冲机制等等。
用户数据报协议(UDP)
UDP是一个简单的传输层协议。应用进程往一个UDP套接字写入一个消息,该消息随后被封装到一个UDP数据报,该UDP数据报进而又被封装为一个IP数据报,然后发送到目的地。
注意:
UDP不保证UDP数据报会到达其最终目的地,不保证各个数据报的先后顺序跨网络后保持不变,也不保证每个数据报只到达一次。
我们使用UDP进行网络编程所遇到的问题是它缺乏可靠性。
如果一个数据报到达了其最终目的地,但是校验和检测发现有错误,或者该数据报在网络传输中被丢弃了,它就无法被投递给UDP套接字,也不会被源端自动重传。如果想要确保一个数据报到达其目的地,可以往应用程序中添置一大堆的特性:来自对端的确认、本端的超时与重传等。
每个UDP数据报都有一个长度。如果一个数据报正确地到达其目的地,那么该数据报的长度将随数据一道传递给接收端应用进程。
注意:
- “上述所说的数据报到达其最终目的地”是指数据报被接收端的网络接口接收,并且被路由到正确的IP地址上,但并未将其传递给UDP套接字(应用层)。
- UDP是无连接的服务,因为UDP客户端与服务器之间不必存在任何长期的关系。举例来说,一个UDP客户端可以创建一个套接字并发送一个数据报给一个给定的服务器,然后立即用同一个套机字发送另一个数据报给另一个服务器。同样地,一个UDP服务器可以用同一个UDP套接字从若干个不同的客户端接受数据报,每个客户端一个数据报。
传输控制协议(TCP)
TCP提供客户端与服务器之间的连接。TCP客户端先与某个给定的服务器建立连接,再使用该连接与服务器交换数据,然后终止这个连接。
TCP提供了可靠性。当TCP向另一端发送数据时,它要求对端返回一个确认。如果没有收到确认,TCP就自动重传数据并等待更长的时间。在数次重传失败后,TCP才放弃,如此在尝试发送数据上所花的总时间一般是4~10分钟(依赖于具体实现)。
注意:
TCP也不是100%可靠的协议,它提供的是数据的可靠性或故障的可靠通知。
动态估算(RTT)
TCP含有用于动态估算客户端与服务器之间的往返时间(round-trip time,RTT)的算法,以便它知道等待一个确认需要多少时间。
举例来说,RTT在一个局域网上大约是几毫秒,跨越一个广域网则可能是数秒钟。另外,因为RTT受网络流通各种变化因素影响,TCP还持续估算一个给定连接的RTT。
数据排序
TCP通过给其中每个字节关联一个序列号对所发送的数据进行排序。
举例来说,假设一个应用写2048字节到一个TCP套接字,导致TCP发送2个分节:
- 第一个分节所含数据的序列号为1~1024。
- 第二个分节所含数据的序列号为1025~2048。
注意:
- 分节是TCP传递给IP的数据单元。
- 如果这些分节非顺序到达,接收端TCP将先根据它们的序列号重新排序,再把结果数据传递给接收应用。
- 如果接收端TCP收到来自对端的重复数据(譬如对端认为一个分节已丢失并因此重传,而这个分节并没有真正丢失,只是网络通信过于拥挤),它可以根据序列号判定数据是重复的,从而丢弃重复数据。
流量控制
TCP提供流量控制(flow control)。TCP总是告知对端,在任何时刻它一次能够从对端接受多少字节的数据,这称为通告窗口。
在任何时刻,该窗口指出接收缓冲区中当前可用的空间量,从而确保发送端发送的数据不会使接收缓冲区溢出。
该窗口时刻动态变化:当接收到来自发送端的数据时,窗口大小就减小,但是当接收端应用从缓冲区中读取数据时,窗口大小就增大。
通告窗口大小减小到0是有可能的:当TCP对应某个套接字的接收缓冲区已满,导致它必须等待应用从该缓冲区读取数据时,方能从对端再接受数据。
TCP连接是全双工的,这意味着在一个给定的连接上,可以在任何时刻在进出两个方向上既发送数据又接收数据。因此,TCP必须为每个数据流方向跟踪诸如序列号和通告窗口大小等状态信息。
建立一个全双工连接后,需要的话可以把它转换成一个单工连接。
注意:
- UDP不提供流量控制,让较快的UDP发送端以一个UDP接收端难以跟上的速度发送数据报是非常容易的。
- UDP可以是全双工的。
流控制传输协议(SCTP)
SCTP在客户端可服务器之间提供关联,并像TCP那样给应用提供可靠性、排序、流量控制以及全双工的数据传送。
问1:SCTP和TCP的区别是什么?
答:二者区别如下:
- SCTP中使用“关联”一词取代“连接”是为了避免这样的内涵:一个连接只涉及两个IP地址之间的通信。一个关联指代两个系统之间的一次通信,它可能因为SCTP支持多宿而涉及不止两个IP地址。
- TCP是面相字节流的,而SCTP是面向消息的。它提供各个记录的按序递送服务。与UDP一样,由发送端写入的每条记录的长度随数据一道传递给接收端应用。
- SCTP能够在所连接的端点之间提供多个流,每个流各自可靠地按序递送消息。一个流上某个消息的丢失不会阻塞同一关联其他流上消息的投递。这种做法与TCP刚好相反,就TCP而言,在单个字节流中任何位置的字节丢失都将阻塞该连接上其后所有数据的递送,直到该丢失被修复为止。
此外,SCTP还提供多宿特性,使得单个SCTP端点能够支持多个IP地址。该特性可以增强应对网络故障的健壮性。
问2:SCTP的多宿特性是指什么?多流特性又是指什么?
- 多宿特性是指一个SCTP端点可以通过多个IP地址到达。这意味着两个SCTP端点在建立关联后,数据可以通过不同的物理通路进行传送。例如,一个服务器可能同时拥有有线网络接口和无线网络接口,当有线网络出现故障时,SCTP可以自动切换到无线网络接口继续传输数据,从而提高数据传输的可靠性和灵活性。
- 多流特性是指SCTP支持在一个关联中建立多个独立的流,每个流都可以独立地进行数据传输(每个请求都被分配到一个独立的流中处理)。这种特性使得SCTP能够更有效地利用网络资源,提高数据传输的并行性和效率。
注:多宿特性意在增强网络健壮性,所以多宿中的多个IP地址通常是指一台物理设备上的不同网络接口。
TCP在路由协议的辅助下,也可以获取类似的健壮性。
SCTP的多宿特性允许主机(而不仅仅是路由器)也多宿,而且允许多宿跨越不同的服务供应商发生,这些基于路由的TCP多宿方法都无法做到。
TCP连接的建立和终止
三次握手
建立一个TCP连接时会发生如下情形:
- 服务器必须准备好接受外来的连接。这通常通过调用socket、bind、listen这3个函数来完成,我们称之为被动打开。
- 客户端通过调用connect发起主动打开。这导致客户端TCP发送一个SYN(同步)分节,它告诉服务器客户端将在(带建立的)连接中发送的数据的初始序列号。通常SYN分节不携带数据,其所在IP数据报只含有一个IP首部、一个TCP首部以及可能有的TCP选项。
- 服务器必须确认(ACK)客户端的SYN,同时自己也得发送一个SYN分节,它含有服务器将在同一连接中发送的数据的初始序列号。服务器在单个分节中发送SYN和对客户端SYN的ACK(确认)。
- 客户端必须确认服务器的SYN。
这种交换至少需要3个分组,因此称之为TCP的三次握手。
注意:
- 上图中给出的客户端的初始序列号为J,服务器的初始序列号为K。ACK中的确认号是发送这个ACK的一端所期待的下一个序列号。
- 因为SYN占据一个字节的序列号空间,所以每一个SYN的ACK中的确认号就是该SYN的初始序列号加1。类似地,每一个FIN(表示结束)的ACK中的确认号为该FIN的序列号加1。
扩展:
建立TCP连接就好比一个电话系统。socket函数等同于有电话可用。bind函数是在告诉别人你的电话号码,这样他们可以呼叫你。listen函数是打开电话振铃,这样当有一个外来呼叫到达时,你就可以听到。connect函数要求我们知道对方的电话号码并拔打它。accept函数发生在被呼叫的人应答电话之时。由accept返回客户端的标识(即客户端的IP地址和端口号)类似于让电话机的呼叫者ID功能部件显示呼叫者的电话号码。然而两者的不同之处在于accept只在连接建立之后返回客户的标识,而呼叫者ID功能部件却在我们选择应答或不应答电话之前显示呼叫者的电话号码。如果使用域名系统DNS(见第11章),它就提供了一种类似于电话薄的服务。getaddrinfo类似于在电话薄中查找某个人的电话号码,getnameinfo则类似于有一本按照电话号码而不是按照用户名排序的电话薄。
四次挥手
TCP建立一次连接需要3个分节,终止一个连接需要4个分节。
四次挥手流程如下:
- 某个应用进程首先调用close,称该端执行主动关闭。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
- 接收到这个FIN的对端执行被动关闭。这个FIN由TCP确认。它的接收也作为一个文件结束符传递给接收端应用进程(放在已排队等候该应用进程接收的任何其他数据之后),因为FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
- 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
- 接收这个最终FIN的原发送端TCP(即执行主动关闭的一端)确认这个FIN。
注意:
- 因为每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。之所以说通常,是因为某些情形下步骤1中的FIN可能会随着数据一起发送。另外,步骤2和步骤3发送的分节都出自执行被动关闭的一端,有可能被合并为一个分节。
- 类似SYN,一个FIN也占1个字节的序列号空间。因此,每个FIN的ACK确认号就是这个FIN的序列号加1。
- 在步骤2和步骤3之间,从执行被动关闭一端到执行主动关闭一端流动数据是可能的,这被称为半关闭。
- 无论是客户端还是服务器,任何一端都可以执行主动关闭。
当套接字被关闭时,其所在端TCP各自发送了一个FIN,这是由应用进程调用close而发生的。
当一个Unix进程无论自愿地(调用exit或从main函数返回)还是被动地(收到一个终止本进程的信号)终止时,所有打开的描述符都会被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN。
TCP状态转换图
TCP涉及连接建立和连接终止的操作可以用状态转换图来表示:
TCP为一个连接定义了11种状态,并且TCP规则规定如何基于当前状态及在该状态下所接收的分节从一个状态转换到另一个状态。
示例:
当某个应用进程在CLOSED状态下执行主动打开时,TCP将发送一个SYN,且新的状态是SYN_SENT。如果这个TCP接着接收到一个带ACK的SYN,它将发送一个ACK,且新的状态是ESTABLISHED。这个最终状态是绝大多数数据传送发生的状态。
自ESTABLISHED状态引出的两个箭头处理连接的终止:
- 如果某个应用进程在接收到一个FIN之前调用close(主动关闭),那就转换到FIN_WAIT_1状态。
- 如果某个应用进程在ESTABLISHED状态期间接收到一个FIN(被动关闭),那就转换到CLOSE_WAIT状态。
图中存在两个未曾讨论过的状态:同时打开(发生在两端几乎同时发送SYN并且这两个SYN在网络中交错的情形下)、同时关闭(发生在两端几乎同时发送FIN的情形下)。
注意:
- 粗实线:表示客户端状态转换。
- 粗虚线:表示服务器状态转换。
- 应用:表示状态转换在应用进程发起操作时发生。
- 接收:表示状态转换在接收到分节时发生。
- 发送:表示这个转换发送过什么。
观察分组
下图展示了一个完整的TCP连接所发生的实际分组交换情况,包括连接建立、数据传送和连接终止3个阶段。图中还展示了每个端点所经历的TCP状态:
注意:
- 客户端通告值为536的MSS,表明客户端只实现了最小重组缓冲区大小,即客户端接收数据的单个分节为536字节。
- 服务器通告值为1460的MSS(以太网上的IPv4的典型值),即服务器接收数据的单个分节为1460字节。
- 不同方向上MSS值可以不相同。
- 建立连接(三次握手,3个分节)。
- 成功建立一个连接后,客户端就构造一个请求并发送给服务器。这里假设该请求适合于单个TCP分节(即请求大小小于服务器通告的值1460字节的MSS)。服务器处理该请求并发送一个应答,假设该应答也适合于单个分节(即小于536字节)。图中箭头表示这两个数据分节。
- 终止连接(四次挥手,4个分节)。
注意:
- 服务器对客户端请求的确认是伴随其应答发送的。这种做法称为捎带,它通常在服务器处理请求并产生应答的时间少于200ms时发生。如果服务器处理请求并产生应答的时间较长,如1s,则会先确认后应答。
问:网络应用是选择TCP还是UDP?
答:从上图可知,如果该连接的整个目的仅仅是发送一个单分节的请求和接收一个单分节的应答,那么使用TCP有8个分节的开销(除了请求和应答两个分节)。如果改用UDP,那么只需要交换两个分组:一个承载请求,一个承载应答。然而从TCP切换到UDP将丧失TCP提供的全部可靠性,迫使可靠服务的一大堆细节从传输层(TCP)转移到UDP应用层。此外,TCP提供的另一个重要特性即拥塞控制也必须由UDP应用进程来处理。
尽管如此,仍然有许多网络应用是使用UDP构建的,因为UDP需要交换的数据量较少,而且UDP避免了TCP连接建立和终止所需要的开销。
TIME_WAIT状态
TIME_WAIT状态通常发生在主动关闭的一端,且该状态持续时间是最长分节生命期(maximum segment lifetime,MSL)的两倍,有时候称之为2MSL。
任何TCP实现都必须为MSL选择一个值,RFC的建议值是2分钟,不过源自Berkeley的实现传统上改用30秒,这意味着TIME_WAIT状态的持续时间在1分钟到4分钟之间。MSL是任何IP数据报能够在因特网中存活的时间。
问:TIME_WAIT状态的作用是什么?
答:TIME_WAIT状态有两个存在的理由:
- 可靠地实现TCP全双工连接的终止。
- 允许老的重复分节在网络中消逝。
第一个理由解释:
假设上图中最后一个ACK丢失了,服务器将重新发送最终那个FIN,因此客户端必须维护状态信息,以允许它重新发送最终那个ACK。如果客户端不维护状态信息,它将响应一个RST(另一种类型的TCP分节),该分节将被服务器解释成一个错误。
第二个理由解释:
假设在12.106.32.254的1500端口和206.168.112.219的21端口之间有一个TCP连接。关闭这个连接后,在相同的IP地址和端口号之间建立另一个连接。后一个连接称为前一个连接的化身,因为它们的IP地址和端口号都相同。TCP必须防止来自上一个已终止的连接的重复分组在该连接中再现。而TIME_WAIT状态的持续时间是2MSL,这就足以让上一个连接中的分组最多存活MSL秒即被丢弃,另一个方向上的应答最多存活MSL秒也被丢弃。
注意:
上述所说的重复分组是指:发送端在发送一个TCP分节后,由于某个路由器崩溃或某两个路由器之间的某个链路断开时,路由协议需要花费数秒钟到数分钟的时间才能稳定并找到另一条通路,在这段时间内可能会超时重传该TCP分节,而重传的TCP分节却通过某条候选路径达到最终目的地。然而不久后(自迷途的TCP分节开始其旅程起最多MSL秒以内),路由循环被修复,早先迷失的TCP分节最总也被送到目的地,此时该分节被称为迷途的重复分组或漫游的重复分组。
端口号
任何时候,多个进程可能同时使用TCP、UDP和SCTP这3中传输层协议中的任何一种。这3种协议都使用16位整数的端口号来区分这些进程。
IANA(因特网已分配数值权威机构)维护着一个端口号分配状况的清单。端口号被划分为以下3段:
- 众所周知的端口为0~1023:这些端口由IANA分配和控制。可能的话,相同端口号就分配给TCP、UDP和SCTP的同一给定服务。例如,不论是TCP还是UDP端口号80都被赋予Web服务器,尽管它目前的所有实现都单纯使用TCP。
- 已登记的端口为1024~49151:这些端口不受IANA控制,不过由IANA登记并提供它们的使用情况。可能的话,相同端口号也分配给TCP和UDP的同一给定服务。例如,6000~6063分配给这两种协议的X Windows服务器,尽管它的所有实现当前单纯使用TCP。49151这个上限的引入是为了给临时端口号留出范围。
- 动态的或私用的端口为49152~65535:IANA不管这些端口,也即临时端口(49152这个魔数是65536的四分之三)。
端口号的划分和常见的分配情况:
注意:
Unix系统有保留端口的概念,是指小于1024的任何端口(IANA众所周知的端口),这些端口只能赋予特权用户进程的套接字,且分配使用这些端口的服务器必须以超级用户特权启动。
套接字对
一个TCP连接的套接字对是一个定义该连接的两个端点的四元组:本地IP地址、本地TCP端口、外地IP地址、外地TCP端口。套接字对唯一标识一个网络上的每个TCP连接。
就SCTP而言,一个关联由一组本地IP地址、一个本地端口、一组外地IP地址、一个外地端口标识。在两个端点均非多宿的情形下,SCTP与TCP所用的四元组套接字对一致。然而在某个关联的任何一个端点为多宿的情形下,同一个关联可能需要多个四元组标识(这些四元组的IP地址各不相同,但端口号是一样的)。
标识每个端点的两个值(IP地址和端口号)通常称为一个套接字。
TCP端口号与并发服务器
并发服务器中主服务器循环通过派生一个子进程来处理每个新的连接。
并发服务器示例:
假设服务器的套接字对为{ * : 21, * : *},其中第一个星号表示未指定服务器的IP地址(服务器主机是多宿的),后面两个星号分别表示外地IP地址和端口号。
TCP端口号与并发服务器通信流程:
1)假设在IP地址为206.168.112.219的主机上启动第一个客户端(假设TCP选择的临时端口为1500),它对服务器的IP地址之一12.106.32.254执行主动打开。
2)当服务器接收并接受这个客户端连接时,它会fork一个自身副本,让子进程来处理该客户端的请求,如图所示。
3)假设在客户端主机上另有一个客户请求连接到同一个服务器。客户端主机的TCP为这个新客户的套接字分配一个未使用的临时端口1501,如图所示。
注意:
- 服务器上这两个连接是有区别的:第一个连接的套接字对和第二个连接的套接字对不一样,因为客户端的TCP给第二个连接选择了一个未使用的端口1501。
- TCP无法仅仅通过查看目的端口号来分离外来的节点到不同的端点。它必须查看套接字对的所有4个元素才能确定由哪个端点接收某个到达的分节。
- 上图中对于同一个本地端口(21)存在3个套接字:{ 12.106.32.254 : 21, 206.168.112.219 : 1500}(第一个子进程)、{ 12.106.32.254 : 21, 206.168.112.219 : 1501}(第二个子进程)、{ * : 21, * : *}(父进程)。