1.1 概述

本篇我们的焦点是传输层,包括TCP、UDP。绝大多数客户/服务器网络应用使用TCP或UDP。
UDP是一个简单的、不可靠的数据报协议,而TCP是一个复杂、可靠的字节流协议。这里我们将阐述以下相关主题:TCP的三路握手、TCP的连接终止序列和TCP的TIME_WAIT状态,加上由套接字提供的TCP和UDP的缓冲机制等。

1.2 总览

虽然协议族被称为“TCP/IP”,但除了TCP和IP这两个主要协议外,还有许多其他成员。下图展示了这些协议的概况。
我的网络编程学习之路——TCP和UDP
下面我们来看下上图中的每一个协议框。
  • IPv4: 网际协议版本4。IPv4(通常称之为IP)自20世纪80年代早期以来一直是网际协议族的主力协议。它使用32位地址。
  • IPv6: 网际协议版本6。IPv6是在20世纪90年代中期作为IPv4的一个替代品设计的。主要变化是使用128位更大地址。
  • TCP: 传输控制协议。TCP是一个面向连接的协议,为用户进程提供可靠的全双工字节流。TCP套接字是一种流套接字。TCP关心确认、超时和重传之类的细节。大多数因特网应用程序使用TCP。注意,TCP既可以使用IPv4,也可以使用IPv6.
  • UDP: 用户数据报协议。UDP是一个无连接协议。UDP套接字是一种数据报套接字。UDP数据报不能保证最终达到它们的目的地。与TCP一样,UDP既可以使用IPv4,也可以使用IPv6.
  • SCTP: 流控制传输协议。SCTP是一个提供可靠全双工关联的面向连接的协议。
  • ICMP: 网际控制消息协议。ICMP处理在路由和主机之间流通的错误和控制消息。
  • IGMP: 网际组管理协议。IGMP用于多播。
  • ARP: 地址解析协议。ARP把一个IPv4地址映射成一个硬件地址(如以太网地址)。
  • RARP: 反向地址解析协议。RARP把一个硬件地址映射成一个IPv4地址。
  • ICMPv6: 网际控制消息协议版本6。ICMPv6综合了ICMPv4、IGMP和ARP的功能。
  • BPF: BSD分组过滤器。该接提供对于数据链路层的访问能力。
  • DLPI: 数据链路提供接口。该接口也提供对于数据链路层的访问能力。

1.3 用户数据报协议(UDP)

UDP是一个简单的传输层协议。应用进程往一个UDP套接字写入一个消息,该消息随后被封装到一个UDP数据报,该数据报进而又被封装到一个IP数据报,然后发送到目的地。UDP不保证UDP数据报会到达其最终目的地,不保证各个数据报的先后顺序跨网络后保持不变,也不保证每个数据报只到达一次。
每个UDP数据报都有一个长度。如果一个数据报正确的到达其目的地,那么该数据报的长度将随数据一道传递给接收端应用进程。
我们也说UDP提供无连接的服务,因为UDP客户与服务器之间不必存在任何长期的关系。举例来说,一个UDP客户可以创建一个套接字并发送一个数据报给一个给定的服务器,然后立即用同一个套接字发送另一个数据报给另一个服务器。同样地,一个UDP服务器可以用同一个UDP套接字从若干个不同的客户接收数据报,每个客户一个数据报。

1.4 传输控制协议(TCP)

TCP向应用进程提供的服务不同于由UDP提供的服务。
首先,TCP提供客户与服务器之间的连接。TCP客户先与某个给定服务器建立一个连接,再跨该连接与那个服务器交换数据,然后终止这个连接。
其次,TCP还提供了可靠性。当TCP向另一端发送数据时,它要求对端返回一个确认。如果没有收到确认,TCP就自动重传数据并等待更长时间。在数次重传失败后,TCP才放弃,如此在尝试发送数据上所花的总时间一般为4~10分钟(依赖于具体实现)。TCP通过给其中每个 字节关联一个序号对所发送的 数据进行排序。如果接收端TCP接收到来自对端的重复数据(譬如说对端认为一个分节已丢失并因此重传,而这个分节并没有真正丢失,只是网络通信过于拥挤),它可以(根据***)判定数据是重复的,从而丢弃重复数据。
再次,TCP提供流量控制。TCP总是告知对端在任何时刻它一次能够从对端接收多少字节的数据,这称为通告窗口。在任何时刻,该窗口指出接收缓冲区中当前可用的空间量,从而确保发送端发送的数据不会使接收缓冲区溢出。该窗口时刻动态变化:当接收到来自发送端的数据时,窗口大小 就减少,但是当接收端应用从缓冲区中读取数据时,窗口大小就增大。通告窗口大小减少到0是有可能的:当TCP对应某个套接字的接收缓冲区已满,导致它必须等待应用从该缓冲区读取数据时,方能从对端再接收数据。
最后,TCP连接是全双工的。这意味着在一个给定的连接上应用可以在任何时刻在进出两个方向上既发送数据又接收数据。

1.5 TCP连接的建立和终止

为了理解connect、accept和close这3个函数并使用netstat程序调试TCP应用,我们必须了解TCP连接如何建立和终止,并掌握TCP的状态转换图。

1.5.1 三路握手

建立一个TCP连接时会发生下述情形。
  1. 服务器必须准备好接受外来的连接。这通常通过调用socket、bind和listen这3个函数来完成,我们称之为被动打开。
  2. 客户通过调用connect发起主动打开。这导致客户TCP发送一个SYN(同步)分节,它告诉服务器客户将在(待建立)连接中发送数据的初始***。通常SYN分节不携带数据,其所在IP数据报只含有一个IP首部、一个TCP首部及可能有的TCP选项。
  3. 服务器必须确认(ACK)客户的SYN,同时自己也得发送一个SYN分节,它含有服务器将在同一连接中发送的初始***。服务器在单个分节中发送SYN和对客户SYN的ACK(确认)。
  4. 客户必须确认服务器的SYN。
这种交换至少需要3个分组,因此称之为TCP三路握手。下图显示所交换的3个分节。
我的网络编程学习之路——TCP和UDP
上图给出的客户初始***为J,服务器的初始***为K。ACK中的确认号是发送这个ACK的一端所期待的下一个***。因为SYN占据一个字节的***空间,所以每一个SYN的ACK中的确认号就是该SYN的初始***加1.类似的,每一个FIN(表示结束)的ACK中的确认号为该FIN的***加1。

1.5.2 TCP选项

每一个SYN可以含有多个TCP选项。下面是常用的TCP选项。
  • MSS选项。发送SYN的TCP一端使用本选项通告对端它的最大分节大小即MSS,也就是它在本连接的每个TCP分节中愿意接受的最大数据量。发送端TCP使用接收端的MSS值作为所发送分节的最大大小。
  • 窗口规模选项。TCP连接任何一端能够通告对端的最大窗口大小是65535,因为在TCP首部中相应的字段占16位。然而当今因特网上已经普及的高速网络连接或长延迟路径(卫星链路)要求有更大的窗口以获取尽可能大的吞吐量。这个新选项指定TCP首部中的通告窗口必须扩大(即左移)的位数(0-14),因此所提供的最大窗口接近1GB(65535 x 2 ^14)。在一个TCP连接上使用窗口了规模的前提是它的两个端系统必须都支持这个选项。
  • 时间戳选项、这个选项对于高速网络连接是必须的,它可以防止由失而复得的分组可能造成的数据损坏。作为编程人员,我们无需考虑此项。

1.5.3 TCP 连接终止

TCP建立一个连接需要3个分节,终止一个连接则需要4个分节。
  1. 某个应用进程首先调用close,我们称该端执行主动关闭。该端的TCP于是发送一个FIN分节,表示数据发送完毕。
  2. 接收到这个FIN的对端执行被动关闭。这个FIN由TCP确认。它的接收也作为一个文件结束符传递给接收端应用进程(放在已排队等候该应用进程接收的任何其他数据之后),因为FIN的接收意味着接收端应用进程在相应连接上再无额外数据可接收。
  3. 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
  4. 接收这个最终FIN的原发送端TCP(即主动关闭的那一端)确认这个FIN。
既然每个方向都需要一个FIN和一个ACK,因此通常需要4个分节。
我的网络编程学习之路——TCP和UDP
类似SYN,一个FIN也占据1个字节的***空间。因此,每个FIN的ACK确认号就是这个FIN的***加1.
当套接字被关闭时,其所在端TCP各自发送一个FIN。这是由应用进程调用close而发生的,不过需认识到,当一个Unix进程无论自愿地(调用exit或从main函数返回)还是非自愿地(收到一个终止本进程的信号)终止时,所有打开的描述符都被关闭,这也导致仍然打开的任何TCP连接上也发出一个FIN。

1.5.4  观察分组

下图展示一个完整的TCP连接所发送的实际分组交换情况,包括连接建立、数据传送和连接终止3个阶段。
我的网络编程学习之路——TCP和UDP
本例中的客户通过一个值为536的MSS(表明该客户只实现了最小重组缓冲区大小),服务器通告一个值为1460的MSS(以太网上IPv4的典型值)。不同方向上MSS值不相同不成问题。
一旦建立一个连接,客户就构造一个请求并发送给服务器。这里我们假设该请求适合于单个TCP分节(即请求大小小于服务器通告的值为1460字节的MSS)。服务器处理该请求并发送一个应答,我们假设该应答也适合于单个分节(本例即小于536字节)。图中使用粗箭头表示这两个数据分节。注意,服务器对客户请求的确认是伴随其应答发送的。这种做法称为捎带。它通常在服务器处理请求并产生应答的时间少于200ms时发生的。如果服务器耗用更长时间,譬如说1s,那么我们将看到先是确认后应答。
如果该连接的整个目的仅仅是发送一个单分节的请求和接收一个单分节的应答,那么使用TCP有8个分节的开销。如果改用UDP,那么只需交换两个分组:一个承载请求,一个承载应答。然而从TCP切换到UDP将丧失TCP提供给应用进程的全部可靠性,迫使可靠服务的一大堆细节从传输层转移到UDP应用进程。TCP提供的另一个重要特性即拥塞控制也必须由UDP应用进程处理。尽管如此,我们仍然需要知道许多网络应用是使用UDP构建的,因为它们需要交换的数据量较少,而避免了TCP连接建立和终止所需的开销。

1.5.5 TIME_WAIT状态

任何TCP实现都必须为MSL选择一个值。MSL是任何IP数据报能够在因特网中存活的最长时间。
TIME_WAIT状态有两个存在的理由:
  1. 可靠地实现TCP全双工连接的终止;
  2. 允许老的重复分节在网络中消逝。
第一个理由可以通过上图并假设最终的ACK丢失来解释。服务器将重新发送它的最终那个FIN,因此客户必须维护状态信息,以允许它重新发送那个ACK。要是客户不维护状态信息吗,它将响应一个RST(另外一种类型的TCP分节),该分节将被服务器解释成一个错误。如果TCP打算执行所有必要的工作以彻底终止某个连接上两个方向的数据流(即全双工关闭),那么它必须正确处理连接终止序列4个分节中任何一个分节丢失的情况。
第二个理由,我们假设在12.106.32.254的1500端口和206.168.112.219的21端口之间有一个TCP连接。我们关闭这个连接,过一段时间后在相同的IP地址和端口之间建立另一个连接。后一个连接称为前一个连接的化身,因为它们的IP地址和端口号都相同。TCP必须防止来自某个连接的老的重复分组在该连接已经终止后再现,从而被误解成属于同一连接的某个新化身。为了做到这一点,TCP将不给处于TIME_WAIT状态的连接发起新的化身。既然TIME_WAIT状态的时间是MSL的2倍,这就足以让某个方向上的分组最多存活MSL秒即被丢弃,另一个方向上的应答最多存活MSL秒也被丢弃。

1.6 端口号

任何时候,多个进程可能同时使用TCP、UDP话SCTP这3种传输层协议中的任何一种。这3中协议都使用16位整数的端口号来区分这些进程。
当一个客户想要跟一个服务器联系是,它必须标识想要与之通信的这个服务器。TCP、UDP和SCTP定义了一组众所周知的端口,用于标识众所周知的服务。举例来说,支持FTP的任何TCP/IP实现都把21这个众所周知的端口分配给FTP服务器。分配给简化文件传送协议(TFTP)的是UDP端口69.
另一方面,客户通常使用短期存活的临时端口。这些端口号通常由传输层协议自动赋予客户。
端口号被划分成以下3端。
  1. 众所周知的端口号0~1023。这些端口号由IANA分配和控制。例如,不论TCP还是UDP端口号80都被赋予Web服务器。
  2. 已登记的端口为1024~49151。
  3. 49152~65535是动态的或私用的端口。它就是我们所称的临时端口。

1.7 缓冲区大小及限制

下面我们将介绍一些影响IP数据报大小的限制。
  • IPv4数据报的最大大小是65535字节,包括IPv4首部。
  • IPv6数据报的最大大小是65575字节,包括40字节的IPv6首部。
  • 在两个主机之间的路径中最小的MTU称为路径MTU。
  • 当一个IP数据报将从某个接口发送出去时,如果它的大小超过相应链路的MTU,IPv4和IPv6都将执行分片。这些分片在到达最终目的地之前通常不会被重组。IPv4主机对其产生的数据报执行分片,IPv4路由器则对其转发的数据报执行分片。然而IPv6只有主机对其产生的数据报执行分片,IPv6路由器不对其转发的数据报执行分片。
  • IPv4首部的“不分片”位(即DF位)若被设置,那么不管是发送这些数据报的主机还是转发它们的路由器,都不允许对它们分片。当路由器接收到一个超过其外出链路MTU大小且设置了DF位的Ipv4数据报时,它将产生一个ICMPv4(目的地不可达,需分片但DF位已设置)出错消息。
  • IPv4和IPv6都定义了最小重组缓冲区大小,它是IPv4或IPv6的任何实现都必须保证支持的最小数据报大小。其值对于IPv4位576字节,对于IPv6为1500字节。
  • TCP有一个MSS(最大分节大小),用于向对端TCP通告对端在每个分节中能发送的最大TCP数量。

1.8 TCP输出

下图显现了某个应用进程写数据到一个TCP套接字中时发生的步骤。
我的网络编程学习之路——TCP和UDP
每一个套接字有一个发送缓冲区,我们可以使用SO_SNDBUG套接字选项来更改该缓冲区的大小。当某个应用进程调用write时,内核从该应用进程的缓冲区中复制所有数据到所写套接字的发送缓冲区。如果该套接字的发送缓冲区容不下该应用进程的所有数据,该应用进程将被投入睡眠。这里假设该套接字时阻塞的。它是通常的默认设置。内核将不从write系统调用返回,直到应用进程缓冲区中的所有数据复制到套接字发送缓冲区。因此,从写一个TCP套接字的write调用成功返回仅仅表示我们可以重新使用原来的应用进程缓冲区,并不表明对端的TCP或应用进程已接收到数据。
每个数据链路都有一个输出队列,如果该队列已满,那么新到的分组将被丢弃,并沿协议栈向上返回一个错误:从数据链路到IP,再从IP到TCP。TCP将注意到这个错误,并在以后某个时刻重传相应的分节。应用进程并不知道这种暂时的情况。

1.9 UDP 输出

下图展示了某个应用进程写数据到一个UDP套接字中时发生的步骤。
我的网络编程学习之路——TCP和UDP
任何UDP套接字都有发生缓冲区大小,不过它仅仅是可以写到该套接字的UDP数据报的大小上限。如果一个应用进程写一个大于套接字发送缓冲区大小的数据报,内核将返回该进程一个EMSGSIZE错误。既然UDP数不可靠的,它不必保持应用进程数据的一个副本,因此无需一个真正的发送缓冲区。(应用进程的数据在沿协议栈向下传递时,通常被复制到某种格式的一个内核缓冲区中,然而当该数据被发送之后,这个副本就被数据链路层丢弃了)。
从写一个UDP套接字的write调用成功返回表示所写的数据报或其所有片段已被加入数据链路层的输出队列。如果该队列没有足够的空间存放该数据报或它的某个片段,内核通常会返回一个ENOBUFS错误给应用进程。

相关文章:

  • 2021-09-25
  • 2021-10-09
  • 2022-12-23
  • 2021-11-24
  • 2021-07-09
  • 2022-01-07
  • 2021-06-10
  • 2021-11-01
猜你喜欢
  • 2021-11-19
  • 2021-10-14
  • 2021-05-08
  • 2021-11-20
  • 2022-02-06
  • 2022-02-18
相关资源
相似解决方案