1. 互联网应用的基石:DNS 与传输层
1.1 DNS:互联网的"电话簿"
在深入传输层之前,我们首先需要理解一个关键的应用层协议:DNS(域名系统)。
1.1.1 为什么我们需要DNS?
计算机在网络中使用IP地址(例如 172.217.14.228)来相互识别和通信。但对于人类来说,记住一长串数字极其困难。我们更擅长记住像 google.com 这样的人类可读的域名。
DNS协议的核心任务就是扮演一个翻译官,将这些人类可读的域名转换为机器可读的IP地址。
1.1.2 DNS的演进与设计
-
早期历史:最早,互联网的"电话簿"是一张实际的纸。随后演变为一个放在互联网上的中央文件。但随着网络规模的扩大,这种方式很快变得难以维护和臃肿。
-
现代设计:最终的DNS提案设计了一个可扩展、高可用的分布式系统。
-
核心目标:
-
可拓展性:能够应对互联网的爆炸式增长。
-
高可用性:DNS服务不应该崩溃,需要始终在线。
-
轻量与快速:互联网上的几乎每一个连接都涉及DNS查询。
-
-
工作机制:DNS的基本结构基于"名称服务器"。当一台名称服务器无法回答你的请求时,它会重定向你到其他能回答请求的服务器。
2. 传输层原理:在不可靠上构建可靠
2.1 网络层的挑战
互联网的第三层(网络层,IP层)只提供"尽力而为"(Best-Effort)的服务。这意味着数据包可能会丢失、被丢弃、被复制,甚至重新排序。网络层本身是不可靠的。
传输层(第四层)的使命就是弥补这一差距。它在终端主机上实现,为上层应用程序提供它们需要的服务。
2.2 传输层的核心目标
-
提供抽象:让应用程序开发者从"连接"的角度思考,而不是"单个数据包"。
-
解复用(Demultiplexing):一台计算机可以同时运行多个应用程序(如浏览器、邮件客户端)。当数据包到达时,传输层需要知道该把数据包交给哪个程序。
-
实现控制:实现流量控制和拥塞控制。
2.3 端口与解复用
传输层通过端口号来实现解复用。端口号是一个16位的数字(范围 0-65535)。当数据包到达时,操作系统中的传输层会检查其头部中的"目标端口号",并将数据包交给绑定到该端口的特定应用程序。
-
知名端口(0-1023)通常保留给标准服务(如HTTP的80,HTTPS的443)。
-
1024-65535 范围的端口可供应用程序随机选择使用。
2.4 两种选择:UDP vs. TCP
传输层提供了两种主要的协议选择:
| 特性 | UDP (用户数据报协议) | TCP (传输控制协议) |
|---|---|---|
| 可靠性 | 不提供。 | 提供可靠的、按顺序的字节流。 |
| 消息边界 | 消息限于单个数据包。 | 抽象为无限长的字节流管道。 |
| 控制 | 不提供拥塞控制或流量控制。 | 提供流量控制和拥塞控制。 |
| 使用场景 | 速度优先、能容忍丢包(如DNS、视频流、在线游戏)。 | 必须保证数据完整(如网页浏览、文件传输、电子邮件)。 |
可靠性的定义:"可靠"意味着数据包必须至少被交付一次,且不能损坏。协议必须能够检测并报告发送失败,而不是静默地丢弃数据。
3. TCP 深度解析:构建可靠字节流
3.1 字节流抽象与数据包
TCP向应用程序提供了"字节流"的抽象。应用程序可以写入一个没有长度限制的字节流,TCP负责将其"打包"。
-
TCP收集足够的字节来形成一个数据包。
-
它在数据顶部添加一个TCP头,然后交给IP层发送。
-
接收方TCP处理数据包后,将相应的字节放入接收方的字节流中。
MTU与MSS:
-
MTU (最大传输单元):IP层所能承载的最大数据包大小,包括IP头和TCP头。
-
MSS (最大分段大小):TCP负载(Payload)的最大长度。$MSS = MTU - IP头 - TCP头$。
3.2 序列号:实现有序和可靠
TCP的核心机制之一是序列号(Sequence Number)。
-
作用:确保字节流的顺序和可靠性。
-
ISN (初始序列号):TCP连接不从0开始编号,而是选择一个随机的初始序列号 (ISN)。这主要是出于安全原因,使攻击者更难注入伪造的数据包。
-
确认号 (ACK):确认号是期望接收到的下一个字节的序列号。
3.3 连接管理
TCP是面向连接的,这意味着发送方和接收方在通信期间都需要维护状态。
3.3.1 维护状态
-
发送方:必须记住哪些包已发送、哪些未发送、哪些收到了确认;还需要为已发送但未确认的包维护计时器。
-
接收方:必须记住所有乱序到达的数据包,并将它们暂时缓存,等待缺失的数据包到达。
3.3.2 全双工通信
TCP连接是全双工的,数据可以在两个方向上同时传输。这可以理解为在A到B和B到A两个方向上同时运行着两个字节流管道。
3.3.3 结束连接 (FIN vs. RST)
-
FIN (Finish):正常关闭。
-
一方发送
FIN包,意思是:“我不会再发送任何数据了,但如果你还要发送,我会继续接收”。 -
另一方确认此
FIN。 -
当另一方也准备好关闭时,它也会发送自己的
FIN包并等待确认。
-
-
RST (Reset):异常中断。
-
发送
RST包,意思是:“我不会再发送或接收任何信息”。 -
此数据包不必被确认。
-
主机可以在发送
RST后立刻断开连接。 -
这通常在主机遇到严重错误或崩溃时使用,任何正在传输的数据都会丢失。
-
4. 流量控制:防止接收方过载
4.1 为什么需要流量控制
流量控制是一个端到端的问题,用于解决发送方速率快于接收方处理速率的问题。
如果两个数据包同时到达,路由器或接收主机的缓冲区无法同时处理它们。一个包会被放入队列,导致延迟。如果队列已满,数据包将被丢弃。
4.2 滑动窗口机制
TCP使用滑动窗口来实现流量控制。
-
滑动窗口:定义了在任何给定时间可以传输(即已发送但未确认)的数据包数量(或字节数)。
-
广告窗口 (RWND):接收方在TCP头中设置一个"广告窗口"字段,明确告诉发送方自己还有多少可用的缓冲区空间。
-
机制:发送方的滑动窗口大小不能超过接收方通告的"广告窗口" (RWND)。这确保了发送方不会发送超过接收方处理能力的数据,从而防止接收方过载。
5. 拥塞控制:全局网络效率的核心
如果说流量控制是防止"接收方"过载,那么拥塞控制就是防止"网络"本身过载。
5.1 拥塞控制的设计目标
这是一个全局问题,因为路径上的资源(如路由器队列)被多个连接共享。
-
避免堵塞:防止数据包延迟和丢弃。
-
保证效率:不要浪费带宽。
-
公平分配:公平分配带宽给共享链路的每个连接。
5.2 主机侧拥塞控制
大多数拥塞控制(如TCP使用的)是基于主机的。路由器只管转发数据包,主机通过线索来检测和调整速率。
5.2.1 检测拥塞
主机没有网络的全局视图,只能通过"线索"推断拥塞。
-
基于丢失 (TCP 使用):
-
线索:收到重复的ACK,或计时器超时。
-
逻辑:数据包丢失 = 网络出现拥塞。
-
缺点:丢包不一定等于拥塞(也可能是无线错误);且丢包通常发生在网络_已经_拥塞时,存在滞后性。
-
-
基于延迟:
-
线索:测量数据包的RTT(往返时间),如果RTT开始增加,则认为出现拥塞。
-
缺点:RTT变化也可能由路由改变引起;测量延迟也不总是准确。
-
5.2.2 AIMD:公平与效率的博弈
TCP采用AIMD(加法增加,乘法减少)策略来实现公平和效率。
-
加法增加 (AI):网络畅通时,每个RTT(往返时间)将发送速率(窗口)增加一个固定的量(例如
+1)。 -
乘法减少 (MD):检测到拥塞(丢包)时,将速率(窗口)减半(例如
/2)。
AIMD算法的美妙之处在于,当多个连接共享一个瓶颈时,它们的"乘法减少"会使彼此的速率差距缩小,最终趋于公平。
5.2.3 TCP 拥塞控制的状态机
TCP的拥塞控制不只是AIMD,它是一个更复杂的状态机(以 TCP New Reno 为例):
-
慢启动 (Slow Start)
-
目标:快速找到网络的可用带宽。
-
行为:以一个很小的速率开始(CWND = 1)。每收到一个ACK,
CWND + 1。 -
效果:速率(CWND)每RTT翻倍,呈指数增长。
-
退出:直到CWND达到
SSTHRESH(慢启动阈值)或检测到丢包。
-
-
拥塞避免 (Congestion Avoidance)
-
目标:进入慢启动阈值后,缓慢探测更多带宽。
-
行为:进入AIMD的"加法增加"阶段。
CWND = CWND + 1/CWND。 -
效果:速率(CWND)每RTT + 1,呈线性增长。
-
-
快速重传与快速恢复 (Fast Retransmit / Fast Recovery)
-
情景:当收到3个重复的ACK时。
-
逻辑:这表明有一个包丢失了,但后续的包(2、3、4)成功到达了。网络只是"堵塞",而不是"崩溃"。
-
行为:
-
立即重发丢失的数据包(不等待超时)。
-
SSTHRESH = CWND / 2。 -
CWND = (CWND / 2) + 3(减半并加上3个重复ACK代表的数据包)。 -
(New Reno 改进):之后每收到一个重复ACK,
CWND + 1,保持数据包在网络中传输,直到收到新数据的ACK,才恢复到"拥塞避免"状态。
-
-
-
超时 (Timeout)
-
情景:计时器超时,一个包彻底丢失,连重复ACK都没收到。
-
逻辑:这被视为严重拥塞。
-
行为:
-
SSTHRESH = CWND / 2。 -
CWND = 1。 -
重新进入慢启动模式。
-
-
# TCP New Reno 拥塞控制逻辑(简化伪代码)
def on_new_ack_received():
if state == "Slow Start":
CWND += 1
if CWND >= SSTHRESH:
state = "Congestion Avoidance"
elif state == "Congestion Avoidance":
CWND += 1.0 / CWND
elif state == "Fast Recovery":
CWND = SSTHRESH # 恢复
state = "Congestion Avoidance"
reset_timer()
def on_duplicate_ack_received():
duplicate_count += 1
if duplicate_count == 3:
# 快速重传
retransmit_missing_packet()
state = "Fast Recovery"
SSTHRESH = CWND / 2
CWND = SSTHRESH + 3
elif state == "Fast Recovery":
# 保持数据包在传输
CWND += 1
def on_timer_timeout():
# 严重拥塞
SSTHRESH = CWND / 2
CWND = 1
state = "Slow Start"
retransmit_missing_packet()
5.3 路由器辅助拥塞控制
虽然TCP是基于主机的,但路由器也可以提供帮助。
-
公平队列 (Fair Queuing):
-
路由器可以管理多个TCP流,找出每个流的公平份额并分配。
-
它需要跟踪不同的队列,并通过轮询等方式公平地发送数据包。
-
优点是队列被隔离,作弊的流也无法抢占过多带宽。
-
缺点是实现复杂,且公平分配不一定能解决拥塞。
-
-
ECN (显式拥塞通知):
-
路由器不丢弃数据包,而是在IP头中设置一个"拥塞"标志位(ECN位)。
-
主机收到带ECN标记的包后,主动降低速率,就像发生了丢包一样。
-
优点是更早地警告拥塞(在丢包前),实现简单(只占一个比特)。
-
缺点是普及率低,大多路由器不支持。
-
5.4 拥塞控制的挑战与问题
-
缓冲膨胀 (Bufferbloat)
-
现代路由器有非常大的队列(缓冲区)。
-
拥塞发生时,数据包不会立即丢失,而是在队列中长时间排队,导致高延迟。
-
TCP基于"丢包"来检测拥塞,因此它无法检测到这种"高延迟"状态,会持续发送数据,直到队列最终溢出并丢包。
-
-
短连接问题
-
许多Web连接可能只需要发送少量数据包(少于10个)。
-
TCP在"慢启动"阶段就以很慢的速率把包发完了。
-
如果包的数量不足3个,甚至无法触发快速恢复。
-
-
作弊的TCP
-
TCP拥塞控制是一个君子协议,它依赖_每个人_都遵守规则。
-
恶意用户可以篡改自己的TCP算法(例如,不执行乘法减少,或者打开更多连接),从而获得不公平的更多带宽。
-
5.5 TCP吞吐量方程
我们可以根据网络的两个关键属性来估计TCP(在拥塞避免阶段)的发送速率:
-
$RTT$:往返时间
-
$p$:丢包率
假设拥塞窗口从 $W_{max}/2$ 线性增长到 $W_{max}$ 时发生一次丢包。我们可以推导出简化的TCP吞吐量(Throughput)方程:
$$\text{Throughput} \approx \frac{\text{MSS}}{RTT} \times \sqrt{\frac{3}{2p}}$$
这个方程告诉我们,吞吐量与RTT成反比,与丢包率 $p$ 的平方根成反比。
6. 结论
从DNS的地址翻译,到UDP的"尽力而为",再到TCP复杂的可靠性、流量控制和拥塞控制机制,互联网的传输层是建立在不可靠的网络基础上的一套精妙的工程奇迹。
TCP的拥塞控制本质上是一个分布式的、去中心化的算法,它试图在没有全局视图的情况下,让数以亿计的设备在共享资源(带宽)时,动态地在"效率"和"公平"之间寻找平衡。虽然它面临着缓冲膨胀、短连接和作弊等挑战,但正是这套机制,支撑了过去几十年来互联网的稳定运行和规模扩张。