简介:基于软件定义网络(SDN)架构的 Python 网络流量监控与控制源码,面向毕业设计、期末大作业和课程设计场景,实现流量实时采集、状态可视化展示与转发策略控制等核心功能。项目由个人独立手写,代码注释完整,模块划分清晰,新手也能较快读懂,部署简单,稍加配置即可运行,适合直接作为高分数项目模板或二次开发基础。资源压缩包总共收录两千个文件,其中 Python 源文件占绝大多数,另有底层 C 扩展代码负责网络数据处理与性能优化,配套文档与配置类文件则用于参数调整和功能说明,整体体积约一百一十二兆字节。截至目前已有三百八十七人学习浏览。对需要完成网络相关课题的学生而言,这套带有完整注释、兼具监控与控制能力的源码,能够提供从架构理解、代码阅读到实际部署的全程参考,是提升项目完成度、答辩分数和教师评价的有力支撑。
1. SDN网络流量监控与控制Python源码:它解决的是一整套可编排闭环
传统网络里排查两台主机之间为什么卡顿,多数人第一反应是登录交换机一条条敲 show 命令,或者跑到服务器上 tcpdump 抓包。OpenFlow 出现之后,交换机的转发行为被抽象成控制器里的一张张流表,流量统计也从"设备各报各的"变成"控制器统一收"。基于 SDN 架构的网络流量监控和控制 python 源码,做的就是把端口速率、包计数这类南向数据收上来,再按阈值或策略反向下发控制动作。你家里那台 sdn 光猫,管理后台能看到每个终端的实时速率,本质上也是这套控制面与转发面分离思想的下沉。这套东西适合两类人:一是拿 SDN 课题做毕业设计或实验网络项目的开发者,需要一份能跑通、能改、能写进论文的代码骨架;二是想从传统运维转向可编程网络的工程师,用它看懂控制面和转发面到底怎么分工。
2. 环境搭建:Mininet模拟拓扑与Ryu控制器的两个关键选型
做 SDN 流量监控,第一件事不是写代码,而是先搭出一个能反复折腾的实验环境。真机环境成本高、排错慢,Mininet 就是为这件事设计的:在一台 Linux 主机里用进程模拟主机、用 Open vSwitch 模拟交换机,毫秒级拉起一个实验网络。控制器则用 Ryu,一个纯 Python 的 OpenFlow 控制器框架。这两个组合是 SDN 学习与二次开发最主流的搭配,但正式动手之前,有两个选型问题值得先想清楚。
2.1 为什么选Ryu:Python写控制器,OpenFlow 1.3是能力分界
市面上控制器不少,常见的有 Ryu、ONOS、Floodlight、OpenDaylight。对一份以 Python 为载体的源码项目来说,Ryu 几乎是唯一不需要跨语言维护的选项。它本身就是一个 Python 包,应用的写法就是继承一个 RyuApp 类,没有 Java 那套工程化负担,二次开发和 debug 都直接落在 Python 生态里。
| 控制器 | 语言 | 部署成本 | 适合场景 |
|---|---|---|---|
| Ryu | Python | 低,pip 安装 | 中小规模、学习与二次开发 |
| ONOS | Java | 高,需要 Karaf 环境 | 运营商级、集群部署 |
| Floodlight | Java | 中 | 传统企业网络实验 |
| OpenDaylight | Java | 高 | 大规模生产实验 |
对流量监控和控制这个需求而言,OpenFlow 协议版本比控制器品牌更关键。OpenFlow 1.0 只有单级流表,做转发控制没问题,但想做流量整形、限速,必须依赖 Meter 表,而 Meter 表是 OpenFlow 1.3 才开始具备的能力。所以这个项目里有个硬性要求:Ryu 应用的OFP_VERSIONS必须显式声明为 1.3,否则 mininet 里的 OVS 交换机协商回 1.0,后面所有 Meter 限速代码都会变成摆设。
安装环节我一般分成两步走。先建虚拟环境,再装 Ryu,这样避免污染系统 Python,也方便以后整个目录直接删掉重来。Mininet 用 apt 安装即可:
sudo apt update sudo apt install -y python3 python3-pip python3-venv mininet python3 -m venv ~/sdn-venv source ~/sdn-venv/bin/activate pip install ryuvenv 的作用是给项目一个独立的 Python 环境,pip install ryu会把 eventlet、greenlet、oslo.config 这些依赖一并装进 venv,而不是系统目录。后面升级或重装时,删掉~/sdn-venv重建即可,这就是后悔药。装完后用ryu-manager --version验证命令可用。如果对 Python 安装这块还不熟,先把虚拟环境这步走通再来,后面所有操作都建立在它之上。
注意:
sudo执行 ryu-manager 时,如果提示 command not found,是因为 sudo 的 PATH 不含 venv 的 bin 目录。用绝对路径启动,例如sudo /home/你的用户名/sdn-venv/bin/ryu-manager。
2.2 跑通最小拓扑:2台主机1台交换机,先看到控制器握手
最小实验拓扑是一台交换机下面挂两台主机,这也是 Mininet 里最经典的 single 拓扑。启动命令要显式把控制器指给本机的 Ryu:
sudo mn --topo single,2 \ --mac \ --controller remote,ip=127.0.0.1,port=6633 \ --switch ovsk参数逐个说:--topo single,2表示一台交换机带 2 台主机 h1、h2;--mac让主机 MAC 地址可读,方便抓包时辨识设备;--controller remote是关键,它告诉 Mininet 不启动内置控制器,而是去连外部控制器,ip=127.0.0.1,port=6633指向本机 Ryu 监听的地址和端口;--switch ovsk指定使用 OVS 内核态数据路径,这是后续 Meter 限速能正常工作的前提,用户态 ovs 的 Meter 支持在某些版本下是残缺的。
正确的启动顺序是:终端 1 先运行sudo /home/你的用户名/sdn-venv/bin/ryu-manager flow_monitor.py --observe-links,终端 2 再执行上面的 mn 命令。Mininet 启动过程中,OVS 交换机会主动向 6633 端口发起连接,Ryu 日志里出现dpid=1的字样,说明交换机已完成注册,握手成功。此时在 mininet 里执行pingall,不通是正常的,因为你还没向交换机下发任何流表。可以顺手用sudo ovs-ofctl dump-flows s1看看交换机的流表,基本是空的。环境就绪,接下来进入源码阶段。
3. 源码拆解:用Ryu实现端口统计的完整数据闭环
这个项目的"高质量"体现在哪儿,我判断有两个标准:一是数据闭环完整,监控数据从南向协议收上来之后能落成可用的速率指标,而不是停留在打印日志;二是事件循环不阻塞,所有耗时操作都不能卡住主线程。下面这份flow_monitor.py是监控部分的完整骨架,控制扩展在下一章接上。
3.1 控制器入口:状态监听、统计请求线程与事件回调
先贴监控部分完整代码,它可以直接保存为flow_monitor.py运行:
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 time class FlowMonitor(app_manager.RyuApp): OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION] def __init__(self, *args, **kwargs): super(FlowMonitor, self).__init__(*args, **kwargs) self.datapaths = {} # dp_id -> datapath实例 self.port_stats = {} # (dp_id, port_no) -> [采样时间, rx_bytes, tx_bytes] self.port_speed = {} # (dp_id, port_no) -> (rx_kbps, tx_kbps) self.monitor_thread = hub.spawn(self._monitor) @set_ev_cls(ofp_event.EventOFPStateChange, MAIN_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) 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): req = dp.ofproto_parser.OFPPortStatsRequest( dp, 0, dp.ofproto.OFPP_ANY) dp.send_msg(req) @set_ev_cls(ofp_event.EventOFPPortStatsReply, MAIN_DISPATCHER) def _port_stats_reply_handler(self, ev): body = ev.msg.body dp = ev.msg.datapath now = time.time() for stat in body: if stat.port_no == dp.ofproto.OFPP_LOCAL: continue key = (dp.id, stat.port_no) old = self.port_stats.get(key) if old is not None: dt = now - old[0] if dt > 0: rx_kbps = (stat.rx_bytes - old[1]) * 8.0 / dt / 1000.0 tx_kbps = (stat.tx_bytes - old[2]) * 8.0 / dt / 1000.0 self.port_speed[key] = (rx_kbps, tx_kbps) self.logger.info( "dp=%d port=%d rx=%.2f kbps tx=%.2f kbps", dp.id, stat.port_no, rx_kbps, tx_kbps) self.port_stats[key] = [now, stat.rx_bytes, stat.tx_bytes]这段代码的逻辑不算复杂,但每个部分都有讲究。app_manager.RyuApp让类变成 Ryu 应用;@set_ev_cls把函数注册成事件回调,框架收到对应消息就调用;OFP_VERSIONS锁定 OpenFlow 1.3,保证 Meter 能力可用。_state_change_handler在交换机上线时把 datapath 存进字典,离线时移除,避免向死连接发消息导致异常。
hub.spawn启动一个绿色协程跑_monitor,循环里遍历所有在线交换机,逐个发起端口统计请求。这里必须用hub.sleep(3),它是 Ryu 协程安全的睡眠;如果用标准time.sleep,整个事件循环会被阻塞,后续所有消息回调都排不上队,这是 Ryu 新手最容易翻车的地方。三个采样间隔值得说:3 秒是折中值,实验网络够用;设备多时统计请求是串行的,间隔太短会让事件循环积压;间隔太长控制反应慢,后面做限速时会明显感觉到迟钝。
OFPPortStatsRequest(dp, 0, OFPP_ANY)三个参数分别是被查询的 datapath、flags 和端口号,OFPP_ANY表示查询全部端口。想要精确定位某一个口,把第三个参数换成ovs-ofctl show s1里看到的 ofport 编号。OFPP_LOCAL是交换机本地端口,相当于设备自己,不能拿它算用户流量,所以直接跳过。
3.2 端口统计响应的处理:速率计算与数据出口
OpenFlow 统计的是累计字节数,不是速率。_port_stats_reply_handler里做的是差分计算:用本次采样值减去上一次的字节数,得到间隔内的增量,再除以采样间隔得到每秒字节数,乘 8 转成比特,除 1000 得到 kbps。这就是监控面板上速率的原始来源。old为空说明这是第一次采样,没有历史值,直接存快照,不计算速率;dt小于等于 0 的情况直接跳过,避免除零。
把监控数据变成可用的"数据出口"有几种做法。这个项目骨架用 logger 输出就够了,Ryu 的 logger 就是标准 logging,后续接日志采集系统不需要改代码。如果想做可视化,就在self.port_speed更新时把数据同步到一个字典里,再用 REST API 暴露,不必一开始就上消息队列。我见过不少人在这个阶段直接上 kafka、时序库,最后发现监控点一共就几台设备,纯属给系统增加复杂度。
这里有个排查方向值得记住:如果日志里只有第一次输出、之后不再更新,大概率是采样协程被异常打断了。Ryu 的协程异常会体现在日志尾部,启动时加--verbose看完整日志,优先找有没有 Traceback。这套源码的监控半边到此闭环,下一章把速率数据用起来,变成真正的"控制"。
4. 从监控到控制:阈值触发、Meter限速与流表绑定的完整动作链
监控数据已经上来了,接下来把"看到速率"变成"控制速率"。SDN 里的控制动作常见的有三条路:下发流表改路径、通过 Meter 限速、直接 down 掉端口。对流量监控项目来说,Meter 限速最温和、可恢复,也是 OpenFlow 1.3 提供的最标准手段。这一章把决策循环写成函数,在上一章的FlowMonitor类里直接调用即可。
4.1 阈值决策放哪里:在监控协程里直接查port_speed
决策逻辑不能放在 handler 回调里——handler 是在事件循环里执行的,频繁的耗时操作会阻塞事件循环,影响其他消息处理。正确做法是把它放在_monitor协程中,统计结果回来后统一检查。在类里追加一个_enforce方法:
THRESHOLD_KBPS = 1000 # 超过1Mbps就触发 LIMIT_KBPS = 300 # 限速目标300kbps METER_BASE = 100 # meter id从100开始,避免与其他id冲突 def _enforce(self): for (dp_id, port_no), (rx_kbps, _) in self.port_speed.items(): if rx_kbps > THRESHOLD_KBPS: dp = self.datapaths[dp_id] meter_id = METER_BASE + port_no # 一个端口对应一个Meter self._install_meter(dp, meter_id, LIMIT_KBPS) match = dp.ofproto_parser.OFPMatch( in_port=port_no, eth_type=0x0800) actions = [ dp.ofproto_parser.OFPActionOutput(dp.ofproto.OFPP_NORMAL) ] self._bind_meter( dp, priority=100, match=match, actions=actions, meter_id=meter_id)阈值和限速目标分开设置,是为了避免"一超就限、一限就恢复"的抖动。更稳妥的做法是双阈值滞回:速率高于上限时触发限速,低于下限时才释放,中间留一个缓冲带。meter_id按端口号偏移规划,简单且不会冲突。match里写eth_type=0x0800只匹配 IPv4 流量,避免把 ARP、LLDP 也纳入限速,否则可能引发网络协议报文被丢,造成更诡异的故障。OFPP_NORMAL动作把流量交给交换机正常处理路径,实际速率由 Meter 的令牌桶决定,而不是靠流表硬丢。
_monitor循环里这样接入:
def _monitor(self): while True: for dp in self.datapaths.values(): self._request_port_stats(dp) hub.sleep(3) self._enforce()先请求统计、睡眠、再执行决策。等了 3 秒后port_speed里已经是新数据,避免用上一轮的旧值触发误判。
4.2 Meter限速参数怎么设:rate与burst_size的关系
Meter 是 OpenFlow 1.3 引入的流量计量单元。它不直接丢包,而是把匹配到的流交给 band 处理,drop band 就是按速率丢弃报文。两个参数决定了限速行为:rate 和 burst_size,再加上 flags 决定 rate 的单位。
| 参数 | 单位/含义 | 建议值 |
|---|---|---|
| flags=OFPMF_KBPS | rate 单位是 kbps | 固定带宽场景 |
| flags=OFPMF_PKTPS | rate 单位是包/秒 | 报文速率敏感场景 |
| rate | 令牌桶填充速率 | 按业务带宽上限的 80% 给余量 |
| burst_size | 突发容量,0 表示交换机决定 | 小流量突发场景设 rate/8 |
安装 Meter 的代码如下:
def _install_meter(self, dp, meter_id, rate_kbps): ofproto = dp.ofproto parser = dp.ofproto_parser req = parser.OFPMeterMod( dp, command=ofproto.OFPMC_ADD, flags=ofproto.OFPMF_KBPS, meter_id=meter_id, bands=[parser.OFPMeterBandDrop(rate=int(rate_kbps), burst_size=0)], ) dp.send_msg(req)OFPMC_ADD重复调用同一个 meter_id 会报冲突,恢复限速时需要用OFPMC_MODIFY改速率或OFPMC_DELETE删除。band 目前 OVS 只实现了 drop 类型,所以不用纠结其他 band。burst_size设 0 是交给交换机决定,实际效果往往是突发被压得更狠;如果业务对瞬时抖动敏感,把 burst_size 设成 rate/8,大约相当于允许 125 毫秒的突发量,体验会平滑不少。
4.3 用FlowMod把流量引入Meter:优先级和已有流的替换
Meter 建好了,但流量没进 Meter,限速依然不生效。要让一个端口的流量走进 Meter,需要一个带OFPInstructionMeter的 FlowMod:
def _bind_meter(self, dp, priority, match, actions, meter_id): parser = dp.ofproto_parser ofproto = dp.ofproto instructions = [ parser.OFPInstructionMeter(meter_id), parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS, actions), ] mod = parser.OFPFlowMod( dp, priority=priority, match=match, instructions=instructions, idle_timeout=0, hard_timeout=0, buffer_id=ofproto.OFP_NO_BUFFER, ) dp.send_msg(mod)OFPInstructionMeter写在OFPInstructionActions之前,语义是"先经 Meter 计量,放行的再执行动作"。这里有一个极容易踩的替换逻辑:OpenFlow 规定,同 match 同 priority 的新 FlowMod 会覆盖旧条目;如果新 FlowMod 的 match 范围更宽(比如只写in_port + eth_type),而旧流是完整的五元组精确匹配,那么两者 match 不同,旧流不会被覆盖,已经建立的连接会继续走旧路径绕开 Meter。
解决方式有两种,实践中我习惯同时用:一是下发控制流表前,先删除同 match 域的旧流;二是把动态控制流表的 priority 统一放在一个高位段(比如 100-199),静态转发流表放低段(10),让动态流优先匹配。恢复动作则反过来,先删除 FlowMod 再删除 Meter,顺序反了 Meter 还在被引用,删除会报错。
5. SDN监控项目避坑:5个让新手直接翻车的细节
下面这五条,都是我在实验环境里反反复复踩出来的血泪经验。每一条都按"现象 → 原因 → 解决"讲清楚,对照自己日志排查即可。
5.1 现象1:Ryu连上Mininet,却收不到任何packet_in
现象:ryu-manager 日志已经出现 dpid=1 的握手成功记录,但 mininet 里pingall全部失败,控制器里一个 PacketIn 事件都没有。
原因:OpenFlow 1.3 里,流表 miss 的行为由 table-miss 条目决定。如果应用不安装任何流表,部分 OVS 配置下 PacketIn 根本不会上报给控制器。或者安装了 FlowMod 但 match 过窄,比如只匹配了eth_type=0x0800,ICMP、ARP 全被丢弃。
解决:在交换机特性事件里先下发一条低优先级 table-miss 流,把未命中报文上送给控制器:
@set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER) def _switch_features_handler(self, ev): dp = ev.datapath parser = dp.ofproto_parser match = parser.OFPMatch() actions = [ parser.OFPActionOutput( dp.ofproto.OFPP_CONTROLLER, dp.ofproto.OFPCML_NO_BUFFER) ] mod = parser.OFPFlowMod(dp, priority=0, match=match, actions=actions) dp.send_msg(mod)priority=0是所有流表里最低的优先级,不会抢业务流;OFPCML_NO_BUFFER表示报文不要缓存在交换机上,完整上送控制器,便于抓包分析。装完这条流,pingall通常就通了。
5.2 现象2:端口统计请求发出去了,返回全是0
现象:日志里dp=1 port=1 rx=0.00 tx=0.00一直不动,而 mininet 里主机明明在互相 ping,控制器上也能看到 PacketIn。
原因:最常见的是采样那一刻端口确实没有流量。ping 报文量很小,3 秒一次的采样很容易错过瞬时流量。另一个隐蔽原因是 Mininet 用了--switch user用户态交换机,部分低版本 OVS 用户态 datapath 的统计更新有明显延迟,导致差分出来的速率永远接近 0。
解决:先用iperf3生成持续流量再观察统计,不要用 ping 验证监控。把采样间隔从 3 秒调到 2 秒,降低错过概率。然后核对请求里的 port_no:在 mininet 里执行ovs-ofctl show s1,比对返回的 ofport 列表,你查询的端口编号必须能在这个列表里找到;如果请求里填的是OFPP_ANY,那就先打印ev.msg.body的长度,body 为空说明请求没到或端口号不对。
5.3 现象3:Meter表下发报错或者不生效
现象:OFPMeterMod发出去了,代码也没报异常,但流表绑上 Meter 后速率完全没变化,或者日志里出现 MeterModFailed 的报错。
原因:OVS 内核态 datapath 对 Meter 支持完整,但用户态 ovs 某些版本只支持包速率,不支持字节速率。更典型的是 Ryu 应用没有声明OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION],握手协商回 OpenFlow 1.0,而 1.0 根本没有 Meter 消息,发送端完全不知道已经发了无效消息。
解决:先确认 Ryu 应用版本声明;Mininet 启动必须带--switch ovsk;然后执行sudo ovs-ofctl -O OpenFlow13 dump-meters s1,看 Meter 是否真的装上。如果上述都正常,检查 meter_id 不要用 0,部分实现把 0 保留给特殊用途。
5.4 现象4:限速只对新建连接生效,正在跑的iperf不受限
现象:触发限速后,新建的 TCP 连接速率被压到目标值,但已经建立的 iperf 会话带宽纹丝不动。
原因:iperf 的大流量流已经通过一条 match 更精确的旧流表转发,这条旧流不带 Meter 指令。按 OpenFlow 规则,新下发的 FlowMod 与旧流 match 不同,不会覆盖旧条目,所以新规则只在新建连接上生效。
解决:下发 Meter 绑定流之前,先删除同 match 域的所有旧流:
def _delete_flow(self, dp, match): parser = dp.ofproto_parser mod = parser.OFPFlowMod( dp, command=dp.ofproto.OFPFC_DELETE, match=match) dp.send_msg(mod)OFPFC_DELETE是流表删除命令,match 传什么就删什么。或者更简单:动态控制流表的 priority 统一提到旧流之上,让新流优先匹配。动态表固定在 100 段,静态转发表固定在 10 段,冲突发生前就能预见。
5.5 现象5:重装或换Python环境后Ryu起不来
现象:pip install ryu明明成功了,运行 ryu-manager 时却报import eventlet相关错误,或者 greenlet 版本冲突,出现莫名其妙的ImportError。
原因:Ryu 对 eventlet 和 greenlet 的版本要求很严格,pip 在公共环境里会把它们升到新版本,新版与 Ryu 不兼容,启动即崩。
解决:不要用系统 Python 直接 pip install,建 venv,装完立即验证ryu-manager --version。如果已经崩了,在 venv 里强制重装 eventlet 和 greenlet,以 pip 官方索引给出的兼容版本为准,并把这两个包固定写进 requirements.txt,下次换机器一条命令恢复。我后来所有 SDN 实验都沿用这个习惯,省掉了大量环境级玄学问题。
6. 验证与进阶:用iperf给整套系统做一次压测
6.1 用iperf3验证监控准不准
环境跑起来后,用 iperf3 生成确定性流量,是验证监控数据最直接的办法。在 mininet 里执行:
mininet> h2 iperf3 -s -D mininet> h1 iperf3 -c 10.0.0.2 -t 15 -b 2Mh2 后台起服务端,h1 以 2Mbps 打流 15 秒。控制器日志里,端口速率应该稳定在 2000kbps 附近。验证标准有两条:速率误差在 10% 以内,说明统计公式和采样间隔没问题;随后速率超过 1Mbps 阈值,触发限速到 300kbps,iperf 实际吞吐跟着掉下来,说明 Meter 链路完整生效。我一直用这个方法做回归测试,每次改完代码先跑一遍,比看日志猜快得多。
6.2 把固定阈值改成PID调节
固定阈值限速有个通病:超限就猛压,恢复后流量又瞬间冲高,形成反复震荡。这个场景和电机控制里的转速环很像,所以我把 pid 控制里最常见的增量式思路搬过来,让 Meter 的 rate 跟着误差平滑调整,而不是一刀切:
class SimplePID: def __init__(self, kp=0.3, ki=0.05, kd=0.1, target=1000.0): self.kp, self.ki, self.kd = kp, ki, kd self.target = target self.integral = 0.0 self.last_err = 0.0 def update(self, current, dt): err = self.target - current self.integral += err * dt diff = (err - self.last_err) / dt if dt > 0 else 0.0 self.last_err = err return self.kp * err + self.ki * self.integral + self.kd * diffupdate的返回值作为 Meter 的 rate 参数,每次采样后调用OFPMC_MODIFY更新速率。需要注意 PID 输出可能为负,要 clamp 到 0 以上;dt 直接用采样间隔传入。这套反馈调节的思路,和电机控制、pmsm 无感控制里用的完全是同一套数学,放在 SDN 里就是带反馈的带宽整形器,比拍脑袋定阈值稳得多。我把采样间隔从 3 秒改到 2 秒、把阈值改成双阈值滞回之后,这套系统在实验网里跑了一周没出现误限速。测试时记得每次都先用mn -c清理干净再重启拓扑,这个习惯帮我省掉了大半诡异的排查时间。希望帮到你。
本文还有配套的精品资源,点击获取