简介:这份PDF面向高校研究生与网络方向学习者,聚焦软件定义网络实验课程的教学设计难题,如实验科目匮乏、硬件交换设备昂贵且难以规模化部署、环境灵活性不足、初学者上手门槛高等。作者结合Mininet轻量级模拟平台,配合POX、Kinetic、Pyretic等控制器,系统阐述SDN网络环境搭建、特定拓扑绘制、网络分割、防火墙编写等实验科目的设计思路,并给出基础型、验证型、综合型三类共11个实验的模块化组织方案,兼顾不同层次学生的个性化培养需求。资源包为单个PDF文件,约174KB,内容涵盖问题分析、设计方法、Mininet特性介绍与教学效果说明,结构完整、条理清晰。目前已有104人学习,适合SDN入门者、课程设计者及实验教学人员参考,可帮助读者快速理解模拟环境下的实验体系搭建逻辑,为后续动手实践与课程开发提供直接借鉴。
1. 从一张 PDF 标题说起:Mininet 模拟 SDN 到底能跑出什么名堂
很多人第一次看到「基于 Mininet 模拟环境的软件定义网络实验课程设计」这个标题,脑子里蹦出来的画面是:装个虚拟机、敲几行命令、截几张图、交一份报告。真按这个思路做,最后大概率是拓扑能起来,但一问「控制器怎么下发流表」「链路断了流量怎么绕」就答不上来。Mininet 的价值不在于「模拟一个网络」,而在于它把 SDN 里最核心的那条链路——控制器通过 OpenFlow 协议操控数据平面——压缩到一台笔记本上就能反复验证。你可以在里面跑通一个自定义转发逻辑,抓包看到 Packet-In 消息,改一条流表看吞吐变化,这些在真实交换机上要排队申请资源的事,在 Mininet 里几秒钟就能重来一次。这篇笔记面向的是要拿 Mininet 做 SDN 课程设计的人,也适合想低成本验证 OpenFlow 应用的在职工程师。接下来我会按「环境怎么搭、拓扑怎么建、控制器怎么接、流表怎么调、坑怎么躲」的顺序,把一套能复现的实验路径讲清楚。
2. 环境搭建与版本选型:为什么我劝你别用最新版
2.1 Mininet 安装方式对比与选择理由
Mininet 的安装方式直接决定了你后面调试的难度。常见做法有三种:源码安装、apt 安装、以及官方提供的虚拟机镜像。源码安装最灵活,能改内核模块参数,但依赖 Python 版本和 Open vSwitch 的编译,新手容易卡在configure阶段。apt 安装最省事,apt install mininet一行搞定,但 Ubuntu 仓库里的版本往往偏旧,OpenFlow 协议支持可能停在 1.0 或 1.3 的早期实现。官方虚拟机镜像开箱即用,但镜像里的工具链版本被锁死,你想升级 Ryu 或 ONOS 时会遇到依赖冲突。
我一般会推荐:课程设计用 apt 安装 + 手动升级 Open vSwitch,理由是这样你能看到版本号的变化,知道哪个组件在起作用。如果你只是要快速验证一个控制器逻辑,直接用官方镜像,省掉环境折腾的时间。下面是一套在 Ubuntu 20.04 上验证过的安装流程。
# 更新源并安装 Mininet 核心包 sudo apt update sudo apt install -y mininet mininet-doc # 检查 Mininet 版本,确认是否可用 mn --version # 安装 Open vSwitch 用户态工具,用于查看流表 sudo apt install -y openvswitch-switch openvswitch-common # 验证 OVS 版本,OpenFlow 1.3 需要 OVS 2.3 以上 ovs-vsctl --version这段脚本的逻辑是:先装 Mininet 本体,再装 OVS 工具。mn --version输出的是 Mininet 的版本号,不是 OpenFlow 版本。ovs-vsctl --version输出的第二行才是 OVS 版本,它决定了你能否使用 OpenFlow 1.3 的group和meter特性。参数上唯一需要注意的是-y不能省,否则安装过程会卡在交互确认。如果你在虚拟机里跑,建议给至少 2GB 内存和 2 个 CPU 核心,否则启动 10 个以上交换机时 OVS 进程会互相抢资源。
2.2 控制器选型:Ryu、ONOS、Floodlight 怎么挑
控制器是 SDN 实验的大脑。Ryu 是 Python 写的,代码量少,适合自己改逻辑,比如写一个带最短路径计算的自定义控制器。ONOS 是 Java 写的,集群能力强,适合做大规模拓扑实验,但启动慢,配置文件多。Floodlight 也是 Java 系,REST API 比较全,适合做北向接口实验。课程设计里最常被选的是 Ryu,因为它的simple_switch_13.py例子几乎就是 OpenFlow 1.3 的教科书实现,你可以在它基础上加自己的转发规则。
选 Ryu 的另一个理由是调试方便。你可以在控制器代码里直接print出收到的 Packet-In 消息,看到match字段和actions字段的原始结构。ONOS 的日志分散在多个 Karaf 日志文件里,新手找一条流表下发记录要翻半天。下面是用 pip 安装 Ryu 的命令,注意 Python 版本。
# Ryu 对 Python 3.6 以上支持较好,Ubuntu 20.04 默认 Python 3.8 sudo apt install -y python3-pip pip3 install ryu # 验证安装,查看 Ryu 版本和可用模块 ryu --version ryu-manager --help | head -20pip3 install ryu会同时装上 eventlet 和 oslo.config 等依赖。ryu --version输出的是 Ryu 框架版本,不是 OpenFlow 版本。ryu-manager --help列出的是所有可加载的 Ryu 应用模块,比如ryu.app.simple_switch_13就是最常用的那个。如果你在安装时遇到eventlet超时,换用国内 pip 源,或者指定eventlet==0.30.2这个较稳定的版本。
3. 拓扑构建与 OpenFlow 通道打通:从mn命令到流表下发
3.1 用mn命令快速拉起一个可调试的拓扑
Mininet 自带的mn命令能直接生成常见拓扑,比如--topo=tree,2生成两层的树形拓扑,--topo=single,4生成一个交换机挂四台主机。但课程设计往往需要自定义拓扑,比如三个交换机环形连接,或者一个交换机下挂不同网段的主机。这时候用 Python 脚本调 Mininet 的 API 更灵活。
下面是一个自定义拓扑脚本,创建一个线性三交换机拓扑,每个交换机下挂一台主机,并指定远程控制器地址。
# custom_topo.py from mininet.topo import Topo from mininet.net import Mininet from mininet.node import RemoteController, OVSKernelSwitch from mininet.cli import CLI from mininet.log import setLogLevel class Linear3Topo(Topo): def build(self): # 创建三台交换机 s1 = self.addSwitch('s1') s2 = self.addSwitch('s2') s3 = self.addSwitch('s3') # 创建三台主机,分别指定 IP h1 = self.addHost('h1', ip='10.0.0.1/24') h2 = self.addHost('h2', ip='10.0.0.2/24') h3 = self.addHost('h3', ip='10.0.0.3/24') # 交换机之间线性连接 self.addLink(s1, s2) self.addLink(s2, s3) # 主机挂到对应交换机 self.addLink(h1, s1) self.addLink(h2, s2) self.addLink(h3, s3) if __name__ == '__main__': setLogLevel('info') topo = Linear3Topo() # 指定远程控制器,Ryu 默认监听 6633 和 6653 net = Mininet(topo=topo, controller=RemoteController, switch=OVSKernelSwitch) net.start() CLI(net) # 进入交互式命令行,方便手动测试 net.stop()这段代码的关键点有三个。第一,RemoteController告诉 Mininet 不要启动自带的ovs-controller,而是去连接外部控制器。第二,OVSKernelSwitch表示使用内核态的 Open vSwitch,性能比用户态好,但需要 OVS 内核模块已加载。第三,CLI(net)让你进入一个类似mininet>的提示符,可以在里面执行h1 ping h2、sh ovs-ofctl dump-flows s1等命令。参数上,ip='10.0.0.1/24'里的/24不能省,否则主机不会自动配置路由。
运行这个脚本之前,先在一个终端启动 Ryu 控制器:
# 启动 Ryu 的简单交换机应用,监听 6653 端口 ryu-manager ryu.app.simple_switch_13然后在另一个终端运行sudo python3 custom_topo.py。注意必须用sudo,因为 Mininet 要创建网络命名空间和虚拟网卡。启动后你会看到s1、s2、s3依次连接控制器,每个交换机上会打印connected状态。
3.2 验证 OpenFlow 通道:ovs-ofctl与dump-flows的用法
拓扑起来之后,第一件事是确认交换机和控制器的 OpenFlow 通道真的通了。在 Mininet 的 CLI 里执行sh ovs-ofctl show s1,你会看到类似下面的输出:
OFPT_FEATURES_REPLY (xid=0x2): dpid:0000000000000001 n_tables:254, n_buffers:256 capabilities: FLOW_STATS TABLE_STATS PORT_STATS QUEUE_STATS ARP_MATCH_IP actions: output enqueue set_vlan_vid set_vlan_pcp strip_vlan mod_dl_src mod_dl_dst mod_nw_src mod_nw_dst mod_tp_src mod_tp_dst 1(s1-eth1): addr:... config: 0 state: 0 2(s1-eth2): addr:... config: 0 state: 0dpid是交换机的唯一标识,n_tables:254表示支持多级流表。如果这里显示dpid全零或者端口列表为空,说明交换机没有正确连接到 OVS 数据库。常见原因是 OVS 服务没启动,或者ovs-vsctl show里看不到网桥。
接着执行sh ovs-ofctl dump-flows s1,你会看到控制器下发的默认流表。Ryu 的simple_switch_13会下发一条table=0, priority=0, actions=CONTROLLER的兜底规则,意思是所有未知流量都上送控制器。当你执行h1 ping h2时,控制器会收到 Packet-In,然后计算路径并下发新的流表项。你可以再次dump-flows看到新增的priority=1规则,匹配ip,nw_src=10.0.0.1,nw_dst=10.0.0.2,动作是output:2。
这里有一个容易翻车的点:ovs-ofctl命令必须在 Mininet 的 CLI 里用sh前缀执行,或者先sudo su进入 root 再执行。直接在你的普通终端里敲ovs-ofctl dump-flows s1会报s1不存在,因为s1是 Mininet 创建的虚拟网桥,只在 Mininet 的命名空间里可见。
4. 流表编程与转发逻辑验证:让数据包按你的规则走
4.1 写一个自定义 Ryu 应用:从 Packet-In 到 Flow-Mod
Ryu 的simple_switch_13只做了最基本的 MAC 学习转发。课程设计里通常要求你实现更复杂的逻辑,比如基于 IP 的转发、基于端口的阻断、或者带 VLAN 标签的隔离。下面是一个自定义 Ryu 应用,实现「只允许 h1 和 h2 通信,h3 的流量全部丢弃」的访问控制。
# acl_switch.py from ryu.base import app_manager from ryu.controller import ofp_event from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER from ryu.controller.handler import set_ev_cls from ryu.ofproto import ofproto_v1_3 from ryu.lib.packet import packet, ethernet, ipv4 class ACLSwitch(app_manager.RyuApp): OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION] def __init__(self, *args, **kwargs): super(ACLSwitch, self).__init__(*args, **kwargs) self.mac_to_port = {} @set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER) def switch_features_handler(self, ev): datapath = ev.msg.datapath ofproto = datapath.ofproto parser = datapath.ofproto_parser # 默认流表:所有未知流量上送控制器 match = parser.OFPMatch() actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER, ofproto.OFPCML_NO_BUFFER)] self.add_flow(datapath, 0, match, actions) def add_flow(self, datapath, priority, match, actions, buffer_id=None): ofproto = datapath.ofproto parser = datapath.ofproto_parser inst = [parser.OFPInstructionActions(ofproto.OFPIT_APPLY_ACTIONS, actions)] if buffer_id: mod = parser.OFPFlowMod(datapath=datapath, buffer_id=buffer_id, priority=priority, match=match, instructions=inst) else: mod = parser.OFPFlowMod(datapath=datapath, priority=priority, match=match, instructions=inst) datapath.send_msg(mod) @set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER) def packet_in_handler(self, ev): msg = ev.msg datapath = msg.datapath ofproto = datapath.ofproto parser = datapath.ofproto_parser in_port = msg.match['in_port'] pkt = packet.Packet(msg.data) eth = pkt.get_protocol(ethernet.ethernet) ip_pkt = pkt.get_protocol(ipv4.ipv4) # 只处理 IPv4 包,其他丢弃 if not ip_pkt: return src_ip = ip_pkt.src dst_ip = ip_pkt.dst # 访问控制:h3 的 IP 是 10.0.0.3,禁止它参与通信 if src_ip == '10.0.0.3' or dst_ip == '10.0.0.3': # 下发一条丢弃流表,优先级高于默认流表 match = parser.OFPMatch(eth_type=0x0800, ipv4_src=src_ip, ipv4_dst=dst_ip) self.add_flow(datapath, 100, match, []) # 空动作表示丢弃 return # 正常转发:学习源 MAC 和端口 self.mac_to_port.setdefault(datapath.id, {}) self.mac_to_port[datapath.id][eth.src] = in_port if eth.dst in self.mac_to_port[datapath.id]: out_port = self.mac_to_port[datapath.id][eth.dst] else: out_port = ofproto.OFPP_FLOOD actions = [parser.OFPActionOutput(out_port)] match = parser.OFPMatch(in_port=in_port, eth_dst=eth.dst) self.add_flow(datapath, 10, match, actions) # 把当前包也转发出去 out = parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id, in_port=in_port, actions=actions, data=msg.data) datapath.send_msg(out)这段代码的核心逻辑在packet_in_handler里。当控制器收到 Packet-In 时,先解析出 IP 层,如果源或目的 IP 是10.0.0.3,就下发一条空动作的流表,优先级设为 100,这样后续匹配到的包直接在交换机里被丢弃,不会再上送控制器。对于正常流量,用mac_to_port做 MAC 学习,然后下发priority=10的转发流表。参数上,priority越大越优先,OFPP_FLOOD表示泛洪,OFPCML_NO_BUFFER表示不缓存整个包,只上送包头。
启动这个应用用ryu-manager acl_switch.py,然后重新运行拓扑脚本。在 Mininet CLI 里执行h1 ping h2应该通,h1 ping h3应该不通。你可以用sh ovs-ofctl dump-flows s1看到两条流表:一条priority=100的丢弃规则,一条priority=10的转发规则。
4.2 用iperf和ping验证转发性能与连通性
流表下发之后,需要验证实际转发效果。ping只能告诉你通不通,iperf能告诉你带宽和丢包。在 Mininet CLI 里,先让 h2 启动 iperf 服务端:
h2 iperf -s &然后在 h1 上跑客户端:
h1 iperf -c 10.0.0.2 -t 10 -i 1你会看到每秒的带宽报告。如果带宽远低于预期(比如只有几 Mbps),常见原因是 OVS 走了用户态转发,或者流表没有正确命中导致每个包都上送控制器。检查方法是sh ovs-ofctl dump-flows s1看有没有n_packets计数在增长。如果所有包都走priority=0的默认流表,说明你的自定义流表没有匹配上,需要检查match字段里的eth_type和ipv4_src是否写对。
另一个验证手段是sh ovs-ofctl dump-ports s1,看每个端口的rx和tx字节数。如果rx有增长但tx为零,说明包被丢弃了,对应你下的丢弃流表。这个命令在排查「为什么 ping 不通」时特别有用,能快速定位是入口没收到还是出口没发出去。
5. 避坑与排查:Mininet 实验里最容易翻车的五个地方
5.1 现象:mn启动时报Unable to contact the remote controller
原因:Ryu 控制器没有启动,或者监听端口不是 Mininet 默认连接的 6653。Mininet 的RemoteController默认连127.0.0.1:6653,而 Ryu 默认监听6633和6653两个端口,但如果你用--ofp-tcp-listen-port改了端口,两边就对不上。
解决:先确认 Ryu 进程在跑,ps aux | grep ryu能看到ryu-manager。然后确认端口,netstat -tlnp | grep 6653应该显示LISTEN。如果端口不对,在拓扑脚本里指定controller=RemoteController('c0', ip='127.0.0.1', port=6653)。
5.2 现象:h1 ping h2显示Destination Host Unreachable
原因:主机 IP 配错了网段,或者 ARP 请求没有被正确转发。Mininet 里主机默认在同一个子网,但如果你的addHost里写了ip='10.0.0.1/24'而另一台是ip='10.0.1.2/24',它们就不在同一网段,ARP 广播会被交换机丢弃。
解决:用h1 ifconfig和h2 ifconfig确认 IP 和掩码。如果网段不同,要么改 IP,要么在控制器里加路由逻辑。另外检查sh ovs-ofctl dump-flows s1里有没有 ARP 相关的流表,Ryu 的simple_switch_13会把 ARP 广播泛洪,但如果你的自定义应用只处理 IPv4,ARP 包会被丢弃,导致 ping 不通。
5.3 现象:ovs-ofctl dump-flows输出为空
原因:交换机没有连接到控制器,或者 OVS 数据库没有正确配置。Mininet 启动时会自动创建网桥并设置fail-mode=secure,但如果 OVS 服务在 Mininet 启动后才启动,网桥可能没有连上控制器。
解决:在 Mininet CLI 里执行sh ovs-vsctl show,看每个网桥的Controller字段是否指向tcp:127.0.0.1:6653。如果是空的,手动加上:sh ovs-vsctl set-controller s1 tcp:127.0.0.1:6653。然后sh ovs-ofctl dump-flows s1应该能看到默认流表。
5.4 现象:iperf带宽只有几 Mbps,远低于链路容量
原因:流表没有命中,每个包都上送控制器,控制器处理速度成为瓶颈。或者 OVS 使用了用户态 datapath,没有走内核模块。
解决:先sh ovs-ofctl dump-flows s1看n_packets计数。如果默认流表的计数很大,说明自定义流表没生效。检查match字段是否包含了in_port和eth_dst,这两个字段在 MAC 学习转发里是必须的。另外确认ovs-vsctl get Open_vSwitch . datapath_type返回的是system而不是netdev,netdev是用户态,性能差很多。
5.5 现象:重启 Mininet 后流表丢失,需要重新下发
原因:Mininet 每次启动都会创建新的 OVS 网桥,旧的流表随网桥删除而消失。这是正常行为,不是 bug。
解决:如果你需要持久化流表,可以在拓扑脚本里用net.start()之后手动调用ovs-ofctl add-flow命令,或者把流表下发逻辑写进 Ryu 应用的switch_features_handler里,这样每次交换机连接时自动下发。不要试图用ovs-vsctl save保存流表,Mininet 的网桥是临时创建的,保存了也恢复不了。
6. 进阶技巧:用mininet的link命令模拟链路故障与恢复
课程设计里如果只做静态转发,很难体现 SDN 的「可编程」优势。一个能加分的实验是:模拟链路中断,观察控制器如何重新计算路径并下发新流表。Mininet 提供了link命令,可以在运行时改变链路状态。
在 Mininet CLI 里,先确认拓扑是线性的h1-s1-s2-s3-h3。执行h1 ping h3应该通。然后执行:
link s2 s3 down这条命令会把 s2 和 s3 之间的链路断开。此时h1 ping h3应该超时。如果你用的控制器有最短路径计算逻辑(比如 Ryu 的simple_switch_13不带,但你可以自己加),控制器会收到端口状态变化消息,然后重新计算路径。对于线性拓扑,s2 和 s3 之间只有一条链路,断了就没有备用路径,所以 ping 不通是正常的。但你可以观察sh ovs-ofctl dump-flows s2里流表的变化,看控制器有没有下发新的丢弃规则或者修改动作。
如果你想验证路径恢复,执行link s2 s3 up,链路恢复后,控制器应该重新下发转发流表,h1 ping h3恢复连通。这个过程中,你可以用sh ovs-ofctl dump-flows s2前后对比,看到流表的n_packets计数从增长到停止再到增长。
更进阶的做法是写一个带 Dijkstra 算法的 Ryu 应用,维护全局拓扑图,当收到EventOFPPortStatus消息时更新图并重新计算所有主机对之间的路径。这个逻辑大概需要 200 行 Python 代码,核心是监听EventOFPPortStatus和EventOFPSwitchFeatures,用networkx库做图计算。我一般会建议先跑通静态转发,再加动态路径计算,否则调试时会分不清是拓扑问题还是算法问题。
最后说一个我踩过的坑:Mininet 的link down命令只是把端口状态设为down,但 OVS 流表里已有的output动作不会自动失效。如果控制器没有及时下发新流表,数据包会被发到一个已经 down 的端口,然后被内核丢弃。你会在ovs-ofctl dump-ports里看到tx计数在涨但rx在另一端不涨。这时候需要检查控制器有没有处理EventOFPPortStatus,以及流表的idle_timeout是否设置得太长。我习惯把转发流表的idle_timeout设为 30 秒,这样即使控制器没反应过来,旧流表也会自动过期,避免流量黑洞。希望帮到你。
本文还有配套的精品资源,点击获取