简介:这份资源面向计算机网络课程学习者与实验实践者,围绕基于Python的网络拓扑实验展开,重点解决传输机制实验中拓扑运行与文件收发功能的实现问题。包内共96个文件,以C源码与头文件为主体(28个.c、40个.h),辅以14个Shell脚本、4个Python脚本及Makefile、数据文件、实验报告PDF、演示PPT与说明文档等,压缩包约2.65MB,结构上按实验阶段与代码版本分目录组织,便于对照查阅。实验内容涵盖运行给定网络拓扑tcp_topo.py,以及修改tcp_apps.c与tcp_stack.py实现文件收发,配套报告与讲义可辅助理解协议栈设计思路。目前已有172人学习,适合需要完成课程设计、理解TCP传输机制与网络拓扑搭建的读者参考,可从中获取可运行的实验代码、分阶段实现思路与调试排错线索。
1. 用 Python 把网络拓扑跑起来:从邻接矩阵到可视化,一套能复现的最小实验
手里只有一台笔记本、一个 Python 环境,能不能把「网络拓扑」这件事从抽象概念变成看得见、算得动的东西?能。我做过很多次这类实验,核心就三件事:把拓扑抽象成图、把图存成矩阵或邻接表、再把结果画出来或者算指标。标题里的「基于 Python 实现网络拓扑的实验」,本质就是用 Python 的图论工具链,把节点和链路的关系建模、计算、可视化,最后验证拓扑特性(连通性、中心度、最短路径、鲁棒性)。它适合两类人:一类是刚学完 Python 基础、想找个能落地的练手项目的新手;另一类是做运维、网络规划、甚至量化交易里研究关联网络的从业者,需要快速验证一个拓扑假设。下面这套流程,我在自己机器上反复跑过,环境干净、依赖少、每一步都能单独验证。
2. 拓扑建模先想清楚:节点、边、有向还是无向
2.1 网络拓扑在代码里到底长什么样
网络拓扑在数学上就是一个图 G=(V,E),V 是节点集合,E 是边集合。落到 Python 里,最常用的两种表示是邻接矩阵和邻接表。邻接矩阵是一个 N×N 的二维数组,matrix[i][j]=1表示 i 和 j 之间有链路;邻接表则是每个节点维护一个邻居列表。选哪个取决于你的拓扑规模和后续要算什么:节点数在几百以内、要做矩阵运算(比如谱聚类、特征值分析),用邻接矩阵;节点上千、边稀疏,用邻接表省内存。我一般先用邻接矩阵把逻辑跑通,因为直观、好调试,等规模上来了再换networkx的图对象。
这里有个容易翻车的点:无向图的邻接矩阵必须对称,matrix[i][j]和matrix[j][i]要同时置 1。我见过有人只填了上三角,结果算出来的度数是实际的一半,排查了半天。有向图则不用对称,但要注意入度和出度的区别。另外,带权拓扑(比如链路延迟、带宽)就把 1 换成具体权重,0 表示不通。下面这段代码用纯 Python 列表构造一个 6 节点的无向拓扑,不依赖任何第三方库,方便你先验证逻辑。
# 6 节点无向拓扑的邻接矩阵构造 N = 6 # 初始化全 0 矩阵 adj = [[0] * N for _ in range(N)] # 定义链路:(起点, 终点),无向图两个方向都要置 1 edges = [(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (4, 5)] for u, v in edges: adj[u][v] = 1 adj[v][u] = 1 # 无向图必须对称,漏掉这行度数会算错 # 打印矩阵,方便肉眼检查 for row in adj: print(row)逻辑说明:adj是 N×N 的嵌套列表,edges里每条链路都双向赋值,保证对称。参数说明:N是节点总数,改拓扑时同步改edges即可;如果是有向图,删掉adj[v][u] = 1那一行。跑完你应该看到一个对角线为 0、其余对称的 0/1 矩阵。这一步别急着上可视化,先把矩阵打印出来核对,比后面画图发现错了再回头找要省事得多。
2.2 用 networkx 建图:比手写矩阵省一半代码
纯列表适合理解原理,但真要做实验,networkx是绕不开的。它把节点、边、属性都封装好了,还能直接调最短路径、中心度、连通分量这些算法。安装就一行pip install networkx,如果你还要画图,再装matplotlib。我一般会同时装这两个,画图那步用得上。下面把上面的拓扑用networkx重建,并顺手算几个基础指标。
import networkx as nx G = nx.Graph() # 无向图,有向图用 nx.DiGraph() edges = [(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (4, 5)] G.add_edges_from(edges) print("节点数:", G.number_of_nodes()) print("边数:", G.number_of_edges()) print("各节点度数:", dict(G.degree())) print("是否连通:", nx.is_connected(G)) print("平均最短路径长度:", nx.average_shortest_path_length(G))逻辑说明:add_edges_from一次性把边列表灌进去,节点会自动创建。G.degree()返回每个节点的度,is_connected判断整个图是否连通,average_shortest_path_length算平均跳数。参数说明:nx.Graph()换成nx.DiGraph()就是有向图,换成nx.MultiGraph()支持两点间多条链路。这几个指标是拓扑实验的「体检报告」,连通性为 False 说明有孤立子图,平均路径长度能反映拓扑的紧凑程度。跑通这步,你就有了一个可计算、可扩展的拓扑对象,后面所有实验都基于它。
3. 从矩阵到图:可视化与指标计算的完整链路
3.1 用 matplotlib 把拓扑画出来,节点位置怎么定
拓扑画出来才直观,但节点位置(layout)是新手最容易卡住的地方。networkx内置了好几种布局:spring_layout模拟弹簧斥力,适合一般拓扑;circular_layout把节点排成圆,适合看环形结构;kamada_kawai_layout基于距离,画出来的图比较规整。我一般先用spring_layout,因为它对大多数拓扑都能给出可读的结果。下面这段代码把图存成 PNG,注意matplotlib在无界面环境(比如服务器)要指定后端,否则会报错。
import matplotlib matplotlib.use("Agg") # 无界面环境必须加,否则 plt.show() 会崩 import matplotlib.pyplot as plt import networkx as nx G = nx.Graph() G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (4, 5)]) pos = nx.spring_layout(G, seed=42) # seed 固定,保证每次布局一致 nx.draw_networkx_nodes(G, pos, node_color="lightblue", node_size=500) nx.draw_networkx_edges(G, pos, width=1.5) nx.draw_networkx_labels(G, pos, font_size=12) plt.axis("off") plt.savefig("topology.png", dpi=150, bbox_inches="tight") print("已保存 topology.png")逻辑说明:spring_layout返回每个节点的 (x, y) 坐标字典,seed固定后每次运行布局相同,方便对比不同拓扑。draw_networkx_nodes/edges/labels分别画节点、边、标签。参数说明:node_size控制节点大小,dpi控制输出清晰度,bbox_inches="tight"去掉多余白边。如果你在 Jupyter 里跑,把matplotlib.use("Agg")去掉、savefig换成plt.show()即可。画完打开图片,先看有没有孤立节点、有没有明显画错的边,这一步能帮你抓出建模阶段的低级错误。
3.2 中心度与最短路径:拓扑实验里最常算的两个指标
拓扑实验不只是画图,更重要的是算指标。最常用的两个是中心度和最短路径。中心度衡量节点的重要性,degree_centrality看连接数,betweenness_centrality看节点在多少条最短路径上(中介性),closeness_centrality看节点到其他节点的平均距离。最短路径则用shortest_path和shortest_path_length。下面这段代码把三个中心度都算出来,并找出节点 0 到 5 的最短路径。
import networkx as nx G = nx.Graph() G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (4, 5)]) deg_c = nx.degree_centrality(G) btw_c = nx.betweenness_centrality(G) clo_c = nx.closeness_centrality(G) for n in G.nodes(): print(f"节点{n}: 度中心度={deg_c[n]:.3f}, " f"中介中心度={btw_c[n]:.3f}, 接近中心度={clo_c[n]:.3f}") path = nx.shortest_path(G, source=0, target=5) length = nx.shortest_path_length(G, source=0, target=5) print("0 到 5 的最短路径:", path, "长度:", length)逻辑说明:三个中心度函数都返回字典,键是节点、值是分数。shortest_path返回节点序列,shortest_path_length返回跳数。参数说明:betweenness_centrality可以加weight参数支持带权图,shortest_path默认按跳数算,带权图要传weight="weight"。跑完你会发现节点 3 的中介中心度最高,因为它连接了左右两个子图,是拓扑里的「咽喉」。这个结论在真实网络里很有用——咽喉节点一旦故障,整个网络可能分裂。我一般会把中心度排序后取 Top 3,作为重点监控对象。
3.3 把邻接矩阵和 networkx 互相转换
实际项目里经常需要在两种表示之间切换:比如从数据库读出来的是矩阵,但算法要用networkx;或者用networkx算完要导出成矩阵给别的模块。转换本身不难,但有几个细节容易出错。下面这段代码演示双向转换,并验证转换前后一致。
import networkx as nx import numpy as np # networkx -> 邻接矩阵 G = nx.Graph() G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (4, 5)]) mat = nx.to_numpy_array(G) print("矩阵形状:", mat.shape) print(mat) # 邻接矩阵 -> networkx G2 = nx.from_numpy_array(mat) print("还原后边数:", G2.number_of_edges()) print("与原图是否同构:", nx.is_isomorphic(G, G2))逻辑说明:to_numpy_array把图转成 numpy 矩阵,from_numpy_array反向还原。is_isomorphic验证两个图结构是否一致。参数说明:to_numpy_array默认按节点标签排序,如果节点标签不是 0 到 N-1 的整数,要先G = nx.convert_node_labels_to_integers(G)再转,否则矩阵行列顺序会对不上。这个坑我踩过——节点标签是字符串时直接转,矩阵看着没问题,但和原始节点对应关系全乱了。转换完一定要用is_isomorphic验一下,这是最省事的后悔药。
4. 避坑与排查:拓扑实验里最容易翻车的 5 个地方
4.1 邻接矩阵不对称导致度数算错
现象:算出来的节点度数总是实际的一半,或者连通性判断为 False。原因:无向图只填了上三角,adj[i][j]=1但adj[j][i]还是 0。解决:构造时强制双向赋值,或者构造完加一句adj = adj + adj.T(numpy 矩阵)再检查对角线是否为 0。我现在的习惯是写完矩阵先打印出来,肉眼扫一遍对称性,比后面算法报错再回头找快得多。
4.2 matplotlib 在服务器上报 no display 错误
现象:本地跑得好好的画图代码,放到服务器或 Docker 里就报no display name and no $DISPLAY environment variable。原因:matplotlib默认用交互式后端,无界面环境没有显示设备。解决:在import matplotlib.pyplot之前加matplotlib.use("Agg"),强制用非交互后端。注意这行必须在pyplot导入之前,顺序反了不生效。如果还要在 Jupyter 里显示,就只在服务器脚本里加,别写进公共模块。
4.3 节点标签不是整数时矩阵行列错位
现象:用to_numpy_array转出来的矩阵,和原始节点对应不上,算出来的中心度张冠李戴。原因:to_numpy_array默认按节点标签排序,字符串标签排序结果和插入顺序不一致。解决:转换前先G = nx.convert_node_labels_to_integers(G, first_label=0),把标签统一成 0 到 N-1,转换后再用映射表还原。或者直接用nodelist参数指定顺序:nx.to_numpy_array(G, nodelist=sorted(G.nodes()))。
4.4 spring_layout 每次画出来不一样
现象:同一个拓扑,每次运行画出来的节点位置都不同,没法做对比图。原因:spring_layout初始位置是随机的。解决:加seed参数固定随机种子,比如nx.spring_layout(G, seed=42)。这样每次布局一致,方便把不同拓扑的画图结果并排对比。如果要做论文级别的图,还可以先算好pos存成 JSON,画图时直接读,彻底消除随机性。
4.5 大规模拓扑用纯 Python 列表内存爆炸
现象:节点数上千后,邻接矩阵占用内存飙升,程序卡死。原因:N×N 矩阵的内存是 O(N²),10000 个节点就是 1 亿个元素。解决:节点数超过 1000 且边稀疏时,改用邻接表或直接上networkx的图对象(内部用字典存储,内存是 O(N+E))。如果必须用矩阵,考虑scipy.sparse的稀疏矩阵,只存非零元素。我一般以 500 个节点为分界线,超过就换稀疏表示。
5. 进阶技巧:用拓扑鲁棒性实验验证你的网络设计
前面都是基础操作,真正能体现拓扑实验价值的是鲁棒性分析——模拟节点或边失效,看网络会不会分裂。这个技巧在运维容量规划、网络架构评审里特别实用。做法很简单:依次移除每个节点,重新算连通分量数量和最大连通子图的大小,下降越快说明拓扑越脆弱。下面这段代码对 6 节点拓扑做节点移除实验,输出每个节点失效后的最大连通子图占比。
import networkx as nx G = nx.Graph() G.add_edges_from([(0, 1), (0, 2), (1, 3), (2, 3), (3, 4), (4, 5)]) N = G.number_of_nodes() print("节点移除鲁棒性分析:") for node in list(G.nodes()): H = G.copy() H.remove_node(node) if H.number_of_nodes() == 0: print(f"移除节点{node}: 网络完全消失") continue largest = max(nx.connected_components(H), key=len) ratio = len(largest) / N print(f"移除节点{node}: 最大连通子图占比={ratio:.2f}, " f"连通分量数={nx.number_connected_components(H)}")逻辑说明:每次复制原图、移除一个节点,然后算最大连通子图占原节点总数的比例。占比越低,说明该节点越关键。参数说明:connected_components返回连通分量的节点集合,max(..., key=len)取最大的那个。跑完你会看到移除节点 3 后占比明显下降,因为它连接左右两个子图。这个实验可以扩展到边移除、随机失效、按中心度排序失效等多种策略。我一般会把结果画成折线图,横轴是移除节点数,纵轴是最大连通子图占比,曲线下降越陡说明拓扑越脆弱。
这个技巧的价值在于:它把「拓扑设计好不好」从主观判断变成了可量化对比。你可以设计两套拓扑方案,分别跑鲁棒性实验,看哪套在关键节点失效后仍能保持连通。我自己的习惯是,任何拓扑方案上线前都跑一遍这个实验,把最脆弱的三个节点标出来,作为冗余设计的重点。这套流程不依赖任何外部服务,纯 Python 加networkx就能跑,适合反复迭代。希望帮到你。
本文还有配套的精品资源,点击获取