还是那句话,在学习一个协议的时候,先搞清楚问题是什么,也就是说这个协议是为了解决什么问题满足什么需求的,然后试着自己在脑子里实现它,也就是说如果自己碰到这样的问题,在没有任何背景,没有任何可用的技术时,该如何来解决它。等到自己想到了办法,再和标准的协议作对比,看看自己的想法缺失了什么,什么地方理解有偏差,只有这样才能领略到该协议设计思想的精髓,学习也才会事半功倍!如果上来就是看RFC,啃IEEE,啃代码,那十有八九会迷失在细节中,最终累得够呛,好不容易看明白了,基本没什么积累,最终过不了几天不用不温习,就彻底忘完了...难道不是这样吗?
    对于以太网,为何会有最小生成树的问题?这实际上是一种为冗余补偿所付出的代价,毕竟没有什么是免费的,任何技术都是解决不了所有问题的,只可惜当年的克拉苏没有意识到这一点,终归命断帕提亚...以太网最初是一个广播网络,只要在一点发出一个数据帧,过不了多久,该数据帧就会传染遍整个网络,当时的任何设备都没有学习机制,只管收到帧就毫不犹豫地转发,这种方式会不会有问题完全取决于如何连线,如果连接线缆出现环,那么数据帧便会永远在网络里面环绕,进而引发广播风暴,如果没有环,数据帧就会在到达线缆的终点时自动被吸收而消失。理解以上的文字只需要知道当初的线缆是半双工同轴线缆,设备也只是洪泛转发的两端口网桥或者是多端口HUB。
    那么在布线时显式避免环路不就可以了吗?然而一旦某条链路出现问题,就要重新布线,这也是不可取的,因此需要一种机制,那就是人工提供冗余布线,也就是人工将以太网组成一个环路,然后设计一种软件机制来避免环路的出现,也就是通过软件途径来断开某条连路从而切断环,并且在链路出现问题的时候,可以自动生成另一条无环的链路,这一切必须不需要人工干预才行得通。这就必须要求所有的设备能联合完成上面的事。
    总而言之,以太网上之所以会出现STP,就是因为两点原因,第一,以太网最初被设计成了在同轴上跑的广播网,第二,需要提供冗余功能。二者结合就会出现“需要自动避免环路以及动态触发更新路径问题”,而这个问题的解决方案就是--STP,即生成树协议。
    任何技术背后都要有理论支撑,STP也不例外。我们几乎都知道最小生成树算法,其中Prim算法生成的树是由一个根慢慢长出来的,而Kruskal算法生成的树则开始是无根的N棵树,然后将其嫁接而成一棵树所得的。虽其思想不同却能殊同归。如果自己碰到了以太网环路的问题,则怎么利用既有的生成树算法来实现一个STP'(以区分IEEE的生成树协议)协议呢?由于以太网本质上是一个广播网,数据帧可以通传整网,那么使用嫁接的Kruskal算法就有点不妥了,Kruskal算法的本质在于它能使游离的树嫁接成一棵树,而在以太网上,没有什么是游离的,只要设备节点有线缆连接,它们就是一棵树,而为了冗余备份,所有的节点人工连线时就是互联互通的,这就决定了针对以太网的故意环路,用Prim算法是比较好的。
    问题在于,对于一个有环的以太网,虽然我们人类可以一眼看出环在哪里,可是对于设备(HUB,网桥,交换机...)却不能,它们只是一部机器而已。我们需要的是要这些设备形成一个共识,这个共识可以让它们中的每一个都知道全局的情况,而这正是以太网的优势所在,因为它是广播网络。
    这里顺便插一段,所有的网络路由协议,包括链路层的和网络层的,都有两种算法,一种是所有节点先生成全局数据库后再分别计算的协议,比如三层路由协议中的OSPF,另外一种则是分布式的依赖于洪泛时间的协议,比如三层的RIP协议以及二层的STP协议。对于前者,基于全局数据库,则比较简单,因为大家的数据库都是一致的,计算的结果也必然一致,但是对于后者,分布式的,则比较困难,每一个节点的决策都要依赖其它的节点,而对于某一个设备又不可能知道其它设备的情况,这该怎么办?万能的东西是什么?时间!我们只需要知道一个节点发出的消息在最多多久可以传遍全网,就可以知道所有的节点消息达到一致的时间是多久,在过了这段时间之后,我们就可以让其做下一步的事情了。当然,这个结论的前提在于每个节点都信任其它节点的消息,不会自作主张而篡改消息,这是一个原则,叫做“效率来自于信任”(我会再写一篇阐述)。
    怎么办呢?首先要选择一个根。在Prim算法中,根是随意选出的,可是对于设备,“随意”这个对于人而言最简单不过的词对于机器设备而言却显得过于复杂,因此需要一种机制来选择一个根节点。对于每一个设备而言,它自己需要有[且不止只有]一条路径连接到根节点,对于标准的Prim算法,任何构成环的“边”都要被砍掉,可是对于以太网来讲,所谓的边正是承载计算机的所在,因为还需要为被砍掉的边再选择一个链路到达根,只有这样,才能使不仅节点,还有边,都能通过唯一的链路到达根节点,从而形成一棵树!
    Prim算法中的树根是随意选择的,可是在STP'中却不能这样,必须有一定的规则来确定一个根,那么就必须在所有的交换机/桥设备之中选择一个树根,记住,我们本质上就是要把全连通的图切割成一棵树。选择树根比较简单,只需遵循一定的规则,比如IEEE802.1d中为所有交换机进行编号,取最小的即可。可是怎么知道谁是最小的呢?这正是用到了一个数据帧在一个以太网中传输的时间这一特性。一个以太网是有规模限制的,它必须被限制到几百米之内,而一个电信号在这么个距离内的传输时间由此而被确定,因此我们可以确认,一个数据帧肯定能在最长X时间内传遍全网,进而各个设备节点得到的信息终相一致,这个选择消息在实现中可以用洪泛的方式来广播,当然IEEE的实现做了些优化,即当一个节点收到了更优的信息时便不再广播自己的消息。
    接下来就是如何切断某条链路的问题了。对于每一个设备,有且只有一条路通往已经被选出的树根,而一个设备可能有多个端口,因此每个设备必须在这多个端口中选择一个端口来作为通往树根的端口,怎么选择呢?这也是通过洪泛实现的,树根既然已经被选出,它便发出一个帧,这个帧由于以太网的广播特性,最终将可以到达所有的端口,每个设备在转发该帧时需要加入自己的度量值,最终每个设备的每个端口都将收到一个带有度量值的帧,每个设备在每个端口的这些帧中选择一个度量值最小的最为该设备通往树根的端口,这个端口在IEEE标准中叫做根端口!
    到此为止,生成树其实已经生成了,还需要做什么?标准的生成树Prim算法毅然的砍断了N条构成环路的边,这些边将不再需要,可是对于以太网的应用,每一条边都不是可以丢弃的,因为所有的计算机全部都挂在这些边上,换句话说,每一条边上都挂接有计算机,因此必须将每一条被Prim算法砍掉的边选择一个设备节点作为该边通往树根的出口。我们知道,以太网上所谓的一条边其实就是一条同轴线或者双绞线,它们根本没有智能,拥有智能的还是设备。刚才在选择IEEE802.1d根端口的时候,除了根端口之外的所有端口按Prim算法说都应该被阻断,可是它们可能还需要承载被砍掉的边上计算机的数据帧而不能被砍断。到底哪些端口需要承载被砍断边的流量呢,这仍然和度量有关,我们称为度量3,每一个端口都有一个到树根的度量,也能收到对端到根的度量,和根端口选择不同的是,这个度量3对于本端和对端来讲都已经加上了设备的度量,而根端口的选择过程中,都没有加入本设备的度量。最终我们只需要阻断那些度量3大于对端传来的度量3的端口即可!没有被阻断的端口在这一步中叫做指定端口。
    到此为止,一棵树生成了!而且是最小树!所需要的知识无非几点:Prim算法,以太网广播特性,洪泛。如果总结一下整个过程,那么将得到三点:
1.首先在所有设备中选择一个根节点;
2.其次为每一个设备选择一个根端口通向根节点;
3.再次为每一条被砍断的链路选择一个指定端口通向根。
    理解了以上的过程,再看IEEE802.1d文档,那就只是一个总结的过程了,没有什么难以理解的了。学习一个协议,难道还有比这更简单且愉快的吗?相比一开始就阅读IEEE文档,阅读Linux代码来讲,这个过程既轻松又愉快,你只需要明白Prim算法,然后拥有一颗喜欢思考的大脑即可,不是吗?