基于Simpy/Python的通信网络仿真工具(一):数据包生成,转发和接收
基于Simpy/Python的通信网络仿真工具(二):多主机、端口互联实现数据包转发与交换机仿真
在软件定义网络SDN中,控制器是网络的大脑,需要完成网络状态监测,路径规划和流表下发等工作(关于SDN,可参考博客SDN介绍(什么是SDN))。
控制器
实现控制器功能的代码如下
class Ctrler(object):
"""
功能: 控制器实现路径计算、生成和下发
参数
---------
topo # 网络拓扑,包括交换机和交换机,用于生成路由(networkx)
link_list # 设备连接情况, 用于根据routing生成转发流表
"""
def __init__(self, env, topo = None):
self.env = env
self.topo = topo # 网络拓扑,包括交换机和交换机,用于生成路由
self.flow_pools = simpy.Store(env) # 流表处理队列
self.switch_maps = None # 所属交换机列表
self.action = env.process(self.run())
# 流量请求处理
def run(self):
routings = {"H1-H2": ["H1", "S1", "S2", "H2"],
"H2-H1": ["H2", "S2", "S1", "H1"],
"H1-H3": ["H1", "S1", "S3", "H3"],
"H3-H1": ["H3", "S3", "S1", "H1"],
"H2-H3": ["H2", "S2", "S3", "H3"],
"H3-H2": ["H3", "S3", "S2", "H2"]
}
while True:
pkt = (yield self.flow_pools.get())
pkt_key = pkt.src + '-' +pkt.dst
routing = Routing(pkt_key=pkt_key, path=routings[pkt_key], time=round(self.env.now, 3))
for i in range(1, len(routing.path) - 1): # 首尾是非交换机
address_switch = self.switch_maps.get(routing.path[i]) # 找到路径中的交换机
address_switch.switch_monitor.routings.put(routing) # 将路径发送给交换机monitor
# 流量请求接受
def put(self, pkt):
self.flow_pools.put(pkt)
# 设置网络设备的src和实体类的映射
def setMaps(self, switch_maps):
self.switch_maps = switch_maps
控制器在接收到交换机上报的通信请求后,根据流量的源交换机和目的交换机地址计算出转发路径,并将这些路径转发给对应的交换机,以指导流量转发。
路径和流表
Routing和FLow类分别代表转发路径和流表,实现代码如下
class Routing(object):
"""
功能: 生成路由信息
参数
-------
pkt_key: 数据包key
path: 转发路径
time: 路径有效时间
"""
def __init__(self, pkt_key, path, time):
self.pkt_key = pkt_key
self.path = path
self.time = time
def __repr__(self):
return "pkt_key :{}, path :{}, time :{}".format(self.pkt_key, self.path, self.time)
class Flow(object):
"""
功能: 转发流表实体
参数
--------
pkt_key: 包key
port_id : 目标端口的id
gen_time : 流表生成时间
"""
def __init__(self, pkt_key, port_id, gen_time):
self.pkt_key = pkt_key
self.port_id = port_id
self.gen_time = gen_time
def __repr__(self):
return "pkt_key :{}, port_id :{}, time :{}".format(self.pkt_key, self.port_id, self.gen_time)
交换机
由于交换机需要和控制器交互与动态转发流量,因此需要拓展原始交换机的功能,以及增加端口的相关功能,实现如下
class SwitchMonitor(object):
"""
功能: switch的监控类
参数
-----------
env: 运行环境
address: 交换机地址
ctrler: 所属控制器
"""
def __init__(self, env, address, ctrler=None):
self.env = env
self.address = address
self.ctrler = ctrler # 定义所连接的控制器
self.port_lists = None # 交换机下的端口
self.routings = simpy.Store(env) # 记录已分配的路由
self.device_map = dict() # 网络设备连接表
self.action = env.process(self.genTables())
# 将路径请求信息上报到控制器
def put(self, pkt):
# print(pkt)
self.ctrler.put(pkt)
# 生成流表
def genTables(self):
while True:
routing = (yield self.routings.get())
loc = routing.path.index(self.address) # 找到交换机在路径中的位置编号
""" 生成流表 """
try :
pre_device_i = self.device_map[routing.path[loc - 1]] # 找到前一个设备在link_list中的编号,该编号和port_list中port的编号一致
except:
print(loc-1, loc, loc+1, routing)
print(self.address, pre_device_i)
next_device_i = self.device_map[routing.path[loc + 1]] # 找到下一个设备连接本交换机的端口id
# 通过前一个设备编号找到交换机上的对应端口
in_port = self.port_lists[pre_device_i].in_port
flow = Flow(pkt_key=routing.pkt_key, port_id=next_device_i, gen_time=routing.time) # 生成转发流
in_port.transflow_buffer.put(flow)
class Switch(object):
def __init__(self, env, address, port_num, send_rate, queue_limit, valid_time=None, ctrler=None, debug=False):
self.address = address # 交换机的地址
self.port_num_list = [i for i in range(port_num)] # 端口编号列表
self.port_list = [None for _ in range(port_num)] # 端口列表
self.switch_monitor = SwitchMonitor(env=env, address=address, ctrler=ctrler)
# 生成每个端口
for port_i in range(port_num):
trans_list = self.port_num_list.copy() # 对象数组采用copy
trans_list.remove(port_i) # 生成port_i 对应的 转发端口列表
self.port_list[port_i] = Port(
env, switch_addr = self.address, port_id = port_i, trans_list=trans_list, valid_time=valid_time, table=None, send_rate=send_rate, queue_limit=queue_limit, switch=self.switch_monitor, debug=debug)
# 将端口信息传给monitor
self.switch_monitor.port_lists = self.port_list
# 连接交换机内部的端口
for port in self.port_list:
trans_list = self.port_list.copy()
trans_list.remove(port) # 生成port 对应的 转发端口
port.setTransPort(target_list=trans_list) # 连接转发端口
# 设置转发表
def setTables(self, tables_list):
for i in range(len(tables_list)):
self.port_list[i].setTable(tables_list[i])
# 设置连接对象
def setLink(self, link_list, port_list):
"""
link_list : 连接对象(Port or Host)
port_list : 目标对象的端口编号,连接主机时为空
"""
for i in range(len(link_list)):
# 添加设备和端口的连接信息
self.switch_monitor.device_map[link_list[i].address] = i
if type(link_list[i]) is Host:
# 设置(out)连接对象
self.port_list[i].setOutObj(link_list[i].ps)
# 设置(in)连接对象
link_list[i].setLink(self.port_list[i])
else:
# 设置(out)连接对象, 交换机与交换机之间均为 in-->out
self.port_list[i].setOutObj(link_list[i].port_list[port_list[i]].in_port)
def __repr__(self):
return "address :{}".format(self.address)
新的交换机代码主要增加了SwitchMonitor模块,以收集Port上报的通信请求和向Port下发转发流表,Port主要修改如端口的内容
class PortIn(object):
"""
功能: 交换机包接收端口,通过加入一个出口选择器,实现根据源地址,目的地址转发数据
参数
----------
env : 运行环境
trans_list : 出口列表(list)
valid_time : 流表有效时间(int)
rec_arrivals : boolean 记录到达时间
absolute_arrivals : boolean 如果为True,记录真实的绝对到达时间,否则记录连续到达之间的时间间隔。
rec_waits : boolean 记录每个数据包的等待时间
table : 转发流表项
switch: 所属交换机
"""
def __init__(self, env, trans_list, valid_time, table, rec_arrivals=False, absolute_arrivals=False, rec_waits=False, switch=None, debug=False):
self.env = env
self.address = switch.address
self.out = [None for i in range(len(trans_list))] # 出口列表
self.trans_list = trans_list
self.table = dict() if table is None else table # 流表存储
self.wait_flow_buffer = [] # 等待流表下发
self.byte_size = 0 # Current size of the queue in bytes
self.debug = debug
self.valid_time = 100 if valid_time == 0 or valid_time is None else valid_time # 等待时间为0, 或者为空,设置为100
self.rec_waits = rec_waits
self.rec_arrivals = rec_arrivals
self.absolute_arrivals = absolute_arrivals
self.waits = [] # 记录等待时间
self.sends_time = [] # 记录发送时间
self.sends_id = [] # 记录发送的包名
self.sends_src = [] # 记录发送的包源地址
self.sends_dst = [] # 记录发送的包源地址
self.packets_rec = 0 # 记录发送包数量
self.bytes_rec = 0 # 记录发送字节数量
self.switch = switch # 所属交换机(SwitchMonitor)
self.pkt_buffer = simpy.Store(env) # 包等待区,用于未查询到流表
self.transflow_buffer = simpy.Store(env) # 流表接收区域
self.action1 = env.process(self.waitTable())
self.action2 = env.process(self.waitPkt())
# 执行转发
def transPkt(self, pkt):
pkt_key = pkt.src + '-' +pkt.dst
now = self.env.now
if self.rec_waits:
self.waits.append(self.env.now - pkt.time)
self.sends_id.append(pkt.id)
self.sends_src.append(pkt.src)
self.sends_dst.append(pkt.dst)
if self.rec_arrivals:
if self.absolute_arrivals:
self.sends_time.append(now)
else:
self.sends_time.append(now - self.last_arrival)
if pkt_key not in self.table: # 流表项不存在
self.pkt_buffer.put(pkt)
return
trans_port_id = self.table[pkt_key].port_id
if trans_port_id == 100 or trans_port_id not in self.trans_list: # 丢弃
return
if pkt.size <= 2000 * 8: # 包的最大字节数为2000
self.table.pop(pkt_key) # 完成传输,删除流表
target_port = self.trans_list.index(trans_port_id) # 通过包的源地址,目的地址查询ctrler下发的流表中,对应的转发端口
self.out[target_port].put(pkt)
self.packets_rec += 1
self.bytes_rec += pkt.size
if self.debug:
print("Port_in : ", pkt)
def put(self, pkt):
pkt_key = pkt.src + '-' +pkt.dst
if pkt_key in self.table: # 流表项存在
self.transPkt(pkt)
else:
if pkt_key not in self.wait_flow_buffer:
self.switch.put(pkt) # 流表申请中
self.wait_flow_buffer.append(pkt_key) # 记录该流表正在申请
self.pkt_buffer.put(pkt)
"""
等待首次流表下发的处理函数
"""
def waitTable(self):
while True:
flow = yield self.transflow_buffer.get() # 等待流表下发
flow.in_time = self.env.now
self.table[flow.pkt_key] = flow
if flow.pkt_key in self.wait_flow_buffer:
self.wait_flow_buffer.remove(flow.pkt_key) # 流表申请完成
def waitPkt(self):
while True:
pkt = yield self.pkt_buffer.get()
pkt_key = pkt.src + '-' +pkt.dst
if pkt_key in self.table: # 流表项和包匹配,进行转发
self.transPkt(pkt)
else:
self.pkt_buffer.put(pkt) # 不匹配,再次将包加入缓冲区
def __repr__(self):
return "switch: {}".format(self.switch.address)
class Port(object):
"""
功能: 接收和转发全功能端口,实现端口的接收和转发
参数
------
env: 运行环境
switch_addr: 交换机id
port_id: 端口的id
trans_list: 内部端口的连接列表, A_in --> B_out, C_out
table: 转发规则表, src-dst : port_out
valid_time: 流表有效时间
send_rate: out端口的传输速率
queue_limit: 队列最大长度
is_limit: 是否丢包
switch: 所属交换机,用于申请流表
debug: 输出执行信息
"""
def __init__(self, env, switch_addr, port_id, trans_list, table=None, valid_time=None,
send_rate=0, queue_limit=None, is_limit=True, switch=None, debug=False) -> None:
self.env = env
self.switch_id = switch_addr
self.port_id = port_id
self.queue = [] # 端口队列记录
self.drop = [] # 端口丢包记录
self.in_port = PortIn(env=env, trans_list=trans_list, valid_time=valid_time,
table=table, switch=switch, debug=debug)
self.out_port = PortOut(env=env, address=None, rate=send_rate, qlimit=queue_limit,
limit_bytes=is_limit, debug=debug)
# 设置转发表(判断包应该转发到哪个port的out)
def setTable(self, table):
self.in_port.table = table
# 设置out端口的连接对象
def setOutObj(self, out):
self.out_port.out = out
# 设置in端口的连接对象
def setTransPort(self, target_list):
for i in range(len(target_list)):
self.in_port.out[i] = target_list[i].out_port
# 获取out端口的队列
def getQueue(self):
return self.out_port.queue
# 输出out端口的丢包情况
def getOutDrop(self):
return self.out_port.out_drop
def __repr__(self):
return "switch_address :{}, port_id :{}".format(self.switch_id, self.port_id)
控制器仿真
网络拓扑如下
def constArrival():
return 1.0 # time interval
if __name__ == '__main__':
# 定义时隙
port_rate = 2000.0
valid_time = 1
env = simpy.Environment() # Create the SimPy environment
# 定义三个主机
h1 = Host(env, address="H1", dst_address=[
"H2", "H3"], interval=constArrival, rec_arrivals=True, debug=False)
h2 = Host(env, address="H2", dst_address=[
"H1", "H3"], interval=constArrival, rec_arrivals=True, debug=False)
h3 = Host(env, address="H3", dst_address=[
"H1", "H2"], interval=constArrival, rec_arrivals=True, debug=False)
# 定义控制器
ctrler = Ctrler(env=env, topo=None)
# 定义三个交换机
s1 = Switch(env=env, address="S1", port_num=3, send_rate=port_rate, queue_limit=1001,
valid_time=valid_time, ctrler=ctrler, debug=False)
s2 = Switch(env=env, address="S2", port_num=3, send_rate=port_rate, queue_limit=1001,
valid_time=valid_time, ctrler=ctrler, debug=False)
s3 = Switch(env=env, address="S3", port_num=3, send_rate=port_rate, queue_limit=1001,
valid_time=valid_time, ctrler=ctrler, debug=False)
# 连接交换机和主机
s1.setLink(link_list=[h1, s2, s3], port_list=[0, 0, 0])
s2.setLink(link_list=[s1, h2, s3], port_list=[1, 0, 1])
s3.setLink(link_list=[s1, s2, h3], port_list=[2, 2, 0])
# 向控制器映射交换机
ctrler.setMaps({"S1": s1, "S2": s2, "S3": s3})
env.run(until=UNTIL_TIME + 2) # 等待2s结束,将队列中的包全部接受完
receive_count = h1.ps.packets_rec + h2.ps.packets_rec + h3.ps.packets_rec
sent_count = h1.pg.packets_sent + h2.pg.packets_sent + h3.pg.packets_sent
print("received pkt count : {}".format(receive_count))
print("sent pkt count : {}".format(sent_count))
运行结果
received pkt count : 54
sent pkt count : 54
如果网络拓扑比较简单,可以逐一设置连接的方式进行仿真,但随着拓扑的复杂化,该项工作将会非常繁琐。同时,网络的通信时延、丢包率、吞吐量和抖动等指标也需要进行测量。
欢迎通过企鹅(488128665)进行交流!