Chapter 3 Transport Layer

Residing between the application and network layers, the transport layer is a central piece of the layered network architecture. It has the critical role of providing communication services directly to the application processes running on different hosts.

3.1 Introduction and Transport-Layer Services

传输层协议为运行在不同主机上的应用进程提供logical communication 。而 logical communication 的意思其实是用应用的视角来看,它们传输数据时好像主机之间是通过一道传送门直接相连一样。但通过第一章的network overview,我们知道现实上两台主机之间的通信是依靠路由器、交换机这些 physical infrastructures 来进行的。而应用进程使用传输层所提供的逻辑通信,规避底层传递信息的实现细节,使得应用开发者可以专注于应用逻辑。极大地简化了网络编程和应用开发。

因为传输层是 TCP/IP 协议簇中的第四层协议,所以传输层协议只在主机上实现。在发送端,传输层将来自应用层的报文(message)转化成传输层的包,也叫报文段(segment) 。在转化过程中,最先将应用报文切割成更小的块(chunks),然后加入传输层头部报文就成了报文段。之后,传输层会将封装好的报文段传递给发送终端的网络层,由网络层将 segment 封装成数据报(datagram)后发送给目标主机。在接收端,网络层将数据报解析成报文段后发给传输层,后由传输层处理好报文段后,将数据传递给应用进程。

3.1.1 Relationship Between Transport and Network Layers

传输层在协议栈(protocol stack)中位于网络层的上层。传输层协议会为不同主机间运行的应用进程提供逻辑通信( provides logical communication between processes running on different hosts ),而网络层协议提供主机间的逻辑通信( provides logical communication between hosts )。

A Package Postal Analogy

要理解传输层和网络层之间的关系,我们在下面用一个例子来近似模拟一下,我们继续使用上一章最后的 快递邮寄 的例子。我们将房子看作应用进程,将街道看成主机host,但现在我们要加入快递驿站(每个街道一个)。这样我们就可以模拟网络层及网络层向上的数据逻辑通信了。

每个街道中存在的房子中都有很多活动,其中就不乏有寄快递的activity。这时,”房子“就需要把这些 快递件 给驿站让驿站发出去(应用层应用将报文交给传输层)。”房子“可以选择提供的邮寄服务,快发还是慢发,有没有快递丢失赔款的服务(应用层进程可以规定传输层协议)

如果快递很多,那么驿站还会将这些快递分成许多 包裹 按批次发出去(传输层将应用层报文分成一个个 chunks 然后封装传输层报文头变成传输层数据段)。驿站在发快递之前,”房子“需要将把xx街道xx收件人作为收件地址交给快递驿站,之后就不用操心任何东西了(传输层协议只为应用进程之间提供逻辑通信)。快递站将包裹交给邮寄公司,邮寄公司负责将快递送达到目标街道(网络层只为主机提供逻辑通信)

我们可以在这个模拟中看到,应用进程只需要用socket接口将报文交给传输层,不需要关心之后发生了什么,而传输层需要把报文成段封装成数据段后给网络层,至于网络层之后链路带宽什么的都不需要关心。我们之后还会了解到,传输层是如何做到即使底下的网络层为用户提供不可靠的服务,传输层仍然可以保证传输是可靠的。

Pasted image 20240705151453.png

3.1.2 Overview of the Transport Layer in the Internet

传输层提供两个不同的协议——UDP(User Datagram Protocol)TCP(Transmission Control Protocol)。其中,UDP为上层应用提供无连接的不可靠服务,TCP为上层应用提供可靠的有连接的服务。从2.7节中,我们也了解到,在进行应用程序开发时,程序员可以选择使用UDP还是TCP作为传输层协议。

在我们简单介绍UDP和TCP这两个传输层协议之前,我们来看看IP为上层提供什么服务。IP(Internet Protocol)协议提供尽力而为的传输服务(Best-effort delivery service),意思是IP尽它最大努力(take its best effort)在端到端上传输,但不保证数据传输过程中的完整性和顺序。

看过IP提供的服务之后,我们再来看看传输层UDP和TCP的服务。UDP和TCP作为传输层协议最基本的作用就是把IP协议的端到端信息传输延伸到进程到进程上的服务。这个过程也叫传输层多路复用(transport-layer multiplexing)解多路复用(transport-layer demultiplexing)。UDP和TCP还会通过检查段头的信息来提供数据包的完整性的检查(integrity checking)。(1)进程到进程间的传输服务;(2)数据包的完整性检查。这两个服务是传输层提供服务的最低要求(minimal transport-layer services),也是UDP唯二提供的服务。因此,我们看到,UDP并不提供可靠性的服务。

而TCP为上层应用提供额外的服务。它提供可靠的数据传输(reliable data transfer) 服务。使用流控制(flow control)、序列号(sequence numbers)、确认机制(acknowledgments)和计时器(timers),通过这些工具TCP保证数据正确且按顺序的从传输进程传到接收进程。这样,TCP将IP端系统间的不可靠服务转化成进程间的可靠的服务。除此之外,TCP还提供拥塞控制(congestion control) 的服务,这些额外的服务也给TCP增添了不少复杂性。

3.2 Multiplexing and Demultiplexing

我们将在本节讨论传输层多路复用和解多路复用。传输层提供的这对服务拓展了网络层的端到端的传输服务,有了这对服务,我们可以在不同主机上应用进程到应用进程间进行信息传输。从这段描述中我们能够很清楚地感受到 multiplexing/demultiplexing 有多重要。

我们下面简单看看传输层是如何将网络层的消息传到指定的应用的。在我们上网的时候,一般会打开好多个网络应用,一个网页刷视频的同时还在某公司官网上下载着它们的应用程序。而当传输层收到来自网络层的数据包后,它需要将这些数据包给到主机上其中一个运行的应用进程。那么传输层怎么指定把这些数据转发给哪个应用进程去呢?

学习socket编程的时候,我们了解到一个进程(网络应用的一部分)可以有一个或多个socket接口。我们也知道,socket作为应用层和传输层的接口,在传输层想应用进程转发来自网络层的数据时,其实是通过应用的socket接口来向应用进程转发的。传输层可以通过socket的标识符来决定给哪个socket转发信息,TCP和UDP标识符的格式是不一样的,传输层还可以根据这个判断数据传输时用的上面传输层协议。

现在,我们有能力轻松描述multiplexing/demultiplexing的过程了。所谓demultiplexing其实就是接收主机端将收到的传输层数据段传递到正确的socket上的过程,其中,传输层会检查segment header fields中的信息来指定某个接收socket。反过来,multiplexing就是在应用进程通过socket向传输层发生数据的时候,传输层将收到的数据划分成一个个chunks并根据socket加入header field封装为传输层数据段的过程。通过multiplexing,封装好的数据就可以直接发出去,而不用顾及会不会混淆其他应用进程。

在下图中,中间主机的传输层就必须将来自网络层的段进行demultplexing将信息正确地转发给上层的应用进程P1/P2(直直地给响应的进程socket)。在P1和P2要向外发报文时,所在主机的传输层还需要起到收集这些报文并封装起来的任务,多个上层应用共同使用同一个传输层,实现了传输层的multiplexing。
Pasted image 20240707015702.png
我们再用收发快递的例子来解释一下传输层的multiplexing/demultiplexing。房间中住着很多人,可能每个人都有快递发给不同的人(我们用“人”表示socket,”房子“延续表示进程的概念)。在发快递的过程中,这些快递件会先收集到街道快递驿站(传输层),根据发件/收件信息封装成一个个快递包并交给邮寄公司(网络层),这就是快递驿站的multiplexing。在另一个街道的快递驿站处,包裹会从卡车上卸下来并根据收件人信息发给收件人(socket),实现了快递驿站的demultiplexing。

我们现在知道了,要实现传输层的复用,需要 (1)标有特定标识符的sockets;(2)传输层数据段中需要包含这些sockets的标识符信息。学习了这么多,那“收件人信息”到底长什么样呢?
Pasted image 20240707150002.png
上图展示了一个传输层数据段的构成,在传输层的header fields,包括了源地址端口号(source port number field)目的地端口号(destination port number field) 各16位的二元组(two tuple)。这16位信息能够表示0-65535之间的65536个数。从中,我们明白主机上端口号数最多能有65536个。其中0-1023端口号知名端口号(well-known port numbers),预留给如HTTP(端口号80)、FTP(端口号21)等等这样的知名应用协议的。范围从1024到49151的端口叫做注册端口号 (Registered Ports),这些端口号可以由用户或应用程序注册使用。范围从49152到65535的端口是动态/私有端口号 (Dynamic/Private Ports),通常用于临时或私有连接。

Connectionless Multiplexing and Demultiplexing

UDP socket是用一个二元组(源端口号,目的端口号)组成的,即报文段segments在到达传输层后只要知道这些报文段的目的端口号就可以找到相关的socket接口了。也因此在UDP中,一个特定的端口号(例如8080)在同一台主机上只能被一个应用进程绑定。正是因为采用UDP协议的传输层端口只能绑定一个应用进程。因此,知道了端口号,自然知道其唯一对应的UDP socket在哪里了。
Pasted image 20240709001051.png

在之前C++ 的 socket编程实验中,我们用sockfd = socket(AF_INET, SOCK_DGRAM, 0)来创建一个地址族为IPv4的数据报套接字(Datagram socket)。UDP的socket用这种方式创建后,传输层会自动地分配一个从1024到65535端口号范围内的端口。

但是我们在配置服务器地址时也看到其实端口号是可以自定义的:

// 配置服务器地址
    memset(&server_addr, 0, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = INADDR_ANY;
    server_addr.sin_port = htons(PORT);
//关联端口号
	bind(sockfd, (const struct sockaddr *)&server_addr, sizeof(server_addr))

但我们要注意,由于0-1023号端口是知名端口,所以在创建socket时往往不可以将socket于这些端口相关联!

有了socket端口号,再回顾2.7节的编程实验。在实验中,我们创建了两个应用进程,它们再通信时的multiplexing/demultiplexing是怎么样的?我们使用了sockfd = socket(AF_INET, SOCK_DGRAM, 0)创建了一个udp_client应用的socket接口,系统自动分配一个源端口号。然而,我们需要知道服务器socket接口的端口号是多少,因此我们自定义绑定(bind)服务器端口号为8080。当udp_server进程一打开,就开始监听来自端口8080的segments。一旦收到有客户端发来的segments,服务器上的传输层会从segment header fields中获得有用的信息(udp_client应用进程来自哪里?)。之后处理来自客户进程发来的消息,转换成大写后再发回去。

Connection-Oriented Multiplexing and Demultiplexing

要理解传输层TCP协议的demultiplexing,我们首先需要搞明白TCP的sockets和TCP连接建立是怎么一回事。UDP socket和TCP socket最明显的区别就是UDP socket是用一个二元组标识的(源端口号,目标端口号)TCP socket是用一个四元组标识的(源端口号,目标端口号,源IP地址,目标IP地址)。所以,当有传输层数据段到达时,传输层使用全部四个数值来寻找要找的socket。因此在TCP协议实现的传输层上,即使端口号为80的端口是唯一的,但 socket不一定是唯一的(一个socket对应一个执行流)

下面,我们用图看看TCP socket与UDP socket有何不同。在 Figure 3.5 中,Host A 与 Server B 建立了一个HTTP会话,Host C 与 Server B 建立了两个HTTP会话。从而,在 Server B 的应用进程上一共有三个socket分别对应着 Host A 的一个会话和 Host C 的两个会话。因为TCP socket是用一个四元组标识的,所以即使我们看到 Host A 和 Host C 的一个源端口号是相同的,Server B 的传输层任然可以通过源IP地址分辨两个会话。也可以通过 Host C 上两个会话的源端口号分辨这两个会话。

Pasted image 20240709003932.png

Web Server and TCP

我们已经简单了解了 TCP socket 的轮廓,具体来说,假如我们有很多 clients 要通过 HTTP 访问网页。在 server host 上,每个来自 client 的 HTTP 请求报文都要发到80号端口解复用后与服务器应用进程(socket)通信,服务器的响应报文也会通过80号端口复用将信息发给客户端。

之前提到过,一个socket对应一个执行流。所以在 server host 上,每个来自 cilent host 的HTTP请求都会先在80号端口上建立起一个用四元组标记的socket接口(一个新的执行流)。现代高性能服务器往往采用创建线程流的方式创建新的socket。如果连接采用 persistent connection ,在连接关闭前socket连接都会保留。但如果连接采用 non-persistent connection ,每次报文传输都要频繁地进行 socket 创建、socket 销毁,这样额外的开销可能严重拖累 server host。

3.3 Connectionless Transport: UDP

3.3 节课中,我们会详尽地介绍UDP这个传输层协议。在2.1节中,我们简单了解了一下UDP服务模型,2.7节中我们通过编程更深入地了解了这个传输层协议。

假设我们要设计一个简单轻便的传输层协议,了解过UDP的我们很难不去参照UDP的实现方式去设计。这样也催生我们思考UDP存在的意义。UDP感觉像一盆吃不死人的饭菜,相比TCP那样的饕餮盛宴,UDP只有着不讲究、上菜速度快的优点,如果只是吃不死人,为何干脆不装盘,直接让这些应用进程趴在大锅上吃?砍掉传输层,让应用进程直接和网络层进行信息交互难道不好么?但我们要明白,一个主机上不单单有一个进程,没有传输层multiplexing和demultiplexing的分盘,应用进程很有可能出现进程A吃掉进程B的饭这样吃错饭的情况!

作为一个合格的丐版传输层协议,UDP的服务也只限于传输层的 multiplexing/demultiplexing 和简单的检错功能。UDP接收应用层的报文→加上简单的field→将封装好的数据段发给网络层;UDP接收网络层的数据段→解封装成应用层报文→发给对应的端口。同时,UDP也不提供数据传输前的握手,也因此UDP是无连接的。

既然我们有TCP这样的饕餮盛宴可供选择,为什么许多应用还会选择UDP这种吃不死人的饭菜呢?原因当然有很多。一方面UDP由于提供更少的服务,所以速度会更快,没有拥塞控制,而且撤去烦人的握手礼仪,也减轻了服务器的负载。DNS就是一个采用UDP作为传输层协议的典型应用。

  • UDP相比TCP的优点:

    • ==Finer application-level control over what data is sent, and when. ==当传输层使用UDP,它可以将应用进程发送的报文封装后马上就发出去。而TCP的 congestion-control mechaniam 一旦检测到有大的拥塞就会掐断传输,而且TCP的重传机制也使得TCP的即时性远远不如UDP。(TCP传输时的延迟)
    • No connection establishment.在传输数据前,TCP需要三次握手(three-way handshake)确认连接,UDP发送数据不需要握手。也因为TCP的这种握手延迟,Google的Chrome浏览器使用QUIC(Quick UDP Internet Connection)代替HTTP作为其传输层协议。(传播前的延迟)
    • No connection state.TCP维护通信两端上的连接状态,其中包括接收和发送缓冲区、拥塞控制参数、序列号和确认号等信息。服务器维护这些信息会消耗大量的资源,不适合高并发应用。而UDP恰恰相反,TCP的缺点反而是它的优点,不维护连接状态,传输速度快,适合高并发应用,但不保证数据传输的可靠性。
    • Small packet header overhead.TCP的段头有20个字节,作为对比,UDP只有8个。
  • 下图罗列了一些知名的Internet应用,以及它们都使用哪种传输层协议。

    我们可以看到,这些运行在TCP上的应用无一不是需要TCP的可靠数据传输服务。这是UDP所不能提供的。但我们也同时发现 HTTP/3 的Web应用是运行在UDP上的,这是因为HTTP/3在应用层面上提供了检错纠错和拥塞控制的机制。
    Pasted image 20240709230729.png
    尽管UDP并不控制拥塞,但UDP可能会导致拥塞状态。在当今这个流媒体直播盛行的时代如果大家都观看高码率的直播且不采用任何拥塞控制,这会导致路由器的 overflow,不但影响UDP自己的流量,TCP也会由于拥塞控制而动态降低传输率,网络中的劣币驱逐良币。因而,许多学者建议强制所有的发送源应用都采用动态拥塞控制的机制。

我们前面看到HTTP/3运行在UDP上,我们看到UDP其实是可以实现可靠的信息传输的,但是这种可靠性是在应用层面实现的(例如QUIC)。尽管苦了debugging的程序员,但这让应用实现了鱼(速度)和熊掌(可靠性)的兼得。

3.3.1 UDP Segment Structure

我们3.2节知道了段头中包含着源端口号和目的端口号的信息,现在我们终于可以一睹UDP数据段的段头的芳容了!下图给出了UDP数据段的段结构,鉴于我们已经了解过前两个字段——源端口字段和目标端口字段。我们直接来学习后两个字段,长度字段(length field) 标识当前UDP数据段的长度(段头加数据)。校验和字段(checksum field) 是接收主机用于检测传输过程中,段有没有发生错误。在UDP检错前,其实在网络层中IP头也会进行一些校验和的检错,我们之后会学习到。
Pasted image 20240710003357.png

3.3.2 UDP Checksum

UDP的校验和为传输层提供了一种简单的检错机制,用于检查在源到目的传输过程中数据段是否受到噪音干扰。如果在传输过程中出现干扰或数据丢失,接收端的传输层可以通过校验和来判断数据段是否正确。如果数据不正确,接收端会直接丢弃该数据段。下面我们来看看校验和是如何发挥作用的。

Calculation of the Checksum

在计算校验和之前,先要明确一件事:我们在计算UDP Checksum 之前,我们要在UDP报文段的头部前加入一个伪头部(Pseudo header)。
udp_pseudo_header.png
校验和的计算过程其实很简单,其过程如下:

  1. 从“伪头部”开始,按每16位当作一个数,逐次求和,最终得出一个32位的数;
  2. 如果这个32位的数的高16位不为0,则进行“回卷”操作。
  3. 重复第2步直到高16位为0。
  4. 最终,将低16位取反,得到校验和,填入checksum字段中

简单举个例子,我们要将数据从 IP source:192.168.2.1,Port src:8080传递到IP dest:192.168.2.2,Port source:9090。这里的8位协议字段(protocol)是17,表示UDP协议。再假设我们要传4字节的数据字段(0x55aa)
小端方式表示的UDP segment如下:
Pseudo Header:

0-7bits 8-15bits 16-23bits 24-31bits
Source IP addr. 0000 0001(1) 0000 0010(2) 1010 1000(168) 1100 0000(192)
Destination IP addr. 0000 0010(2) 0000 0010(2) 1010 1000(168) 1100 0000(192)
Zero & Protocol & length 0000 0000 0001 0001 0000 0100 0000 0000

UDP Header:

0-7bits 8-15bits 16-23bits 24-31bits
src port & dest port 1001 0000 0001 1111 1000 0010 0010 0011
length & checksum 0000 0100 0000 0000 0000 0000 0000 0000

Segment Body:

Body data 0-7bits 8-15bits 16-23bits 24-31bits
0x1234 55aa 1010 1010 0101 0101 1000 0011 0001 0010
	 0000 0010 0000 0001      Pseudo Header Starts HERE
	+1100 0000 1010 1000
	————————————————————
	 1100 0010 1010 1001(SUM)
	+0000 0010 0000 0010
	————————————————————
	 1100 0100 1010 1011(SUM)
	+1100 0000 1010 1000
	————————————————————
   1 1000 0101 0101 0011(SUM)
	+0001 0001 0000 0000 +1
    ————————————————————
	 1001 0110 0101 0100(SUM)
	+0000 0000 0000 0100
	————————————————————
	 1001 0110 0101 1000(SUM)
	+0001 1111 1001 0000      UDP Header Starts HERE
	————————————————————
	 1011 0101 1110 1000(SUM)
	+0010 0011 1000 0010
	————————————————————
	 1101 1001 0110 1010(SUM)
	+0000 0100 0000 0000
	————————————————————
	 1101 1101 0110 1010(SUM)
	+0101 0101 1010 1010      UDP Body Starts HERE
	————————————————————
   1 0011 0011 0001 0100(SUM)
    +0001 0010 1000 0011 +1
    ————————————————————
     0100 0101 1001 1000
    ^
    ————————————————————
     1011 1010 0110 0111(CHECKSUM) 

在接收端,传输层会将UDP segment中每16位相加并加上进位,最终与CHECKSUM相或,若结果不是0xFFFF(即全1)说明报文段出错,应舍弃

尽管报文段在传播过程中会有链路和设备对段文信息进行检错,但只要过程中有一个链路(link)不提供这种可靠的保证,那么这一报文段的可靠性就存疑。再者,即使路由器检错没有问题,段文存储在buffer中还是可能造成错位。因此,我们需要在传输层上实现纠错机制。这也是系统设计中对 end-end principle 的典例。

还有就是,UDP 并不实现纠错机制。当 UDP 检测到段文错误,根据实现上的不同,可能会直接舍弃错误的报文段,如果要采纳一个不完全准确的报文段可能还会警告应用。下节课,我们将学习可靠数据传输应遵循的原则。

3.4 Principles of Reliable Data Transfer

传递一些文本数据或者浏览网页时,一点微小的变动就可能使传递的整个报文变为不可读的垃圾。因此,数据传输过程中的可靠性保证就成了网络工程师所绝对关注的事情。为了这种数据的可靠,应用层、传输层、甚至数据链路层都有相应的协议来实现数据传输过程中的可靠性。

在下图中,我们将提供可靠性服务的下层抽象成一个 reliable channel。这个抽象出的信道会向上层应用提供:(1)无数据位反转(2)数据不会丢失(3)数据按发送顺序接收。这也是可靠性数据传输协议为实现可靠性要做的事情。虽然传输层是可靠的,但是IP不提供可靠性(尽力而为)。而且在端到端的传输链路中即便大部分的链路都是可靠的,只要有一条链路使用了不可靠的协议,那么整条链路就是不可靠的。TCP向上层应用提供的可靠信道其实是基于下层不可靠的抽象信道之上的,看似是一个不可能完成的任务,我们将在后续学习中逐步了解这种可靠性是如何实现的。TCP 需要在这种滩涂地上建高楼属实不容易。

下面我们看 3.8(b) 当发生进程发送报文(packets)时,它会调用rdt_sent(),将报文可靠的传输到接收进程上。在接收端,传输层会调用rdt_rcv为上层提供可靠的报文,之后调用deliver_data向上层应用传输报文。之后的小结中,我们用 "packet" 代替 "segment",这是因为这种可靠性的理论是面向整个计算机网络的,不单单为传输层独有。
Pasted image 20240712160221.png
在本节中,我们只考虑 单向数据传输(unidirectional data transfer) 的情况,也就是数据只有发送方发给接收方。而 双向数据传输(bidirectional data transfer),也只是单工的 verse visa 。尽管我们考虑的单向数据传播,但在TCP协议在发送和接收两端仍然需要双向的报文传输来回交换数据之外的控制信息(control packets)。在交换报文时,收发两端都会调用udt_send()通过不可靠的信道发送报文。

3.4.1 Building a Reliable Data Transfer Protocol

Reliable Data Transfer over a Perfectly Reliable Channel: rdt1.0

在 rdt1.0 中,我们暂且不考虑传输层下层信道的可靠性问题。下面,我们罗列出了 rdt1.0 的发送端(Figure 3.9a)和接收端(Figure 3.9b)双方的有限状态机(finite-state machine) 的模型操作定义。
在图中,我们看到,无论是接收端还是发送端,它们都只有一种状态(state),一个事件(event) ,每个事件后对应两个动作(action),动作完成后转换(transition)到原有状态。

在发送端,FSM从虚线箭头处初始化发送端的状态,之后等待来自上层应用发送信息报文。一旦有上层应用的报文传来,触发事件rdt_send(data)并执行动作packet=make_pkt(data)udt_send(packet),完成后返回到初始状态。
在接收端,同样先初始化接收端的状态,等待下层网络发来的信息报文。一旦报文到来,触发事件rdt_rcv(packet)并执行动作extract(packet,data)deliver_data(data)将报文通过socket送到应用进程上。之后返回到初始状态。
Pasted image 20240715175100.png
从这个例子中,数据报文总是从发送方传播到接收方,有了下层信道的可靠性保证,接收端完全不需要给发送方反馈任何信息(前提假设下层信道完全可靠)。

Reliable Data Transfer over a Channel with Bit Errors: rdt2.0

为了更贴合实际情况,我们在 rdt2.0 中引入位错误(bit error)。我们知道给传输层提供服务的下层网络不是可靠的,在报文的传输(transmission)传播(propagation)缓存(buffer)都可能会导致报文中出现位错误。现在我们依然假设 不会出现丢失 的情况。

传输层该如何实现在这种信道下的可靠性保证?在我们打电话时,当别人说完一句话后,我们会有肯定回应和不确定回应的方式,在我们不确定对方说了什么时,我们可以要求对方再说一遍。在这种信道上实现的可靠性的方式也类似。Message-dictation protocol 会让接收端用positive acknowledgments ("OK")negative acknowledgments ("Please repeat") 的控制报文使发送端知道哪个报文出错了。最后发送端将出错的报文重传。这种可靠数据传输协议也叫 ARQ(Automatic Repeat reQuest) protocols

要实现自动重传请求(ARQ)协议,就必须引入以下三个机制:

  • Error detection:我们最起码要有检错机制来知道有位错误出现了,之后才能根据重传出现错误的报文。先前学了UDP的校验和,我们知道额外添加字段(checksum)就能实现简单的检错了。
  • Receiver feedback:因为接发双方之间可能相隔上千公里,只有接收方给回馈才能让发送方知道并提供数据报文的重传。接收端会根据收到的报文返回 positive(ACK) 和 negative(NAK) acknowledgments。由于只有两个状态,我们用一位就可以表示清除。
  • Retransmission:发送端根据反馈重新传输过程出错的报文。

下图我们展示 rdt2.0 收发两端的有限状态机模型。在 rdt2.0 中,发送端的FSM有两个状态:(1)等待来自上层应用的发送信息报文;(2)等待来自接收端反馈控制报文。接收端的FSM仍然只有那一个状态。

在发送端,FSM先完成等待上层应用数据状态的初始化,一旦触发发送数据的事件rdt_send(data)就会产生相应动作(sndpkt=make_pkt(data,checksum)udt_send(sndpkt))。之后,发送端会停止发送报文并转变状态到等待接收端发来控制报文的状态。也因此,rdt2.0 这种协议叫做 stop-and-wait protocol。当发送端接收到来自接收端的控制报文后,如果收到 NAK 信号,就启动重传,如果收到 ACK 信号,发送端回到初始位置并等待下一次的数据发送。

在接收端,FSM先完成等待发送端数据状态的初始化。之后,当接收方收到来自发送方的数据,接收方会先校验 checksum 是否无误,如果无误就将数据传给上层应用并返回 ACK 的反馈,如果 checksum 检验后不一致,就会丢弃当前数据报并反馈 NAK 的报文。
Pasted image 20240715220352.png
rdt2.0 看似已经很完善了,但是我们仍然没有考虑到反馈过程中 ACK 或 NAK 若是出现位错误该怎么办?我们考虑以下三种解决方案:

  • 引入新的询问报文:如果发送方不清楚接收方发来的反馈报文是什么,发送方可引入一个发送方到接收方的报文类型来给接收方说明对当前的反馈报文存疑。但要是这个报文也出现位错了呢?
  • 增加足够多的校验和位:通过增加足够多的校验和位,发送方不但可以检错,也可以纠错。但是如果传输过程中发生了 packet loss 就完蛋了。
  • 重复报文传输(Duplicate packets):发送方在收到存在位错误的反馈报文时再发一遍当前数据包。但这样可能引入新的问题——接收方应如何处理重复的报文?

而被大多数据传输协议所采用的方法是在数据包前面加入一个新字段——序列号(Sequence number)。现在,通过判断 序列号,接收方就知道收到的包是不是重传的。对于停等协议来说,1bit 的序列号就够了。1 bit 只能表示两位的状态。因此,我们用模2运算来表示包的先后顺序(前提假设下层网络的传输不会造成丢包),比如,对于0而言,1就表示下一个数据包的序列号,由于 module-2 运算,(1+1)/2结果余数0,这时序号0的数据报又变成了序号1的下一个数据包。(但注意:虽然序列号相同,但是数据包是不同的)

下面,我们来将接收端和发送端的两个 FSM 按部就班地解读一下。发送端等待并收到应用进程的第0号报文,将报文传输给接收端并等待接收端的控制报文。接收方发来控制报文后,判断如果发来的是 NAK 或者控制报文有位错误,就启动重传机制并继续等待。接收方再次发来的控制报文没有为错误且信号位ACK,这时的发送端转换状态并等待应用进程发来第1号报文。
Pasted image 20240715234126.png
在接收端,当收到第0号报文,接收端会检测位错误和序列号是否满足条件,若报文有位错误,则反馈 NAK 信号,如果没有位错误但序列号和当前等待接收的序列号不同则返回 ACK 信号,告诉发送端发重了。

Pasted image 20240715234140.png
rdt2.2 和 rdt2.1 最大的区别就是将报文nNAK信号和报文(n-1)的ACK信号放在一块检测。之前在rdt2.1中,接收方收到发送方的报文后会检查两个部分——报文有位错误?和报文发重了?然后根据这两个部分发送不同的反馈报文。但是在rdt2.2中,接收方将这些放在一起判断,并将判断结果放在一个反馈报文中返回给接收方。
Pasted image 20240715235048.png
Pasted image 20240715235100.png

Reliable Data Transfer over a Lossy Channel with Bit Errors: rdt3.0

我们再考虑丢包的情况进去,现在,我们不得不考虑:(1)怎么检测数据包的丢失;(2)数据包丢失之后该怎么办。考虑到这种新情况,我们需要在协议中引入一种新的机制来应对丢包。在这个机制中,我们只考虑让发送端来检测和恢复丢失的数据包。

假设在数据传输的过程中,发送方的数据包或者接收方的 ACK 信号由于重重原因丢了。这时,发送方在会在一个设定的时间猛惊醒,这么长时间,数据包肯定是丢失了吧,随后重传(retransmit)数据包。至于这个时间多长合适呢?发送方发送数据包后,它至少需要一倍的往返延时时间收到 ACK 信号(还要包括中转路由器中的时间和接收方对这些数据的处理时间)。但是这种时间在现实中的网络环境是很难测量的。而做最坏延迟情况又会使发送方启动重传之前等待很长时间。所以要等多久启动重传由发送方决定。

在发送方确定一个重传时间后,只要它在这个事件内没有收到 ACK/NAK 等信号,发送方就重传。我们可能会想到,如果发送方的数据包没丢只是现在没有收到 ACK 信号怎么办?这样,在发送方-接收方信道中引入了重复的数据包(duplicate data packets)。对于这种情况的处理我们在 rdt2.2 中已经学过(序列号),这里不再赘述。

从发送方的角度看,重传(Retransmission) 好像一副万能药。数据包传输过程中丢失、接收方的ACK信号丢失、ACK信号过度延迟等都可以用重传解决。这种基于一个时间重传的机制需要一个 定时器(Countdown timer) 在到时间后叫醒发送方重传数据包。因而,发送方需要(1)每次发送数据包都要重设时间;(2)相应计数器的中断,重传数据包;(3)收到 ACK 信号后停止计数。

下图展示了发送方rdf3.0的FSM。In Figure 3.16, time moves forward from the top of the diagram toward the bottom of the diagram; note that a receive time for a packet is necessarily later than the send time for a packet as a result of transmission and propagation delays.
Because packet sequence numbers alternate between 0 and 1, protocol rdt3.0 is sometimes known as the alternating-bit protocol. We have now assembled the key elements of a data transfer protocol.
Pasted image 20240718000203.png
Pasted image 20240715235100.png
终于,经过完善,我们的传输层协议可以实现可靠的数据传输(不用担心信道中会发送位错误还是丢包)
Pasted image 20240718003101.png

3.4.2 Pipelined Reliable Data Transfer Protocols

在上小结的内容中,虽然我们完成的 rdt3.0 可以实现一个可靠传输协议的功能性,但这种基于停等协议的传输层协议的性能太弱了。
Pasted image 20240718003522.png
为直观理解停等协议带来的性能影响,我们假设现在有如上图所示的两台主机分布在美国的东西海岸,假设传播介质中的传播速度是光速,那大概需要30毫秒的时延。假设现在我们的传输率 是 1Gbps,数据包的大小 是1000 bytes(8000 bits),传输完这8000bits需要:μ考虑到传播时延,整个数据包从开始传输到收到 ACK 信号总共需要 。由于30ms的时间都是传播所用的废物时间,所以这时的利用率(utilization) 为:只有 不到万分之三 这个利用率是很低的,几乎不占用网络资源。发送方在30.008毫秒内只能发送一个1000bytes的数据包(1 Gbps带宽可用的情况下只用了267 Kbps)。而且我们忽略了下层协议的处理时间和重传丢包等情况,加上这些,效率会更低。
Pasted image 20240718014251.png
要提高停等传输的效率,我们可以将数据包做的很大,但是会导致严重的网络拥塞,而且一旦丢包,重传的代价会很大。另一种方案就是在不确定接收方是否收到数据包,收到数据包后是否有错误,而接连发送多个数据包,这样不仅合理利用了网络带宽,还避免了拥塞问题的发生。我们称后一种方法为流水线传输(pipelining) 在可靠数据传输中实验流水线有如下要求:

  1. 序列号必须是增大的(不包括重传情况)而且每个传输数据包的序列号必须唯一。
  2. 协议中的收发两端的实现中需要有一个buffer来存放这些连续传输的数据包。发送方buffer必需装得下还没有获得确认信号的数据。
  3. 序列号和buffer将在后面请求重传时用到。我们之后会学习两种带纠错机制的流水线传输协议——回退N帧协议(Go-Back-N)选择重传协议(Selective Repeat)
    Pasted image 20240718014358.png

3.4.3 Go-Back-N (GBN)

在上小节的学习中,我们学到停等协议下可靠数据传输协议是如何实现的,在小节末,我们通过例子直观感受到了停等协议对带宽利用的低下。这小节及下小节,我们就来着眼看看流水线传输方式实现的两个可靠数据传输协议。这小节我们来学习 回退N帧协议(Go-Back-N)

下图展示了一个GBN协议中序列号,这是发送方的视图。我们用 base 表示为目前发送buffer中等待 ACK 信号时间最长的数据包的序列号。我们在buffer中划一个窗口,窗口大小设置为 base 及往后的 N 个数据包。定义 nextseqnum 为下一个要发送数据包的序列号。通过这两个定义的序列号,我们现在可以将buffer中所有的数据包(序列号)如下分组:

  1. \[0, base-1]用来表示发送后已经收到 ACK 信号的数据包(序列号);
  2. \[base, nextseqnum-1]表示发送了,但是没有收到接收方反馈信号的数据包(序列号);
  3. \[nextseqnum, base+N-1]表示收到的应用层数据包,但是没有发送的数据包(序列号);
  4. 最后,base+N往后的那些序列号分成一组,它们需要等前面 base 得到 ACK 信号后,basenextseqnum 才会向后挪。
    Pasted image 20240720004907.png
    通过分组,我们了解到窗口大小 (N) 是不变的,而且窗口会随着GBN的运转而往后滑动。由于这种滑动,GBN协议也叫 sliding-window protocol。但是我们还要注意,这个窗口大小 (N) 并不是无限大的,(N) 不仅仅受到发送端缓冲区大小的制约,还受到序列号位数和拥塞控制的影响。

现实中,数据包的序列号是包含在数据包头的一个定长字段。如果序列号位数为 ,那么序列号的范围就是[0,-1]。它也最多只能表示个序列号,由于序列号是循环使用的,序列号达到 (-1) 后,下一个序列号将回到 0。这就是为什么我们需要采用模 () 计算。我们需要采用模计算。(将模()想想成一个环,这个环起点和终点都是00……000,但是终点前一个数11……111加1就又会回到起点了)之前实现的 rdt3.0 中,我们使用的mod 2,序列号只能是0和1。而TCP的序列号字段很大,有 32-bit,因而,我们可以在TCP的传输中用序列号表示字节流中的字节数而不是数据包(不然重传代价可能会很大)

下面的两个图描述了使用GBN协议收发两端的有限状态机模型。在使用GBN后,当报文发生错误后发送方并不用等待来自接收方的 NAK 信号,当发送方没有接收到 ACK 信号且超时后发送方重传相关数据即可。在这个 extended FSM 中,新增加了 base 和 nextseqnum 的变量,下面我们看看发送方都要做出那些反应。

  • Invocation from above.
    rdt_send(data)开始发送数据前,发送端系统会先判断窗口是不是满的。如果窗口满了,那么发送方会将数据返回到上层应用,告诉应用现在发不了数据(在实际实现中,发送方更可能会将数据缓冲(但不立即发送),或者使用同步机制(例如信号量或标志),以便只有在发送窗口未满时,上层才会调用 rdt_send())。如果没满,发送方就会创建并发送数据包并更新变量 base 和 nextseqnum。
  • Receipt of an ACK.
    在GBN协议中,从接收方发来的 ACK 信号会被视为一种 累计确认(Cumulative ack),也就是当接收方发来对序列号为n的 ACK 确认报文时,代表序列号小于n的所有报文都是没有问题的。收到确认后,发送方就可以将窗口向前滑动,继续发送新数据。
  • A timeout event.
    GBN对超时处理的方式就是它名字(Go-Back-N)的由来。当超时事件(序列号为N的数据包规定时间内没有收到 ACK 信号)发生,发送方就会先重启定时器,然后将序列号 N 往后的所有报文重发一遍,。如果在重发过程中有新的ACK信号到达,定时器会相应地调整。

Pasted image 20240720033306.png
GBN协议发送方的FSM也十分简单,如果发送方的数据包按序正确到达,它就回馈对这些报文的 ACK 信号(某个最近一次数据包序列的ACK)并将信号传送到上层应用。同理,如果已经收到并传送过序列号k的数据包,那就意味着序列号小于k的数据包都已经被传送过了。这种累计确认(Cumlative acknowledgment)在GBN协议中十分常见。

从刚才起,可能我们就会有疑问,底层网络并不提供按序到达的保证,那么传输层向上层应用提供的按序到达的保证从何谈起呢?当前接收方需要收到序列号 n 的数据包,但如果网络先传给它序列号 n+1 的数据包该怎么办?发生这种情况,GBN协议的接收方会直接丢弃所有的乱序数据包

假设接收方会将序列号 n+1 数据包正确后将这个数据包缓存起来,将序列号 n 的数据包递交给应用后在按顺序传输缓存的第 n+1 个数据包。但如果序列号 n 的数据包丢失了呢?GBN的超时重传机制会将序列号 n 往后的所有数据包都重传,缓存的第 n+1 个数据包就没有用武之地了。直接丢弃第 n+1 个数据包反而简化了接收方的缓冲处理。这样做也有缺点——后续重传的数据包可能继续丢失或损坏,从而需要更多的重传。
Pasted image 20240720033322.png
下图展示了当窗口大小为4时的GBN协议运行时的样子。由于这种窗口的限制,序列号范围只能从0到3,在发送完三号数据包后就等待来自接收端的 ACK 信号。收到正确的 ACK 信号后,窗口就会往后滑动发送新的数据包(pkt4 和 pkt5,由于系统是mod 4的,所以pkt4 和 pkt5实际等价于 pkt0 和 pkt1)。 在接收端,由于 pkt2 的丢失,往后的 pkt3、pkt4、pkt5 都要重传。
Pasted image 20240720042336.png
在现实的协议栈(protocol stack)中,对GBN协议的实现实际上和我们图3.20 的FSM非常相似。在这种 基于事件编程(event-base programming) 中,各个过程都是被协议栈中的其他过程(或中断)所调用起来的。在发送端,这种事件可能是:

  1. 上层应用实体唤起 rdt_send(),
  2. 定时中断
  3. 有数据包到达时的下层实体调用 rdt_rcv()

通过本节的学习,对我们后续TCP的理解也会很有帮助。GBN协议几乎包含了研究TCP可靠数据传输组件时会遇到的所有技术。包括使用序列号、累积确认、校验和以及超时/重传操作。

3.4.4 Selective Repeat (SR)

在上节课中的GBN协议中,我们解决了停等协议中的资源利用率低下问题。但是GBN协议也会有性能方面的问题。更确切地说,在窗口大小和带宽都足够大时,全部重传问题不大,但在网络带宽较小时,GBN协议可能会导致网络拥塞,并且对接收方和发送方的时间效率都不友好(发送方会重传大量数据,接收方无法提前缓存损坏或丢失的数据包)。在这些前提下,选择性的重传好像是一个更好的选择。

选择性重传(Selective-Repeat protocol) 中,当发送方的某个数据包没有收到ACK信号时,它只需要重新发送该数据包,这样信道就不会被无用的数据包占用。

这种按需重传要求接收方逐一检查数据包并回发ACK信号,这就要求接收方缓存相关的数据包,并在排序后将其交给应用层。下面展示了 SR 发送方和接收方的视图。这里,窗口大小N继续用作限制一次性发送数据包的最大数量,避免拥塞。
屏幕截图 2024-07-21 161530.png

SR sender events and actions

发送方的一生会处理以下三种事件(events),还有对这三种事件的处理方式(action)如下:

  1. Data received from above
    当发送方收到上层应用传来的数据,SR发送方就会检查 nextseqnum 的大小。如果序列号在发送方的窗口内,传输层就会封装并发送数据;不然就先缓存或者让应用待会再传。
  2. Timeout
    和GBN中一样,SR发送方也会有应对数据包丢失的定时器。但由于选择重传的缘故,每个数据包都要有单独的逻辑定时器(logical timer),这些逻辑定时器都会在一个物理定时器中实现。
  3. ACK received
    一旦发送方收到某个数据包的 ACK 信号,它就会对这个数据包进行标记(Already ACK'd)。如果这个数据包的序列号是 send_base,窗口就会向前滑动到下一个标记为 Sent, not yet ACK'd 的数据包处。
SR receive events and actions

接收方要处理的事件和处理方式如下:

  1. Packet with sequence number in [rcv_base, rcv_base+N-1] is correctly received
    这种情况下,序列号落在接收方窗口内,接收方收到数据包后随即反馈 ACK 信号。要是这个数据包之前没有收到就缓存起来。如果数据包的序列号等于 rcv_base,就将之后连着的数据包一同送到应用上层,同时滑动接收方窗口。
  2. Packet with sequence number in[rcv_base-N, rcv_base-1]is correctly received.
    如果发生这种情况,肯定是接收方的 ACK 反馈报文丢失或出错了。这种情况下接收方必须赶紧打包发送这个数据包的 ACK 信号报文(re-acknowledgment),不然可能出现传输停止的情况,毕竟发送窗口就那么大。
  3. Otherwise
    如果接收方没有收到或者收到了错误的数据包,接收方什么都不用操心,安心等待发送方重传就好。

对于第二种接收方的事件处理的情形我们在下面的图中予以更直观的展示。为方便理解,下图中的窗口大小只有4个序列号。一旦发送方没有收到序列号为2的 ACK 报文,发送方的窗口就会卡在这里。这也就是我们为什么强调接收方在收到数据包序列号在接收窗口前时,就必须发送 re-ack 信号,不然发送方就一直卡在这里了。
Pasted image 20240721225159.png

SR Transmission with Mod-4 Scenario

从上面的例子我们能学到,发送方和接收方的同步非常重要。接着上面 Figure 3.26的例子继续,但我们的窗口大变为3。回忆之前了解过的模运算(modular arithmetic),假如我们的系统是mod-4系统,不能够表示除了序列号范围0、1、2、3以外的数字。在mod-4系统中,我们要怎么才能够知道发送方发来的数据包是新的还是重传的?我们通过下面两个例子来了解一下这种情况。

在第一个例子(Figure 3.27a)中,接收方收到数据包后窗口向后挪动,由于我们窗口大小是3,一次性能够传输3个数据包。在接收方收到数据后返回这3个数据包的 ACK 报文,如果这三个 ACK 报文丢了怎么办?在发送方,超时后又开始重传这三个数据包,等 pkt0 到接收方时它会以为这是新的0号数据包。而第二个例子中,pkt0、pkt1、pkt2都正常传输接收并返回 ACK 数据包。随后传输pkt3、pkt0和pkt1,这次的pkt3在传播过程中丢失了,但后面传输的pkt0是新数据。

怎么辨别呢?在窗口大小等于3时没有办法知道收到的pkt0到底是重传还是新的报文。但是如果我们将窗口大小改为2或者1,那就很容易辨别后面传输的数据包属于哪种情况。因此在SR协议中的窗口大小必须小于等于序列号数的一半。
Pasted image 20240722010246.png
关于可靠数据传输协议的实现我们已经了解许多,我们在下表中回顾这些rdt的机制,所有的所有都使我们有能力看见"the big picture"。
Pasted image 20240722021917.png
Let’s conclude our discussion of reliable data transfer protocols by considering one remaining assumption in our underlying channel model. Recall that we have assumed that packets cannot be reordered within the channel between the sender and receiver. This is generally a reasonable assumption when the sender and receiver are connected by a single physical wire. However, when the “channel” connecting the two is a network, packet reordering can occur. One manifestation of packet reordering is that old copies of a packet with a sequence or acknowledgment number of x can appear, even though neither the sender’s nor the receiver’s win dow contains x. With packet reordering, the channel can be thought of as essen tially buffering packets and spontaneously emitting these packets at any point in the future. Because sequence numbers may be reused, some care must be taken to guard against such duplicate packets. The approach taken in practice is to ensure that a sequence number is not reused until the sender is “sure” that any previously sent packets with sequence number x are no longer in the network. This is done by assuming that a packet cannot “live” in the network for longer than some fixed maximum amount of time. A maximum packet lifetime of approximately three minutes is assumed in the TCP extensions for high-speed networks [RFC 7323]. [Sunshine 1978] describes a method for using sequence numbers such that reorder ing problems can be completely avoided.

3.5 Connection-Oriented Transport: TCP

VINTON CERF, ROBERT KAHN, AND TCP/IP

在20世纪70年代初,分组交换网络开始普及,ARPAnet(互联网的前身)只是众多网络中的一个。每个网络都有自己的协议。两位研究员,Vinton Cerf 和 Robert Kahn,认识到互联这些网络的重要性,并发明了一种跨网络协议,称为 TCP/IP,即传输控制协议/互联网协议。虽然 Cerf 和 Kahn 最初将该协议视为一个整体,但后来将其分为两个部分,分别是 TCP 和 IP,它们独立运行。Cerf 和 Kahn 于1974年5月在《IEEE通信技术》上发表了一篇关于 TCP/IP 的论文 [Cerf 1974]。

TCP/IP 协议是当今互联网的基础,它在个人电脑、工作站、智能手机和平板电脑出现之前,在以太网、电缆和 DSL、WiFi 以及其他接入网络技术普及之前,在网络、社交媒体和流媒体视频出现之前就已经被设计出来了。Cerf 和 Kahn 认识到需要一种网络协议,一方面为尚未定义的应用程序提供广泛支持,另一方面允许任意主机和链路层协议互操作。

2004年,Cerf 和 Kahn 因“在互联网上的开创性工作,包括设计和实现互联网的基本通信协议 TCP/IP,以及在网络领域的启发性领导”而获得了被誉为“计算机界诺贝尔奖”的 ACM 图灵奖。

3.5.1 The TCP Connection

从第一章开始,我们就提到了TCP是面向连接的(connection-oriented),这是因为在应用进程向另一个应用进程发送数据前,两个进程需要先“握手(handshake)”。在握手的过程中,客户端和服务器会互相发送一些初始数据包(preliminary segments),如:SYN、SYN-ACK、ACK。通过这些初始数据包的交换,客户端和服务器都可以确定一些确保数据传输的关键通信参数,如:初始序列号、窗口大小、确认号(ACK)。

TCP的“连接(Connection)”并不是在线路交换网络(circuit switched network)中那种端到端的 TDM 或 FDM 线路。这里的“连接”是一种逻辑概念,这种状态只有通信端系统上才知道,中间节点(路由器等)是看不见连接的,它们只能看到数据报(datagram)的传输。

TCP连接为客户端/服务器双方提供全双工的服务(full-duplex service):也就是在TCP连接中,进程A向进程B发送数据的同时进程B也可以给进程A发送数据。除此之外,TCP的连接也永远是点对点(point-to-point) 的,也就是TCP的连接局限于一个接收方和一个发送方。一个发送方多个接收方这种“多播(multicasting)”的情形不会出现。

那TCP连接建立的过程是怎么样的?我们之前提到过初始化连接的是 客户进程(client process),另一个进程就是 服务器进程(server p'r)。在 2.7 节的 TCP 客户端编程课中,我们把连接服务器的代码放在 if 判断语句中了。这里的serv_addr是一个结构体,包含服务器的 IP 地址和端口号。

if (connect(sock, (struct sockaddr *)&serv_addr, sizeof(serv_addr)) < 0)

当客户进程执行这段代码后,客户端 TCP 就会开始初始化建立与服务器 TCP 的连接。现在我们只需要知道连接的建立是客户端和服务器互发初始数据段实现的,而 SYN 、SYN-ACK 是不带负载的,也就是没有应用层的数据,在第三个数据段 ACK 会带一点负载。正是由于连接的建立过程是通过这三个报文段的发送与接收建立起来的,因此这个过程往往被称为三次握手(three-way handshake)

一旦 TCP 连接建立,两个应用进程就可以互发数据了。现在我们考虑客户端是怎么给服务器发送数据的。沿用发快递的例子,应用进程(房子)将数据(快递件)通过 socket(门)交给传输层(快递员,房子之外的地方)。在将快递件送给快递员之前,要先确保驿站还有放快递的空位(发送缓冲区(send buffer))。发送缓冲区在三次握手期间就设置准备好了(预约发快递,驿站会确定是否可发)。如果可以,应用进程(房子)就会把数据(快递件)源源不断的送给传输层(驿站),在传输层添加头部信息后(包裹信息),再送给网络层(快递公司),由网络层负责将数据传输到目的地。TCP 规范并不规定 TCP 需要什么时候才能将缓冲区中的段送给网络层。
Pasted image 20240725192031.png
每一次最大传输多大的数据收到最大段大小(MSS) 的限制。MSS的大小又收到最大传输单元(MTU) 的限制。这很好理解因为在网络中的数据是层层封装之后以类似“包裹”一样传输的,而链路层的帧最大多大,MSS 就理所当然的等于 MTU(通常是1500字节)减去 TCP/IP 的头部长度(通常是40字节)也就是1460字节的大小。以太网和 PPP 链路层协议中都规定 MTU 大小为1500字节,因而 MSS 典型值就是1460字节了。当然了,路径 MTU (path MTU) 会受到路径中最小链路层帧大小的限制。

NOTE:在这里,MSS (Maximum Segment Size) 指的是应用层传下来数据的大小,并不包含 TCP/IP 的头字段。

TCP 通过加入头字段的方式将数据封装成 TCP 报文段,然后传下去到网络层封装成 IP 数据报,之后把 IP 数据报发向网络。当接收方的传输层收到 TCP 报文段后,这些段就会先放在 TCP 接收缓冲区中。应用就从这些缓冲区中读取数据流。每个主机都会有自己的发送缓冲区和接收缓冲区。

3.5.2 TCP Segment Structure

和 UDP 一样,TCP 报文段也是由头部字段和数据字段构成的。报文段的头部字段大小是固定的(通常为20字节)剩下的数据字段就是从上层应用传下来的数据。上小结我们了解的 MSS 就表示数据字段最大的字节数。如果我们要传输一张很大的照片,通常我们会将图像文件分割成不大于 MSS 的数据库(chunks),最后一个数据块往往比 MSS 小。而在一些交互式应用上,数据块的大小可能会远远小于 MSS,比方说像 Telnet 或 ssh 这样的远程登陆应用中,当用户在终端中输入一个字符时,这个字符会立即被封装成一个 TCP 段发送出去。TCP 数据段的数据字段通常只有 1 字节,如此,一整个 TCP 段就只有 21 字节,头字段占了20/21。

下图我们展示了 TCP 段的段结构,和 UDP 一样,段头都有源和目的端口号,用于数据在应用层和传输层之间multiplexing/demultiplexing。还包括了和 UDP 一样的校验和字段。除此之外,TCP 还包括一些额外的字段:

  • 32位的序列号(sequence number) 字段 和 32位的确认号(acknowledgment number) 字段。它们用来实现可靠数据传输的服务。
  • 16位的接收窗口(receive window) 字段用来流量控制。
  • 4位的头部长度(header length) 字段,也叫数据偏移(data offset),以32位字(word)为单位指定 TCP 头部的长度(最大 60 字节)。由于 TCP 选项字段的存在,TCP 头部的长度可以是可变的。(通常情况下,选项字段是空的,因此典型的 TCP 头部长度为 20 字节。)
  • 选项字段(options field) 的长度是可变的,用于在发送方和接收方协商最大段大小(MSS)或作为高速网络中使用的窗口缩放因子。此外,还定义了时间戳选项(time-stamping option)。
  • 控制位(flag field) 包括8个bit位,有URG、ACK、PSH、RST、SYN、FIN。
    1. CWR 和 ECE 位用于拥塞控制,我们之后会有专门的小节介绍 TCP 的拥塞控制。
    2. URG(Urgent):紧急指针有效。表示数据段中有紧急数据,接收方应优先处理。
    3. ACK(Acknowledgment):确认序号有效。表示确认接收到的数据,通常在数据传输过程中每个数据段都会设置这个位。
    4. PSH(Push):推送数据。表示接收方应立即将数据推送给应用层,而不是在缓冲区中等待。
    5. RST(Reset):重置连接。表示连接出现问题,需要重新建立连接。
    6. SYN(Synchronize):同步序号。用于建立连接的初始同步过程,通常在三次握手的前两次握手中使用。
    7. FIN(Finish):结束连接。表示发送方已经完成数据传输,要求关闭连接。
  • 紧急指针(urgent data pointer) 字段,共16个字段指示紧急数据的结束位置。
    Pasted image 20240726044658.png
    在现实中,我们很少使用 TCP 的 PSH(Push)、URG(Urgent)标志位和紧急指针。在此介绍只是为了整个段构成的完整性。
Sequence Numbers and Acknowledgment Numbers

序列号和确认号是 TCP 最重要的两个字段,这两个字段保障了 TCP 可靠数据传输的实现。在上节课中我们也讨论过相关的 rdt 实现。现在,我们就来看看 TCP 到底在这两个字段里面塞了什么。
Pasted image 20240727043719.png
如上图,TCP 将数据视为一种无结构有序的字节流。和我们想象的可能不同,TCP 用序列号并不用于段的编号,而是数据流中对各个字节所编号。每个段第一个字节的序列号就代表了这个段的序列号。因而,序列号的编号是只针对数据字段的。上图的文件包含了500 000字节的数据流,而每个 MSS 1000字节,因此我们有500个数据段,第一个段的编号就是0,第二个段的编号为1000,以此类推。这样段的编号就会作为序列号字段插在头部字段(header field)中。

OK,现在我们来学习另外的确认序列号(acknowledgment numbers)。回忆一下,我们说 TCP 是全双工(full-duplex)的,在接收方(Host B)接收数据段的同时向发送方(Host A)反馈段的接收情况。每个数据段都会对应唯一的序列号,Host B 在正确收到数据段后会向 Host A 发送序列号 + 1的 ACK 信号段文。举个例子,在 1st 段文(0-999)被 Host B 收到之后,Host B 会发送序列号为1000的 ACK 段。TCP 提供累计确认(cumulative acknowledgments) 的机制,如果 ACK 段中序列号是1000,就表示序列号小于1000的字节均已收到,等待序列号1000往上的数据。

在 TCP 的 RFC 相关文档中其实并没有规定 TCP 连接中的主机收到乱序的段该怎么办。结合我们之前讲过的,我们有两种办法:
(1)丢弃乱序的数据段;
(2)将乱序的数据段放在一个接收缓冲区中,等待丢失的数据段(missing bytes)来填满空隙。
尽管第一种方法简单且减少接收主机的部件设计,但由于第二种方案在网络带宽利用的更加高效,因此在现实中多采用第二种方案。

在图3.30中,我们假设初始序列号为0。实际上,TCP连接的双方会随机选择一个初始序列号。这么做是为了减少这样一种可能性:两个主机的连接已经终止了,但是先前的某个数据段还留存在网络之中,这个数据段就可能被之后同一对主机间新创建的连接当成有效的数据段(并且新连接恰好使用了与旧连接相同的端口号)

Telnet: A Case Study for Sequence and Acknowledgment Numbers

Telnet 是在 RCF 854 文档中规定的应用层协议。Telnet 跑在 TCP 协议上,可以用在任意一对主机上的远程登录。但由于 Telnet 连接并不加密,很多人会选择 SSH 协议。

假设我们现在有两个主机 Host A 和 Host B,其中 Host A 是客户端,初始化 Telnet 会话, Host B 是服务器。在 Telnet 上,Host A 键入的每个字符都会封装成一个 TCP 数据段发给远程主机;然后远程主机会回发每个字符的拷贝,显示到用户的屏幕上。这种”回发(echo back)“机制保证每个字符在远程主机上收到后都得到了处理。

假设用户输入了一个字母’C',那客户端服务器之间的数据段传输是怎么样的?从传输开始到结束,客户端和服务器总共发了三个数据段,在TCP连接建立完毕后,客户端等待服务器端送来79号字节,服务器等待客户端的42号字节。双方对此心知肚明,因此客户端发送第一个报文段时会将42号序列号的字节发给服务器(包含 ASCII 的 'C"),并将服务器索要79号序列号的字节。

之后,服务器会在序列号为79的字节将字母 'C' 通过TCP报文回发给客户端,并请求客户端的43号报文(告诉客户端序列号42前面的数据都收到啦)。在服务器向客户端发送的TCP数据段中,我们可以发现这个数据段Seq=79, ACK=43, data='C'实际上发挥了两个作用——(1)ACK=43,收到前面的数据啦;(2)对数据 'C' 的回发。在同一个数据段中实现了对前面发送数据的确认,这种技术叫捎带确认(piggybacking)

最后一个段只有一个作用——客户端对服务器发送信息的确认(ACKed)。这个数据段没有数据字段(这个 acknowledgment 就不是客户端-服务器数据字段的捎带确认了)。我们注意到这个数据段并没有数据字段但是仍然有序列号,这是因为TCP就是这样要求的。
Pasted image 20240727055138.png

3.5.3 Round-Trip Time Estimation and Timeout

TCP和我们前面设计的 rdt 协议一样,都使用超时重传(timeout/retransmit) 机制来弥补那些已经丢失的段。尽管看起来很简单,但是我们不得不考虑在超时中的这个“时”是怎么来的?在此之前,我们得先知道往返时延(RTT) 多大,之后根据 RTT 确定多长时间重传一次。

Estimating the Round-Trip Time

现在,我们开始学习在 TCP 是怎么预估发送方-接收方之间的往返时间RTT的。我们用SampleRTT来表示数据段从传输层发送出去到接收到 ACK 报文段之间这么长的时间。SampleRTT在 TCP 的实现上通常表示目前还没收到 ACK 报文的这报文段发送后已经过了SampleRTT的时间。还有,TCP 不会记录重传段的SampleRTT,只记录发送一次的报文段。

显然的是,报文段传输路径拥塞度不同,SampleRTT值也会随之浮动变化,由此产生的SampleRTT时间我们通常称作非典型的(Atypical)。要想预估到更稳定的往返时间,我们就需要将每个段的SampleRTT来平均一下,获得的时间我们叫EstimatedRTT。我们通常用下面的公式计算:经过大量实验和应用,实验人员证明 的 α 值能够在大多数情况下提供稳定且准确的 RTT 估计。因而也将 1/8 的 alpha 值写进了 [RFC 6298]。这样,上面的公式就变成了:我们在这里计算的 EstimatedRTTSampleRTT 的加权平均值,我们给当前的 SampleRTT0.125的权重,这样算得的EstimatedRTT就更好反应目前接收双方之间节点的拥塞度。在统计学中,这种平均被称为 exponential weighted moving average (EWMA)。下图展示gaia.cs.umass.edufantasia.eurecom.fr之间 TCP 连接的 RTT。我们看到EstimatedRTTSampleRTT更加平滑。
Pasted image 20240811181317.png
然除了 RTT 的预估,RTT 的多变性也是我们要关注的。 在[RFC 6298]中同样定义了 RTT 变动,用DevRTT表示。用下列的公式计算:我们用DevRTT表示SampleRTTEstimatedRTT之间的 EWMA 差异。DevRTT越小就说明网络越稳定,反之,网络则不稳定。对 β 的推荐值是0.25。

PRINCIPLES IN PRACTICE

*TCP 通过使用积极确认和定时器来提供可靠的数据传输,这与我们在第 3.4 节中研究的方法非常相似。TCP 确认已正确接收的数据,然后在认为数据段或其相应的确认丢失或损坏时重新传输这些数据段。某些版本的 TCP 还具有隐式 NAK 机制——通过 TCP 的快速重传机制,接收到某个数据段的三个重复 ACK 被视为该数据段后续数据段的隐式 NAK,从而在超时之前触发该数据段的重传。TCP 使用序列号使接收方能够识别丢失或重复的数据段。就像我们的可靠数据传输协议 rdt3.0 一样,TCP 本身不判断某个数据段或 ACK 报文是否丢失、损坏或延迟过长。在发送方,TCP 的响应都会是一样的:重新传输相关的数据段。

TCP 还使用流水线技术,允许发送方在任何给定时间内有多个已传输但尚未确认的数据段。我们之前看到,当数据段大小与往返延迟的比率较小时,流水线技术可以大大提高会话的吞吐量。发送方可以拥有的未确认数据段的具体数量由 TCP 的流量控制和拥塞控制机制决定。TCP 的流量控制将在本节末尾讨论;TCP 的拥塞控制将在第 3.7 节讨论。目前,我们只需要知道 TCP 发送方使用了流水线技术。*

Setting and Managing the Retransmisson Timeout Interval

好了,现在我们知道怎么算得EstimatedRTTDevRTT,很显然,我们所要的重传间隔得大于等于EstimatedRTT,但是不要大太多。而且我们想的重传间隔最好跟网络的拥塞变化要息息相关。当网络一时间更加拥塞了,我们需要调整重传间隔让它变得更大。这样我们就会得到下面式子:在[RFC 6298]中,建议初始的超时间隔为 1 秒。当发生超时时,TCP 会将当前的超时间隔值加倍。这是为了避免在接下来的数据段即将被确认时发生过早的超时。通过加倍超时间隔,可以减少不必要的重传,从而提高传输效率。一旦接收到数据段并更新了 EstimatedRTT(估计的往返时间),TCP 会重新计算超时间隔。新的超时间隔将基于更新后的 EstimatedRTT 计算。

3.5.4 Reliable Data Transfer

我们提到过网络层的 IP 协议是不可靠的,IP 不保证传输过程中数据报的按序到达和完整性。而传输层建立在网络层之上,所有传输层的数据报会遇到到达时的乱序、不完整、丢失等各类问题。因而对于传输层提供可靠性的 TCP,需要在 IP 尽力而为的非可靠服务上提供可靠数据传输服务,也就是说接收端接收的字节流要和发送方发送字节流一致。其中很大一部分的原则我们都在 3.4 rdt 那一节学习过。

还需要注意的是,虽然我们之前提到的超时重传为每个数据段都配备一个定时器时刻检测间隔时间(rdt)。但是尽管后面还可能有许多未收到 ACK 的数据段,TCP 定时器管理也只使用了 一个 重传定时器(跟踪新的最早发送但还没有确认的数据段)。

知道关于定时器的相关知识,现在我们逐步讨论一下 TCP 所提供的可靠数据传输服务。我们首先看一下 TCP 发送方只使用超时重传机制恢复丢失段的简化描述;之后介绍一个更完整的 TCP 发送方模型,该模型不仅使用超时机制,还使用重复确认(duplicate acknowledgments) 来从丢失的数据段中恢复。

我们用下面高度简化的 TCP 伪代码描述一下三个主要的事件及其处理。

/*假设暂且不受TCP拥塞控制的约束,且传递节点单一*/

NextSeqNum = InitialSeqNumber
SendBase = InitialSeqNumber

loop(forever){
	
	switch(event)
	
		event: data receive from application above
			create TCP segment with sequence number NextSeqNum
			if(timer current not running)
				start timer
			pass segment to IP
			NextSeqNum=NextSeqNum+length(data) 
			break;
		event: timer timeout 
			retransmit not-yet-acknowledged segment with smallest sequence number
			start timer
			break;
		event: ACK received, with ACK field value of y 
			if (y > SendBase) { 
				SendBase=y 
				if (there are currently any not-yet-acknowledged segments) 
					start timer 
			} 	
			break;

}/*end of loop*/

我们一件一件描述上面对发送方的三大事件。

  1. 收到应用层传来的数据包:封装来自应用层的数据包并发送给 IP。在此之间,TCP 会检测计时器是否在工作,如果没有就启动定时器。
  2. 定时器超时:从未得到确认报文的最小的那个序列号开始重传。
  3. 收到确认号为 y 的 ACK 数据段:如果 y 的大小大于 SendBase,就将滑动窗口,将 y 赋予 SendBase。然后如果检测到还有未得到确认的报文,就重启定时器。
A Few Interesting Scenarios

虽然我们刚刚只是给出了 TCP rdt 的简化版本,但其中也不乏一些微妙之处。在图3.34中,Host A给Host B发送了包含 8 字节数据序列号为 92 的数据段,并期望收到来自Host B包含确认号 100 的确认字段。但是第一次Host B传输 ACK 报文段的时候段丢失了,Host A的计时器在一段时间间隔中没有收到来自Host B的 ACK 信号,就启动重传,之后成功收到Host B传来的响应报文段。
Pasted image 20240812102024.png
但在下图3.35中,没有数据段的丢失,但是Host B的 ACK 响应没有在超时间隔内传到Host A,因而Host A启动重传。重传后在间隔内收到Host B传来对两个数据段的确认,因为对两个数据段都进行了确认,Host A也就不需要重传第二个数据段了。
Pasted image 20240812120639.png
在下图3.36的第三种情形下,我们看到在 Host B 回传对两个数据段的确认时丢失了第一个响应报文。但是由于Host A收到第二个响应报文(ACK=120)因此并不会启动重传,因为 120 的确认号对前面的字节序列进行了累计确认。
Pasted image 20240812120931.png

Doubling the Timeout Interval

现在我们做一些现实性的改变。在现实应用中,TCP 的重传时间并不是一成不变的,没多一次重传,TCP 都会设置重传时间为原先的两倍。比方说刚开始的超时间隔是 0.75 秒,如果时间内没有收到 ACK 确认段,TCP 就会启动重传并设置超时间隔为 1.5 秒,然后 3 秒。但如果如果在此期间接收到新的数据(定时器未工作)或 ACK 确认,超时间隔就会通过最新的 EstimatedRTT 和 DevRTT 重新计算。

这种超时间隔设置翻倍的改变是我们后面学习 TCP 拥塞控制的前提。因为定时器超时最可能是因为网络拥塞问题造成的。一次性太多数据包同时到达某一路由器队列就可能造成路由器的丢包或过久的排队时延。试想,网络状况本来就不太理想的情况下还频繁地发起重传,拥塞就会更加严重,但增加重传间隔就会在网络状况改善时能够迅速调整超时间隔。这种思想也会在我们 Chapter 6 中学习 CSMA/CD 时用到。

Fast Retransmit

当然,超时间隔翻倍也有一个坏处,即超时间隔会变得很长。会出现每次丢包都会让重传的时间间隔翻倍,这样就增加了端到端的延时。但发送方可以通过检测重复的 ACK 来发现数据包丢失,而不必等待超时事件的发生。接收方通过重复 ACK(duplicate ACK) 告诉发送方还有还有哪一序列号的报文没有收到。发送方在收到多个重复ACK后(一般认为是3个)会认为某个数据段丢失,并立即重传该数据段。

下图我们总结了 TCP 接收方的ACK生成策略。一共有 4 种情况导致接收方生成ACK:

  1. 接收方收到按序到达的数据段,且所有数据都已经被确认。
    • 接收方做法:延迟确认(Delayed ACK),即接收方不会立即发送ACK,而是等待最多500ms,看看还有没有下一个按序到达的数据段。
  2. 接收方收到按序到达的数据段,并伴随一个按顺序到达尚未确认的数据段。
    • 接收方做法:立即发送累计确认ACK,对这两个按顺序到达的数据段进行确认。
  3. 接收方收到乱序数据段并检测到前面数据段之间存在字节序列号的gap。
    • 立即发送重复ACK,期待收到的下一个收到字节序列号为gap的下沿。
  4. 接收方收到填充gap的数据段。
    - 立即回发ACK信号。
    Pasted image 20240812144354.png
Go-Back-N or Selective Repeat?

那么 TCP 采用的是 GBN 协议还是 SR 协议呢?我们之前说过,TCP 对报文的确认是累积性确认的,接收方并不会按个确认那些乱序但准确到达的数据段。发送方也只需要在那些未收到确认的序列号中维护最小的那个(SendBase),另外

维护下一个要发送字节的序列号(NextSeqNum)就可以了。截至这里,TCP 看起来好像是一个 GBN 的协议。但是TCP 和 GBN 还有许多不同之处。因为 TCP 的实现需要缓存那些收到的无误的数据段。(GBN会丢掉那些乱序的数据段)

假想一下,发送方要发送序列号从 1, 2, 3,......, N 的数据段,若传输期间没有丢包或corrupted bits,那么无事发生。但是如果数据包 n<N 在传输过程中丢失了,那么两种回传协议就会大不同。GBN 协议下的传输层协议会把 n, n+1, n+2,......, N全部重传一遍,这会极大的增加网络拥塞。

我们前面提到,在 TCP 接收方收到数据段后并不会立即发送 ACK 数据段,而是等待500ms看看还有没有新到达的数据段,以便一同累计确认。TCP 发送方在开始序列号 n 的报文段重传前如果接收到序列号为n+1数据段的确认就会因为累计确认的关系滑动发送窗口。

TCP 也被叫做是 选择性确认(Selective ACKnowledgment) 的协议。TCP 允许接收方选择性地确认乱序数据段,而不是仅仅累计确认最后一个按顺序正确接收的数据段。TCP的错误恢复机制结合了GBN和SR协议的特点。虽然TCP使用累积确认(类似于GBN),但它也实现了选择性重传(类似于.SR),使得TCP在处理数据段丢失时更加高效。

3.5.5 Flow Control