EE1608-Computer-Networks-Part2-2-传输层
传输层基础
传输层的功能
网路层是为了主机与主机间在网络中路由互通,传输层则负责进程之间的逻辑端到端通信。例如手机上的QQ与QQ服务器间的通信。
一条通信链路上的任何一个路由器都需要具备网络层的解析功能,但是仅仅通信的两端需要具备传输层的功能。
传输层接受应用层的的数据,并把数据切分成“段(segment)”,然后交给网络层传输。
传输层基础
传输层的两种协议
传输层有两种协议:UDP和TCP。他们都属于传输层协议,功能一致,只是可靠性、延迟不一样。
| 维度 | TCP | UDP |
|---|---|---|
| 数据导向 | 面向字节流(Stream oriented) | 面向数据报(Datagram oriented) |
| 可靠性与连接性 | 可靠、面向连接(Reliable, connection-oriented) | 不可靠、无连接(Unreliable, connectionless) |
| 协议复杂度 | 复杂 | 简单 |
| 通信方式 | 仅单播(Only unicast) | 单播、多播(Unicast and multicast) |
| 典型应用 | 多数互联网应用(ftp、telnet、http、smtp 等) | 多媒体应用(如流媒体);服务类应用(SNMP、RIP、DNS、DHCP 等) |
传输层所使用的地址
数据链路层:使用MAC 地址,网络层使用IP地址,传输层使用端口号。
端口号由16bit构成,范围是0-65535。
通常来说,客户端的端口号是由应用自己随机选择的,而服务器的端口号使用一些知名的约定俗成端口。例如对于http服务,客户端的端口是自己随机的,服务器通常使用固定的80端口。
- 知名端口(Well-known ports):
0~1023,用于受限的知名服务(如 HTTP 用 80,FTP 用 21); - 注册端口(Registered ports):
1024~49151,由机构注册给特定应用; - 动态端口(Dynamic ports):
49152~65535,供客户端临时使用。
套接字(Socket)
套接字是传输层进程间通信的端点,核心作用是多路复用与多路分解(即区分不同进程的通信流)
套接字其实就是 “端口号 + 主机 IP 地址” ,当某个进程的某一个服务对外传输或请求数据时,是以套接字面向网络的。
- 源套接字(source socket):源端口号 + 源IP地址;
- 目的套接字(destination socket):目的端口号 + 目的IP地址。
在对外传输消息时,我们只需要新建一个套接字,然后使用诸如connect之类的函数连接到服务器的socket地址,就可以不需要操心传输层网络层,不需要操心TCP之类的协议,进行通信。这被成为套接字编程。
复用与解复用
是什么
传输层负责复用与解复用,其流程如下:
多路复用(Multiplexing):在源主机,传输层收集来自不同套接字的 “数据块”,为每个数据块封装传输层首部信息(用于后续多路分解),生成 “段(segment)” 后传递给网络层。简单说,是 “多进程数据→ 封装成段→ 交给网络层” 的聚合过程。
多路分解(Demultiplexing):在接收端,传输层检查段的首部信息,通过IP+端口识别出对应的接收套接字,并将段定向到该套接字。简单说,是 “接收段→ 解析首部→ 分发到对应进程” 的拆分过程。
复用与解复用的的点如下图所示

面向无连接的多路分解
对于无连接的线路(基于UDP), 仅通过 “目的端口号”识别套接字。在创建本地socket时,也只需要指定它的本地端口;但是发送时还是需要指定目的IP和端口好。
仅通过 “目的端口号”识别的特性造就了UDP在解复用时,并不关心这个数据包是哪里来的(不关心源站IP),只关心它应该被分配给哪个socket。
例如下面这个例子:虽然有两个设备都向服务器端口为6428的socket发消息,但是由于他们是无连接的,所以在服务器侧他们被分流到一个socket内。

面向连接的多路分解
面向连接的多路分解更复杂,TCP 套接字由 “4 元组” 唯一标识:源IP地址 + 源端口号 + 目的IP地址 + 目的端口号。通过识别这4个字段,将段精准定向到对应的 TCP 套接字。
TCP要求两个套接字间需要握手,且它会对数据包的可用性负责,当发现损坏的数据包,它会要求源socket重传。
考虑下面这个例子:有两台设备,他们与服务器通信的每一个套接字,都在服务器侧有一个单独的套接字进行处理。不像UDP那样只要端口一样就被分流到一个套接字内。

面向无连接的传输层协议:UDP
引入
UDP 的核心服务特性可总结为 “极简、尽力而为、无可靠性保障”:
- 基于端口号实现进程间通信的多路分解
- 尽力而为服务(Best-effort service):
- 不可靠:无确认(acknowledgment)、无序号(sequence numbers),数据可能丢失、乱序。
- 无连接:无握手(handshaking)、无连接状态,通信前无需建立连接。
- 无流量控制、无差错控制、无拥塞控制。
- 首部简单,传输效率高,开销低
UDP的典型应用是流媒体这类对丢包容忍但对速率敏感的服务。要实现UDP的可靠传输,需在应用层自行实现(如添加确认、差错恢复逻辑)。
UDP的包结构
UDP的包构造如下,其中Source Port + Destination Port + Length + Checksum是UDP Header,这几个加上应用层的Data一起被称为UDP包(下图非红色部分),其中Length字段是以字节为单位的UDP 段的总长度(首部 + 数据)。

上图红色部分被称为UDP 的 “伪首部(pseudo header)” 。它包含源站和目的站的IP,协议标识(UDP 的协议号为 17),UDP 段长度(UDP 首部 + 数据的总长度)。
之所以被称为 “伪”,核心原因是它并非 UDP 数据报实际传输的组成部分,仅在校验和计算阶段临时构造。其字段(如源 IP、目的 IP、协议号等)是从 IP 首部中提取的,既不随 UDP 数据报向下传输到链路层,也不向上递交到应用层,仅在传输层计算校验和时 “临时借用”。
也就是说,它不会随数据包一起发送。以校验和为例,发送方在计算校验和时临时构造伪头部,计算完成后丢弃。接收方在收到数据包后,也会根据当前 IP 头部信息重新构造伪头部,用于校验数据的完整性。
UDP的Checksum
为何需要Checksum
尽管链路层协议也有差错检测,UDP 仍需校验和,因为:
- 无法保证所有链路都提供差错检测。
- 数据在路由器内存中存储时可能引入比特错误。
- 双重检测可确保 IP 首部的错误也能被准确识别。
- 提供 端到端(end-to-end)的差错检测。
IPv4 中 UDP 校验和是可选的,若不使用,校验和字段需设为 0
Checksum的工作机理
UDP 校验和的计算逻辑是:对伪首部、UDP 首部、数据进行 “1 的补码和” 运算,再取 “1 的补码” 作为校验和(若数据长度为奇数,需补 0 凑成偶数字节)。伪首部在计算完成会被丢弃。
接收端若检测到校验和错误,通常会丢弃该 UDP 段。
校验和的计算过程可以看下面这个16bit校验和的例子:

首先将两个 16 位数相加,若有进位(wraparound),需将进位加到结果中。
对最终的和取 “1 的补码”,得到校验和(这种计算方式能检测传输中的比特翻转错误)。
可靠数据传输原理(Reliable Data Transfer)
引入
可靠数据传输(RDT)模型适用于网络的多个层(应用层、传输层、链路层等),其核心框架是:为上层实体提供一个可靠信道,通过该信道可传输数据。
所谓可靠信道,指的是:1. 所有传输的比特不会损坏或丢失;2. 数据按发送顺序交付。在可靠信道中,如果有任何的数据出现了错误或者丢失,会根据协议进行纠错或重传。
如果下层信道是可靠信道,无需关心底层细节。如果底层实际是不可靠信道,则可靠传输协议需要在不可靠信道的基础上,通过一系列机制(如确认、重传等)实现可靠效果。在不可靠信道上运行可靠通信协议之后,即可将其升级为可靠信道。
RDT是基于协议实现的,其设计逻辑是假设一个单向数据传输的场景,针对这个场景设计数据传输协议。
可靠信道设计(设计停等协议)
系统模型
对于一个可靠信道,其可以被看做由4个函数组成:
rdt_send():由上层(如应用层)调用,将数据传递给可靠传输协议的发送端。udt_send():由 RDT 协议调用,通过不可靠信道将数据包传输到接收端。rdt_rcv():当数据包到达接收端的信道时被调用。deliver_data():由 RDT 协议调用,将数据交付给上层(如应用层)。

也就是说,通信协议暴露一个可靠传输的API给应用层,协议内自己调用不可靠的信道提供的API进行通信,并通过协议的机制让它变得可靠(无错误,正确顺序)。
如何设计
在下面的步骤中,将会逐渐来设计一个完整的RDT协议。首先先从最基础的部分看起,先考虑单向数据传输(但控制信息是双向的,比如确认信息)。
使用有限状态机(Finite State Machine FSM)来明确发送端、接收端的状态、事件和动作。
理想可靠信道下的 RDT 协议(rdt1.0)
在rdt1.0中,假设底层信道完全可靠:无比特错误、无数据包丢失。
在这个时候,发送端和接收端的状态机如下图:

对于发送端,它处于“wait for call”状态待机,一旦应用层调用rdt_send(data)函数去call它,它就会执行packet=make_pkt(data)封装包,然后调用udt_send(packet)进行传输,然后回到待机状态继续等待下一次call。
接收端类似,只不过它是等待被下层下一call,调用解析函数,最后把它传递给应用层。
会出现错误的信道下的RDT协议(rdt2.0)
RDT2.0:停等差错控制
此时我们假设底层信道可能出现比特翻转(数据损坏),并通过校验和(checksum)检测错误。在此处我们称其为rdt2.0。
- 如果无错误的数据收到,则应答acknowledgements (ACKs),表示数据正常收到。
- 如果有错误的数据收到,则应答negative acknowledgements (NAKs),表示数据有误。
此时发送端和接收端的状态机如下图:

发送端:
- 初始状态 “Wait for call from above”:等待上层调用
rdt_send(data),封装带校验和的数据包(sndpkt = make_pkt(data, checksum)),通过udt_send(sndpkt)发送,随后进入 “Wait for ACK or NAK” 状态。 - 若收到
rdt_rcv(rcvpkt) && isACK(rcvpkt)(正确 ACK),则回到初始状态;若收到rdt_rcv(rcvpkt) && isNAK(rcvpkt)(NAK),则重发sndpkt。
接收端:
- 初始状态 “Wait for call from below”:等待下层调用
rdt_rcv(rcvpkt)。 - 若
corrupt(rcvpkt)(数据包损坏),则发送 NAK(udt_send(NAK));若notcorrupt(rcvpkt)(数据包正确),则提取数据(extract(rcvpkt.data))并交付上层(deliver_data(data)),同时发送 ACK(udt_send(ACK))
RDT2.1:带有序号的停等
rdt2.0有一个致命缺陷:若 ACK 或 NAK 本身被损坏,发送端无法判断接收端的状态,若盲目重发,会导致接收端收到重复数据包。
为了解决这个缺陷,重复数据包,发送端对每个数据包添加序号(sequence number);接收端通过序号识别重复包,直接丢弃(不向上层交付重复数据)。这就是停等(stop and wait)差错控制。我们称引入了这种机制的rdt为rdt2.1。
rdt2.1的发送端状态机如下图所示:
- 在待机状态等待应用层call,一旦应用层call就进行封装,udt发送。在封装时,对数据标上序号0。
- 然后进入等待ACK或NACK0。
- 此时接收机进行校验,如果正确,则向发送端发送ACK,发送端再次等待应用层call,只不过这次call之后会将序号标为1。
- 如果校验不正确,则发送NACK,此时发送端重传,并再次进入等待ACK0的状态。
- 对于序号1,与0处理方式一致。

接收端状态机如下图:

RDT2.2:无NACK的魔改版本
除了使用NACK来标识错误数据包,也可以只应答正确的数据包。例如正确接收了数据0,就ACK0;正确接收了数据1,就ACK1。如果接收端连续两次对一个数据包进行ACK,则对该数据包进行重传(如传输包1,其损坏,则接收端会再次发送ACK0,来表示1损坏了,现在接收到的最后一个正确包是0)。这被称为RDT2.2
其状态机如下图,与RDT2.1的主要区别是,isACK函数的使用规则被更改了,原本需要发送NACK的地方被改为了发送上一个ACK。

有错误和丢包的信道(RDT3.0)
现在我们在RDT2.0的基础上再加入包在传输过程中丢失的情况。一旦包丢失,接收端不会收到任何消息,因此此时需要Timer和t超时(timeout)机制。一旦发送端timer溢出之前未收到ACK,则重新发送该包。
发送端状态机如下图所示,增加了在timeout情况下重传的情况。

下图是RDT3.0下所有情况的传输处理。

其中,对于ACK延迟的情况,上图做了两种讨论(上图的两个d),一种是收到第二次ack1之后重传pkt0,另一种是连续收到2次ack1之后什么都不做。相较于重传的情况,什么都不做或许会更好。即,在你等待ACK0时,如果收到了ACK1,则什么都不干。此时如果是包0错误导致的ACK1,那么也等待Timer超时之后重传。
RDT3.0的性能分析
RDT3.0可以保证系电脑可靠,但是其性能极差。
以 1Gbps 链路、15ms 传播延迟、8000 比特数据包为例。
传输时延:$D_{trans} = \frac{L}{R} = \frac{8000}{10^9} = 8\mu s$
RTT指Round-Trip Time,即从发送端发送这个包开始,到它等到ACK的时间差。由于单程传播延迟是15ms,RTT=30ms
发送端利用率 (U_{sender} = \frac{L/R}{RTT + L/R} = \frac{0.008}{30.008} \approx 0.00027)
若每 30ms 发 1KB 包,吞吐量仅 33KB / 秒,1Gbps的链路,实际传输吞吐量只有33KBps,资源利用率极低。
流水线协议(Pipelined protocols)
为了解决发送就等,等又等半天的问题,需要流水线协议。流水线协议的发送端允许存在多个 “在途(in-flight)” 且未被确认的数据包。
在传输层,有两种流水线协议,回退 N 步(Go-Back-N)和选择重传(Selective Repeat)(与链路层的类似机制逻辑一致)
Go Back N协议
在GBN协议中:
- 发送端最多可在流水线中存在N个未确认的包;
- 接收端发送累积确认(cumulative ack)—— 若有数据包缺失(出现 “gap”),则不确认后续包;
- 发送端只为最老的未确认包维护定时器,超时则重传所有未确认的包。
GBN协议通过维护一个滑窗来实现。如下图。

例如一下面窗口大小为4举例(N=4)。
- 首先依次发送 pkt0、pkt1、pkt2、pkt3;
- pkt2 丢失→接收端收 pkt3 时因序号不匹配(期望 pkt2),丢弃 pkt3 并重发 ack1(累积确认 pkt1 及之前的包)
- 发送端将窗更新到pkt1之后。发送端超时(pkt2 的定时器到期)→重传 pkt2、pkt3、pkt4、pkt5;
- 接收端收到 pkt2 后,按序交付 pkt2、pkt3、pkt4、pkt5,并发送对应的累积 ACK。

GBN协议的发送端状态机如下图所示:

- 当上层调用
rdt_send(data)时,若窗口未满,就创建并发送数据包,更新nextseqnum并启动定时器;若窗口已满,则拒绝数据。 - 若收到损坏的 ACK,忽略;若收到正确的累积 ACK,则更新
send_base,并根据窗口是否已满决定是否停止 / 重启定时器。 - 超时则重传
send_base及之后所有未确认的包。
选择重传
选择重传和GBN一样,维护一个窗口,只不过对每个正确收到的数据包单独确认(individual acknowledgment),同时每个数据包也独立维护Timer;若收到乱序包,接收端会缓存这些包,最终按序交付给上层。发送端仅重传未收到确认的数据包。
GBN协议只要求发送端维护一个窗口,但SR协议要求双方都维护窗口,如下图所示:

对于发送端:
- 上层有数据时:若窗口内有序号可用,就发送数据包。
- 超时(timeout (n)):重传数据包n,并重启定时器。
- 收到 ACK (n) 时:标记数据包n已收到;若n是最小的未确认包序号,则将窗口基址(send_base)推进到下一个未确认的序号。
对于接收端:
- 若数据包n在窗口内(([rcvbase, rcvbase+N-1])):发送 ACK (n);若乱序则缓存,若按序则交付上层并推进窗口到下一个未收到的序号。
- 若数据包n在窗口外(([rcvbase-N, rcvbase-1])):发送 ACK (n)(确认历史包)。
- 其他情况:忽略该数据包。
例如下图这个N=4的例子:
- 发送端依次发送 pkt0、pkt1、pkt2、pkt3;
- pkt2 丢失→接收端收 pkt3 时因乱序缓存,发送 ack3;
- 发送端收 ack0、ack1、ack3 后,继续发送 pkt4、pkt5;
- 发送端 pkt2 超时→重传 pkt2;
- 接收端收到 pkt2 后,将缓存的 pkt2、pkt3、pkt4、pkt5 按序交付上层,发送 ack2;
- 发送端收到 ack2 后,标记 pkt2 已确认,窗口继续推进。由于ACK3 4 5都已经收到过,因此窗口会直接更新到6开始。

RDT中的机制和作用
| Mechanism | Use, Comments |
|---|---|
| Checksum | 用于检测传输数据包中的比特错误。 |
| Timer | 用于数据包的超时重传,可能是因为数据包(或其 ACK)在信道中丢失。由于数据包延迟但未丢失(过早超时)或数据包已被接收方接收但接收方到发送方的 ACK 丢失时也会发生超时,接收方可能会收到数据包的重复副本。 |
| Sequence number | 用于对从发送方流向接收方的数据包进行顺序编号。接收数据包的序号出现间隙时,接收方可检测到丢失的数据包;序号重复时,接收方可检测到数据包的重复副本。 |
| Acknowledgment | 接收方用于告知发送方已正确接收一个或一组数据包。确认通常会携带已确认数据包的序号。确认可以是单独的或累积的,具体取决于协议。 |
| Negative acknowledgment | 接收方用于告知发送方未正确接收数据包。否定确认通常会携带未正确接收数据包的序号。 |
| Window, pipelining | 发送方可能被限制只能发送序号在给定范围内的数据包。通过允许多个未确认的数据包被传输,与停等模式相比,发送方的利用率得以提高。窗口大小可根据接收方接收和缓存消息的能力、网络拥塞程度或两者来设置。 |
面向连接的传输层协议:TCP
引入
与UDP的尽力而为不同,TCP是保障可靠通信的核心协议。其核心特性为:
- 点到点(point-to-point):通信仅在一个发送方和一个接收方之间进行。
- 可靠的有序字节流(reliable, in-order byte stream):
- TCP 将用户消息拆分为分段(segment) 传输,分段大小上限 64KB,常规为 1500 字节(适配以太网 MTU)。
- 接收端会将分段重组为有序的字节流,确保数据无乱序、无丢失。
- 流水线传输(pipelined):通过拥塞控制和流量控制机制动态调整 “发送窗口大小”,支持多分段并发传输。
- 全双工数据(full duplex data):同一连接中支持双向数据传输。
- 面向连接(connection-oriented):通信前需通过 “三次握手” 交换控制消息,建立收发双方的状态。
流控(flow control):防止发送方 “淹没” 接收方,即,保证发送方发送的数据速率不大于接收方接收的速率。
拥塞控制(congestion control):防止网络中间节点(如路由器)因负载过重而拥塞,如果网络中有任意节点负载过重,则会减慢当前传输速率(区别于流控,流控是接收方)。
TCP Header
TCP头由20字节组成,如下图所示。

| 字段 | 功能说明 |
|---|---|
| 源端口 / 目的端口(各16byte) | 标识发送和接收的应用进程,与 IP 地址结合形成套接字(socket)(如IP地址:端口号),唯一标识 TCP 连接。 |
| 序列号(Sequence number)(32byte) | 32 位无符号数,标识分段中第一个字节的序号。初始序列号(ISN)由主机在连接建立时指定,后续分段序列号随数据字节数递增(如第一段 800 字节序号为 1500,下一段序号为 2300)。 |
| 确认号(Acknowledgment number)(32byte) | 仅当 “ACK 标志位” 置 1 时有效,表示期望接收的下一个字节的序号(累积确认)。 |
| 头部长度(4byte) | 标识 TCP 头部的字节数,最大为60byte。 |
| 标志位(URG、ACK、PSH、RST、SYN、FIN) | 用于控制连接状态(SYN = 建立连接、FIN = 关闭连接、RST = 重置连接)、数据传输(PSH = 立即推送数据)、错误处理(ACK = 确认有效),特殊功能(URG = 紧急指针有效)。 |
| 窗口大小 | 接收方告知发送方 “自己还能接收的字节数”,是流量控制的核心字段,在2-65535之间。 |
| 校验和(12-byte) | 用于检测 TCP 分段在传输中的错误。 |
| 紧急指针 | 与 URG 标志配合,标识紧急数据的位置。 |
| 选项 | 可扩展 TCP 功能(如窗口缩放、时间戳等),其长度可变,在0-320byte之间,以32为单位进行改变。 |
| 填充(Padding) | TCP头部的长度必须是32位的整数倍,不足整数倍的,用填充位进行补充。 |
在实际应用中,PSH,URG和紧急指针一般用不到
序列号和确认号
值得一提的是,TCP头中的序列号和确认号代表的是数据字节,而非字段。例如,一个800字节的数据要被传输,传输它的第一位时它的序列号是1500,那么最后一位的序列号就是2300。
接收方告知发送方 “下一个期望接收的字节序号”,是累积确认(即确认号之前的所有字节已正确接收)。
Telnet 的 “回显” 机制:Telnet 是一种远程登录协议,其特性是 “回显(echo-back)”—— 即发送方(如 Host A)发送的每个字符,接收方(如 Host B)都会原样返回一个副本,用于确认字符已被接收。回显的字符会出现在发送方的Terminal上。
举个例子,telnet中使用的就是TCP,我们来看Host A输入字符C时,telnet利用TCP的回显流程:

- Host A 的用户输入字符‘C’,发送的 TCP 分段中:
Seq=42:表示该分段数据部分的第一个字节的序列号是 42(此处数据仅 1 个字符‘C’,所以序列号标识这个字符的位置)。ACK=79:表示 Host A期望从 Host B 收到的下一个字节的序列号是 79(即确认 Host B 之前发送的、序列号≤78 的所有数据已收到)。data = ‘C’:实际传输的字符数据。
- Host B 收到‘C’后,执行 “回显” 逻辑,发送包含‘C’的分段:
Seq=79:表示该分段数据(‘C’)的第一个字节序列号是 79。ACK=43:表示 Host B期望从 Host A 收到的下一个字节的序列号是 43(即确认 Host A 发送的、序列号≤42 的字符‘C’已收到)。data = ‘C’:回显的字符‘C’。
- Host A 没有额外的数据发送(即,无法在发送数据的同时发送ACK),因此单独确认回显的‘C’:
Seq=43:表示该分段(无数据,仅确认)的序列号是 43。ACK=80:表示 Host A期望从 Host B 收到的下一个字节的序列号是 80(即确认 Host B 发送的、序列号≤79 的回显‘C’已收到)。
对于分组网络中乱序到达的segment,TCP 规范未强制统一处理方式,由实现者决定。通常有两种策略:
- 丢弃乱序分段;
- 暂存乱序分段,等待缺失的字节到达后再一起重组。
校验和
同UDP一样,TCP在计算校验和时也使用伪头部。伪头部构成与UDP一致,其中长度表示的是TCP报头不包含数据的长度=20+选项长度(UDP无选项)。
TCP下的可靠数据传输
引入
TCP是建立在IP的不可靠服务上的RDT服务。在TCP中,重传是由超时或重复确认包触发的(即,没有NACK)。
TCP往返时间与传输超时
在提供Reliable Data Transmission时,会遇到两个问题:一是数据错误,二是数据丢失。对于大部分的数据错误,都可以通过校验和来检查(除非错误的数据有很特殊的Error pattern)。而对于数据丢失,则需要超时机制。
如何确定TCP的超时值呢?超时值太短会导致 “不必要的重传”,太长会导致 “丢包后反应过慢”。唯一可以确定的是,超时值需长于 RTT(Round-Trip Time 数据往返传输时间),但RTT是动态变化的。
1. 对RTT进行估计
因此,我们需要尽可能精确地去估计平均RTT,再使用平均RTT来算出一个科学的超时时间。
首先,我们需要样本RTT(SampleRTT),样本RTT指测量从 “分段发送” 到 “收到对应 ACK” 的时间,且忽略重传的分段(避免因重传导致 RTT 估计偏大)。
然后通过样本RTT,计算出估计RTT(EstimatedRTT),通常采用指数加权移动平均(EWMA):
其中,(\alpha) 典型值为 0.125,该公式让 “近期 SampleRTT 的权重更高,历史 SampleRTT 的影响指数级衰减”,使 EstimatedRTT 更 “平滑”

2. 加上安全裕量,计算超时时间
为了应对 RTT 的波动,需引入 “RTT 偏差估计(DevRTT)”:
其中,(\beta) 典型值为 0.25。
最终超时区间公式为:
这个$4 \times \text{DevRTT}$引入的安全裕量(Safety Margin)。
TCP收发方规则
发送方规则
(1)从应用层接收数据(data rcvd from app)
- 为数据创建 TCP 分段,分配序列号(seq #):序列号标识 “分段中第一个字节在字节流中的编号”。
- 若定时器未运行,则启动定时器:定时器针对 “最旧的未确认分段” 设置超时区间(Timeout Interval,即之前讲解的 EstimatedRTT + 4×DevRTT)。
(2)超时事件(timeout):超时事件触发后,重传导致超时的分段并重启定时器。
(3)收到确认(ack rcvd):若确认号覆盖了 “之前未确认的分段”,则更新已确认的字节范围;若更新确认范围后仍有未确认的分段,则重启定时器。
接收方规则
| 接收方事件 | TCP 接收方动作 |
|---|---|
| 期望的segment按序到达,且其他数据已全部确认 | 延迟确认(delayed ACK):最多等待 500ms,若没有下一个分段则发送 ACK |
| 期望的segment按序到达,且有另一个segment的 ACK 待发送 | 立即发送单个ACK,确认这两个按序分段 |
| 收到的segment序列号大于期望收到的(检测到间隙(gap),接收产生了乱序) | 立即发送重复 ACK(duplicate ACK),指示下一个期望字节的序列号,即,gap的序列号 |
| 到达的分段部分或完全填补了间隙,且分段从间隙的低端开始 | 立即发送 ACK |
数个例子

(a)lost ACK scenario(丢失确认场景):Host A 发送Seq=92(8 字节数据),期望 Host B 返回ACK=100(表示已收到前 99 字节,期望下一个字节是 100)。但该 ACK 在传输中丢失,Host A 因 “超时” 重传Seq=92的分段,最终 Host B 返回ACK=100,确认数据接收。
(b)premature timeout(过早超时场景):Host A 先发送Seq=92(8 字节),收到ACK=100;接着发送Seq=100(20 字节),收到ACK=120。但此时若 Host A 的定时器 “过早超时”,会错误重传Seq=92的分段(实际已被确认)。Host B 收到重复分段后,返回ACK=120(再次确认,我已经把你后面的一个都收到了),Host A 更新状态。
(c)cumulative ACK(累积确认场景):Host A 发送Seq=92(8 字节)和Seq=100(20 字节),但Seq=100的分段在传输中丢失。Host B 仅能确认到Seq=92,返回ACK=100。Host A 因超时重传Seq=100及后续数据(如Seq=120的 15 字节),最终 Host B 返回ACK=120,确认所有数据接收。
TCP快速重传
发送方通常会连续发送多个分段。若某一分段丢失,接收方会因 “乱序” 持续发送重复 ACK(如前面提到的 “检测到间隙时发送重复 ACK”)。
当发送方收到 3 个针对同一数据的重复 ACK(triple duplicate ACKs) 时,就会判定对应分段丢失,立即重传序列号最小的未确认分段,而无需等待超时。这样解决了等待超时后传输等待时间过长的问题。
考虑下面这个例子:

Host A 连续发送两个分段:Seq=92(8 字节数据)和Seq=100(20 字节数据)。Seq=100的分段在传输中丢失。Host B 因检测到 “间隙”,持续发送ACK=100(期望接收Seq=100的分段)。当 Host A 收到 3 个重复的ACK=100 后,立即重传Seq=100的分段,而不需要等待超时定时器到期。
TCP的流控
TCP 流控由接收方主动控制发送方的发送速率。其机制通过TCP头中的“窗口通告”实现:接收方通过 TCP 头部的接收窗口(rwnd,receive window) 字段,向发送方通告自己的 “空闲缓冲区大小”。
这个Buffer的容量(RcvBuffer)可以在创建socket的时候设置,一般默认是4096bytes。对于接收方来说,rwnd的值是:
即,接收buffer的容量 - (应用程序已读取的最后一个字节序号 - 接收方已收到的最后一个字节序号)
发送方会将未确认的数据量限制在对方接收窗口之内,来确保对方Buffer不会溢出,即:
TCP连接管理
建立连接:三次握手
TCP 通过 “三次握手” 建立可靠连接,步骤如下:
- 第一步(A→B):客户端 A 发送SYN segment (
SYN标志位1),指定对方端口号,并设置自己分段计数器的初始序列号(ISN) - 第二步(B→A):服务端 B 响应SYN+ACK 分段(也叫 SYNACK),包含自己的 ISN,同时通过ACK确认 A 的 SYN(确认号为 A 的 ISN+1)。
- 第三步(A→B):客户端 A 发送ACK 分段,确认 B 的 SYN(确认号为 B 的 ISN+1),此时
SYN标志位不设置。
在三步握手中,ISN是随机选择的,以避免与先前连接的序列号重叠。同时第三次握手可以携带数据,也可以不携带。

关闭连接:四次挥手
TCP 通过 “四次挥手” 关闭连接,核心逻辑是通信双方各自关闭自己的发送方向:
- 通信任意一方发送FIN 分段(
FIN标志位 = 1),表示 “不再发送数据”。 - 对方回复
ACK,确认收到 FIN。此时表示自己这一方没有数据发送,但是扔能从对方接收数据。 - 如果对方也需关闭,发送一个段给我方,
FIN置1。 - 我方回复
ACK,确认收到服务端的 FIN。

这个四次挥手过程,可以是 “一方先关闭,另一方后关闭”;也可是 “双方同时发送 FIN” 的场景,即,收到 FIN 后,ACK 可与自己的 FIN 合并发送
拥塞控制
引入
拥塞会表现为丢包(路由器缓冲区溢出)或长延迟(路由器buffer在排队)
通常来说,拥塞都是由发送方发现的。因为一旦链路拥塞,接收方可能只是感觉对面没有发东西给我,但是发送方会发现“我发了这么多,ACK呢?”
不妨设想这样一个场景:一个路由器(有限缓冲区),发送方会重传超时的数据包。应用层输入速率$\lambda_{in}$等于输出速率$\lambda_{out}$,但传输层输入速率$\lambda_{in}’(含重传)\geq \lambda_{out}$($\lambda_{in}’$也叫 “提供负载”)。由于拥塞,会触发重传,重传会增加网络负载,若路由器缓冲区满,数据包会被丢弃,进而触发更多重传,形成 “拥塞 - 重传 - 更拥塞” 的循环。
若能精准感知网络状态,发送方仅在路由器有缓冲空间时才发送数据,则可避免拥塞。但实际中无法实现完美感知。
TCP的拥塞控制
在实际中,TCP的拥塞控制有多种机制,而且这还是一个在持续研究的领域。这门课只介绍2种。
控制机制1:加性增大、乘性减小(additive increase multiplicative decrease (AIMD))
这种拥塞控制的核心理念是:发送方通过 “增大窗口→探测可用带宽→丢包后调整” 的逻辑,自主控制传输速率。
- 每经过一轮RTT将拥塞窗口(cwnd,Congestion Window)增加 1 个最大分段大小(maximum segment size,MSS),缓慢提升速率。要加性提升速率的原因是,如果网络中的每个人都快速提升速率,那网络很快又会再度拥塞。
- 发生丢包时,将 cwnd 减半,快速降低速率以缓解拥塞。
在这个机制下,cwnd 随时间呈 “锯齿状” 变化 —— 加性增长直到丢包,然后乘性下降,循环往复,实现 “充分利用带宽又避免拥塞” 的平衡。

与rwnd不同,cwnd不在TCP头中进行维护,而是由发送方的传输层的控制程序维护。如流控一样,发送端会将已发送但未确认的segment数量控制在cwnd之内。其发送速率约为:
TCP控制机制2:慢启动(Slow Start)
在初始时,将cwnd设置为1MSS,每收到一个 ACK,就将 cwnd 增加 1 MSS。这样每个RTT后,cwnd就会翻倍,在第二个RTT后cwnd翻4倍,第三个翻8倍这样指数级增长。慢启动下,启动时MSS小,但是增长非常快。

然而,这样指数级的增长会使得网络快速饱和,因此需要一个阈值来限制它。它的机制是这样的:
- 一旦检测到了timeout(丢包导致),就把阈值(slow start threshold, 或 ssthresh)设置为当前cwnd的一半,然后把cwnd重置成1MSS。在到达阈值之前,保持指数级增长,到达阈值后,变成线性增长。
- 一旦检查到3次重复针对一个数据的ACK,就将cwnd减半,并将其转换成线性增长。(在TCP Tahoe这种早期拥塞控制中,这种情况也给它重置成1MSS)
基于控制机制的控制协议:TCP Tahoe和 TCP Reno
TCP Tahoe:TCP Tahoe 是早期的 TCP 拥塞控制版本,核心特点是对所有丢包事件采用 “激进” 的重置策略,具体机制如下:
- 慢启动(Slow Start):初始拥塞窗口(
cwnd)设为 1 个 MSS(最大分段大小),每收到一个 ACK,cwnd翻倍(指数增长),直到cwnd达到慢启动阈值(ssthresh),进入拥塞避免阶段。 - 拥塞避免(Congestion Avoidance):
cwnd每经过一个 RTT(往返时间)增加 1 个 MSS(线性增长),平稳提升发送速率。
无论丢包是由超时(Timeout)还是3 个重复 ACK(3 Duplicate ACKs)触发,Tahoe 都会进入快速恢复状态(fast recovery),执行以下操作:
- 将
cwnd直接重置为 1 个 MSS; - 将
ssthresh设为丢包前cwnd的一半; - 重新进入慢启动阶段,从 1 开始指数增长。
由于对所有丢包都 “一刀切” 地重置 cwnd,Tahoe 在网络仅出现部分分段丢失(如 3 个重复 ACK 场景)时,会因频繁重启慢启动而浪费带宽,吞吐量表现较差。
TCP Reno:TCP Reno 是对 Tahoe 的改进版本,核心优化是区分丢包类型,对 3 个重复 ACK 采用更温和的 “快速恢复” 策略,具体机制如下:
慢启动 + 拥塞避免:与 Tahoe 逻辑一致(指数增长→线性增长)。
3 个重复 ACK 触发的丢包(快速恢复):
- 将
cwnd减半; - 将
ssthresh设为减半后的cwnd; - 直接进入拥塞避免阶段(线性增长),无需重启慢启动。
- 将
- 超时触发的丢包(快速恢复):
- 与 Tahoe 逻辑一致:
cwnd重置为 1,ssthresh设为丢包前cwnd的一半,重启慢启动。
- 与 Tahoe 逻辑一致:
相较于Tahoe,Reno对 3 个重复 ACK 场景的 “温和处理”,减少了慢启动的频繁触发,显著提升了带宽利用率和吞吐量,是早期 TCP 版本中更高效的实现。

注:上图红色的线第三个threshold存在错误,Tahot机制是减半之后线性增长,会cwnd降低到5,而非1。
这两种控制协议的状态机如下图

TCP均衡(Fairness)
均衡(fairness)指的是:若有K个 TCP 会话共享带宽为R的瓶颈链路,每个会话的平均速率应接近R/K,实现带宽的 “公平分配”。
通过上面介绍的拥塞控制协议,即可实现均衡,这是因为加性增 - 乘性减(AIMD)这种机制让多个 TCP 连接在竞争带宽时,最终会收敛到 “等带宽共享” 的状态(如下图中两条红色轨迹最终趋近于 “equal bandwidth share” 的虚线)。

然而,对于流媒体应用,其常不使用 TCP,因为 TCP 的拥塞控制会 “限制速率”,而多媒体应用希望以稳定速率传输(即使容忍丢包)。这类应用转而使用 UDP:以固定速率发送音视频数据,通过对丢包的容忍性来保障体验。
现在的TCP均衡也有新的问题:应用可在两台主机间建立多个并行 TCP 连接(如网页浏览器为加速加载)。若链路速率为R,9 个已有连接 + 1 个新连接(共 10 个独立连接),理论上每个连接应获得R/10的速率 —— 这是 “单连接公平性” 的理想状态。然而,当应用在两台主机间建立多个并行的 TCP 连接时(比如同一浏览器为加速下载,同时开 11 个 TCP 连接),这些并行连接会被网络视为 “独立连接”,但实际上它们属于同一个应用 / 用户,这就打破了现有的公平性。
其他拥塞控制(简要介绍)
网络层与传输层共担拥塞控制
如果网络层可以实现:
- 流量感知路由(Traffic-aware routing):基于长期流量趋势调整路由,提升整体网络效率(SDN 可高效支持)。
- 准入控制(Admission control):虚拟电路网络中,若新连接会导致拥塞,则拒绝建立。
- 流量抑制(Traffic Throttling):路由器要求源端限制流量,或自身减慢流量传输。
- 流量丢弃(Load shedding):必要时丢弃数据包以缓解拥塞。
则可以实现与传输层一起进行拥塞控制。其中流量感知路由的机理是这样的:
最短路径算法找到的链路可能并不是吞吐量最大的,如果将最短路径算法的cost设置为链路带宽+传播延迟+测量负载(平均排队延迟),可以找到最轻路径。然而,这些链路cost会快速变化,这样会导致路由表的大幅震荡,引发其他问题。
Admission Control(准入控制)
准入控制是从输入端进行控制,从输入上限制住某一用户能使用的最大速率,即与运营商通过“服务合约”,明确流量需求(峰值速率、平均速率、突发大小、延迟 / 丢包要求等)。
网络计算满足需求所需的 “有效带宽”,若接纳该流,则分配资源并保障 QoS(只要源端遵守合约)。
Leaky Bucket Algorithm(漏桶算法)
漏桶算法用于流量监管(Policing)或整形(Shaping),核心机制。
想象有一个桶,桶有固定容量,以恒定速率 “漏水”(类比流量以固定速率输出)。流量以任意速率 “注水”,若桶未满则接纳(合规流量),若溢出则判定为 “非合规流量”。每个虚拟连接或用户对应一个固定容量的桶,漏水速率固定。
漏桶算法可以控制流量的峰值速率和突发大小,确保流量符合网络合约。
流量抑制:显式拥塞通知(Explicit Congestion Notification ECN)
ECN 是一种网络辅助的拥塞控制机制,核心逻辑是让网络主动向端系统传递拥塞信号,其通过 IP 头 “服务类型(ToS)” 字段中的两位标记(ECN 位),指示拥塞状态。
- 当路由器检测到拥塞时,将 IP 数据报的 ECN 位标记为 “拥塞(ECN=1)”;
- 目的主机收到带拥塞标记的 IP 数据报后,在向源主机发送的 TCP ACK 分段中,设置ECE 位(ECN-Echo),通知源主机网络发生了拥塞;
- 源主机收到 ECE 位的通知后,调整发送速率,缓解拥塞。
ECN是End-to-End的拥塞控制,但是对于长网络中,Hop-by-Hop的拥塞控制能更快环节拥塞,但是H2H对上游节点的Buffer资源占用更大。