3 运输层
特别关注因特网协议,即 TCP 和 UDP 运输层协议。
1 概述和运输层服务
-
运输层协议是在端系统中,而不是在路由器中实现的(一般是操作系统的一部分)
-
在发送端,运输层将从发送应用程序进程接收到的报文转换成运输层分组(运输层报文段)
实现的方法(可能)是将应用报文划分为较小的块,并为每块加上一个运输层首部以生成运输层报文段
- 在发送端系统中,运输层将这些报文段传递给网络层,网路层将其封装成网络层分组(即数据报)并向目的地发送
- 每一层只检查对应层的字段,不检查其他层的字段
1.1 运输层和网络层的关系
关系I
- 运输层刚好位于网络层之上
- 网络层提供了主机之间的逻辑通信
- 运输层为运行在不同主机上的进程之间提供了逻辑通信
关系II
运输协议能够提供的服务常常受制于底层网络层协议的服务模型
如果网络层协议无法为主机之间发送的运输层报文段提供时延或带宽保证的话,运输层协议也就无法为进程之间发送的应用程序报文提供时延或带宽保证
但不是绝对的,例子:
-
即使底层网络协议是不可靠的,也就是说网络层协议会使分组丢失、篡改和冗余,运输协议也能为应用程序提供可靠的数据传输服务
-
即使网络层不能保证运输层报文段的机密性,运输协议也能使用加密来确保应用程序报文不被入侵者读取
1.2 因特网网络层简单介绍
- 因特网网络层协议有一个名字叫 IP,即网际协议
- IP 为主机之间提供了逻辑通信
- IP 的服务模型是尽力而为交付服务,但它并不做任何确保
- IP 不确保报文段的交付,不保证报文段的按序交付,不保证报文段中数据的完整性
- IP 被称为不可靠服务
- 每台主机至少有一个网络层地址(IP 地址),在这一章中,每台主机有一个IP地址
1.3 因特网运输层概述
运输层两个协议
- UDP(用户数据报协议):提供不可靠、无连接服务
- TCP(传输控制协议):提供可靠的、面向连接的服务
UDP 和 TCP 的服务模型
两种协议都能提供的(最低限度)服务
- 进程间数据交付:将两个端系统间 IP 的交付服务扩展为运行在端系统上的两个进程之间的交付服务 —— 运输层的多路复用与多路分解
- 差错检查:通过在其报文段首部中包括差错检查字段而提供完整性检查
TCP 提供的额外附加服务
-
可靠数据传输:通过使用流量控制、序号、确认和定时器;确保正确地、按序地将数据从发送进程交付给接收进程
-
拥塞控制:防止任何一条 TCP 连接用过多流量来淹没通信主机之间的链路和交换设备,力求为每个通过一条拥塞网络链路的连接平等地共享网络链路带宽
UDP 流量是不可调节的。使用 UDP 传输的应用程序可以根据其需要以其愿意的任何速率发送数据。
2 多路复用与多路分解
接收主机怎样将一个到达的运输层报文段定向到适当的套接字(多路分解,demultiplexing)
- 在接收端,运输层检查运输层报文段特殊字段,标识出接收套接字,进而将报文段定向到该套接字
发送主机如何处理其上很多进程的发送请求(多路复用,multiplexing)
- 在源主机,从不同套接字中收集数据块,并为每个数据块封装上首部信息(用于分解)从而生成报文段,然后将报文段传递到网络层
运输层的多路复用要求
- 套接字有唯一标识符 —— 源端口号,16比特
- 每个报文段有特殊字段指明要交付到的套接字 —— 目的端口号,16比特
0 ~ 1023 范围的端口号称为周知端口号,是受限制的,保留给诸如 HTTP(端口号 80)和 FTP(端口号 21)之类的周知应用层协议来使用
2.1 无连接的多路复用与多路分解(UDP)
-
s = socket(AF_INET, SOCK_DGRAM)创建一个 UDP 套接字时,运输层自动地为该套接字分配一个范围 1024~65535 内未被使用的一个端口号 -
s.bind(('', 19157))手动给 s 指定一个端口号(源端口号)
如果应用程序开发者所编写的代码实现的是一个 “周知协议” 的服务器端,那么开发 者就必须为其分配一个相应的周知端口号
- 一个 UDP 套接字是由一个二元组全面标识的,该二元组包含一个目的 IP 地址和一个目的端口号
即使源不同,只要目的相同,UDP 也会分解到同一个套接字进行处理
看起来报文段只要有目的地 IP 地址和端口号就行了,源 IP 地址和端口号是干嘛的?
—— 源端口号用作 “返回地址” 的一部分,即当 B 需要回发一个报文段给 A 时,B 到 A 的报文段中的目的端口号便从 A 到 B 的报文段中的源端口号中取值(比如服务器使用 recvfrom() 方法)
2.2 面向连接的多路复用与多路分解(TCP)
-
一个 TCP 套接字是由一个四元组(源 IP 地址,源端口号,目的 IP 地址,目的端口号)来标识的
-
服务端接收到 TCP 连接请求后,要创建一个新的套接字(
accept()方法;用四元组来标识),多路分解时根据四元组值来分解
源端口号相同的两台主机进程此时不会被分解到同一个套接字上,因为其四元组的 IP 地址不同
-
服务器主机可以支持很多并行的 TCP 套接字,每个套接字与一个进程相联系
-
注意:连接套接字与进程之间并非总是有着一一对应的关系
3 无连接运输:UDP
UDP 只是做了运输协议能够做的最少工作。除了复用/分解功能及少量的差错检测外,它几乎没有对 IP 增加别的东西。
UDP 工作概览
-
UDP 从应用进程得到数据,附加上用于多路复用/分解服务的源和目的端口号字段,以及两个其他的小字段,然后将形成的报文段交给网络层
-
网络层将该运输层报文段封装到一个 IP 数据报中,然后尽力而为地尝试将此报文段交付给接收主机
-
如果该报文段到达接收主机,UDP 使用目的端口号将报文段中的数据交付给正确的应用进程
使用 UDP 时,在发送报文段之前,发送方和接收方的运输层实体之间没有握手。正因为如此,UDP 被称为是无连接的
示例:DNS 服务
- 一台主机中的 DNS 应用程序想要进行一次查询
- 它构造了一个 DNS 查询报文并将其交给 UDP
- 主机端的 UDP 为此报文添加首部字段,然后将形成的报文段交给网络层
- 网络层将此 UDP 报文段封装进一个 IP 数据报中,然后将其发送给一个名字服务器
- 查询主机中的 DNS 应用程序等待对该查询的响应
- 如果它没有收到响应(可能是由于底层网络丢失了查询或响应),则要么试图向另一个名字服务器查询,要么通知调用者说无响应
更适合 UDP 的特点
- 关于发送什么数据以及何时发送的应用层控制更为精细:使用 UDP 传输给网络层的数据是立即传输的,而 TCP 有拥塞控制机制,可能会延迟。一些应用可能容忍丢失,但是不容忍延迟。
- 不需要建立连接:UDP 不会引入建立连接的时延(DNS 运行在 UDP 之上的主要原因)
- 不需要维护连接状态:TCP 需要在端系统中维护连接状态,包括接收和发送缓存、 拥塞控制参数以及序号与确认号的参数,UDP 不维护、不跟踪这些参数。因此 UDP 可以支持更多的活跃客户。
- 分组首部短、开销小:每个 TCP 报文段都有 20 字节的首部开销,而 UDP 仅有 8 字节的开销。
虽然目前通常这样做,但在 UDP 之上运行多媒体应用是有争议的。
- UDP 没有拥塞控制。如果每个人都启动流式高比特率视频而不使用任何拥塞控制的话,就会使路由器中有大量的分组溢出。
- 由无控制的 UDP 发送方引入的高丢包率将引起 TCP 发送方大大地减小它们的速率(TCP 拥塞控制的表现形式)。
3.1 UDP 报文段结构

- 长度字段指示了在 UDP 报文段中的字节数(首部加数据)
- 接收方使用检验和来检查在该报文段中是否出现了差错
3.2 UDP 检验和
发送方的 UDP 对报文段中的所有 16 比特字的和进行反码运算,溢出回卷。

在接收方,全部的 16 比特字(包括检验和)加在一起。如果分组没有差错,则显然在接收方处该和将是全 1,如果这些比特之一是 0,说明有差错。
- 虽然 UDP 提供差错检测,但它对差错恢复无能为力
- 如果出现差错,实现方式可能是丢弃,或者是提交受损报文和警告
4 可靠数据传输(rdt)原理
-
仅考虑单向数据传输(双向传输仅是量的差别)
-
协议需要在发送端和接收端两个方向上传输分组,除了交换 含有待传送的数据 的分组之外,rdt 的发送端和接收端还需往返交换控制分组
-
rdt 的发送端和接收端都要通过调用
udt_send()发送分组给对方
udt:不可靠数据传输
4.1 构造可靠数据传输协议
经完全可靠信道的 rdt(1.0)

- 有了完全可靠的信道,接收端就不需要提供任何反馈信息给发送方,因为一定会收到
- 假定接收方接收数据的速率能够与发送方发送数据的速率一样快
经具有比特差错信道的 rdt(2.0)
考虑一种重传机制:
- 确认报文:肯定确认(ACK)和否定确认(NAK)
- 发送方收到确认报文后采取相应措施
基于这样重传机制的可靠数据传输协议称为自动重传请求(ARQ)协议,实现这种协议需要三种服务:
- 差错检测:要求有类似检验和的冗余信息
- 接收方反馈:让接收方提供明确的反馈信息给发送方
- 重传:有差错时发送方重传分组


- 当发送方处于等待 ACK/NAK 状态时,它不能从上层获得更多的数据(阻塞,停等)
- 没有考虑到 ACK/NAK 分组受损的可能性
处理 ACK/NAK 分组受损可能的三种思路:
-
发送方听不清 A/N,要求接收方重传,这种方式非常困难
-
是增加足够的检验和比特,使发送方不仅可以检测差错,还可恢复差错
对于会产生差错但不丢失分组的信道,直接解决问题
- 发送方在收到出错的确认后,重传该分组(假设分组也出错了)
这种方法可能会发送重复的分组,造成冗余分组
接收方不知道他发的 A/N 有没有被发送方收到,因此接收方不知道发送方重传的分组是新的分组还是重传的分组
解决方法是,在数据分组中添加一新字段,让发送方对其数据分组编号,接收方只需要检查序号即可确定收到的分组是否是重传
- 如果是停等协议,1 比特序号就足够了,可以让发送方正常的分组序号为 1,0,1,0……,这样如果接收方看到两个连续相同,就知道是重传
对比 rdt(2.0) 和 rdt(2.1)
对于是否出错,全靠 checksum 来判断。
发送方:
| rdt(2.0) | rdt(2.1) |
|
构造分组:未加序号 等待反馈:
|
构造分组:加入序号 等待反馈:
|

接收方:
| rdt(2.0) | rdt(2.1) |
|
|

rdt(2.2) 的改进
rdt(2.2) 不使用 NAK。
- 让 ACK 携带所确认分组的序号
- 接收方只对正确接收的分组发送 ACK
- 对于出错的分组,重发最近一次的 ACK
发送方:
- 收到期待序号的 ACK:发送下一个分组
- 其他情况(收到出错的 ACK、非期待序号的 ACK):重发当前分组

接收方:
- 收到期待序号(假设是 n)的分组:发送 (ACK, n),交付分组
- 其他情况(收到出错的分组、非期待序号的分组):重发最近一次 ACK

在序号只有 0 和 1 的情况下,接收方甚至收到几号分组就发送 ACK 几
对比 rdt(1.0) 和 rdt(2.2)
rdt(2.2) 新增:
- 分组检错
- 使用 1 比特分组序号
- 使用带序号的 ACK 进行反馈
- 在收到出错分组或非期待序号的分组时,重传分组(发送方和接收方一样)
经具有比特差错的丢包信道的 rdt(3.0)
新假定:除了比特受损外,底层信道还会丢包。
需要关注的问题:
- 怎样检测丢包
- 丢包后该做些什么【rdt(2.2) 的机制对这个问题已够用】
一种检测丢包的方法:
- 让发送方负责检测和恢复丢包工作
- 如果发送方愿意等待足够长的时间以确定分组已丢失,则它只需重传该数据分组即可
要等待多久才能确定丢包了?
- 发送方至少需要等待:即发送方与接收方之间的一个往返时延(可能会包括在中间路由器的缓冲时延)加上接收方处理一个分组所需的时间
- 最坏情况下的最大时延通常是很难估算的,但是我们希望不要等太长时间
- 如果时延 > 等待时间,就可能冗余,但是 rdt(2.2) 已经解决冗余问题
基于时间的重传机制
- 发送方不管是分组丢失,还是 ACK 丢失,还是 ACK 时延过大,都可以重传!
- 启用 “定时器” 机制,发送方 FSM 中需要加入处理定时器中断(timeout 事件)和启动定时器(start_timer)的部分
发送方:

接收方:
rdt(2.2) 的接收方 FSM 不加修改就可以用于 rdt(3.0) 的接收方,因为丢包的处理是发送方做的
4.2 流水线可靠数据传输协议
rdt(3.0) 的性能问题在于它是一个停等协议。
从发送方或者信道的利用率来考虑性能:
$$
U_{sender}=\frac{L/R}{RTT+L/R}
$$

直到 L/R+RTT 时间后,发送方才可以重新利用信道,在 RTT 时间内都不能利用信道,因为发送方必须停下来等待 ACK 的到来。
一个简单解决办法:不以停等方式运行,允许发送方发送多 个分组而无须等待确认。这种技术被称之为流水线(能不能别蹭啊)。
简单对比:

流水线技术需要新增的特性:
- 必须增加分组序号的范围。可能有多个在传输中的未确认报文。
- Sender 和 Receiver 需要缓存。Sender 至少要缓存已发送但是没有确认的分组以便重传。Receiver 至少需要缓存已正确接收的分组。
上面的特性实现取决于协议的设计要求。
解决流水线的差错恢复的基本方法:
- 回退 N 步(Go-Back-N, GBN)
- 选择重传(Selective Repeat, SR)
4.3 回退 N 步(GBN)
GBN 协议允许发送方发送多个分组而不需等待确认,但在流水线中未确认的分组数不能超过某个最大允许数 N。

序号 base,nextseqnum,N- base + 1 三个数将序号范围分割成 4 段。
-
已被发送但还未被确认的分组的许可序号范围可以被看成是一个在序号范围内长度为 N 的窗口
-
随着协议的运行,该窗口在序号空间向前滑动
为什么不允许这些分组为无限制的数目?—— 流量控制,后话
- 在实践中,分组序号是一个分组首部字段,有固定的位数 k,则分组序号的取值范围是 0\sim 2^k-1,所以涉及序号的运算必须是模 2^k 的
此时 FSM 只有一个状态,不如直接用 if-else 来描述,相当于响应事件发生的模型。
GBN 发送方
init: # 初始化
base = 1
nextseqnum = 1
if 上层调用要求发信息: # rdt_send(data)
if 窗口没满: # nextseqnum < base+N
发送信息 # make_pkt() AND udt_send()
if 当前是窗口第一个序号: # nextseqnum == base
启动计时器
移动下一个序号指针 # nextseqnum++
if 发生超时:
for (i in [base, nextseqnum-1]):
重传 msg_pkt[i] # 重传所有已发送但还未被确认过的分组
if 收到ACK n and 没有损坏:
base = n + 1 # 认为 n 及以前的全收到了
if base == nextseqnum:
停止定时器 # 当前窗口全部确认完毕了
else:
启动定时器 # ?
if 收到ACK and 损坏了:
pass
GBN 接收方
init:
expected_seq_num = 1
snd_pkt = mk_pkt(0, ACK, checksum)
if 收到信息 and 没损坏 and 收到的分组序号 == expected_seq_num:
使用数据
发送 (expected_seq_num, ACK, checksum)
expected_seq_num++
if 收到信息 and 其他不对情况:
发送 (expected_seq_num, ACK, checksum) # 这是之前的 ACK 信息
所以接收方保证发出第 n 个 ACK 时前面的都已收到(累计确认)。
GBN 接收方丢弃任何失序分组
- 优点是,接收方不需要缓存任何失序分组
- 缺点是,丢弃的可能是正确的,后面重传可能出错,可能因此需要更多重传
4.4 选择重传(SR)
GBN 本身存在一些性能问题,尤其是当窗口长度和带宽-时延积都很大时。
- 单个分组的差错就能够引起 GBN 重传大量分组,许多分组根本没有必要重传
- 随着信道差错率的增加,流水线可能会被这些不必要重传的分组所充斥
SR 协议的要点是:发送方仅重传它认为出错(未收到ACK)的分组,以避免不必要的重传
为此,需要做到:
- 接收端需缓存失序的分组
- 接收端需对每个正确收到的分组单独确认(选择确认,不使用累积确认)
- 发送的每个分组需要一个定时器,以便被单独重发
特点:
- 出错后重传代价小,发送端使用较多定时器,接收端需要较大缓存,实现复杂
SR 发送方事件与动作
- 从上层收到数据。和 GBN 一样,看窗口是否满。
- 超时。现在每个分组拥有其自己的逻辑定时器,因为超时发生后只能发送一个分组。
- 收到ACK。如果收到 ACK,若该分组序号在窗口内,则标记为已接收。 如果该分组的序号等于 send_base,则窗口基序号向前移动到具有最小序号的未确认分组处。如果窗口移动了,则发送窗口内的未发送分组。
SR 接收方事件与动作
此时,接收方也有一个窗口 N。

- 正确收到窗口内的分组。收到的分组落在接收方的窗口内,发送一个选择 ACK。如果该分组以前没收到过,则缓存该分组。如果该分组的序号等于 rcv_base,则该分组和其后连续的已缓存分组被交付,然后窗口滑动。
-
正确收到 rcv_base 过去 N 步的分组。如果接收方不确认该分组,则发送方窗口将永远不能向前滑动,因为可能发回去的 ACK 丢失了,但是接收方窗口已经滑动了。发送方和接收方的窗口并不总是一致!
-
其他情况。忽略该分组。
SR 窗口长度的限制

对于 SR 协议,窗口长度 \le\frac12 序号空间大小
4.5 总结

4.6 下层信道模型的一个遗留假设
可能产生比特错误、丢包和分组重排序的下层信道:
- 后发的分组可能早于先发的分组到达
- 当连接两端的信道是一个网络时,可能发生重排序
新的问题:
- 一个很老的分组可能到达接收端,并且其序号刚好落在当前接收窗口内(序号重用引起的问题)
解决方法:
- 假设分组在网络中的 “存活” 时间不超过某个固定值
- 使用很长的序号,确保序号回绕时间超过该固定值
5 面向连接的运输:TCP
5.1 Overview
- 服务模型:连接一对通信进程的、理想的字节流管道
- 点到点通信:涉及一对通信进程(不能多播)
- 全双工:可以同时双向传输数据
- 可靠、有序的字节流:不保留报文边界
需要的机制:
- 建立连接:通信双方为本次通信建立数据传输所需的状态(套接字、缓存、变量等)
- 流水式发送报文段:可靠数据传输
- 流量控制:通过调整发送速率,不使接收方缓存溢出
需要知道的事实:
- TCP 的 “连接” 是一个逻辑连接,只运行在端系统上,中间路由器只能看到数据报,看不到连接
-
TCP 在建立连接时进行 “三次握手”,设置一些数据(包括缓存)
-
TCP 可从缓存中取出并放入报文段中的数据数量受限于最大报文段长度(MSS, Maximum Segment Size)
- MSS 是指在报文段里应用层数据的最大长度,而不是指包括首部的 TCP 报文段的最大长度

5.2 TCP 报文段结构
- 当 TCP 发送一个大文件,通常是将该文件划分成长度为 MSS 的若干块(最后一块除外,它通常小于 MSS)
- TCP 的首部一般是 20 字节,UDP 的一般是 8 字节

- 序号和确认号:用于实现 rdt
- 接收窗口:用于流量控制,后文会讨论
- 首部长度:由于 TCP 选项字段的原因,TCP 首部的长度是可变的。通常,选项字段为空,所以 TCP 首部的典型长度是 20 字节
序号和确认号
- 序号是建立在传送的字节流之上,而不是建立在传送的报文段的序列之上。
比如:数据流 500 000 字节,MSS = 1000 字节,则 TCP 将为该数据流构建 500 个报文段。给第一个报文段分配序号 0,第二个报文段分配序号 1000,第三个报文段分配序号 2000……
-
主机 A 填充进报文段的确认号是主机 A 期望从主机 B 收到的下一字节的序号。(因为 TCP 是全双工的)
-
TCP 只确认流中至第一个丢失字节为止的字节,所以 TCP 被称为提供累计确认。
比如,A 收到了 B 的 0~535 和 900~1000 的报文段,在 A 收到 536 之前,其发给 B 的报文段中确认号就是 536
- 收到失序报文段:这一问题留给实现 TCP 的编程人员去处理。
①接收方立即丢弃失序报文段(这可以简化接收方的设计)
②接收方保留失序的字节,并等待缺少的字节以填补该间隔。显然,后一种选择对 网络带宽而言更为有效,是实践中采用的方法。
- 一条 TCP 连接的双方均可随机地选择初始序号。这样做可以减少将那些仍在网络中存在的来自两台主机之间先前已终止的连接的报文段,误认为是后来这两台主机之间新建连接所产生的有效报文段的可能性(它碰巧与旧连接使用了相同的端口号)

- 回显报文段:通过在确认号字段中填入 43,服务器告诉客户它已经成功地收到字节 42 及以前的所有字节,现在正等待着字节 43 的出现。该报文段的第二个目的是回显字符 C
- 即使报文段里没有数据还仍有序号。这是因为 TCP 存在序号字段,报文段需要填入某个序号
5.3 往返时间估计与超时
超时间隔必须大于该连接的往返时间(RTT),即从一个报文段发出到它被确认的时间。否则会造成不必要的重传。
- 这个时间间隔到底应该是多大呢?
- 刚开始时应如何估计往返时间呢?
- 是否应该为所有未确认的报文段各设一个定时器?
估计往返时间
TCP 是如何估计往返时间的
报文段的样本RTT(SampleRTT)= t(收到确认) - t(报文段交给IP发出)
-
大多数 TCP 仅在某个时刻做一次 SampleRTT 测量
-
TCP 不为重传报文段测量 SampleRTT
原因:TCP是对接收到的数据而不是对携带数据的报文段进行确认,因此TCP的确认是有二义性的,对重传报文段的RTT估计不准确
- TCP 维持一个 SampleRTT 均值(称为 EstimatedRTT),一旦获得一个新 SRTT,TCP 就根据下列公式更新 ERTT:
- \alpha 的推荐值是 1/8;这是一个指数加权移动平均(EWMA)
- RTT 偏差:DevRTT,用于估算 SampleRTT 一般会偏离 EstimatedRTT 的程度:
- \beta 的推荐值是 1/4
设置和管理重传超时间隔(Timeout)
- Timeout > EstimatedRTT
- 但是不能大于太多,否则时延大
- Timeout = EstimatedRTT + 4×DevRTT
- 推荐的初始 Timeout = 1秒
- 出现超时后 Timeout 值将加倍,以免即将被确认的后继报文段过早出现超时
- 只要收到报文段并更新 EstimatedRTT,就使用上述公式再次计算 Timeout
5.4 可靠数据传输
- IP 不保证数据报的交付,不保证数据报的按序交付,也不保证数据报中数据的完整性
- TCP 在 IP 不可靠的尽力而为服务之上创建了一种可靠数据传输服务
- TCP 推荐的定时器管理过程只使用单一重传定时器,即使有多个已发送但是还未被确认的报文段
一个高度简化的 TCP 协议
接收方:
- 仅在正确、按序收到报文段后,更新确认序号;其余情况,重复前一次的确认序号(与GBN类似,使用累积确认)
- 缓存失序的报文段(与SR类似)
发送方:
- 流水式发送报文段
- 仅对最早未确认的报文段使用一个重传定时器(与GBN类似)
- 仅在超时后重发最早未确认的报文段(与SR类似,接收端缓存了失序的报文段)
发送方

- TCP 采用累计确认,y 确认了在 y 之前的所有字节都已收到;如果 y>SendBase,说明 ACK 在确认先前未被确认的报文段


- 某种形式的拥塞控制:定时器过期很可能是由网络拥塞引起的,即太多的分组到达源与目的地之间路径上的一台(或多台)路由器的队列中,造成分组丢失或长时间的排队时延。在拥塞的时候,如果源持续重传分组,会使拥塞更加严重。所以超时后,Timeout 加倍(指数增长)。
- TCP 减少重传的机制总结:①只使用一个定时器,避免超时设置过小时重发大量报文,②利用流水式发送和累积确认,避免重发某些丢失了 ACK 的报文段
- 重传:Timeout 翻倍;不重传:计算 SRTT,ERTT,DRTT(Timeout)
接收方
理论上说,接收端只需区分两种情况:
- 收到期待序号的报文段:发送更新的确认序号
- 其它情况:重复当前确认序号
为减小通信量,TCP允许接收端推迟确认:
- 接收端可以在收到若干个报文段后,发送一个累积确认的报文段
推迟确认的缺点:
- 若延迟太大,会导致不必要的重传
- 推迟确认造成RTT估计不准确
TCP 协议规定:
- 推迟确认的时间最多为 500 ms
- 接收方至少每隔一个报文段使用正常方式进行确认

快速重传
- 仅靠超时重发丢失的报文段,恢复太慢!
- 发送方可以在 Timeout 发生之前检测冗余的 ACK 来重传
- 冗余 ACK 就是再次确认某个报文段的 ACK,而发送方先前已经收到对该报文段的确认。
为什么会出现冗余ACK?
- 发送方通常连续发送许多报文段
- 若报文段丢失,会有许多重复ACK发生
- 多数情况下IP按序交付分组,重复ACK极有可能因丢包产生
协议规定
如果 TCP 发送方接收到对相同数据的 3 个冗余 ACK,它把这当作一种指示,说明跟在这个已被确认过 3 次的报文段之后的报文段已经丢失,发送方此时在定时器到期之前就重传丢失的报文段

对于有快速重传的 TCP,发送方的收到 ACK 事件更改为:

5.5 TCP 流量控制
为什么GBN或SR不考虑流量控制?
GBN和SR均假设:
- 正确、按序到达的分组被立即交付给上层
- 其占用的缓冲区被立即释放
从而,GBN和SR不会出现接收端缓存溢出的问题
为什么UDP没有流量控制?
UDP不保证交付:
- 接收端UDP将收到的报文载荷放入接收缓存
- 应用进程每次从接收缓存中读取一个完整的报文载荷
- 当应用进程消费数据不够快时,接收缓存溢出,报文数据丢失,并不违反UDP的服务承诺
- TCP 为它的应用程序提供了流量控制服务以消除发送方使接收方缓存溢岀的可能性
- 流量控制是一个速度匹配服务,即发送方的发送速率与接收方应用程序的读取速率相匹配
- TCP 发送方也可能因为 IP 网络的拥塞而被遏制;这种形式的发送方的控制被称为拥塞控制
本节假设 TCP 接收方会丢弃失序报文段
- TCP 流量控制的方式:让发送方维护一个名叫接收窗口(rwnd)的变量
- 接收窗口用于给发送方一个指示一一该接收方还有多少可用的缓存空间
- TCP 是全双工通信,在连接两端的发送方都各自维护一个接收窗口
以文件传输为例
A 通过 TCP 向 B 发送一个大文件
- B 为此连接分配一个大小为 RcvBuffer 的接收缓存
- LastByteRead:B 上应用从缓存读出的数据流最后一个字节编号
- LastByteRcvd:放入 B 缓存数据流的最后一个字节
TCP 不允许缓存溢出,即: $$ \text{LastByteRcvd}-\text{LastByteRead}\le\text{RcvBuffer} $$ 接收窗口根据缓存可用空间来设置:
当前缓存已用:[\text{LastByteRcvd}-\text{LastByteRead}]
接收窗口定义:\text{rwnd}=\text{RcvBuffer}-[\text{LastByteRcvd}-\text{LastByteRead}]

A 跟踪的变量:
- LastByteSent
- LastByteAcked
- LastByteSent - LastByteAcked = A 已发送但是还未被确认的数据量
保证 A 不使 B 的接收缓存溢出: $$ \text{LastByteSent}-\text{LastByteAcked}\le\text{rwnd} $$
零窗口通告和零窗口探测
如果 rwnd = 0,告诉 A 后,B 有空间了但是不告诉 A,A 就被阻塞了,不能继续发送
发送端收到 rwnd = 0 的报文段(零窗口通告)时,必须停止发送,然后:
- 发送端启动一个定时器
- 定时器超时后,发送端发送一个零窗口探测报文段(序号为上一个段中最后一个字节的序号)
- 接收端在响应的报文段中通告当前的接收窗口
- 若发送端仍收到零窗口通告,重新启动定时器
糊涂窗口综合症(silly window syndrome)
当数据发送很快、而消费很慢时,零窗口探测的简单实现带来以下问题:
- 接收方不断发送微小窗口通告
- 发送方不断发送很小的数据分组
- 大量带宽被浪费
接收方启发式策略
接收端避免糊涂窗口综合症的策略:
- 通告零窗口之后,仅当窗口大小显著增加之后才发送更新的窗口通告
- 什么是显著增加:窗口大小达到缓存空间的一半或者一个 MSS,取两者的较小值
TCP 执行该策略的做法:
- 当窗口大小不满足以上策略时,推迟发送确认(但最多推迟 500 ms,且至少每隔一个报文段使用正常方式进行确认),寄希望于推迟间隔内有更多数据被消费
- 仅当窗口大小满足以上策略时,再通告新的窗口大小
发送方启发式策略
发送方避免糊涂窗口综合症的策略:
- 发送方应积聚足够多的数据再发送,以防止发送太短的报文段
问题:发送方应等待多长时间?
- 如等待时间不够,报文段会太短
- 如等待时间过久,应用程序的时延会太长
- 更重要的是,TCP 不知道应用程序会不会在最近的将来生成更多的数据
Nagle 算法
Nagle算法很好地解决了该问题:
- 在新建连接上,当应用数据到来时,组成一个TCP段发送(哪怕只有一个字节)
- 在收到确认之前,后续到来的数据放在发送缓存中
- 当数据量达到一个MSS或上一次传输的确认到来(取两者的较小时间),将缓存中的数据发走
Nagle算法的优点:
- 适应网络延时、MSS长度及应用速度的各种组合
- 常规情况下不会降低网络的吞吐量
5.6 TCP 连接管理
如何建立和拆除一条 TCP 连接?
建立
建立连接要确定两件事:
- 双方都同意建立连接(知晓另一方想建立连接)
- 初始化连接参数(序号,MSS等)


如何选择起始序号?
为什么起始序号不从1开始?
- 若每个新建连接都从序号1开始,那么在不同时间、同一对套接字之间建立的连接,它们的握手报文段都一样,且旧连接上的报文段可能会干扰新连接上的传输(报文段序号有重叠)
可以随机选取起始序号吗?
- 新、旧连接上报文段序号重叠的可能性将大为减小,但不能完全避免
必须避免新、旧连接上的序号产生重叠!
TCP起始序号的选取
基于时钟的起始序号选取算法:
- 每个主机使用一个时钟,以二进制计数器的形式工作,每隔 \Delta T 时间计数器 +1
- 新建一个连接时,以计数器值低 32 位作为起始序号
- 该方法确保连接的起始序号随时间单调增长
取较小的 \Delta T,确保起始序号的增长速度超过 TCP 连接上序号的增长速度
使用较长的序号(32位),确保序号回绕的时间远大于分组在网络中的最长寿命
序号消耗特例说明
- SYN 段不携带任何数据,但它要消耗一个序号
- SYN+ACK 段不携带任何数据,但它要消耗一个序号
- ACK 段如果不携带数据就不消耗序号
- 释放连接 FIN 消耗序号与 SYN 相同
关闭

拆除连接方式
不对称方式:任何一方都可以关闭双向连接,存在数据丢失的危险
对称方式:每个方向的连接单独关闭,双方都执行DISCONNECT 才能关闭整条连接
由于两军问题的存在,可以证明不存在安全的通过N次握手实现对称式连接释放的方法
但是在实际的通信过程中,使用三次握手 + 定时器的方法释放连接在绝大多数情况下是成功的
5.7 拥塞控制
网络拥塞:
- 起因:大量分组短时间内进入网络,超出网络的处理能力
- 表现:网络吞吐量下降,分组延迟增大
- 措施:减少分组进入网络(拥塞控制)
流量控制与拥塞控制:
- 流量控制:限制发送速度,使不超过接收端的处理能力
- 拥塞控制:限制发送速度,使不超过网络的处理能力