简介:Floodlight 是一种当前主流的开源软件定义网络(SDN)控制器,基于 Java 语言实现,以稳定、易用和完全开源著称,常被用于网络研究、实验平台搭建及创新应用开发。这份资源围绕 Floodlight 的安装部署展开,面向希望快速搭建控制器环境的初学者、网络工程师和高校学生,帮助其在 Ubuntu 等 Linux 系统中完成从环境评估、准备工作到实际部署的完整流程。压缩包采用 zip 格式,体积为 64.72MB,内容以安装部署文档为主,适合离线查阅并对照实践。目前已有 554 人学习下载,可作为 SDN 入门和实操的重要参考。文档不仅给出安装前准备和虚拟机环境搭建建议,还阐述了 Ubuntu 下的安装过程、注意事项与常见排错思路,读者可据此逐步构建可用的 Floodlight 运行环境,为后续网络实验、控制器二次开发或应用层研究提供有力支撑。
1. Floodlight是什么:SDN控制器里最容易被低估的那一个
很多刚接触SDN的同学,一听到控制器就只想到OpenDaylight、ONOS这些大而全的框架,反而把Floodlight当成一个“老古董”。但真要在自己的机器上把一个OpenFlow网络跑起来、看到流表怎么下发、想要动手改控制器行为,Floodlight是最容易上手的那个。它用Java写的,模块结构很直白,REST API对HTTP请求友好,从下载源码到交换机上线,花的时间通常不超过半小时。这篇文章写给两类人:一类是网络工程师,想在测试环境里验证OpenFlow转发逻辑;另一类是要做控制器二次开发或找毕业设计切入点的同学,需要快速理解一个可运行的控制器的骨架。我会从编译启动讲到自定义模块,每一步都给出实际命令和参数,希望帮你少走弯路。
2. 先跑起来:从源码构建到北向接口第一次调用
2.1 Floodlight的架构和模块化到底是怎么回事
Floodlight的核心是Controller模块,它负责维护OpenFlow连接、维护交换机列表、解析Packet-In消息。其他功能全部由模块承担,比如DeviceManager维护主机MAC和端口的关系,TopologyService维护链路状态,RestApiServer把内部服务通过HTTP暴露出来。一个模块可以实现多个接口,比如既提供服务给别人调用,自己也监听Packet-In事件。
模块之间的通信不靠消息总线,而是通过获取服务接口来调用。比如你的自定义模块想查交换机信息,可以在startUp里getProviderService(IOFSwitchService.class),然后调用getAllSwitchDpid()。这种设计比OSGi轻得多,调试时也能直接看对象。
Floodlight默认加载哪些模块,写在floodlightdefault.properties里。这个文件就是模块清单,一行一个全类名,或者用逗号分隔同一行的多个模块。启动时框架会按列表顺序依次实例化模块、调用startUp。所以如果你要改默认行为,基本思路就是“复制默认模块改一版,然后在properties里替换掉默认模块”。
2.2 构建与最小启动:git clone、mvn编译、启动参数
Floodlight本身是Maven项目,虽然仓库里也带了eclipse相关工程文件,但我建议直接用命令行,干净利落。先把代码拉下来:
git clone https://github.com/floodlight/floodlight.git cd floodlight mvn install -DskipTests-DskipTests会跳过单元测试。这里多说一句:Floodlight的测试代码依赖一些网络环境,直接跑mvn install可能因为某个测试拉不到资源而失败。所以首次构建我基本都会跳过测试,等后面需要验证某个模块行为时再单独跑测试类。
构建完成后,有两种启动方式。最省事的是用脚本:
./floodlight.sh这个脚本会检查java和maven环境,然后默认加载src/main/resources/floodlightdefault.properties。如果你需要换一份配置,用-cf参数指定:
java -jar target/floodlight.jar -cf /path/to/myfloodlight.properties不管是哪种方式,启动后看到Controller startup complete类似日志就算成功。Floodlight默认监听两个端口:OpenFlow协议端口是6653,REST API端口是8080。这两个端口在配置文件里都能改,但我建议初学阶段不要动,先固定下来,后面排查问题时会少一个变量。
2.3 第一次用REST API验证控制器活没活
Floodlight的北向接口走的是REST风格,所有路径都以/wm开头。启动后,先用一条最简单的请求确认REST API活着:
curl http://localhost:8080/wm/core/controller/switches/json返回[]是正常的,因为现在还没有交换机连上来。这个接口是查控制器已知的交换机列表,返回空数组不代表控制器挂了。接下来用Mininet在本地建一个虚拟的OpenFlow交换机,连到Floodlight:
sudo mn --controller=remote,ip=127.0.0.1,port=6653 --switch=ovs,protocols=OpenFlow13注意--protocols=OpenFlow13这个参数,它让Mininet里的OVS使用OpenFlow 1.3协议。Floodlight默认对OpenFlow 1.3的支持是开启的,两边版本一致才能握手成功。启动Mininet后再执行一次刚才的 curl,就能看到类似"dpid":"00:00:00:00:00:00:00:01"的交换机信息。
如果第二次curl还是返回空数组,先别急着怀疑Floodlight。在Mininet里执行pingall,如果主机之间能通,说明控制器已经介入了;再查本机防火墙是否屏蔽了6653端口。我自己遇到过最气人的一种情况是Floodlight启动日志里报了一个IOException,但进程还挂着,REST API也能访问,就是没有交换机上线。后来发现是我先启动了Mininet,然后才启动Floodlight,OVS已经尝试连接旧地址多次失败后进入了等待周期。所以顺序很重要:先控制器,后网络设备。
3. 下发真实转发规则:静态流表与动态Packet-Out
3.1 OpenFlow流表项的核心字段和Floodlight的匹配逻辑
REST API能查到交换机在线之后,下一个问题就是怎么让它干活。OpenFlow的本质是“匹配-动作”,控制器向交换机下发流表项,交换机收到数据包后按流表逐条匹配。流表项里最重要的几个字段是priority、match、actions和timeout。
priority决定多条流表项的匹配顺序,数值越大越先匹配。match是匹配条件,比如指定入端口、源MAC、目的MAC、以太网类型、IPv4五元组等。actions是匹配后要执行的动作,常见是output=端口号或者drop。timeout分为idle_timeout和hard_timeout,前者是空闲多久后删除,后者是装上后多久强制删除。
Floodlight本身不自动把所有流量都下发成精确流表。它有个默认的Forwarding模块,在收到Packet-In后按照自己的算法决定是泛洪、直接转发还是丢包。但这个默认行为对生产场景往往不够,比如你想固定让某个主机的流量只走某条链路,就需要自己下发流表。
3.2 用静态流表API手写一条二层转发规则
Floodlight有一个静态流表下发模块,叫StaticFlowEntryPusher,对应的REST接口是/wm/staticflowentrypusher/json。最小的一条规则是这样:
curl -X POST -d '{ "switch": "00:00:00:00:00:00:00:01", "name": "flow1", "priority": "400", "eth_type": "0x0806", "actions": "output=2" }' http://localhost:8080/wm/staticflowentrypusher/json这个JSON里,switch是交换机的DPID,必须和前面curl /wm/core/controller/switches/json查出来的一致;name是这个流表项在Floodlight内存里的唯一标识,下发同名规则会报错或覆盖;priority设置为400,用于保证这条ARP规则不被其他默认规则压住;eth_type是0x0806,即ARP协议;actions是转发动作,这里表示把匹配到的ARP包从交换机的端口2发出去。
逻辑上为什么要先下一条ARP规则?因为很多场景下两台主机之间通信,先要知道对方MAC地址,发出的是ARP请求。如果只有IP转发规则而没有ARP规则,ARP请求就会被控制器处理掉或者直接丢弃,结果是ping永远不通,但直接看IPv4流表又好像什么都正确。这是SDN实验里最常见的“半通”状态。
下发成功后,再用同样的方式加一条IPv4转发规则,比如eth_type=0x0800、ipv4_src=10.0.0.1、ipv4_dst=10.0.0.2、actions=output=2。这样从主机1发往主机2的流量有了精确匹配。注意这里匹配字段用了IP地址,说明静态流表不仅能做二层的MAC转发,也可以做简单的三层路由分流。
3.3 看流表命中效果:用REST查询和ovs-ofctl对照
规则下发之后,需要验证它到底有没有进交换机。先查Floodlight里的静态流表记录:
curl http://localhost:8080/wm/staticflowentrypusher/list/00:00:00:00:00:00:00:01/json返回JSON里能看到你刚才下发的flow1,同时还有status: pending或status: installed字段。如果一直是pending,说明Floodlight还没把规则推到交换机,通常是因为交换机连接断了,或者DPID没对上。
再看OVS侧实际安装的流表:
ovs-ofctl -O OpenFlow13 dump-flows br0这里br0是Mininet里默认创建的交换机名。对比两个输出,你会发现Floodlight静态流表的actions写法是output=2,而OVS显示的是OUTPUT:2,这是不同层面表示格式的差异。重点是看OVS里是否出现了对应arp或ip的流表项,以及n_packets和n_bytes计数器是否增长。如果OVS里没有这条流表,说明Floodlight和OVS之间的OpenFlow连接是通的,但下发消息被某个环节吞掉了,检查一下配置的高优先级规则是否有冲突。
还有一种情况:OVS里有流表,但计数器始终为0,说明流量没匹配到。这时候去主机的命名空间里执行ip neigh看ARP表是否正常,如果ARP表为空,基本就是ARP规则没覆盖到主机发出的ARP请求。
4. 自己写一个Floodlight模块:监听Packet-In的Java套路
4.1 为什么需要自定义模块:Floodlight默认行为与场景缺口
StaticFlowEntryPusher只能解决“预先知道要转发到哪个端口”的问题。但真实网络里,交换机第一次收到一个未知目的MAC的数据包时,会上报Packet-In给控制器,控制器要根据网络拓扑、主机位置、当前链路状态来决定怎么做。Floodlight默认的Forwarding模块能做基本转发,但如果你的场景是“某些MAC必须丢弃”“某些端口不做泛洪”,或者“所有Packet-In都要打印日志”,就得写自己的模块。
常见做法是写一个监听IOFMessageListener的模块。这个接口的核心方法只有一个receive,每当有OpenFlow消息进入控制器时,它会被调用。你在这个方法里对OFType.PACKET_IN做处理,返回Command.CONTINUE或Command.STOP。前者表示让后续监听器继续处理这条消息,后者表示你已经处理完了,后面监听器不用管。
理解CONTINUE和STOP的语义特别重要。很多初学者写的模块不生效,就是因为返回了CONTINUE,但默认的ForwardingListener在后面又做了一次处理,把你的规则覆盖了。如果你希望完全接管,就要拦截在更早的管道位置,返回STOP。
4.2 一个最小模块的骨架:实现IFloodlightModule和IOFMessageListener
写一个模块需要实现两个接口:IFloodlightModule负责模块生命周期,IOFMessageListener负责监听OpenFlow消息。下面是最小可运行的骨架:
package net.floodlightcontroller.myTest; import java.util.Collection; import java.util.Collections; import java.util.Map; import org.projectfloodlight.openflow.protocol.OFMessage; import org.projectfloodlight.openflow.protocol.OFType; import net.floodlightcontroller.core.FloodlightContext; import net.floodlightcontroller.core.IFloodlightProviderService; import net.floodlightcontroller.core.IOFMessageListener; import net.floodlightcontroller.core.IOFSwitch; import net.floodlightcontroller.core.module.FloodlightModuleContext; import net.floodlightcontroller.core.module.FloodlightModuleException; import net.floodlightcontroller.core.module.IFloodlightModule; import net.floodlightcontroller.core.module.IFloodlightService; import net.floodlightcontroller.restserver.IRestApiService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SimplePacketInModule implements IFloodlightModule, IOFMessageListener { private static final Logger log = LoggerFactory.getLogger(SimplePacketInModule.class); private IFloodlightProviderService floodlightProvider; private IRestApiService restApiService; @Override public Collection<Class<? extends IFloodlightService>> getModuleServices() { return Collections.emptyList(); } @Override public Map<Class<? extends IFloodlightService>, IFloodlightService> getServiceImpls() { return Collections.emptyMap(); } @Override public Collection<Class<? extends IFloodlightService>> getModuleDependencies() { return Collections.<Class<? extends IFloodlightService>>singleton(IFloodlightProviderService.class); } @Override public void init(FloodlightModuleContext context) throws FloodlightModuleException { floodlightProvider = context.getServiceImpl(IFloodlightProviderService.class); log.info("SimplePacketInModule init"); } @Override public void startUp(FloodlightModuleContext context) throws FloodlightModuleException { floodlightProvider.addOFMessageListener(OFType.PACKET_IN, this); log.info("SimplePacketInModule startUp complete"); } @Override public String getName() { return "SimplePacketInModule"; } @Override public boolean isCallbackOrderingPrereq(OFType type, String name) { return false; } @Override public boolean isCallbackOrderingPostreq(OFType type, String name) { return false; } @Override public Command receive(IOFSwitch sw, OFMessage msg, FloodlightContext cntx) { log.info("Received Packet-In from switch {}: {}", sw.getId(), msg); return Command.CONTINUE; } }这段代码的逻辑拆开讲:init阶段先取得核心服务IFloodlightProviderService的引用,这是后面注册监听器要用的。startUp阶段才是真正的“启动完成”,在这里调用addOFMessageListener把当前类注册为PACKET_IN消息的监听者。getModuleDependencies告诉框架当前模块依赖哪些服务,框架在加载模块时会先初始化依赖。
里面有几个方法必须实现但通常不需要写逻辑,比如isCallbackOrderingPrereq和isCallbackOrderingPostreq,这两个方法用来声明监听器的回调顺序,默认返回false就可以。getName返回的字符串会出现在Floodlight的日志和回调链路里,建议取一个容易认的名字。
receive方法里,我暂时只打日志,返回CONTINUE。这样你就能看到交换机上报的每一个Packet-In,同时不影响原有转发逻辑。如果你想自己处理,可以在receive里解析OFPacketIn消息,拿到入端口和Ethernet帧,然后调用IOFSwitch.write下发Packet-Out。但这一步需要你对OpenFlow协议消息结构比较熟,第一次写模块建议先只监听、只打日志,把链路跑通再逐步加深。
4.3 把模块接进Floodlight的配置:floodlightdefault.properties和模块加载顺序
Java代码写完后,还要让Floodlight认识它。否则编译不会把它打进控制器。打开src/main/resources/floodlightdefault.properties,找到floodlight.modules这一行:
floodlight.modules = net.floodlightcontroller.core.internal.FloodlightProvider,\ net.floodlightcontroller.restserver.RestApiServer,\ net.floodlightcontroller.myTest.SimplePacketInModule注意我的做法是把自定义模块加在一行模块列表的最后。为什么要刻意放到最后?因为Floodlight按这个列表顺序实例化模块,如果你的模块要依赖前面的服务,就必须放在它们之后。比如SimplePacketInModule依赖FloodlightProvider和RestApiServer,把它放前面,启动时依赖服务还没被初始化,直接就会在context.getServiceImpl那一步抛异常。
模块自己只能通过@Override注解表示是net.floodlightcontroller.myTest包下的类,真正决定加载使用的是properties里的全类名。所以类名和包名必须写对,一个字母错都会导致ClassNotFoundException,但这里有个坑:Floodlight启动时如果找不到某个模块,不会立即退出,而是打印一条警告日志继续跑。看起来控制器活着,可你的逻辑就是不执行。所以配置完模块一定要看启动日志里有没有Module not found类似的警告。
重新编译也很简单,在工程根目录执行mvn install -DskipTests,然后重启Floodlight。启动日志里如果出现SimplePacketInModule startUp complete,说明模块已经加载。这时在Mininet里pingall,日志里就会打印每一条Packet-In的交换机ID和消息内容。
5. Floodlight常见问题排查:五个血泪踩坑现场
5.1 控制器连接不上:端口、协议和防火墙
现象:Mininet启动时提示无法连接remote controller,pingall全部丢包,Floodlight日志里也看不到交换机加线通知。
原因:最常见的三种。一是Floodlight监听端口不是6653,你在配置里改了端口但Mininet里还写老端口;二是系统防火墙屏蔽了6653,但本地回环接口有时也会受防火墙规则影响;三是Mininet里的OVS默认用OpenFlow 1.0去连接,而Floodlight配置只启用了OpenFlow 1.3,协议协商失败。
解决:先确认Floodlight监听端口,用netstat -an | grep 6653,如果没监听,去看日志里到底绑定的是哪个端口。然后在Mininet里用--controller=remote,ip=127.0.0.1,port=6653显式指定的同时,加--switch=ovs,protocols=OpenFlow13。防火墙方面,测试阶段可以直接sudo ufw disable或者只放行6653,但生产环境不要这样干,按最小原则开端口。
5.2 REST API路径或请求格式返回异常
现象:curl 请求返回404 Not Found,或者400 Bad Request,但REST接口文档里明明有这个路径。
原因:Floodlight的REST API路径前缀是/wm,漏掉这个前缀会404。另外更多时候是JSON格式问题,比如actions字段写成"output:2",Floodlight的解析器严格按output=2的格式读取,格式错误直接返回400。
解决:规范路径和请求头。路径是http://<controller-ip>:8080/wm/staticflowentrypusher/json,务必带/wm。POST请求加上-H "Content-Type: application/json",且JSON双引号不要被shell转义出错。我习惯把JSON写到独立文件里,用curl -X POST -d @flow.json方式提交,避免终端转义带来的玄学问题。
5.3 静态流表下发成功但Ping不通:ARP拦截和优先级
现象:/wm/staticflowentrypusher/list里显示规则installed,OVS里也能看到对应流表,但主机之间就是ping不通。
原因:这是SDN实验里翻车率最高的问题。只下发了IPv4转发规则,没有下发ARP规则。控制器收到ARP请求后,如果在自己的转发模块里处理掉,但你的自定义模块又拦截或丢弃了,主机的ARP表就一直学不到MAC地址。另一个常见原因是优先级不够,Floodlight默认规则里可能有一条drop-all,优先级和你的规则相同或更高,导致你的规则永不匹配。
解决:ARP规则和IP规则一起下,eth_type分别写0x0806和0x0800。优先级统一设置一个高于默认规则的值,比如400。同时用ovs-ofctl dump-flows查看所有规则,确认没有一条priority=0, actions=drop压在你上面。如果发现默认drop,可以把你的静态规则priority改成500。
5.4 自定义模块不生效:配置没加载或类名拼错
现象:重启Floodlight后,日志里没有任何自定义模块的打印,getName()里设置的名字也不出现在日志中,连报错都没有。
原因:大概率是floodlight.modules里没有写你的类,或者写了但类名拼错。最气人的是Floodlight对找不到模块不报警,它只会在加载列表里跳过。另外还有一个原因是你改的properties文件没有被加载,比如你用了-cf指定了一个新文件,但文件里没有floodlight.modules这条配置。
解决:启动时加一个-Djava.util.logging.config.file或看日志时用grep -i module筛一下。我通常会在startUp方法里写一行强日志输出,因为模块是否加载成功,日志是最直接的证据。如果日志里没有,就先检查properties文件是否被加载。可以用ps aux | grep floodlight看启动命令激活参数,确认-cf指向的确实是你的文件。
5.5 试试就逝世:Java环境与依赖冲突
现象:mvn install时报一大堆编译错误,或者启动Floodlight时直接NoClassDefFoundError/NoSuchMethodError。
原因:Floodlight的代码历史比较长,对JDK版本比较敏感。我现在手头的经验是JDK 8最稳,JDK 11在部分老分支会出现javax.xml.bind缺失,JDK 17直接编译失败。Maven依赖也可能因为网络问题下载了损坏的jar包。
解决:统一用JDK 8,安装后设置JAVA_HOME。编译失败时先rm -rf ~/.m2/repository后重试,虽然慢但能解决大部分损坏依赖问题。如果项目里有gradle或ant文件,优先用项目自带的构建说明里的工具链,不要自己换新版本。
6. 让Floodlight真正可用:拓扑发现、链路故障和性能参数
把规则和模块都跑通之后,Floodlight还差一步才算真正可用:自动感知链路变化。Floodlight默认通过LLDP做拓扑发现,交换机每隔一段时间上报链路信息,控制器把链路关系存在TopologyService里。你可以在REST接口里看链路状态:
curl http://localhost:8080/wm/topology/links/json返回的JSON里会列出每条链路的源交换机DPID、源端口、目的交换机DPID和目的端口。如果链路断了,条目会从列表里消失。这个机制在处理链路故障时很有用,但要注意LLDP报文是控制器主动注入到交换机再通过数据通道泛洪的,如果你的交换机把LLDP当成普通数据包,需要检查是否配置了丢弃规则。
性能调优方面,我通常关注两个点。第一是JVM内存堆大小,默认启动脚本给的不一定够,大拓扑场景下可以显式指定:
java -Xms512m -Xmx2g -jar target/floodlight.jar第二是Java启动参数里加入-Djava.net.preferIPv4Stack=true,避免在某些双栈环境里控制器监听IPv6地址而交换机的IPv4连接请求落空。
我自己的习惯是:每次改完代码,先启动控制器看日志,再启动Mininet,然后再curl一次REST确认交换机在线。三步都通过才进行下一步,避免把环境问题和新代码问题混在一起浪费时间。希望这篇笔记能帮你绕开我踩过的那些坑,顺利把Floodlight用起来。
本文还有配套的精品资源,点击获取