目录 ← 首页
CS168

第二层路由(STP)

使用以太网的第二层网络

到目前为止,我们展示的第二层协议都是在单条链路上运行,有多台计算机连接到该链路。但我们也可以引入多条链路,完全使用第二层来构建网络。数据包可以被转发,机器甚至可以运行路由协议,所有这些都完全使用第二层的 MAC 地址。

我们在 IP 层运行的路由协议也可以在第二层工作,但有一个缺点是我们无法聚合 MAC 地址。IP 地址是基于地理分配的,而 MAC 地址是基于制造商分配的,因此没有明确的聚合方式。这个缺点就是为什么我们不能仅用第二层来构建全球互联网。

如果在单个本地网络中有多条链路,我们必须确保当有人广播消息时,第二层的所有交换机都要将数据包从所有出端口转发出去。

在有多条链路的第二层网络中,组播会变得更加复杂。这需要额外的协议,我们将在后面的 “特殊主题” 部分讨论。

局域网中组播有用的一个例子是 Bonjour/mDNS,这是苹果公司开发的协议。在该协议中,所有苹果设备(如 iPhone、iPad、Apple TV)都被硬编码加入本地网络上的一个特殊组。如果你的 iPhone 想寻找附近可以播放音乐的设备(如 Apple TV、苹果音箱或 HomePod 等),iPhone 可以向该组发送组播消息,询问是否有设备可以播放音乐。组内的设备也可以发送组播响应,比如 “我是 Apple TV,我可以播放音乐”。有趣的是,该协议实际上还在组播组中使用 DNS 来发送 SRV 记录,该记录将每台机器与其功能进行映射。

历史注释:在现代互联网中,我们说 “路由器” 和 “交换机” 这两个术语是可以互换的。现在有了第二层网络的概念,我们可以说交换机只在第一层和第二层运行,而路由器则在第一层、第二层和第三层运行。

回顾我们之前关于封装和解封装头部的图示,我们假设每个路由器都会解析到第三层的数据包,并通过 IP 将数据包转发到下一个路由器。然而,如果我们有一个包含多条链路的第二层网络,交换机只需要将数据包处理到第二层,并通过以太网将数据包转发到下一个交换机。

如今,几乎所有的交换机也实现了第三层功能,这就是为什么我们可以互换使用这些术语。从历史上看,以太网早于互联网出现,这也是交换机和路由器之间存在区别的原因。

第二层网络拓扑

就像我们在路由单元中看到的那样,有许多不同的拓扑结构可用于连接本地网络中的计算机。

我们可以使用单条链路连接所有计算机,但这效率很低。我们只能使用单条链路的带宽。此外,所有设备都需要轮流发送消息,如果两台计算机同时发送消息,可能会发生冲突。

我们也可以使用全 mesh 结构,即每对主机都有一条专用链路,但这种结构难以扩展。

就像在第三层一样,我们可以引入交换机,通过拓扑结构将数据包转发到目的地。但是,和在第三层一样,这也带来了路由问题,即交换机需要决定将数据包转发到哪里。

在本节中,我们将探讨一些专为本地第二层网络设计的路由协议。我们还将看到一些阻碍这些协议扩展并用于全球第三层网络的挑战。

基于泛洪的转发

最朴素的转发方法是对收到的每个数据包进行泛洪。当交换机收到一个数据包时,会将该数据包从所有端口发送出去。

一个小小的优化是,我们不需要将数据包从接收它的端口再发送回去。

img

这种朴素的方法有两个主要问题:

  1. 浪费带宽。数据包的副本会被不必要地发送到不需要该数据包的交换机和主机。

  2. 泛洪可能导致数据包循环,从而使网络不堪重负。

学习型交换机

让我们先解决第一个问题:泛洪数据包会浪费带宽。

为了解决这个问题,我们希望填充交换机的转发表,以便它们能够直接将数据包转发到目的地,而不是向所有方向泛洪数据包副本。

我们可以运行路由算法来填充转发表,但有一种更简单的方法是使用学习型交换机

假设你是路由器 R2。你没有关于整个网络拓扑的任何信息,你的转发表是空的。你有向北、向南、向东和向西的端口。

你看到一个数据包从西边的端口传来。该数据包显示:“来自 A,到 B”。从这个数据包中,你可以推断出 A 一定在西边。

img

你现在可以在转发表中添加一个条目:前往 A 的数据包应该转发到西边。

这就是学习型交换机的核心思想。当你收到一个入站数据包时,你会得到关于发送方位置的线索。你可以使用该信息来填充发送方的转发条目。

请注意,入站数据包不会告诉你关于接收方位置的任何信息。在上面的例子中,当你从西边的端口收到 “来自 A,到 B” 的数据包时,这并不会告诉你 B(接收方)在哪里。相反,你会填充 A 的转发表条目,以便将来前往 A 的数据包可以转发到西边。

随着你收到更多的入站数据包,你就能够开始在转发表中添加更多条目。如果你收到一个目的地不在转发表中的数据包,你仍然可以通过从所有端口(除了入站端口)泛洪该数据包来进行转发。

例如,当你从西边的端口收到 “来自 A,到 B” 的数据包时,你的转发表中还没有 B 的条目。因此,你应该将该数据包从所有端口(除了西边的端口)发送出去。

img

注意:没有必要将数据包从入站端口(例如西边)发送回去,因为前一个交换机 / 主机(例如你的西边)已经有了该数据包的副本并进行了转发(这也是它到达你这里的方式)。如果你再次将数据包发送回去,前一个交换机 / 主机只会做出相同的转发决策(要么再次泛洪,要么再次转发给你),而这种重复转发对数据包到达目的地没有帮助。

总之,学习型交换机遵循两条规则:

  1. 当收到入站数据包时,更新转发表,将发送方与入站端口相关联。

  2. 如果目的地在转发表中,则将数据包转发到正确的下一跳。否则,从所有端口(除入站端口外)泛洪该数据包。

下面是学习型交换机工作的一个例子。考虑这个网络拓扑。所有交换机都是学习型交换机,它们的转发表一开始是空的。

A 向 B 发送一个数据包。A 将数据包转发到 R1。

R1 看到 “来自 A,到 B” 的数据包从端口 1 传入。因此,A 一定在端口 1 的方向。R1 将这个映射添加到它的转发表中。

R1 不知道 B 在哪里,所以 R1 从所有端口(除了入站端口)泛洪这个数据包。

img

R2 和 R4 都收到了 “来自 A,到 B” 的数据包。它们现在都知道了 A 的位置,并在各自的转发表中添加了 A 的映射。它们都不知道 B 在哪里,所以从所有端口(除了入站端口)泛洪该数据包。

img

R3 和 R5 都收到了 “来自 A,到 B” 的数据包。它们现在都知道了 A 的位置,并在各自的转发表中添加了 A 的映射。它们都不知道 B 在哪里,所以从所有端口(除了入站端口)泛洪该数据包。

img

C 收到 “来自 A,到 B” 的数据包。C 检查头部,发现自己不是该数据包的预期接收者,因此 C 丢弃该数据包。

B 收到 “来自 A,到 B” 的数据包。B 检查头部,发现自己是接收者,因此 B 成功接收并处理该数据包。

img

接下来,假设 B 向 A 发送一个数据包。首先,B 将数据包转发到 R3。

R3 收到 “来自 B,到 A” 的数据包。这让 R3 知道了 B 的位置,所以 R3 在转发表中添加了 B 的映射。此外,R3 注意到 A 在其转发表中,因此 R3 可以将数据包沿着到 A 的下一跳转发(而不是泛洪该数据包)。

img

R2 收到 “来自 B,到 A” 的数据包。这让 R2 可以在转发表中添加 B 的映射。R2 在转发表中看到 A 的条目,因此将数据包沿着到 A 的下一跳转发。

img

R1 收到 “来自 B,到 A” 的数据包。这让 R1 可以在转发表中添加 B 的映射。R1 在转发表中看到 A 的条目,因此将数据包沿着到 A 的下一跳转发。

img

随着更多数据包的发送,转发表中会添加更多条目,泛洪的情况会越来越少。

我们需要添加的最后一个功能是:当安装转发表条目时,我们会为其分配一个生存时间(TTL)。如果 TTL 过期,该条目将被删除。这使得失效的路由(例如,由于链路、主机或交换机出现故障)能够过期。例如,在上面的例子中,如果 B 离开了网络,TTL 将确保所有关于 B 的转发表条目最终都会过期。

STP 的动机:环路

回想一下,泛洪有两个问题:浪费带宽,以及环路可能使网络不堪重负。学习型交换机解决了第一个问题,但它们没有解决环路问题。

为了理解原因,考虑这个带有环路的拓扑结构。假设所有交换机都是学习型交换机,并且所有转发表一开始都是空的。A 尝试向 B 发送一个数据包,并将数据包转发到 R1。

img

R1 没有 B 的条目,所以它将数据包泛洪到 R2(和 R3)。

R2 没有 B 的条目,所以它将数据包泛洪到 R4。

R4 没有 B 的条目,所以它将数据包泛洪到 R3。

R3 没有 B 的条目,所以它将数据包泛洪到 R1。

R1 没有 B 的条目,所以它将数据包泛洪到 R2,循环就这样继续下去。

同时,数据包的一个副本也在另一个方向上形成环路:R1 最初泛洪到 R3,R3 然后泛洪到 R4,R4 然后泛洪到 R2,R2 然后泛洪到 R1,R1 然后泛洪到 R3,循环继续。

在整个过程中,交换机安装了 A 的转发条目,但它们从未获得 B 的任何条目,因此无限循环永远无法解决。没有人有 B 的转发条目,所以每个人在收到数据包时都会泛洪它。

这个问题有时被称为广播风暴,因为网络被广播流量淹没了。

我们如何解决这个问题?理想情况下,我们希望 “删除” 冗余链路,使拓扑结构没有环路。这样,学习型交换机的方法就能很好地工作,不会出现广播风暴。

img

注意:另一个解决方案可能是在每个数据包中添加一个 TTL 字段,以便数据包在被转发太多次后过期。不幸的是,以太网头部没有 TTL 字段,所以这个解决方案无法实现。

注意:另一个解决方案可能是如果你以前见过某个数据包就丢弃它。这需要为每个数据包附加某种时间戳或唯一 ID。同样,以太网头部没有这个字段,所以这个解决方案也无法实现。

STP:选举根交换机

生成树协议(STP) 帮助我们禁用链路,使得到的拓扑结构没有环路。这将帮助我们避免广播风暴。

注意,主机不参与这个协议。路由器将协同工作来禁用链路,从拓扑结构中消除环路。因此,在描述这个协议时,我们会忽略主机。

img

STP 如何决定禁用哪些链路?让我们先从全局视角解决这个问题。然后,我们再思考交换机如何在没有全局网络视图的情况下通过交换消息来实现这一点。

STP 的第一步是选举一个根交换机,步骤如下:

每个交换机都被分配一个 ID,由优先级值(由网络管理员手动设置)和交换机的 MAC 地址组成。

比较两个交换机时,优先级较低的交换机 ID 较低。如果优先级相同,则 MAC 地址较低的交换机 ID 较低。

根交换机是 ID 最低的交换机。

img

如果网络管理员想选择一个特定的根,可以通过手动设置各个交换机的优先级来实现。或者,管理员可以将所有交换机的优先级保留为默认值,这将导致 MAC 地址最低的交换机被选举为根。在这些笔记中,我们不会讨论哪个根最好;重要的是明确地选择其中一个路由器作为根。

STP:端口状态

现在我们有了根交换机,我们将每个交换机上的每个端口分为以下三种状态之一:

  1. 指定端口(Designated Port):这些端口指向远离根的方向(即它们通向离根更远的地方)。

  2. 根端口(Root Port):有一个或多个端口指向根的方向(即它们通向离根更近的地方)。在这些端口中,沿着到根的成本最低路径的那个端口是根端口。

  3. 阻塞端口(Blocked Port):所有指向根但不是根端口(到达根的最佳路径)的端口都是阻塞端口。

img

下面是一些端口状态的示例。假设 ID 按照路由器标签的顺序排列。这意味着 R1 的 ID 最低,因此它被选举为根交换机。

img

根交换机(R1)上的所有端口都指向远离根的方向,因此它们都是指定端口。

R2 有两个端口。只有一个端口指向根的方向,所以那一定是到根的最佳路径。因此,R2 的朝上的端口被标记为根端口。

R2 的另一个端口指向远离根的方向,所以 R2 的朝下的端口被标记为指定端口。

R6 有三个端口。朝下的端口指向远离根的方向,所以它是指定端口。

在 R6,通向 R4 和 R3 的端口都指向根的方向。然而,通向 R3 的端口提供了到根的成本最低的路径(成本 2),而通向 R4 的端口提供了到根的较差路径(成本 3)。因此,我们将通向 R3 的端口标记为根端口(到达根的最佳方式),将通向 R4 的端口标记为阻塞端口(指向根,但不是最佳路径)。

有时,我们会遇到平局,即有两条到达根的最佳路径。

img

例如,在 R4,通向 R2 和 R3 的端口都指向根的方向,并且它们都提供了成本为 2 的到根的路径。在平局的情况下,我们认为具有较低 ID 的下一跳是到根的更好路径。这使得通向 R2 的端口成为根端口,通向 R3 的端口成为阻塞端口。

有时,我们会有一条通向离根距离相同的地方的链路。

img

例如,R4 距离根 2,它有一条链路通向 R5,R5 距离根也为 2。同样,我们将使用路由器 ID 作为平局决胜规则。如果链路通向 ID 更高的路由器,我们说该链路指向远离根的方向。如果链路通向 ID 更低的路由器,我们说该链路指向根的方向。在这个例子中,R4 的朝右的端口指向远离根的方向(通向距离相同但 ID 更高的地方),所以它是指定端口。另一方面,R5 的朝左的端口指向根的方向(通向距离相同但 ID 更低的地方),所以它要么是根端口,要么是阻塞端口。

STP:禁用链路

现在每个端口都被分配了状态(指定端口、根端口或阻塞端口),我们准备从网络拓扑中移除环路。

为了移除环路,每个交换机只需要假装其阻塞端口不存在。换句话说,不要从该端口发送任何用户数据,也不要从该端口接收任何用户数据。

img

(注意:我们这里指定用户数据是因为 STP 数据包仍然可以从阻塞端口发送和接收。这将允许 STP 在拓扑结构改变时重新启用阻塞端口。)

如果我们停止通过阻塞端口发送用户数据,那么任何带有阻塞端口的链路最终都会被禁用。

为什么这会起作用?让我们从特定交换机的角度来思考。你的根端口是你到达根的最佳方式。你的阻塞端口也指向根的方向,但它们不是到根的最佳路径。这意味着阻塞端口实际上创建了一条冗余(但更差)的到根的路径,所以我们应该禁用该链路。

你可能会担心:如果你阻塞了一个端口,但其他人需要使用那条被禁用的链路来将数据包转发给你,以到达根,该怎么办?幸运的是,这种情况永远不会发生。记住,你的阻塞端口指向根的方向(即你离根更远,而另一端离根比你更近)。因此,如果另一端的交换机(更近的那端)将数据包转发给你(更远的那端),他们将把数据包转发到远离根的方向。这意味着我们可以安全地阻塞这个端口并禁用这条链路,而不用担心其他交换机试图使用该链路作为它们到根的路径的一部分。

相比之下,指定链路不能安全地禁用,因为它们通向远离根的方向(即另一端的交换机离根比你更远)。另一端的交换机实际上可能希望将数据包转发给你,因为你在它们到根的最佳路径上。幸运的是,这也不是问题。虽然你不能安全地禁用这条链路,但你可以依靠另一端的交换机在不需要时禁用该链路。另一端的交换机比你更远,所以如果这条链路是它们到根的最佳路径(即根端口),它们会保留这条链路;如果不是它们到根的最佳路径(即阻塞端口),它们会禁用这条链路。

通过这种策略,每条链路只被一端禁用。离根更远的那端会问:我是否使用这条链路作为我到根的最佳路径?如果是,这条链路的端口是根端口。如果不是,这条链路的端口是阻塞端口。

离根更近的那端总是将这条链路的端口作为指定端口。这实际上是将禁用的决定留给了更远的那端。这很好,因为更近的那端不知道更远的那端是否会使用这条链路作为它们到根的最佳路径。

img

STP:指定端口

旁注:我们为什么称它们为指定端口?到目前为止,我们绘制的网络中每条链路都连接两台机器,但请记住,有时我们可以有连接多台计算机的链路。

假设连接两个交换机的链路还有许多主机连接到它。如果这些主机想要发送或接收数据,它们会将数据发送到指定端口,而不是阻塞端口。(阻塞端口不会接收任何用户数据。)这确保了它们的数据通过唯一的路径到达目的地。如果数据同时发送到指定端口和阻塞端口,数据可能会通过两条路径到达目的地,从而产生环路。

img

考虑到这一点,指定端口的另一个等效解释是:链路上的主机应该将数据发送到指定端口以到达根(或生成树上的任何其他地方)。从交换机的角度来看,指定端口指向远离根的方向。从主机的角度来看,发送到指定端口会使它们更接近根(或生成树上的任何其他地方)。

STP:BPDU 交换

我们现在知道如何使用 STP 来禁用链路并从网络拓扑中移除环路。然而,到目前为止,我们的协议假设具有网络的全局知识。你需要全局视图来确定根,以及判断端口是指向根还是远离根。

为了让交换机了解标记其端口所需的信息,交换机交换称为桥协议数据单元(BPDU) 的消息。这些消息与我们在其他路由协议中交换的控制平面路由消息几乎相同,只是名称更花哨。请注意,这些控制平面消息与数据平面的用户数据包(我们要转发的实际数据)是不同的。

当协议开始时,每个交换机都认为根是它自己,到根(它自己)的成本是 0。

随着协议的运行,每个交换机都会跟踪它认为的根是什么,以及到该根的最佳已知路径(以及该路径的成本)。

img

当你发送 BPDU 时,你会包含两条信息:你认为的根是谁,以及你离根有多远。例如,一个 BPDU 可能会说:“根是 R2,我可以以成本 7 到达 R2。”

当你收到一个 BPDU 时,你会检查它是否有任何 “更好” 的信息。BPDU 可能因为两个原因而更好:

  1. BPDU 中的根具有更低的 ID。这意味着你发现了一个更好的根。你应该放弃当前的根和成本,转而采用新的根和到新根的路径。

  2. BPDU 中的根相同,但 BPDU 提供了到根的更好路径。你应该采用到根的新路径。

img

到根的成本计算方式与我们在距离矢量协议中所做的相同。例如,假设你的邻居告诉你 “根是 R2,我可以以成本 7 到达 R2。” 那么你到根的成本就是你到邻居的直接链路成本加上邻居到根的成本(如通告中所指定的)。

当你更新你的状态(你认为的根是谁,或者你到根的最佳已知成本)时,你应该向你的邻居发送一个 BPDU,告知他们你的新状态。

一旦协议收敛,该状态就会给每个交换机足够的信息来标记其所有端口。你知道到根的最佳路径,因此你可以将相应的端口标记为根端口。

你的邻居也都告诉你他们离根有多远。如果一个邻居说他们更远,那么你可以将相应的端口标记为指定端口。如果一个邻居说他们更近(但他们不在你到根的最佳路径上),那么你可以将相应的端口标记为阻塞端口。

BPDU 会定期交换,以便如果网络拓扑发生变化,协议可以适应并为新拓扑找到生成树(即禁用链路)。

STP:BPDU 交换示例

路由器并行发送和接收 BPDU 交换,因此没有特定的路由器会发送第一个 BPDU。在这个例子中,我们将展示所发送的 BPDU 的一个子集。

R3 的初始状态是:根 R3 距离为 0。R3 的第一个通告将这个状态发送给它的邻居。

img

R1 收到这个通告。R1 当前认为根是 R1,而该通告提供的根是 R3。通告的根更差(ID 更高),所以 R1 拒绝这个通告。

R6 收到这个通告。R6 当前认为根是 R6,而该通告提供的根是 R3。通告的根更好,所以 R6 接受这个通告。R6 的更新状态是:根 R3 距离为 1。注意:成本是从通告中的 0(来自 R3 的通告中的成本)加上 1(到 R3 的链路成本)计算得出的。

此时,R6 已经更新了它的状态,所以它将向它的邻居发送一个通告(本演示中未显示)。

一段时间后,R1 向它的邻居发送一个通告,说明它的状态:根 R1 距离为 0。

img

R2 收到这个通告。通告的根(R1)比当前已知的最佳根(R2)更好,所以 R2 接受这个通告。R2 的更新状态是:根 R1 距离为 1。

同样,R3 收到这个通告,并接受它,因为通告的根(R1)比当前已知的最佳根(R3)更好。R3 的更新状态是:根 R1 距离为 1。

R2 和 R3 已经更新了它们的状态,所以它们将各自向它们的邻居发送通告。

img

R4 收到来自 R2 的通告。通告的根(R1)比当前已知的最佳根(R4)更好,所以 R4 接受这个通告。R4 的更新状态是:根 R1 距离为 2。注意:这个成本是通过将来自 R2 的通告中的成本 1 加上到 R2 的链路成本 1 求和得出的。

R6 收到来自 R3 的通告。通告的根(R1)比当前已知的最佳根(R3)更好,所以 R6 接受这个通告。R6 的更新状态是:根 R1 距离为 2。注意:R6 的旧状态说 R3 距离为 1,而新状态说 R1 距离为 2。尽管新状态的距离更大,但它仍然更好,因为新状态有一个更好的根。

R4 和 R6 已经更新了它们的状态,所以它们将向它们的邻居发送带有更新状态的通告。我们将首先展示 R4 的通告,然后稍后再回到 R6(再次提醒,实际上所有这些都是并行发生的)。

img

R5 收到来自 R4 的通告。通告的根(R1)比当前已知的最佳根(R5)更好,所以 R5 接受这个通告。R5 的更新状态是:根 R1 距离为 3(通告中的 2 加上到 R4 的链路成本 1)。

R6 也收到来自 R4 的通告。通告的根(R1)与当前已知的最佳根(R1)相同,所以我们需要检查成本。接受该通告将得到的成本是通告中的 2 加上到 R4 的链路成本 1,总共 3。当前已知的最佳成本是 2。因此,R6 拒绝该通告(3 比 2 差)。

R5 已经更新了它的状态,所以它将向它的邻居发送带有更新状态的通告。

img

R7 收到来自 R5 的通告。通告的根(R1)比当前已知的最佳根(R7)更好,所以 R7 接受这个通告。

R7 已经更新了它的状态,并将向它的邻居发送通告,尽管该通告未在此处显示(R6 会因为到根的成本更差而拒绝它)。

img

接着前面的内容,R6 向它的邻居发送一个通告,R7 收到这个通告。通告的根(R1)与当前已知的最佳根(R1)相同,所以我们需要检查成本。接受该通告将得到的成本是通告中的 2 加上到 R6 的链路成本 1,总共 3。当前已知的最佳成本是 4。因此,R7 拒绝该通告(3 比 4 好),并将其到根的成本更新为 3(而不是 4)。

路由器继续定期相互交换通告。本演示中没有显示所有通告,但最终,协议将收敛,所有路由器都将知道根是 R1。此外,所有路由器都将知道它们到根的成本。

img

一旦所有路由器都知道它们到商定根的成本,它们就可以交换定期通告。这允许路由器了解其邻居的到根成本值,进而允许路由器将端口分配为指定端口(DP)、根端口(RP)或阻塞端口(BP)。

(注:文档部分内容可能由 AI 生成)