引子
我个人是非计算机专业,从零开始学习的SDN,整个过程异常艰难。为此愿意降低大家学习SDN的难度,本系列的目标是,即使一个非计算机专业的人,只要有一定的编程能力,也能在学习了系列后,能够上手SDN相关的应用与实战。主要利用仿真技术来学习,以降低学习成本。
我们将使用第一性原理的思路,来开始这段旅程。首先第一个问题就是,网络的本质是什么?
网络的本质
如果要用一句话概括网络,网络就是将信息从A送到B。这和交通行业的本质很相似,交通的本质就是从A到B,只是将人、将物体运送。
如何理解现代互联网,是如何达成这个从A到B的过程的?我们平时访问一个网站,刷一段视频,并不知道这个信息从哪里来的。互联网的架构,将这些复杂性隐藏了起来。通常称为网络七层,你去看网络上所有的讲解网络七层的内容,大多是讲技术,但很少有讲这些技术为什么是这样的。
我尝试我的的经验和思考,来切开这个复杂系统。
水流的比喻
仔细观察下水,水能很快发现从高到底的那个最短路径。泼去出的水,总能流向那个最低的位置,并且,总会有一滴水,最先到达那个最低点。如果我们将这个模型抽象,就能得出:水总能找到最高到最低的最短路径。甚至你阻断了原本的最短路径,它也会找到新的最短路径。
人们甚至根据水的这个特性发明了Flood Fill算法。为什么会这样呢?仔细观察,水其实会浸湿更多的地方,而不是只浸湿最短路径。水其实是在一个广度优先搜索(BFS)。正是有了广度,也就知道了所有的路径。随着最短路径的流速增加,由于各种力学原因,从而会让更多的水流过这个路径。从宏观上看,就是水找到了最短路径。
请牢记这个图像,它将会贯穿整个互联网的系统。
交换机,最初也就是利用了这种原理,来进行通讯的。
交换机(Switch)
交换机,英文名字其实也可以翻译为开关。因为最初的交换机,真的就是一堆开关,用来切换线路。开关扳到B,就是给B发消息,扳到C,就是给C发消息。后来交换机出现,就取代了人工。
交换机是用寻址来实现寻找目的地。但如何找到目标地址的呢?可以模拟这个过程。
模拟
假设有个信息中转站S,收到信息后,无脑复制,全部发送给后续的其它节点。那么就一定能把信息发给目的地B。这就类似水流,就是Flood(或者叫广播,或者叫泛洪,名字只是个代号)。像水,不挑,所有的地方都流过。
但如果中间有很多节点,能否送到呢?肯定可以的。消息必然会流经所有的节点。
那如何返回呢?如法炮制,所有的信息都复制给所有的节点。但这样效率太低了,并且消息会泄露给不相关的节点或者人(不用怀疑,这是真实网络上真实存在的问题)。
那么如何缓解这个问题呢?大家都走过陌生的路,摸索一次就行,下次就按照之前走通的路走。并不会再次陌生。交换机也是这样,如果消息返回了,那我就记录下从哪里返回的,下次直接照对应的位置发就行了呗。就是记下路,这谁都懂。
这真的可行吗?让我们用代码验证一下。
代码模拟
首先,模拟出几个节点,他们中间有N个交换机连接。
接下来,任何节点发来的消息,都无脑复制一份转发所有其它节点。
然后,在消息返回的时候,就记录下最先返回的那个位置,是从哪里返回的。最先的一定是最短路径,因为时间消耗是评价路径长短的唯一指标。既然如此,为什么不把最先发送来消息的位置也记录下来呢,这样也就知道了来源的最短路径。
按照这个思路可以将代码拆解为几部分:
1. 节点,也就是收发消息的来源和目标,一般代表人和计算机;
2. 交换机,用来中转消息的,并且用来寻找最短路径;
1. 有记忆消息最先从哪里发送的功能。
2. 有记忆消息最先从哪里返回的功能。
3. 整个网络的构建
4. 运行起来一切,收发消息。
以下是Python代码
from dataclasses import dataclass
import random
import time
from multiprocessing import Queue
import threading
def debug(*arg, **kwarg):
return print(*arg, **kwarg)
@dataclass
class Message:
src: int
dst: int
payload: str = 'ping'
max_hop = 10
class Port():
def __init__(self, num, parent=None):
self.msgs = Queue()
self.linked_sw = None
self.linked_port = None
self.num = num
self.parent = parent
def write(self, msg):
self.msgs.put(msg)
def send(self, msg):
if self.linked_sw:
self.linked_port.write(msg)
def read(self):
return self.msgs.get()
def empty(self):
return self.msgs.empty()
class Switch(threading.Thread):
def __init__(self, name, ports=4, delay=100) -> None:
super().__init__()
self.name = name
self.ports = [Port(i, self) for i in range(ports)]
self.port_mapping = {}
self.delay = delay # in ms
self.mac = self.rnd_mac()
self.running = True
self.unit = 1000.0
def __repr__(self) -> str:
return "%s:%s"%(self.name, self.mac)
def _replay(self, msg: Message):
replay_dict = {
'':'Please say something.',
'ping':'pong'
}
return replay_dict.get(msg.payload, None)
def rnd_mac(self):
return random.randint(10000, 100000-1)
def set_port(self, sw, this_n, that_n):
if self.ports[this_n].linked_sw is not None:
raise ValueError('SW:%s,起点端口%s被占用'%(self.name, this_n))
if sw.ports[that_n].linked_sw is not None:
raise ValueError('终点端口被占用')
self.ports[this_n].linked_sw = sw
sw.ports[that_n].linked_sw = self
self.ports[this_n].linked_port = sw.ports[that_n]
sw.ports[that_n].linked_port = self.ports[this_n]
def remove_port(self, sw, this_n, that_n):
if self.ports[this_n].linked_sw is None:
raise ValueError('SW:%s,起点端口%s为空'%(self.name, this_n))
if sw.ports[that_n].linked_sw is None:
raise ValueError('终点端口为空')
self.ports[this_n].linked_sw = None
sw.ports[that_n].linked_sw = None
self.ports[this_n].linked_port = None
sw.ports[that_n].linked_port = None
def broadcast(self, msg, exclude_port):
for i in self.ports:
if i is exclude_port:
continue
if i.linked_sw:
i.send(msg)
def write_port(self, msg, num):
self.ports[num].write(msg)
def send_port(self, msg, num):
debug(self, 'send_port',msg, num)
self.ports[num].send(msg)
def replay(self, msg, port_num):
ret = self._replay(msg)
if ret is None:
return
msg = Message(
src=msg.dst,
dst=msg.src,
payload=ret
)
self.write_port(msg, port_num)
def handle_recv(self, port):
msg = port.read()
msg.max_hop -= 1
if msg.max_hop <=0 :
return
debug('%s recvived:%s'%(self, msg))
if msg.src not in self.port_mapping:
self.port_mapping[msg.src] = port.num
if msg.dst == self.mac:
debug('Hit target %s, msg: %s'%(self, msg))
self.replay(msg, port.num)
return
pn = self.port_mapping.get(msg.dst, None)
if pn is None:
self.broadcast(msg, exclude_port=port)
else:
if self.ports[pn].linked_sw is None:
print('broadcast....')
self.port_mapping[msg.dst] = None
self.broadcast(msg, exclude_port=pn)
self.send_port(msg, pn)
def read_all(self):
for p in self.ports:
while not p.empty():
self.handle_recv(p)
def stop_running(self):
self.running = False
def run(self):
while self.running:
self.read_all()
time.sleep(self.delay/self.unit)
sws = [Switch(name=i, delay=100) for i in range(10)]
sws[0].set_port(sws[1], 0, 0)
sws[1].set_port(sws[2], 1, 0)
sws[1].set_port(sws[3], 2, 0)
sws[1].set_port(sws[4], 3, 0)
sws[3].set_port(sws[5], 1, 0)
sws[3].set_port(sws[8], 2, 2)
sws[3].set_port(sws[6], 3, 0)
sws[4].set_port(sws[7], 2, 1)
sws[6].set_port(sws[7], 2, 2)
sws[7].set_port(sws[8], 3, 0)
sws[8].set_port(sws[9], 1, 2)
def run():
for t in sws:
debug(t)
t.start()
msg = Message(
src=sws[0].mac,
dst=sws[9].mac,
payload='ping'
)
debug('----------第1次-------------')
sws[0].ports[1].write(msg)
time.sleep(5)
debug('----------第2次-------------')
sws[0].ports[1].write(msg)
# 删除一个最短路径里的连接,看是否会自动寻找
sws[3].remove_port(sws[8], 2, 2)
debug('-'*30)
msg = Message(
src=sws[0].mac,
dst=sws[9].mac,
payload=''
)
sws[0].ports[1].write(msg)
time.sleep(5)
#再次发送看新路径
debug('----------删除节点后-------------')
msg = Message(
src=sws[0].mac,
dst=sws[9].mac,
payload=''
)
sws[0].ports[1].write(msg)
time.sleep(5)
for t in sws:
debug('stop:', t)
t.stop_running()
debug(t.is_alive())
run()
这段代码就是这些节点在通讯 :
其输出如下,第一次通讯的时候,会有很多的冗余信息。但整体是能送达,并且返回的。这我们就不要强求什么了:
0:97497
1:77754
2:41864
3:67597
4:70479
5:77894
6:39914
7:63306
8:59644
9:45514
----------第1次-------------
0:97497 recvived:Message(src=97497, dst=45514, payload='ping')
1:77754 recvived:Message(src=97497, dst=45514, payload='ping')
4:70479 recvived:Message(src=97497, dst=45514, payload='ping')
2:41864 recvived:Message(src=97497, dst=45514, payload='ping')
3:67597 recvived:Message(src=97497, dst=45514, payload='ping')
5:77894 recvived:Message(src=97497, dst=45514, payload='ping')
7:63306 recvived:Message(src=97497, dst=45514, payload='ping')
8:59644 recvived:Message(src=97497, dst=45514, payload='ping')
8:59644 recvived:Message(src=97497, dst=45514, payload='ping')
9:45514 recvived:Message(src=97497, dst=45514, payload='ping')
Hit target 9:45514, msg: Message(src=97497, dst=45514, payload='ping')
....
7:63306 recvived:Message(src=45514, dst=97497, payload='pong')
7:63306 send_port Message(src=45514, dst=97497, payload='pong') 1
4:70479 recvived:Message(src=45514, dst=97497, payload='pong')
4:70479 send_port Message(src=45514, dst=97497, payload='pong') 0
1:77754 recvived:Message(src=45514, dst=97497, payload='pong')
1:77754 send_port Message(src=45514, dst=97497, payload='pong') 0
0:97497 recvived:Message(src=45514, dst=97497, payload='pong')
Hit target 0:97497, msg: Message(src=45514, dst=97497, payload='pong')
但在第二次,就直接使用了之前记忆的最短路径。直接使用的 0-1-3-8-9 这条路径,返回是9-8-3-1-0:
----------第2次-------------
------------------------------
0:25318 recvived:Message(src=25318, dst=68139, payload='ping')
0:25318 send_port Message(src=25318, dst=68139, payload='ping') 0
0:25318 recvived:Message(src=25318, dst=68139, payload='')
0:25318 send_port Message(src=25318, dst=68139, payload='') 0
1:77154 recvived:Message(src=25318, dst=68139, payload='ping')
1:77154 send_port Message(src=25318, dst=68139, payload='ping') 2
1:77154 recvived:Message(src=25318, dst=68139, payload='')
1:77154 send_port Message(src=25318, dst=68139, payload='') 2
3:33212 recvived:Message(src=25318, dst=68139, payload='ping')
3:33212 send_port Message(src=25318, dst=68139, payload='ping') 2
3:33212 recvived:Message(src=25318, dst=68139, payload='')
3:33212 send_port Message(src=25318, dst=68139, payload='') 2
8:21762 recvived:Message(src=25318, dst=68139, payload='ping')
8:21762 send_port Message(src=25318, dst=68139, payload='ping') 1
8:21762 recvived:Message(src=25318, dst=68139, payload='')
8:21762 send_port Message(src=25318, dst=68139, payload='') 1
9:68139 recvived:Message(src=25318, dst=68139, payload='ping')
Hit target 9:68139, msg: Message(src=25318, dst=68139, payload='ping')
9:68139 recvived:Message(src=25318, dst=68139, payload='')
Hit target 9:68139, msg: Message(src=25318, dst=68139, payload='')
9:68139 recvived:Message(src=68139, dst=25318, payload='pong')
9:68139 send_port Message(src=68139, dst=25318, payload='pong') 2
9:68139 recvived:Message(src=68139, dst=25318, payload='Please say something.')
9:68139 send_port Message(src=68139, dst=25318, payload='Please say something.') 2
8:21762 recvived:Message(src=68139, dst=25318, payload='pong')
8:21762 send_port Message(src=68139, dst=25318, payload='pong') 2
8:21762 recvived:Message(src=68139, dst=25318, payload='Please say something.')
8:21762 send_port Message(src=68139, dst=25318, payload='Please say something.') 2
3:33212 recvived:Message(src=68139, dst=25318, payload='pong')
3:33212 send_port Message(src=68139, dst=25318, payload='pong') 0
3:33212 recvived:Message(src=68139, dst=25318, payload='Please say something.')
3:33212 send_port Message(src=68139, dst=25318, payload='Please say something.') 0
1:77154 recvived:Message(src=68139, dst=25318, payload='pong')
1:77154 send_port Message(src=68139, dst=25318, payload='pong') 0
1:77154 recvived:Message(src=68139, dst=25318, payload='Please say something.')
1:77154 send_port Message(src=68139, dst=25318, payload='Please say something.') 0
0:25318 recvived:Message(src=68139, dst=25318, payload='pong')
Hit target 0:25318, msg: Message(src=68139, dst=25318, payload='pong')
0:25318 recvived:Message(src=68139, dst=25318, payload='Please say something.')
Hit target 0:25318, msg: Message(src=68139, dst=25318, payload='Please say something.')
其中删除了一个关键节点,改变了最短路径的走势,我们发现也能自适应。新的返回路径变成了9-8-7-4-1:
----------删除节点后-------------
0:66224 recvived:Message(src=66224, dst=66280, payload='')
0:66224 send_port Message(src=66224, dst=66280, payload='') 0
1:48729 recvived:Message(src=66224, dst=66280, payload='')
1:48729 send_port Message(src=66224, dst=66280, payload='') 2
3:53518 recvived:Message(src=66224, dst=66280, payload='')
6:13184 recvived:Message(src=66224, dst=66280, payload='')
7:33667 recvived:Message(src=66224, dst=66280, payload='')
7:33667 send_port Message(src=66224, dst=66280, payload='') 3
5:32869 recvived:Message(src=66224, dst=66280, payload='')
8:85701 recvived:Message(src=66224, dst=66280, payload='')
8:85701 send_port Message(src=66224, dst=66280, payload='') 1
9:66280 recvived:Message(src=66224, dst=66280, payload='')
Hit target 9:66280, msg: Message(src=66224, dst=66280, payload='')
9:66280 recvived:Message(src=66280, dst=66224, payload='Please say something.')
9:66280 send_port Message(src=66280, dst=66224, payload='Please say something.') 2
8:85701 recvived:Message(src=66280, dst=66224, payload='Please say something.')
7:33667 recvived:Message(src=66280, dst=66224, payload='Please say something.')
7:33667 send_port Message(src=66280, dst=66224, payload='Please say something.') 1
4:82756 recvived:Message(src=66280, dst=66224, payload='Please say something.')
4:82756 send_port Message(src=66280, dst=66224, payload='Please say something.') 0
1:48729 recvived:Message(src=66280, dst=66224, payload='Please say something.')
1:48729 send_port Message(src=66280, dst=66224, payload='Please say something.') 0
0:66224 recvived:Message(src=66280, dst=66224, payload='Please say something.')
Hit target 0:66224, msg: Message(src=66280, dst=66224, payload='Please say something.')
于是我们成功的模拟了交换机的功能,并创造出来一个自适应网络。有以下特点:
1. 记忆功能,能记住最先接受到的消息,来自哪个位置;
2. 自适应,个别节点掉线,不影响整体网络的功能;
3. 仅首次使用flood,后续用最短路径通讯(除非网络结构发生改变)。
而其只是基于一个简单的水流模型加上一点纪录。
恭喜你,你发明了互联网。
你也可以直接运行这段模拟代码。