简介这是一套面向网络测量课程拓展实验的SDN网络测量项目基于Mininet仿真平台与Ryu控制器开发适合计算机、网络工程、通信等专业的学生用于课程设计、综合实验或毕设初期演示。项目围绕网络测量场景集成了拓扑构建、Ryu控制逻辑、流量生成与测量结果可视化等Python代码可帮助读者在虚拟环境中快速搭建实验并验证SDN测量思路。压缩包共23个文件包含12个Python源码、6张拓扑及效果示意图、2个HTML说明页、README文档与许可证文件整体仅549KB结构精简便于按需查看。目前已有164人学习使用项目代码经测试运行成功完整度较高。除源码外还附带使用说明与文档并包含拓扑示意图与HTML说明页能帮助读者快速理解项目结构与运行方式现有代码均基于实际运行验证适合二次开发也可作为课程报告或毕设演示素材。1. 这个基于Mininet和Ryu的SDN测量实验先搞清楚值不值得做一句话说清楚这不是一个让你改改拓扑、跑通ping就交差的课程实验而是一条把「虚拟网络」和「真实测量逻辑」串起来的完整链路。你在Mininet里用Python脚本搭出SDN拓扑让Ryu控制器接管交换机再从控制器的北向接口或OpenFlow消息里把端口流量、流表命中数据按时拉出来换算成速率、丢包和字节数。很多网络测量课程只讲到tcpdump和iperf这个实验补的是「控制面视角下的测量」也就是让你看到交换机自己眼里的流量长什么样。这个标题适合三类人。第一类是网络工程或计算机专业的本科生正在为「拓展实验」找能写进报告、能现场演示的题目第二类是自学者已经装过Mininet但只会敲两行命令想往SDN控制面走一步第三类是刚入行的开发想找一个Python技术栈齐全、能改源码当作品集的小项目。它解决的核心问题是在网络模拟器里如何用控制器提供的统计接口做测量而不是靠抓包去猜。如果你只是想要一份能交差的源码那直接跑通默认脚本就够了如果你想让实验报告里出现「控制器统计值 vs 实际吞吐量」的对比图和误差分析这篇文章就是按这个目标写的。所有命令和源码都是我在Ubuntu环境里实际用过的路径按顺序执行即可复现。2. 搭出可测的SDN环境Mininet拓扑与Ryu控制器的选定2.1 为什么是MininetRyu而不是OVSP4选型逻辑课程实验最常见的替代方案是直接用Open vSwitch写流表再用ovs-ofctl dump-port-stats来读端口统计。这条路不是不行但它要求的不是Python而是OpenFlow命令行的熟练度。对多数人来说ovs-ofctl的字段名比Python类方法更难记而且你很难把测量逻辑和网络行为组织成一个完整的程序。Ryu的出现解决了两个问题。第一它是Python实现的OpenFlow控制器你可以用pip安装不需要编译C代码源码结构清晰适合在课程实验里「读得懂、改得动」。第二Ryu内置了OFPMatch、OFPPortStats等OpenFlow协议的Python绑定你不需要自己构造二进制报文只需要调用API。Mininet在这条链路里扮演的角色是「被测量的对象」。它用轻量级虚拟化创建host和switch每个switch在Controller之外再连一个OvsSwitch这个OvsSwitch同时是数据面的转发设备。Ryu通过6633端口连接Mininet里的交换机之后通过OpenFlow协议下发流表并周期收集统计。用MininetRyu还有一个隐性优势不用真机。你不必在物理交换机上抓包也不怕配置错了影响网络。实验做完mn -c把环境清理干净宿主机毫发无伤。这是它在课程场景里压倒性的优势。2.2 最小可跑通的环境安装命令与版本搭配我建议你先把Mininet装好再装Ryu。以Ubuntu 22.04为例Mininet可以用apt直接安装装完自带ovs、iperf等工具省去手动配Open vSwitch的步骤。Ryu用pip安装但先要把Python环境里的setuptools和pip升级一遍不然编译Ryu依赖时容易报错。# 1. 更新apt源并安装Mininet全家桶 sudo apt update sudo apt install -y mininet # 2. 安装Python开发头文件和pip sudo apt install -y python3-dev python3-pip # 3. 升级pip并安装Ryu控制器 python3 -m pip install --upgrade pip pip install ryu # 4. 验证安装结果 sudo mn --version ryu-manager --version提示如果你的系统里同时装了多个Python版本执行pip前先确认pip指向的是python3。用pip --version查看若指向python2改成pip3。这套组合是目前最稳定的Mininet 2.3、Ryu 4.34、OpenFlow 1.3。Ryu 4.x对Python 3.6以上都兼容。装完后不要急着跑mn --toposingle,3先用ryu-manager启动一个空控制器再启动Mininet拓扑让它自己去连6633端口。2.3 一台宿主机上同时跑拓扑与控制器的启动顺序顺序错了是会翻车的。先起控制器再起网络这个顺序不能反过来。Mininet在创建交换机的瞬间会尝试连接控制器如果控制器端口没在监听交换机就会进入Standalone模式端口统计接口也不会正常响应Ryu的请求。# 终端1先启动Ryu控制器监听6633端口 ryu-manager ryu.app.simple_switch_13 # 终端2启动Mininet连到本机控制器 sudo mn --topolinear,2 --controllerremote,ip127.0.0.1,port6633这里有两个参数要解释。--topolinear,2创建两台交换机交换机之间有一条链路每台交换机下挂一个host适合测量跨交换机流量。--controllerremote,ip127.0.0.1,port6633是告诉Mininet的OVS不要用内置控制器而是往本机6633端口发起OpenFlow连接。跑通后你会看到Ryu的日志里连续输出EVENT OFPStateChange说明两台交换机都成功注册到控制器上了。到这一步模拟网络和SDN控制面已经建立后面所有测量逻辑都发生在Ryu这一侧。3. 读懂Python源码的测量逻辑拓扑、控制器与数据采集三件套3.1 拓扑脚本双主机走向量测量拓扑的套路课程实验的拓扑不用复杂双主机接单交换机是最容易控制变量的。一个host发流量另一个host接收控制器在交换机上统计端口数据就能算出单向吞吐、丢包率等指标。如果你发的是TCP流还要留意iperf的窗口大小对测量结果的影响。# custom_topo.py from mininet.topo import Topo class DualHostTopo(Topo): 双主机直连单交换机拓扑适合做端口级测量 def __init__(self): Topo.__init__(self) # 创建一台交换机dpid设为1便于后续查看流表 switch self.addSwitch(s1, dpid1) # 创建两个主机MAC和IP按实验报告习惯固定 host1 self.addHost(h1, ip10.0.0.1/24) host2 self.addHost(h2, ip10.0.0.2/24) # 两端连接到交换机带宽限制成10M模拟真实链路瓶颈 self.addLink(host1, switch, bw10) self.addLink(host2, switch, bw10)参数说明addSwitch里的dpid参数直接决定控制器看到的交换机编号在Ryu的PortStats消息里会带上这个编号多交换机实验时靠它区分数据属于哪台设备。bw10是Mininet的链路带宽限制默认值是100M如果你不设置测出来的速率上限会非常宽松难以体现链路瓶颈。对于课程演示来说把瓶颈压到10M或20M更容易讲清楚「测量值为何不等于链路带宽」这个问题。启动这个自定义拓扑需要带上文件路径sudo mn --custom custom_topo.py --topodualhost --controllerremote,ip127.0.0.1,port66333.2 Ryu控制器里的测量核心端口统计与流表统计Ryu控制器获取测量数据的方式有两种一种是主动向交换机发送流表统计请求交换机返回匹配的数据包数和字节数另一种是控制器开启端口状态监听每隔一段时间接收交换机的异步消息从中解析每个端口的收发字节。前者能细致到某条流后者更适合观察端口整体负载。下面这段是我常用的一版精简控制器它在处理packet-in的同时周期性向交换机请求端口统计。# measure_controller.py from ryu.base import app_manager from ryu.controller import ofp_event from ryu.controller.handler import MAIN_DISPATCHER, set_ev_cls from ryu.ofproto import ofproto_v1_3 from ryu.lib import hub class PortMeasure(app_manager.RyuApp): 周期采集端口统计并以日志输出的示例控制器 OFP_VERSIONS [ofproto_v1_3.OFP_VERSION] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.datapaths {} # 启动协程每3秒发一次统计请求 self.monitor_thread hub.spawn(self._monitor) set_ev_cls(ofp_event.EventOFPStateChange, MAIN_DISPATCHER) def _state_change(self, ev): 交换机上线/下线时记录datapath对象 dp ev.datapath if dp.id: self.datapaths[dp.id] dp def _monitor(self): 定时向所有已连接的交换机请求端口统计 while True: for dp in self.datapaths.values(): self._request_port_stats(dp) hub.sleep(3) def _request_port_stats(self, dp): 构造PortStatsRequest消息并发送 ofp dp.ofproto parser dp.ofproto_parser req parser.OFPPortStatsRequest(dp, 0, ofp.OFPP_ANY) dp.send_msg(req) set_ev_cls(ofp_event.EventOFPPortStatsReply, MAIN_DISPATCHER) def _port_stats_reply(self, ev): 收到交换机回复后打印每个端口的字节数 body ev.msg.body for stat in body: self.logger.info( switch_id%s port%s rx_bytes%s tx_bytes%s, ev.msg.datapath.id, stat.port_no, stat.rx_bytes, stat.tx_bytes)这段代码的测量思路是先收集datapath对象再周期下发OFPPortStatsRequest交换机收到请求后会把统计信息通过EventOFPPortStatsReply送回来。hub.sleep(3)是采样间隔课程实验里设成2到5秒都合适间隔太短对交换机的CPU负载偏高间隔太长则测量曲线不够平滑。3.3 通过REST API把测量数据拉出来的常见做法实际交报告时你通常不想让测量数据只在Ryu的日志里滚动而是希望程序像调用接口一样持续拿到数值存下来画图。Ryu自带了一组REST应用比如ryu.app.ofctl_rest它会暴露一个可查询流表统计的HTTP接口。你可以在启动Ryu时同时加载几个应用ryu-manager ryu.app.simple_switch_13 ryu.app.ofctl_rest这样就会在8080端口开一个REST服务。查询端口统计用GET /stats/port/{dpid}即可返回JSON是交换机发给控制器的原始数据字段是rx_bytes、tx_bytes等。注意这里只能查到交换机在「回复那一刻」的快照值速率需要你缓存前一条记录再用差值除以时间间隔计算。常见做法是写一个Python脚本周期轮询这个接口把接口数值存到内存列表或数据库里。我一般会加上时间戳再存方便后续画速率曲线。如果不想引入额外的HTTP依赖也可以直接在Ryu控制器里维护一个全局字典在_port_stats_reply回调里覆盖更新哪个方案更顺手取决于你的报告需要。REST方式的优势在于解耦控制器的任务只剩转发和统计数据采集由独立脚本负责两边的Python代码都能保持最短。4. 跑通一次测量的完整路径常用命令、参数调整与结果验证4.1 快速复现的最小操作集与启动顺序这份实验整套跑通的步骤不复杂但每一步之间有时间依赖。控制器的启动时间要预留3秒等待它绑定6633端口Mininet的创建过程会触发交换机和控制器的握手这一步通常耗时不到1秒。# 终端A启动控制器前台运行方便直接看日志 ryu-manager measure_controller.py # 终端B启动Mininet sudo mn --custom custom_topo.py --topodualhost --controllerremote,ip127.0.0.1,port6633 # 终端C在mininet里查看网络状态 mininet pingall mininet h1 ping h2上面最后一条命令藏在Mininet交互终端里执行。如果你从终端B进入mn后没有看到mininet提示符说明控制器连接失败或拓扑创建异常。可以按CtrlD退出后重新执行或者检查Ryu日志里有没有move to master字样。4.2 测量参数怎么调间隔、端口与带宽单位测量间隔直接写在hub.sleep()里这个值改起来最直观但不要指望改成0.1秒就能得到更精确的数据。交换机的PortStats缓存本身不是实时更新的OpenFlow协议规定交换机至少每N秒刷新一次统计过高的请求频率只会让控制器收到重复的旧数据。# 修改周期采样间隔为5秒并在回复里附带字节速率换算 old_bytes {} def _port_stats_reply(self, ev): body ev.msg.body now time.time() for stat in body: key (ev.msg.datapath.id, stat.port_no) if key in old_bytes: duration now - old_bytes[key][0] rx_speed (stat.rx_bytes - old_bytes[key][1]) * 8 / duration self.logger.info(port%s rx_speed%.2f bit/s, stat.port_no, rx_speed) old_bytes[key] (now, stat.rx_bytes)计算速率时最容易踩的坑是单位。OpenFlow统计的rx_bytes是原始字节数你要转成bit需要乘以8再除以采样间隔才是每秒比特数。很多人在报告里忘记乘8导致控制器的测量结果与iperf显示的12 Mbits/s对不上差了8倍这是非常常见的翻车现场。4.3 数据验证用iperf做一次双向测速对比控制器读数测量结果本身没有任何意义必须拿到一个对标物去验证。家里测速还会对照运营商App的数字实验里最有说服力的对照就是iperf。Mininet内置了iperf你可以在mininet终端里直接执行也可以分两个终端进入host的shell执行。# 终端B中进入h2先启动服务端 mininet h2 iperf -s -u -i 1 # 终端C中从h1发起UDP流量持续20秒带宽限制12M mininet h1 iperf -c 10.0.0.2 -u -b 12M -t 20 -i 1注意我用的是UDP。TCP模式会受到拥塞控制影响速率会波动不好用来与控制器统计做精确对比。UDP配合-b参数给出发送速率接收端iperf -u会显示实际收到的带宽、丢包率控制器的端口统计再给出接收端口的字节速率三者对照误差在5%以内算正常。如果控制器算出的速率和iperf显示的速率差了近10%先确认采样间隔是否稳定再检查Mininet的链路带宽设置。链路bw10意味着两种速率都不可能超过10M如果iperf显示12M说明带宽限制没生效检查一下topo文件里的addLink是否真的传入了bw参数。5. 你大概率会踩的5个坑控制面、数据面与测量精度5.1 Ryu连不上Mininet的交换机现象Mininet启动后交换机显示Connected但Ryu日志毫无动静pingall也失败。原因最常见是控制器地址写错或者Ryu启动时6633端口被其他进程占用。另一个隐蔽原因是Mininet默认会尝试往6653端口连而Ryu监听在6633。两者不一致时交换机不会报错但控制器永远收不到上线事件。解决先netstat -tlnp | grep 6633确认端口在被Ryu监听再看Mininet启动时--controllerremote给的端口号。如果从Mininet里pingall失败试着手动指定IPsudo mn --controllerremote,ip127.0.0.1,port6633不要依赖默认值。5.2 控制器算出的速率与iperf差距非常大现象iperf显示UDP 10M控制器打印的rx_speed只有9M或更少有些时候还忽高忽低。原因采样间隔抖动造成的瞬时误差。hub.sleep(3)不是精确睡眠如果控制器忙于处理packet-in消息两次请求之间的实际间隔可能变成3.8秒你用固定差值除以3算出来的速率自然失真。解决不要用一个固定的数字去除以间隔要用两次实际收到回复的时间戳之差。我在4.2节代码里用time.time()记录时刻就是为这个。另一种做法是用时间窗例如每5秒取前后两条统计做差值窗口越长单次抖动的影响越小。5.3 端口统计数据一直是零现象控制器能收到OFPStateChange但_port_stats_reply里打印的rx_bytes和tx_bytes为0。原因交换机没有收到任何流量。Ryu的simple_switch_13默认会下发流表把未知流量发给控制器但如果你的控制器没有实现packet-in处理交换机里根本不会安装转发规则所有流量都会因为buffer miss而被丢弃。解决先确认流量真正到达了交换机。在mininet里执行h1 ping h2。如果ping通说明流表已安装如果没通回到控制器代码确认有packet-in的set_ev_cls方法在维护MAC转发表。最简单加一段默认流表把OpenFlow消息交给simple_switch_13统一使用ryu.app.simple_switch_13作为控制器。5.4 多个交换机的统计数据混在一起难以区分现象拓扑是linear,2控制器里有两台交换机但统计数据打印出来只有一台或两台的数据混淆。原因控制器里用datapath.id作为字典key来区分datapath。如果Mininet创建交换机时没有显式指定dpid系统会按顺序分配1、2与你在topo文件里看到的s1、s2未必对应。线性拓扑里的s1和s2会被当成两台不同的交换机但如果你用单拓扑就永远只有一个datapath。解决在topo文件里给每台交换机写死dpid例如self.addSwitch(s1, dpid1)这样控制器日志里switch_id可以和拓扑脚本一一对应。多交换机实验务必在一开始就固定id不然报告里的数据没法对应到拓扑图上。5.5 采到的数据曲线不平滑像锯齿一样现象同一台交换机相同端口前后两次采样的速率值相差30%以上图形很难看。原因iperf的UDP发包频率不是绝对均匀交换机统计的刷新周期也非毫秒级精确再加上控制器采样时刻漂移三条时间线叠加天然会产生抖动。解决在计算速率时做移动平均。维护一个长度为5的队列每来一条新数据就丢弃最旧的那条取平均值输出。这样曲线会平滑很多又不会像加权平均那样引入明显的滞后。课程报告的图表里平滑后效果更像展示真实数据抖动太大反而显得测量不严谨。6. 让回程实验变成可交付作品在测量数据上做两层进阶6.1 把测量数据接进SQLite做时序存储如果你想让这个实验成为简历里可展示的作品第一步是把Ryu的日志输出升级成数据库存储。SQLite是零配置、Python内置好、不需要另起服务的数据库。在控制器里集成SQLite比用MySQL简单得多而且数据文件可以被Excel直接读取交作业前导出成csv非常顺手。import sqlite3 import time def _init_db(self): self.conn sqlite3.connect(measure.db) self.conn.execute(CREATE TABLE IF NOT EXISTS port_stats (timestamp REAL, switch_id TEXT, port_no INT, rx_bytes INT, tx_bytes INT)) def _save_to_db(self, stat, dpid): self.conn.execute(INSERT INTO port_stats VALUES (?,?,?,?,?), (time.time(), str(dpid), stat.port_no, stat.rx_bytes, stat.tx_bytes))表结构里我故意只存原始字节数不存换算好的速率。速率是应用层需要算的存原始值可以灵活回放去做不同窗口长度的对比分析。这是报告里「误差分析」部分很实用的数据来源。6.2 用轻量脚本直接出对比图Grafana和InfluxDB的组合对这个实验来说太重了课程报告一张matplotlib图就够。你可以在实验结束后用sqlite3查询表把rx_bytes之差转成速率然后和iperf的输出画在同一个坐标系里。import sqlite3 import matplotlib.pyplot as plt conn sqlite3.connect(measure.db) cursor conn.execute(SELECT timestamp, rx_bytes FROM port_stats WHERE port_no2 ORDER BY timestamp) rows cursor.fetchall() timestamps, rx_bytes zip(*rows) duration [timestamps[i1] - timestamps[i] for i in range(len(timestamps) - 1)] speed [(rx_bytes[i1] - rx_bytes[i]) * 8 / duration[i] / 1e6 for i in range(len(duration))] plt.plot(speed) plt.ylabel(Mbps) plt.xlabel(sample) plt.savefig(throughput_curve.png, dpi200)这张图交到老师手里比打印一长串日志有说服力得多。个人习惯测量实验最后必须留这么一张图所有误差论述在没有图的情况下都显得空泛。6.3 一个让你少走弯路的小习惯做这种带控制器的实验最怕改完代码后忘记重启Ryu导致测量用的还是旧逻辑。我的习惯是每次改完measure_controller.py执行ryu-manager时加--verbose参数让它打印完整的OpenFlow消息同时用CtrlC结束旧进程再启动新进程绝不热替换。脚本化的启动命令放在一个run.sh里一行启动一行清环境实验记录也更规范。希望这次把Mininet和Ryu的测量链路拆细之后你能直接照着这套路径跑出自己的数据图。坑已经标出来了剩下的事就是动手试一遍数据会告诉你哪里还需要调。本文还有配套的精品资源点击获取