基于Simpy的通信网络仿真工具(三):控制器与简单拓扑仿真

  • 控制器
  • 路径和流表
  • 交换机
  • 控制器仿真


基于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)

控制器仿真

网络拓扑如下

python 调用ADS仿真_1024程序员节

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

如果网络拓扑比较简单,可以逐一设置连接的方式进行仿真,但随着拓扑的复杂化,该项工作将会非常繁琐。同时,网络的通信时延、丢包率、吞吐量和抖动等指标也需要进行测量。