运输层
运输层服务,多路复用与分解、UDP、可靠数据传输、TCP 和拥塞控制
概述和运输层服务
-
逻辑通信 (logic communication):
- 运输层协议为运行在不同主机的运用进程之间提供了逻辑通信功能
- 通过逻辑通信,运行不同进程的主机好像直接相连一样
-
运输层协议在端系统中实现,发送端运输层将从发送应用程序进程接收到的报文转换成运输层报文段 (segment),网路层将其封装成网络层分组(即数据报)并向目的地发送,接收端,网络层从数据报中提取运输层报文段,并将该报文段向上交给运输层
-
网络层提供了主机之间的逻辑通信,运输层为运行在不同主机上的进程之间提供了逻辑通信
因特网运输层概述
-
因特网为应用层提供了 UDP (用户数据报协议) 和 TCP (传输控制协议)
- UDP 为调用它的应用程序提供了一种不可靠、无连接的服务
- TCP 为调用它的应用程序提供了一种可靠的、面向连接的服务
-
网际协议 (Internet Protocol):网络层协议,为主机之间提供了逻辑通信
- 服务模型:尽力而为交付服务 (best-effort delivery service),不确保报文段的交付,不保证报文段的按序交付,不保证报文段中数据的完整性
- 不可靠服务 (unreliable service)
- 每台主机至少有一个网络层地址
-
TCP 和 UDP 提供的服务模型:进程到进程的数据交付和差错检查
- 将主机间交付拓展到进程间交付,运输层的多路复用 (transport-layer multiplexing) 和多路分解 (demultiplexing)
- 通过在其报文段首部中包括差错检查字段而提供完整性检查
-
TCP 提供的附加服务
- 可靠数据传输 (reliable data transfer):通过流量控制、序号、确认和定时器,确保正确地、按序地将数据从发送进程交付给接收进程
- 拥塞控制 (congestion control):力求为每个通过一条拥塞网络链路的连接平等地共享网络链路带宽
多路复用与多路分解
-
运输层没有直接将数据交付给进程,而是将数据交给了中间的套接字,每个套接字都有唯一的标识符 (端口号)
-
多路分解 (demultiplexing):将运输层报文段中的数据交付到正确的套接字
-
多路复用 (multiplexing):在源主机从不同套接字中收集数据块,并为每个数据块封装上首部信息(这将在以后用于分解)从而生成报文段,然后将报文段传递到网络层
-
运输层报文段组成
- 周知端口号 (well-known port number):0~1023,保留给周知应用层协议使用 RFC 3232 [@editorAssignedNumbersRFC2002]
- 如果编写的代码实现的是一个周知协议的服务器端,就需要分配一个相应的周知端口号,通常应用程序的客户端让运输层自动地 (并且是透明地) 分配端口号,而服务器端则分配一个特定的端口号
无连接的多路复用与多路分解
- UDP 套接字是由一个二元组全面标识的(一个目的 IP 地址和一个目的端口号)
- 如果两个 UDP 报文段有不同的源 IP 地址和 / 或源端口号,但具有相同的目的 IP 地址和目的端口号,那么这两个报文段将通过相同的目的套接字被定向到相同的目的进程
- 原端口号用作"返回地址"的一部分,在回发报文段时提取原端口号作为目的端口号
面向连接的多路复用和多路分解
-
TCP 套接字是由一个四元组(源 IP 地址,源端口号,目的 IP 地址,目的端口号)来标识的
-
两个具有不同源 IP 地址或源端口号的到达 TCP 报文段将被定向到两个不同的套接字,除非 TCP 报文段携带了初始创建连接的请求
-
对于服务器,在任意给定的时间内都可能有(具有不同标识的)许多连接套接字连接到相同的进程,服务器可以采用多种并发模型来处理多个连接,例如: ◦ 多进程模型(Forking):对每个新连接,服务器可以 fork 一个新的进程来处理该连接。 ◦ 多线程模型(Threading):服务器可以为每个新连接创建一个新的线程。 ◦ 事件驱动模型(Event-driven):服务器使用非阻塞 IO 和事件循环,通过一个单独的进程或线程来管理所有连接。 ◦ 异步 IO 模型:服务器利用操作系统提供的异步 IO 接口进行非阻塞 IO 操作,而不需要每个连接一个线程。
无连接运输:UDP
- 应用程序更适合 UDP 的原因:
- 关于发送什么数据以及何时发送的应用层控制更为精细
- 无需连接建立:QUlC 协议(快速 UDP 因特网连接) 将 UDP 作为其支撑运输协议并在 UDP 之上的应用层协议中实现可靠性
- 无连接状态
- 分组首部开销小
- 网络管理应用程序通常必须在该网络处于重压状态时运行,所以在这种场合下 UDP 要优于 TCP
- UDP 中缺乏拥塞控制能够导致 UDP 发送方和接收方之间的高丢包率,并挤垮了 TCP 会话
- 使用 UDP 的应用是可能实现可靠数据传输的 (eg. QUIC)
UDP 报文段结构
0 7 8 15 16 23 24 31
+--------+--------+--------+--------+
| Source | Destination |
| Port | Port |
+--------+--------+--------+--------+
| | |
| Length | Checksum |
+--------+--------+--------+--------+
|
| data octets ...
+---------------- ...
User Datagram Header Format
-
由 RFC 768 [@UserDatagramProtocol1980] 定义
-
长度字段指示了在 UDP 报文段中的字节数 (首部加数据)
-
接收方使用检验和来检查在该报文段中是否出现了差错
UDP 检验和 [@ComputingInternetChecksum1988]
检验和计算过程
0 7 8 15 16 23 24 31
+--------+--------+--------+--------+
| source address |
+--------+--------+--------+--------+
| destination address |
+--------+--------+--------+--------+
| zero |protocol| UDP length |
+--------+--------+--------+--------+
Pseudo Header Format
准备数据:将 UDP 头部(除了校验和字段,该字段在计算时设为0)和数据载荷分为16位的字(2字节)。如果数据的字节总数不是偶数(即不能完全分成16位的字),则在最后添加一个额外的字节的填充(通常是0)以使之成对添加伪头部:根据 UDP 协议,校验和的计算应包含一个"伪头部",它包括发送者和接收者的 IP 地址(各占4字节),一个8位的全0字段(用于占位,确保伪头部的格式与传输层数据段的其余部分保持一致),1字节的协议号(UDP 是17, 0x11),以及 UDP 数据报的长度反码求和:使用 16 位反码运算,将所有的 16 位字加到一起。在这种加法中,如果任何一次加法结果超过 16 位,即最左边产生了进位,那么进位会被加回到计算结果的最低位。这就是所谓的“环绕”或“端到端进位”求和的反码:将最终的求和结果进位的一部分和基本部分相加之后取反码,即将所有位从 0 变为 1 或从 1 变为 0,这就是要存储在校验和字段的值特殊情况:如果计算出的检验和为 0,为防止将合法的检验和值 0 解释为没有使用检验和,发送端将检验和字段为 0 的情况下置为全 1
为什么 UDP 提供检验和
- 端到端原则 [@EndtoendPrinciple2023](end-eend principle):因为某种功能(在此时为差错检测)必须基于端到端实现:"与在较高级别提供这些功能的代价相比、在较低级别上设置的功能可能是冗余的或几乎没有价值的。"
- 虽然许多链路层协议提供了差错检测,但是不能保证源和目的之间所以链路都提供
- 报文段存储在路由器的内存中,也可能引入比特差错
- 虽然 UDP 提供差错检测,但是 UDP 对差错恢复无能为力
可靠数据传输原理
- 可靠数据传输为上层实体提供的服务抽象:数据可以通过一条可靠的信道进行传输,借助于可靠信道,传输数据比特就不会受到损坏 (由 0 变为 1 或者相反) 或丢失,而且所有数据都是按照其发送顺序进行交付
- 可靠数据传输协议 (reliable data transfer protocol):将下层协议提供的不可靠信道封装成可靠信道
- 考虑因素:底层信道
- 损坏比特
- 丢失分组
- 不会对分组重排序
- 在本节中仅考虑单向数据传输 (unidirectional data transfer) 的情况,不考虑可靠的 双向数据传输 (bidirectional data transfer),但是需要注意本节实现协议需要在发送端和接收端两个方向上传输控制分组
构造可靠传输协议
- Principle of Reliable Data Transfer [@PrincipleReliableData]
- 有限状态机 [@FinitestateMachineWikipedia] (Finite-State Machine, FSM):一种计算的数学模型。作为一种抽象机器,FSM 可以在任何给定时间内处于有限数量的状态之一。FSM 可以根据一些输入从一个状态转换到另一个状态;从一个状态转换到另一个状态的变化称为转换。FSM 由其状态列表、初始状态和触发每个转换的输入定义 (PS:在宏观上看通用计算机也可以看作一个有限状态机哦,只是计算机的状态要比这里描述的状态机多得多得多,寄存器、cache、内存、外存的状态等随着指令的执行不断转换,其中存储数据的位模式不断改变)
经完全可靠信道的可靠数据传输:rdt 1.0
- 考虑底层信道是完全可靠的,既不出现比特损坏(位元错误),也不丢失分组,分组还是按序到达的
- 水平线上方为造成转移的事件,下方为事件发生时所采取的动作,若不采取任何动作则为空白
- 发送端和接收端均只有一种状态即等待上/下层的调用
- 函数作用
rdt_send(data):从上层接收数据 (如果可靠数据传输实现在 Layer4,那么这些数据就是 Layer5 注入 socket 的报文 (message)(可以是 HTTP 报文,SMTP 报文...))make_pkt(packet, data):将上层数据(根据需要拆分)加上头部建立分组 (注意⚠️:这里的分组指的是封包,在不同网络层级中的名称不同)udt_send(packet):将此分组送入下层信道rdt_rcv(packet):从下层信道接收分组extract(packet,data):从分组中取出数据deliver_data(data):将数据交给上层
┌────────────┐ ┌────────────┐
│ │ │ │
│ sender │ │ receiver │
│ │ │ │
└────────────┘ └────────────┘
layer 5 layer 4 layer 3 layer 4 layer 5
│ │
┌────────────┐ waiting for │ │
│datadatadata│ layer 5 │ │
│datadata │ invocation │ │
└────────────┘ │ │
layer 5 call rdt_send(data) │ │ waiting for
───────────────────► │ │ layer 3
packet=make_pkt(data)│ │ invocation
udt_send(packet) │ │
┌────────────┐ │ │
│headdatadata│ │ layer 3 channel delivery │
│datadatadata│ │ reliable │
└────────────┘ │ ────────────────────────► │ ┌────────────┐
│ │ │headdatadata│
│ │ │datadatadata│
│ │ └────────────┘
│ layer 3 call │ rdt_rcv(packet)
waiting for │ │ ────────────────────►
layer 5 │ │ extract(packet,data)
invocation │ │ deliver_data(data) ┌────────────┐
│ │ │datadatadata│
│ │ │datadata │
│ │ └────────────┘
│ │
│ │
│ │ waiting for
│ │ layer 3
From: │ │ invocation
Chris White │ │
▼ ▼
经具有比特差错信道的可靠数据传输:rdt 2.0
- 考虑经过底层信道后分组的比特可能受损(1 变 0,0 变 1)
- 通过肯定确认 (positive acknowledgement) 和否定确认 (negative acknowledgement),接收方让发送方知道那些内容能被正确接收,哪些内容需要重传,于是有自动重传请求 (Automatic Repeat reQuest, ARQ) 协议
- ARQ 协议基于三种机制
差错检测:让接收端能够发现比特差错,UDP 使用反码求和的方式来检验接收端反馈:接收端反馈信息给发送端,ACK 代表成功,NAK 代表失败重传:分组有差错,就重传分组
- 函数作用
make_pkt(data,checksum):计算 data 的 checksum 并且将数据封包isNAK(rcvpkt):received packet为 NAKisACK(rcvpkt):received packet 为 ACKcorrupt(rcvpkt):rcvpkt 有比特错误notcorrupt(rcvpkt):rcvpkt 没有比特错误
- 注意在 make packet 的过程中同时还有计算 checksum 的过程
- 发送端具有两种状态
- 等待来自上层的调用,当接收上层的数据后进入状态 2,
- 等待接收端传送 ACK 或 NAK ,收到 ACK 转移到状态 1 并等待上层的下一个调用,收到 NAK 重传一次分组并重复状态 2
- 接收端只有一种状态
- 根据收到的分组回传 ACK 或 NAK
- 停等 (stop-and-wait):发送方将不会发送一块新数据,除非发送方确信接收方已正确接收当前分组
- 缺陷:未考虑 ACK 或 NAK 受损的可能性
rdt 2.1
- 当发送端收到损坏的 ACK 或 NAK 分组时,就重传当前的数据分组,但是引入了一个问题,当接收方发送的 ACK 分组损坏时会收到冗余分组(duplicate packet),而接收端分不清接收到的是新分组还是重传的分组 (发送 ACK 时接收端期望得到新分组但是 ACK 损坏后接收端会收到冗余分组,rdt 2.0接收端:🤔️😅🤪,发送 NAK 时接收端期望得到重传的分组则不会出现歧义)
- 解决方法是在分组头部加入序号 (sequence number) 字段
- 函数作用:
has_seq0(rcvpkt):接收分组的序号字段为 0has_seq1(rcvpkt):接收分组的序号字段为 1
- 发送方最开始处于
等待来自上层的调用0状态,当发送方发送一个 seq=0 的分组后,进入等待 ACK 或 NAK 0状态。 - 此时在
等待来自下层的0的接收方出现两种情况,情况 1 接收方收到了无差错的分组返回一个 ACK 分组,并转换为等待来自下层的1状态,情况 2 接收方收到了有差错的分组返回一个 NAK 分组,并且保持当前状态 - 当
等待 ACK 或 NAK 0状态的发送方收到确认分组时,出现三种情况,情况 1 收到 ACK 分组,情况 2 收到 NAK 分组,情况 3 分组出现了差错,情况 1 时分组转换为等待来自上层的调用1状态,进入下半个周期,情况 2 与情况 3 时发送端重传 seq=0 的分组并保持当前状态等待确认分组 - 当接收方再次收到重传的 seq=0 分组时,接收方可能存在两种状态,第一种 2 中的报文出现了差错,仍然停留在
等待来自下层的0状态,第二种 2 中的报文已经被接收送往上层,转换为等待来自下层的1状态,第一种状态下跳转至 2,第二种状态存在两种情况,情况 1 重传的 seq=0 分组完好,此时接收方发送 ACK 分组,情况 2 重传的 seq=0 分组出现差错,此时接收方发送 NAK 分组,跳转至 3,直至发送端接收到 ACK 分组转换为等待来自上层的调用1状态
rdt 2.2
经具有比特差错的丢包信道的可靠数据传输:rdt 3.0
- 除了比特受损以外,底层信道还会出现丢包的情况
- 这里将检测和回复丢包的工作都交给发送端处理
- 发送方发送的数据分组或接收方对该分组的 ACK 发生了丢失,发送方都接收不到来自接收方的响应,如果发送方愿意等待足够长的时间以便确定分组已丢失,则它只需重传该数据分组即可
- 理想的协议应尽可能快地从丢包中回复过来,所以对等待时间的选择非常重要,如果在这段时间内没收到 ACK 则重传该分组,如果分组经历了一个特别大的时延,发送端也会重传该分组,即使数据分组和 ACK 都没丢失,于是在信道中引入了冗余数据分组 (duplicate data packet)
- 倒数定时器 (countdown timer):在一个给定时间量后,可中断发送方
- 发送方每次发送一个分组,便启动一个定时器
- 响应定时器中断
- 中止定时器
- 因为分组序号在 0 和 1 之间交替,rdt 3.0 被称为比特交替协议 (alternating-bit protocol)
- 数据传输协议的要点
- 检验和
- 序号
- 定时器
- 肯定的否定确认
流水线可靠数据传输协议
-
考虑一种情况,两个端系统之间的 RTT 为 ,通过一条发送速率 为 的信道相连,包括首部字段和数据的分组 为 ,将分组送入信道的 传输时延 为
-
信道的利用率 (utilization):发送方将分组送进信道的传输时延,与发送时间之比 () (ACK 分组太小,接收端返回 ACK 的传输时延忽略不计),还忽略了发送方和接收方的底层协议处理时间,和可能出现的路由器处理和排队实验
-
使用流水线技术 (pipelining):
- 增加序号范围
- 发送方和接收方缓存多个分组,发送方最低限度应该缓存发送但未被确认的分组
- 差错恢复的两种基本方法:回退 N 步 (Go-Back-N, GBN)和选择重传 (Selective Repeat, SR)
回退 N 步
-
基序号 (base):最早未确认的分组
-
下一个序号 (nextseqnum):下一个待发分组的序号
-
N 常被称为窗口长度 (window size),GBN 协议也常被称为滑动窗口协议 (sliding-window protocol)
-
分组的序号承载在分组首部的定长字段中,如果分组序号字段的比特数是
k,则该序号的范围是 ,TCP 有一个 32bit 的序号字段,TCP 序号是按字节流中的字节进行计数的,而不是按分组计数 -
GBN 发送方需要相应三种类型的事件:
- 上层调用:当收到来自上层的调用时需要检查窗口是否已满,如果未满则发送一个分组,如果已满则将数据返回给上层让上层一会再试,实际情况中发送方可能缓存这个数据,或者使用同步机制允许上层在窗口不满时才调用
- 收到一个 ACK :更新 base,如果有已发送但未被确认的分组,则定时器被重新启动,如果没有已发送但未被确认的分组,则停止定时器
- 超时事件:如果出现超时,发送方重传所有已发送但未被确认过的分组
-
接收方动作:
- 如果序号为 n 的分组按序到达,则为分组 n 发送一个 ACK n,如果到达的分组不是 n (失序),则丢弃该分组等待发送端重传
-
基于事件的编程 (event-based programming) 方式:在协议栈中对拓展 FSM 的实现以各种过程形式出现,每个过程实现了在响应可能出现的事件时要采取的动作,可能出现的事件
- 上层实体调用
- 定时器中断
- 下层实体调用
选择重传
-
GBN 本身存在着一些性能问题,当窗口长度和带宽时延积都很大时,流水线中有很多分组,单个分组的差错就能引起 GBN 重传大量分组
-
选择重传 (SR) 协议通过让发送方仅重传怀疑在接收方出错的分组避免了不必要的重传
-
SR 发送方的事件与动作:
从上层收到数据:若 ,则将数据打包并发送,否则将数据缓存或返回给上层超时:每个分组必须拥有自己的逻辑定时器,超时发生后只能发送一个分组收到ACK:若分组序号在窗口内,则 SR 发送方将被确认的分组标记为已接收,若分组的序号等于 send_base 则将窗口基序号向前移动到最小的未确认分组处,如果窗口已到了有序号落在窗口内的未发送分组,则发送这些分组
-
SR 接收方的事件与动作:
序号在[rcv_base, rcv_base + N - 1]内的分组被正确接收:回传选择 ACK (与当前收到分组 seq 对应) ,在接收端如果该分组以前没收到过,且序号大于接收窗口的基序号 (图中 rcv_base + 123的黑框框) 时,这些分组是失序分组,由接收端缓存,如果序号等于接收窗口的基序号 (rcv_base) 时,则将该分组及以前缓存的序号连续失序分组 (图中的 rcv_base + 1, rcv_base + 2, rcv_base + 3 的黑框框)交付给上层,接收窗口向前移动到下一个未交付的分组处 (图中 rcv_base + 4)序号在[rcv_base - N, rcv_base - 1]内的分组被正确接收:必须产生一个 ACK (与分组 seq 对应),此分组出现的原因是接收端已经收到分组移动窗口,但返回的 ACK 损坏,发送端重传该分组,为了让发送端接收到 ACK 确认并让发送端窗口向前移动,所以要产生 ACK其他情况:若出现分组损坏等情况则忽略该分组等待发送端重传
-
发送方和接收方件的窗口长度必须小于或等于序号空间大小的一半否则会引起歧义,接收方不知道接收到的下个分组是重传的还是发送方窗口滑动后发送的新分组
-
先前假定分组在发送方和接收方的信道中不能被重新排序,但是实际上重新排序有可能发生,以前被发向信道但被认为丢失了的分组可能会突然出现,为了确保序号不被重新使用,发送方在发送序号为 x 的分组时需要确信任何先前发送过的序号为 x 的分组都不在网络中,通过假定分组在网络中的存活时间不会超过某个固定最大时间量来做到这一点
面向连接的运输:TCP
-
TCP 是面向连接的 (connection-oriented),在一个应用进程想另一个进程发送数据前,两个进程必须相互握手
-
TCP 连接是点对点的 (point-to-point)
-
三次握手 (three-way handshake)
-
发送缓存 (send buffer):在三次握手期间设置的缓存,用来存储通过套接字的数据,TCP 从缓存中取出数据发送到网络层
-
单个 TCP 报文段的数据数量受限于最大报文段长度 (Maximum Segment Size, MSS),MSS 根据本地主机发送的最大链路层帧长度(最大传输单元(Maximum Transmission Unit, MTU),不同的链路层协议会规定不同的 MTU),如果运输层报文段超过了 MTU - IP 头部长度,就会被 IP 协议分片
6. MSS 的典型值为 1460 字节,Ethernet 和 PPP Protocol 都有 1500 的标准 MTU,MSS 是指 Segment 中应用层 data 的最大长度,而不是包括首部的 TCP segment 的最大长度,很容易混淆⚠️ (如果又感到困惑了,看看这里) 7. TCP 为每块客户数据配上首部,形成多个 TCP 报文段 (TCP segment) 8. TCP 连接的组成: 1. 客户端主机上的缓存、变量和与进程连接的套接字 2. 服务端主机上的缓存、变量和与进程连接的套接字
TCP 报文段结构
源端口号和目的端口号(各 16 bits):用于多路复用 / 分解来自上层应用的数据序号字段(sequence number field) 和确认号字段(acknowledgement number field)(各 32 bits):被 TCP 发送方和接收方用来实现可靠数据传输服务首部长度字段(header length field)(4 bits):指示了以 32 bits 的字为单位的 TCP 首部长度 (最右边的括号里的 8)标志字段(flag field) (最初定义 6 bits,现在存在 3 bits 的保留位与 9 bits 的控制位):Accurate ECN:指示经过的路由器正在经历拥塞,被路由器置位CWR(Congestion Window Reduced):发送方接收到了设置了 ECE 标志的 TCP 包,并已降低拥塞窗口的大小ECE(ECN-Echo):这个标志在两种情况下会被设置。一种是作为对于收到设置了 CE(拥塞经历)标志的 IP 包的响应。另一种是在 TCP 三次握手中,用来指示 ECN(Explicit Congestion Notification,显式拥塞通告)的可用性URG(Urgent):表示紧急指针字段有效ACK(Acknowledgement):表示确认字段有效PSH(Push):告诉接收方应该立即将接收到的数据传递给上层应用,而不是等待缓冲RST(Reset):用于重置一个错误的连接,或者拒绝非法的段或启动关闭连接SYN(Synchronize):在建立连接时使用,用于初始化序列号字段FIN(Finish):发送方完成发送任务,希望关闭连接
紧急数据指针字段(urgent data pointer field):紧急指针字段的值表示从当前序列号开始,紧急数据的序号数
序号和字节流
-
TCP 序号建立在传送的字节流之上,而不是建立在传送的报文段序列之上
-
一个报文段的序号 (sequence number for a segment) 是该报文段首字节的字节流编号,TCP 根据 MSS 将文件数据划分为 TCP 报文段,并将每个报文段对应的首字节的编号被填入相应报文段首部的序号字段中
-
TCP 是全双工的,如果主机 A 在向主机 B 发送数据的同时,主机 B 也在向主机 A 发送数据,那么主机 A 填充进报文段的确认号是主机 A 期望从主机 B 收到的下一字节的序号
-
如果主机 A 已经收到主机 B 包含字节 0~535 的报文段和包含字节 900~1000 的报文段,但主机 A 还没有收到字节 536 ~ 899 的报文段,那么 A 到 B 的下个报文段将在确认号中包含 536,而不会发送确认号位 1001 的报文段,TCP 只确认该流中至第一个丢失字节为止的报文段,收到的失序报文段不发送对应的确认报文段,而对失序报文段的处理方式由编程人员决定,所以 TCP 提供 累积确认 (cumulative acknowledgement)(注意与 回退N步 和 选择重传 确认机制区分,回退 N 步中接收端直接将收到的分组(先前对于 rdt 的讨论不是基于特定 Layer 而言的,所以用 packet 来指代)丢弃,所以不存在累积确认的说法,而选择重传中是接收端收到分组,无论是不是失序分组,均回传与收到分组对应的 ACK,所以发送端能够知道失序的分组已经被接收了,在累积确认机制中,TCP 发送端(主机 B)是不知道字节 900~1000 的报文段(失序)已经被接收了,只能通过下个 A 到 B 的报文段中的确认号 536 知道到丢失字节(536)之前的所有报文段被正确接收了)
5. TCP 编程人员对失序报文段有两种处理方式: 1. 接收方立即丢弃失序报文段(简化接收当设计) 2. 接收方保留失序的报文段,并等待缺失的报文段填补间隔(有可能在传送的过程中丢失或损坏由发送端重传) 6. 图中初始序号为 0,实际上序号 TCP 连接的双方可以随机选择序号,可以减少将仍在网络中存在的来自两台主机之间先前已中止的连接的报文段,误认为新连接的有效报文段的可能性 (需要恰巧新连接和旧连接都在发送方和接收方的同一对端口上)
Telnet
- 客户端的用户键入的每个字符都会被发送到远程主机,远程主机会送每个字符的副本给客户,并将这些字符显示在 telnet 用户的屏幕上
- 回显 (echo back) 用于确保由 Telnet 用户发送的字符已被远程主机收到并在远程站点上得到处理
- 不妨试着用 Python 运行一个简单的 Telnet 服务端 (Generated by ChatGPT)
import socket
import threading
def client_thread(conn, addr):
print(f"Connected to {addr}")
conn.send(b"Welcome to the Telnet server!\n")
while True:
try:
data = conn.recv(1024)
if not data:
break
conn.sendall(b"Echo: " + data)
except ConnectionResetError:
break
print(f"Connection with {addr} closed")
conn.close()
def start_telnet_server(host, port):
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind((host, port))
server_socket.listen(5)
print(f"Telnet server started on {host}:{port}")
try:
while True:
conn, addr = server_socket.accept()
threading.Thread(target=client_thread, args=(conn, addr)).start()
except KeyboardInterrupt:
print("Shutting down the server...")
finally:
server_socket.close()
if __name__ == "__main__":
HOST = "127.0.0.1" # 绑定到所有网络接口
PORT = 23 # 标准Telnet端口号是23,但如果没有管理员权限,您可能需要选择1024以上的端口号
start_telnet_server(HOST, PORT)
-
当程序运行时,服务端套接字监听 localhost 上 23 端口,当 socket 没有连接被建立时,
accept()方法是阻塞的,直到有新连接建立时accept()方法返回一个(conn, addr)对,conn 是一个新的套接字对象,用于在此连接上收发数据,address 是连接另一端的套接字所绑定的地址 (IP 和端口号),随后程序创建并启动新的线程调用client_thread()方法来处理这个连接,连接开始时服务端打印了客户端的地址,并调用 socket 对象发送欢迎文本,接着该子线程进入循环,recv的参数 bufsize 指定 buffer 的大小 (bufsize参数指定了单次调用recv方法时最多可以读取的字节数,这个大小是由程序员根据具体的网络应用程序而定的,它可以是任意非零的整数,告诉recv方法一次性从内部缓冲区中读取多少字节的数据),当 TCP 缓冲中没有数据可用时,recv()会阻塞,如果 buffer 中有可用数据,但小于 bufsize,recv()会返回实际可用的数据量,如果到达的数据量大于 bufsize,数据会存放在操作系统的缓冲区中,recv()一次读取 bufsize 大小的数据,将这些数据加上Echo:字节串头部后使用 socket 对象发送回客户端 -
可以通过
lsof命令查看 TCP 连接是否被建立,图中在回环地址的 56971 端口到 23 端口建立了一条 TCP 连接
-
在 telnet 中执行一系列操作(telnet 开启了 character 模式),通过 wireshark 可以看到执行这些输入时对应的网络活动 ( loopback 接口的好处是不容易 capture 到操作系统其他网络活动的奇怪分组)
捎带 (piggybacked):需要注意的是,在上图的情况中对于携带数据的报文段的确认 (ACK) 均是是一个单独的不携带数据的 ACK 报文段,在实际情况下,对携带数据报文的确认有可能被装载在返回的承载数据的报文段中,这种确认称为是被捎带在数据报文段中
往返时间的估计与超时
- TCP 中实现超时 / 重传机制,最明显的问题时超时间隔长度的设置
估计往返时间
- 样本 RTT (SampleRTT):某报文段被发出 (交给 IP) 到对该报文段的确认被收到之间的时间量,通常 TCP 遵循以下规则来选择测量 SampleRTT 的报文段
单个报文确认原则:为了避免对 RTT 估计引入额外的变化,通常不对重传的报文段进行 SampleRTT 的测量。换句话说,TCP 只测量对于那些被首次传输的报文段的确认,而不是对重传的报文段的确认Karn/Partridge 算法:如果一个报文必须重传,那么它的 SampleRTT 就不会被采用,因为这个 RTT 可能包含了重传的延迟,这不利于准确计算网络的真实 RTT避免并行测量:TCP 通常避免在同一时间测量多个报文段的 SampleRTT,通常是在任何给定的时间只对一个在传输路径上的数据段测量 SampleRTT。这样做是为了防止报文段的确认回应由于网络路径上的排队延迟而彼此影响,进而影响 RTT 的准确性样本选择:在某些实现中,TCP 可能会选择性地估算 RTT,例如只在特定间隔或者对特定的报文段测量 SampleRTT
- 为了估计一个典型的 RTT,需要采取对间隔测量到的 SampleRTT 取平均的方法,TCP 维护一个 SampleRTT 均值 (EstimatedRTT),当获得一个新的 SampleRTT 时,TCP 会根据指数加权移动平均 (Exponential Weight Moving Average) 来更新 EstimatedRTT, 的推荐值通常是 0.125
- EstimatedRTT 的新值是由以前的 EstimatedRTT 值与 SampleRTT 新值加权组合而成,这个加权平均对最近的样本赋予的权值要大于对旧样本赋予的权值,因为越近的样本越能更好地反映网络的当前拥塞状况,一个给定的 SampleRTT 的权值在更新的过程中呈指数型快速衰减
RFC 6298定义了 RTT 偏差,用于估算 SampleRTT 会偏离 EstimatedRTT 的程度, 的推荐值通常为 0.25
设置和管理重传超时间隔
- 超时间隔应大于等于 EstimatedRTT ,否则将造成不必要的重传,超时间隔不应该比 EstimatedRTT 大太多,否则当报文段丢失时,TCP 不能很快重传该报文段,因此要求将超时间隔设为 EstimatedRTT 加上一定余量。当 SampleRTT 值波动较大时,这个余量应该大些;当波动较小时,这个余量应该小些
- 推荐的初始 Timeoutlnterval 值为 1 秒 RFC 6298 [@sargentComputingTCPRetransmission2011]。同时,当出现超时后,Timeoutlnterval 值将加倍,以免即将被确认的后继报文段过早出现超时。然而,只要收到报文段并更新 EstimatedRTT , 就使用上述公式再次计算 Timeoutlnterval
- TCP 通过使用肯定确认和定时器来实现可靠数据传输,当 TCP 认为报文段或其确认丢失或受损时,TCP 会重传这些报文段,有一些 TCP 还有一个隐式 NAK 机制 (
快速重传(Fast Retransmit): 当接收方收到一个失序的数据包时,它会立即发送一个重复的 ACK(也就是隐式 NAK),这是当前已正确接收的最高序列号的数据包。如果发送方收到三个或者更多连续的重复 ACK,它将推测在传输中出现了数据包丢失,并且会在定时器到期之前立即重传那些丢失的数据包) - TCP 无法分辨报文段或其 ACK 是丢失了、受损了还是时延过长了,但 TCP 对这些情况的处理方式是相同的,重传出现问题的报文段
- TCP 也使用流水线,当报文长度和往返时延只比很小时,流水线能显著增加一个会话的吞吐量,一个发送方能够具有的未被确认报文段的具体数量是由 TCP 的流量控制和拥塞控制机制决定的
可靠数据传输
- IP 的尽力而为服务:不保证数据报的交付 (丢包),不保证数据报的按序交付服务 (乱序),不保证数据报中数据的完整性 (损坏)
- TCP 在 IP 不可靠的尽力而为服务至上创建了一种可靠数据传输服务 (reliable data transfer service),确保一个进程从其接收缓存中读出的数据流是无损坏、无间隙、非冗余和按序的数据流
- 在研究选择重传机制时,曾假定每一个已发送但未被确认的报文都与一个定时器相关联 逻辑定时器,但是定时器的管理需要相当大的开销,RFC 6298 [@sargentComputingTCPRetransmission2011] 推荐的定时器管理过程仅使用单一的重传定时器,即使有多个已发送但还未被确认的报文段
TCP 可靠数据传输简化描述:假设发送方不受 TCP 流量和拥塞控制的限制的,来自上层数据的长度小于 MSS,且数据传送只在一个方向进行
NextSeqNum = InitialSeqNumber
SendBase = InitialSeqNumber
while (1) {
switch (event) {
case 1: //从上层接收到数据e
sndpkt[NextSeqNum] = make_pkt(NextSeqNum, data, checksum);
if (isTimerInactive) //定时器当前没有运行
timer_start();
udt_send(sndpkt[NextSeqNum]);
NextSeqNum = NectSeqNum + length(data);
break;
case 2: //定时器超时
udt_snd(sndpkt[SendBase]); //重传具有最小序号但仍未应答的报文段
timer_start();
break;
case 3: //收到ACK,Ack = y
if (y > SendBase) {
SendBase = y; //累积确认
if (NextSeqNum > SendBase) //当前有未被确认的报文段
timer_start();
}
break;
}
/*没有事件到来时循环阻塞*/
}
- TCP 采用累积确认,所以代码中的 y 确认了字节编号在 y 之前的所有字节都已经收到
有趣的情况
-
当 TCP 的一个分组丢失或遭受延迟时,如果超时事件发生,TCP 发送端会重传该分组,但是如果是两个连续的分组,超时事件发生时,发送端会采取什么样的行动🤔️
-
如图 2,当主机 A 连续发送了两个报文段,假设两个报文段在超时前都没有到达主机,当超时事件发生时,主机 A 重传序号 92 的第一个报文段,并重启定时器,只要第二个报文段 ACK 在新的超时发生以前到达,则第二个报文段将不会被重传
-
还有一种情况,主机 A 像 2 中一样发送两个报文段但第一个报文段的 ACK 报文总在网络中丢失或者遭受延迟,而第二个报文在超时事件发生前正确到达了,根据累积确认机制,主机 A 知道主机 B 已经收到了序号为 119 及之前的所有字节,所以主机 A 不会重传这两个报文段中的任何一个
超时间隔加倍
- 考虑在大多数 TCP 实现中所做的一些改变,首先关注定时器 timeout 时超时间隔的长度,在这种修改中,当超时事件发生时,TCP 重传具有最小序号的未确认的报文段,但是每次 TCP 重传都会将下一次的超时间隔设为先前值的两倍,而不是用从 EstimatedRTT 和 DevRTT 推算出的值
- 这种方法提供了一种形式受限的拥塞控制,在拥塞时,如果源持续重传分组会使拥塞更严重,所以 TCP 采用更优雅的方式
快速重传
-
发送方通常可在超时事件发生之前通过冗余 ACK 来较好地检测到丢包的情况
-
冗余 ACK 是再次确认某个报文段的 ACK ,而发送方先前已经收到对该报文段的确认
-
当接收方期望收到一个序列号为 N 的报文段,但是却收到了一个序列号大于 N 的报文段时,它会意识到一些数据丢失了(可能是因为网络中的某些报文段丢失或到达顺序错乱)。此时,为了通知发送方哪些报文段已成功接收,并请求重传丢失的报文段,接收方会发送一个确认最后成功接收报文段的冗余 ACK
-
发送方经常一个接一个地发送大量的报文段,如果一个报文段丢失,就很可能引起许多一个接一个的冗余 ACK 。如果 TCP 发送方接收到对相同数据的 3 个冗余 ACK,,它把这当作一种指示,说明跟在这个已被确认过 3 次的报文段之后的报文段已经丢失,一旦收到 3 个冗余 ACK ,TCP 就执行
快速重传(fast retransmit),即在该报文段的定时器过期前重传丢失的报文段
if (y > SendBase) {
SendBase = y;
DupAck = 0;
if (NextSeqNum > SendBase)
timer_start();
} else { /*当收到对已经确认报文段的一个冗余ACK,此时 y = SendBase*/
DupAck++;
if (DupAck == 3) { //TCP快速重传
udt_snd(sndpkt[y]) //重新发送具有序号 y 的报文段
}
}
break;
TCP 是回退 N 步还是选择重传
- TCP 协议更像 GBN 协议和 SR 协议的混合体
- GBN 协议特征
- 累积式的,正确接收但失序的报文不会被接收方逐个确认
- TCP 发送方维护
已发送但未被确认的字节的最小序号(SendBase)和下一个要发送的字节的序号(NextSeqNum)
- SR 协议特征
- 当发送方发送一组报文段,其中的一个报文段丢失时,TCP 至多重传一个类型的报文段 (这个报文段,若重传还丢失,那么仍旧重传该报文段),GBN 则会重传所有的该报文段后的报文段
- 选择确认 (selective acknowledgement) RFC 2018 [@floydTCPSelectiveAcknowledgment1996] ,允许 TCP 接收方有选择地确认失序报文段,选择确认机制还能和选择重传机制结合起来使用,跳过重传那些被接收方选择性确认过的报文段
流量控制
- 当数据进入接收方主机的缓存时,接收方应用可能正忙于其他任务,并不能立刻读取数据,此时如果发送方发送的数据太多、太快,到达接收方的数据很容易让接收缓存溢出
- 流量控制服务 (flow-control service) 可以消除发送方使接收方缓存溢出的可能性
- TCP 防止接收方缓存溢出采用的策略为流量控制,因为 IP 网络的拥塞而被遏制的机制为 拥塞控制 (congestion control),请注意区分
- TCP 通过让发送方维护一个称为 接收窗口 (receive window) 的变量来提供流量控制 (在上方的 Telnet 服务端案例中有出现)
- 假设主机 A 通过一条 TCP 连接向主机 B 发送一个大文件,主机 B 为连接分配了一个大小为
RcvBuffer的接收缓存,主机 B 上的应用进程不时从该缓存中读取数据,定义LastByteRead为主机 B 的应用进程从缓存读出的数据流的最后一个字节的编号,LastByteRcvd为从网络中到达的并且已放入主机 B 接收缓存中的数据流的最后一个字节的编号 - TCP 不允许已分配的缓存溢出
- 接收窗口用
rwnd表示 - 主机 B 通过把
rwnd值放入报文段接收窗口字段中,通知主机 A 在该连接的缓存中还有多少可用空间 - 主机 A 需要将未确认的数据量控制在
rwnd内,即 - ⚠️当主机 B 的接收窗口为 0 时,主机 A 继续发送只有一个字节数据的报文段,这些报文段将会被接收方确认,最终缓存开始清空,并在确认报文里将包含一个非 0 的 rwnd 值
- UDP 不提供流量控制,报文段由于缓存溢出可能在接收方丢失
TCP 连接管理
-
TCP 连接的建立会显著地增加人们感受到的时延
-
三次握手 (three-way handshake) 过程:
- 客户端向服务器端发送 TCP SYN 报文段,并随机选择该 SYN 报文段的初始序号,并封装为 IP 数据报发送给服务器
- 服务器从 IP 数据报中提取 TCP SYN 报文段,为该 TCP 分配 TCP 缓存和变量,并向该客户 TCP 发送 SYN ACK 报文段,并随机选择该报文段的初始序号
- 收到 SYN ACK 报文段后,客户端给连接分配缓存和变量,然后发送一个 ACK 报文段,确认号设置为收到的序号加1,同时可以在该报文段中携带客户到服务器的数据
-
四次挥手 (four-way handshake) 过程:
- 客户端向服务器端发送 FIN 报文段
- 服务器收到该报文后,向发送方回送一个 ACK 报文段
- 服务器发送自己的 FIN 报文段
- 最后,客户对服务器的中止报文段进行确认
-
在一个 TCP 连接的生命周期中,运行在每台主机中的 TCP 协议在各种 TCP 状态 (TCP state) 之间变迁
- 除了客户应用程序可以发送 FIN 报文段关闭该连接外,服务器也可以选择关闭该连接
- TCP
RST标志代表一个异常的终止,通常有以下几种情况:主动拒绝连接建立:如果接收方收到 SYN 报文段,但在对应端口上没有运行服务,不希望建立连接,则可以发送 RST 报文段来拒绝连接,告诉该源"我没有那个报文段的套接字,请不要再发送该报文段了",如果接收的 UDP 分组目的端口与 UDP 套接字不匹配,主机会发送一个特殊的 ICMP 报文致命的协议错误:如果一个端点检测到对方违反了协议的规定时,发送 RST 报文段来关闭连接未经期望的数据到达:如果一个 TCP 端点接收到不存在的连接到达的数据,可以发送 RST 报文段回复
SYN 洪泛攻击
- 经典的 Dos 攻击,攻击者发送大量 TCP SYN 报文段,但不完成第三次握手过程,服务器不断为半开连接分配资源,直至连接资源被消耗殆尽
- 可以使用 SYN cookie 来记忆对应的发送端 IP 地址和端口号
拥塞控制原理
- 分组重传时网络拥塞的征兆,为了处理网络拥塞的原因,需要一些机制以在面临网络拥塞时遏制发送方
拥塞原因与代价
-
供给载荷 (offered load):运输层向网络中发送含有初始数据或重传数据的报文段的速率
-
考虑以下具有四个发送方和有限缓存的多台路由器及多条路径的情况
-
假设每台主机都使用超时 / 重传机制来实现可靠数据传输,不执行流量控制或拥塞控制,忽略由于添加运输层和较低层首部信息产生的额外开销,所有的主机都具有相同的 值,所有路由器的链路容量都是 字节 / 秒,到达主机应用层的接收速率为
-
考虑 A - C 连接,A - C 与 B - D 连接共享路由器 R2,对于较小的 值,路由器几乎不出现缓存溢出的情况,所以 的增大会导致 的增大
-
当 很大时,经过 R1 到达 R2 的 A - C 流量速率至多是 R ,当来自 B - D 连接的供给载荷越来越大时,经过 R1 转发的 A - C 流量相比之下就更小,由于两条连接的流量需要为 R2 上的有限缓存空间竞争,所以当供给载荷趋近于∞时,R2 的缓存空间中几乎被 B - D 连接的流量占满,因此 A - C 连接的流量在 R2 上的吞吐量趋近于 0
-
由于拥塞而丢弃分组的另一种代价:当一个分组沿一条路径被丢弃时,每个上游路由器用于转发该分组到丢弃该分组而使用的传输容量最终被浪费掉了,所以当选择一个分组时,路由器最好优先考虑那些已经经历过一定数量的上游路由器的分组
-
拥塞控制方法
- 根据网络层是否为运输层拥塞控制提供显示帮助区分:
端到端拥塞控制:网络层没有为运输层拥塞控制提供显式支持,网络层对端系统来说是一个黑箱,需要通过对 IO 的观察 (分组丢失与时延) 来推断,TCP 采取端到端的拥塞控制方法网络辅助的拥塞控制:在 ATM 可用比特率 (Available Bite Rate, ABR) 拥塞控制中,路由器显式地通知发送方能在输出链路上支持的最大主机发送速率- 直接网络反馈, 采用路由器发送阻塞分组 (choke packet) 的形式
- 经由接收方的网络反馈,路由器标记或更新从发送方流向接收方的分组中的某个字段来指示拥塞的产生,至少需要 1 RTT
TCP 拥塞控制
-
TCP 发送方限制向其连接发送流量的方式
- 跟踪拥塞窗口 (congestion window, cwnd),需要满足
- 上述约束限制了送方中未被确认的数据量,因此间接地限制了发送方的发送速率,发送方的发送速率大概是 字节 / 秒
-
TCP 发送方感知路径上出现拥塞的方式
- 发送方定时器超时
- 发送方接收到 3 个冗余 ACK
- TCP 将确认的到达作为正常的指示,并根据确认到达的速率来调整增大拥塞窗口长度的迅速程度 (二阶导),所以 TCP 是自计时 (self-clocking) 的
-
TCP 确定发送速率的方式:发送太快,会导致拥塞崩溃。发送太慢,就不能充分利用网络的带宽
- 丢包意味着拥塞,当遇到丢失报文段时应当降低 TCP 发送方的速率
- 确认报文段指示正在交付,当确认报文段到达时应当增加发送方的速率
- 带宽探测
-
TCP 拥塞控制算法 (TCP congestion control algorithm):加性增、乘性减 (Additive-Increase, Multiplicative-Decrease, AIMD) 拥塞控制方式
慢启动 (slow-start)- TCP 连接开始时,cwnd 常置为一个 MSS 的较小值,每当传输的报文段被确认 cwnd 就增加一倍
- 如果发送方检测到拥塞 (超时) 时,将第二个状态变量的值 ssthresh 设置为 cwnd / 2,将 cwnd 设置为 1
- 如果存在 ssthresh,当 cwnd 的值等于 ssthresh 时,结束慢启动并且将 TCP 转为拥塞避免模式
- 如果检测到三个冗余 ACK,TCP 执行快速重传并进入快速恢复状态
拥塞避免- 每个 RTT 指将 cwnd 的值增加一个 MSS ,等同于 ACK 每次将 cwnd 的值增加 (MSS / cwnd) x MSS 字节
- 当超时丢包事件出现时,ssthresh 的值被更新为 cwnd 值的一半
- 当收到三个冗余 ACK 时,TCP 将 ssthresh 的值记录为 cwnd 值的一半,然后进入快速恢复状态
快速恢复- 对于进入快速恢复状态的缺失报文段,对每个收到的冗余 ACK ,cwnd 值增加一个 MSS
- 最终当对丢失报文段的 ACK 到达时, TCP 在降低 cwnd 后进入拥塞避免状态
- 出现超时,将 ssthresh 的值被更新为 cwnd 值的一半, cwnd 设置为 1,进入慢启动状态
-
TCP 连接的平均吞吐量
6. 经高带宽路径的 TCP
公平性
- 假设仅有 TCP 穿过瓶颈链路,所有的连接具有相同的 RTT 值,对于一个主机 - 目的地而言只有一条 TCP 连接与之相关联,TCP 趋于在多条连接之间平等地共享带宽
- 影响 TCP 拥塞控制公平性的因素:
- RTT:多条连接共享一个共同的瓶颈链路时具有较小 RTT 的连接能够在链路空闲时更快地抢占可用带宽,将比具有较大 RTT 的连接享用更高的吞吐量
- UDP:UDP 没有内置的拥塞控制,有可能压制 TCP 流量
- 并行 TCP 连接:Web 浏览器通常使用多个并行 TCP 连接来传送一个 Web 页中的多个对象,多条并行连接在拥塞链路中通常会抢占更多的带宽
网络辅助拥塞控制
- 明确拥塞通告 (Explicit Congestion Notification, ECN):网络明确向发送方和接收方发出拥塞信号
- 用于网络辅助拥塞控制的标志位:Accurate ECN ECE
- RFC 3168 [@floydAdditionExplicitCongestion2001] 推荐仅当拥塞持续不断存在时才设置 ECN 比特
- 对于具有 ECE 拥塞指示的 ACK ,TCP 发送方将拥塞窗口减半作为回应,并在下个发送方到接收方的报文段中对 CWR (拥塞窗口缩减) 置位
- 其他能利用网络层发送 ECN 信号的协议有数据报拥塞控制协议 (Datagram Congestion Control Protocol, DCCP),DCTCP (数据中心 TCP)