From 0230d4834c86d98d2384b5344805df188098a379 Mon Sep 17 00:00:00 2001 From: bettleChen <2207153529@qq.com> Date: Wed, 12 Jul 2023 10:03:17 +0800 Subject: [PATCH] upload project --- .idea/.gitignore | 8 + .idea/NetworkAnalog.iml | 8 + .idea/inspectionProfiles/Project_Default.xml | 12 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 4 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + NetworkAnalog.py | 954 ++++++++++++++++++ NetworkAnalog/NetworkAnalog.py | 954 ++++++++++++++++++ NetworkAnalog/SimObjs.py | 757 ++++++++++++++ NetworkAnalog/dbUtil.py | 104 ++ NetworkAnalog/network.db | Bin 0 -> 40960 bytes NetworkAnalog/tkTest.py | 19 + README.md | 171 +++- SimObjs.py | 757 ++++++++++++++ datas/images/packet.png | Bin 0 -> 26968 bytes datas/images/主机.png | Bin 0 -> 19448 bytes datas/images/主机_tm.png | Bin 0 -> 16883 bytes datas/images/交换机.png | Bin 0 -> 5044 bytes datas/images/交换机_tm.png | Bin 0 -> 3666 bytes datas/images/路由器.png | Bin 0 -> 12094 bytes datas/images/路由器_tm.png | Bin 0 -> 11198 bytes datas/images/集线器.png | Bin 0 -> 10309 bytes datas/images/集线器_tm.png | Bin 0 -> 9634 bytes dbUtil.py | 104 ++ document.md | 26 + network.db | Bin 0 -> 40960 bytes tkTest.py | 19 + 28 files changed, 3916 insertions(+), 1 deletion(-) create mode 100644 .idea/.gitignore create mode 100644 .idea/NetworkAnalog.iml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 NetworkAnalog.py create mode 100644 NetworkAnalog/NetworkAnalog.py create mode 100644 NetworkAnalog/SimObjs.py create mode 100644 NetworkAnalog/dbUtil.py create mode 100644 NetworkAnalog/network.db create mode 100644 NetworkAnalog/tkTest.py create mode 100644 SimObjs.py create mode 100644 datas/images/packet.png create mode 100644 datas/images/主机.png create mode 100644 datas/images/主机_tm.png create mode 100644 datas/images/交换机.png create mode 100644 datas/images/交换机_tm.png create mode 100644 datas/images/路由器.png create mode 100644 datas/images/路由器_tm.png create mode 100644 datas/images/集线器.png create mode 100644 datas/images/集线器_tm.png create mode 100644 dbUtil.py create mode 100644 document.md create mode 100644 network.db create mode 100644 tkTest.py diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/NetworkAnalog.iml b/.idea/NetworkAnalog.iml new file mode 100644 index 0000000..d0876a7 --- /dev/null +++ b/.idea/NetworkAnalog.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..95ce354 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..9b688a8 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..aaea674 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/NetworkAnalog.py b/NetworkAnalog.py new file mode 100644 index 0000000..866c8a6 --- /dev/null +++ b/NetworkAnalog.py @@ -0,0 +1,954 @@ +import ipaddress +import sys +import threading +import ttkbootstrap as tk +from ttkbootstrap import * +from ttkbootstrap import ttk +from tkinter import messagebox +import re +from PIL import ImageTk, Image +import platform + +from SimObjs import SimPacket, SimHost, AllSimConnect, SimRouter, SimSwitch, SimHub, SimBase +from dbUtil import search, execute_sql, delete_obj, truncate_db + + +def validate_ip_address(ip_address): + """ + 匹配ip地址格式是否规范 + :param ip_address: IP地址 + :return: Boolean + """ + # 定义IP地址的正则表达式模式 + pattern_with_subnet = r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})$' + pattern_without_subnet = r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$' + # 使用re模块进行匹配 + match_with_subnet = re.match(pattern_with_subnet, ip_address) + match_without_subnet = re.match(pattern_without_subnet, ip_address) + if match_with_subnet: + # 带有子网掩码的IP地址 + # 检查每个组件的取值范围是否在 0-255 之间 + for group in match_with_subnet.groups()[:4]: + if not (0 <= int(group) <= 255): + return False + # 检查子网掩码的取值范围是否在 0-32 之间 + subnet_mask = int(match_with_subnet.groups()[4]) + if not (0 <= subnet_mask <= 32): + return False + return True + elif match_without_subnet: + # 不带子网掩码的IP地址 + # 检查每个组件的取值范围是否在 0-255 之间 + for group in match_without_subnet.groups(): + if not (0 <= int(group) <= 255): + return False + return True + else: + # IP地址格式不正确 + return False + + +class RouterConfigWindow(tk.Toplevel): + def __init__(self, parent, router_obj): + super().__init__(parent) + self.geometry("435x433+350+200") + self.title(f"{router_obj.ObjLabel}路由表配置") + self.router_obj = router_obj + self.interface_entries = [] + self.router_table = {} + self.create_interface_inputs() + self.create_router_table() + + def create_interface_inputs(self): + label_text = ["接口1", "接口2", "接口3", "接口4"] + for i in range(4): + label = tk.Label(self, text=label_text[i]) + label.grid(row=i, column=0, padx=10, pady=5, sticky="w") + entry = tk.Entry(self, width=20) + entry.grid(row=i, column=1, padx=10, pady=5, sticky="w") + self.interface_entries.append(entry) + button = tk.Button(self, text="添加", command=lambda index=i: self.add_router_entry(index)) + button.grid(row=i, column=2, padx=10, pady=5) + lab = LabelFrame(self, text="示例") + lab.grid(row=4, column=0, columnspan=3, sticky=W, padx=20) + Label(lab, text="10.1.2.0/24 或者 10.1.2.12" if self.router_obj.ObjType == 2 else "MAC11").pack() + + def create_router_table(self): + def on_right_click(event): + row = self.router_treeview.identify_row(event.y) # 获取鼠标位置的行索引 + if row: + self.router_treeview.selection_set(row) # 选中该行 + delete_menu.post(event.x_root, event.y_root) # 在鼠标位置弹出删除菜单 + + def delete_row(): + selected_items = self.router_treeview.selection() # 获取选中的行 + for item in selected_items: + ifs, network = int(self.router_treeview.item(item)["values"][0][-1:]), self.router_treeview.item(item)["values"][1] + self.router_obj.delete_config(ifs, network) + self.router_treeview.delete(item) + self.router_table_frame = tk.Frame(self) + self.router_table_frame.grid(row=5, column=0, columnspan=3, padx=10, pady=5) + self.router_treeview = ttk.Treeview(self.router_table_frame, columns=("Interface", "Route"), show="headings") + self.router_treeview.heading("Interface", text="接口") + self.router_treeview.heading("Route", text="网段") + self.router_treeview.pack(side="left", fill="both") + scrollbar = ttk.Scrollbar(self.router_table_frame, orient="vertical", command=self.router_treeview.yview) + scrollbar.pack(side="right", fill="y") + self.router_treeview.configure(yscrollcommand=scrollbar.set) + self.router_table = self.router_obj.router_table + self.router_treeview.bind("", on_right_click) + # 创建删除菜单 + delete_menu = tk.Menu(root, tearoff=False) + delete_menu.add_command(label="删除", command=delete_row) + self.update_router_table() + + def add_router_entry(self, index): + entry_text = self.interface_entries[index].get() + try: + ipaddress.ip_network(entry_text) + if isinstance(self.router_obj, SimRouter): + if not validate_ip_address(entry_text): + messagebox.showerror("注意", message="添加的网段信息格式不合格") + self.interface_entries[index].delete(0, tk.END) + self.focus_set() + return + if entry_text: + if index + 1 in self.router_table: + self.router_table[index + 1].append(entry_text) + else: + self.router_table[index + 1] = [entry_text] + self.interface_entries[index].delete(0, tk.END) + self.router_obj.add_config(entry_text, index + 1) + self.update_router_table() + except: + messagebox.showerror("注意", message="网段格式错误!网段示例如下:\n10.1.2.0/24\n10.1.2.12") + return + + def update_router_table(self): + self.router_treeview.delete(*self.router_treeview.get_children()) + for i, entrys in self.router_table.items(): + for entry in entrys: + self.router_treeview.insert("", "end", values=(f"接口{i}", entry)) + + +class SwitchConfigWindow(RouterConfigWindow): + def __init__(self, parent, router_obj): + super().__init__(parent, router_obj) + self.geometry("435x433+350+200") + self.title(f"{router_obj.ObjLabel}交换表配置") + self.router_obj = router_obj + self.interface_entries = [] + self.router_table = {} + self.create_interface_inputs() + self.create_router_table() + + def create_router_table(self): + def on_right_click(event): + row = self.router_treeview.identify_row(event.y) # 获取鼠标位置的行索引 + if row: + self.router_treeview.selection_set(row) # 选中该行 + delete_menu.post(event.x_root, event.y_root) # 在鼠标位置弹出删除菜单 + + def delete_row(): + selected_items = self.router_treeview.selection() # 获取选中的行 + for item in selected_items: + ifs, network = int(self.router_treeview.item(item)["values"][0][-1:]), self.router_treeview.item(item)["values"][1] + self.router_obj.delete_config(ifs, network) + self.router_treeview.delete(item) + + self.router_table_frame = tk.Frame(self) + self.router_table_frame.grid(row=5, column=0, columnspan=3, padx=10, pady=5) + self.router_treeview = ttk.Treeview(self.router_table_frame, columns=("Interface", "Route"), show="headings") + self.router_treeview.heading("Interface", text="接口") + self.router_treeview.heading("Route", text="mac") + self.router_treeview.pack(side="left", fill="both") + scrollbar = ttk.Scrollbar(self.router_table_frame, orient="vertical", command=self.router_treeview.yview) + scrollbar.pack(side="right", fill="y") + self.router_treeview.configure(yscrollcommand=scrollbar.set) + self.router_table = self.router_obj.mac_table + self.router_treeview.bind("", on_right_click) + # 创建删除菜单 + delete_menu = tk.Menu(root, tearoff=False) + delete_menu.add_command(label="删除", command=delete_row) + self.update_router_table() + + + def add_router_entry(self, index): + entry_text = self.interface_entries[index].get() + if isinstance(self.router_obj, SimRouter): + if not validate_ip_address(entry_text): + messagebox.showerror("注意", message="添加的网段信息格式不合格") + self.interface_entries[index].delete(0, tk.END) + self.focus_set() + return + if entry_text: + if index + 1 in self.router_table: + self.router_table[index + 1].append(entry_text) + else: + self.router_table[index + 1] = [entry_text] + self.interface_entries[index].delete(0, tk.END) + self.router_obj.add_config(entry_text, index + 1) + self.update_router_table() + + +class NetWorkAnalog(Canvas): + def __init__(self, master, **kwargs): + super().__init__(master, **kwargs) + self.master = master + self.router_img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/datas/images/路由器.png").resize((60, 60))) + self.switch_img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/datas/images/交换机.png").resize((60, 60))) + self.hub_img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/datas/images/集线器.png").resize((60, 60))) + self.host_img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/datas/images/主机.png").resize((60, 60))) + self.chose = self.host_img + self.AllSimObjs = {} + self.conns = [] + self.drawLine = True # 画线标志 + self.line_start_obj = None + self.line_end_obj = None + self.line_start_ifs = None + self.line_end_ifs = None + self.chose_obj = None + self.show_label_flag = True + self.show_interface_flag = True + self.create_widget() + + def is_chose(self): + """ + 当被选中时,绘制选中框 + :return: + """ + self.delete("rectangle") + self.create_rectangle(self.chose_obj.ObjX - 25, self.chose_obj.ObjY - 25, self.chose_obj.ObjX + 25, + self.chose_obj.ObjY + 25, outline="red", tags="rectangle") + + def tag_bind_event(self): + """ + 为每个子组件绑定事件 + :return: + """ + + def init(): + """ + 初始化方法,将连接对象赋值为初始化值 + :return: + """ + self.line_start_obj = None + self.line_end_obj = None + self.line_start_ifs = None + self.line_end_ifs = None + + def show_menu(event, tag: SimBase): + """ + 右击组件弹出选择接口框 + :param event: + :param tag: + :return: + """ + menu = Menu(self, tearoff=0) + flag = False + for conn in tag.connections: + i = tag.connections.index(conn) + if not isinstance(conn, AllSimConnect): + menu.add_command(label=f"接口{i + 1}", command=lambda num=i + 1: interface_selected(num, tag)) + flag = True + if not flag: + menu.add_command(label="暂无可用接口", state="disabled") + menu.post(event.x_root, event.y_root) + + def interface_selected(interface, tag): + """ + 选择接口回调 + :param interface: + :return: + """ + if self.line_start_ifs is None: + self.line_start_ifs = interface + self.line_start_obj = tag + self.drawLine = True + self.bind("", lambda event: right_motion(event)) + self.bind("", quit) + self.focus_set() # 获取焦点 + else: + self.line_end_ifs = interface + self.line_end_obj = tag + self.delete("Line") + flag = False + if self.line_start_obj.ObjID == self.line_end_obj.ObjID: + messagebox.showerror("注意!", message="不能连接自己") + return + conn = AllSimConnect(self, self.line_start_obj, self.line_start_ifs, self.line_end_obj, + self.line_end_ifs) + for conn_obj in self.line_start_obj.connections: # 判断两个连接对象是否已经连接过了 + if conn_obj == conn: + flag = True + if not flag: + self.line_start_obj.connections[self.line_start_ifs - 1] = conn + self.line_end_obj.connections[self.line_end_ifs - 1] = conn + conn.save() + self.conns.append(conn) + init() + self.unbind("") + else: + del conn + self.delete("Line") + self.unbind("") + init() + messagebox.showerror("注意!", message="已经连接过了") + + def right_motion(event): + """ + 移动鼠标 + :param event: + :param tag: + :return: + """ + if self.drawLine: + self.delete("Line") + x, y = event.x, event.y + if not (150 < x < 650 and 40 < y < 490): + if x < 150: + x = 150 + elif x > 650: + x = 650 + if y < 40: + y = 40 + elif y > 490: + y = 490 + self.create_line(self.line_start_obj.ObjX, self.line_start_obj.ObjY, x + 8, y + 8, fill="#5b9bd5", + width=1, + tags="Line") + return + self.create_line(self.line_start_obj.ObjX, self.line_start_obj.ObjY, x + 8, y + 8, fill="#5b9bd5", + width=1, tags="Line") + + def quit(event): + self.delete("Line") # 删除刚刚产生的连接线 + self.drawLine = False # 关闭画线标志 + init() + + for tag_id, tag in self.AllSimObjs.items(): + self.tag_bind(tag.ObjID, "", lambda event, tag=tag: show_menu(event, tag)) # 绑定右击事件 + + def bind_event(self, component, name): + # todo: 绑定事件 + """ + 绑定事件 + :param component: 组件对象 + :param name: 组件名称 + """ + def move(event, name): + """ + 鼠标左键松开事件 + :param event: 事件对象 + """ + if 170 < event.x < 630 and 60 < event.y < 470: # 在方框内,无变化 + if name == "路由器": + tag = SimRouter(self, event.x, event.y) + elif name == "集线器": + tag = SimHub(self, event.x, event.y) + elif name == "交换机": + tag = SimSwitch(self, event.x, event.y) + else: + tag = SimHost(self, event.x, event.y) + self.AllSimObjs[tag.ObjID] = tag + tag.save() + self.tag_bind_event() + else: + self.delete("L") + + def motion(event, name): + """ + 鼠标拖动事件 + :param event: 事件对象 + """ + if name == "路由器": + self.chose = self.router_img + elif name == "集线器": + self.chose = self.hub_img + elif name == "交换机": + self.chose = self.switch_img + else: + self.chose = self.host_img + self.delete("L") + if 170 < event.x < 630 and 60 < event.y < 470: # 在方框内,无变化 + pass + else: # 在方框外,显示禁止放置标识 + self.create_oval(event.x - 10, event.y - 45, event.x + 10, event.y - 25, outline="red", width=2, + tags="L") + self.create_line(event.x - 7, event.y - 42, event.x + 8, event.y - 27, fill="red", width=2, tags="L") + self.create_image(event.x - 30, event.y - 30, image=self.chose, anchor="nw", tags="L") + + self.tag_bind(component, "", lambda event: move(event, name)) + self.tag_bind(component, "", lambda event: motion(event, name)) + + def reload_data(self): + # todo: 加载上一次程序运行的数据 + """ + 加载上一次程序运行时的数据 + :return: + """ + self.AllSimObjs = {} + self.conns = [] + sim_obj_sql = "select * from sim_objs" + sim_obj_data = search(sim_obj_sql) + for index, row in sim_obj_data.iterrows(): # 初始化组件对象 + sim_type = row["ObjType"] + ObjX = row["ObjX"] + ObjY = row["ObjY"] + ConfigCorrect = row["ConfigCorrect"] + ObjLable = row["ObjLabel"] + ObjID = row["ObjID"] + if sim_type == 1: + tag = SimHost(self, ObjX, ObjY, ObjID, ConfigCorrect, ObjLable) + elif sim_type == 2: + tag = SimRouter(self, ObjX, ObjY, ObjID, ConfigCorrect, ObjLable) + elif sim_type == 3: + tag = SimSwitch(self, ObjX, ObjY, ObjID, ConfigCorrect, ObjLable) + else: + tag = SimHub(self, ObjX, ObjY, ObjID, ConfigCorrect, ObjLable) + self.AllSimObjs[tag.ObjID] = tag + + sim_conn_sql = "select s.conn_id, ConfigCorrect, node_id, node_ifs from sim_conn s join conn_config c on s.conn_id=c.conn_id" + sim_conn_data = search(sim_conn_sql) + conn_datas = {} + for index, conn in sim_conn_data.iterrows(): + if (conn["conn_id"], conn["ConfigCorrect"]) not in conn_datas: + conn_datas[(conn["conn_id"], conn["ConfigCorrect"])] = [(conn["node_id"], conn["node_ifs"])] + else: + conn_datas[(conn["conn_id"], conn["ConfigCorrect"])].append((conn["node_id"], conn["node_ifs"])) + for key, value in conn_datas.items(): + conn_obj = AllSimConnect(self, self.AllSimObjs[value[0][0]], value[0][1], + self.AllSimObjs[value[1][0]], value[1][1], key[1]) + self.AllSimObjs[value[0][0]].connections[value[0][1] - 1] = conn_obj # 将连接对象传入组件对象 + self.AllSimObjs[value[1][0]].connections[value[1][1] - 1] = conn_obj + self.conns.append(conn_obj) + conn_obj.ConfigCorrect = key[1] + self.tag_bind_event() + + def delete_obj(self): + # todo: 删除对象 + """ + 选中删除对象 + :return: + """ + if self.chose_obj is None: + messagebox.showerror("注意", message="请先选择要删除的对象!") + return + ask = messagebox.askquestion(title='确认操作', message='确认删除该对象吗?') + if ask == "no": + return + self.delete(self.chose_obj.ObjID) # 删除图片 + self.delete(self.chose_obj.ObjID + "text") # 删除标签 + for conn in self.chose_obj.connections: # 删除该对象的所有连接线 + if isinstance(conn, AllSimConnect): + conn.delete_line() + delete_sql = f"delete from sim_conn where conn_id in (select conn_id from conn_config where node_id='{self.chose_obj.ObjID}')" + execute_sql(delete_sql) + delete_obj(self.chose_obj.ObjID) + self.delete("rectangle") + self.reload_data() + + def delete_line(self): + # todo: 删除连接线 + """ + 删除连接线 + :return: + """ + if self.chose_obj is None: + messagebox.showerror("注意", message="请先选择要删除连接线的对象!") + return + conn_sql = f""" + select + s.conn_id, ConfigCorrect, node_id, node_ifs + from sim_conn s + join conn_config c on s.conn_id=c.conn_id + where node_id='{self.chose_obj.ObjID}' + """ + conn_data = search(conn_sql) + conn_names = {} + for index, conn in conn_data.iterrows(): + if conn["conn_id"] not in conn_names: + conn_names[conn["conn_id"]] = [(conn["node_id"], conn["node_ifs"])] + else: + conn_names[conn["conn_id"]].append((conn["node_id"], conn["node_ifs"])) + child_d = tk.Toplevel() + child_d.title(f"{self.chose_obj.ObjLabel}的连接线配置") + child_d.geometry('300x200+450+200') + child_d.grab_set() + cv1 = Canvas(child_d) + cv1.pack() + value = StringVar() + combobox = ttk.Combobox( + master=child_d, # 父容器 + height=12, # 高度,下拉显示的条目数量 + width=15, # 宽度 + state='readonly', # readonly(只可选) + font=('', 18), # 字体 + textvariable=value, # 通过StringVar设置可改变的值 + values=list(conn_names.keys()), # 设置下拉框的选项 + ) + + def del_line(): + if value.get() == "": + messagebox.showerror("注意", message="请选择需要删除的连接线!") + return + conn_sql = f""" + select + conn_id, node_id, node_ifs + from conn_config + where conn_id='{value.get()}' + """ + conn_data = search(conn_sql) + delete_sql = f"delete from sim_conn where conn_id='{value.get()}'" + execute_sql(delete_sql) + conn_names = {} + for index, conn in conn_data.iterrows(): + if conn["conn_id"] not in conn_names: + conn_names[conn["conn_id"]] = [(conn["node_id"], conn["node_ifs"])] + else: + conn_names[conn["conn_id"]].append((conn["node_id"], conn["node_ifs"])) + for data in conn_names[value.get()]: + self.AllSimObjs[data[0]].connections[data[1] - 1].delete_line() + self.AllSimObjs[data[0]].connections[data[1] - 1] = None + child_d.destroy() + messagebox.showinfo("提示", f"连接线{value.get()}删除成功!") + + btn_yes = tk.Button(child_d, text='确定', font=('黑体', 12), height=1, command=del_line) + btn_yes.place(x=240, y=20) + combobox.place(x=20, y=20) + + def update_tag_name(self): + # todo: 更新组件名称 + """ + 更新组件名称 + :return: + """ + if self.chose_obj is None: + messagebox.showerror("注意", message="请先选择要更新的对象!") + return + child1 = tk.Toplevel() + child1.title(self.chose_obj.ObjLabel + "的标签信息") + child1.geometry('240x100+450+250') + child1.grab_set() # 设置组件焦点抓取。使焦点在释放之前永远保持在这个组件上,只能在这个组件上操作 + tk.Label(child1, text='原标签:' + self.chose_obj.ObjLabel, + font=('黑体', 12)).grid(row=0, column=0, columnspan=2, sticky='w') + tk.Label(child1, text='新标签:', font=('黑体', 12)).grid(row=1, column=0, sticky='w') # ,sticky='w'靠左显示 + new_name = tk.Entry(child1, font=('黑体', 12), textvariable=tk.StringVar()) + new_name.grid(row=1, column=1) + + def update_tag(): + name = new_name.get() + if name == "": + messagebox.showerror("注意", message="请输入新名称!") + return + self.chose_obj.ObjLabel = name + self.chose_obj.update() + for conn in self.chose_obj.connections: + if isinstance(conn, AllSimConnect): + conn.update_info(self.chose_obj.ObjID) + child1.destroy() + messagebox.showinfo("提示", message="修改成功!") + + tk.Button(child1, text='确定', font=('黑体', 10), height=1, command=update_tag).grid(row=2, column=0, + sticky='e') + tk.Button(child1, text='取消', font=('黑体', 10), height=1, command=child1.destroy).grid(row=2, column=1, + sticky='e') + + def network_config(self): + # todo: 网络配置 + """ + 网络配置 + :return: + """ + if self.chose_obj is None: + messagebox.showerror("注意", message="请先选择要配置的对象!") + return + if len(self.chose_obj.get_config()) == 0: + messagebox.showerror("注意", message="请先给对象添加连接线!") + return + child_r = tk.Toplevel() + child_r.title(self.chose_obj.ObjLabel + "的网络配置信息") + child_r.geometry('530x395+350+200') + child_r.grab_set() # 设置组件焦点抓取。使焦点在释放之前永远保持在这个组件上,只能在这个组件上操作 + ifs_frame = tk.Frame(child_r) + ifs_frame.grid(row=1, column=0, columnspan=4) # 装接口的框架 + ifs_frame_YN = tk.Frame(child_r) + ifs_frame_YN.grid(row=2, column=0, columnspan=4) + num = 0 + datas = {} + for conn in self.chose_obj.connections: + if isinstance(conn, AllSimConnect): + index = self.chose_obj.connections.index(conn) + num_label = tk.LabelFrame(ifs_frame, text=f'接口{index + 1}') + num_label.grid(row=num, column=0, padx=18, ipady=5) + tk.Label(num_label, text='MAC:', font=('黑体', 10)).grid(row=0, column=0) + mac_en = tk.Entry(num_label, font=('黑体', 12), textvariable=tk.StringVar(), state=DISABLED if self.chose_obj.ObjType not in [1, 2, 3] else NORMAL) + mac_en.insert('0', str(self.chose_obj.interface[index].get("mac", ""))) + mac_en.grid(row=0, column=1, padx=10) + tk.Label(num_label, text='IP:', font=('黑体', 10)).grid(row=1, column=0) + ip_en = tk.Entry(num_label, font=('黑体', 12), textvariable=tk.StringVar(), state=DISABLED if self.chose_obj.ObjType not in [1, 2] else NORMAL) + ip_en.insert('0', str(self.chose_obj.interface[index].get("ip", ""))) + ip_en.grid(row=1, column=1, padx=10) + tk.Label(num_label, text='端口:', font=('黑体', 10)).grid(row=0, column=2) + port_en = tk.Entry(num_label, font=('黑体', 12), textvariable=tk.StringVar(), state=DISABLED if self.chose_obj.ObjType != 1 else NORMAL) + port_en.insert('0', str(self.chose_obj.interface[index].get("conn_port", ""))) + port_en.grid(row=0, column=3, padx=10) + tk.Label(num_label, text='应用层地址:', font=('黑体', 10)).grid(row=1, column=2) + addr_en = tk.Entry(num_label, font=('黑体', 12), textvariable=tk.StringVar(), state=DISABLED if self.chose_obj.ObjType != 1 else NORMAL) + addr_en.insert('0', str(self.chose_obj.interface[index].get("addr", ""))) + addr_en.grid(row=1, column=3, padx=10) + num += 1 + datas[index + 1] = {"mac": mac_en, "ip": ip_en, "conn_port": port_en, "addr": addr_en} + + num_label = tk.LabelFrame(ifs_frame, text=f'示例') + num_label.grid(row=num, column=0, padx=18, ipady=5) + tk.Label(num_label, text='MAC:', font=('黑体', 10)).grid(row=0, column=0) + mac_en = tk.Entry(num_label, font=('黑体', 12), textvariable=tk.StringVar()) + mac_en.insert('0', "MAC11") + mac_en.config(state=READONLY) + mac_en.grid(row=0, column=1, padx=10) + tk.Label(num_label, text='IP:', font=('黑体', 10)).grid(row=1, column=0) + ip_en = tk.Entry(num_label, font=('黑体', 12), textvariable=tk.StringVar()) + ip_en.insert(0, "10.1.1.10") + ip_en.config(state=READONLY) + ip_en.grid(row=1, column=1, padx=10) + tk.Label(num_label, text='端口:', font=('黑体', 10)).grid(row=0, column=2) + port_en = tk.Entry(num_label, font=('黑体', 12), textvariable=tk.StringVar()) + port_en.insert('0', "80") + port_en.config(state=READONLY) + port_en.grid(row=0, column=3, padx=10) + tk.Label(num_label, text='应用层地址:', font=('黑体', 10)).grid(row=1, column=2) + addr_en = tk.Entry(num_label, font=('黑体', 12), textvariable=tk.StringVar()) + addr_en.insert('0', "10.1.2.10:10810:Name3") + addr_en.config(state=READONLY) + addr_en.grid(row=1, column=3, padx=10) + def commit(): + self.chose_obj.config(datas) + child_r.destroy() + + tk.Button(ifs_frame_YN, text='确定', font=('黑体', 10), height=1, command=commit).grid(row=0, column=0, + padx=20) # ,sticky="w" + tk.Button(ifs_frame_YN, text='取消', font=('黑体', 10), height=1, + command=child_r.destroy).grid(row=0, column=2, padx=20) + + def router_table_config(self): + # todo: 路由表配置 + """ + 路由表配置 + :return: + """ + if self.chose_obj is None: + messagebox.showerror("注意", message="请先选择要配置的对象!") + return + if self.chose_obj.ObjType != 2: + messagebox.showerror("注意", message="请选择路由器对象!") + return + RouterConfigWindow(self, self.chose_obj) + + def mac_table_config(self): + # todo: 交换表配置 + """ + 交换表配置 + :return: + """ + if self.chose_obj is None: + messagebox.showerror("注意", message="请先选择要配置的对象!") + return + if self.chose_obj.ObjType != 3: + messagebox.showerror("注意", message="请选择交换机对象!") + return + SwitchConfigWindow(self, self.chose_obj) + + def show_label(self): + """ + 显示/不显示标签 + :return: + """ + self.show_label_flag = not self.show_label_flag + if self.show_label_flag: + for tag in self.AllSimObjs.values(): + tag.create_img() + else: + for tag in self.AllSimObjs.values(): + self.delete(tag.ObjID + "text") + + def show_interface(self): + # todo: 显示/不显示接口 + """ + 显示/不显示接口 + :return: + """ + self.show_interface_flag = not self.show_interface_flag + if self.show_interface_flag: + for conn in self.conns: + conn.draw_line() + else: + for conn in self.conns: + self.delete(conn.NobjS.ObjID + conn.NobjE.ObjID) + + def show_network_config(self): + # todo: 显示网络配置信息 + """ + 显示网络配置信息 + :return: + """ + if self.chose_obj is None: + messagebox.showerror("注意", message="请先选择要显示的对象!") + return + self.delete("netSet") + self.create_text(740, 120, text="(" + self.chose_obj.ObjLabel + ")", anchor="n", font=('微软雅黑', 10, 'bold'), + fill="#7030a0", tags="netSet") + self.create_text(675, 145, text=self.chose_obj, + anchor="nw", font=('宋体', 11), tags="netSet") + + def show_router_config(self): + # todo: 显示路由表交换表信息 + """ + 显示路由交换表信息 + :return: + """ + if self.chose_obj is None: + messagebox.showerror("注意", message="请先选择要显示的对象!") + return + if self.chose_obj.ObjType != 2 and self.chose_obj.ObjType != 3: + messagebox.showerror("注意", message="请选择路由器/交换机对象!") + return + self.delete("routerSet") + self.create_text(905, 120, text="(" + self.chose_obj.ObjLabel + ")", anchor="n", font=('微软雅黑', 10, 'bold'), + fill="#7030a0", tags="routerSet") + self.create_text(835, 145, text=self.chose_obj.get_table_config(), + anchor="nw", font=('宋体', 11), tags="routerSet") + + def send_packet(self): + """ + 发送数据包 + :return: + """ + if self.chose_obj is None: + messagebox.showerror("注意", message="请先选择要显示的对象!") + return + if self.chose_obj.ConfigCorrect != 1: + messagebox.showerror("注意", message="请先对选择对象进行网络配置!") + return + if self.chose_obj.ObjType != 1: + messagebox.showerror("注意", message="请选择主机对象!") + return + child2 = tk.Toplevel() + child2.title("数据包配置") + child2.geometry('240x100+450+250') + child2.grab_set() # 设置组件焦点抓取。使焦点在释放之前永远保持在这个组件上,只能在这个组件上操作 + tk.Label(child2, text='目的IP:', font=('黑体', 12)).grid(row=0, column=0, columnspan=2, sticky='w') + packet_ip = tk.Entry(child2, font=('黑体', 12), textvariable=tk.StringVar()) + packet_ip.grid(row=0, column=1) + tk.Label(child2, text='目的MAC:', font=('黑体', 12)).grid(row=1, column=0, sticky='w') # ,sticky='w'靠左显示 + packet_mac = tk.Entry(child2, font=('黑体', 12), textvariable=tk.StringVar()) + packet_mac.grid(row=1, column=1) + tk.Label(child2, text='消息:', font=('黑体', 12)).grid(row=2, column=0, sticky='w') # ,sticky='w'靠左显示 + packet_message = tk.Entry(child2, font=('黑体', 12), textvariable=tk.StringVar()) + packet_message.grid(row=2, column=1) + + def send(): + """ + 发送数据包 + :return: + """ + if packet_ip.get() == "": + messagebox.showerror("注意", message="ip地址不能为空") + return + if not validate_ip_address(packet_ip.get()): + messagebox.showerror("注意", message="IP地址不规范!") + return + if packet_mac.get() == "": + messagebox.showerror("注意", message="mac地址不能为空!") + return + if packet_message.get() == "": + messagebox.showerror("注意", message="消息不能为空!") + return + self.chose_obj.create_packet(packet_ip.get(), + packet_mac.get(), + packet_message.get()) + child2.destroy() + + tk.Button(child2, text='确定', font=('黑体', 10), height=1, command=send).grid(row=3, column=0, sticky='e') + tk.Button(child2, text='取消', font=('黑体', 10), height=1, command=child2.destroy).grid(row=3, column=1, + sticky='e') + + def send_packet_list(self): + """ + 批量发送数据包 + :return: + """ + hosts = {} + for tag in self.AllSimObjs.values(): + if tag.ObjType == 1 and tag.ConfigCorrect == 1: + hosts[tag.ObjLabel] = tag.ObjID + child2 = tk.Toplevel() + child2.title("批量数据包配置") + child2.geometry('400x420+450+200') + tk.Label(child2, text='目的IP:', font=('黑体', 12)).grid(row=0, column=0, columnspan=2, sticky='w', ipady=10) + packet_ip = tk.Entry(child2, font=('黑体', 12), textvariable=tk.StringVar()) + packet_ip.grid(row=0, column=1, pady=5) + tk.Label(child2, text='目的MAC:', font=('黑体', 12)).grid(row=1, column=0, sticky='w', + pady=5) # ,sticky='w'靠左显示 + packet_mac = tk.Entry(child2, font=('黑体', 12), textvariable=tk.StringVar()) + packet_mac.grid(row=1, column=1, pady=5) + tk.Label(child2, text='消息:', font=('黑体', 12)).grid(row=2, column=0, sticky='w', pady=5) # ,sticky='w'靠左显示 + packet_message = tk.Entry(child2, font=('黑体', 12), textvariable=tk.StringVar()) + packet_message.grid(row=2, column=1, pady=5) + host = StringVar() + tk.Label(child2, text='发送主机:', font=('黑体', 12)).grid(row=3, column=0, sticky='w', + pady=5) # ,sticky='w'靠左显示 + combobox = ttk.Combobox( + master=child2, # 父容器 + height=5, # 高度,下拉显示的条目数量 + width=12, # 宽度 + state='readonly', # readonly(只可选) + font=('', 18), # 字体 + textvariable=host, # 通过StringVar设置可改变的值 + values=list(hosts.keys()), # 设置下拉框的选项 + ) + combobox.grid(row=3, column=1, pady=5) + packet_frame = tk.Frame(child2) + packet_frame.grid(row=5, column=0, columnspan=3, padx=10, pady=5) + packet_treeview = ttk.Treeview(packet_frame, columns=("source_host", "ip", "mac", "message"), show="headings") + packet_treeview.heading("source_host", text="发送主机") + packet_treeview.column("source_host", width=60, anchor=CENTER) + packet_treeview.heading("ip", text="IP") + packet_treeview.column("ip", width=120, anchor=CENTER) + packet_treeview.heading("mac", text="MAC") + packet_treeview.column("mac", width=60, anchor=CENTER) + packet_treeview.heading("message", text="消息") + packet_treeview.column("message", width=120, anchor=CENTER) + packet_treeview.pack(side="left", fill="both") + scrollbar = ttk.Scrollbar(packet_frame, orient="vertical", command=packet_treeview.yview) + scrollbar.pack(side="right", fill="y") + packet_treeview.configure(yscrollcommand=scrollbar.set) + packets = [] + + def add(): + ip = packet_ip.get() + mac = packet_mac.get() + message = packet_message.get() + chose_host = host.get() + if ip == "" or mac == "" or message == "" or chose_host == "": + messagebox.showerror("注意", message="输入框不能为空") + return + packet = SimPacket(self.AllSimObjs[hosts[chose_host]].interface[0]["ip"], + self.AllSimObjs[hosts[chose_host]].interface[0]["mac"], + ip, mac, message) + packets.append((self.AllSimObjs[hosts[chose_host]], packet)) + packet_treeview.delete(*packet_treeview.get_children()) + for datas in packets: + tag: SimBase = datas[0] + packet_data: SimPacket = datas[1] + packet_treeview.insert("", "end", values=( + tag.ObjLabel, packet_data.destination_ip, packet_data.destination_mac, packet_data.message)) + + def send(): + if len(packets) == 0: + messagebox.showerror("注意", message="请至少添加一条数据包!") + return + for data in packets: + threading.Thread(target=data[0].send, args=(data[1],)).start() + child2.destroy() + + tk.Button(child2, text="添加数据包", font=('黑体', 12), height=3, command=add).grid(row=0, column=2, rowspan=4) + button_frame = Frame(child2) + tk.Button(button_frame, text="发送", font=('黑体', 12), height=1, command=send).pack(side=RIGHT, padx=20) + tk.Button(button_frame, text="取消", font=('黑体', 12), height=1, command=child2.destroy).pack(side=RIGHT) + button_frame.grid(row=6, column=0, columnspan=3) + + def clear_canvas(self): + """ + 清除画布 + :return: + """ + ask = messagebox.askquestion(title='确认操作', message='确认要清除画布吗?') + if ask == "no": + return + truncate_db() # 清除数据库 + for tag_id, tag in self.AllSimObjs.items(): + self.delete(tag_id) + self.delete(tag_id + "text") + for conn in self.conns: + conn.delete_line() + self.delete("rectangle") + self.AllSimObjs.clear() + self.conns.clear() + + def create_widget(self): + # todo: 创建页面 + """ + 创建整体页面布局 + :return: + """ + self.create_rectangle(150, 40, 650, 490, outline="#7f6000", width=3) # 矩形框,左上角坐标,右下角坐标 + self.create_rectangle(660, 100, 815, 400, outline="#ffff00", width=3, fill="#f4b88e") # 矩形框,左上角坐标,右下角坐标 + self.create_text(735, 105, text="网络配置信息", anchor="n", font=('微软雅黑', 11, 'bold')) + self.create_rectangle(825, 100, 1000, 400, outline="#ffff00", width=3, fill="#f4b88e") # 矩形框,左上角坐标,右下角坐标 + self.create_text(915, 105, text="路由/交换表信息", anchor="n", font=('微软雅黑', 11, 'bold')) + # 显示左边的固定图片 + self.create_text(80 - 55, 120 + 15, text="路由器", anchor="nw", font=('微软雅黑', 14, 'bold')) # 显示文字 + router = self.create_image(80, 120, image=self.router_img, anchor="nw") + self.create_text(80 - 55, 190 + 15, text="交换机", anchor="nw", font=('微软雅黑', 14, 'bold')) # 显示文字 + switch = self.create_image(80, 190, image=self.switch_img, anchor="nw") + self.create_text(80 - 55, 260 + 15, text="集线器", anchor="nw", font=('微软雅黑', 14, 'bold')) # 显示文字 + hub = self.create_image(80, 260, image=self.hub_img, anchor="nw") + self.create_text(80 - 55, 330 + 15, text="主机", anchor="nw", font=('微软雅黑', 14, 'bold')) # 显示文字 + host = self.create_image(80, 330, image=self.host_img, anchor="nw") + self.bind_event(router, "路由器") + self.bind_event(switch, "交换机") + self.bind_event(hub, "集线器") + self.bind_event(host, "主机") + + # 创建一个菜单栏,这里我们可以把他理解成一个容器,在窗口的上方 + menubar = tk.Menu(root) + # 定义一个空菜单单元 + setMenu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label='删除与修改', menu=setMenu) + setMenu.add_command(label='删除对象', command=self.delete_obj) + setMenu.add_command(label='删除连接线', command=self.delete_line) + setMenu.add_command(label='修改标签', command=self.update_tag_name) + # # 定义一个空菜单单元 + setMenu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label='基础配置', menu=setMenu) + setMenu.add_command(label='网络配置', command=self.network_config) + setMenu.add_command(label='路由表配置', command=self.router_table_config) + setMenu.add_command(label='交换表配置', command=self.mac_table_config) + root.config(menu=menubar) + # 定义一个空菜单单元 + setMenu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label='显示设置', menu=setMenu) + setMenu.add_command(label='显示/不显示标签', command=self.show_label) + setMenu.add_command(label='显示/不显示接口', command=self.show_interface) + setMenu.add_command(label='显示网络配置信息', command=self.show_network_config) + setMenu.add_command(label='显示路由/交换表信息', command=self.show_router_config) + # 定义一个空菜单单元 + setMenu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label='发送数据包', menu=setMenu) + setMenu.add_command(label='发送数据包', command=self.send_packet) + setMenu.add_command(label='批量发送数据包', command=self.send_packet_list) + + menubar.add_command(label='清除屏幕', command=self.clear_canvas) + root.config(menu=menubar) + self.reload_data() + + +if __name__ == '__main__': + root = Window() + root.title('网络拓扑图') + width = 1030 + height = 530 # 窗口大小 + screen_width = root.winfo_screenwidth() # winfo方法来获取当前电脑屏幕大小 + screen_height = root.winfo_screenheight() + x = int((screen_width - width) / 2) + y = int((screen_height - height) / 2) - 40 + size = '{}x{}+{}+{}'.format(width, height, x, y) + canvas = NetWorkAnalog(root, width=1030, heigh=height, bg="white") + canvas.place(x=0, y=0, anchor='nw') + root.geometry(size) + root.mainloop() diff --git a/NetworkAnalog/NetworkAnalog.py b/NetworkAnalog/NetworkAnalog.py new file mode 100644 index 0000000..f4bfe9d --- /dev/null +++ b/NetworkAnalog/NetworkAnalog.py @@ -0,0 +1,954 @@ +import ipaddress +import sys +import threading +import ttkbootstrap as tk +from ttkbootstrap import * +from ttkbootstrap import ttk +from tkinter import messagebox +import re +from PIL import ImageTk, Image +import platform + +from SimObjs import SimPacket, SimHost, AllSimConnect, SimRouter, SimSwitch, SimHub, SimBase +from dbUtil import search, execute_sql, delete_obj, truncate_db + + +def validate_ip_address(ip_address): + """ + 匹配ip地址格式是否规范 + :param ip_address: IP地址 + :return: Boolean + """ + # 定义IP地址的正则表达式模式 + pattern_with_subnet = r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/(\d{1,2})$' + pattern_without_subnet = r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$' + # 使用re模块进行匹配 + match_with_subnet = re.match(pattern_with_subnet, ip_address) + match_without_subnet = re.match(pattern_without_subnet, ip_address) + if match_with_subnet: + # 带有子网掩码的IP地址 + # 检查每个组件的取值范围是否在 0-255 之间 + for group in match_with_subnet.groups()[:4]: + if not (0 <= int(group) <= 255): + return False + # 检查子网掩码的取值范围是否在 0-32 之间 + subnet_mask = int(match_with_subnet.groups()[4]) + if not (0 <= subnet_mask <= 32): + return False + return True + elif match_without_subnet: + # 不带子网掩码的IP地址 + # 检查每个组件的取值范围是否在 0-255 之间 + for group in match_without_subnet.groups(): + if not (0 <= int(group) <= 255): + return False + return True + else: + # IP地址格式不正确 + return False + + +class RouterConfigWindow(tk.Toplevel): + def __init__(self, parent, router_obj): + super().__init__(parent) + self.geometry("435x433+350+200") + self.title(f"{router_obj.ObjLabel}路由表配置") + self.router_obj = router_obj + self.interface_entries = [] + self.router_table = {} + self.create_interface_inputs() + self.create_router_table() + + def create_interface_inputs(self): + label_text = ["接口1", "接口2", "接口3", "接口4"] + for i in range(4): + label = tk.Label(self, text=label_text[i]) + label.grid(row=i, column=0, padx=10, pady=5, sticky="w") + entry = tk.Entry(self, width=20) + entry.grid(row=i, column=1, padx=10, pady=5, sticky="w") + self.interface_entries.append(entry) + button = tk.Button(self, text="添加", command=lambda index=i: self.add_router_entry(index)) + button.grid(row=i, column=2, padx=10, pady=5) + lab = LabelFrame(self, text="示例") + lab.grid(row=4, column=0, columnspan=3, sticky=W, padx=20) + Label(lab, text="10.1.2.0/24 或者 10.1.2.12" if self.router_obj.ObjType == 2 else "MAC11").pack() + + def create_router_table(self): + def on_right_click(event): + row = self.router_treeview.identify_row(event.y) # 获取鼠标位置的行索引 + if row: + self.router_treeview.selection_set(row) # 选中该行 + delete_menu.post(event.x_root, event.y_root) # 在鼠标位置弹出删除菜单 + + def delete_row(): + selected_items = self.router_treeview.selection() # 获取选中的行 + for item in selected_items: + ifs, network = int(self.router_treeview.item(item)["values"][0][-1:]), self.router_treeview.item(item)["values"][1] + self.router_obj.delete_config(ifs, network) + self.router_treeview.delete(item) + self.router_table_frame = tk.Frame(self) + self.router_table_frame.grid(row=5, column=0, columnspan=3, padx=10, pady=5) + self.router_treeview = ttk.Treeview(self.router_table_frame, columns=("Interface", "Route"), show="headings") + self.router_treeview.heading("Interface", text="接口") + self.router_treeview.heading("Route", text="网段") + self.router_treeview.pack(side="left", fill="both") + scrollbar = ttk.Scrollbar(self.router_table_frame, orient="vertical", command=self.router_treeview.yview) + scrollbar.pack(side="right", fill="y") + self.router_treeview.configure(yscrollcommand=scrollbar.set) + self.router_table = self.router_obj.router_table + self.router_treeview.bind("", on_right_click) + # 创建删除菜单 + delete_menu = tk.Menu(root, tearoff=False) + delete_menu.add_command(label="删除", command=delete_row) + self.update_router_table() + + def add_router_entry(self, index): + entry_text = self.interface_entries[index].get() + try: + ipaddress.ip_network(entry_text) + if isinstance(self.router_obj, SimRouter): + if not validate_ip_address(entry_text): + messagebox.showerror("注意", message="添加的网段信息格式不合格") + self.interface_entries[index].delete(0, tk.END) + self.focus_set() + return + if entry_text: + if index + 1 in self.router_table: + self.router_table[index + 1].append(entry_text) + else: + self.router_table[index + 1] = [entry_text] + self.interface_entries[index].delete(0, tk.END) + self.router_obj.add_config(entry_text, index + 1) + self.update_router_table() + except: + messagebox.showerror("注意", message="网段格式错误!网段示例如下:\n10.1.2.0/24\n10.1.2.12") + return + + def update_router_table(self): + self.router_treeview.delete(*self.router_treeview.get_children()) + for i, entrys in self.router_table.items(): + for entry in entrys: + self.router_treeview.insert("", "end", values=(f"接口{i}", entry)) + + +class SwitchConfigWindow(RouterConfigWindow): + def __init__(self, parent, router_obj): + super().__init__(parent, router_obj) + self.geometry("435x433+350+200") + self.title(f"{router_obj.ObjLabel}交换表配置") + self.router_obj = router_obj + self.interface_entries = [] + self.router_table = {} + self.create_interface_inputs() + self.create_router_table() + + def create_router_table(self): + def on_right_click(event): + row = self.router_treeview.identify_row(event.y) # 获取鼠标位置的行索引 + if row: + self.router_treeview.selection_set(row) # 选中该行 + delete_menu.post(event.x_root, event.y_root) # 在鼠标位置弹出删除菜单 + + def delete_row(): + selected_items = self.router_treeview.selection() # 获取选中的行 + for item in selected_items: + ifs, network = int(self.router_treeview.item(item)["values"][0][-1:]), self.router_treeview.item(item)["values"][1] + self.router_obj.delete_config(ifs, network) + self.router_treeview.delete(item) + + self.router_table_frame = tk.Frame(self) + self.router_table_frame.grid(row=5, column=0, columnspan=3, padx=10, pady=5) + self.router_treeview = ttk.Treeview(self.router_table_frame, columns=("Interface", "Route"), show="headings") + self.router_treeview.heading("Interface", text="接口") + self.router_treeview.heading("Route", text="mac") + self.router_treeview.pack(side="left", fill="both") + scrollbar = ttk.Scrollbar(self.router_table_frame, orient="vertical", command=self.router_treeview.yview) + scrollbar.pack(side="right", fill="y") + self.router_treeview.configure(yscrollcommand=scrollbar.set) + self.router_table = self.router_obj.mac_table + self.router_treeview.bind("", on_right_click) + # 创建删除菜单 + delete_menu = tk.Menu(root, tearoff=False) + delete_menu.add_command(label="删除", command=delete_row) + self.update_router_table() + + + def add_router_entry(self, index): + entry_text = self.interface_entries[index].get() + if isinstance(self.router_obj, SimRouter): + if not validate_ip_address(entry_text): + messagebox.showerror("注意", message="添加的网段信息格式不合格") + self.interface_entries[index].delete(0, tk.END) + self.focus_set() + return + if entry_text: + if index + 1 in self.router_table: + self.router_table[index + 1].append(entry_text) + else: + self.router_table[index + 1] = [entry_text] + self.interface_entries[index].delete(0, tk.END) + self.router_obj.add_config(entry_text, index + 1) + self.update_router_table() + + +class NetWorkAnalog(Canvas): + def __init__(self, master, **kwargs): + super().__init__(master, **kwargs) + self.master = master + self.router_img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/../datas/images/路由器.png").resize((60, 60))) + self.switch_img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/../datas/images/交换机.png").resize((60, 60))) + self.hub_img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/../datas/images/集线器.png").resize((60, 60))) + self.host_img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/../datas/images/主机.png").resize((60, 60))) + self.chose = self.host_img + self.AllSimObjs = {} + self.conns = [] + self.drawLine = True # 画线标志 + self.line_start_obj = None + self.line_end_obj = None + self.line_start_ifs = None + self.line_end_ifs = None + self.chose_obj = None + self.show_label_flag = True + self.show_interface_flag = True + self.create_widget() + + def is_chose(self): + """ + 当被选中时,绘制选中框 + :return: + """ + self.delete("rectangle") + self.create_rectangle(self.chose_obj.ObjX - 25, self.chose_obj.ObjY - 25, self.chose_obj.ObjX + 25, + self.chose_obj.ObjY + 25, outline="red", tags="rectangle") + + def tag_bind_event(self): + """ + 为每个子组件绑定事件 + :return: + """ + + def init(): + """ + 初始化方法,将连接对象赋值为初始化值 + :return: + """ + self.line_start_obj = None + self.line_end_obj = None + self.line_start_ifs = None + self.line_end_ifs = None + + def show_menu(event, tag: SimBase): + """ + 右击组件弹出选择接口框 + :param event: + :param tag: + :return: + """ + menu = Menu(self, tearoff=0) + flag = False + for conn in tag.connections: + i = tag.connections.index(conn) + if not isinstance(conn, AllSimConnect): + menu.add_command(label=f"接口{i + 1}", command=lambda num=i + 1: interface_selected(num, tag)) + flag = True + if not flag: + menu.add_command(label="暂无可用接口", state="disabled") + menu.post(event.x_root, event.y_root) + + def interface_selected(interface, tag): + """ + 选择接口回调 + :param interface: + :return: + """ + if self.line_start_ifs is None: + self.line_start_ifs = interface + self.line_start_obj = tag + self.drawLine = True + self.bind("", lambda event: right_motion(event)) + self.bind("", quit) + self.focus_set() # 获取焦点 + else: + self.line_end_ifs = interface + self.line_end_obj = tag + self.delete("Line") + flag = False + if self.line_start_obj.ObjID == self.line_end_obj.ObjID: + messagebox.showerror("注意!", message="不能连接自己") + return + conn = AllSimConnect(self, self.line_start_obj, self.line_start_ifs, self.line_end_obj, + self.line_end_ifs) + for conn_obj in self.line_start_obj.connections: # 判断两个连接对象是否已经连接过了 + if conn_obj == conn: + flag = True + if not flag: + self.line_start_obj.connections[self.line_start_ifs - 1] = conn + self.line_end_obj.connections[self.line_end_ifs - 1] = conn + conn.save() + self.conns.append(conn) + init() + self.unbind("") + else: + del conn + self.delete("Line") + self.unbind("") + init() + messagebox.showerror("注意!", message="已经连接过了") + + def right_motion(event): + """ + 移动鼠标 + :param event: + :param tag: + :return: + """ + if self.drawLine: + self.delete("Line") + x, y = event.x, event.y + if not (150 < x < 650 and 40 < y < 490): + if x < 150: + x = 150 + elif x > 650: + x = 650 + if y < 40: + y = 40 + elif y > 490: + y = 490 + self.create_line(self.line_start_obj.ObjX, self.line_start_obj.ObjY, x + 8, y + 8, fill="#5b9bd5", + width=1, + tags="Line") + return + self.create_line(self.line_start_obj.ObjX, self.line_start_obj.ObjY, x + 8, y + 8, fill="#5b9bd5", + width=1, tags="Line") + + def quit(event): + self.delete("Line") # 删除刚刚产生的连接线 + self.drawLine = False # 关闭画线标志 + init() + + for tag_id, tag in self.AllSimObjs.items(): + self.tag_bind(tag.ObjID, "", lambda event, tag=tag: show_menu(event, tag)) # 绑定右击事件 + + def bind_event(self, component, name): + # todo: 绑定事件 + """ + 绑定事件 + :param component: 组件对象 + :param name: 组件名称 + """ + def move(event, name): + """ + 鼠标左键松开事件 + :param event: 事件对象 + """ + if 170 < event.x < 630 and 60 < event.y < 470: # 在方框内,无变化 + if name == "路由器": + tag = SimRouter(self, event.x, event.y) + elif name == "集线器": + tag = SimHub(self, event.x, event.y) + elif name == "交换机": + tag = SimSwitch(self, event.x, event.y) + else: + tag = SimHost(self, event.x, event.y) + self.AllSimObjs[tag.ObjID] = tag + tag.save() + self.tag_bind_event() + else: + self.delete("L") + + def motion(event, name): + """ + 鼠标拖动事件 + :param event: 事件对象 + """ + if name == "路由器": + self.chose = self.router_img + elif name == "集线器": + self.chose = self.hub_img + elif name == "交换机": + self.chose = self.switch_img + else: + self.chose = self.host_img + self.delete("L") + if 170 < event.x < 630 and 60 < event.y < 470: # 在方框内,无变化 + pass + else: # 在方框外,显示禁止放置标识 + self.create_oval(event.x - 10, event.y - 45, event.x + 10, event.y - 25, outline="red", width=2, + tags="L") + self.create_line(event.x - 7, event.y - 42, event.x + 8, event.y - 27, fill="red", width=2, tags="L") + self.create_image(event.x - 30, event.y - 30, image=self.chose, anchor="nw", tags="L") + + self.tag_bind(component, "", lambda event: move(event, name)) + self.tag_bind(component, "", lambda event: motion(event, name)) + + def reload_data(self): + # todo: 加载上一次程序运行的数据 + """ + 加载上一次程序运行时的数据 + :return: + """ + self.AllSimObjs = {} + self.conns = [] + sim_obj_sql = "select * from sim_objs" + sim_obj_data = search(sim_obj_sql) + for index, row in sim_obj_data.iterrows(): # 初始化组件对象 + sim_type = row["ObjType"] + ObjX = row["ObjX"] + ObjY = row["ObjY"] + ConfigCorrect = row["ConfigCorrect"] + ObjLable = row["ObjLabel"] + ObjID = row["ObjID"] + if sim_type == 1: + tag = SimHost(self, ObjX, ObjY, ObjID, ConfigCorrect, ObjLable) + elif sim_type == 2: + tag = SimRouter(self, ObjX, ObjY, ObjID, ConfigCorrect, ObjLable) + elif sim_type == 3: + tag = SimSwitch(self, ObjX, ObjY, ObjID, ConfigCorrect, ObjLable) + else: + tag = SimHub(self, ObjX, ObjY, ObjID, ConfigCorrect, ObjLable) + self.AllSimObjs[tag.ObjID] = tag + + sim_conn_sql = "select s.conn_id, ConfigCorrect, node_id, node_ifs from sim_conn s join conn_config c on s.conn_id=c.conn_id" + sim_conn_data = search(sim_conn_sql) + conn_datas = {} + for index, conn in sim_conn_data.iterrows(): + if (conn["conn_id"], conn["ConfigCorrect"]) not in conn_datas: + conn_datas[(conn["conn_id"], conn["ConfigCorrect"])] = [(conn["node_id"], conn["node_ifs"])] + else: + conn_datas[(conn["conn_id"], conn["ConfigCorrect"])].append((conn["node_id"], conn["node_ifs"])) + for key, value in conn_datas.items(): + conn_obj = AllSimConnect(self, self.AllSimObjs[value[0][0]], value[0][1], + self.AllSimObjs[value[1][0]], value[1][1], key[1]) + self.AllSimObjs[value[0][0]].connections[value[0][1] - 1] = conn_obj # 将连接对象传入组件对象 + self.AllSimObjs[value[1][0]].connections[value[1][1] - 1] = conn_obj + self.conns.append(conn_obj) + conn_obj.ConfigCorrect = key[1] + self.tag_bind_event() + + def delete_obj(self): + # todo: 删除对象 + """ + 选中删除对象 + :return: + """ + if self.chose_obj is None: + messagebox.showerror("注意", message="请先选择要删除的对象!") + return + ask = messagebox.askquestion(title='确认操作', message='确认删除该对象吗?') + if ask == "no": + return + self.delete(self.chose_obj.ObjID) # 删除图片 + self.delete(self.chose_obj.ObjID + "text") # 删除标签 + for conn in self.chose_obj.connections: # 删除该对象的所有连接线 + if isinstance(conn, AllSimConnect): + conn.delete_line() + delete_sql = f"delete from sim_conn where conn_id in (select conn_id from conn_config where node_id='{self.chose_obj.ObjID}')" + execute_sql(delete_sql) + delete_obj(self.chose_obj.ObjID) + self.delete("rectangle") + self.reload_data() + + def delete_line(self): + # todo: 删除连接线 + """ + 删除连接线 + :return: + """ + if self.chose_obj is None: + messagebox.showerror("注意", message="请先选择要删除连接线的对象!") + return + conn_sql = f""" + select + s.conn_id, ConfigCorrect, node_id, node_ifs + from sim_conn s + join conn_config c on s.conn_id=c.conn_id + where node_id='{self.chose_obj.ObjID}' + """ + conn_data = search(conn_sql) + conn_names = {} + for index, conn in conn_data.iterrows(): + if conn["conn_id"] not in conn_names: + conn_names[conn["conn_id"]] = [(conn["node_id"], conn["node_ifs"])] + else: + conn_names[conn["conn_id"]].append((conn["node_id"], conn["node_ifs"])) + child_d = tk.Toplevel() + child_d.title(f"{self.chose_obj.ObjLabel}的连接线配置") + child_d.geometry('300x200+450+200') + child_d.grab_set() + cv1 = Canvas(child_d) + cv1.pack() + value = StringVar() + combobox = ttk.Combobox( + master=child_d, # 父容器 + height=12, # 高度,下拉显示的条目数量 + width=15, # 宽度 + state='readonly', # readonly(只可选) + font=('', 18), # 字体 + textvariable=value, # 通过StringVar设置可改变的值 + values=list(conn_names.keys()), # 设置下拉框的选项 + ) + + def del_line(): + if value.get() == "": + messagebox.showerror("注意", message="请选择需要删除的连接线!") + return + conn_sql = f""" + select + conn_id, node_id, node_ifs + from conn_config + where conn_id='{value.get()}' + """ + conn_data = search(conn_sql) + delete_sql = f"delete from sim_conn where conn_id='{value.get()}'" + execute_sql(delete_sql) + conn_names = {} + for index, conn in conn_data.iterrows(): + if conn["conn_id"] not in conn_names: + conn_names[conn["conn_id"]] = [(conn["node_id"], conn["node_ifs"])] + else: + conn_names[conn["conn_id"]].append((conn["node_id"], conn["node_ifs"])) + for data in conn_names[value.get()]: + self.AllSimObjs[data[0]].connections[data[1] - 1].delete_line() + self.AllSimObjs[data[0]].connections[data[1] - 1] = None + child_d.destroy() + messagebox.showinfo("提示", f"连接线{value.get()}删除成功!") + + btn_yes = tk.Button(child_d, text='确定', font=('黑体', 12), height=1, command=del_line) + btn_yes.place(x=240, y=20) + combobox.place(x=20, y=20) + + def update_tag_name(self): + # todo: 更新组件名称 + """ + 更新组件名称 + :return: + """ + if self.chose_obj is None: + messagebox.showerror("注意", message="请先选择要更新的对象!") + return + child1 = tk.Toplevel() + child1.title(self.chose_obj.ObjLabel + "的标签信息") + child1.geometry('240x100+450+250') + child1.grab_set() # 设置组件焦点抓取。使焦点在释放之前永远保持在这个组件上,只能在这个组件上操作 + tk.Label(child1, text='原标签:' + self.chose_obj.ObjLabel, + font=('黑体', 12)).grid(row=0, column=0, columnspan=2, sticky='w') + tk.Label(child1, text='新标签:', font=('黑体', 12)).grid(row=1, column=0, sticky='w') # ,sticky='w'靠左显示 + new_name = tk.Entry(child1, font=('黑体', 12), textvariable=tk.StringVar()) + new_name.grid(row=1, column=1) + + def update_tag(): + name = new_name.get() + if name == "": + messagebox.showerror("注意", message="请输入新名称!") + return + self.chose_obj.ObjLabel = name + self.chose_obj.update() + for conn in self.chose_obj.connections: + if isinstance(conn, AllSimConnect): + conn.update_info(self.chose_obj.ObjID) + child1.destroy() + messagebox.showinfo("提示", message="修改成功!") + + tk.Button(child1, text='确定', font=('黑体', 10), height=1, command=update_tag).grid(row=2, column=0, + sticky='e') + tk.Button(child1, text='取消', font=('黑体', 10), height=1, command=child1.destroy).grid(row=2, column=1, + sticky='e') + + def network_config(self): + # todo: 网络配置 + """ + 网络配置 + :return: + """ + if self.chose_obj is None: + messagebox.showerror("注意", message="请先选择要配置的对象!") + return + if len(self.chose_obj.get_config()) == 0: + messagebox.showerror("注意", message="请先给对象添加连接线!") + return + child_r = tk.Toplevel() + child_r.title(self.chose_obj.ObjLabel + "的网络配置信息") + child_r.geometry('530x395+350+200') + child_r.grab_set() # 设置组件焦点抓取。使焦点在释放之前永远保持在这个组件上,只能在这个组件上操作 + ifs_frame = tk.Frame(child_r) + ifs_frame.grid(row=1, column=0, columnspan=4) # 装接口的框架 + ifs_frame_YN = tk.Frame(child_r) + ifs_frame_YN.grid(row=2, column=0, columnspan=4) + num = 0 + datas = {} + for conn in self.chose_obj.connections: + if isinstance(conn, AllSimConnect): + index = self.chose_obj.connections.index(conn) + num_label = tk.LabelFrame(ifs_frame, text=f'接口{index + 1}') + num_label.grid(row=num, column=0, padx=18, ipady=5) + tk.Label(num_label, text='MAC:', font=('黑体', 10)).grid(row=0, column=0) + mac_en = tk.Entry(num_label, font=('黑体', 12), textvariable=tk.StringVar(), state=DISABLED if self.chose_obj.ObjType not in [1, 2, 3] else NORMAL) + mac_en.insert('0', str(self.chose_obj.interface[index].get("mac", ""))) + mac_en.grid(row=0, column=1, padx=10) + tk.Label(num_label, text='IP:', font=('黑体', 10)).grid(row=1, column=0) + ip_en = tk.Entry(num_label, font=('黑体', 12), textvariable=tk.StringVar(), state=DISABLED if self.chose_obj.ObjType not in [1, 2] else NORMAL) + ip_en.insert('0', str(self.chose_obj.interface[index].get("ip", ""))) + ip_en.grid(row=1, column=1, padx=10) + tk.Label(num_label, text='端口:', font=('黑体', 10)).grid(row=0, column=2) + port_en = tk.Entry(num_label, font=('黑体', 12), textvariable=tk.StringVar(), state=DISABLED if self.chose_obj.ObjType != 1 else NORMAL) + port_en.insert('0', str(self.chose_obj.interface[index].get("conn_port", ""))) + port_en.grid(row=0, column=3, padx=10) + tk.Label(num_label, text='应用层地址:', font=('黑体', 10)).grid(row=1, column=2) + addr_en = tk.Entry(num_label, font=('黑体', 12), textvariable=tk.StringVar(), state=DISABLED if self.chose_obj.ObjType != 1 else NORMAL) + addr_en.insert('0', str(self.chose_obj.interface[index].get("addr", ""))) + addr_en.grid(row=1, column=3, padx=10) + num += 1 + datas[index + 1] = {"mac": mac_en, "ip": ip_en, "conn_port": port_en, "addr": addr_en} + + num_label = tk.LabelFrame(ifs_frame, text=f'示例') + num_label.grid(row=num, column=0, padx=18, ipady=5) + tk.Label(num_label, text='MAC:', font=('黑体', 10)).grid(row=0, column=0) + mac_en = tk.Entry(num_label, font=('黑体', 12), textvariable=tk.StringVar()) + mac_en.insert('0', "MAC11") + mac_en.config(state=READONLY) + mac_en.grid(row=0, column=1, padx=10) + tk.Label(num_label, text='IP:', font=('黑体', 10)).grid(row=1, column=0) + ip_en = tk.Entry(num_label, font=('黑体', 12), textvariable=tk.StringVar()) + ip_en.insert(0, "10.1.1.10") + ip_en.config(state=READONLY) + ip_en.grid(row=1, column=1, padx=10) + tk.Label(num_label, text='端口:', font=('黑体', 10)).grid(row=0, column=2) + port_en = tk.Entry(num_label, font=('黑体', 12), textvariable=tk.StringVar()) + port_en.insert('0', "80") + port_en.config(state=READONLY) + port_en.grid(row=0, column=3, padx=10) + tk.Label(num_label, text='应用层地址:', font=('黑体', 10)).grid(row=1, column=2) + addr_en = tk.Entry(num_label, font=('黑体', 12), textvariable=tk.StringVar()) + addr_en.insert('0', "10.1.2.10:10810:Name3") + addr_en.config(state=READONLY) + addr_en.grid(row=1, column=3, padx=10) + def commit(): + self.chose_obj.config(datas) + child_r.destroy() + + tk.Button(ifs_frame_YN, text='确定', font=('黑体', 10), height=1, command=commit).grid(row=0, column=0, + padx=20) # ,sticky="w" + tk.Button(ifs_frame_YN, text='取消', font=('黑体', 10), height=1, + command=child_r.destroy).grid(row=0, column=2, padx=20) + + def router_table_config(self): + # todo: 路由表配置 + """ + 路由表配置 + :return: + """ + if self.chose_obj is None: + messagebox.showerror("注意", message="请先选择要配置的对象!") + return + if self.chose_obj.ObjType != 2: + messagebox.showerror("注意", message="请选择路由器对象!") + return + RouterConfigWindow(self, self.chose_obj) + + def mac_table_config(self): + # todo: 交换表配置 + """ + 交换表配置 + :return: + """ + if self.chose_obj is None: + messagebox.showerror("注意", message="请先选择要配置的对象!") + return + if self.chose_obj.ObjType != 3: + messagebox.showerror("注意", message="请选择交换机对象!") + return + SwitchConfigWindow(self, self.chose_obj) + + def show_label(self): + """ + 显示/不显示标签 + :return: + """ + self.show_label_flag = not self.show_label_flag + if self.show_label_flag: + for tag in self.AllSimObjs.values(): + tag.create_img() + else: + for tag in self.AllSimObjs.values(): + self.delete(tag.ObjID + "text") + + def show_interface(self): + # todo: 显示/不显示接口 + """ + 显示/不显示接口 + :return: + """ + self.show_interface_flag = not self.show_interface_flag + if self.show_interface_flag: + for conn in self.conns: + conn.draw_line() + else: + for conn in self.conns: + self.delete(conn.NobjS.ObjID + conn.NobjE.ObjID) + + def show_network_config(self): + # todo: 显示网络配置信息 + """ + 显示网络配置信息 + :return: + """ + if self.chose_obj is None: + messagebox.showerror("注意", message="请先选择要显示的对象!") + return + self.delete("netSet") + self.create_text(740, 120, text="(" + self.chose_obj.ObjLabel + ")", anchor="n", font=('微软雅黑', 10, 'bold'), + fill="#7030a0", tags="netSet") + self.create_text(675, 145, text=self.chose_obj, + anchor="nw", font=('宋体', 11), tags="netSet") + + def show_router_config(self): + # todo: 显示路由表交换表信息 + """ + 显示路由交换表信息 + :return: + """ + if self.chose_obj is None: + messagebox.showerror("注意", message="请先选择要显示的对象!") + return + if self.chose_obj.ObjType != 2 and self.chose_obj.ObjType != 3: + messagebox.showerror("注意", message="请选择路由器/交换机对象!") + return + self.delete("routerSet") + self.create_text(905, 120, text="(" + self.chose_obj.ObjLabel + ")", anchor="n", font=('微软雅黑', 10, 'bold'), + fill="#7030a0", tags="routerSet") + self.create_text(835, 145, text=self.chose_obj.get_table_config(), + anchor="nw", font=('宋体', 11), tags="routerSet") + + def send_packet(self): + """ + 发送数据包 + :return: + """ + if self.chose_obj is None: + messagebox.showerror("注意", message="请先选择要显示的对象!") + return + if self.chose_obj.ConfigCorrect != 1: + messagebox.showerror("注意", message="请先对选择对象进行网络配置!") + return + if self.chose_obj.ObjType != 1: + messagebox.showerror("注意", message="请选择主机对象!") + return + child2 = tk.Toplevel() + child2.title("数据包配置") + child2.geometry('240x100+450+250') + child2.grab_set() # 设置组件焦点抓取。使焦点在释放之前永远保持在这个组件上,只能在这个组件上操作 + tk.Label(child2, text='目的IP:', font=('黑体', 12)).grid(row=0, column=0, columnspan=2, sticky='w') + packet_ip = tk.Entry(child2, font=('黑体', 12), textvariable=tk.StringVar()) + packet_ip.grid(row=0, column=1) + tk.Label(child2, text='目的MAC:', font=('黑体', 12)).grid(row=1, column=0, sticky='w') # ,sticky='w'靠左显示 + packet_mac = tk.Entry(child2, font=('黑体', 12), textvariable=tk.StringVar()) + packet_mac.grid(row=1, column=1) + tk.Label(child2, text='消息:', font=('黑体', 12)).grid(row=2, column=0, sticky='w') # ,sticky='w'靠左显示 + packet_message = tk.Entry(child2, font=('黑体', 12), textvariable=tk.StringVar()) + packet_message.grid(row=2, column=1) + + def send(): + """ + 发送数据包 + :return: + """ + if packet_ip.get() == "": + messagebox.showerror("注意", message="ip地址不能为空") + return + if not validate_ip_address(packet_ip.get()): + messagebox.showerror("注意", message="IP地址不规范!") + return + if packet_mac.get() == "": + messagebox.showerror("注意", message="mac地址不能为空!") + return + if packet_message.get() == "": + messagebox.showerror("注意", message="消息不能为空!") + return + self.chose_obj.create_packet(packet_ip.get(), + packet_mac.get(), + packet_message.get()) + child2.destroy() + + tk.Button(child2, text='确定', font=('黑体', 10), height=1, command=send).grid(row=3, column=0, sticky='e') + tk.Button(child2, text='取消', font=('黑体', 10), height=1, command=child2.destroy).grid(row=3, column=1, + sticky='e') + + def send_packet_list(self): + """ + 批量发送数据包 + :return: + """ + hosts = {} + for tag in self.AllSimObjs.values(): + if tag.ObjType == 1 and tag.ConfigCorrect == 1: + hosts[tag.ObjLabel] = tag.ObjID + child2 = tk.Toplevel() + child2.title("批量数据包配置") + child2.geometry('400x420+450+200') + tk.Label(child2, text='目的IP:', font=('黑体', 12)).grid(row=0, column=0, columnspan=2, sticky='w', ipady=10) + packet_ip = tk.Entry(child2, font=('黑体', 12), textvariable=tk.StringVar()) + packet_ip.grid(row=0, column=1, pady=5) + tk.Label(child2, text='目的MAC:', font=('黑体', 12)).grid(row=1, column=0, sticky='w', + pady=5) # ,sticky='w'靠左显示 + packet_mac = tk.Entry(child2, font=('黑体', 12), textvariable=tk.StringVar()) + packet_mac.grid(row=1, column=1, pady=5) + tk.Label(child2, text='消息:', font=('黑体', 12)).grid(row=2, column=0, sticky='w', pady=5) # ,sticky='w'靠左显示 + packet_message = tk.Entry(child2, font=('黑体', 12), textvariable=tk.StringVar()) + packet_message.grid(row=2, column=1, pady=5) + host = StringVar() + tk.Label(child2, text='发送主机:', font=('黑体', 12)).grid(row=3, column=0, sticky='w', + pady=5) # ,sticky='w'靠左显示 + combobox = ttk.Combobox( + master=child2, # 父容器 + height=5, # 高度,下拉显示的条目数量 + width=12, # 宽度 + state='readonly', # readonly(只可选) + font=('', 18), # 字体 + textvariable=host, # 通过StringVar设置可改变的值 + values=list(hosts.keys()), # 设置下拉框的选项 + ) + combobox.grid(row=3, column=1, pady=5) + packet_frame = tk.Frame(child2) + packet_frame.grid(row=5, column=0, columnspan=3, padx=10, pady=5) + packet_treeview = ttk.Treeview(packet_frame, columns=("source_host", "ip", "mac", "message"), show="headings") + packet_treeview.heading("source_host", text="发送主机") + packet_treeview.column("source_host", width=60, anchor=CENTER) + packet_treeview.heading("ip", text="IP") + packet_treeview.column("ip", width=120, anchor=CENTER) + packet_treeview.heading("mac", text="MAC") + packet_treeview.column("mac", width=60, anchor=CENTER) + packet_treeview.heading("message", text="消息") + packet_treeview.column("message", width=120, anchor=CENTER) + packet_treeview.pack(side="left", fill="both") + scrollbar = ttk.Scrollbar(packet_frame, orient="vertical", command=packet_treeview.yview) + scrollbar.pack(side="right", fill="y") + packet_treeview.configure(yscrollcommand=scrollbar.set) + packets = [] + + def add(): + ip = packet_ip.get() + mac = packet_mac.get() + message = packet_message.get() + chose_host = host.get() + if ip == "" or mac == "" or message == "" or chose_host == "": + messagebox.showerror("注意", message="输入框不能为空") + return + packet = SimPacket(self.AllSimObjs[hosts[chose_host]].interface[0]["ip"], + self.AllSimObjs[hosts[chose_host]].interface[0]["mac"], + ip, mac, message) + packets.append((self.AllSimObjs[hosts[chose_host]], packet)) + packet_treeview.delete(*packet_treeview.get_children()) + for datas in packets: + tag: SimBase = datas[0] + packet_data: SimPacket = datas[1] + packet_treeview.insert("", "end", values=( + tag.ObjLabel, packet_data.destination_ip, packet_data.destination_mac, packet_data.message)) + + def send(): + if len(packets) == 0: + messagebox.showerror("注意", message="请至少添加一条数据包!") + return + for data in packets: + threading.Thread(target=data[0].send, args=(data[1],)).start() + child2.destroy() + + tk.Button(child2, text="添加数据包", font=('黑体', 12), height=3, command=add).grid(row=0, column=2, rowspan=4) + button_frame = Frame(child2) + tk.Button(button_frame, text="发送", font=('黑体', 12), height=1, command=send).pack(side=RIGHT, padx=20) + tk.Button(button_frame, text="取消", font=('黑体', 12), height=1, command=child2.destroy).pack(side=RIGHT) + button_frame.grid(row=6, column=0, columnspan=3) + + def clear_canvas(self): + """ + 清除画布 + :return: + """ + ask = messagebox.askquestion(title='确认操作', message='确认要清除画布吗?') + if ask == "no": + return + truncate_db() # 清除数据库 + for tag_id, tag in self.AllSimObjs.items(): + self.delete(tag_id) + self.delete(tag_id + "text") + for conn in self.conns: + conn.delete_line() + self.delete("rectangle") + self.AllSimObjs.clear() + self.conns.clear() + + def create_widget(self): + # todo: 创建页面 + """ + 创建整体页面布局 + :return: + """ + self.create_rectangle(150, 40, 650, 490, outline="#7f6000", width=3) # 矩形框,左上角坐标,右下角坐标 + self.create_rectangle(660, 100, 815, 400, outline="#ffff00", width=3, fill="#f4b88e") # 矩形框,左上角坐标,右下角坐标 + self.create_text(735, 105, text="网络配置信息", anchor="n", font=('微软雅黑', 11, 'bold')) + self.create_rectangle(825, 100, 1000, 400, outline="#ffff00", width=3, fill="#f4b88e") # 矩形框,左上角坐标,右下角坐标 + self.create_text(915, 105, text="路由/交换表信息", anchor="n", font=('微软雅黑', 11, 'bold')) + # 显示左边的固定图片 + self.create_text(80 - 55, 120 + 15, text="路由器", anchor="nw", font=('微软雅黑', 14, 'bold')) # 显示文字 + router = self.create_image(80, 120, image=self.router_img, anchor="nw") + self.create_text(80 - 55, 190 + 15, text="交换机", anchor="nw", font=('微软雅黑', 14, 'bold')) # 显示文字 + switch = self.create_image(80, 190, image=self.switch_img, anchor="nw") + self.create_text(80 - 55, 260 + 15, text="集线器", anchor="nw", font=('微软雅黑', 14, 'bold')) # 显示文字 + hub = self.create_image(80, 260, image=self.hub_img, anchor="nw") + self.create_text(80 - 55, 330 + 15, text="主机", anchor="nw", font=('微软雅黑', 14, 'bold')) # 显示文字 + host = self.create_image(80, 330, image=self.host_img, anchor="nw") + self.bind_event(router, "路由器") + self.bind_event(switch, "交换机") + self.bind_event(hub, "集线器") + self.bind_event(host, "主机") + + # 创建一个菜单栏,这里我们可以把他理解成一个容器,在窗口的上方 + menubar = tk.Menu(root) + # 定义一个空菜单单元 + setMenu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label='删除与修改', menu=setMenu) + setMenu.add_command(label='删除对象', command=self.delete_obj) + setMenu.add_command(label='删除连接线', command=self.delete_line) + setMenu.add_command(label='修改标签', command=self.update_tag_name) + # # 定义一个空菜单单元 + setMenu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label='基础配置', menu=setMenu) + setMenu.add_command(label='网络配置', command=self.network_config) + setMenu.add_command(label='路由表配置', command=self.router_table_config) + setMenu.add_command(label='交换表配置', command=self.mac_table_config) + root.config(menu=menubar) + # 定义一个空菜单单元 + setMenu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label='显示设置', menu=setMenu) + setMenu.add_command(label='显示/不显示标签', command=self.show_label) + setMenu.add_command(label='显示/不显示接口', command=self.show_interface) + setMenu.add_command(label='显示网络配置信息', command=self.show_network_config) + setMenu.add_command(label='显示路由/交换表信息', command=self.show_router_config) + # 定义一个空菜单单元 + setMenu = tk.Menu(menubar, tearoff=0) + menubar.add_cascade(label='发送数据包', menu=setMenu) + setMenu.add_command(label='发送数据包', command=self.send_packet) + setMenu.add_command(label='批量发送数据包', command=self.send_packet_list) + + menubar.add_command(label='清除屏幕', command=self.clear_canvas) + root.config(menu=menubar) + self.reload_data() + + +if __name__ == '__main__': + root = Window() + root.title('网络拓扑图') + width = 1030 + height = 530 # 窗口大小 + screen_width = root.winfo_screenwidth() # winfo方法来获取当前电脑屏幕大小 + screen_height = root.winfo_screenheight() + x = int((screen_width - width) / 2) + y = int((screen_height - height) / 2) - 40 + size = '{}x{}+{}+{}'.format(width, height, x, y) + canvas = NetWorkAnalog(root, width=1030, heigh=height, bg="white") + canvas.place(x=0, y=0, anchor='nw') + root.geometry(size) + root.mainloop() diff --git a/NetworkAnalog/SimObjs.py b/NetworkAnalog/SimObjs.py new file mode 100644 index 0000000..c450c4c --- /dev/null +++ b/NetworkAnalog/SimObjs.py @@ -0,0 +1,757 @@ +import math +import sys +import threading +from time import sleep + +from ttkbootstrap import * +from uuid import uuid4 +import ipaddress + +from PIL import ImageTk, Image + +from dbUtil import search, execute_sql + + +class SimBase(): + # todo: 组件父类 + """ + 图标类,所有组件的父类 + """ + def __init__(self, canvas: Canvas, x, y, id, config=None, label=None): + self.ConfigCorrect = 0 if config is None else config # 是否进行配置 + self.ObjType = None # 组件类型 1->主机,2->路由器,3->交换机,4->集线器 + self.ObjLabel = "" + self.ObjID = id + self.ObjX = x + self.ObjY = y + self.canvas = canvas + self.img = None + self.img_tm = None + self.interface = [{}, {}, {}, {}] + self.connections = ["1", "2", "3", "4"] + self.set_default_config() + + def bind_event(self): + self.canvas.tag_bind(self.ObjID, "", self.start_drag) + self.canvas.tag_bind(self.ObjID, "", self.drag) + self.canvas.tag_bind(self.ObjID, "", self.release) + + def set_default_name(self): + data_frame = search(f"select objid from sim_objs where objType={self.ObjType}") + num = data_frame.size + 1 + if isinstance(self, SimHost): + name = "SHO%d" % num + elif isinstance(self, SimRouter): + name = "SRO%d" % num + elif isinstance(self, SimSwitch): + name = "SWI%d" % num + else: + name = "SHUB%d" % num + return name + + def release(self, event): + if self.click_x == event.x and self.click_y == event.y: # 鼠标左键单击 + self.canvas.chose_obj = self + self.canvas.is_chose() + else: + self.update() + + def start_drag(self, event): + self.canvas.tag_raise(self.ObjID) # 将 SimBase 组件置于最上层 + self.start_x = event.x + self.start_y = event.y + self.click_x = event.x + self.click_y = event.y + + def drag(self, event): + """ + 移动图标 + :param event: + :return: + """ + self.canvas.delete("rectangle") + self.canvas.chose_obj = None + dx = event.x - self.start_x + dy = event.y - self.start_y + # 移动范围限制, 超出移动范围则直接返回 + if not (170 <= self.ObjX + dx <= 630 and 60 <= self.ObjY + dy <= 470): + return + self.ObjX += dx + self.ObjY += dy + self.canvas.move(self.ObjID, dx, dy) # 移动 SimBase 组件 + self.canvas.move(self.ObjID + "text", dx, dy) # 移动 SimBase 组件 + self.start_x = event.x + self.start_y = event.y + for conn in self.connections: + if isinstance(conn, AllSimConnect): + conn.update_line() + + def create_img(self): + """ + 创建图片 + :return: + """ + self.canvas.delete("L") + id = self.canvas.create_image(self.ObjX - 30, self.ObjY - 30, + image=self.img if self.ConfigCorrect == 1 else self.img_tm, anchor="nw", + tags=self.ObjID) + self.canvas.dtag("L", "L") + self.canvas.create_text(self.ObjX, self.ObjY - 40, text=self.ObjLabel, font=("", 12, "bold"), + fill="#7030a0", tags=self.ObjID + "text", anchor="nw") + self.canvas.tag_raise(id) + self.bind_event() + + def config(self, interface): + """ + 网络配置方法, + :param interface: 传入配置数据 + :return: + """ + for key, value in interface.items(): + self.interface[key - 1] = {key: value.get() if not value.get() == "" else "NULL" for key, value in value.items()} + self.ConfigCorrect = 1 + self.create_img() + for conn in self.connections: + if isinstance(conn, AllSimConnect): + index = self.connections.index(conn) + 1 + conn.update_node_config(self, index) + self.update() + + def set_default_config(self): + sql = f"select * from conn_config where node_id='{self.ObjID}'" + conn_data = search(sql) + for index, conn in conn_data.iterrows(): + self.interface[int(conn["node_ifs"]) - 1] = {"ip": conn["ip"], + "mac": conn["mac"], + "conn_port": conn["conn_port"], + "addr": conn["addr"]} + + def get_config(self): + sql = f"select * from conn_config where node_id='{self.ObjID}'" + conn_data = search(sql) + return conn_data + + def save(self): + """ + 将对象存储至mysql当中 + :return: + """ + sql = f"insert into sim_objs values ('{self.ObjID}', {self.ObjType}, '{self.ObjLabel}'," \ + f"{self.ObjX}, {self.ObjY}, {self.ConfigCorrect})" + execute_sql(sql) + + def update(self): + """ + 当坐标发生改变时修改数据库数据 + :return: + """ + self.canvas.delete(self.ObjID + "text") + self.canvas.create_text(self.ObjX, self.ObjY - 40, text=self.ObjLabel, font=("", 12, "bold"), + fill="#7030a0", tags=self.ObjID + "text", anchor="nw") + sql = f"update sim_objs set objLabel='{self.ObjLabel}', ObjX={self.ObjX}," \ + f"ObjY={self.ObjY}, ConfigCorrect={self.ConfigCorrect} where ObjID='{self.ObjID}'" + execute_sql(sql) + + def transfer_animate(self, status, packet, error_message=None): + if status: + text = f"目的IP: {str(packet.destination_ip)}\n" \ + f"目的MAC: {packet.destination_mac}\n" \ + f"消息内容: {packet.message}" + self.canvas.create_rectangle(self.ObjX + 30, self.ObjY - 30, self.ObjX + 160, self.ObjY + 20, outline="#92d050", + width=3, fill="#92d050", tags=self.ObjID + "packetData") + self.canvas.create_text(self.ObjX + 35, self.ObjY - 25, text=text, anchor="nw", + font=('', 10), tags=self.ObjID + "packetData") # 显示文字 + self.canvas.update() + sleep(2) + self.canvas.delete(self.ObjID + "packetData") # 删除展示的数据包内容 + else: + text = f"传输失败\n" if error_message is None else error_message + self.canvas.create_rectangle(self.ObjX + 30, self.ObjY - 30, self.ObjX + 160, self.ObjY, outline="red", + width=3, fill="red", tags=self.ObjID + "packetData") + self.canvas.create_text(self.ObjX + 35, self.ObjY - 25, text=text, anchor="nw", + font=('', 10), tags=self.ObjID + "packetData") # 显示文字 + self.canvas.update() + sleep(2) + self.canvas.delete(self.ObjID + "packetData") # 删除展示的数据包内容 + + def __str__(self): + str = "" + config = self.get_config() + for index, data in config.iterrows(): + str += f"【接口{data['node_ifs']}】\n" + str += f"IP: {data['ip']}\n" + str += f"MAC: {data['mac']}\n" + return str + + +class SimPacket(): + # todo: 数据包类 + """ + 数据包类 + """ + def __init__(self, source_ip, source_mac, destination_ip, destination_mac, message): + """ + :param source_mac: 源主机mac地址 + :param source_ip: 源主机ip地址 + :param destination_mac: 目的主机mac地址 + :param destination_ip: 目的主机ip地址 + :param message: 数据 + """ + self.source_mac = source_mac + self.source_ip = source_ip + self.destination_mac = destination_mac + self.destination_ip = destination_ip + self.message = message + self.up_jump = None # 上一跳连接对象 + self.img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/../datas/images/packet.png").resize((30, 30))) + self.id = str(uuid4()) + + def move(self, cv, target_x, target_y, duration): + start_x, start_y = cv.coords(self.id) + distance_x = target_x - start_x + distance_y = target_y - start_y + steps = duration // 10 # 以10毫秒为间隔进行移动 + step_x = distance_x / steps + step_y = distance_y / steps + self._move_step(cv, start_x, start_y, target_x, target_y, step_x, step_y, steps) + + def _move_step(self, cv, start_x, start_y, target_x, target_y, step_x, step_y, steps): + if steps > 0: + new_x = start_x + step_x + new_y = start_y + step_y + cv.coords(self.id, new_x, new_y) + cv.update() # 更新画布显示 + sleep(0.01) # 添加延迟以控制动画速度 + self._move_step(cv, new_x, new_y, target_x, target_y, step_x, step_y, steps - 1) + else: + cv.coords(self.id, target_x, target_y) + cv.delete(self.id) + + def transfer_packet(self, cv: Canvas, nodex_tag: SimBase, nodey_tag: SimBase): + cv.create_image(nodex_tag.ObjX - 15, nodex_tag.ObjY - 15, image=self.img, anchor="nw", tags=self.id) + self.move(cv, nodey_tag.ObjX - 15, nodey_tag.ObjY - 15, 500) + + +class AllSimConnect(): + # todo: 连接类 + def __init__(self, canvas: Canvas, nodex: SimBase, nodex_ifs, nodey: SimBase, nodey_ifs, config=None): + """ + 连接对象 + :param nodex: 节点 + :param nodex_ifs: 节点接口 + :param nodey: 节点 + :param nodey_ifs: 节点接口 + """ + self.canvas = canvas + self.ConfigCorrect = 0 if config is None else config + self.NobjS = nodex + self.NobjE = nodey + self.IfsS = nodex_ifs + self.IfsE = nodey_ifs + self.width = 1 if self.ConfigCorrect == 0 else 4 # 线的粗细 + self.draw_line() + + def is_connected_to(self, other): + """ + 判断两个连接对象是否已经连接 + :param other: 另一个连接对象 + :return: 如果已连接,则返回True;否则返回False + """ + return ( + self.NobjS.ObjID == other.NobjS.ObjID + and self.NobjE.ObjID == other.NobjE.ObjID + ) + + def update_node_config(self, node, ifs): + """ + 当节点进行网络配置后将节点的接口配置保存 + :return: + """ + try: + nodex_update_sql = f""" + update conn_config set + ip='{node.interface[ifs - 1]["ip"]}', + mac='{node.interface[ifs - 1]["mac"]}', + conn_port='{node.interface[ifs - 1]["conn_port"]}', + addr='{node.interface[ifs - 1]["addr"]}' + where node_id='{node.ObjID}' and node_ifs={ifs} + """ + execute_sql(nodex_update_sql) + self.ConfigCorrect = 1 + self.check_config() + update_sql = f""" + update sim_conn set ConfigCorrect={self.ConfigCorrect} + where conn_id in ( + select distinct conn_id from conn_config where node_id='{node.ObjID}' + ) + """ + execute_sql(update_sql) + except Exception as E: + pass + + def check_config(self): + """ + 检查两边节点的配置是否正确 + :return: + """ + if self.NobjS.interface[self.IfsS - 1]["mac"] != "" and self.NobjS.interface[self.IfsS - 1][ + "ip"] != "" \ + and self.NobjE.interface[self.IfsE - 1]["mac"] != "" and self.NobjE.interface[self.IfsE - 1][ + "ip"] != "": + self.width = 4 + self.draw_line() + else: + self.width = 1 + self.draw_line() + + def transfer(self, source_node, packet: SimPacket): + """ + 传输数据 + :param packet: 数据包 + :return: + """ + if source_node == self.NobjS: + packet.transfer_packet(self.canvas, self.NobjS, self.NobjE) + self.NobjE.receive(packet) + else: + packet.transfer_packet(self.canvas, self.NobjE, self.NobjS) + self.NobjS.receive(packet) + + def draw_line(self): + line = self.canvas.create_line(self.NobjS.ObjX, self.NobjS.ObjY, self.NobjE.ObjX, self.NobjE.ObjY, + width=self.width, fill="#5b9bd5", tags=self.NobjS.ObjID + self.NobjE.ObjID + "line") + self.canvas.tag_lower(line) + self.analyseIFS(self.NobjS.ObjX, self.NobjS.ObjY, self.NobjS.ObjLabel, self.IfsS, + self.NobjE.ObjX, self.NobjE.ObjY, self.NobjE.ObjLabel, self.IfsE) + + def update_line(self): + """ + 当组件移动时重新绘制线 + :return: + """ + self.canvas.delete(self.NobjS.ObjID + self.NobjE.ObjID + "line") + self.canvas.delete(self.NobjS.ObjID + self.NobjE.ObjID) + self.draw_line() + + def update_info(self, obj_id): + """ + 修改数据库中的连接信息 + :return: + """ + update_sql = f""" + update sim_conn set conn_id='{self.NobjS.ObjLabel}-{self.NobjE.ObjLabel}', ConfigCorrect={self.ConfigCorrect} + where conn_id in ( + select distinct conn_id from conn_config where node_id='{obj_id}' + ) + """ + execute_sql(update_sql) + + def analyseIFS(self, SX, SY, SLabel, IfsS, EX, EY, ELabel, IfsE): # NobjS的x,y;NobjE的x,y; #分析接口在哪个位置,再显示 + ''' + :param SX: S对象的x坐标 + :param SY: S对象的y坐标 + :param SLabel: S对象的标签 + :param IfsS: S对象要显示的接口号 + ''' + if (EX - SX) == 0: # 即垂直的时候,NobjS在NobjE对象的正上方和正下方,x坐标无变化,只需要y变化 + R = 18 + x = 0 # x无偏移量 + y = R + else: + k = (EY - SY) / (EX - SX) # ey-sy/sx-ex + reat = math.atan(k) # 根据斜率计算弧度 + # 连线与图标交接点坐标 + R = 18 + x = abs(math.cos(reat) * R) + 6 # python math中三角函数中的数值是弧度,而计算器中的数值是角度 + y = abs(math.sin(reat) * R) + 6 + if SX <= EX and SY <= EY: # NobjS在NobjE的左上角,NobjS的右下角连接NobjE的左上角 + x_S = SX + x + y_S = SY + y # NobjS的连接点坐标 + x_E = EX - x + y_E = EY - y # NobjE的连接点坐标 + # 显示S接口号 + self.canvas.create_text((x_S - 5, y_S - 5), text=IfsS, anchor="nw", font=("幼圆", 12, "bold"), fill="red", + tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 + # 显示E接口号 + self.canvas.create_text(x_E - 10, y_E - 10, text=IfsE, anchor="nw", font=("幼圆", 12, "bold"), fill="red", + tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 + elif SX < EX and SY > EY: # NobjS在NobjE的左下角,NobjS的右上角连接NobjE的左下角 + x_S = SX + x + y_S = SY - y # NobjS的连接点坐标 + x_E = EX - x + y_E = EY + y # NobjE的连接点坐标 + # 显示S接口号 + self.canvas.create_text(x_S + 5, y_S - 10, text=IfsS, anchor="nw", font=("幼圆", 12, "bold"), fill="red", + tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 + # 显示E接口号 + self.canvas.create_text(x_E - 5, y_E, text=IfsE, anchor="nw", font=("幼圆", 12, "bold"), fill="red", + tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 + elif SX > EX and SY < EY: # NobjS在NobjE的右上角,NobjS的左下角连接NobjE的右上角 + x_S = SX - x + y_S = SY + y # NobjS的连接点坐标 + x_E = EX + x + y_E = EY - y # NobjE的连接点坐标 + # 显示S接口号 + self.canvas.create_text(x_S - 5, y_S, text=IfsS, anchor="nw", font=("幼圆", 12, "bold"), fill="red", + tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 + # 显示E接口号 + self.canvas.create_text(x_E + 5, y_E, text=IfsE, anchor="nw", font=("幼圆", 12, "bold"), fill="red", + tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 + elif SX >= EX and SY >= EY: # NobjS在NobjE的右下角,NobjS的左上角连接NobjE的右下角 + x_S = SX - x + y_S = SY - y # NobjS的连接点坐标 + x_E = EX + x + y_E = EY + y # NobjE的连接点坐标 + # 显示S接口号 + self.canvas.create_text(x_S - 5, y_S - 15, text=IfsS, anchor="nw", font=("幼圆", 12, "bold"), fill="red", + tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 + # 显示E接口号 + self.canvas.create_text(x_E - 5, y_E - 5, text=IfsE, anchor="nw", font=("幼圆", 12, "bold"), fill="red", + tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 + + def save(self): + """ + 将连接对象保存至数据库 + :return: + """ + conn_id = self.NobjS.ObjLabel + "-" + self.NobjE.ObjLabel + sql = f"insert into sim_conn values ('{conn_id}', {self.ConfigCorrect})" + execute_sql(sql) + execute_sql( + f"insert into conn_config values ('{conn_id}', '{self.NobjS.ObjID}', {self.IfsS}, '', '', '', '')") + execute_sql( + f"insert into conn_config values ('{conn_id}', '{self.NobjE.ObjID}', {self.IfsE}, '', '', '', '')") + + def delete_line(self): + self.canvas.delete(self.NobjS.ObjID + self.NobjE.ObjID) + self.canvas.delete(self.NobjS.ObjID + self.NobjE.ObjID + "line") + + def __eq__(self, other): + """ + 重写equals方法,判断两个连接对象是否相同 + :param other: 连接对象 + :return: Boolean + """ + if isinstance(other, AllSimConnect): + other_ids = other.NobjS.ObjID + other.NobjE.ObjID + return (self.NobjS.ObjID in other_ids + and self.NobjE.ObjID in other_ids) + return False + + +class SimHost(SimBase): + # todo: 主机类 + """ + 主机类 + """ + def __init__(self, canvas: Canvas, x, y, id=None, config=None, label=None): + self.ObjID = str(uuid4()) if id is None else id + super().__init__(canvas, x, y, self.ObjID, config, label) + self.ObjType = 1 + self.ObjLabel = label if label is not None else self.set_default_name() + self.interface = [{}] + self.connections = [None] + self.img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/../datas/images/主机.png").resize((60, 60))) + self.img_tm = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/../datas/images/主机_tm.png").resize((60, 60))) + self.set_default_config() + self.create_img() + + def create_packet(self, ip, mac, message): + """ + 创建数据包 + :param ip: 目的主机ip + :param mac: 目的主机mac + :param message: 消息 + :return: + """ + packet = SimPacket(self.interface[0]["ip"], self.interface[0]["mac"], ip, mac, message) + print(f"创建数据包成功,数据包由{packet.source_ip} 发往 {packet.destination_ip}") + self.send(packet) + + def send(self, packet): + """ + 发送数据包 + :param packet: + :return: + """ + connection: AllSimConnect = self.connections[0] + print(f"数据包从 {self.ObjLabel} 发出") + packet.up_jump = connection + connection.transfer(self, packet) + + def receive(self, packet: SimPacket): + """ + 接收数据 + :param packet: 数据包 + :return: + """ + print(f"主机{self.ObjLabel}接受到数据{packet.message}") + if packet.destination_ip == self.interface[0]["ip"]: + self.transfer_animate(True, packet) + else: + self.transfer_animate(False, packet) + + def __str__(self): + str = "" + config = self.get_config() + for index, data in config.iterrows(): + str += f"【接口{data['node_ifs']}】\n" + str += f"AppAddr: /Root\n" + str += f"PORT: {data['conn_port']}\n" + str += f"IP: {data['ip']}\n" + str += f"MAC: {data['mac']}\n" + return str + + +class SimRouter(SimBase): + # todo: 路由类 + """ + 路由类 + """ + def __init__(self, canvas: Canvas, x, y, id=None, config=None, label=None, *args): + self.ObjID = str(uuid4()) if id is None else id + super().__init__(canvas, x, y, self.ObjID, config, label) + self.ObjType = 2 + self.ObjLabel = label if label is not None else self.set_default_name() + self.img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/../datas/images/路由器.png").resize((60, 60))) + self.img_tm = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/../datas/images/路由器_tm.png").resize((60, 60))) + self.create_img() + self.router_table = {} + self.set_default_router_table() + + def set_default_router_table(self): + """ + 将数据库中的路由表信息提取 + :return: + """ + sql = f"select * from router_table where obj_id='{self.ObjID}'" + router_tables = search(sql) + for index, router_table in router_tables.iterrows(): + if router_table["node_ifs"] in self.router_table: + self.router_table[router_table["node_ifs"]].append(router_table["segment"]) + else: + self.router_table[router_table["node_ifs"]] = [router_table["segment"]] + + def check_destination_ip(self, destination_ip, network): + """ + 检查目标ip是否属于网段范围内 + :param destination_ip: 目标ip + :param network: 网段 + :return:10.2.3.0/24 + """ + ip = ipaddress.ip_address(destination_ip) + network = ipaddress.ip_network(network) + if ip in network: + return True + if network == "0.0.0.0/24": # 如果网段为0.0.0.0/24 则为默认路由 + return True + + def transmit(self, packet: SimPacket): + """ + 转发数据包 + :return: + """ + flag = False + next_hop_ifs = None + for conn in self.connections: + if isinstance(conn, AllSimConnect): + if conn.ConfigCorrect == 0: + continue + if conn == packet.up_jump: + continue + ifs = self.connections.index(conn) + 1 + for network in self.router_table[ifs]: + if self.check_destination_ip(packet.destination_ip, network): + flag = True + next_hop_ifs = ifs + if flag: + conn = self.connections[next_hop_ifs - 1] + packet.up_jump = conn + conn.transfer(self, packet) + else: + for conn in self.connections: + if isinstance(conn, AllSimConnect): + if conn == packet.up_jump: + continue + if conn.NobjS != self: + if conn.NobjE.ObjType == 1: + conn.transfer(self, packet) + break + error_message = "路由寻址失败" + self.transfer_animate(False, packet, error_message) + + def receive(self, packet): + """ + 接收数据 + :param packet: 数据包 + :return: + """ + print(f"{self.ObjLabel}-路由器接受到数据{packet.message}") + self.transmit(packet) + + def add_config(self, router, router_ifs): + sql = f"insert into router_table values ('{self.ObjID}', {router_ifs}, '{router}')" + execute_sql(sql) + + def delete_config(self, ifs, network): + sql = f"delete from router_table where obj_id='{self.ObjID}' and node_ifs={ifs} and segment='{network}'" + execute_sql(sql) + + def get_table_config(self): + """ + 返回对象的路由表配置信息,用于展示 + :return: + """ + str = "" + sql = f"select * from router_table where obj_id='{self.ObjID}'" + router_tables = search(sql) + for index, router in router_tables.iterrows(): + str += f"网段号: {router['segment']}\n" + str += f"接口号: {router['node_ifs']}\n" + return str + + +class SimSwitch(SimBase): + # todo: 交换机类 + """ + 交换机类 + """ + def __init__(self, canvas: Canvas, x, y, id=None, config=None, label=None, *args): + self.ObjID = str(uuid4()) if id is None else id + super().__init__(canvas, x, y, self.ObjID, config, label) + self.ObjType = 3 + self.ObjLabel = label if label is not None else self.set_default_name() + self.img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/../datas/images/交换机.png").resize((60, 60))) + self.img_tm = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/../datas/images/交换机_tm.png").resize((60, 60))) + self.create_img() + self.mac_table = {} + self.set_default_mac_table() + + def set_default_mac_table(self): + """ + 将数据库中的路由表信息提取 + :return: + """ + sql = f"select * from mac_table where obj_id='{self.ObjID}'" + router_tables = search(sql) + for index, router_table in router_tables.iterrows(): + if router_table["node_ifs"] in self.mac_table: + self.mac_table[router_table["node_ifs"]].append(router_table["mac"]) + else: + self.mac_table[router_table["node_ifs"]] = [router_table["mac"]] + + def add_config(self, router, router_ifs): + sql = f"insert into mac_table values ('{self.ObjID}', {router_ifs}, '{router}')" + execute_sql(sql) + + def delete_config(self, ifs, mac): + sql = f"delete from mac_table where obj_id='{self.ObjID}' and node_ifs={ifs} and mac='{mac}'" + execute_sql(sql) + + def get_table_config(self): + """ + 返回对象的交换表配置信息,用于展示 + :return: + """ + str = "" + sql = f"select * from mac_table where obj_id='{self.ObjID}'" + router_tables = search(sql) + for index, router in router_tables.iterrows(): + str += f"网段号: {router['mac']}\n" + str += f"接口号: {router['node_ifs']}\n" + return str + + def transmit(self, packet: SimPacket): + """ + 转发数据包 + :return: + """ + flag = False + next_hub_ifs = None + for conn in self.connections: + if isinstance(conn, AllSimConnect): + if conn.ConfigCorrect == 0: + continue + ifs = self.connections.index(conn) + 1 + if packet.destination_mac in self.mac_table.get(ifs, []): + flag = True + next_hub_ifs = ifs + if flag: + conn = self.connections[next_hub_ifs - 1] + packet.up_jump = conn + conn.transfer(self, packet) + return + for conn in self.connections: # 将数据包往所有接口进行转发 + if isinstance(conn, AllSimConnect): + if conn == packet.up_jump: + continue + if conn.ConfigCorrect == 0: + continue + new_packet = SimPacket(packet.source_ip, + packet.source_mac, + packet.destination_ip, + packet.destination_mac, + packet.message) + new_packet.up_jump = conn + threading.Thread(target=conn.transfer, args=(self, new_packet)).start() + + def receive(self, packet: SimPacket): + """ + 接收数据 + :param packet: 数据包 + :return: + """ + print(f"交换机{self.ObjLabel}接受到数据{packet.message}") + self.transmit(packet) + + +class SimHub(SimBase): + # todo: 集线器类 + """ + 集线器类 + """ + def __init__(self, canvas: Canvas, x, y, id=None, config=None, label=None, *args): + self.ObjID = str(uuid4()) if id is None else id + super().__init__(canvas, x, y, self.ObjID, config, label) + self.ObjType = 4 + self.ObjLabel = label if label is not None else self.set_default_name() + self.img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/../datas/images/集线器.png").resize((60, 60))) + self.img_tm = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/../datas/images/集线器_tm.png").resize((60, 60))) + self.create_img() + + def transmit(self, packet: SimPacket): + """ + 集线器转发数据包 + :return: + """ + for conn in self.connections: # 将数据包往所有接口进行转发 + if isinstance(conn, AllSimConnect): + if conn == packet.up_jump: + continue + if conn.ConfigCorrect == 0: + continue + new_packet = SimPacket(packet.source_ip, + packet.source_mac, + packet.destination_ip, + packet.destination_mac, + packet.message) + new_packet.up_jump = conn + threading.Thread(target=conn.transfer, args=(self, new_packet)).start() + + def receive(self, packet: SimPacket): + """ + 接收数据 + :param packet: 数据包 + :return: + """ + print(f"集线器-{self.ObjLabel}接受到数据,将进行转发!") + self.transmit(packet) diff --git a/NetworkAnalog/dbUtil.py b/NetworkAnalog/dbUtil.py new file mode 100644 index 0000000..f2e20ef --- /dev/null +++ b/NetworkAnalog/dbUtil.py @@ -0,0 +1,104 @@ +import sqlite3 +import sys + +import pandas as pd +from pandas import DataFrame + +conn = sqlite3.connect(sys.path[0]+"/network.db") + +def execute_sql(sql): + """ + 执行sql语句 + :param sql: + :return: + """ + cursor = conn.cursor() + cursor.execute(sql) + conn.commit() + + +def search(sql) -> DataFrame: + return pd.read_sql(sql, conn) + + +def delete_obj(obj_id): + cursor = conn.cursor() + delete_obj_sql = f"delete from sim_objs where ObjID='{obj_id}'" + cursor.execute(delete_obj_sql) + delete_conn_sql = f"delete from sim_conn where conn_id in (select conn_id from conn_config where node_id='{obj_id}')" + cursor.execute(delete_conn_sql) + conn.commit() + + +def truncate_db(): + init_database() + +def init_database(): + cursor = conn.cursor() + cursor.execute(""" + DROP TABLE IF EXISTS `conn_config`; + """) + cursor.execute(""" + CREATE TABLE `conn_config` ( + `conn_id` varchar(55) NULL DEFAULT NULL, + `node_id` varchar(55) NULL DEFAULT NULL, + `node_ifs` int(0) NULL DEFAULT NULL, + `ip` varchar(55) NULL DEFAULT NULL, + `mac` varchar(128) NULL DEFAULT NULL, + `conn_port` varchar(32) NULL DEFAULT NULL, + `addr` varchar(255) NULL DEFAULT NULL, + CONSTRAINT `conn_config_sim_conn_conn_id_fk` FOREIGN KEY (`conn_id`) REFERENCES `sim_conn` (`conn_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ; + """) + cursor.execute(""" + DROP TABLE IF EXISTS `mac_table`; + """) + cursor.execute(""" + CREATE TABLE `mac_table` ( + `obj_id` varchar(55) NULL DEFAULT NULL, + `node_ifs` int(0) NULL DEFAULT NULL, + `mac` varchar(55) NULL DEFAULT NULL, + CONSTRAINT `mac_table_sim_objs_ObjID_fk` FOREIGN KEY (`obj_id`) REFERENCES `sim_objs` (`ObjID`) ON DELETE CASCADE ON UPDATE CASCADE +) ; + """) + cursor.execute(""" + DROP TABLE IF EXISTS `router_table`; + """) + cursor.execute(""" + CREATE TABLE `router_table` ( + `obj_id` varchar(55) NULL DEFAULT NULL, + `node_ifs` int(0) NULL DEFAULT NULL, + `segment` varchar(55) NULL DEFAULT NULL, + CONSTRAINT `router_table_sim_objs_ObjID_fk` FOREIGN KEY (`obj_id`) REFERENCES `sim_objs` (`ObjID`) ON DELETE CASCADE ON UPDATE CASCADE +) ; + + """) + cursor.execute(""" + DROP TABLE IF EXISTS `sim_conn`; + """) + cursor.execute(""" + CREATE TABLE `sim_conn` ( + `conn_id` varchar(255) NOT NULL , + `ConfigCorrect` int(0) NULL DEFAULT NULL , + PRIMARY KEY (`conn_id`) +) ; + + """) + cursor.execute(""" + DROP TABLE IF EXISTS `sim_objs`; + """) + cursor.execute(""" + CREATE TABLE `sim_objs` ( + `ObjID` varchar(50) NOT NULL, + `ObjType` int(0) NULL DEFAULT NULL, + `ObjLabel` varchar(20) NULL DEFAULT NULL, + `ObjX` int(0) NULL DEFAULT NULL, + `ObjY` int(0) NULL DEFAULT NULL, + `ConfigCorrect` int(0) NULL DEFAULT NULL, + PRIMARY KEY (`ObjID`) +) ; + """) + conn.commit() + +if __name__ == '__main__': + init_database() diff --git a/NetworkAnalog/network.db b/NetworkAnalog/network.db new file mode 100644 index 0000000000000000000000000000000000000000..290ba3f1f3a97e983a8d648eddde03b6e7fbe671 GIT binary patch literal 40960 zcmeI5TWlOx8Gz@qw^@64N=q;)O+9In#%?<4oHO^*HrZ^P#E$Dt?JR9niapoXZDOa6 zTPW}VX?lSNUJ6A;0tpEO5{SYBY6VCLBnm;U5h>QvLMMerZoN-arHoBLO6U1dsp{KmthM ztwZ3fQ!0;a+s2=3u6q8-=*;4&`DOq9GwEx&IXlrfKA{|M9GIR^7Sh8DiZWJJl!cam z|K!01U@Fd)F&U+87E+_BjQ&jUL-K%```a;6`>}Zd5u)MUy&a>Czde-c4 z%9`!ZFNPULrkTO5B)psXs+AO8Td!1oQnTV_HA_k-HDh}vi02B6>9f;fd1Tiv{@mS3 z4Of;QT8&ob*XvvF`jfg|l>2U%z99;uF>y+tUGjE?f>4krjtc|nI|Z%14UvUmK_ zQ3MljmSE4T%Z=~Rhxzizjvf3HdV(*BS@;9}KC>&VMVL90LQD>2JKmS@@s5Fs>-j`e_nsAesk?Fwa?b>sBy#34L>k^?a&{Fo*7yg z+FgCI`mySy^dIRv(ubrQDz8?)R9UL*EdQkZX!&UQ%F=I2pD!(zwBkRD-z`2-oGET8 z{I>9g!pVXmz9xQO{J1zPZWUe@zAT&;Z2rIe3;ZYeHouMgJ@-{+I^Sm9TG`%~YOu{6 zX&LI=k@f*%#IEl;o})UtZmUM*Q`I;1Ky`>~S;RDHWKjDSkmJ|N*@hn*)U;GrcZg~P zAmVG3s#>Urv8CCXO|^pc?)3&~KsD3TeATjOpc2CYF2ngbd%1~sS;XpjzSkPK$f z()w}`*bv?=BUjts)OR$M%N5=B-o9YkopK3UYTwgTFpsC!jeWV)(>!wap1z~0Ts}u{ z7|`JOfClZczFNRc2(_ zgitRg+Fr7k?$xc^8%?+!(i|w&J#sU9cjTJNj?}^S0eS%*+V28Q-79!cQ$&en#za>M zTsc)EHsI>*>%K|@L-RZ@v0V+vz!%OO zSw6E$)7O0?ppdVP!tG3|5N<}i*m5J)b~KOOTsW#5c(Lj`Brqu41cZ($&b@zeHMoz= z>-*l(Q!v|tW&tgE6bPvTI<1}Ukc#_)8wg68~XHs9t)KwW@y7HkG=^8OqJF zydbb?#uw7@h0N1qSF(`AI%F<1Ql_M}aSWk1 zFtJu8fbagNBoqm+dsn`2-jAwc}0~;G;SwHd(N)0!Hy9+ab znXGQOs^=P!8rm>Cxjr#NxY6idgh+Z3B5)CcNw;)sP5vp&|DWOHXW$QRNB{{S0VIF~ zkN^@u0!RP}AOR$R1dzbxB|wEjn;(61pXY*yx|B<%_pxhD{uC$3J30Bi@=o^e@>UY9 zApsb8>NV1wR28>M$>U-TNrK%*@}O0%B5s%Z&-@J+#Zd{K zu7L*x;3=${z)toaVW{z)vfZZ#*aCkg`RkngZTWBV3-T-Si|__FB!C2v01`j~NB{{S z0VIF~kN^@u0!ZNU5P*sJ^PJqD|4U%@nz=h+{?B*j|5dizng2_5aXa6i|5s|_DBqj^ z7n75{`M*$GGykv2V_d2JWI?`1eoFpv{d@H%>wkwsxFG=~fCP{L5t&fmk|Yw&5M@~oN3 zX6*da{IDs2)Cl;OXW7C+vIZdEW^hjxcsZ#t|@#*6Gin|JbE_}1_;X<=8B>qzTjOdHw z!mGmb!bgO|g3SM#{~RCj>iV`nn&h?FG71Ti(zG;r+9k>0kS=UU)6@x5NnV<+vd$9` zd6Y5nR2NBgVFE%-K$uNFnWrZfEKESSL3&M0K)7Ld!UTjHqQL}&8>Xwp1ca~4;JYqG zA|@cbmC zSU<*-N1`g-ywlPeS?IKdH!#??b|Bbd*-?;2Il&9@_$2G6E^?= literal 0 HcmV?d00001 diff --git a/NetworkAnalog/tkTest.py b/NetworkAnalog/tkTest.py new file mode 100644 index 0000000..8cd1853 --- /dev/null +++ b/NetworkAnalog/tkTest.py @@ -0,0 +1,19 @@ +from tkinter import * +import platform + + +def get_platform(): + import platform + sys_platform = platform.platform().lower() + if "windows" in sys_platform: + print("Windows") + elif "macos" in sys_platform: + print("Mac os") + elif "linux" in sys_platform: + print("Linux") + else: + print("其他系统") + + +if __name__ == "__main__": + get_platform() diff --git a/README.md b/README.md index 4a85a63..8ee0538 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,171 @@ -# NetworkAnalog +# 项目编程基本原则 + +## 文档及代码编辑的注意事项 + +### 进度汇报 +**查文档和编程任务是否符合。** + +首先自查一遍文档,每个编程任务是否符合文档上编程任务的要求---请在进度表中标示【自查:符合 或 不符合】,是按编程任务,不是按X1,X2... + +**查命名问题,函数框架,函数名称,和变量名规范** + +(2)按我们前两天讨论的,主函数框架、函数关系调用图、函数名称与变量名称规范化等方面回复【已优化完成,未优化完成】 + + +### 主函数框架示例: + +```python +# 需要导入的库 +import tkinter as tk # UI界面 +from tkinter import scrolledtext # 下拉框 +from tkinter import END # 下拉框自动下拉到最后一行 +import random # 随机数 +import time # 应用sleep函数睡眠 +import threading # 多线程任务 +from pymouse import PyMouse # 模拟鼠标自动点击 +import win32gui # 获取扫雷窗口信息 + +""" +所有的系统库或第三方库,一次性引出,避免因为确少库而导致运行不了。 +如果是自写的模块,可以在函数前引出 +""" + +#方式1--在文件中定义全局变量,加载全局变量定义文件(如果全局变量实在太多) +from setting import * # (setting为自定义程序存放的是全局变量)导入全局变量 +# setting中定义了哪些全局变量及其定义 +#方式2--直接定义全局变量(一般多) +GLOBAL ... ... # 常量用大写 + +########### +'此处说明是放函数定义的地方' +########### + + +# 主控函数, 将函数定义拷贝到main函数上面,将函数调用放在main函数 +if __name__ == '__main__': + pass + + ########### + '此处说明是函数调用的地方' + ########### + +""" +要求主函数框架单独运行不报错 +""" +``` + +```python +# 文档贴函数提示说明 如下图所示 +########################################################################### + +# 以func1()[编程章节]==>func2()[编程章节]的方式说明调用当前函数要用的前述函数。 +set_random_state()[5.1]==>create_boards()[5.4]==>show_window()[5.4] +``` +![img_2.png](img_2.png) + +### 根据不同情况不同的贴程序方法 + +```python +# 情况一 +##### 第一次出现init初始化方法需要写全 ##### +##### 在类里面增添方法,需要将类名写出,再写方法 ##### +X2: +class MineSweeper: # 建立扫雷类 + def __init__(self): # 第一次出现init初始化方法需要写全, + def f2(): + pass + +# 情况二 +##### 如果函数实在太多,需要标出函数调用关系图,同时将贴合编程要求的程序给出 ##### +X3: # f1(), f2(), f3(), f4(), ..., f10() 本段仅给出f4(), 完整的在[位置](可以在文档末尾建立附录)。 +Class Class_some + def f4(): 体现核心功能的函数给出。 + pass + +# 情况三 +##### 如果需要在后续编程章节中扩充原有的功能,可以扩写函数,##### +##### 而且不要出现已经出现过的代码,已经出现过的代码用...代指 ##### +X4: 【友好阅读性原则;最小重复代码原则;递增式代码表达原则;界面元素处理与业务逻辑完全分开原则】 +class MineSweeper: # 扩展扫雷类--在X2基础上进一步定义 + # _init_(self), f2(), 一并处理 + def _init_(self): #在原函数基础上增加/调整 + ... + self.par = "example" + def f3(): + ... + def f4(): + ... + +每个函数不超一页,原则上就是半页。 + +``` + +## 项目编程的普遍要求 + +### GUI界面和数据结构分开 + +借用前后端分离的架构模式。 + +GUI界面负责处理用户界面的展示和交互,数据结构部分负责处理业务逻辑和数据处理。 + +将GUI界面负和数据结构分为两个独立的部分,各自负责不同的功能和任务。GUI界面通过接口函数与更新和加载数据结构。 + +### 代码命名规则: + +在Python中,有一些命名规则和约定被广泛遵循,以提高代码的可读性和一致性。 + +变量和函数名:使用小写字母,单词之间使用下划线(snake_case)进行分隔。例如:my_variable, calculate_result。 + +常量:使用全大写字母,单词之间使用下划线进行分隔。例如:MAX_VALUE, PI。 + +类名:使用驼峰命名法(CamelCase),即首字母大写,不使用下划线。例如:MyClass, Calculator。 + +模块名:使用小写字母,单词之间使用下划线进行分隔。例如:my_module, utils。 + +- 首先是做好程序的优化,即做好程序函数的封装。 每个函数的函数名尽可能符合文档的编程任务及其要求--要让读者看到函数名就能想到编程任务,这样会更易于理解。 + +- 函数调用关系图的理解是正确的,就是要有一张全局关系图。 + +- 正确理解X1、X2、X3、X4和X5。通常,X1仅是围绕数据结构赋值,和界面或者界面元素坐标无关。X2是依据数据结构中的数据做输出,通常是仅输出。X3通常可以定义函数,或者定义类及其中的函数。X4是在X2基础上做界面交互元素,同时将界面交互元素相关的事件/消息绑定X3或X5的函数。X5也是一些函数。这样界面逻辑和业务逻辑是完全分离的。 + +- 注意:自前向后,编程是越来越复杂,但不要用后面的编程直接替代前面的编程任务中的程序,前面是简单的后面是复杂的。例如“Select Sno from Student”,这是前面的编程,Select :attr1 from :table1",尽管将attr1赋值成Sno,将table1赋值成Student,也能实现前面的语句,但不可用后面的替换前面的。 + + +### 函数调用关系图: + +函数调用图(Function Call Graph)是用于描述程序中函数之间调用关系的图形表示。它展示了函数之间的依赖关系和调用流程,帮助我们理解程序的执行流程和函数之间的交互 + +在函数调用图中,函数被表示为节点,函数之间的调用关系被表示为边。每个函数调用都会生成一个新的节点和一条连接调用者和被调用者的边。函数调用图可以是有向图或无向图,具体取决于函数调用的方向性。 + +调用图可以用软件生成。 + +![img.png](img.png) + +### 项目文件应包含(后续规范) +项目应该包含以下基本文件: + +1. README.md:项目的说明文档,包含项目的介绍、使用方法、安装指南、贡献指南等重要信息。README.md通常是其他开发者了解和使用项目的入口。 + +2. LICENSE:项目的开源许可证,明确了项目的使用条件和权利限制。选择适合项目的开源许可证对于保护项目的权益和推动开源合作非常重要。 + +3. .gitignore:Git版本控制系统的忽略文件配置,用于指定哪些文件或目录应该被忽略,不纳入版本控制。通常包括一些编译生成的文件、临时文件、敏感信息等。 + +4. requirements.txt:项目的依赖项清单,列出了项目所需的外部库、框架和工具的版本信息。这样其他开发者可以方便地安装相同的依赖项,确保项目的可重复性和一致性。 + +5. setup.py 或者 setup.cfg:用于打包和发布项目的配置文件。可以定义项目的元数据、依赖关系、安装过程等,以便其他人能够方便地安装和使用项目。 + +6. docs 目录:包含项目的文档,例如用户手册、API文档、开发者指南等。良好的文档对于其他开发者和用户理解和使用项目非常重要。 + +7. tests 目录:包含项目的测试代码和测试数据,用于验证项目的正确性和稳定性。包括单元测试、集成测试等,帮助开发者确保项目的质量和可靠性。 + +8. src 或者 lib 目录:包含项目的源代码文件。根据项目的规模和结构,可以进一步组织成子目录或包,方便代码的组织和维护。 + +除了上述基本文件,根据项目的特点和需求,还可以包含其他文件,如配置文件、示例代码、演示视频等。重要的是根据项目的具体情况进行文件的组织和描述,确保项目的可理解性、可维护性和可扩展性。 + +```bash +# 将输出重定向到文件: +pip freeze > requirements.txt +# 老师一键安装项目所有依赖,一般像Pycharm会自动识别这个文件,按提示安装即可 +pip install -r requirements.txt +``` diff --git a/SimObjs.py b/SimObjs.py new file mode 100644 index 0000000..8c1555c --- /dev/null +++ b/SimObjs.py @@ -0,0 +1,757 @@ +import math +import sys +import threading +from time import sleep + +from ttkbootstrap import * +from uuid import uuid4 +import ipaddress + +from PIL import ImageTk, Image + +from dbUtil import search, execute_sql + + +class SimBase(): + # todo: 组件父类 + """ + 图标类,所有组件的父类 + """ + def __init__(self, canvas: Canvas, x, y, id, config=None, label=None): + self.ConfigCorrect = 0 if config is None else config # 是否进行配置 + self.ObjType = None # 组件类型 1->主机,2->路由器,3->交换机,4->集线器 + self.ObjLabel = "" + self.ObjID = id + self.ObjX = x + self.ObjY = y + self.canvas = canvas + self.img = None + self.img_tm = None + self.interface = [{}, {}, {}, {}] + self.connections = ["1", "2", "3", "4"] + self.set_default_config() + + def bind_event(self): + self.canvas.tag_bind(self.ObjID, "", self.start_drag) + self.canvas.tag_bind(self.ObjID, "", self.drag) + self.canvas.tag_bind(self.ObjID, "", self.release) + + def set_default_name(self): + data_frame = search(f"select objid from sim_objs where objType={self.ObjType}") + num = data_frame.size + 1 + if isinstance(self, SimHost): + name = "SHO%d" % num + elif isinstance(self, SimRouter): + name = "SRO%d" % num + elif isinstance(self, SimSwitch): + name = "SWI%d" % num + else: + name = "SHUB%d" % num + return name + + def release(self, event): + if self.click_x == event.x and self.click_y == event.y: # 鼠标左键单击 + self.canvas.chose_obj = self + self.canvas.is_chose() + else: + self.update() + + def start_drag(self, event): + self.canvas.tag_raise(self.ObjID) # 将 SimBase 组件置于最上层 + self.start_x = event.x + self.start_y = event.y + self.click_x = event.x + self.click_y = event.y + + def drag(self, event): + """ + 移动图标 + :param event: + :return: + """ + self.canvas.delete("rectangle") + self.canvas.chose_obj = None + dx = event.x - self.start_x + dy = event.y - self.start_y + # 移动范围限制, 超出移动范围则直接返回 + if not (170 <= self.ObjX + dx <= 630 and 60 <= self.ObjY + dy <= 470): + return + self.ObjX += dx + self.ObjY += dy + self.canvas.move(self.ObjID, dx, dy) # 移动 SimBase 组件 + self.canvas.move(self.ObjID + "text", dx, dy) # 移动 SimBase 组件 + self.start_x = event.x + self.start_y = event.y + for conn in self.connections: + if isinstance(conn, AllSimConnect): + conn.update_line() + + def create_img(self): + """ + 创建图片 + :return: + """ + self.canvas.delete("L") + id = self.canvas.create_image(self.ObjX - 30, self.ObjY - 30, + image=self.img if self.ConfigCorrect == 1 else self.img_tm, anchor="nw", + tags=self.ObjID) + self.canvas.dtag("L", "L") + self.canvas.create_text(self.ObjX, self.ObjY - 40, text=self.ObjLabel, font=("", 12, "bold"), + fill="#7030a0", tags=self.ObjID + "text", anchor="nw") + self.canvas.tag_raise(id) + self.bind_event() + + def config(self, interface): + """ + 网络配置方法, + :param interface: 传入配置数据 + :return: + """ + for key, value in interface.items(): + self.interface[key - 1] = {key: value.get() if not value.get() == "" else "NULL" for key, value in value.items()} + self.ConfigCorrect = 1 + self.create_img() + for conn in self.connections: + if isinstance(conn, AllSimConnect): + index = self.connections.index(conn) + 1 + conn.update_node_config(self, index) + self.update() + + def set_default_config(self): + sql = f"select * from conn_config where node_id='{self.ObjID}'" + conn_data = search(sql) + for index, conn in conn_data.iterrows(): + self.interface[int(conn["node_ifs"]) - 1] = {"ip": conn["ip"], + "mac": conn["mac"], + "conn_port": conn["conn_port"], + "addr": conn["addr"]} + + def get_config(self): + sql = f"select * from conn_config where node_id='{self.ObjID}'" + conn_data = search(sql) + return conn_data + + def save(self): + """ + 将对象存储至mysql当中 + :return: + """ + sql = f"insert into sim_objs values ('{self.ObjID}', {self.ObjType}, '{self.ObjLabel}'," \ + f"{self.ObjX}, {self.ObjY}, {self.ConfigCorrect})" + execute_sql(sql) + + def update(self): + """ + 当坐标发生改变时修改数据库数据 + :return: + """ + self.canvas.delete(self.ObjID + "text") + self.canvas.create_text(self.ObjX, self.ObjY - 40, text=self.ObjLabel, font=("", 12, "bold"), + fill="#7030a0", tags=self.ObjID + "text", anchor="nw") + sql = f"update sim_objs set objLabel='{self.ObjLabel}', ObjX={self.ObjX}," \ + f"ObjY={self.ObjY}, ConfigCorrect={self.ConfigCorrect} where ObjID='{self.ObjID}'" + execute_sql(sql) + + def transfer_animate(self, status, packet, error_message=None): + if status: + text = f"目的IP: {str(packet.destination_ip)}\n" \ + f"目的MAC: {packet.destination_mac}\n" \ + f"消息内容: {packet.message}" + self.canvas.create_rectangle(self.ObjX + 30, self.ObjY - 30, self.ObjX + 160, self.ObjY + 20, outline="#92d050", + width=3, fill="#92d050", tags=self.ObjID + "packetData") + self.canvas.create_text(self.ObjX + 35, self.ObjY - 25, text=text, anchor="nw", + font=('', 10), tags=self.ObjID + "packetData") # 显示文字 + self.canvas.update() + sleep(2) + self.canvas.delete(self.ObjID + "packetData") # 删除展示的数据包内容 + else: + text = f"传输失败\n" if error_message is None else error_message + self.canvas.create_rectangle(self.ObjX + 30, self.ObjY - 30, self.ObjX + 160, self.ObjY, outline="red", + width=3, fill="red", tags=self.ObjID + "packetData") + self.canvas.create_text(self.ObjX + 35, self.ObjY - 25, text=text, anchor="nw", + font=('', 10), tags=self.ObjID + "packetData") # 显示文字 + self.canvas.update() + sleep(2) + self.canvas.delete(self.ObjID + "packetData") # 删除展示的数据包内容 + + def __str__(self): + str = "" + config = self.get_config() + for index, data in config.iterrows(): + str += f"【接口{data['node_ifs']}】\n" + str += f"IP: {data['ip']}\n" + str += f"MAC: {data['mac']}\n" + return str + + +class SimPacket(): + # todo: 数据包类 + """ + 数据包类 + """ + def __init__(self, source_ip, source_mac, destination_ip, destination_mac, message): + """ + :param source_mac: 源主机mac地址 + :param source_ip: 源主机ip地址 + :param destination_mac: 目的主机mac地址 + :param destination_ip: 目的主机ip地址 + :param message: 数据 + """ + self.source_mac = source_mac + self.source_ip = source_ip + self.destination_mac = destination_mac + self.destination_ip = destination_ip + self.message = message + self.up_jump = None # 上一跳连接对象 + self.img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/../datas/images/packet.png").resize((30, 30))) + self.id = str(uuid4()) + + def move(self, cv, target_x, target_y, duration): + start_x, start_y = cv.coords(self.id) + distance_x = target_x - start_x + distance_y = target_y - start_y + steps = duration // 10 # 以10毫秒为间隔进行移动 + step_x = distance_x / steps + step_y = distance_y / steps + self._move_step(cv, start_x, start_y, target_x, target_y, step_x, step_y, steps) + + def _move_step(self, cv, start_x, start_y, target_x, target_y, step_x, step_y, steps): + if steps > 0: + new_x = start_x + step_x + new_y = start_y + step_y + cv.coords(self.id, new_x, new_y) + cv.update() # 更新画布显示 + sleep(0.01) # 添加延迟以控制动画速度 + self._move_step(cv, new_x, new_y, target_x, target_y, step_x, step_y, steps - 1) + else: + cv.coords(self.id, target_x, target_y) + cv.delete(self.id) + + def transfer_packet(self, cv: Canvas, nodex_tag: SimBase, nodey_tag: SimBase): + cv.create_image(nodex_tag.ObjX - 15, nodex_tag.ObjY - 15, image=self.img, anchor="nw", tags=self.id) + self.move(cv, nodey_tag.ObjX - 15, nodey_tag.ObjY - 15, 500) + + +class AllSimConnect(): + # todo: 连接类 + def __init__(self, canvas: Canvas, nodex: SimBase, nodex_ifs, nodey: SimBase, nodey_ifs, config=None): + """ + 连接对象 + :param nodex: 节点 + :param nodex_ifs: 节点接口 + :param nodey: 节点 + :param nodey_ifs: 节点接口 + """ + self.canvas = canvas + self.ConfigCorrect = 0 if config is None else config + self.NobjS = nodex + self.NobjE = nodey + self.IfsS = nodex_ifs + self.IfsE = nodey_ifs + self.width = 1 if self.ConfigCorrect == 0 else 4 # 线的粗细 + self.draw_line() + + def is_connected_to(self, other): + """ + 判断两个连接对象是否已经连接 + :param other: 另一个连接对象 + :return: 如果已连接,则返回True;否则返回False + """ + return ( + self.NobjS.ObjID == other.NobjS.ObjID + and self.NobjE.ObjID == other.NobjE.ObjID + ) + + def update_node_config(self, node, ifs): + """ + 当节点进行网络配置后将节点的接口配置保存 + :return: + """ + try: + nodex_update_sql = f""" + update conn_config set + ip='{node.interface[ifs - 1]["ip"]}', + mac='{node.interface[ifs - 1]["mac"]}', + conn_port='{node.interface[ifs - 1]["conn_port"]}', + addr='{node.interface[ifs - 1]["addr"]}' + where node_id='{node.ObjID}' and node_ifs={ifs} + """ + execute_sql(nodex_update_sql) + self.ConfigCorrect = 1 + self.check_config() + update_sql = f""" + update sim_conn set ConfigCorrect={self.ConfigCorrect} + where conn_id in ( + select distinct conn_id from conn_config where node_id='{node.ObjID}' + ) + """ + execute_sql(update_sql) + except Exception as E: + pass + + def check_config(self): + """ + 检查两边节点的配置是否正确 + :return: + """ + if self.NobjS.interface[self.IfsS - 1]["mac"] != "" and self.NobjS.interface[self.IfsS - 1][ + "ip"] != "" \ + and self.NobjE.interface[self.IfsE - 1]["mac"] != "" and self.NobjE.interface[self.IfsE - 1][ + "ip"] != "": + self.width = 4 + self.draw_line() + else: + self.width = 1 + self.draw_line() + + def transfer(self, source_node, packet: SimPacket): + """ + 传输数据 + :param packet: 数据包 + :return: + """ + if source_node == self.NobjS: + packet.transfer_packet(self.canvas, self.NobjS, self.NobjE) + self.NobjE.receive(packet) + else: + packet.transfer_packet(self.canvas, self.NobjE, self.NobjS) + self.NobjS.receive(packet) + + def draw_line(self): + line = self.canvas.create_line(self.NobjS.ObjX, self.NobjS.ObjY, self.NobjE.ObjX, self.NobjE.ObjY, + width=self.width, fill="#5b9bd5", tags=self.NobjS.ObjID + self.NobjE.ObjID + "line") + self.canvas.tag_lower(line) + self.analyseIFS(self.NobjS.ObjX, self.NobjS.ObjY, self.NobjS.ObjLabel, self.IfsS, + self.NobjE.ObjX, self.NobjE.ObjY, self.NobjE.ObjLabel, self.IfsE) + + def update_line(self): + """ + 当组件移动时重新绘制线 + :return: + """ + self.canvas.delete(self.NobjS.ObjID + self.NobjE.ObjID + "line") + self.canvas.delete(self.NobjS.ObjID + self.NobjE.ObjID) + self.draw_line() + + def update_info(self, obj_id): + """ + 修改数据库中的连接信息 + :return: + """ + update_sql = f""" + update sim_conn set conn_id='{self.NobjS.ObjLabel}-{self.NobjE.ObjLabel}', ConfigCorrect={self.ConfigCorrect} + where conn_id in ( + select distinct conn_id from conn_config where node_id='{obj_id}' + ) + """ + execute_sql(update_sql) + + def analyseIFS(self, SX, SY, SLabel, IfsS, EX, EY, ELabel, IfsE): # NobjS的x,y;NobjE的x,y; #分析接口在哪个位置,再显示 + ''' + :param SX: S对象的x坐标 + :param SY: S对象的y坐标 + :param SLabel: S对象的标签 + :param IfsS: S对象要显示的接口号 + ''' + if (EX - SX) == 0: # 即垂直的时候,NobjS在NobjE对象的正上方和正下方,x坐标无变化,只需要y变化 + R = 18 + x = 0 # x无偏移量 + y = R + else: + k = (EY - SY) / (EX - SX) # ey-sy/sx-ex + reat = math.atan(k) # 根据斜率计算弧度 + # 连线与图标交接点坐标 + R = 18 + x = abs(math.cos(reat) * R) + 6 # python math中三角函数中的数值是弧度,而计算器中的数值是角度 + y = abs(math.sin(reat) * R) + 6 + if SX <= EX and SY <= EY: # NobjS在NobjE的左上角,NobjS的右下角连接NobjE的左上角 + x_S = SX + x + y_S = SY + y # NobjS的连接点坐标 + x_E = EX - x + y_E = EY - y # NobjE的连接点坐标 + # 显示S接口号 + self.canvas.create_text((x_S - 5, y_S - 5), text=IfsS, anchor="nw", font=("幼圆", 12, "bold"), fill="red", + tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 + # 显示E接口号 + self.canvas.create_text(x_E - 10, y_E - 10, text=IfsE, anchor="nw", font=("幼圆", 12, "bold"), fill="red", + tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 + elif SX < EX and SY > EY: # NobjS在NobjE的左下角,NobjS的右上角连接NobjE的左下角 + x_S = SX + x + y_S = SY - y # NobjS的连接点坐标 + x_E = EX - x + y_E = EY + y # NobjE的连接点坐标 + # 显示S接口号 + self.canvas.create_text(x_S + 5, y_S - 10, text=IfsS, anchor="nw", font=("幼圆", 12, "bold"), fill="red", + tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 + # 显示E接口号 + self.canvas.create_text(x_E - 5, y_E, text=IfsE, anchor="nw", font=("幼圆", 12, "bold"), fill="red", + tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 + elif SX > EX and SY < EY: # NobjS在NobjE的右上角,NobjS的左下角连接NobjE的右上角 + x_S = SX - x + y_S = SY + y # NobjS的连接点坐标 + x_E = EX + x + y_E = EY - y # NobjE的连接点坐标 + # 显示S接口号 + self.canvas.create_text(x_S - 5, y_S, text=IfsS, anchor="nw", font=("幼圆", 12, "bold"), fill="red", + tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 + # 显示E接口号 + self.canvas.create_text(x_E + 5, y_E, text=IfsE, anchor="nw", font=("幼圆", 12, "bold"), fill="red", + tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 + elif SX >= EX and SY >= EY: # NobjS在NobjE的右下角,NobjS的左上角连接NobjE的右下角 + x_S = SX - x + y_S = SY - y # NobjS的连接点坐标 + x_E = EX + x + y_E = EY + y # NobjE的连接点坐标 + # 显示S接口号 + self.canvas.create_text(x_S - 5, y_S - 15, text=IfsS, anchor="nw", font=("幼圆", 12, "bold"), fill="red", + tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 + # 显示E接口号 + self.canvas.create_text(x_E - 5, y_E - 5, text=IfsE, anchor="nw", font=("幼圆", 12, "bold"), fill="red", + tag=self.NobjS.ObjID + self.NobjE.ObjID) # 显示文字 + + def save(self): + """ + 将连接对象保存至数据库 + :return: + """ + conn_id = self.NobjS.ObjLabel + "-" + self.NobjE.ObjLabel + sql = f"insert into sim_conn values ('{conn_id}', {self.ConfigCorrect})" + execute_sql(sql) + execute_sql( + f"insert into conn_config values ('{conn_id}', '{self.NobjS.ObjID}', {self.IfsS}, '', '', '', '')") + execute_sql( + f"insert into conn_config values ('{conn_id}', '{self.NobjE.ObjID}', {self.IfsE}, '', '', '', '')") + + def delete_line(self): + self.canvas.delete(self.NobjS.ObjID + self.NobjE.ObjID) + self.canvas.delete(self.NobjS.ObjID + self.NobjE.ObjID + "line") + + def __eq__(self, other): + """ + 重写equals方法,判断两个连接对象是否相同 + :param other: 连接对象 + :return: Boolean + """ + if isinstance(other, AllSimConnect): + other_ids = other.NobjS.ObjID + other.NobjE.ObjID + return (self.NobjS.ObjID in other_ids + and self.NobjE.ObjID in other_ids) + return False + + +class SimHost(SimBase): + # todo: 主机类 + """ + 主机类 + """ + def __init__(self, canvas: Canvas, x, y, id=None, config=None, label=None): + self.ObjID = str(uuid4()) if id is None else id + super().__init__(canvas, x, y, self.ObjID, config, label) + self.ObjType = 1 + self.ObjLabel = label if label is not None else self.set_default_name() + self.interface = [{}] + self.connections = [None] + self.img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/datas/images/主机.png").resize((60, 60))) + self.img_tm = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/datas/images/主机_tm.png").resize((60, 60))) + self.set_default_config() + self.create_img() + + def create_packet(self, ip, mac, message): + """ + 创建数据包 + :param ip: 目的主机ip + :param mac: 目的主机mac + :param message: 消息 + :return: + """ + packet = SimPacket(self.interface[0]["ip"], self.interface[0]["mac"], ip, mac, message) + print(f"创建数据包成功,数据包由{packet.source_ip} 发往 {packet.destination_ip}") + self.send(packet) + + def send(self, packet): + """ + 发送数据包 + :param packet: + :return: + """ + connection: AllSimConnect = self.connections[0] + print(f"数据包从 {self.ObjLabel} 发出") + packet.up_jump = connection + connection.transfer(self, packet) + + def receive(self, packet: SimPacket): + """ + 接收数据 + :param packet: 数据包 + :return: + """ + print(f"主机{self.ObjLabel}接受到数据{packet.message}") + if packet.destination_ip == self.interface[0]["ip"]: + self.transfer_animate(True, packet) + else: + self.transfer_animate(False, packet) + + def __str__(self): + str = "" + config = self.get_config() + for index, data in config.iterrows(): + str += f"【接口{data['node_ifs']}】\n" + str += f"AppAddr: /Root\n" + str += f"PORT: {data['conn_port']}\n" + str += f"IP: {data['ip']}\n" + str += f"MAC: {data['mac']}\n" + return str + + +class SimRouter(SimBase): + # todo: 路由类 + """ + 路由类 + """ + def __init__(self, canvas: Canvas, x, y, id=None, config=None, label=None, *args): + self.ObjID = str(uuid4()) if id is None else id + super().__init__(canvas, x, y, self.ObjID, config, label) + self.ObjType = 2 + self.ObjLabel = label if label is not None else self.set_default_name() + self.img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/datas/images/路由器.png").resize((60, 60))) + self.img_tm = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/datas/images/路由器_tm.png").resize((60, 60))) + self.create_img() + self.router_table = {} + self.set_default_router_table() + + def set_default_router_table(self): + """ + 将数据库中的路由表信息提取 + :return: + """ + sql = f"select * from router_table where obj_id='{self.ObjID}'" + router_tables = search(sql) + for index, router_table in router_tables.iterrows(): + if router_table["node_ifs"] in self.router_table: + self.router_table[router_table["node_ifs"]].append(router_table["segment"]) + else: + self.router_table[router_table["node_ifs"]] = [router_table["segment"]] + + def check_destination_ip(self, destination_ip, network): + """ + 检查目标ip是否属于网段范围内 + :param destination_ip: 目标ip + :param network: 网段 + :return:10.2.3.0/24 + """ + ip = ipaddress.ip_address(destination_ip) + network = ipaddress.ip_network(network) + if ip in network: + return True + if network == "0.0.0.0/24": # 如果网段为0.0.0.0/24 则为默认路由 + return True + + def transmit(self, packet: SimPacket): + """ + 转发数据包 + :return: + """ + flag = False + next_hop_ifs = None + for conn in self.connections: + if isinstance(conn, AllSimConnect): + if conn.ConfigCorrect == 0: + continue + if conn == packet.up_jump: + continue + ifs = self.connections.index(conn) + 1 + for network in self.router_table[ifs]: + if self.check_destination_ip(packet.destination_ip, network): + flag = True + next_hop_ifs = ifs + if flag: + conn = self.connections[next_hop_ifs - 1] + packet.up_jump = conn + conn.transfer(self, packet) + else: + for conn in self.connections: + if isinstance(conn, AllSimConnect): + if conn == packet.up_jump: + continue + if conn.NobjS != self: + if conn.NobjE.ObjType == 1: + conn.transfer(self, packet) + break + error_message = "路由寻址失败" + self.transfer_animate(False, packet, error_message) + + def receive(self, packet): + """ + 接收数据 + :param packet: 数据包 + :return: + """ + print(f"{self.ObjLabel}-路由器接受到数据{packet.message}") + self.transmit(packet) + + def add_config(self, router, router_ifs): + sql = f"insert into router_table values ('{self.ObjID}', {router_ifs}, '{router}')" + execute_sql(sql) + + def delete_config(self, ifs, network): + sql = f"delete from router_table where obj_id='{self.ObjID}' and node_ifs={ifs} and segment='{network}'" + execute_sql(sql) + + def get_table_config(self): + """ + 返回对象的路由表配置信息,用于展示 + :return: + """ + str = "" + sql = f"select * from router_table where obj_id='{self.ObjID}'" + router_tables = search(sql) + for index, router in router_tables.iterrows(): + str += f"网段号: {router['segment']}\n" + str += f"接口号: {router['node_ifs']}\n" + return str + + +class SimSwitch(SimBase): + # todo: 交换机类 + """ + 交换机类 + """ + def __init__(self, canvas: Canvas, x, y, id=None, config=None, label=None, *args): + self.ObjID = str(uuid4()) if id is None else id + super().__init__(canvas, x, y, self.ObjID, config, label) + self.ObjType = 3 + self.ObjLabel = label if label is not None else self.set_default_name() + self.img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/datas/images/交换机.png").resize((60, 60))) + self.img_tm = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/datas/images/交换机_tm.png").resize((60, 60))) + self.create_img() + self.mac_table = {} + self.set_default_mac_table() + + def set_default_mac_table(self): + """ + 将数据库中的路由表信息提取 + :return: + """ + sql = f"select * from mac_table where obj_id='{self.ObjID}'" + router_tables = search(sql) + for index, router_table in router_tables.iterrows(): + if router_table["node_ifs"] in self.mac_table: + self.mac_table[router_table["node_ifs"]].append(router_table["mac"]) + else: + self.mac_table[router_table["node_ifs"]] = [router_table["mac"]] + + def add_config(self, router, router_ifs): + sql = f"insert into mac_table values ('{self.ObjID}', {router_ifs}, '{router}')" + execute_sql(sql) + + def delete_config(self, ifs, mac): + sql = f"delete from mac_table where obj_id='{self.ObjID}' and node_ifs={ifs} and mac='{mac}'" + execute_sql(sql) + + def get_table_config(self): + """ + 返回对象的交换表配置信息,用于展示 + :return: + """ + str = "" + sql = f"select * from mac_table where obj_id='{self.ObjID}'" + router_tables = search(sql) + for index, router in router_tables.iterrows(): + str += f"网段号: {router['mac']}\n" + str += f"接口号: {router['node_ifs']}\n" + return str + + def transmit(self, packet: SimPacket): + """ + 转发数据包 + :return: + """ + flag = False + next_hub_ifs = None + for conn in self.connections: + if isinstance(conn, AllSimConnect): + if conn.ConfigCorrect == 0: + continue + ifs = self.connections.index(conn) + 1 + if packet.destination_mac in self.mac_table.get(ifs, []): + flag = True + next_hub_ifs = ifs + if flag: + conn = self.connections[next_hub_ifs - 1] + packet.up_jump = conn + conn.transfer(self, packet) + return + for conn in self.connections: # 将数据包往所有接口进行转发 + if isinstance(conn, AllSimConnect): + if conn == packet.up_jump: + continue + if conn.ConfigCorrect == 0: + continue + new_packet = SimPacket(packet.source_ip, + packet.source_mac, + packet.destination_ip, + packet.destination_mac, + packet.message) + new_packet.up_jump = conn + threading.Thread(target=conn.transfer, args=(self, new_packet)).start() + + def receive(self, packet: SimPacket): + """ + 接收数据 + :param packet: 数据包 + :return: + """ + print(f"交换机{self.ObjLabel}接受到数据{packet.message}") + self.transmit(packet) + + +class SimHub(SimBase): + # todo: 集线器类 + """ + 集线器类 + """ + def __init__(self, canvas: Canvas, x, y, id=None, config=None, label=None, *args): + self.ObjID = str(uuid4()) if id is None else id + super().__init__(canvas, x, y, self.ObjID, config, label) + self.ObjType = 4 + self.ObjLabel = label if label is not None else self.set_default_name() + self.img = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/datas/images/集线器.png").resize((60, 60))) + self.img_tm = ImageTk.PhotoImage( + Image.open(sys.path[0] + "/datas/images/集线器_tm.png").resize((60, 60))) + self.create_img() + + def transmit(self, packet: SimPacket): + """ + 集线器转发数据包 + :return: + """ + for conn in self.connections: # 将数据包往所有接口进行转发 + if isinstance(conn, AllSimConnect): + if conn == packet.up_jump: + continue + if conn.ConfigCorrect == 0: + continue + new_packet = SimPacket(packet.source_ip, + packet.source_mac, + packet.destination_ip, + packet.destination_mac, + packet.message) + new_packet.up_jump = conn + threading.Thread(target=conn.transfer, args=(self, new_packet)).start() + + def receive(self, packet: SimPacket): + """ + 接收数据 + :param packet: 数据包 + :return: + """ + print(f"集线器-{self.ObjLabel}接受到数据,将进行转发!") + self.transmit(packet) diff --git a/datas/images/packet.png b/datas/images/packet.png new file mode 100644 index 0000000000000000000000000000000000000000..50a8ef046144a6eaaa029659285ed37dbf6d9b37 GIT binary patch literal 26968 zcmV)5K*_&}P)AK00b-s3=Fw>B}GB*P6`o`Q3?!5AThbPfB`pPWMKGq4MZ|9T-L>-4j14| zNi9wW(jS0Wr3h#_&`1#=TO=LA)&Q}SAZ!~DyS${J1jr5mv5P{2oPq2VAX_6HiJgSR zPA(_{sqX=@ZE|x;lM(J_;7QF34F=L2KrF*h%n-!j&ftXbfBqr{28*u@49pV{Vo5U@ z7`C@EFi6}&h^Z`KVBl|IVAysZA%+N(g2bXkun-FnGo__5Fnn6hz`z^Hz#x2qfq^R; z77#exrT})EJ3~GYrZeO)qyj}e8S)sC8T5cGBL)KoLk2Sd9)CVo->dpK001BWNklhPM3)RFGf5Il{*V@9CWYCY!mV!d8J>i+u8 zPn`Lk4dv5=KJ*AYapn&_13&Tize~>g*r9mh%#R(xr~7|2Bk)AbKbom{YRF?p;0b0t zb_9RN?|&@Jc&q~XqyYYo6X4Ia``vYlHNwEsTCX-Dk5>9_B_P6ioh&e|b-QTGniAI& zsl>0%gYQ5v@!P6yz0v(eOW$)&2hZkN2c7i}j61yIq33rosqetlD|cO9sTPrTSa%V*55_Zg@kC-#zEua-*UMuvMCagVuG!8U_RG-YNVf+ zKt6Zd?cGGG_jdHWm7dqU?>?Vt@^TSvuZz1~qOUGe{Wty4%HEucJorsA$yYVUs^f9Z z?`*bXwTkQWyPM}hKae8dH~%&Jb-SOza$sV+j(sqq4P%1>w%Cey-B<75vb`*pow?|1 zQqa$iXtCXfqS}yQF<*)@+dX~mx}CjNM}Nfv>N4m0a=KcSH>&1siD_tRjn^nng8)#&wdt*f}N zC_0@UZ;ZdUoaubEHsgZYTGe8i`@4AB!_SFUyx@3O40fFJz-J<1aaIJwBGRBTq9xs? zKWb8^E}>%4n=7Q&^3F8S5ck@m$ydqq!-bUO35 zeldL&>1ZrO!1}udMCep%U+Zi(H_PsdgZj5}oYhP{V=2~0U!#q>MlXe+d-@Wd&cl|P zkP;>|#QuCS-57y&!()^=&z=$6^OIVjjz20ayBz~n5$q=6)L&lbp7HmpSS(^OB~nV~ z8f?gu7t@h`PlN197Yn*2*{UZ(%uOy={~VG_jAILD&ezr3&GYi-&Ki!_Q-^ok0rqj` zwc5SxO0r6d$6DJ2dTVifFIF>w>UX|p1wd~@Q7_0qvOoH;)B-j6c8ZlaDzs%r2ouoz z)Or!|QgkZxAWqb7cj7l4T{tiV1&mmVP?w1VxDz7ql~xYSp{KdC0MM#v>v|r~GFnI! ztXNLvu_<)dtC;M}>DuOaYOL-t++Z>fJ~WXFE!_ii444e=rNxFN>a$5Sotqfk96NfG zFx$E~N~^ymNfi<)K`9Zj@7H&{Cl5TMqPkEhsS+_&oTcBf8@=IG5X3B3O-3KwbR}0gIwVhe&%K7Vqww+d}0$ z2*U2@V#Wf{qJ|Fjb$0oomCm>YwRHC{x?D zjCHKAt-rHKEwvlt--JYUoL~QYzUm6bb|o8xg~e5Adlwz9C*nRD7R6v->EG@T^?SFN zi@4{ydZhtb=~~9EYrdX|8MY32!^QQ$7Au60BDAgux)0~MI@iDNi}}r2v6!D1^Z8{l znO_wX;pxs`R9ZT=(NxB?F~utZ?`XNw3c0u~Q$;BWuawfTGN0O=a8)Ay@Y zw^w3NuiM%`uHkd}tn@jHktIYj6KB+f%=8{a#DcT_W$}{UQ)pz1BN50@|HMip9b2s< z_36cWGW$r2QtvUi)ic_NqM0}w_$nx!ql6Ov2;1PE!FttEe_KcTj_2~Puh%mM^mVEC zWJ=;bz7vgFcJpsEoldxiwrd)tjjZ7`{+sW7ov!zo>dgK9bnXdrcGjy|Pt1&W&}Z7t zeVp^(p%leGkCFb3^>3nfzg0ia^a}keF7=N-Gk&~}b{*^QXK4;;%Rqng{i)8Ux^LNS z4Ow3c!2;da`o7=pem5K*{r%DL^Z&PZbiDrP_kXRcKh~2zDta0!q{$qEgc#^j3^G*1 zG{n=Zi7gj%M1EJq3=GDiF}^8!ldEF>{ny3p)mO#kH@_K;FTVb}i{;z@Vo_WT$BV0C zHoq=b%UR+QjCF9K`e8g|&Ih!BTcCw%y`}5Adgs1lA7(3B3t?7`-yY1l_u(1@>C#?Y zjA;Ic1txPZ?#~dZgVajg&u1tDvBzMRN?+>Oh7!m_w%}z!h!tyB=UvA(E6awpMRS$g zPrD5n)7eZTVPx*9x8HqYIkiOld&Y0y_+zF8cXQr$-QKxVhk*^9qk$L-aF+2`8wGn> z8k4ow-?A9I{{27tm;dF8_c-?BX_F!CyJb(t1 zs2`Ui()P`HvAlj$oPY7B#pJu+{C8Jh|M`D8z4=^|q+=WLZf{%Mu$1mFZM_bn76D_$C>EV!dUI7=Ux{Vj6d(VSfB40l>+^s0 zfBk1xi+`v83(@AO=&5}Y>)-#Wbgu4Nw9plI0n9HYa{E0d?5248-JciV{nLL`y#2HP z^SA4ZuRb5n&x((quZ!m|WR4o?nt^zVuI=mEmTi5pIxVm2#6f4%WVQ_cnZk;Er$F-t zH!nx$&Oeyk7|c+-SSa^Dvj}3clLD3Ot-Cl%-6{}GL+(I&nOGs}7z3Y6tQ?vw}0^u|Lx1Go3B6q_0?y;_a`3}-P2zdZ(hGC2B$Ae zX2Ls!qogf^Jv6v|J-nD)6xVOQD9(QSkBiIS{^|c--2CSA)9yuaIvDHUtQZOw^!u6w zapvV>B4928((1wn-+c;Aap!?Vj$^w5xbORwVBQiYn5;9lNQ79~>d~!#Z-ra^>R}*| z61$MUV2w5ox0M7GpqGXqg>O75j)fl^}@Z+M*z0&#e z0YI?=XrqHx+UoLEarRe#TDb{atlS-d28Qpe0CCf&Sdt$w4C$H8y>y?M47YRmWeef5QR=VDCO zjSbA5JG#a6R)!sHaee?X)|y|DU6>xt#887nR^`v0ZO=X32$+;VYaJKE^+>Q*x{HX? zI+dO~m+xaWo677e#;GxSCK|e0To&Ja{b&F2YWT^2`r=o=ULSpSDot~u#axYGSJ#KA zgc_F9V)Ev<#n~7CkIsMocLwX%#b=+4iW3>eJFRIko88EiB7(h{>LpTq20dXO5p!22 z6l~6LXDR6mcQiUn!}ApJSAYTmcm0#WPfF+TaAF2Ct21{npM7*$jBys_D-M7=oxxGW z7L-^iKW&?0yTPaZ)}`&p**J0A1crw+V9++snHrwnT~}H&VK*`BhPY2I;S96(4%Chx zvGr~n|NNePPBGs%Q{Tkl=_bnR0vemhC~-5kUt@IiOq{t>^k(hiVm2-&XTSZQFTVSW zzdd{*Wl#&C-PK}9`b~Ex>gq~VDgvV@uFqtF`=S`X`pw@TuHF`(e$p>~^=VhOU|}PP zf|l6vV!G1HWMdH_9rcci*66v!!BBQ@*#hy#u)^(ORa0!OLZIY{MF|XIhRSWemkp80 zJczSu5CmjA7zZ6{YtJks+P9TsZFz9X4K)T`Y@$9IL|j({%0QY;V#Z3kZprFrMdsmR z-fhm3vF7ZmbL%En&6b&HvzQ9*+ylq2BgWO00^70kVmqAKN6cMG)wVz}TzD$K#e6;~ zuH|>Qnu)<^Y*A-M$MO)OcJZKPrYFkBX_RfNNVUMR5=_ zu0gmm){E;mUlsGKZ~nsMmk55qfP3v>v6?K4*|oUzQV*C4s6T%p^U7&4 z(7#r1V3;vb=Qyu*Q%}TNKVW`945Wdb7Rk9_^Ny_^_dhcUZ6-pd#X}YYt>~%Ep*d{m7Jj-R5M~Ax53er-|OQHH(O4CGE)RRI>;)*Ukc`$eyDs1ui0E;%GvRZHia5 zXmzJB@1dMIg3iF34UtwS>|K9*Es5(L-4yL>`7tifip#6B;$m_m$K9$pky3a%l#OGE zi+il_iFR>%+%I0;ToxDK|5blIITJ!p(Xl%50Z8$Y#(+EKKuRciDpXHqI232ziSl zT3=k-{_d{idui!kXCh2|UYw0Zf2+&lcqAGhi4jU_wB(z?8I9?6)IAlvoy+QR{U5Xz zlYdYwq{nsSZ%R$jOw4uRQVQivn0PD?-{fzsR})Q`9-~)Lle~%rYs!_RwQh^OXv7Auj}d@#2!jRMXwpI zZ1RC^M04j9lmyq)>W?E#h~;^gLX@Mz@_EyAkXx;Dvt4-s)1g}bN(FFxn>*Q%z2#@O z+;K5mz1K%8i$3%Qn`djy!C4l*1zO4c$KoW6!ZzaTE79CV zaWg%aNBLaWOp7Bad@^$36tsk2VjU6F9qQjmA)`_e|NZuA`43iCS_ne9`RE?uED=HW zyZvG$&N^St`W-oHPo((GfEV(KFL0*x*2Pdn-5*R9sip>s`1`$n@%HShc>UGu;^yMK z`1JEXE_xryOW8V>WkanIIG+l*cl&azcv93EJa%-12?bMcL~NyzwYr=B##&(&7)}}Z zHsC1E38JPBn1ggnFvv{I4a{n#0xsG(`-S?R2rbSv(@!^GAY7}&U(z)@5rl&Fg zxE;dYYFz6v18CyJ8kU4ZiX+MM!}wUrIsPX}QDRv{0}FYWi`kpv^_PEEOm4m_uE*oz z*{ENtAEXlIO$s@F?a0MA_yp(msTBz z_Nfv4LrKfJ46CDP%bX#c)z;r_olgYNTN80@t*ke4eZOd(6ocnSBHp87CK2A!%Ei5^ zKW&e;y&0lp9AuKAvnYV}3gX1D1P41eX9GBp!co&t+(*ROT_&Vmze7hIt^Rj+Je=N|8IgzK2*Ph*uVxmhOr>z`O1OU|J95w7Mopd|?dP8 zs8sB@k?XLQgk!?&Ivg-w8F220oq5G$N#U`W^Rf5epX}bpl`x09oXI&i8()ecu8Wh?)8bgB-u_xTnxsAyV|`#de|UBq#zj=x z)?QWwoYW)7D^%GRHgMB!z4T}x5B)$Ul8coz(yt^Q7RASMjX(e7b8$|ogItIrsD`ba zixqHz7*)KF!HI)-!=he&_Sc9qw=!0LtbDvoxIe=IbMQfIh@N+lHH3DsC?9dZ{d5dnu7!6AB6$S6@fJCT6E zPI&!g@#Yngh&RRX+4G{KnCF2ML98L*poQF)9m4r!DPIyCvSG3cxDliqk!ltMP@1_-py)B)KP-#h@>% z7y(5i(U?NN35{6Danjdwh)zZ;Ok!a>&iz7Q_Csc=l`{cp+16C-AXRw~Y!Z^Ccy?#yjc+om-qHzjoS|$ym?O zEHB+$PzDdkbrYAWFHCqw6)@T!XXz|uWT*p3Ws!L{_6~djili&oH8YX#C+ia zscIpVayu;|E-3x~y0(*?7n~q}uhK(hJ-2!>C5y@f&m>z$D}rY~^z2})N4)>gcE5}J zOdL)s2?a0{bTB9W!KlR4CU1(1^WPRX*WbwzqR7kRZgC`UUVkWMa2dV}2R&H-3Ft@w zQ5|8{qXWpAYC0WhgJFhadWCKHTSPY_Ed@7f`J1zEHx)tW!V&4OdyQ)ozM)ytUyJUr zTMR|GN6I9)Q5fjun{Q-uUKY{;=yPjrLjSry3eWWWjCKSOSm;30lrBi>r%Q;^6WT z4nL98Whi(hCD52F$TN~KQET}e`GPj}`1cBKl%D2f4T$T}dhdT$3S8>d!%TV`CO~@v zY2%^ihno~1YJz-!*Ncz|ce5uk$^&3(#o;lP)#B=0(Bk47`5-RjIGc+btJu&;bF6rH zh}iJLPNzuo43^SR__M@?I`nsN5a|p15umlobk(%NnKL&`ZnJw6^9FeYQ3ou@ zm`N*F)(*IH3+_Cb%m1z<*|7pQC7PzXrN5MOy`x|yyN3}c>L~cj3gtEn3~xv_1$B^Y z@;b(T2sxXRB)nQKmb_^0d5!_y4<+{f9_u#u^QU*lyKB5Fu)7kpn2i;Ac`5hwr8u&% z@?fx(#X^KEv+YoN6iOahCAkulWniA#X5P1ptp{)fCm&xu@yt%B-gTU$gMeiK3DkYi=uy|fFyZ~DBZ>QG8!2!y#W}oVt(asH z^7-rE%4+eY81&m>(3=!5o})Ll?O21U%@i9Sik%Ds8?h`%s5BVmpax!!+8uQqjWH}L z8r36SHi$WyxHnVgh7D%%PGiRqrqJBF~ z>M1z)9{q%uOOP&M5MCTsCE941V4~n6|50qJjOlm;v5pK!$FhPbNC^&2NhUgPI2!5u z+ct7n5GRjmvxexs$NEQbp~qgP*c$~6U7ZVBTzqSed?bwgTrTPt z&lN4HnmBfbS?42&NR|&*z)GgvG78eOoBrj_RrNTaQD)h518mOiGLN`&0 zBRF4P98FB)%QxktYwecUFNn)Atah}-FJ!a;LRLL8L`2xa&F43&lq6f@YIUU-zAQ#< zrIHU7N1~3;5F;sgf7jeatZeGxE~iPtv{AINj&V0c*N=cae%L0|m|e_wZAzPV+zj)| z7#dT6AOms8GQFL@R)-cIfAlHE*FVjQ|qc}!p$?59pCO7X-HOfGiF-Cij) zfNWeVYaWhZq$LiI3Xhew06)E~lgcIRC?=xEZ}Q8J1v%#G^!QRKG~(oBLiGmElvO}& zUME;ZEsg}MXh*Kk#=?sVR%s-R!{~UNBM-5D)IaA%w`mUgVM*%4z0J%fOEV2waK4a& z*}pm$%p1sj%DhoTlYCG(t*bsMsQ&er|5(iVd-+CG|3ZQD1PvXFh>6*mPE<@N$A9xO z@kp1zRp^6jhX__K$>SuK5De`XlhLGlX}W%aq!)yt?0{>X3ZJ|3mZs1!MUMoY=E9vd zJ99FvjBH2s2I=Nh!5`3&BR#a~yec9HLk|RBY|TQPYp+%^|HKb&bIaZf6s^#vR5% z_yHwCTB>ESoKBQpC+2hhjh!KFr8@PL-ZdB~`psD)B0d+&l2apPmQ*G-URCKl)~_H= z!gER`pTdZ`jslj{X>~uM=`C zc-J^dk!+T@4vd-0`86&sg^|x+{Z?fn#gV1p4Ts89Qmn=BNIX_jS0yp6s0CQw%`n0Q zdwViMy6?0VMXMW@xDQOs;{9_KUX)vMAZxno1#7@&BIs*j=^KR!p0++wX0Ad=J0fc3 z6q0C)8cDH`05z7CWhzyWN|oikk8~bWZ{tTsk_R=XGIQ4L+wphAS%?G5Hrm0f2VRvG zYOV;$tMfO)$=}N&bt#KQN79u9)PV?EP=QcRqU2nY4`%fM$!Fr+&SmPsZ@VtqM$AoI zaL_>A=ccGTN6~0mEtHPc6Netz^083K+zYaB6`VpnJ7s=rF#98&G2yAIEH%j-XcQ6P zo3C>nj^NzQBM0*Tt`l+|7sD^pPfB~rW2W)5*5R7TnBS&?6mN?&#Uo#ye{b$QlD_l& z*^xN$j0&8(SP_!0psIj<6LFrGx^ZATWthwEV8nF}oQPVd+eE*Om>;l|?nAQsujz|` zmkM%P%EUvMKjn}6%Kb9Ys>H#3@m2vVB-R4_l~AiVSkm+9it9>mIRb*{hs=7KYI}K4 zeZU{zm!IMjN{pN=QC{Q@zDmkGU&ymJxqd4f@;Al#8(A&JXG(6Sj)h=_@=fi9wM$xo z_3Q$``bYso0X23X+h9iK!qDpuB9FxM9~aC3JYPiv001BWNkluO7Px&Xox9 zY$=|jrL5e^EK2QC)*yn8dQvn*?6P#kkTdpmqE5l`8;WVZvKrJ!6w>`E((Lv{Zwp3c z+A7U&p^-@P5z;G;eDPX@{I!C_zLnMDq8Q4wdMa3Ps#uGzEEsTNT+3bnwy{N#9~7UR zstG?cclDOtA7*St58F8NyG&Q!JMpw=snSfWFjX*R_&hLdNx>jvt|1+3LCxryR0?70 zJ{5qJ`Q7zC7*r$U+DM8Gn|WyF!4Cj2i8JJ=wx10tzPpaphsIS}v*JcfvBEg7&tDgB zzyC%U`Hf=L6>A~$+cOdJNF^!eGS?9SXS53^#e8bh9d2s{nU&?s-c5TGH@-J9Zv^Ml zAQz&9Ow>q!76f=#S;Ya-QX%hTDo4rdh;V@&Y`umOvQXUYLPbKZ<{cHQSrx}Bc-~c= zkB)5N%9NCdqv}6+I@Tw9eGrq~qo`v8Q`^3p#`aO(zjre~AM_~>6jEVAhworQ%Go1x zStF#gynXehAco9u%I7?i8TItES3ExvM^+t`g)AA4#7VX4_&ZQV&`93)?LBm`Bxdi? zLUxK)(g)?tRVNvN6beX^q?M7R91;&L?E)uGOI&=plKXqC>gp(X;^Id=McrYiG*!mV zN3Q#vlMZ5NoeaQ{wesDlIL$pIRy<}o_{T9 z@rAhZ8&$e$xg@~RNOdg~SWmGB7ki+qI-0db`8R-|h`F zelW}onZ+WjwGFu7XgEAjI@>XVg25KEGS7)CkesjrddSLgeJ%@!YWQ|WCvvXH{m!O0 zaA|vu_4&+k0z~BXwF27haK2D~fx?dwG*u($pDG)D5A8N*Hs)pVOUYKb z*fV+cFu~2IZmltumaJ2^*Qn~+aWuqC6Lz*1liNJ;@e#?PR;;1=HK;E+v7ppc47bZT|rkXQx z;jt1Z=OW(Wv7iXm)@7rvHUj#cCfS{x_ZSPaO)iyaEM@Of``O@i?MV-LrfQteo+1J*C2#JHtu5<*y`PZ9C!pqugX1@p&7}D8JwRK*)m10=tPh`5wG;yG2l1ab>7r zx}f4}i12s8$P;GH#ShC)*m`LV+aafH!ON~h3@+si6vR&^!`1byxE5zw$l8H+-d8Cw z~FL+yKC#`!xvEL1IigRwMT_1v1(W4FG{xdbZs;4 zBEUdW)==JD#~)4;*p0k!_BX?H6BUHj-CszD<(X27Ul$< zX5Ud^<|g}&qV1+Fg~&yF;z{|Qz|4sW8y}~*5!2^V@q6rlT^-$pv{x5D7ZJEnOQ$pD z!E0R1GKh;_9R9~+z(@X#|Is0=24!&kVUxG1?PBMwO_uY&QXLIHK6S@4BXjslTtuxp zRl5nop6cINr3~1)Xm}!G*VYsrMRAoyoYB-3W8k{}*rZ+agAQe>l}&fKFHw^r!xT$F zq=f&#b4DozFefvRJZZIH@y!w04ry{|YLr;0h^@LebCeM4(H2ViCUIIKRCxWR_6gz4UXWMk=afcH#1MgHiHbqh^*CH!gKD!;r`gKTdNGja_^STy zZccHk4g7}F#NNq#2NTbRfi=)?;(j+eKw~xJ6~Aq$(!jD+?(#~DQ4w3Rfv)CM1!WT< z1u3Z}$BG04958|+OejeNYsLW%Z|6^K;goOB+_lCfHY_4Z<#C_0Hz`Y&^t@Y4X!DtY z)%SC>QfIYkF;8|RLIfEnMLD+{*K?W&*CBDeWKEX}y*q)yc$WpIZ$-#c*^#dmk9;XY zUd)y1e8JQ2Jf64LCXoa_{cq|3Ke7ivuA){hXXV>tLVMd^iUu7V(b zOUj1INhHaP=*@EKyZvs%%#EqoPpL_}yieQT!!;;}ly_qDpn?7^g!vS(B%+?&2sg{h zL2Aws8`>W~PZe?4Ks*G1_iu;uN8s+(_Qhbtf5~vEU{xNM`uNz!b(>Ic5qFwAbM`&s z3*k7GIazeMmJD}Gg80r$ix61*zalNo1mVY~l3DDa>;6TR&86}#-zwJP?9~@i{LbXr zlSlRBNdIK7mx8iR*SAvvLqDfvV9kCY{#Jkz zH9Abp5c1Y-!z%0>m@Ue(nnr5G2XR{&%10gerSIbU1{cngpEwhjI@+ux3dT7efZaS- zq|ezkCFnzs@J&FSC^{LTJpl*dYE-R{$Zw4m=1k$@3n_aPF&y8#aR&8BQ0(~lNDdLQ ze6*Jho68Hvd5PR1K%#GlTZK$lCAZHn+cZ=JcHUer`fjkr&fMTE4nfNk0A`JqZf1~8 zF=;|8Lc6oayO-{rzwBQ2R^kW-SM^bp8G`qRvO{+iIyzn4=$|G;`(Vs8q1Q~%u`(hQ z6W!59nxyow0Zi;WQ7N$%WJ;ngaU_GQG*ne72Ds!FVlDfWC%LiUlhTw$q|Y@1uVyY5 zUHIA?i)Y$G zT9Qo`34U#1=f49mR^VlB;^d9F(ZYmuWWk}n$$$>J{*vy2X+3#4U_WB^c}N;~09!J-Rjr4RD^AY4Je8Rs zctnDEm_EIou;Wu8P$uuw=Olv#Eqz(q}!`l~nJ6|cViQZDM(GP_A3 zR?W9l_7M?R9$&L*sVp8EE9=3G6$?p^ad>Dh)o>fMy3Kc(neUjy8136!`95>6Y`O%0 z^VKTP1u-C|(_~3Fmxk(+SYripbKYabTbnV*;F}UV-K!A~x@f-w$UL)$21l$ZX03Ql7(;m9uRi!hUdm^f&*OS4rpgOy8rM9qD$pVzNg>baF z_*^xERk*RI!sra3n77Xz*`@p1G?Bf%jZZCl6EX1?O?cFB&@;+Z`iKvyq*f;4?8!<> zWv%EA){wG%0(0pMb+&b;*c3Az(jk}njSY&jc$QNOqLMy4OeX3 z`+)h*>!n766*JhRx!ahTiQgRIdj=YX=PdiifgzZD4BfXXG!N*|9hdHh8OgC~&LSef z#cr{cAt<6(P{8RxGEqtKA>D_7r0KP)@>~d0ay?tQ2`UUvWVXY!r^V`PrrK0!pp7|! z9wk;4URA*w6D0qYRfKo*jxcLMAZcriJ{g5>{Y&g*Q`H3*f|sGN{jGf*Y%t&ao^v&!y;{QZlQhvRJtDSu?W9 z--7eyTqFr>fkzwa7{8&>?NiY2np<}+FVbWN&S<7I3+^OpMN;e~X2T7T9RKv8F46+; zW!@4TFu$nD-QOKCH&jHA)^u z&_xV7W0Q68Ka&h6l^PiXLTzaqxJ4)tx|$b3Nc7PBvlYU2V=`eNW^Bh+#90@DIfic4 z{Z2GhKjHk~d~zS{B+47&usyFFB4=;D6(N7=ePfPfM?MlxUeYJ+ZN9+%u4i`zv*6-H zy@A(J0OQ!kkOGQSzuhM2Lru)}d98EUO_&jcy$gCYX(N@iOCL)5JlOTbHkmnyGRsqJzs#Nwd0?M{rC@U8T#*g?l{G5Qs>LxSNWo zFGL7yIT}tyd40Kh@wCCs?JYF!rd9+BTH+wUfkey@H_>lV2qn%jMwuXzUDo+?v>c2h zYmA>^@!`H;RC0@f;3!6a%htA(Bg!4|DJn5<(ZW-}6BZCIUZ&#IePv#IrcpJ@-V8QF_gI*|;Ig4?hOJsb%xG-FSd8`7)_ ziC^sws$ghRf_f@va3bXS_Ij$!mCNG#tztaHF_prpFjDP^MWzLuk^P^=>5a<;5nR;6 zV-7oJ^`y8J5sV|eD9KwJaKoP*3gGFXqypGD0^Ng{vpbq!16BYhz`AP{k|fRqLgw-= z@e|r3hMiBp|MstBp?M>0+%8^dQ+1->R&wbp?mptc9Xv#VZ?8?QJ6UTK6}Q%Nw?nz0 zftXh^eSkqVg)#A|z0BqgUdGV53TxW+%S#*yrYf)<+U$4pFD)URUDE^oBq#PBe}A?GT;%OrGFb~j2HMC`NUlA-p5 z0Z~dbJ-eBPA&eQ<5|_*u<{TbR&R}owF~nS`2)pEp{wJc7>zX(VXWj^IEEF{K=9@p4 zoBA8ITXtk^OU7n6)O`a-ca@;<;**hfN4+Uty`B`OQfObkR3S>$5?O1JQ(UL3a%Zta zDve5Zj@zI&@mvCyk3Hrjfb#!qa4fNo~11ozS64SO;@+XVVvv8fx`8Mck!^6K7x z4$fZ}Ci}0C0SZVQRN;k{#e%+u>sqOyl({#a539sf${vS&AQ7xic%$xH5JHOMB*BMc zjarAyZxsfdoT(D2oEF`YHV}IL(m_d6aWbCA=nnd)ngq7XrLq#h0n=bw{NP~PNWG5| zkAml!vw#@^B!DBafvC^M=y)-~#ZvHq6JTOlDvXiMFx3vRTol*yHwxoagdi^Jp_mcX ze<5W0)>hU9Mct27w56>AlrwEAdUbKF%Jzy|R@GJWCo*?R#V$;SvHFtvP=8zU(G|17 zP23!uc~jiqgN)N98aeA=EZ>X<7$C?|P|bu@SLmg@>V4?D9)!kWV!rFhT4g4>jGw~| z$+|hTGrT#;lClx`Sy>@TpX>6(*Jj5-9$aBTxFO;&JrWK$pU$M*U5fxQ%N63tDvzt2 z`7W5&uHxg^J4N@9Rns4YSeQ&i%nBcU^}Q;r}Dye`;N0|299*3UwKYOpCfJ2X48~853mGh zY7}Rj<9Hw(67DR_*_Q?6dpRA>FK&v7HspLMu6+8@=dyr|^!%1WD)n3}4VDga#;_TU z&20h(NIFrAQ-0w5`p##yqs|mo^46g!+EM{Gh9imF8yEAPBuB^9hV*Q97Z4#|oKgGj ztKwWcpuRk%%8;alv^{5rP@qM9L1D#t(@V-~OYs;pxv!B>P$bahBXucXYgys(o3Z1+ILD9ezSE#^8NxQm#}rfsugxYK?h z^(kT{A9R!r-LkrGapG;)q=0l@PpkuGIC;;Oa??(=RRP2{nFPFG4{vj2v1b=BF;^5k z5W#Y8i*q@YRPh@fZH?QJHJ~jH_x$;)xKQTz^`#sR;*Q|B<7dAN>j367^&ND}y+}o@ zDswnJgv?Tb{ptH|f^CsU$%VmrmXaJnDkyb4i2y0R-&PGIhLQ^)YV8?vtvtQUx2nh~ z>&0CAg+{;c)ERC(%9Y)a=pLiAm@H)xmyAwWj>3UcE~;>=M^p^SxT$NOLS zvPuLzn4t4pn#M{uy50ody9LU?k!=d|5;m@fkcp0ih}kS0UN{_%oJlcI);3r&$LV5m zm=hUBGOtAMsx6BFJ19xiqjDjEUp)KBA(H1J=BYNK`R*%;^t1Eg*^AG$qteGt4uD_@ z2C~7-O0h^2r5mrK-pS!?W{4MJ(~ovo=x^J=)uu3JL^Pk$P*HzO$jWi@_Hx&ks%aq% ze;0y%$5180; z`?+@|%YzfJL+da{grG2GLFlX1jWDUiu?lNnjaS;9LglN(O;=J7monpxmg4EjGXNd@TOwS_%y%suP350YKl(kz&ySQ}sIp)Z z+Z{=Pu8KB#Q%+28GCeUl3as_I?^}CEe1h$nshf0GN7zzKFlWL3;#w?g?Iv)oy+h7a zW$03_>b}}>e4_q{__{s9FRx8|9c89>Xt?4vz}?P))%#eiD1SZjBBethwTO~CJq2#a z$S7h)g1HR)hyE}Ku1v?2MGq*ZwcQPhfJEX=KY zR5b}v&9=!GuMo!~V5RhTWxMX3oG24qE|?=~N6Tv9u@6T`{0nSqM**@j=OBvciR7E8 z-rHE*e{!RUzL}gJDq^Td7az&1XGIUfwbL;;CA4Q3w+BU%B>=P9BoC&b2z?JFl-OuY zhiv;RBG=;k#jk#;g`(1Cl4N}M`~@2!Ns$%d16$YvjfLS5YAb1xTpP4hlhAuC8ubXv z+cXlkWN-0dD#~;Pkw?OX83H(7QWvG31$c7uEACTR3Q!F{LCozf&YTD)nw#*6xm0Om z3?LpE7>_68=-_S+ocCC6UX$IOSP5pt?}~%Mk#X{{A06g5i+dkQP&sy^5?UdeA=2zL z$Zes&*wtwl)?+ZJg|3Jj?iNKHmwNu?o7WnPYncEA7bb!uSABU6@ErNg{q=-53-^r2?^MVd=n?&#X&0Rd* z?@AV+JTuwSAsVDS*T$~h1q*~^w6HKk!_HV#D4i?^5pwdT0y!XNmqu62^i248PVqt@iN2+Z5UysV z;)zTaSUKRv_I6RHItxM^BQ(9vR6;us0(Z9(A(mqoH^=S=N=$%z>z;{R{bT7F1V0TW z0odkwMFOy6QvxFgqyZu+-PQ$BPl!sgZ$K5011wNP{05z!M$Dh#^yhu58R&LdSV0$wvt4^oCggItyG6ZNk)9PBaau?y^C!g zDxbw|>0GBvS4&7!=n$8(@6~-3%vL-kQvd)U07*naRKPc0t+ongr12^VD6Mdd2v(A# z)E>i~w?K><73U5GtjUConKry;mqgNNqEK=mWULl_0?*l|0PIN93|4t)mWff!+}}CS zKF1fNOkt1o*4Og``5)(U-D;bJp8{Q%NfeX_PWxFOm2=g3Dr*#f!FJVo* zkFkNLh*kGFjzS-RMYgK>IviX%-8JBmBLfpR%U(rCz1`CHqZS=U)^938%!8X2xs^ZQ z#z2d2W*uchRG~OGPM9e6o|XQH0>sH=eCybqM=~`pvf706%o=yDICYJQ#q|*=Sv!n7 zA`_$Pyc#I8X-%w`csVC-a(VUp?D)I%fMXxLNYOj$gYT4t8%_;#f4%~EhXRjXlq7&1QK!QXt}83;CGrJG~XzEXtlPs zNK6tqglDgpw#A~2h*!8vGkZAXK*q>~rUabG3&7!qNlHP)tDIMus)53#ve;gV2zv@8 z4eJC76UnU*97-D4Tf}UpPaH@%c>H!GAXJDlX}hq9*&TDSx&_AGKectFJQx0Sj5euR*R+umu3qk-UV@d}HS7x`bE)pbLHQah6 zPUsl5Wauq){cz453!q|_nb&6BJ<;E7Vs5*NAofY;06QaC7ca?!^X4rHciL8}mD+V# zR6<}3EJO4*m(9MVdhg%D!jP>Qo)8j1ffs}?D#&N9^rz$Fp&^E8&HacdFEFo(dEUgmg#MLEUDXvoGZ=@X^^EQ^Bn|i# z&#t6xcPJ6bcE4m-%)l#(3X>>Z-zXm8=u|xu=i{EXuw_?i`Ca?VB?5`Vg9CZ;AHMqB zYBe(AbgG7Vf--jZn1SV^j>SSDjI)Ux8S=!fv>zaLq35U1^n12N2gk#fOTynup-k9(~+g|cfCJ8jlGQI@Sw zYP?WDUp#v$?u@DPrZ|7A7!J7`JEyXx6U(9wLoaR73DzQ~bk&X~@m4US(Rq(%^c$-Q z%+VndJRe5^b0?ZZNkwu*RSlLL2?b&#%bEOFk=VPIxsvyagmp9Gl#F6egony$t)_Zo zM;;1i)}wHqz=moxu~{#zF}8Pn4CEIpFD5+*D&Qf3=%J%x$Y032H-ZB^w5eFqx#Y5j%g;~08Hes9e4*J5;!uxQ={$FO5)*k^7J)P0G}^*^kTMK6BH zGXrx7r@c3wKHx&nv(1@UaB!Ie{gj9x?u%!2Lgo#n8B|2ZCPc@C+Z3aJRUm!EkN|wy z61A%#8QW*CGK8HZHFA3ID!*NNc9WP(6I_1zCQ+wlwDFR#J05z(omqQz#VvS~u%9L&37$iUIa_(_s%37`UFSnb?Oom% zY@kx+3?Xs3Tlrpxg(J-oc4(AC4x}Q%N!a=m`rbl(M&+B$Z5oGbzvg)>Eo}RBu^pVb zjD)Mdm+1+OcvB2i-#>m(ff%XWlnL2_PTvXi3tB7$?r((EfElRdeYvQC7Xv3UhSS8v zbfEreYl*Q;j7we;4>_BoF-B?H9&&#C_`QQCST_mJZeUO_Y*JqJHw23fpMHu#+Xl_% zJzO^&Vu7jXHT3LQ)&coaxwJaA^#BDEE3@iL7<n8Vf^Y4JEsvezv{P?%wG1SFfCucZ?;T6*||*p{nsN&>@CHVD>{wSpWq z;)TJ@M4bUIFy2SdOvDCw|3MFYj;ly(qS1=V{k*MtcoeHWVjtdtnDR#*WqGVeJ>{^Q zOKVhkSA8TJ3Q3;paue~Vk_8T{?O72gejNNfR44(l9hmZBFGKe2vj-^*B&Fof&)?+I zb|jgUGdHhHsu|v96yWBin3xJzS6xrUoK{HneYlP6feGf)QL_DPcjDrHD0woLCLvxn-T&0a9}NEshg&!yKl1U$|!&v`qorN(mcVLEN?j zW+Dg6N70kR4-u(cz~t*qrR+@<(uwldl~ZJ(ED`7K5lrstKC#+EDnwm~Y*0V!{n7Bp z@#e$cobSq6+mCz7;dhtJDusxa6q4NG5%*Mu6*x#r&J$;Wi^*MIT$&><6kX+b?G&_c zqPAuX-px=DP@Oq6KUO-^_8tI1Gl9c!H)vHfdP@P^_L#c;q^{eC1!04^W~gz{c%pcy zG?QX1uw~xJmQ3y*147U^sZY47bEQ$csu>^GbOSb%wR<32KB&F#{-*od!{7az^AjPG z9279iv1ev)Sbl#N<`i@j#FzZ0}x*-hz-JXl$DtkZ;}) z-iw`_FoJYk|LySOZaw4hXWw;a=sk~X|6T96>-VUOoDQ5vVL~YL>PQ6UE21YNt|PX> z7^+4cq2Nc#b;Lu++!<1+xOqnjy`kS^qRpJS2FOc-4npRC!N~83!K}FRy@@#)n3D-E zsA=Q44L$A}wr#&l;j;-&Hf8Fj2yh+}=JdHQ-%~#Y@#cBRUN&nIa{Fp_B}JmWZB;&Dxy;Zwnq_ zk!kg&!6c#camQotf85*O^>{w0HHi!FyC-7Efd|3O!kU#GU7-{LEfx#GMM+VL%h&>j zeVTG%C1Tyjo%5VNNJ?#~HCMAESC7&zA9jWiAs5`x$ro-415OW4Y$zr*5h(HmkycpcMCr8#wd4y}qFHoCAyL%ay+BZNZ z5A(xgYS=k5N53aB;oBq=8=7FQGeKGT^j?n1O2TbvleL9uuGTXu{5hH4`5Y1rgj}j) z3dbd8Sn;BeGg}9ssTwz-c*P@04GscN#8kS)A-)hD9c6%Eb#Mc0R}N**fkR4TUZ~Qh zitOm02w8Ed1gx{FffSTor2ya}#}Mm@&WiS-K@&EyIViWm1HkY1JnOP>7JL}Ef(Mht z*Lrcl2a)Iwx94X3LYKI+j#%*&s7qL=UPq zz0!{Qs&f&W#)R!S2kYM!!F3$P-|xUaoYLE)6mGDmAMBizFITAcf`8!I15V`NVdt2_ zy0xnM-17d4`VAidwBU1deBhknSV4L^eN9lY3jtFqO&EHH^GgNXJIeO%XlE`5*ZXFURC1sXuXO_o~c47`8nXNV^sm7B7K{ZSlJDVmq&Lm4PokBGcA|WR*3{itIC96 zS$6+3x2AWMTfzCW2Ji z>J0Q2F(=7&i{<4PvVthxR5s!kX3yFvL=}zAO2iaf@{RVY6H(inBscXydxkh5#q*dJ z0YwmPzfQzi(raa(4A~W;Zy$%*#d-4gyfY>dW>IBZ{7|1w$0icQYDyit_c`k-S#5xe zG3@s{2lEJS3~eEIn`b6)+xKx@EC;SZ%62txQf0!%EC~!;m??ahgYq(cYUJsfZp&MI zIaSIeY8SOb#{wbxyA&z`U8KM3#GJTgrVO8{!tcqfn;YTo`6?4svfotZxEOrz*KBZ~ zu#mZS^|zbF7F`(CrR7fK%LgNK^n|5R_#){v0)}G6hOs=JvIEQ3+^>~YvXTZ_D%^dJ zrsstet3Stoy#I3~z`d5pDV52gB`=wI0xt5`cr)GF@rYIr-ClX zDU_4Y{Ta)nYl46?JWo3^} zAyj^N#xY6MK=rl!zaaL&deN5rcbw6N=V~;;DQLRhTvynNXe+iLYaa2uZK5Z)B`KTf z#?s5Qf5>;t77q!y6wUu(&*pAsjUi>sjE;`@N+360`s1CFX#~;&317V&T+jEm$L~vV-l=zRwy?%zEA8X z!O%$JsA{`p8lrH-ErRl!nuoKpgJ4~-#4cqxYVK^T-+;4;wdyYtP~pmR;U^|*DANVu zoOG7K#K%uGUPc)98WEe~H-%Lre~PxIMU%L5Z7#O;WAAra8ihIN!yS(+)$oY6C#bMj zkxqoXYjko=4%-N9)uLXWY!b3Ev0>&)(swFiWKS7%rkU#5QAibEf_FJ!|8d`11}ntE zu)v36A~h@2V-`r1kRYS7s;V6L!Q}}0+=!Ea`4D=*)C%E)%76sXDZu2V03@dTy zT2~Fda@R6V#H5u~xANtRb)ptTDjk*BoF1U9*aDl(T^_4OF~)bHdXvy9wjIcfeLV;mgqprd3mk7&20;O_Cg zvt39{_?tR|0#7+G_X+N%LO~E`&s)@mlD`mvVzN^)Yx|d%6d6iz6{`;S%g^iAntQ|S zEVXrisxi2dOHlJpU9^pZ6pyHbmL@j+I?Sdq8t4h9EEauE8Y{+ZE8`fy#fiN&7~UH- zX2u3NN_!d$FQe@B`s`6Rk`~K7sx&E&2|+~od$1*k`NB){)_HF*ks5?l3I2B2;pnbA zxzAzGzVj|MJA1G0yms3KWy250&AA%WLGL%sMAfC((wO-TsNf8eWDIao89a5Ojj8en z7i>R4K_AV1UpoRIiOoc9Hm7u%{%d4FxB|)^rObGKWIQdr8PV$GHZf-+uF*rL`jVja zCTgTbW(`CgtB-*I3%jVd2%37t5OEaUqsFjOo`4o|OUE9#_)6r$%_ULkHjBe$WxW<} zoj08(w}+fcOpoC#QG)Bk9K0I0?ZKp2iwi!q2UwRLb6|sZ02q{E_jbTjmsAr$&M3N= zg7({@hn_-2SEqCv5rplhX*e8ScnB#+SWjGPU{1_2mcKp!g5F-3=W`IU8Dz7eL%l$3JYV^z;ku019v@QGAsp+@eWa(?yF$!xh@U;;RKzC z_|T|9rrq_XV-5~6=tQ7Gijg1y2YnVK<D@ZKB$IceJ5S z95DQv2j@T*9rZWf0dYQfQJ9My*y?=SV-_P5P*pGu@E97%L(V96q06$NCh&qPome-N z%SoLR4KFb|LFB~Y@TbB<70PgNAtQlyo1JM-mzjQ$8LgzP8rh}qQek{%OIzwz8!*LK zMEoSgl~(3bls|9xrZf4^6~!0X9?57OifZI-nV|JQOe~?y3TDW!Ldiflj7v% zanXKpY_YP^K6yk^u?8R*V`S24)Ikn^Z&OTW2L_SJs&SuLLaN;7y4}~HafkUa6p;Jy zXC^Bwl*|x+AJzyvQp7bo$FP(%%s4OVP}cl&mvx=C|6z zv7;^MFyVSvTwS-+4qiRc%uLT6W2Wn#7UP+A%bi|}k;5P(T$Fb!?pdUTQbqxB-Mr8U zFIS!K;i5Q1TBUweP)WHA$TD^@9({ zFmFGx3+532y&KbWSAuD&ZH5Q6zv~dO*+7Tc(AN@rhcUqORXgYBg*UQV2vR)jJu3z+ zRan2)OJ!efAy?FhCEMS6ghQNfGBIK=@}`mxriFCFVO%l4ZQ07NXXLr<^GI$6%>@UA{_Y+y&n3clQl=tV`{G7iB>u+4= z;8hmmfvg$eWyuoRI@_?j!vl6;dU_Wgw@lOzS7aksm87;%+zeYV3haol4wGyZcU}M7 zye|XpUDta9Sf1w?XF^9+k3;|1RB0`L#Z2MCN5|5=Xq{5`xEAj*0ZPN;lkeN|Vbs}( zB0f%{@CyEK;g^qI{DS?a|Ht{o?0;CYhqMgZJ&Ayh;KfuMAC6_YxSlBJ8u6ycr|w8C z>P@7HVqS@u3oOX|-=+Im3F*k`JyC=@gaI!b* zOMgs1Kjhq%a&WLU0?t6Ccw@M1(Mjk0xp{VMBL9z=hVY6Wc5tiRRp=?e1!5Z>ayHp# zzz?d+HQQ2Lvj{baee?VJ=L`W!ETv!h?8H&Gi$RJ6imjiGxkEmAsJ46#wWhkC&#_>` zq!)7_8#J`2`NW?%5m%myn6DMz(_NxS7R9CXF|2H#eDe9<6?I4Pyg$yv+;ie%qXOUbhPTInY`5XY-Zk*J(2yX1Mj0K(it_5 zjl|)~{vl=mG9I>Na8$D&@z8R8A3(&J_^VZtR)becBCfm6>~n{mJ6Tuqt-EmMR{Uq8 zK8&JiURXs+(3%=veU7$RRorooTk37zZ2x`;lu=66x6MI|szmi?WM^7xs{0;FYS6;A z&()-CR5{XZ^4i%FL9iKPfiVIdu24J=P-lE2<|+I0QvYt&?0WT!zxep~{<;5t@aZ4e zR3}SzZb;FcjIWC4{pZGB6PeqF!_(r~=f5tFUj6#N{r2X&|4KY{z((a_z%sQFgDlt~ z&O4IxV~7QWOb2qh++KAQYlb_9W}FI7o62P00uQmc12DWr)`?`vKM zkr19(5mO?Ho%0_C{d@pNY~-(p$d|A#91wf2|gy=yY6?R2*j&pvIQ7IY~Di9)l*BvpT6-Hx08wn9ih9zo17gMG11clD1sE8A9j zW^3FNQEtW(|28%WwaJU*f9%TC!2kdU^GQTORB`;gxY0Jqi@~RtpZ>Fd^6BUQ;=eUL{^ehjvrYyH zd6)4j=hk=Isu5njt8Zb`P0qHC`Uv(JiGzojRnXC%nsNJyF+6y??XZp- z*!eq1aJJO`93G8~^Q&`nc~lE4b1*?FDZz(Axi{8f~=3%S&MYp>=8@)U|>-^>4o1cIGpIxu6TCKSBgaU%cCwdetkd!g zHqEwV>x97(V~i^z+(y*(-|Ix1i96TK&exShpPpMM`p~7K-E#~FE@B?VVSu|3_=u`M>msf9vBv`0M}Vmw)uH6vZdMFNS*}TmYsXVRchrQ0#ui(Hc*`RHtOGTb{}p zgBb+?js|z8@?77mpvRly`r9vx%dfsFmT$i=I#+N0*81wz-&sz+`*#=1$4;F`J0_z{PNRdBH*qHEat( z*B-m61^I{e`b1(7jhbM>#0eekShE|~Kbz4qkDReqwflWkA0AeJehFHuu`1~@l#)nc zR-W);s-YGQRR1()%8@XImgB1DF!1EfNgz0&L5#6=Ga4R44N7DxuJv!G+o%OWmMLe7 z6UHXm@lNjdIlZ4?pL`A1^Lu%%KEo+s{Rngvt7AQ1u@3&uvBN4+(35ceUV->;6zB2h z3N!w)-97r#-tfh$El0;M|9KJdKwC-=UWmX)#Y6#03l)@snEMzmROT&n=GJ8Vg&r&H zA!1%GaTSt>g*drX(=L^#r8Z6_iWL>t8s8Kx?WsMzc&$jMH{#9{VVPrb7pxi;F3W#s z`zSM&xg0|rIfDdP3nitx38sxL$6n5)acxFUH6O8;vkO}FyBesZ5$AU2XWYe^;ZRF9 zir0B7C#2u{cZ);TzDLwPMrSxWmS-!CZ+#A$eMn3uJbuZZ$lq0vSKP60yzYX0xgERa zviW=rdhOO2c%aZx=o@dJ=wBtiildhzU?r1uMEjI=LyD-p*P4uOF%QB@38@&!M4S%= z;S~?JPAs}?4@LpwpdVb>3V)E*LSAXOZ&&7IJuMj!TC>@ieBoris+I*- zJFisO|5P*{@W;DBiG#^U69s38Ibxh}B`+@IL=lKjFvA4rfru4fMpb2-GmEAMNn)S5 z-3s8{qHVt|VgLOUb%ZIATpRAz=9$X<87>jGTaVvms$J*=5`f4d?gyM%8NifaN869P z6u?|A*hG)%t#-~MyX)*XCJDMtL91IYnhKpEuiKv?e8bwY!>Un#55%lEQHK&UctjR- zWlV^K3v8aau?eihe?g6vRRKU~3lX&B`#^bI3wkV*t-N(JmI7pLJYkO%^zAxwUZL3x zgGdgd%NQ@J%%C4^n~dEAx-o4BS^{>?{zHC99C4o*zYFs|=J%?phZ^?+;~pv*f*_NQ zcxZCKBkqe>m65u&dbk&%Wb7mk!>oQ~X?ddUrpbIP=|O^mN5fWCWgw`6n__PtCl0 z1fF2V-DC8lzT&YkPyA07*naRCr$Oy$5(4<@Lt`1B|bgxcB_Weglj>U#qXxnco28YbEYI|FPcyW6#&> zYjx%~!1!8;d(VIDH^A8Qwfb6}`3*3>R^s0CANvh3_I$0rR%d<#jQ_E?mn~anw70k0 zr=511`9J>X|LWtu0mi>suIHbB-ceIiQ{wmg=LG_RBTduXHyjS<_4W0o1_A+>WkvO! z4ZE~xG>pq|6AT9F=xV2_tsN{UczxhWfx$trO|UG8g6?u)1$yxq8lh-}{DOk$ym|8$ zzVN~e5B^uu8^)8r*1)*)&O7G~4h~-7_xlg)?d{F#>FLq>`ugbY?WMoJUkTdl^%99h zln7id7Y>I*{W}_sqG=i#Y1xFsVKm)=jZRg4J;DA6S!1VBShg<#(@n_K2!{h$!2wc@ z2*I948Y?~pa}co9!!Qg+jT*&)2OfCz6Hh$x)-cL^k1qZHdth91%{7^!Q0V;L-rh3@ z1_s6l0s*hz@7D$f1_%TKYLRT)R?i}Ir_-q(^0x?HKwz3Nt)BpfWm(9>p&4jVjcCLo zf<@5QsjS%vDTN$=`o)ZzcmO?q155K@n_*n|vHiQ)^649_`)CQ)Kr=}mH_=E)flCgb z&lm0Q?|1Hz^oH@I|K9=Qq?1ls*V585RfH`Z|1ZLIyWL9gA{fyE z@<<3?&XNBw|CYby{kpCzaR_K!niD;2<8-+R={BtcVd`sI&`PKBt?ylpFK-f-BcDhV ztVo1p#{gYBSFrqzC+Mqu59~fLEwC+hJ^76I^H(mHulx&q8PG5K`JHo@JonNsni^VW zM59qhT3VVCyr_RUdU|@u%*<4R72(UX2wpUWmfDm;r z+C+dQ&u)i7hAT;ZX4vgyXK#S6J|`pReVaqRb0Jap2y~Z|a0r|hqyXDKeV*m7{t{Z( zfe}WJ+Gu7B81jgJ*RNbIU-=jKZ-H^b#n=3F<*HAv?WnDHFxZFTw221#6#$Bhi+2lH z?sO5VfJ1zRgy~551|n#AB+Nv@q$JGB(C?MFoB#Q{8h}7h43Xt?;SV_=YXUP)U%);G zok-Y~Mbre%1fK@_V36g1`vu#U{tiNoV1_}gRLqj?)<}^^uq?#rSy%ST(q2ej62B{embl9Cc-I>dn#Z6KgX2>PF&6Q3(uAOVu0W+HJ1 z%k>ieMndx>%#dbAaoI81nm(O~l>%cA<;b%xBX|6C0$K`aZlx`fBQV&wnYW(1m+ntr z1xFBqVGIS%n8r$IE8Diea=CovUtmvwanNyR{py3&n-)wv=`7On%1KVnBhNPim%o{9 z@BNk)Z$6{cd`e0R9UUENkwwTN+=NDue~bE;^W-7sAYmF3=0VO$*!hHz5}G6d3eh-n zO?e0~0zI8Lylz_i2f&p{=E3th{+!Ek7mPql_9A|(J?LkICyA{eEaBz*u7yA|+Q1+l zG;EiPpx7FSnJ2efwA)uMm#_Q_>|`8C4lFZ>5ga5Vtg-x+r}*Fxi(#-Ek7eKhLRgrZt$-@pDJd!G z>kEuqA9^S?+UK8aYi8}@8*gm>ci>pIvU1*`$DFvl@VHZ$bHW+uNyQje3VJW347T&= zHFF@cLum_%EaV|~zL*xD&!+&80D_o=M8uH*jtC?XbqxI;3E|6ohB~A}!K2wahUFrN z#@f0a*zP)p_EFEW(_!oU7MI)Ha9X$8YPpMhvhkk{}|3qW_c=6(#S6_YgypHbf??xh#iT+^FW16-jGSG+DzPQ4+?c_(Ee4}f@wLf<4ciA_e- zg`Xf%Vg#B1c>MVB_J$1`VlnYocnm8rF1q^a3tw5Xe58wHr9`wne0tO(1M} z@YpG226djh{~ns&dk8~sCpkHp+}vCR3JF&W3kw-NdNk?j>7OB{5>B3IiJ^>ULX|f+ zH&b0*&CZ=Wl@lwfeW;cg>crZXf!mQm*Px%ux=rY#_vNDNA0ux}IeI#{y%MHIa0Wtb zdgl{qh%8%@Ntm9rI&M%c1p6ZhP5GdJFR6S<>D^YJHZci$ zAh{B(rf7!l?rsDS@@Q&mqO-G82};0{I1+xyXUvUdU`O3_^!HNVxRsp!598b`9ww`_ z3`aV++~Bn#*gnAW7yr%+KfMKvYKR2!*alGxG>49@#rPFDCQX{e4L96y_}OQl{qCL! zaJbGHHee*>U5pU>2pGe}L(Kyw-dLlL631-i!C)oZx?{PVc~fd|PdDPiO0 zYTkQ&Ijdg#fMhe7Bu@tIJ?)G*^gvF%@k-3}Bs^Xho~TX#mRcUT@cR&Kz-5FfD=XuO zBaWcFyjz%nUUppW!rY zgK*HIy}yH&&RQma>qJgI_bzfqO~joh6gs$VXszyL$sd;R&f`A^&o+pJ)PJZ&qCtn#Z!T+c04jV9v#!ax#yXZnr``%fkXJ-)%NAM+ik&Hll zPp^swHm+OG4cA`7O}8u}Z*)1^Dr#8z(i?32U^9hDBkAc4Fc=wN(n&{i=5?14PSS8C zxp4;kY=8G%es}%V5a}i}%}Z%%DMuc8BxA;mQPGG1K!lzMQ4`^-Jj>z>7+P9dl-ZE; z1P}?(B(#exp1dZLk=Tt+(L2F+(jV(1Cbk4;bdD`XVl}sQhRl{(^$gP@SzG@h`N&phtf<5;?M>9EP@-+^P;KD2Dg6#LRE zF6a1feH&j|s_0BCEtYvUZA+=|O`A4x^_5q#=(an^8c|IB&Sswb<1=hoSxHXv1bRX? zk^TmbzVRyN9(({9c{!L~17|qI9~LcS^()UnsGn4ympOChaL_>qk(ZaZJMxglPOzJD zN5(kYTQ>}woc`4h9JUV} zwa-5Gne)HTDW{!IVbKU&4g-HULNFM{);0QjdRe`0E!SUr9XH%~6WK*2banRf%Aa3i z^#_}A+a>sWgOD2HyNiCvA^XoJEi09f8wNWDclRZe~uM!3Q6`J8ZK0 zyq`C0AGYt@x%L^~KZ{dOKZD->K5FZBQd3vU)~yw^wzrX*;!}Rc3r{}9kM6jWlJc>1 zclYz+(|=~|@~xye%NY>6;~C)mpDyCS+53^4=D_cOofVDz^4fD~TfYKLw<*lc;_$-{ zS6V|fL&BGkP_wbIQ2`_2L&$5YenQjKx&KUR{dXX-l=iFI*@Zux#aTDs#*E|UP+Xo% zx~_g6TWewS>Kgv?;5D?YeoLuL-HPr826l|Hu%#};A>Jqyj~h)-Pal1)U0^wIxsy5l zlvB;8p88$P5&0h+!v>7;GiTZdAAT4;0|Ttvu%4=_?PTO+Q&u(xUs{^_X%Yccu2_M$ zxP(AwC;8(h5$+GsRn>v+D8-U0s4Sgp9=@BoGbiCmhCUnCe_X+XSDZz-ev7i#<)tMY zb<|NznKC7I7djkD`1SSmtXj27IiLxKGr?aZX!FlYn(g_zTZAHED(X6jQown4+|9IO zW>GXclQc1#CT!Xc73-^c{%4obv-yM10z+y%bPt;0z}9U-(I6RxIb8e0>+vL|ux#0K zmi~1q_(K$qETO4(+px*&pMUePeULXV&(1E$BW#*{_q0>__Q~I6%-FH$x}j9KL;zd1 zY~k|D7jWavw_q9uYgVu4oh5HG&=DouJBGIUCKy-9t-rpH{ilyacR^1WRxaDb16Q7f z)mDp3Gud~aNlIe~Z=vo!+oqwRflojERGE!LjfH?Bkq}GxABiw=&%rSCOiP8{Tei1= ztAva1xsNGF&7`n2l~kd-gRr>T)T8k8MoekJ5y#%Cnqga z6|r>&{P=@G6$Nass$#){OSpN_t&A)yqo=!{XC8Zom9H%aGY5h;OdrqUhwo+9lsx6g zw+_InWovl&y7|zt3nx+bpEFB2o+Cz#P>U?S#MZ4_S-*b0ayb7v=aCTYmop=B6^n=& zWn)DHB$aXHeGf4C@M#njC*cDI`eDlssM%7@V>i#oYT68;&~9MpXn0-8grs`Q=^(0! zQ);sBVFz>hbqg6eYBYw!%X`a~@zi5~q^G5szS_!Rli5(n58HnUgef>ft zZi_@M+#Zj*M}lTVWkEM@-ppke3ovdbXG8&w4XwQN)XP+^+J@ItL|`CH(u~R6ao-(G zo19H33ia)<;iHfF?T;?NYT1shC?5Of3baW)& zBD%vA*o($+>ST-^!;QD!L4I*D{exjLQq%Zo*>Ya^)05nN^Gy$&_ni~2`C{L9&)+p{ zz!q< zwr$%KO+J*dOeD|}X5^m>b@qJO5@%XS<;2=ewcs1e!UulI_=EQ$SAbzdPq+9O7IhVs zJaPT0(6d9C5&?#$qaqTCIwH{sVbP~zB!p~r==}JRUo&y~OdN(399e`1{QUmckC5Ro z_}lLv9X7e`)~5gcV(i&FMopV)&p+>6PCorKQZqAf8Xjdzg3$`PfKDJj@aq$l> z;jX*yBrV5BTWcqO{qq~FUA7tr$(Vh9CZ9Q<1=nB6_0 zj5Ox%KU7R8jv!)#Pj;$<_Y+h?DD9>;$BTjUpwz`$T25@awCQVgGz+HKspnd`2+ zhFfo6M0&oD)|NJ2{NoZfytxA0J}`BroOuqHU3Uc&$GPzbp`zYm<3}I!heekXZK@zE zD?_e0VK-!{*!pW`2UXsqpB$ejQbzj4UD8H^!Gv4E&;|mo)KVl zR(~!qT!xE4G>mQNags~pkn_%E{zVsK_)_WV?I$fc1+N9yUT{8*pRVS}BM;~8*N2Z4 z9X4Q$o;uY&=fd;(&dDd^PENv`HFG>o)RBb7*ht;MHf} zV%58!K*R)plo=OX&c)YW#)L6W0s@R$*zoa6{&3sHm<^TWxNGE1oJ zq4k_!T=@U)opI+=fRR+jH9vn)0V8*WnKbDucaof{tgL3irB`v+-M5oflukuOC9gd7CY5U{VzjZ} zWX1(oaM3jj7+>xp5P|Ko&@0#S$6GHZRKJPb+)QTAn#unA@2?zM(HNqQ62SQKk;lIW zhOm|!tD3OAqq*v5zhKf~Q^_w*BE^aUqoN+Rtl7kq0*sce>S9_nq)bUHT>ZzPx>uZ{ zHFUgX~naGvQ zKHvaGj4Z;|9CUPb($&=?wfIWSS6A=g#-H5AEsJiXWLyDtwYzxX$=6x;;c5sDfMqcA zyleQuHJ37Oj6q0%(EyuPZQ>8NTtu*T137tF%$&N90)`M~Yu2n$%x9t$f4G4m%%=du za+h-1J@+YKj2IQ;F$5Uf>R{WtE&M@%A>pbN_?Y4U0}K&_07G**u>>_4nLP8x>xA51 zqPm7ARC+kf`VZdcq1$eS?rvO$q5O}rW5rZJL>?z&e2qqsba z3`flQ6iU2e{WgBT=v?ew8+HRj)P*R}gw{|AH35d96O~s}eEj+E?-F!*6fm3~7m@y6 zcC25=T~}TKe!uvrD!Utrm`vQunq%02Q9N;?Ep-^}om~w02T4x$F?HHBGBdO3?&~Gw z575&$$ji^X!kk0r5q10N?H!> z8!Hts%=&eT64$JVLWBZ>&=+x3NSa=LqbUX^#m94RzD3yMMN9H3rZm{sL;a?W{OsnN zu={$54g_)Nl0lBK$x?byS68=JWV%-_@_CEz8!^I8PD^9TwCNmh@Ig$Uxt}V%GaRul zu3#X*)-Bt(;>v|wyx?Lo3w>=yd`d?{2Z7#pL`6(J=V~sv`cfv2^(Q(*x^%k6fgzuu>W!n|a_R&l7PdGZDXIMFwb!vE zujKX+jD$&c=&b)>89%%6C(zri-XrLeXe`nD8h2abElgkhwiq^Gc=8JEqfR)1^Dn)W z+@eB~l2eqoZAl}iZ4>MtR1D=M3ohlhJMSPbKcAMSc3ysF39H{*fguwEf)OU4eKBYL z-~wh&&QenaYPz6$!)6{^cs*@vSCXBV!l>c`Mvfe*q7W&-5l{%zsd_)+t%hHGiT?FF zZ4APu&aUQGdV2;r`O1aNp1*+HvLuppR2F1cJ?z}DiN|lgj^3)(60|E|gyVG=F}0y1 zb$&{5#BGbGU<8l9^fJ-pR7|H!QSVO6r1ImBc<{Dc2{tz2uw%XQLs2kk(xiL0Y}xXo zFZ+9k4H%_UrrRf;c?Kt*eg-L78CaU!4Pm0e5DrZz;tx_)UBMMsU7>)HpErWm#&%wL z=C7=N?;{M;AR35JbkcYE!PQqYZBicY6lm~A*|lvOkN@yicCGl3^b9BEqY71lPNEAf zarX=-C}iOU7!l1)Lt_iwZOxo;(Ur`Ye?1vv(nyxh03Ev9pnlzY{&35W>8xChn_lb$ zFa#We1W^&HGfCAmsh2X!#_-FhpCRl`A?k?Lhk9*`kC(p6V?VnWv%Lc&-UT-l7zZAB z;CaiJFaN{;Yk@It*8cVx=bg(T#~p{;mrOWfsv#lbak>mAW;jC4_9_-!zTp1?!ZV*d+%Lgtukd>(LX(ANz+KHb36cilqk#+A5`9+FT@V~9$T zdJJ(aRL6VlpYlh^%>B6gSHB_TO;UX;4q-^KdFAon@t4OQRTcHp9x06n31EnpJo)64 z4}9T;7e4&5zj@d+#^`A??bFXahXap3S^*;}{h8uj>keE_r_vbJRTaB|k(WP$=7x4& zRT^UzPCP^g!!V(opZwzIOddO04XFuupsTToB|rZSA3pOWZr32=%SWiHa4{i?W@h?X>KyXU@sr=a5V9AY)9f0*0viNC0Y9Z|3Fu7E`}!8BTi82*!Y+ zddhBs5y!{Rpu7+%I8&33KkVo&mn~(qUGTNXYsYXozBcUDl zR^RKe0b|VU{q0ldf1g8+ITj@MbI0y~+sjS?_)mL4~t#{r|ZvF^b8ar4b zz<7T(PEv>r2BA2YA3gjF#*dwV>4abs477Ff;gc`2sXQ;LNnu`H`C_r0M@!y*_ijz0ffzIVx`L|k4XwvE^6qNkyeU*CBQI%Ddq>*TiNH9cQ&2}!m% z9X}C0W&p&=mYcMf7rD=Q^686sin6M@0z4%YwW8UFt0{a||o^IFU&F-qk`OmOlRiFF)~nkeXSiOKHUfFeEgOYsbBEv+tFQ zeBK>dK5LGB`Z;HF;8928Oz~oBI+_IRatB+s5^mj&8m_qXQU#3kfNso@+|kHSFE|Ybt0>HIt6B_M99gVHVNQa}7&eWe zf#LDe-PKF=)(X4?pxh{)IhTR4gk6W;&{SOMHsN=p-T0 zgxva1&gam2jnA8nG1L?d)jmpft0+Qea42VmP_RbF6<`5Xwb z{hhz@*SmkLfT7U`j;KnGB**9Wg>@&DX2H8p!hrhDZg{|*=jA9E~GXN>>XrNl23n<8U}qf}RH zK?%boY$P5Q_}8tdA4>yJxVw|oZI{Xb*;OWY@tM}dr;bsL4TbD{MNI*{Uf&c&@07*naRCVZt zq833RFdPQt=W)r8ZeYfN^DyKFkj}JVfLEV;mcRVz8LZAuoH7L{Y7sS~sw_}6h8)wU zPdC@DUAtFW42B&T2OM!E5kpfxhU68*VUnz%ZrapTRq~^S3%TQ-#iSM&(p1;R+rNK} zb<1lpypsq;{m}n01qbcN8JFCK=_@4U7MdFzev`MJdYGD*7L(*2RHj5m9qb;p=y2?w z%=hn1NkU`%E9!jc_@x9`63^0%95h2pE7exi6L1zX^?SFl&*3NHaM>uac~fzlQQAIz zk5_+j3v_Rna%B~c$l`nABvn|Q6=!wQU3&YsVl!wyrSSxC~&4m}PGr*gh)tE;%_ zx*xLWo;yhyIfCY$t-SNZYpnZlC(h()gsdP|$KP>`E#$m~51?h05%IcJ`rQ?=s9EtU zAO7MJ9CRq=Qyz)oD6%+-={o;zp%Tr+pI1qqXomj0_s2|$h3?j|O^cn?yJ!ohGWO&j zvHy4GOT@{5%yVHbF-5OrG;ad=GCMzpT`57`BBO&C(%!xf&sG-0ZYJL(;%|olp z6BBp@7=|Y{+oGkeowjZ#h2L7pVP{^7)1%QF3K76VsuKpbt>TR*evY~Q6QB!C3rQra z`kSRVCt~Z2oHdtouDF`f6DP&m9;A7D?P`AY(|c&ItiT}?gf*S8DaJ!`?s1(FH*b53 z!%!M=PZsTqoRa{?eg_|nCC;a&?*@jtOQR;Wm7fKMgsbm9wv;s=?8KA0Kc*u>sQ)c+ zg_(Qyt(49ey@Y z+-n#5=Y{`_#)vzg(o`eaRnZtS^i+UxIQTl) z{fs>FQf3}=7CojzO(c;JE~&4Ex9>lXXwy0c2ATCK)OcdPmb@m-%m429XXkV%2;^^R zK}dA4B_{su?o)AC*p4uor4jBo*;*UKHFG|PoN)=B6q(JX)33Qm7jsj$k(GaWf}YiH zqHDd__CU;KR1PdmKJ+jJii!KpB^b2{^!4%Xk|n(KyWc=(2Tt2mzgga6OC;iSDrQx+ z%m0Jm+H3OMYZv}7lqspL+Rjg|y@6Zrx&z;+eA*h@ z`TJv%E2+ojJ6MGu;ot`l=_GsRaU6BZd2~l?oL;Z$&2jhD^8W8{q;tnA!l8f)4}}Vd zgk#fbRMt{6)N|~I#*@^V#7MEb>>pYlSorFtCl`b!zX`7*8^A)Kj=|Na*ZdWveCmN5$V zKllLaRRtD_yq@blGG5b45bJnF7 z;`93G>Fi=eUOp?{d5=eaem{M6J5@`gfLQzsl~0M+hQ`W>1?cjjmNKJ_jg5Ovo_p;= z|3_oYm|>rI_Swul_;3Qk>&w^y$*KnYIBXLw8lirB9k<_j8@JrOn7ralS{rt<}O&poq*AOG+sBt`YdCm-d&L-*%zfBiGxJL?QJ=UZfzFw=?5ieczDRIzQC-~8q` zoPYlL=&~h6G{WlDtGWJ1H?U&)M@l{}ufjQ&1PHbPhC4P6TI|1I#EfY$BRvhLE0whL z9QNC99y1O(n7n<<8H@%=^CshS8dR=c%S%r^&Bo;)qWUEy9WS{ODc`eVg-3>4hU81P zW9A%`8xbI(1?{0n}ds^k8p%O*Y5 z)2H*;V~;Uy+BEeOr8Pku+6%It&_XYIx>%kF)9BcU3E+Fr2Br6axJN zG4RG0dG|nR4ZKoC9*r_<)-3y@k3QOKo$$j7jPJ~!&pvbJDPYLWq{7v32&YWKwJaKH zt69A0W^TXZ7P5=-Xlv?X$&*X?M1bKwnW$C*)&S^Xu>3LA>GY~1Nsp8?MT6iDW70{a zYa7_Rxc1hwd4Jh+tXcjcV<%=XVa!-6wp3HKtxL&&*6f+=KldO$`sgFJY}=+L{79-> zHXw>Ey}-`GRz;a6Cmi=J?z-y^Miq}DCEceSJv$QNH^2G~3l?4r$tj$2*%ciBtrM|z zC!IY5XoiOF(p9)U80;l8CzT{AJ&Ky7H_j$xbKRHf`jUXP#x#`|rgx zh2S-ADO84D_%D}6BxUco7+o`I2+qRI=5 zWEYlhGZ^W|<<;?r1~9F#Dz|pWDi@4>Mo5>oPM`wEnuRZ-2K7O|WJ?VHXM-gbEF(lz6j+@jsI6-O| ztf=x+JeU{^G8k;c3hm^$(++0BU90_OG=`jcdWm7e9xSbRimAqP8z5P3TCl2}SRfZiHljj~tKo_+d z>s?I%!)aPH?x^FAo9^J|n{Od|L=N4ZT`Ya}WmYZQ0ZB)JQKU9|LHaP{cF_bB%9~TK zxeZBe`*FLXgnDXWpdR#Yu)1(){kU|C+?*`(3i8P-DxqlfC`!kV2d{_X^3inncA@KG z91d;w*hXocY;S2*6L~s2I#d)=Q@venc~MP8UCS_+c}`}zqD`pM78B_}nLs;!l*djCD1|NZX>wl>Fxwi(X27c0)D zXbK6d9cp%lJlnE`g=JGxR=&2bX6uaM0K~9sjFM^7?32zpo5^$L?FNPnfVILQ4Bf_M zTI{N+W${gm`O!_ckU1ikzOHUwf9@sLEUN+U5nvR8WGO{k*bX%-!jvkf@Bl;x(2XdT zQ&SUo!;RJ8HgT8(xV13;-d0?qO*HV^0UVkO;l_wcBa>K-2u4vBBTIAfrMk(_&sQ7a zj3^$dl5s<)0!u%P5M|;(3V|lmdScHNs%>D}^cj?tkH(T+52Sm@a1ypmH7ZH+D2DF9 zqZv4Djc{*(s!dy1`j=N(_jeiACSx20=&=!xhDwdepNjO34P$b6R4q$>VNq30-L`SV z35a0_#^{+d?Qfm+J*FIRFhPfdpg2pyV+8zaaEK!srM9|;2XDQL>+e`hc3~m?-Tl1w z%uB5M`*yVSqp__#i1dk}#jxb=6Ea6=U@2?q#?WLyyM^ZVs7d~^A&jB9aJf8aQJqjI ztcb8kG>ql%!I>;fd=Iw21GIpoXd&3F0HtiZL;@m}Brn-HIppN#lAe`GW_FfJ>&t7> zpe>+~+7H8%jP6ar@FwF zy*1D$o#S%1i6@(+c0JOZDcDjm9C3ms(Tkg;q%_=436ld@rXQyf#^tiGq67GQcM<57 zYOw%ToNkt+nF!Vv5>8e}W-=zGwW>V12VZuU%H;YovdJzi#gmdkN=7Ql=_z=9Nu;DE z<4a3d;K@$Urlo!t8$Mmb+7+wVzG*Wd@nsa{D&&{&9kCh{c@{oQT&VPPFM0WeS8lDS zdH8>Xh0lD1Q9)2Sx3RZNHI1<9%Dm}%Y|oVV z{%E5{7jVw^FLo?iv}pJ$ng0XYh6NZ)mo9ZLxap>#0Auu&8J`7)5L{C8s(6e_e*4JJ zSg_!7MwCpXx7DwJQMIxiXX-ITB3X*VNRi^4!5|Jt5Zl{Jf6FS+tC>Etgd>hShQWZJ zo~~}18ynfNeFwYh8ZZN5a-^-x8Dk?I$zU4b$y7sCF}!%35zPK7u)0;`BDEZWu|u&h#{q8ps(vMH~6a|{kr>}Vrh z>{&6;3NVDQ6WyBZq*VHGi*nCf{`OzKbi3gfF-5}yjHMqe&AIrhpR^r!#u=1P-;a=F zSJlnsR7o_KWX@H%TFtL+_#rpmdoQEPC$M4ldR}|_8S1KgL7#@D<)XQ4JPr?r-;K-Z zMo*FI`7IDwM_RVdLEk=vlJTP$GieeTIk^}~US+U_CJu%M(X9xMs7au+gWvr8L8{hl zz#I(Ym6Cl^rsc(Md|zs+O5G2{)AVjvY(Een>Lf6;UFZ*A6E<*&Um=m zw7r(!-t`byUv?E8y`3!m+Y7X9TN}5-B_P{Vdc+&^Kr|hfBMFk-gaeH*uq75*xRc|F zD+6a%Hl<_BnKX4@#!ehdZf*|INPyzPd{SLr?z-t#K7M+2t^VUvPwe;K3Vct`9~NM|y6o*amtMQ@N=jd_cxRkjbhW*bt=jjQy$|Gs_OgI*4Om6#YinP6(XuVJO&7@GiJ}DF;%VVAh@4Jth4W>5Q zF)dnJT4fN40!ZV|Mmk#C80hW8KQKsOV33dutncZJBSLy2#D5TV;RQVdv^-LBr(%&y zRL@lYgc^#X25ZD8vke4PJ%@aCT?#<$kOGJ7zGz9F$QnSi1_=7wAlwxLiRwZL;xueZ zOY@jLXNEO*?%eDJ3l>P*&0aZ%2N)lJaPDQ-T=V-Qjy;yShaO5tcaKUHOFiab-ypp` zUF@u@r()ekRxkZ~ysRKX?wArzJ>w+G#*d}Gxs#nu&D7Ozr?2hf<%;c1KYsjpnG@3c_SN5OW_TIVOFgvBOq(uY|1$uB7)v!HiV3Lc?W9X`w{O5&TjILX?*323~*e1y;WCIt0W_h$$(;lY1~> z3b3@anC6o%W+A{NG7?BQ;(L6HX;a!pXjut^? z(cT?#4HByq7eDBvlTNzx#TQ?^`OAP~Sb*{B-{0PP%?&q9@T3UQlg5BQ$gai)dV9pF z3d6u4INg-)JBfo2KZKmZTvbrp+R{R6bDP?MsD4MSDt^t#%B8rpMCB@S^Ya;5QcQMk zj+zr*y`x$It-Yn0hMHPhcQ#_nXGp`ia`WVlGnAvKVgXH_?r${IIv~puxJ#i zxKjP2I^NWD(lWDfC#R4Pp5O?qY)g+)bb3yhqC z9L7u;s~Aq7mdyQk-pksLmdB$H*+#J(U-n!=b{=6(w*L~pLgF(U&2r!n4I#5Jq)JPQ ziuEwI07uTTV}68f`Z1$@SfL)Z{~OV6u=~J{hqkI@&{E$`4lyYLTEE}_rDXf1-kbo& zvSrJR>XWP&e^tlT^%O`gng#~sJ`@e}Fn?m$oW zC~dN7%NE+(+sVnxW!$)NDxoD)a-{ulaG;;A_D45~B|@WX^bY#zAM9h>maSB5-ooa!>+$buKy6{Ln|&0G5>*KNCq|CKsQ`WE}z22Pd5+@4B*hbMC}xu-tjn7rzv2V zx^NY8Er~kBr!W*CU8+!7=6?FIw0Gi(33l#d&yCIi>)ifu3>1XNabu8IVl9(OE3xM<$hK*h$*eEj}$Iy&0f)zkz7J&@+3aMA>|rRS*fQIw7>#p^V1 z%UI_Aewu4`v30|Gwyobn>#jQ7hE8XDtE#6|9HrriO)wVls5?Bq!=KoDVLsLL8_9ge zW?4kUnUuVnJB^U|77DH&qWCbpV{rSX5Y}>tI+9{V>zWkzMpaFQqy-2E(JVivJwVhF z2eUioSel|~1q7mDB)k@$BmPij2V(wg9D1x3GqF3Q_!yG#`?7_>UkDhdpMHAyqmMpv zO3i{H%|zJvGBTMlYdj4w8A?v}Rpw z#B-8I?OEc>%D|QE#+8(e(`~4M@huIzhz^D+6(1ufAFsnniqoz97gts)O&u-lsMe0r0{(sC!aY%Vosj)kSz@+_J{%!ou?u~dAl$~-noTDc&Jq>4(;$BKak z7>Uk&(HLD_U0+DH3C;6gzqzQW=;7w(=F3%IsLM$-8o`~Oq6EJ0jA@LSxDOe**%X(R z;Y>-PwWE_w+bRf|CTZE3Y8IErlcb!4>QqLwtG$Dkh6Z|N3-X|(>BT{f z14Gq_#6}rNgkncsxZPfYAwPy81-ee~6yi&rh^Yy|rDNFn$Jx+t#+FIJ0 z8dYUkQ)45$>gpB$5@>CU)rtyGB4Kp5fo3OR`V&GG?IICKdaQK77ROt{W+A6!>ajSE zA~yj-ALxO&sTd-~IF;&jxYW%g+DPCafRH}=SSe4OnvZLXgw~ilckY`jR;)PY3t}Q} z4!^*=7cN|wymIBr?H{iAC_grXFph4QP}fpl5O>(T9z3~u+WP^X$f=o zn?;&4nU;n|`u$RP>rvOL*;z-_G}R>H(vp#kEF7WO#qPEa0{wl2{Q;UAn-s8i?%YXr z&2~C=HYh$Lx1b1LYO;Foq)8JgA6=$2mP>P}l0Pv~5&`X~sb=fu_0;Xy#z0p$B!U3R zd8j5M@keA3S7w|d7fFlrD5gRHBz%QLM{%NVs2pTD;{lSPhBzitymFF&LKqlJnc84P z@;R|L3owK=4hE(A<;xvk`U3LtQzlKiXY-bA3uVrjF7&S3rLvs1s&c>je zl*$9%QTkd^VJ2@fHKc3Sgiqs0!-GOp5=qE4pd4oqSt+$JvhasLwCD|Vnq_*c6u>JIo}ic-kGnA7VxCBMFOB{PU>O3Pws>dwMv z_GM^5BhZgc7a|n@AQhnz6!n)^ID1#d1%G5O!E$7!R0^V=){`C#BEJ|fWP}A;P7)S}j*GHWH#_V%xL`taW z=nUCpM;Lns;*dK+7*#bG;%=Aqe&c8TV2x|KRXoZ}7YLlP{r7Z07*D zkQ~e?SFE0OsRzDl<;c2Yv1Dqpb^W`&GJq5n z(tO6CK;FC7=m74A>0s^o`Nu~tCPZ_9Kj+mE!0VN6Ee5~L0Z@P$+E>Alx~Bv6A0(NY)ovrg)*jN!gslTHw`15*AZ{~gwJbkZk)3!&7_cc0{flko( zo8)$r4B#9xLf}^89-W;dTaNA&<{YLdxSX6!8|)|cNv7n3sR~cw-l)o|DyM}U@aEyi zHubet(B)uTUDb&Xp3GOszq31t*exIACQQZ~e*B@<_J%}HjQCvYvR~DleFC%+(p9Oc zU)bV6tAp(G+Sp~ee4+1pv!$+;cee3WKRCCT!56jXe_JI~zT|s)*EBEkyUPq#_kh&=D<#huY?bC= zjuLJ@i`QiNkYV){7!c5Oa_2T4kK`?EFUojWP+R`BKFP&c`HhD^bbS8TV9q8;XF{;- zBx{;QlTTqz&)&Fp9&Qf}(=WF=*9+zw!0s^NrsPopXR6aR;oPBlT^_(r$kYy9`mFuP z_!HXP&})39WECg9-k&LxCpK2uGgkU`F2?o5A(*)%R2gj~(2 z$i1NeacfJTnni0-lV7Y4JS|6K)BXhkwbg@o^&Zp4_vx&1+%f~e)XUsTZa-Dj&6kSJ z(Xa4vd^9@9qG**@Ijs9u_omM;yd|}^7P)dU0=izny2WU9_FL@I0R@9;cjR(1_sson z=&_8yX!59kJ(WKC(DPIIig)RQeU6d;QGw+7wnu4Jqk)L3ma(GQB}E zAh*s<)de2)#|dgFIJEj61Ov(<_gYAIWxO+_8s9LdgoBFo zIhi{WNqysMN^!=>M-@2nrjQ8j ztAhjN$(X5tkpq?Q(mBoIs(@-3hU`ezdv$zNxnBX!MLw~lg;)nkfos5G6)MGz2vmcM zr9(1FTQ+tgO&7nlTU7UjyJfg+@1}Qa;IiBc5Cg+(qm$g2=DtL`+Lm+gdmgcEeMax( z_TSRZRgo471w;1xSD@jYc_da($*Lo2V^EW(H^hvRHF2ES6bFb9sdF*Z^;L}(^xb#s zZr+eRlcOjZrQM_K8c}MAaW>8<2}!K0p@6KSY>BR-C5xgd1N(+}P2(EgC30P~`Wqor zx&C-j>ghG%gOB@L?dxYOP40_zd*m8wwbs_sqrl(MX4Dv_9_0=U=EP%Xt1sr^S2wlq zcwhMSAZ4!AjPb|xEzV~Ldjr=Ngo5dEa;)+Fvs}OO5Wg2KqqHk-)Y+AeXZUZIhuR$- z6*^yRLgRVDRFbm=LDd4Ha6^BTk1{}8*Xy;NPseu?vQgeNoSVzr7+b?*ho;CxoX-XD z<5E|0;{3dlRqdbJ>xh@_l<(fX+7Bx#t{F*?MHMeBEMz-MySsjQ;?UT!{^(MPy?l|W zNYkC(YoW}IL0XXYPPW;fBzEf@~1pbx-ln>pN$d6dF+VzeR zZ)?bKCb+&7F}4*MP+NHU0l193-T%eT_M!qKTPoK^~?szqbwHSoJxHNi<;9roIu@W`G2w9*o||oWs|QO`uEm} z07Lb=L8Dl#@e9ryJzTF-gv+nbs0H=TeBxrQ0{Qi{L8d9cv= z0qBO4b26tjTbpM4vAa~jG0$=@AF`Q__FguK!>_~%XyoorpmdHDG%1hao zYpS@k7C)*j4VsH?J~eqZ`j)|c6TR<6Ye8TRk_zvrrvyLKoLf3s`pHEV;OvoxRZQY7 zp1c`;w0s*;I7_~Dthjpyg$ZlZY?I$(e>B8I`q5Kx^G@~Sr;fz?4<*h=#3diF#>I94 zx6|n<0Mt-DKw_u@_3CV10Ta3=X7jTQ7PRPZO1D5C9(hH8A^z8_8$|NU=VUE+Z8HV% zQyT{dP1BYhhr#Ci7v1G|d`~md9~K$O&ZmN7Tfw3jpn><_Y!EXCIqbe#@!+N>fp#W+ zB7u7zn`(hsKmw-L;=|p)nO#*?qRf*$mwc5}&>PPwsWKC^ASJLbZ3ZstNgQU_cCaNY80d6xue%>|5|1J~S zk|CEy3?9)Mr|JqPB%(tZ4;dpOBIH?8iG+xQuEbaC!%r<|f0ot=h<%YUWRpqq=5GW# zvMYWs-?=1xKX=U!%_)oC&S?MqoGQZ2$rZ)!A;1&j@2x@3NvAt4^hKvc$8O60P6L3Kn2{Ta7)N zBz&Txkh56ol;I zvHh{#1hiQviF=2cN!?$_VZg!O-d>Rfu@|f{=-zmC=tEfQSQv<&ZR&7!G$)Ek(gwIB z4I42qTxJx-NnlIq626X<7Mgax(;8hd5_9}Nl~#QyXpM}aI6~3ID=!)Y9b@fUEyvjZ E0mBdkk^lez literal 0 HcmV?d00001 diff --git a/datas/images/主机_tm.png b/datas/images/主机_tm.png new file mode 100644 index 0000000000000000000000000000000000000000..a2e634a80831ecd8118beeb4bea8bd601e77e2e4 GIT binary patch literal 16883 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 Z$`sQhws1=Hdf-6~44$rjF6*2UngH}d#{&QW literal 0 HcmV?d00001 diff --git a/datas/images/交换机.png b/datas/images/交换机.png new file mode 100644 index 0000000000000000000000000000000000000000..1467d82eb87510ab58acefc0d09706b0c5f1dac0 GIT binary patch literal 5044 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 literal 0 HcmV?d00001 diff --git a/datas/images/交换机_tm.png b/datas/images/交换机_tm.png new file mode 100644 index 0000000000000000000000000000000000000000..e2d78d0041abeaff7b4a649fb3321c212f7139b7 GIT binary patch literal 3666 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 b{Rv2Bx)D8VpZ5Bfxdli|D2P{z8V39aD!h~? literal 0 HcmV?d00001 diff --git a/datas/images/集线器.png b/datas/images/集线器.png new file mode 100644 index 0000000000000000000000000000000000000000..5b3de1a7f4e86d9f930ca622be565086cdd2975c GIT binary patch literal 10309 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 XHxJT{8L#^13xtxDR07qCn*{$Ka8Q%s literal 0 HcmV?d00001 diff --git a/datas/images/集线器_tm.png b/datas/images/集线器_tm.png new file mode 100644 index 0000000000000000000000000000000000000000..cdae40399940fa29b89b503fbd7b768c174d80e2 GIT binary patch literal 9634 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 IA_f8f1L}#DzW@LL literal 0 HcmV?d00001 diff --git a/dbUtil.py b/dbUtil.py new file mode 100644 index 0000000..f2e20ef --- /dev/null +++ b/dbUtil.py @@ -0,0 +1,104 @@ +import sqlite3 +import sys + +import pandas as pd +from pandas import DataFrame + +conn = sqlite3.connect(sys.path[0]+"/network.db") + +def execute_sql(sql): + """ + 执行sql语句 + :param sql: + :return: + """ + cursor = conn.cursor() + cursor.execute(sql) + conn.commit() + + +def search(sql) -> DataFrame: + return pd.read_sql(sql, conn) + + +def delete_obj(obj_id): + cursor = conn.cursor() + delete_obj_sql = f"delete from sim_objs where ObjID='{obj_id}'" + cursor.execute(delete_obj_sql) + delete_conn_sql = f"delete from sim_conn where conn_id in (select conn_id from conn_config where node_id='{obj_id}')" + cursor.execute(delete_conn_sql) + conn.commit() + + +def truncate_db(): + init_database() + +def init_database(): + cursor = conn.cursor() + cursor.execute(""" + DROP TABLE IF EXISTS `conn_config`; + """) + cursor.execute(""" + CREATE TABLE `conn_config` ( + `conn_id` varchar(55) NULL DEFAULT NULL, + `node_id` varchar(55) NULL DEFAULT NULL, + `node_ifs` int(0) NULL DEFAULT NULL, + `ip` varchar(55) NULL DEFAULT NULL, + `mac` varchar(128) NULL DEFAULT NULL, + `conn_port` varchar(32) NULL DEFAULT NULL, + `addr` varchar(255) NULL DEFAULT NULL, + CONSTRAINT `conn_config_sim_conn_conn_id_fk` FOREIGN KEY (`conn_id`) REFERENCES `sim_conn` (`conn_id`) ON DELETE CASCADE ON UPDATE CASCADE +) ; + """) + cursor.execute(""" + DROP TABLE IF EXISTS `mac_table`; + """) + cursor.execute(""" + CREATE TABLE `mac_table` ( + `obj_id` varchar(55) NULL DEFAULT NULL, + `node_ifs` int(0) NULL DEFAULT NULL, + `mac` varchar(55) NULL DEFAULT NULL, + CONSTRAINT `mac_table_sim_objs_ObjID_fk` FOREIGN KEY (`obj_id`) REFERENCES `sim_objs` (`ObjID`) ON DELETE CASCADE ON UPDATE CASCADE +) ; + """) + cursor.execute(""" + DROP TABLE IF EXISTS `router_table`; + """) + cursor.execute(""" + CREATE TABLE `router_table` ( + `obj_id` varchar(55) NULL DEFAULT NULL, + `node_ifs` int(0) NULL DEFAULT NULL, + `segment` varchar(55) NULL DEFAULT NULL, + CONSTRAINT `router_table_sim_objs_ObjID_fk` FOREIGN KEY (`obj_id`) REFERENCES `sim_objs` (`ObjID`) ON DELETE CASCADE ON UPDATE CASCADE +) ; + + """) + cursor.execute(""" + DROP TABLE IF EXISTS `sim_conn`; + """) + cursor.execute(""" + CREATE TABLE `sim_conn` ( + `conn_id` varchar(255) NOT NULL , + `ConfigCorrect` int(0) NULL DEFAULT NULL , + PRIMARY KEY (`conn_id`) +) ; + + """) + cursor.execute(""" + DROP TABLE IF EXISTS `sim_objs`; + """) + cursor.execute(""" + CREATE TABLE `sim_objs` ( + `ObjID` varchar(50) NOT NULL, + `ObjType` int(0) NULL DEFAULT NULL, + `ObjLabel` varchar(20) NULL DEFAULT NULL, + `ObjX` int(0) NULL DEFAULT NULL, + `ObjY` int(0) NULL DEFAULT NULL, + `ConfigCorrect` int(0) NULL DEFAULT NULL, + PRIMARY KEY (`ObjID`) +) ; + """) + conn.commit() + +if __name__ == '__main__': + init_database() diff --git a/document.md b/document.md new file mode 100644 index 0000000..975f337 --- /dev/null +++ b/document.md @@ -0,0 +1,26 @@ +### 在计算机网络中,路由器、交换机、集线器和主机是网络中常见的设备,它们之间扮演不同的角色,并负责网络通信的不同方面。 + +- 路由器(SimRouter):路由器是一个网络设备,用于在不同网络之间进行数据包转发。它通过查看数据包的目标地址,并根据网络中的路由表来确定最佳路径将数据包从源地址发送到目标地址。路由器负责跨越不同的网络,如互联网,将数据包从一个网络转发到另一个网络。 + +- 交换机(SimSwitch):交换机是一个用于连接多个设备的网络设备。它在局域网(LAN)中起到数据包转发和交换的作用。当一个数据包从一个端口进入交换机时,交换机会检查数据包的目标MAC地址,并将其转发到相应的目标端口,以便将数据包传递给正确的目标设备。交换机通过建立MAC地址表来维护设备的连接关系,以便快速转发数据包。 + +- 集线器(SimHub):集线器是一种被动的网络设备,用于将多个设备连接在一起形成局域网(LAN)。当一个数据包到达集线器时,它会被广播到所有连接的设备,无论数据包的目标地址是什么。这会导致网络中的所有设备都会接收到数据包,但只有目标设备会处理该数据包。因此,集线器的性能较低,并且在现代网络中很少使用。 + +- 主机(SimHost):主机是指连接到网络的计算机或其他设备。主机可以是个人电脑、服务器、移动设备等。主机可以通过路由器、交换机或集线器与其他设备进行通信。在网络中,主机可以发送和接收数据包,可以是数据的源或目标。 + +### 关于它们之间的通信方式: +- 路由器在不同网络之间进行通信,通过查找路由表将数据包从一个网络转发到另一个网络。它使用IP地址来寻址和路由数据包。 +交换机在局域网中进行通信,它根据目标设备的MAC地址将数据包转发到正确的端口。交换机在数据链路层操作,使用MAC地址来寻址数据包。 +集线器将所有连接的设备广播到网络中,所有设备都可以接收到发送到网络的数据包。这种广播方式会导致网络中的所有设备都能看到数据包,但只有目标设备会处理它。 +主机可以直接连接到交换机或集线器,并通过它们进行通信。主机使用IP地址和MAC地址来寻址和识别数据 + +#### 补充 + +1. TCP/IP协议栈:TCP/IP协议栈是互联网通信所使用的基本协议集合。它由多个协议组成,其中最常用的是TCP(传输控制协议)和IP(互联网协议)。TCP负责可靠的数据传输,而IP则负责将数据包从源主机传送到目标主机。 +2. 以太网:以太网是一种局域网技术,用于在局域网内传输数据。它使用物理介质(如电缆)来连接多台计算机和网络设备。以太网使用MAC地址(媒体访问控制地址)来唯一标识每个网络接口。 +3. IP地址分配:IP地址是在互联网中用于标识网络设备的唯一地址。IP地址分为IPv4和IPv6两个版本。IPv4由32位二进制数组成,通常以点分十进制表示(例如,192.168.0.1)。IPv6由128位二进制数组成,以冒号分隔的八组十六进制数表示。 +4. MAC地址表:MAC地址表是交换机使用的表格,记录了与交换机连接的设备的MAC地址和对应的接口。当交换机接收到一个数据帧时,它会查找目标MAC地址,并将数据帧只发送到目标设备所连接的接口,而不是广播到所有接口上。 +5. 路由表:路由器使用路由表来确定数据包的最佳路径。路由表中包含了目的网络的IP地址范围和下一跳的路由器的IP地址。路由器将数据包转发到适当的下一跳路由器,直到达到目标网络。 +6. 集线器:集线器是一种物理层设备,用于将多个以太网设备连接在一起。当集线器接收到一个数据帧时,它会将数据帧广播到所有连接的设备,这种广播方式会导致网络拥塞和冲突。 +7. 交换机:交换机是一种数据链路层设备,用于连接多个以太网设备,并根据MAC地址表将数据帧只发送到目标设备所连接的接口。交换机提供了更高的带宽和更低的延迟,因为它只将数据发送到目标设备,而不是广播。 +8. 路由器:路由器是一种网络层设备,用于在不同的网络之间转发数据包。路由器使用路由表决定数据包的下一个跳,并负责在网络之间转发数据。路由器能够实现不同网络之间的互联和广域网的连接。 \ No newline at end of file diff --git a/network.db b/network.db new file mode 100644 index 0000000000000000000000000000000000000000..aeddd48499609fe6399120eab8f4c53ca762740a GIT binary patch literal 40960 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 literal 0 HcmV?d00001 diff --git a/tkTest.py b/tkTest.py new file mode 100644 index 0000000..8cd1853 --- /dev/null +++ b/tkTest.py @@ -0,0 +1,19 @@ +from tkinter import * +import platform + + +def get_platform(): + import platform + sys_platform = platform.platform().lower() + if "windows" in sys_platform: + print("Windows") + elif "macos" in sys_platform: + print("Mac os") + elif "linux" in sys_platform: + print("Linux") + else: + print("其他系统") + + +if __name__ == "__main__": + get_platform()