前两篇把 mininet 装好、CLI 玩熟之后,大多数人会卡在同一个路口:命令行敲来敲去,验证连通性没什么问题,可一旦要做批量实验、自动化测试、跟外部 SDN 控制器联动,手敲命令就立刻变成灾难。这时候你就得把 mininet 当成一个 Python 库来用——它本身就是用 Python 写的,API 层面几乎覆盖了 CLI 的全部能力,还多出不少 CLI 根本做不到的事情。这篇文章就把"如何使用 Python API 来操作 mininet"这件事掰开揉碎讲清楚。
前提是你已经把 mininet 装上,至少跑通过mn --topo=linear,2,知道pingall和xterm是干什么用的。如果你正准备把网络仿真实验脚本化、可复现化,或者打算接外部控制器做 SDN 联调,那这篇正合适。我会从对象模型讲到生命周期,再给一份可以直接抄作业的最小脚本,然后展开 API 才有的进阶玩法,最后分享几条实战里踩出来的坑。
1. 从 mn 命令到 Python 脚本:为什么这一步迟早要走
1.1 CLI 的本质是"人跟网络对话",API 的本质是"程序跟网络对话"
mn --topo=single,3敲下去之后,mininet 本质上是创建了一个 Python 进程,这个进程解析你给的参数,设置好网络命名空间、OVS 网桥和管理接口,然后把命令行交给你。你在mininet>提示符后面敲的pingall、h1 ifconfig,实际都是这个 Python 进程里的对象方法在被调用。
CLI 模式在单个实验、快速验证拓扑连通性的时候非常顺手。但它的边界也很明显:不能写 for 循环,不方便按条件生成链路,无法把一组操作包装成函数,更没法把实验数据直接喂给外面的脚本做分析。你说我可以写 shell 脚本调mn命令啊?能,但那是在进程外做字符串拼装,跟直接操作 mininet 内部对象完全是两个层次。
1.2 用 API 脚本,能立刻看到三个实际收益
第一个是可复现性。实验拓扑和操作流程全部沉淀在一个.py文件里,下次直接sudo python3 lab.py就能复现同一套网络环境。不会因为哪次手抖少敲了一个xterm或者多设了个链路参数,导致实验条件前后不一致。
第二个是可编程性。你可以用循环批量创建 20 台主机、50 条链路,可以用随机数生成不规则拓扑,可以根据带宽参数决定某条链路要不要加延迟。这种能力在 CLI 模式下几乎做不到,或者说做了也极其痛苦。
第三个是可集成性。mininet 的 API 能和你的业务代码直接联动。你可以从一个网络仿真脚本里收集流表数据,交给外部程序分析;可以把某台主机替换成自定义进程;可以对接远程的 OpenFlow 控制器,让控制器策略自动下发到 mininet 里的虚拟交换机。CLI 模式只能"人看结果",API 模式是"程序处理结果"。
所以我的建议很直接:CLI 用来做冒烟测试和入门理解,真正要跑实验、写演示、做自动化,一律上 Python API。这也是很多网络仿真项目在实际落地时的事实标准。
2. 认识 mininet 的 API 对象模型:导演、演员和舞台道具
2.1 四个核心类和它们的职责边界
mininet 的 Python API 并不复杂,核心就是几个类在配合。我用一个比喻帮你建立直观印象:Mininet类是总导演,负责整个舞台的搭建和演出顺序;Host和Switch是演员,各自有独立的行为空间;Link是连接演员之间的道具线路,决定带宽和时延;Controller是场外的灯光总监,负责给交换机下发 OpenFlow 规则。
具体到代码层面,你至少需要认识这几个类:
Mininet类来自mininet.net,它是所有对象的容器,管理addHost()、addSwitch()、addLink()、addController()这些注册方法,以及start()、stop()、pingAll()等生命周期方法。
Host和Switch都继承自Node。主机对应终端设备,交换机对应虚拟交换机。默认情况下,每个节点都运行在独立的网络命名空间里,所以h1.cmd('ifconfig')看到的接口列表跟h2完全不同,这个隔离机制正是 mininet 能在一台物理机上模拟复杂网络的基础。
Link代表两条虚拟网线。普通Link只负责连通性,TCLink则支持bw(带宽)、delay(时延)、loss(丢包率)和max_queue_size(队列长度)参数,这也是做网络质量实验最常用的链路类。
Controller类来自mininet.node,负责跟交换机建立 OpenFlow 连接,把网络从"数据平面裸通"变成"控制平面可管"。如果你只想测试纯连通性,默认的Controller()就够了;要接外部控制器,就得换成RemoteController。
2.2 别把 Controller 和 CLI 搞混
很多新手第一次接触 API 时会踩一个概念坑:Controller跟你在终端里看到的mininet>提示符不是一回事。CLI(net)只是把交互式命令行挂载到当前网络上,本质上是mininet.cli.CLI这个类提供一个交互入口;而Controller是真正参与 OpenFlow 协议的控制器进程。
在 API 模式下,你可以完全不启动 CLI,纯脚本自动跑完测试然后退出。这是自动化实验的关键:不需要任何人坐在终端前敲命令。如果你在脚本里写了CLI(net),那脚本会在那一步停下,把交互权交还给人工,直到你输入exit才继续执行后面的代码。我自己的习惯是:调试阶段保留CLI(net),正式批量跑测试时把它注释掉。
2.3 一个 API 脚本的通用生命周期
所有 mininet API 脚本的基本骨架可以归纳为五步:
- 创建
Mininet实例,传入需要的交换机类型、链路类型和控制器类型。 - 依次调用
addHost()、addSwitch()、addLink()、addController()把网络元素注册进实例。 - 调用
net.start(),此时 mininet 才会真正创建网络命名空间、启动虚拟交换机、拉起控制器并建立连接。 - 执行测试或交互操作,比如
net.pingAll()、h1.cmd()、CLI(net)。 - 调用
net.stop()清理所有临时组件。
这里有一个底层逻辑要记住:addHost()这些方法只是在"登记",并没有真正创建实体;真正落地的动作全部发生在start()之后。所以在设计脚本时,你完全可以在start()之前任意调整拓扑,比如加一台交换器、改一条链路带宽,成本极低。等start()之后再做大的拓扑改动,就会涉及到重新初始化接口和流表的问题,那就是后话了。
3. 第一个 API 脚本:声明式拓扑与标准启动流程
3.1 最小可用脚本,逐行拆解
下面这份脚本是完整可运行的,建议你先存下来跑一遍,然后再继续读下面的解释。
#!/usr/bin/env python3 from mininet.net import Mininet from mininet.node import Controller from mininet.link import TCLink from mininet.cli import CLI from mininet.log import setLogLevel setLogLevel('info') net = Mininet(controller=Controller, link=TCLink) c0 = net.addController('c0', ip='127.0.0.1', port=6653) h1 = net.addHost('h1', ip='10.0.0.1/24') h2 = net.addHost('h2', ip='10.0.0.2/24') s1 = net.addSwitch('s1', dpid='0000000000000001') net.addLink(h1, s1, bw=10, delay='5ms') net.addLink(h2, s1, bw=10, delay='5ms') net.start() print(f"pingAll result: {net.pingAll()}") CLI(net) net.stop()第一行#!/usr/bin/env python3是 shebang,方便你直接执行。setLogLevel('info')很关键,它让 mininet 打印出启动和停止过程中的详细日志。如果你不调用这个函数,默认的日志级别可能是 warning,很多报错信息会被吞掉,你都不知道网络启动到哪一步挂的。
net = Mininet(controller=Controller, link=TCLink)是在声明全局配置。controller=Controller表示使用 mininet 自带的默认控制器,link=TCLink表示我们后续创建的链路都能支持带宽和时延参数。这个声明是全局性的,后面addLink()创建的链路对象都基于这个类型。
addController('c0', ip='127.0.0.1', port=6653)给控制器起了个名字叫c0。ip='127.0.0.1'和port=6653表示它监听在本机回环地址的 6653 端口,这是 OpenFlow 的标准端口。较老版本的教程里经常写成6633,如果你的环境里装了比较新的 Open vSwitch,它默认用的可能已经是6653了,这点后面说坑的时候还会再提。
addHost('h1', ip='10.0.0.1/24')在登记时直接给主机配置了 IP 和子网掩码。注意这个配置是在节点创建时写入网络命名空间里的,start()之后h1.cmd('ip addr')就能看到这个地址。如果没有在addHost里给 IP,那pingAll()会全部失败,因为ping需要先知道目标 IP。
addSwitch('s1', dpid='0000000000000001')里的dpid是交换机的 Datapath ID。OpenFlow 协议用这个 64 位十六进制值来唯一标识交换机。如果你不指定,mininet 会自动生成。但你要是跟外部控制器联调,最好手动指定一个固定值,否则控制器侧看到的交换机 ID 每次都会变,排查问题的时候会非常头疼。
addLink(h1, s1, bw=10, delay='5ms')创建了一条带宽 10 Mbps、单向时延 5 毫秒的链路。因为全局声明了link=TCLink,这里的bw和delay参数才会生效。如果用的是普通Link,这些参数会被静默忽略,链路照样通,但你测出来的吞吐量就没有任何限制,很多实验会白做。
net.start()是最重要的一步。它做的工作包括:为每个主机建立网络命名空间和虚拟以太网接口;启动 OVS 交换机并创建虚拟网桥;启动控制器进程;让所有交换机跟控制器建立 OpenFlow 连接。日志里你会看到一串Adding host h1、Adding switch s1的输出,那就是在逐项创建资源。
net.pingAll()会从每台主机向其他所有主机发送ping包,返回丢包率。对上面的双机直连拓扑来说,正确结果应该是0% dropped,也就是返回0.0。如果你看到乱码或者非零丢包率,说明拓扑里有断链,或者控制器没把流表正常下发。
CLI(net)会进入交互式命令行,方便你手动验证。自动化场景下应该把这行注释掉。net.stop()负责清理所有资源,包括停止控制器、删除网桥、销毁命名空间。脚本如果跑到这里正常退出,你再次运行别的 mininet 脚本就不会遇到端口占用或者网桥残留的问题。
3.2 用 Topo 子类把拓扑变成可复用组件
直接在主流程里addHost()一个接一个地写,对小型拓扑还能接受。但拓扑一旦超过 8 个节点,代码就会变得又长又乱。mininet 推荐的做法是自定义一个继承自mininet.topo.Topo的类,把拓扑结构封装在build()方法里。
from mininet.topo import Topo class TwoHostTopo(Topo): def build(self, loss=0.0): h1 = self.addHost('h1', ip='10.0.0.1/24') h2 = self.addHost('h2', ip='10.0.0.2/24') s1 = self.addSwitch('s1') self.addLink(h1, s1, bw=10, loss=loss) self.addLink(h2, s1, bw=10, loss=loss) if __name__ == '__main__': from mininet.net import Mininet from mininet.node import Controller from mininet.link import TCLink from mininet.log import setLogLevel setLogLevel('info') topo = TwoHostTopo(loss=1) net = Mininet(topo=topo, controller=Controller, link=TCLink) net.start() print(f"pingAll result: {net.pingAll()}") net.stop()这样做的好处是:拓扑类可以用参数控制内部细节,比如上面这个loss=1可以在 1% 丢包的链路条件下复现网络状态;同一个类可以被多个脚本复用;而且它跟 mininet 自带的拓扑机制兼容,你可以直接用mn --topo=twohost之类的命令加载类似的类。build()里的节点名是全局唯一的,重复使用同一个名字会报错,这一点在动态生成拓扑时尤其要注意。
3.3 start、测试、stop 的顺序里藏着什么逻辑
为什么一定要按照start()→ 测试 →stop()这个顺序?因为 mininet 只有在start()之后才会真正创建网络实体。之前addLink()只是在link列表里加了一条记录;start()时才把两端接口分别插到两个命名空间里,配置好 IP,再把交换机跟控制器之间建立 OpenFlow channel。
如果你在start()之前调用net.pingAll(),mininet 会直接报错或返回无意义的结果。同样,如果在stop()之后还去调用h1.cmd(),主机命名空间已经被销毁,命令会执行失败。我把这套顺序看成"先画图纸,再施工,再验收,最后拆工地"。见过不少新手把net.start()漏了,然后跑来问为什么pingAll()报错,多半就是图纸画了没施工。
另外一个小细节:stop()之后又立刻运行第二个脚本时,建议手动执行一遍sudo mn -c。这个命令会清理系统中可能残留的网桥、进程和临时文件。它只影响 mininet 相关的东西,不会动你物理机的网络配置,可以放心用。
4. 进阶能力:用 API 做 CLI 做不到的事
4.1 循环与随机化:批量生成拓扑不再靠手敲
CLI 模式下你想搭一个 20 台主机接 1 台交换机的拓扑,得打一大串参数,或者用--topo=tree,depth=2,fanout=4这种内置模板。但 API 模式下这是再普通不过的循环问题。
net = Mininet(controller=Controller, link=TCLink) s1 = net.addSwitch('s1') for i in range(1, 21): host = net.addHost(f'h{i}', ip=f'10.0.0.{i}/24') net.addLink(host, s1, bw=5 + (i % 10), delay='2ms') net.start() print(f"pingAll result: {net.pingAll()}") net.stop()这 20 台主机的 IP 自动分配成10.0.0.1到10.0.0.20,带宽从 5 Mbps 到 14 Mbps 不等。这个脚本在 CLI 模式下根本没法表达。更复杂的随机拓扑,比如节点数量随机、链路延迟随机的场景,本质上就是在循环里加随机数,没任何特殊 API,这就是编程能力带给网络仿真的自由度。
不过要提醒一句:mininet 里主机数量过多时,CPU 和内存会吃紧,因为每台主机都是一个独立的网络命名空间。20 台以内一般没问题,100 台以上就要考虑模拟机的性能了,别一上来就循环 500 台。
4.2 TCLink 细粒度链路控制:带宽、时延与丢包
TCLink的价值在于它替你把 Linux 的tc命令封装成了简单参数。你不光可以用bw限制带宽,还能组合delay和loss同时模拟延迟和丢包场景,这在视频传输实验、TCP 拥塞控制实验里是常用的。
net.addLink(h1, s1, bw=20, delay='50ms', loss=2, max_queue_size=100)bw单位是 Mbit/s,delay是字符串,比如'50ms'。loss的单位是百分比,2表示 2% 的丢包率。max_queue_size是路由器排队队列长度,配合带宽和延迟一起设置能模拟真实网络中的缓存溢出行为。
实测下来,你要是只测连通性,用普通Link就行,快且省资源。但你要测 TCP 吞吐量、视频卡顿、游戏延迟表现,就必须把链路参数加满。这里面的原理其实很简单:TCLink在链路两端加入队规则和延时控制,数据包经过这条虚拟链路时会被强制排队、丢弃和延迟,效果跟真实链路几乎一致。
4.3 主机行为编程:cmd、sendCmd、waitOutput
Node.cmd()是最常用的主机执行方法,比如h1.cmd('ip addr')会返回命令的完整输出。但它有个特性:命令执行期间脚本会阻塞,直到命令完成。如果你执行h1.cmd('iperf -s -p 5001'),这个命令会一直跑,你的脚本卡在那一行,永远走不下去。
正确的姿势是使用sendCmd()和waitOutput()的组合:
h1.sendCmd('iperf -s -p 5001 > /tmp/iperf_server.log 2>&1') h2.cmd('iperf -c 10.0.0.1 -p 5001 -t 5') output = h1.waitOutput()sendCmd()会启动命令但立刻返回,不等待命令结束;waitOutput()则等待前面启动的命令完成并返回输出。上面的例子让h1在后台跑 iperf 服务器,h2作为客户端测试 5 秒带宽,最后h1等待服务器进程退出。
这里有个血泪教训:后台运行的服务进程,输出一定要重定向到文件,否则管道缓冲区被占满后进程会阻塞。2>&1把 stderr 也一起重定向,避免错误信息污染主进程的管道。如果忘掉重定向,你会在waitOutput()那一步卡到怀疑人生。
4.4 对接外部 SDN 控制器
用默认Controller可以验证连通性,但它只是一个很基本的控制器,没有业务逻辑。现在的 SDN 实验大都用外部控制器,mininet 里通过RemoteController对接。
from mininet.node import RemoteController controller = net.addController( 'ext_ctrl', controller=RemoteController, ip='192.168.1.100', port=6653 )把ip指向运行外部控制器的那台机器,port保持跟外部控制器监听端口一致。交换机启动后会自动向这个地址发起 OpenFlow 连接。实测中常见的坑是:TCP 端口都能通,但交换机连上之后控制器不处理消息,那多半是 OpenFlow 版本协商不一致。mininet 默认的交换机可能会用最新版本,而外部控制器只支持较老版本。这时候可以在addSwitch()里传入protocols='OpenFlow13'之类的参数,强制指定协议版本,让两边协商成功。
对接外部控制器的好处是,你能直接观察控制器下发的流表、写自定义转发策略、或者把控制器跟业务后台的数据联动起来。这一套整合下来,mininet 就从一个"网络模拟器"变成了"可编程网络实验床"。
4.5 批量跑实验并收集结果
API 模式的终极优势在于可以把实验封装成函数,然后批量调用。比方说你试了 5 种不同丢包率,每种跑 3 次 TCP iperf,想要收集所有数据:
import time def run_iperf(loss_rate): topo = TwoHostTopo(loss=loss_rate) net = Mininet(topo=topo, controller=Controller, link=TCLink) net.start() h1, h2 = net.get('h1', 'h2') h1.sendCmd('iperf -s -p 5001 > /tmp/iperf_s.log 2>&1') time.sleep(1) out = h2.cmd('iperf -c 10.0.0.1 -p 5001 -t 5') net.stop() return out for rate in [0, 1, 2, 5]: result = run_iperf(loss_rate=rate) with open(f'result_{rate}.log', 'w') as f: f.write(result)这段脚本会连续生成四个不同丢包率的网络环境,每次独立启动、测试、清理,把结果落盘。手工在 CLI 里做同样的事情,至少得反复开启关闭多轮,还容易把链路参数记混。API 模式下,实验数据直接跟参数值绑定在一起,分析阶段就省事多了。
5. API 脚本实战中的典型翻车现场
5.1 清理残留:net.stop() 能做的与不能做的
net.stop()不是万能的。如果你在脚本中途用Ctrl+C强停了进程,进程异常退出,stop()里的清理代码可能根本来不及执行。下次再跑另一个 mininet 脚本时,通常会出现ovs-vsctl: cannot create bridge或端口被占用之类的诡异报错。
我的处理习惯是:凡是跑任何 mininet 脚本之前,先执行sudo mn -c。这条命令清理所有残存的网桥、进程和临时文件,它能识别上次异常退出留下的东西。另外在脚本里用finally块也是一个好习惯:
net.start() try: print(net.pingAll()) finally: net.stop()这样一来,即使测试部分抛出异常,net.stop()也会被执行。如果net.stop()本身也失败,那你再去手动sudo mn -c兜底。
5.2 后台命令的等待陷阱
前面已经提到了iperf -s的坑,这里再扩展一下。cmd()的阻塞特性不仅会卡住后台服务类命令,连一些需要交互的命令也会有类似问题。比如h1.cmd('ping 10.0.0.2')如果没加-c 4,它就会一直 ping 下去,cmd()一直等不到进程结束,脚本就卡死了。
所有"持续运行"或"等待外部条件"的命令,要么加超时参数让进程自然结束,要么用sendCmd()启动后用别的命令去探测结果。我的经验判断标准很简单:这个命令三秒内会不会自己退出?不会的话,别用cmd()。
另外一个相关的小坑是:sendCmd()启动的命令如果产生大量输出且没有重定向到文件,终端里会显示得乱七八糟,而且有管道阻塞风险。凡是后台跑的进程,我都无条件加> /tmp/xxx.log 2>&1,省心不少。
5.3 root 权限与 Python 环境错位
mininet 创建网络命名空间和 OVS 网桥都需要 root 权限。所以你的脚本大概率要以sudo python3 lab.py的方式运行。但如果你用了 Python 虚拟环境,而虚拟环境中安装了 mininet 的 Python API 包,直接sudo python3用的是系统 Python,它可能找不到你这个虚拟环境里的包,于是报ModuleNotFoundError: No module named 'mininet'。
这个问题我遇到不止一次。正确做法有两种:要么用sudo调用虚拟环境里的解释器,比如sudo /path/to/venv/bin/python3 lab.py;要么干脆把 mininet 装到系统 Python 里,不搞虚拟环境。考虑到 mininet 的依赖相对重,而且它本身就需要 root 权限,我的选择是直接装系统环境,省去环境错位的麻烦。
5.4 动态加节点不是不可以,但别在 start 之后硬来
mininet 的 API 理论上是支持在运行期间添加节点和链路的,但实际效果远没有start()之前加那么简单。你net.addHost('h3')之后,这个主机默认没有 IP,没有路由,链路两端接口也需要手动ifconfig配置;交换机侧还需要重新触发与控制器的连接,否则新链路根本没有流表可转发。
我的态度很明确:除非你的实验目标就是研究"动态扩容"本身,否则不要在start()之后做拓扑热插拔。如果确实需要加节点,最省事的方式是先net.stop(),改完拓扑再重新start()。反正 mininet 启动一个几十节点的拓扑也就几秒钟,比起调试怪异的流表状态,重启一次完全可接受。
5.5 6633 还是 6653,新旧端口的兼容问题
很多老教程里写控制器端口是6633,而较新版本的 Open vSwitch 和 OpenFlow 标准推荐端口是6653。mininet 自带的默认控制器会监听在哪个端口,取决于它的版本配置。如果你在addController()里强写了port=6633,而交换机默认往6653连,两边就永远握不上手。
排查这种问题最直接的办法:启动脚本后,在另一个终端执行ss -tlnp | grep ovs或ovs-vsctl show,看看控制器到底在监听哪个端口,交换机有没有连接记录。统一思路是:控制器端口跟交换机配置的端口必须一致。如果你不确定,看一眼 mininet 日志里打印的监听端口,照着设就行,不要盲目照着旧教程抄。
6. 把 mininet API 用顺的几条心法
写完这套脚本、踩完这些坑之后,我复盘出几条可以让后续项目省力不少的习惯,这里一并分享给你。
第一条,拓扑不等于实验。把拓扑定义、实验动作、结果存储分别拆成三个模块。拓扑类只管"网络长什么样",主脚本负责"跑什么测试",输出文件负责"数据放哪里"。这样当你从简单链路实验切到复杂拓扑实验时,测试代码基本不用大改,只需要换一个 Topo 类。
第二条,每个节点命令的输出都要落盘。不管是用cmd()还是sendCmd(),只要输出可能超过一屏,就必须想办法存到文件,或者至少用字符串变量接住。你永远不知道一个命令在某个网络条件下会执行 1 秒还是 1 分钟,把输出接住,分析阶段才不至于失控。
第三条,在脚本里留一个调试开关。我习惯写一个DEBUG = True的开关,为真时调用CLI(net),为假时自动跑完全部测试。这样调试阶段可以手动看看拓扑状态,正式批量实验时则完全不依赖人工介入。
第四条,explore 永远慢一点。mininet 的pingAll()返回很快,但如果你在网络还没有完全稳定时立刻发大量流量,可能因为流表还没下发完毕导致误测。高负载实验之前,可以先跑一轮net.pingAll()或者time.sleep(1),让交换机把初始流表准备好再开始计时。
最后再分享一个小技巧:绝大多数 mininet 脚本的报错,其实都出在环境残留上。遇到莫名其妙的address already in use或者failed to create interface,先不要怀疑你脚本里的逻辑,先sudo mn -c清一遍环境再跑。这一个动作,至少能帮你解决四成以上的启动报错问题。