简介:面向网络测量课程的拓展实验,基于Mininet与Ryu控制器的SDN网络测量项目,提供可直接运行的Python源码、文档说明与使用说明,适合计算机、通信、自动化等相关专业学生完成课程设计、毕业设计或项目演示。压缩包共包含二十三个文件,其中十二个Python脚本为核心,覆盖网络拓扑构建、流量生成、流表信息采集与测量结果处理等环节;另配有六张拓扑与运行效果截图、两个HTML页面,以及README与LICENSE等说明文件,整体约五百四十九KB,轻量易部署。该项目已在实验环境中测试通过后提交,平均评分达九十六分,现有一百六十四人学习使用,可作为高分项目参考。配套文档详细讲解Mininet仿真环境下的Ryu控制器编程和网络测量思路,读者既能直接复现实验,也可基于代码扩展自定义测量指标,例如修改拓扑规模、增加测量项或对接其他南向协议,对理解SDN数据面与控制器交互有切实帮助。
1. 网络测量实验不是只能ping:Mininet+Ryu把测量变成看得见的数据
如果你翻开一门“网络测量”课程的实验大纲,多半是ping、traceroute、查路由表这类老面孔。但真正能拿高分的测量实验,是能拿到网络内部每个端口每秒的字节数、丢包率、队列状态——而基于Mininet和Ryu的SDN网络测量实验,就是把这些“黑匣子”打开给你看。它用Mininet在笔记本里起一个真实运行的多交换机网络,用Ryu控制器接管交换机的转发决策,再用Python写一个测量程序,周期性向控制器要各端口统计,落成CSV、画成曲线。适合网络方向课程设计、毕业设计里想拉高完成度的同学,也适合第一次想跑通SDN控制面加数据面联调的人。下面我会把环境搭建、代码实现、参数调整和踩坑点一次讲完,按我的复现经验来,不绕路。
2. 搭环境先过三关:Mininet与Ryu的版本搭配、安装顺序和一个能跑的拓扑脚本
2.1 Mininet不是eve-ng也不是vnet:为什么课程实验选它
先回答一个很多人问过的问题:网络仿真工具有eve-ng、有vnet那一类图形化平台,为什么偏要用Mininet?因为它们模拟的是“设备的启动过程”,每个节点是一个完整系统,资源开销大、启动慢,不适合做需要反复改拓扑、反复注入流量的测量实验。Mininet的核心思路是进程级仿真——交换机用Open vSwitch进程代替,主机用network namespace隔离,所有节点共享同一个Linux内核。这样一来,一个四交换机环形拓扑加四台主机,几十秒就能拉起来,改一行Python代码就能换拓扑,测量脚本可以直接在控制器侧跑,这正好是网络测量实验要的“快速迭代”。
版本搭配上我踩过不少坑,说一个我目前稳定的组合:Ubuntu 20.04或22.04的系统,Python 3.8到3.10之间,用apt安装Mininet,用pip安装Ryu。Mininet的apt源版本一般是2.3.0,Ryu的PyPI版本是4.34,两者对OpenFlow 1.3的支持很成熟。装完先验证版本,别急着写代码:
sudo apt update sudo apt install -y mininet sudo pip3 install ryu mn --version ryu-manager --version python3 -c "import ryu; print(ryu.__version__)"这里有一个通用问题:如果pip装完在终端里找不到ryu-manager,多半是Python环境变量配置里没有把~/.local/bin或/usr/local/bin加进PATH。我一般用which ryu-manager查路径,查不到就先python3 -m ryu试试,再检查PATH。Mininet装好后mn --version如果提示找不到命令,同样检查/usr/local/bin。
2.2 最小拓扑脚本:先让一台交换机和两台主机跑起来
环境就绪后,不要急着写大网络。我习惯先用最小拓扑验证“控制器-交换机”链路是否通。常见做法是写一个Python脚本,用Mininet的API创建拓扑,并把控制器的连接指向本地已经启动的Ryu:
from mininet.net import Mininet from mininet.node import RemoteController from mininet.topo import SingleSwitchTopo from mininet.cli import CLI # 创建单交换机拓扑,k=2表示带2台主机 topo = SingleSwitchTopo(k=2) # 不启动Mininet自带控制器,改用外部的Ryu net = Mininet(topo=topo, controller=None) net.addController('c0', controller=RemoteController, ip='127.0.0.1', port=6633) net.start() CLI(net) net.stop()这段代码里最关键的是controller=None和RemoteController。如果不写这两行,Mininet会自动启动一个内置控制器去接管交换机,Ryu那边永远等不到交换机连接,你会看到控制器日志一片空白,这是新手最容易翻车的地方。ip='127.0.0.1'表示控制器在本机,port=6633是Ryu默认监听端口,两者要严格对应。
启动顺序也重要:先在一个终端跑ryu-manager,再在另一个终端跑这个拓扑脚本。等Mininet进入mininet>提示符后,先执行pingall,看到两台主机互通,再用net命令确认交换机确实连上了远程控制器。
2.3 再做环形拓扑:测量实验需要的不只是一条直路
单交换机拓扑只能测“一台设备两个端口”,体现不出SDN多路径测量的价值。我一般会把实验网络升级成环形:四台交换机首尾相连,每台交换机挂一台主机。这样流量可以走不同路径,端口统计里能看到转发、收包、丢弃等更完整的指标,报告也好写。写法是自定义一个Topo子类:
from mininet.topo import Topo class RingTopo(Topo): """4台交换机组成环形,每台交换机下面挂1台主机""" def build(self, n=4): switches = [] for i in range(n): sw = self.addSwitch('s%d' % (i + 1), stp=True) switches.append(sw) # 交换机之间连成环 for i in range(n): self.addLink(switches[i], switches[(i + 1) % n]) # 每台交换机挂一台主机 for i in range(n): host = self.addHost('h%d' % (i + 1)) self.addLink(host, switches[i]) topos = {'ring': RingTopo}注意两个细节:addSwitch里的stp=True要加上,因为环形拓扑在逻辑上存在环路,不启用生成树协议的话广播帧会无限循环,后面打流时端口统计会乱跳;self.addLink(switches[i], switches[(i + 1) % n])用取模把最后一台交换机接回第一台,这就是“环”的闭合方式。保存为ring_topo.py,可以把它直接作为Mininet的自定义拓扑文件:
sudo mn --custom ~/net_exp/ring_topo.py --topo ring --controller remote,ip=127.0.0.1,port=6633到这一步,你的SDN实验网络已经具备“可编程数据面”的基础,下一步就是把测量程序写进Ryu控制器。
3. 用Ryu写测量控制器:端口统计请求、CSV落盘与REST查询的完整Python实现
3.1 控制器的数据面接口:Ryu如何拿到端口计数器
Ryu和交换机之间走OpenFlow协议,交换机侧每个端口都维护着一组硬计数寄存器,包括收字节数、发字节数、收包数、发包数、丢弃数。控制器想知道这些数值,只需要给交换机发一个端口统计请求(OFPPortStatsRequest),交换机会回一个应答消息(OFPPortStatsReply)。这比传统SNMP轮询更轻量,也天然适合测定向网络的真实负载——因为统计项是交换机硬件维护的,不是控制器在用户态估算的。
下面这段Python实现了一个最简单的端口监视器。它监听交换机的上下线事件,连接建立后每5秒请求一次所有端口统计,并把结果写入CSV:
from ryu.base import app_manager from ryu.controller import ofp_event from ryu.controller.handler import MAIN_DISPATCHER, DEAD_DISPATCHER, set_ev_cls from ryu.ofproto import ofproto_v1_3 from ryu.lib import hub import csv import time class PortMonitor(app_manager.RyuApp): OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.datapaths = {} # dpid -> datapath self.interval = 5 # 采样周期,单位秒 self.f = open('port_stats.csv', 'w', newline='') self.w = csv.writer(self.f) self.w.writerow(['ts', 'dpid', 'port_no', 'rx_bytes', 'tx_bytes', 'rx_packets', 'tx_packets']) @set_ev_cls(ofp_event.EventOFPStateChange, [MAIN_DISPATCHER, DEAD_DISPATCHER]) def _state_change_handler(self, ev): dp = ev.datapath if ev.state == MAIN_DISPATCHER: self.datapaths[dp.id] = dp elif ev.state == DEAD_DISPATCHER: self.datapaths.pop(dp.id, None) # 第一次有交换机连上来时,启动后台监控循环 if self.datapaths and not hasattr(self, '_loop_started'): self._loop_started = True hub.spawn(self._monitor_loop) def _monitor_loop(self): while True: for dp in list(self.datapaths.values()): self._send_port_request(dp) hub.sleep(self.interval) def _send_port_request(self, dp): parser = dp.ofproto_parser ofproto = dp.ofproto # 请求所有端口统计,port_no传OFPP_ANY req = parser.OFPPortStatsRequest(dp, 0, ofproto.OFPP_ANY) dp.send_msg(req) @set_ev_cls(ofp_event.EventOFPPortStatsReply, MAIN_DISPATCHER) def _port_stats_reply_handler(self, ev): dp = ev.msg.datapath now = time.time() for p in ev.msg.body: row = [now, dp.id, p.port_no, p.rx_bytes, p.tx_bytes, p.rx_packets, p.tx_packets] self.w.writerow(row) self.f.flush()代码的逻辑分三段:上线状态维护、定时循环、应答处理。EventOFPStateChange是Ryu统一上报的交换机状态切换事件,MAIN_DISPATCHER表示交换机已经完成OpenFlow握手,可以发请求;DEAD_DISPATCHER表示断开。hub.spawn是Ryu的协程工具,它替代threading.Thread是因为协程共享事件循环,不会出现锁竞争。这里有一个容易被忽略的细节:循环里必须用hub.sleep(self.interval)不能用time.sleep——后者会阻塞整个Ryu事件循环,交换机回包进来时控制器已经睡死了。
_send_port_request里的三个参数分别是datapath、flags、port_no,OFPP_ANY表示“所有端口”,实际使用时也可以换成具体端口号,比如只测1号口。应答消息ev.msg.body是一个列表,里面每个元素对应一个端口,字段就是我们在CSV表头里写的那些。注意写入后要flush,否则控制器进程异常退出时缓冲区里的数据会丢。
3.2 跑起来看数据:启动命令和REST查询接口
写好的代码保存为port_monitor.py,启动命令是:
sudo ryu-manager port_monitor.py启动后你会看到Ryu打印出加载的应用列表,接着Mininet的交换机连上来时,终端会滚动端口统计的行数据。这时CSV文件已经在写入了,Ctrl+C停掉控制器后文件内容还在。如果只想快速验证测量链路,不写CSV,可以额外加载Ryu自带的ofctl_rest应用,走REST查询同一份数据:
sudo ryu-manager port_monitor.py ryu.app.ofctl_rest curl -s http://127.0.0.1:8080/stats/port/1/stats/port/1里的1是交换机的dpid,圆括号里的返回值就是端口统计的JSON格式。这个接口方便你在实验中实时看数据,不必每次跑到终端里翻日志。注意如果你改过交换机编号,dpid要跟着变,比如环形拓扑里四台交换机的dpid是1、2、3、4。
到这里,测量数据已经能落盘了。但落盘的数据只是一堆“累计值”,要变成能写进报告里的速率曲线,还得做流量注入和差值换算,这两个坑在下一章和第5章会集中讲。
4. Mininet与Ryu联调避坑:五个真实翻车点和排查清单
4.1 连不上控制器:Mininet启动后ping不通
现象:Ryu控制器已经启动,Mininet也进入mininet>提示符,但pingall全军覆没,控制器日志里没有任何交换机的连接记录。
原因:百分之八十是启动Mininet时没有指定RemoteController,Mininet自动拉起了一个内置控制器接管了交换机,数据和控制器根本不走Ryu。另一个高频原因是端口不一致,Ryu默认监听6633,Mininet命令行里却写了--controller remote,port=6653,两边对不上。
解决:先确认Mininet里执行net命令,看交换机标出的controller是不是c0。再用ss -lnt | grep 6633确认Ryu在监听。启动命令统一写成--controller remote,ip=127.0.0.1,port=6633,多个字段之间用英文逗号,不要有空格。
4.2 版本玄学:Ryu跑起来却报ofproto参数错误
现象:控制器加载正常,交换机一连上,终端立刻抛TypeError: __init__() got an unexpected keyword argument 'datapath'或者OFPPortStatsRequest构造失败。
原因:Ryu的API在不同大版本间有变动,尤其是OpenFlow协议支持版本。旧代码可能用了ofproto_v1_3_parser找解析器,或在新版Ryu里OFPPortStatsRequest的参数顺序变了。这个问题看起来玄学,其实就是代码和库版本不匹配。
解决:保证OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION]明确声明用1.3;构造请求前打印dp.ofproto_parser,确认它是ofproto_v1_3_parser。如果报的是参数类型错误,去/usr/local/lib/python3.8/dist-packages/ryu/ofproto/ofproto_v1_3_parser.py里看当前版本的类定义签名,按实际签名改,不要照抄网上老帖。
4.3 数据抖动:周期太短和STP未开是两个隐藏坑
现象:测量出的端口速率忽高忽低,前一秒接近打流带宽,下一秒直接变成0,两条采样点之间跳变得像毛刺。
原因:周期太短是最常见原因。interval = 1时,OpenFlow端口统计是交换机底层聚合值,两次采样之间只有1秒的窗口,iperf的TCP窗口调整、调度器抖动都会直接放大成速率毛刺。另一个原因更隐蔽:环形拓扑没开STP,广播包在环上无限转发,交换机端口被广播流量占满,统计值全体虚高。
解决:我一般把interval设置成5秒或10秒,每秒采样适合看事件,不适合写速率报告。环形拓扑所有交换机stp=True,并用dpctl dump-ports查看端口计数是否收敛。采样后做一次滑动平均也能平滑曲线,但根子上要先把周期和拓扑调对。
4.4 控制器卡死:日志刷屏与监视循环写错
现象:打流几分钟后Ryu进程CPU冲到100%,终端输出每秒几十行,CSV写入变慢,最后Mininet里ping都超时。
原因:多数是周期循环里用了time.sleep而不是hub.sleep,阻塞了Ryu的协程调度,导致回包处理任务堆积;还有人在_port_stats_reply_handler里同时写屏幕和写CSV,每条端口统计都打一整行日志,量一大IO就成瓶颈。
解决:把time.sleep替换成hub.sleep;正式实验时把self.logger.info那行注释掉或者降成debug级别;CSV写入每5秒批量写一次而不是每条都flush。控制器是测量系统的事实标准,先把观测端做轻,才有余量扛流量。
4.5 写CSV翻车:Mininet在VM里文件写到共享目录
现象:CSV文件创建了但内容是空的,或者写到一半抛OSError: [Errno 28] No space left on device。
原因:一个隐蔽场景是我在VMware虚拟机里跑Mininet,把工作目录挂到了共享文件夹。控制器进程的高频小文件写入,在共享磁盘上有IO延迟和缓存冲突,数据落不到盘。另一个原因是直接open('port_stats.csv', 'w')没写绝对路径,控制器启动目录不对,文件生成在别处。
解决:代码里写绝对路径,比如/home/yourname/net_exp/port_stats.csv;虚拟机实验就把CSV写到本地磁盘路径,不要写到共享目录。每次实验前先删除旧CSV再启动控制器,避免把老数据混进新分析。
5. 流量注入与结果校准:iperf打流、速率换算和ifstat对账
5.1 造流量:iperf的三个必调参数
测量程序就绪后,空网络里所有端口速率都是0,看不出测量效果,得造真实流量。Mininet里我习惯用iperf做TCP/UDP打流,因为它起服务端和客户端都只要一行命令,还自带周期性报告:
mininet> h1 iperf -s -i 1 & mininet> h2 iperf -c 10.0.0.1 -t 30 -i 5 -b 20m-s是服务端模式,-i 1让服务端每秒打印一次连接状态;-c指定客户端连接目标(h1的IP是10.0.0.1),-t 30表示打流30秒,-i 5让客户端每5秒输出一次独立的速率报告,-b 20m限速20Mbps。这里三个参数对应三种实验需求:-t决定测量窗口多长,-i决定客户端报告的粒度,-b如果不写iperf会以TCP拥塞窗口全力抢带宽,对端口统计冲击很大。实际做课程实验我一般-t给60秒、-i给5秒,这样能和控制器端的5秒采样对齐,分析时一一对应。
5.2 把CSV变成速率曲线:横坐标别让时间挤成一坨
CSV里存的rx_bytes是累计值,画图前要换算成速率:相邻两次采样字节数差除以采样间隔。下面是我常用的处理脚本,顺手解决了画图时横坐标太密集的问题:
import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv('port_stats.csv') # 按交换机+端口分组,用diff计算两次采样区间内的字节增量 df['rx_rate'] = (df.groupby(['dpid', 'port_no'])['rx_bytes'] .diff() / 5.0) df['tx_rate'] = (df.groupby(['dpid', 'port_no'])['tx_bytes'] .diff() / 5.0) # 时间戳转成可读时间,并设为索引 df['dt'] = pd.to_datetime(df['ts'], unit='s') df = df.set_index('dt') # 画h1所在端口(dpid=1, port_no=1)的接收速率 one_port = df[(df['dpid'] == 1) & (df['port_no'] == 1)] fig, ax = plt.subplots(figsize=(10, 4)) ax.plot(one_port.index, one_port['rx_rate'] / 1e6, label='rx_rate(Mbps)') ax.plot(one_port.index, one_port['tx_rate'] / 1e6, label='tx_rate(Mbps)') ax.set_xlabel('time') ax.set_ylabel('Mbps') ax.legend() # 时间轴标签自动旋转,避免叠成一团 plt.gcf().autofmt_xdate() plt.tight_layout() plt.savefig('port_rate.png', dpi=150)groupby(['dpid', 'port_no'])这一步不能省,因为控制器收的是所有交换机的所有端口统计,必须按端口分组做差值,否则不同端口的字节数会互相减出负数。diff() / 5.0里的5.0要和控制器里self.interval一致,如果你的采样周期改成了10秒,这里也要同步改,对不上的话速率会被整体放大或缩小一倍。autofmt_xdate()是画时间序列时的一个实用小技巧,它会自动把横轴日期标签倾斜45度,招式比手动xticks(rotation=45)省事得多。
5.3 校准数据:和ifstat对账,测量结果才敢写进报告
测量数据自己跟自己一致还不够,要和系统层面的真实计数对账。Mininet里的主机是网络命名空间里的真实进程,可以用ifstat直接读主机的网卡计数。在Mininet CLI里执行:
mininet> h1 ifstat -i h1-eth0 1 5这会每1秒显示一次h1网卡的收发包和字节速率。跑5秒后,把ifstat显示的平均速率和port_rate.png里对应时段的速率对比,误差在几个百分点以内是正常的——因为是两个进程分别采样,存在天然时间差。如果相差几十倍,基本是换算公式写错了,优先检查diff的除数是几秒。
还有一个校准方式是从Open vSwitch侧取证。在Mininet里的交换机会创建对应的虚拟网桥,执行:
sudo ovs-ofctl dump-ports s1能直接看到s1每个端口的rx/tx字节累计值。把这个值和Ryu收到的最后一条CSV记录对比,一致就说明控制面测量的数据可信。这一条我在写实验报告时一定会截图,因为它是“SDN控制面测量是可信的”最直观证据。
6. 从跑通到高分:三个进阶方向和一套答辩汇报基本功
6.1 方向一:拓扑里加入带宽和时延,测出“有瓶颈”的网络
平面的环形拓扑每个链路都一样,测量结果没有层次感。给目标链路加上带宽和时延,就能在报告里讲“瓶颈链路识别”:
self.addLink(switches[0], switches[1], bw=10, delay='5ms') self.addLink(switches[1], switches[2], bw=100, delay='1ms')bw的单位是Mbps,delay是毫秒级模拟时延。这样第5章的速率曲线会出现明显瓶颈——一条链路被占满,另一条链路却闲置。这比“四条链路速率一样”更能体现网络测量的价值。
6.2 方向二:把5秒周期改成动态周期,侧写控制器开销
把self.interval从固定数改成按负载切换:当端口速率变化幅度超过阈值时缩短到2秒,平稳时拉长到15秒。这能引出SDN控制面“测量频率和控制器开销”的权衡讨论,答辩时是一个很好的提问点。需要用两行代码记录最近一次测量的速率变化率,具体实现不复杂,我建议实验报告里一定要写这一段的取舍理由。
6.3 文档和使用说明怎么组织
高分实验项目一般会要求源码之外附文档说明和使用说明。文档说明写测量原理、字段设计、模块划分;使用说明写启动步骤、依赖版本、复现命令。我的习惯是把启动命令固化成README代码块,同时也写清楚“先启动控制器、再启动Mininet、再打流”的顺序——这是我自己踩过坑后最后留下的流程。我第一次做这个实验时,连控制器都没接上,卡了三个小时,最后发现是Mininet默认启用了自带控制器,后来我把这套流程沉淀成了固定套路,先起控制器、再起拓扑、先ping通、再打流、最后采集。希望帮到你。
本文还有配套的精品资源,点击获取