diff --git a/Computer-Network/01-introduction.typ b/Computer-Network/01-introduction.typ index 19a3fb3..5fa43b5 100644 --- a/Computer-Network/01-introduction.typ +++ b/Computer-Network/01-introduction.typ @@ -229,7 +229,7 @@ $ "总时延" = "发送时延" + "传播时延" + "处理时延" + "排队时延 rect(width: 120pt, height: 25pt, fill: rgb("#e8f5e9"))[5.应用层], rect(width: 120pt, height: 25pt, fill: rgb("#d4edda"))[4.运输层], rect(width: 120pt, height: 25pt, fill: rgb("#c3e6e5"))[3.网络层], - rect(width: 120pt, height: 25pt, fill: rgb("#b2dfe8"))[2.数据链路层], + rect(width: 120pt, height: 25pt, fill: rgb("#b2dfe8"))[2.链路层], rect(width: 120pt, height: 25pt, fill: rgb("#a1d4eb"))[1.物理层] ) ) diff --git a/Computer-Network/05-transport.typ b/Computer-Network/05-transport.typ new file mode 100644 index 0000000..d7ce433 --- /dev/null +++ b/Computer-Network/05-transport.typ @@ -0,0 +1,477 @@ +#import "../template/components.typ": card +#import "@preview/cetz:0.3.4": canvas, draw + += 运输层 + +== 概述 + +我们从实际应用引入:就在NH5写下这段话的时候,电脑就在运行着很多程序: + - VSCode在写这篇笔记 + - Apple Music在放音乐 + - Chrome端bilibili在后台,时不时切出来看两个视频 + - clash开着方便上外网 + - ... + +相应的,远端的服务器也是运行很多进程,比如NH5的AM客户端在与Apple的相关的服务器的进程交流. +所以严格地讲,两台主机进行通信就是两台主机中的应用进程互相通信.但是,对于网络层而言,同一主机上不同进程是没有区别,它们的IP一样,实际上应当有区别. + +=== 端口 +这样,我们对运输层的功能要求就可以明确其中一点了: + - 复用和分用:应用层所有的应用进程都可以通过运输层再传送到网络层,这就是*复用*;运输层从IP层收到发送给各应用进程的数据后,必须分别交付指明的各应用进程,这就是*分用*. +这样就需要一个标识来区分不同进程:*端口(port)*. + +端口有很多类: + - 服务器端的端口 + - 熟知端口号/全球通用端口号:0~1023.例如http-80,https-443 + - 等级端口号:1024~49151.为没有熟知端口号的应用准备 + - 客户端的端口:49152~65535.仅在客户进程运行时才使用,又叫短暂端口号 + +=== 两个主要协议 + +运输层主要有两个协议在使用: + - 传输控制协议TCP(Transmission Control Protocol) + - 用户数据报协议UDP(User Datagram Protocol) + +后文详细介绍这两种协议.下面是常用应用对应的主要使用传输层协议. +#figure( + table( + columns:(auto, auto, auto), + + // --- 表头 --- + [*应用*], [*应用层协议*], [*运输层协议*], + + // --- 表格内容 --- + [名字转换], [DNS (域名系统)], [UDP], + [文件传送], [TFTP (简单文件传送协议)], [UDP], + [路由选择协议], [RIP (路由信息协议)], [UDP], + [IP 地址配置], [DHCP (动态主机配置协议)], [UDP], + [网络管理], [SNMP (简单网络管理协议)], [UDP], + [远程文件服务器], [NFS (网络文件系统)], [UDP], + [IP 电话], [专用协议], [UDP], + [流式多媒体通信], [专用协议], [UDP], + [多播], [IGMP (网际组管理协议)], [UDP], + [电子邮件], [SMTP (简单邮件传送协议)], [TCP], + [远程终端接入], [TELNET (远程终端协议)], [TCP], + [万维网], [HTTP (超文本传送协议)], [TCP], + [文件传送], [FTP (文件传送协议)], [TCP], + ) +) + +== UDP + +UDP有以下特点: + - 无连接的 + - best effort + - 面向报文:发送端应用层交给UDP多长的报文,UDP就照样发送,一次发送一个报文;在接收方的UDP,对网络层交上来的UDP用户数据报,在去除首部后就原封不动地交付上层的应用进程 + - 没有拥塞控制 + - 支持一对一,一对多,多对一和多对多的交互通信 + - 首部开销小,只有8个字节 + +=== 首部格式 + +UDP首部包含4个部分:源端口,目的端口,长度,检验和,均为2字节. + +如果接收方UDP发现收到的报文中的目的端口号不正确(即不存在对应于该端口号的应用进程),就丢弃该报文. +由网际控制报文协议ICMP发送"端口不可达"差错报文给发送方. + +在计算检验和时,要在UDP用户数据报之前增加12个字节的伪首部: + - 源IP:4字节 + - 目的IP:4字节 + - 0:1字节 + - 17:1字节 + - UDP长度:2字节 + +UDP的检验和是把首部和数据部分一起都检验 + +== TCP + +IP在网络层为我们提供了在全球范围内发送数据包的能力.然而,IP层所面临的根本设计约束是提供一种"尽力而为"(best-effort)的服务,这意味着它不提供任何可靠性保证.数据包在传输过程中可能会丢失、损坏、重排、延迟甚至被复制.对于绝大多数应用程序而言,直接在这种不确定的基础上构建服务是一项极其复杂且容易出错的任务. + +传输控制协议(TCP)在不可靠的IP网络之上,为应用程序构建一个可靠的、*面向连接的字节流*(bytestream)抽象.它通过一系列精巧的机制,将网络底层的混乱与复杂性完全屏蔽. + +=== 核心特性 + +为了实现其目标,TCP的设计围绕三个核心特性展开,这些特性共同为应用程序提供了强大而便捷的通信服务. + +#figure( + table( + columns:(auto,auto,auto), + [特性\ (Characteristic)], + [技术实现概述 \ (Technical Implementation Overview)], + [对应用程序的意义 \ (Significance for Applications)], + + [面向连接\ (Connection-Oriented)], + [在数据传输开始前,通信双方必须通过"三次握手"建立一个逻辑连接.通信结束后,通过"四次挥手"断开连接.], + [应用程序可以像读写本地文件一样处理网络数据,无需关心单个数据包的发送和接收.这种会话(session)模式简化了编程模型.], + + [可靠交付\ (Reliable Delivery)], + [采用序列号、确认(ACK)、超时重传和校验和等机制,确保数据无损、无错、按序到达.], + [应用程序可以确信其发送的数据将以正确的顺序完整地到达对方,无需自行实现复杂的错误检测与恢复逻辑.这是文件传输等应用的基础.], + + [全双工通信\ (Full-Duplex Communication)], + [TCP连接允许数据在两个方向上同时传输.通信双方既是发送者,也是接收者.], + [提高了通信效率,允许应用程序在发送数据的同时接收来自对方的数据,非常适合交互式应用.] + ) +) + +=== 连接 + +每一条TCP连接有两个端点,叫作*套接字(socket)*或插口.端口号拼接到IP地址即构成了套接字. +因此,套接字的表示方法是在点分十进制的IP地址后面写上端口号,中间用冒号或逗号隔开.例如192.3.4.5:80 + +=== 可靠 + +网络层提供的是不可靠的交付(best-effort),但是用户需要可靠交付,TCP提供了可靠交付,作为对比,UDP不提供可靠交付. + +TCP可靠性的"三驾马车": + - 序列号保证了数据的可排序性和唯一性 + - 确认机制提供了成功接收的反馈 + - 超时重传则构成了应对数据丢失的最终保障 + +==== 序列号(Sequence Numbers) + +应用程序通过TCP发送的是一个连续的、无边界的字节流.TCP的实现首先会将这个字节流分割成大小合适的数据块,这些数据块被称为"TCP报文(TCP Segment)". + +TCP并不为这些段本身编号,而是为字节流中的每一个字节分配一个唯一的序列号.每个TCP段的头部都包含一个序列号字段,该字段的值并非段的编号,而是该段所承载的数据中第一个字节在整个字节流中的序列号. + +这种以字节为单位进行编号的设计带来了巨大的优势: + 1. 精确重组:接收方可以根据每个段的序列号和数据长度,精确地将数据放回字节流的正确位置,从而完美地处理乱序到达的TCP段. + 2. 冗余检测:如果接收方收到了序列号相同的数据,就可以轻易识别出这是重复的数据包,并将其丢弃. + 3. 精确确认:它使得确认机制可以精确地告知发送方已收到的报文的信息. + +==== 确认机制(Acknowledgments) + +当接收方成功收到数据后,它需要向发送方回送一个确认(ACK),告知对方数据已达.设计确认策略时,需要在信息的详尽程度和协议开销之间做出权衡.以下是三种不同策略的对比分析. +#figure( + table( + columns: (auto,auto,auto,auto), + [策略\ (Strategy)], + [工作原理\ (Mechanism)], + table.cell(colspan: 2)[优缺点分析\ (Pros & Cons Analysis)], + + [独立确认\ (Individual ACKs)], + [接收方为每一个成功收到的数据包发送一个独立的ACK.例如,收到包\#1回ACK(1),收到\#2回ACK(2).], + [优点:信息明确,发送方能准确知道哪个包被确认.], + [缺点:非常低效.若ACK(2)丢失,即使ACK(3)和ACK(4)都已到达,发送方仍会认为包\#2丢失并进行不必要的重传.], + + [全信息确认\ (Full-Information ACKs)], + [接收方的ACK中包含一个完整列表,列出所有已收到的数据包.例如,ACK(up to \#1, plus \#3, \#4).], + [优点:信息最详尽,能最大限度地避免不必要的重传.], + [缺点:开销巨大.随着收到的包增多,ACK本身会变得非常长,尤其在有零星丢包的情况下,会严重浪费带宽.], + + [累积确认\ (Cumulative ACKs)], + [*(TCP采用的方案)*接收方的ACK只包含一个数字,表示"所有序号直到该数字为止的数据字节都已连续收到". ], + [优点:开销小,非常高效,一个ACK可以同时确认之前的所有数据.], + [缺点:引入了歧义性.例如,发送方连续收到多个ACK(100),只能推断出100之后有数据包到达,但无法确切知道是哪些数据包.] + ) +) +最终,TCP选择了累积确认作为其核心策略.TCP牺牲了完美的信息清晰度,换取了协议开销的显著降低.这一设计折衷已被证明是有效的,但也正是这种固有的歧义性,催生了后续更为复杂的丢包检测逻辑. + +==== 超时重传(Timeout and Retransmission) + +定时器基本逻辑非常简单:发送方为每一个发出的、尚未被确认的数据包启动一个定时器. +如果在定时器超时之前收到了对该数据包的确认,则取消定时器; +反之,如果定时器到时仍未收到确认,发送方就假定该数据包已在网络中丢失,并立即进行重传. + +然而,设置重传定时器并非简单的配置,其数值的大小会影响网络的运行效率: + - 定时器过长:会导致发送方在数据包确实丢失后,需要等待一段不必要的时间才能发现并重传,从而增加了数据传输的延迟. + - 定时器过短:会导致发送方在数据包及其ACK只是轻微延迟的情况下,错误地判断其为丢失并进行重传.这种不必要的重传会严重浪费网络带宽. + +*理想*的超时时长应略大于*网络的往返时间 (Round-Trip Time, RTT)*,即数据包从发送到收到确认所需的总时间. +然而,在*真实*的互联网中,RTT是一个*动态变化*的值,它受网络路径、路由器队列拥塞状况等多种因素影响,这使得精确估算RTT并设定一个完美的超时时长十分困难. + +==== RTT的计算 + +TCP采用了一种自适应算法,它记录一个报文段发出的时间,以及收到相应的确认的时间.这两个时间之差就是报文段的往返时间RTT. +TCP保留了RTT的一个加权平均往返时间$"RTT"_s$(这又称为平滑的往返时间,S表示Smoothed.因为进行的是加权平均,因此得出的结果更加平滑.) + +每当第一次测量到RTT样本时,$"RTT"_s$值就取为所测量到的RTT样本值.但以后每测量到一个新的RTT样本,就按下式重新计算一次$"RTT"_s$: +$ "新的""RTT"_s = (1-alpha) times "旧的""RTT"_s + alpha times "新的RTT样本" $ +已成为建议标准的RFC 6298推荐的$alpha$值为$1/8$. + +显然,超时计时器设置的超时重传时间RTO(RetransmissionTime-Out)应略大于上面得出的加权平均往返时间$"RTT"_s$.RFC 6298建议使用下式计算RTO:$ "RTO" = "RTT"_s + 4 times "RTT"_D $ + +$"RTT"_D$是RTT的偏差的加权平均值.RFC 6298建议这样计算$"RTT"_D$.当第一次测量时,$"RTT"_D$值取为测量到的RTT样本值的一半.在以后的测量中,则使用下式计算: +$ "新的""RTT"_D = (1-beta) times "旧的""RTT"_D + beta times |"RTT"_s - "新的RTT样本"| $ +推荐$beta = 1/4$. + +上面所说的往返时间的测量,实现起来相当复杂.试看下面的例子. + +发送出一个报文段,设定的重传时间到了,还没有收到确认,于是重传报文段.经过了一段时间后,收到了确认报文段. +现在的问题是:如何判定此确认报文段是对先发送的报文段的确认,还是对后来重传的报文段的确认? +由于重传的报文段和原来的报文段完全一样,因此源主机在收到确认后,就无法做出正确的判断. + +Karn提出了一个算法:在计算加权平均$"RTT"_s$时,只要报文段重传了,就不采用其往返时间样本.这样得出的加权平均$"RTT"_s$和RTO就较准确. + +但是,这又引起新的问题.设想出现这样的情况:报文段的时延突然增大了很多.因此在原来得出的重传时间内不会收到确认报文段,于是就重传报文段. +但根据 Karn 算法,不考虑重传的报文段的往返时间样本.这样,超时重传时间就无法更新. + +因此要对Karn算法进行修正. +方法是:报文段每重传一次,就把超时重传时间RTO增大一些. +典型的做法是取新的重传时间为旧的重传时间的2倍. +当不再发生报文段的重传时,才根据上面给出的公式计算超时重传时间. + + +== 流量控制 + +在为可靠性建立了基线之后,下一个至关重要的设计挑战便是性能.一个协议如果完美可靠但慢到无法使用,那它同样是失败的. + +=== 滑动窗口协议(Sliding Window) + +滑动窗口是这一理念的具体实现.发送方在内部维护一个连续的序列号范围,这个范围被称为发送窗口.该窗口定义了在任何给定时刻,允许被发送但尚未收到确认的数据总量. + +- 窗口大小($W$)是该协议的核心参数.它严格限制了在途数据(无论是按包还是按字节计算)的总量上限. +- 窗口滑动过程则是一个动态的过程:当发送方收到对窗口内最早发送数据的确认时(例如,收到对窗口左边界数据的ACK),窗口的左边界就会向右移动(即"滑动").这表示旧的数据已经被成功接收,从而为发送新的数据腾出了空间. + +通过这种方式,滑动窗口协议实现了数据的连续流动,避免了"停等"协议中的空闲等待时间. + +=== 窗口大小的确定 + +一个自然而然的问题是:发送窗口的大小($W$)应该被设置成多少才最合适?过小的窗口无法充分利用网络带宽,而过大的窗口则可能引发网络问题.TCP的设计综合考虑了三个核心因素来动态地决定其实际窗口大小: + - *带宽时延积(Bandwidth-Delay Product)* + - 要实现"填满管道"的理想状态,窗口大小应恰好等于网络路径的容量.这个容量可以通过"带宽时延积"来计算,即 $W = "RTT" times "Bottleneck Bandwidth"$. + - 这个公式的直观解释是:在发送第一个字节到收到其确认的整个RTT时间内,为了让发送方持续不断地发送数据,需要发送的总字节数. + - 例如,如果一个连接的RTT为1秒,瓶颈带宽为8Mbps(即1MB/s),那么理想的窗口大小就是 $1"s" times 1"MB/s" = 1"MB"$. + - *流量控制(Flow Control)* + - 流量控制的目标是防止速度快的发送方*压垮(overwhelm)*处理能力较慢的接收方. + - 即使网络带宽充裕,如果接收方的应用程序来不及处理收到的数据,其内部的接收缓冲区也可能会溢出,导致数据丢失. + - 接收方会在它发送的每个ACK包中包含一个名为"*通告窗口(Advertised Window, RWND)*"的字段.该字段明确告知发送方其接收缓冲区当前还剩余多少可用空间. + - 发送方必须确保其在途数据总量不超过RWND,从而避免淹没接收方. + - *拥塞控制 (Congestion Control)* + - 拥塞控制的目标则更为宏大:防止发送方的数据流压垮网络本身. + - 当多个连接同时向网络注入过多数据,超出路由器处理和转发能力时,就会发生网络拥塞,导致路由器队列溢出和大量丢包. + - 由于网络本身无法直接告知发送方其拥塞状况,TCP发送方必须通过算法自行推断,并计算出一个"*拥塞窗口 (Congestion Window,CWND)*".这个值动态地反映了发送方根据当前网络状况判断出的、可以安全发送的数据量. + +=== 最终发送窗口的计算 + +为了同时满足上述所有约束——*既要填满管道,又不能压垮接收方,也不能压垮网络*——TCP发送方采取了一个简单而有效的策略.它将其实际的发送窗口大小设置为流量控制窗口和拥塞控制窗口中的较小者: +$ +"发送窗口" = min("RWND", "CWND") +$ +这个公式确保了TCP的数据传输速率始终受到最严格限制因素的制约,无论是接收方的处理能力还是网络的承载能力. + +== 拥塞控制 + +=== 拥塞信号的探测 + +由于缺乏直接的拥塞信号,TCP将"丢包"事件作为网络拥塞的间接指示器.它通过两种不同的丢包检测方式,来判断拥塞的严重程度: + - *超时 (Timeout)* + 这被视为*严重*拥塞的强烈信号.超时意味着在相当长的一段时间内,窗口内最早发送的数据包及其后续数据包的ACK都未能成功传输.这通常表明网络路径上发生了严重的拥塞,导致大量数据包被丢弃. + - *三个重复ACK (3 Duplicate ACKs)* + 这被视为*轻度*拥塞的信号.收到重复的ACK意味着发送方发送的某个数据包丢失了,但其后的数据包(至少三个)仍然成功抵达了接收方.这种情况暗示网络只是发生了零星的丢包,整体路径依然通畅. + +=== 拥塞窗口 (CWND) 的动态调整机制 + +基于对上述两种拥塞信号的判断,TCP通过动态调整拥塞窗口(CWND)来控制发送速率.这一过程主要由三个核心阶段(或算法)组成,它们共同构成了著名的*加性增窗,乘性减窗(AIMD)*策略. + +==== 慢启动 (Slow Start) + +*目标*:在连接建立之初或因严重拥塞重置后,快速地探测网络的可用初始带宽,避免从一个很低的速率缓慢爬升. + +*行为与效果*: + 1. *机制*:CWND的初始值通常被设为1个最大报文段长度(MSS).此后,每当发送方收到一个新的ACK时,CWND就增加1个MSS.*1个ACK对应一次"$"CWND"++$"的行为*. + 2. *涌现行为*:这个看似简单的规则会导致一种*指数*级的增长效应. + - 假设一个窗口内的数据在一个RTT后全部被确认,那么CWND的大小大约每经过一个RTT就会翻一番.这使得TCP可以迅速达到网络的容量附近. + - 例如,发送1个包,收到1个ACK,CWND变2;发送2个包,收到2个ACK,CWND变4. + +*结束条件*:当CWND的值增长到预设的"慢启动阈值"(SSTHRESH)时,或者当发生任何丢包事件时,慢启动阶段结束. + +==== 拥塞避免 (Congestion Avoidance) + +- 目标:当*CWND超过SSTHRESH*后,表明连接已接近网络的设计容量.此时需要转入一种更审慎的探测模式,以避免因增长过快而导致拥塞.这一阶段遵循*加性增窗 (Additive Increase)*原则. +- 行为:每当收到一个新的ACK时,CWND增加 MSS $times$ (MSS / CWND) 字节. +- 效果:这种分数式增长的最终效果是,*大约在每个RTT内,CWND只会线性地增加1个MSS*.这种温和的线性增长模式,使得TCP能够小心翼翼地逐步填充可能存在的剩余带宽. + +==== 乘性减窗 (Multiplicative Decrease) 与丢包响应 + +当探测到拥塞(即发生丢包)时,TCP会立即采取*乘性减窗 (Multiplicative Decrease)*策略来降低其发送速率,但具体的响应措施根据拥塞信号的严重程度而有所不同: + - 对于超时事件 (严重拥塞): + 1. 将SSTHRESH设置为*当前CWND*的一半. + 2. 将CWND重置为1个MSS. + 3. 重新进入慢启动阶段,从头开始探测网络. + - 对于3个重复ACK事件 (轻度拥塞): + 1. 执行*快速重传 (Fast Retransmit)*,即不等超时就立刻重传被认为是丢失的那个数据包. + 2. 将SSTHRESH和CWND均设置为*当前CWND*的一半. + 3. 进入快速恢复阶段,而不是直接退回慢启动. + +==== 快速恢复 (Fast Recovery) + +在响应3个重复ACK并将CWND减半后,出现了一个新的问题:由于CWND突然缩小,可能导致在途数据量小于新的窗口值,发送方会因此陷入停顿,等待丢失数据包的ACK返回,这会造成网络管道的短暂空闲. + +快速恢复机制正是为了解决这个问题而设计的: + 1. 初始调整:在收到第3个重复ACK时,除了将CWND减半,还会额外加上3个MSS(CWND = CWND/2 + 3),这个"3"代表着已经有3个数据包离开网络,为发送新数据腾出了空间. + 2. 持续膨胀:在快速恢复阶段,每当再收到一个额外的重复ACK,CWND就再增加1个MSS.这种临时的、人为的"虚增"CWND,允许发送方在等待丢失包的ACK期间,能够继续发送新的数据包,从而有效地避免了管道的停滞. + 3. 阶段结束:当最终收到一个确认新数据(即确认了那个被重传的包以及它之后的所有包)的ACK时,表明丢包问题已解决.此时,快速恢复阶段结束,CWND被正式设置为SSTHRESH(即减半后的值),然后连接平滑地过渡到拥塞避免阶段. + +=== TCP锯齿图 (TCP Sawtooth) 可视化 + +如果我们将CWND的大小随时间的变化绘制成图,会得到一个非常具有代表性的模式,被称为"TCP锯齿图".这个图形生动地展示了TCP拥塞控制算法的动态行为. + +一个典型的锯齿周期包含以下部分: + +1. 指数增长阶段:在连接开始时,曲线呈现陡峭的指数上升,这对应于慢启动阶段,CWND每隔一个RTT就翻倍. +2. 线性增长阶段:当CWND达到SSTHRESH后,曲线的斜率变得平缓,呈现稳定的线性上升.这对应于拥塞避免阶段,CWND每个RTT只增加1个MSS. +3. 急剧下降点:当线性增长持续到网络开始出现丢包(通常由3个重复ACK触发)时,CWND会被立即减半,在图上表现为一个垂直的、急剧的下降. + +这个"加性增、乘性减"(Additive Increase, Multiplicative Decrease - AIMD)的循环模式,使得TCP流能够持续地、主动地探测网络的可用带宽上限,并在探测到拥塞时迅速退让,然后再次开始温和地探测.这种不断的探测-退让循环,形成了标志性的锯齿形状. + +课本上有个例子,这里放在这里作为前文整体的总结. + +特别注意横坐标采用的单位是往返时延RTT. +在实际的互联网中,TCP发送的每一个报文段的往返时延RTT都是不一样的. +但在这里我们是讲解拥塞控制的原理,因此应当把图中的RTT理解为一个大致的时间,在这样的时间之内,发送方发出了一批报文段,并且都收到了接收方的确认. +图5.1中的数字1至5是特别要注意的几个点,现假定TCP的发送窗口等于拥塞窗口. +#figure( + canvas({ + import draw: * + scale(x: 0.5, y: 0.3) + // --- Styles --- + let dot-style = (radius: 0.15, fill: black) + let badge-style = (radius: 0.4, fill: black, stroke: none) + let badge-text-style = (fill: white, weight: "bold", size: 9pt) + + // --- Data Points --- + let p-ss1 = ((0, 1), (1, 2), (2, 4), (3, 8), (4, 16)) + let p-ca1 = ((5, 17), (6, 18), (7, 19), (8, 20), (9, 21), (10, 22), (11, 23), (12, 24)) + let p-to = ((13, 1),) + let p-ss2 = ((14, 2), (15, 4), (16, 8), (17, 12)) + let p-ca2 = ((18, 13), (19, 14), (20, 15), (21, 16)) + let p-fr = ((22, 8),) + let p-ca3 = ((23, 9), (24, 10), (25, 11)) + + let all-points = p-ss1 + p-ca1 + p-to + p-ss2 + p-ca2 + p-fr + p-ca3 + + // --- Axes & Grid --- + // Y-axis grid lines (dashed) + for y in (4, 8, 12, 16, 20, 24) { + line((0, y), (26, y), stroke: (dash: "dashed", paint: gray, thickness: 0.5pt)) + content((-0.6, y), text(size: 8pt)[#y]) + } + + // Axes Lines + line((0, 0), (27, 0), mark: (end: "stealth"), name: "x") + content("x.end", anchor: "west", padding: .2)[往返时延 RTT] + + line((0, 0), (0, 26), mark: (end: "stealth"), name: "y") + content("y.end", anchor: "south", padding: .2)[拥塞窗口 cwnd] + + // X-axis ticks + for x in range(0, 26, step: 2) { + line((x, 0), (x, 0.2), stroke: 0.5pt) // Upward tick inside or downward? Standard is often out. + content((x, -0.5), text(size: 8pt)[#x]) + } + + // --- Plotting --- + // Draw the main line + line(..all-points, stroke: (thickness: 1pt)) + + // Draw dots at each point + for p in all-points { + circle(p, ..dot-style) + } + + // --- Annotations --- + + // Badges (Numbered Points) + // Helper to draw badge + let badge(pos, num, anchor: "south") = { + content(pos, anchor: anchor, padding: 0.4)[ + #box(width: 1.2em, height: 1.2em, radius: 0.6em, fill: black, align(center+horizon, text(fill: white, weight: "bold", size: 8pt)[#num])) + ] + } + + badge((5, 15), "1", anchor: "south-east") + badge((13, 23), "2", anchor: "south-east") + badge((18, 11), "3", anchor: "south-east") + badge((22, 15), "4", anchor: "south-east") + badge((21, 10), "5", anchor: "north") + + // Text Labels + + // Initial ssthresh + content((-1, 16), anchor: "east", text(size: 9pt)[ssthresh \ 的初始值]) + line((-0.2, 16), (3.5, 16), stroke: (thickness: 0.5pt), mark: (end: "stealth")) + + // Slow Start (First) + content((2.5, 5), anchor: "east", padding: 0.2)[ + #std.rect(fill: white, stroke: 0.5pt, inset: 3pt, text(size: 9pt)[慢开始]) + ] + line((1, 4), (0.5, 1.5), stroke: 0.75pt, mark: (end: "stealth")) + + // Congestion Avoidance 1 + content((8, 20.5), anchor: "south-east", angle: 28deg, text(size: 9pt)[拥塞避免]) + + // Timeout + content((12.5, 24), anchor: "west", padding: 0.5, text(size: 9pt)[超时 (网络发生拥塞,执行慢开始算法)]) + + // 1st Adjust ssthresh + content((15, 22), text(size: 9pt)[第1次调整 ssthresh]) + line((15, 21.5), (13.5, 12.5), stroke: 0.5pt, mark: (end: "stealth")) + + // Slow Start (Second) + content((11.5, 6), padding: 0.2)[ + #std.rect(fill: white, stroke: 0.5pt, inset: 3pt, text(size: 9pt)[慢开始]) + ] + line((12, 5), (12.8, 1.5), stroke: 0.5pt, mark: (end: "stealth")) + + // Congestion Avoidance 2 + content((20, 15.5), anchor: "south-east", angle: 28deg, text(size: 9pt)[拥塞避免]) + + // 3-ACK + content((20.5, 16), anchor: "west", padding: 0.5, text(size: 9pt)[3-ACK (执行快恢复算法)]) + + // 2nd Adjust ssthresh + content((25.5, 5.5), text(size: 9pt)[第2次调整 ssthresh]) + line((23, 6.5), (22.2, 7.8), stroke: 0.5pt, mark: (end: "stealth")) + + // Reno Version + content((25, 13), text(size: 9pt)[TCP Reno 版本]) + line((24.5, 12.5), (23.5, 10.5), stroke: 0.5pt) // Pointer to the line + + // Congestion Avoidance 3 + content((25.5, 9.5), anchor: "south-east", angle: 30deg, text(size: 9pt)[拥塞避免]) + + }), + caption: [TCP 拥塞窗口cwnd在拥塞控制时的变化情况] +) + +当TCP连接已建立后,把拥塞窗口cwnd置为1. +在本例中,慢开始门限的初始值设置为16个报文段,即ssthresh$ = 16$. +在执行慢开始算法阶段,每经过一个往返时间RTT,拥塞窗口cwnd就加倍. +当拥塞窗口cwnd增长到慢开始门限值ssthresh时(图中的点1,此时拥塞窗口cwnd$=16$),就改为执行拥塞避免算法,拥塞窗口按线性规律增长. +但请注意,“拥塞避免”并非完全避免拥塞,而是让拥塞窗口增长得缓慢些,使网络*不容易*出现拥塞. +当拥塞窗口 cwnd = 24时,网络出现了超时(图中的点2),这就是网络发生拥塞的标志. + +于是调整门限值ssthresh = cwnd/2 = 12,同时设置拥塞窗口cwnd = 1,执行慢开始算法. +按照慢开始算法,发送方每收到一个对新报文段的确认ACK,就把拥塞窗口值加1. +当拥塞窗口cwnd = ssthresh = 12时(图中的点3,这是ssthresh第1次调整后的数值),改为执行拥塞避免算法,拥塞窗口按线性规律增大. + +当拥塞窗口cwnd = 16时(图中的点4),出现了一个新的情况,就是发送方一连收到3个对同一个报文段的重复确认(图中记为3-ACK).发送方知道现在只是丢失了个别的报文段.于是不启动慢开始,而是执行快恢复算法. +这时,发送方第2次调整门限值,使ssthresh = cwnd / 28,同时设置拥塞窗口cwnd = ssthresh = 8 (见图中的点5),并开始执行拥塞避免算法. + +=== TCP拥塞控制算法的演进 + +我们所讨论的拥塞控制机制并非一成不变,而是在实践中不断演化.不同的TCP版本对拥塞事件的响应策略有所不同,其中最关键的区别在于如何处理"3个重复ACK". + +一个有趣的事实是,这些不同的拥塞控制算法可以同时在互联网上共存.这是因为拥塞控制完全是在端到端(end-to-end)层面实现的,它属于操作系统内核的一部分.只要发送方遵循标准的TCP数据包格式,它就可以自由选择其内部的速率调整算法.网络中的路由器和对端主机并不知道也不关心发送方具体使用了哪种算法来计算其发送速率. +#figure( + table( + columns: (auto,auto,auto), + [版本 (Version)], + [对"3个重复ACK"的响应\ (Response to 3 Duplicate ACKs)], + [关键特性 (Key Feature)], + + + [TCP Tahoe], + [将SSTHRESH设为CWND/2,然后将CWND直接重置为1,并强制进入慢启动阶段.], + [最早的实现.对所有丢包事件(无论是超时还是重复ACK)都采取最严厉的惩罚,反应过于激进,无法区分拥塞程度.], + + [TCP Reno], + [执行快速重传.将SSTHRESH和CWND均设为CWND/2,然后直接进入拥塞避免.], + [引入了快速重传,首次区分了轻度拥塞和严重拥塞.但缺乏有效的恢复阶段,若单个窗口内丢失多个包,会导致发送方停顿.], + + [TCP New Reno], + [与Reno响应相同,但之后会进入快速恢复阶段.], + [在Reno基础上增加了快速恢复算法.通过在恢复期间临时虚增CWND,防止了管道停滞,显著提升了在有损链路上的性能.] + ) +) diff --git a/Computer-Network/note.typ b/Computer-Network/note.typ index abbdee4..73aade8 100644 --- a/Computer-Network/note.typ +++ b/Computer-Network/note.typ @@ -17,3 +17,4 @@ #include "02-physical.typ" #include "03-links.typ" #include "04-internet.typ" +#include "05-transport.typ" \ No newline at end of file