生动的SDN基础内容介绍(三)--Ryu控制器
控制器
之前介绍完了南向协议OpenFlow,这次说一说Ryu。因为毕设的时候师兄推荐了Ryu,再考虑到Python方便开发,我也就继续用Ryu了。但是后续发现好像支持Ryu开发的框架相较Floodlight、OpenDaylight、ONOS没那么多(但也可能只是我没找到)。
首先非常强烈推荐这位大哥的博客:
https://www.cnblogs.com/ssyfj/p/11730362.html
当时学习的时候多亏了这位大哥,大哥博客里面有很多关于SDN的实验,讲解的很细致。
其次要强烈推荐Ryu官方的手册:
https://ryu.readthedocs.io/en/latest/ofproto_v1_3_ref.html
有不懂的就翻手册。
Ryu的目录
Ryu的安装网上有很多介绍了,这里就不说了。
我用的是Ubuntu20.04,Ryu4.34的目录(ryu/ryu)如下所示:
1、base:
里面有一个很重要的文件:app_manager.py,其作用是Ryu应用的管理中心。用于加载RYU应用程序,接受从APP发送过来的信息,同时也完成消息的路由。其主要的函数有app注册、注销、查找、并定义了Ryu APP基类,定义了Ryu APP的基本属性。
Ryu APP是啥,其实就是用Ryu控制器编写的应用代码,我们在Ryu框架的基础上进行开发,注意这里APP与应用平面不同,我们现在讨论的都是控制平面。
2、controller:
实现控制器和交换机之间的连接和事件处理。
3、lib:
实现了网络的基本协议以及基本的数据结构。
ofctl_v1_3.py中的代码有匹配的函数,可对照之前OpenFlow介绍中显示的匹配域:
4、ofproto:
这里有两类文件,一类是协议的数据结构定义,另外一类是协议的解析,即处理的函数。
如ofproto_v1_3.py是1.3版本的OpenFlow协议数据结构的定义,而ofproto_v1_3_parser.py则定义了1.3版本的协议编码和解码。
5、topology:
这里定义了交换机的数据结构和一些event。
可以看到这里的Switches class将交换机基本的内容已经定义好了:
定义了可支持的OpenFlow的版本、时间、链路、主机、端口等。
6、contrib:
存放了开源社区贡献者的代码。
7、cmd:
为控制器的执行创建环境,执行命令行的命令。
Ryu的学习
Ryu的目录里有一个app目录,这里存放了开发者给我们写好了的一些app。
simple_switch_13.py
这里最要关注的是simple_switch_13.py,这个文件很适合刚接触Ryu的人来熟悉Ryu。
它基于OpenFlow1.3协议实现了简单的学习交换机,为啥叫学习呢,因为最开始交换机不知道去往MAC1的数据包该往哪儿发,然后接受到了一次之后就把要转发的端口记录下来了,下次就知道去MAC1的数据包要从哪个端口走了。其实就是实现了平常咱们用的二层交换机的功能。
关于这个文件的源码讲解,这篇博客还是很好的:
https://www.cnblogs.com/kl107/p/13138568.html
当然还有大哥的这篇:
https://www.cnblogs.com/ssyfj/p/11748465.html
我把我自己的一些注释写在下面的代码里
# Copyright (C) 2011 Nippon Telegraph and Telephone Corporation.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from ryu.base import app_manager #app的基类
from ryu.controller import ofp_event #OpenFlow的事件
from ryu.controller.handler import CONFIG_DISPATCHER, MAIN_DISPATCHER
from ryu.controller.handler import set_ev_cls
from ryu.ofproto import ofproto_v1_3#应用的是OpenFlow1.3协议
from ryu.lib.packet import packet
from ryu.lib.packet import ethernet #引入了基本的网络协议
from ryu.lib.packet import ether_types
class SimpleSwitch13(app_manager.RyuApp):
OFP_VERSIONS = [ofproto_v1_3.OFP_VERSION] #应用的版本是OpenFlow1.3
def __init__(self, *args, **kwargs):
super(SimpleSwitch13, self).__init__(*args, **kwargs)
self.mac_to_port = {} #MAC-端口对
@set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
#起到了事件响应的功能
#当接收到feature-reply报文后执行下面的函数下发table-miss表项
def switch_features_handler(self, ev):
datapath = ev.msg.datapath #获取数据通路即交换机
ofproto = datapath.ofproto #获取数据通路的协议
parser = datapath.ofproto_parser #对协议进行解析
# install table-miss flow entry
#
# We specify NO BUFFER to max_len of the output action due to
# OVS bug. At this moment, if we specify a lesser number, e.g.,
# 128, OVS will send Packet-In with invalid buffer_id and
# truncated packet data. In that case, we cannot output packets
# correctly. The bug has been fixed in OVS v2.1.0.
match = parser.OFPMatch() #全匹配
actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
ofproto.OFPCML_NO_BUFFER)]
#设置table-miss表项的动作:将数据包发至控制器
self.add_flow(datapath, 0, match, actions)
#将0优先级、全匹配、动作为“将数据包发至控制器”的table-miss表项下发给交换机。
#这样以后交换机不知道咋处理的数据包会自动发至控制器。
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)
#在MAIN_DISPATCHER阶段碰到EventOFPPacketIn事件的时候执行下面的函数。
#即控制器收到数据包的时候执行下面的函数。
#控制器什么时候会收到数据包呢:就是交换机现有的流表项无法与数据包匹配,利用table-miss表项交给控制器处理。
def _packet_in_handler(self, ev):
# If you hit this you might want to increase
# the "miss_send_length" of your switch
if ev.msg.msg_len < ev.msg.total_len:
self.logger.debug("packet truncated: only %s of %s bytes",
ev.msg.msg_len, ev.msg.total_len)
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_protocols(ethernet.ethernet)[0]
if eth.ethertype == ether_types.ETH_TYPE_LLDP:
# ignore lldp packet
return
dst = eth.dst #获取数据包的目的MAC地址
src = eth.src #获取数据包的源MAC地址
dpid = format(datapath.id, "d").zfill(16)
self.mac_to_port.setdefault(dpid, {})
self.logger.info("packet in %s %s %s %s", dpid, src, dst, in_port)
# learn a mac address to avoid FLOOD next time.
#设置该交换机的MAC-端口对。
#即将数据包的源MAC地址与进入的端口相对应,完成了学习。
self.mac_to_port[dpid][src] = in_port
#如果知道目的MAC地址与哪个出的端口相对应,那么就直接转发。
#否则需要泛洪寻找出的端口。
if dst in self.mac_to_port[dpid]:
out_port = self.mac_to_port[dpid][dst]
else:
out_port = ofproto.OFPP_FLOOD
#给流表项赋予动作:遇到该目的MAC地址的时候从这个端口转发。
actions = [parser.OFPActionOutput(out_port)]
# install a flow to avoid packet_in next time
if out_port != ofproto.OFPP_FLOOD:
#创建匹配域:以后再遇到这种情况就知道从哪个端口转发了。
match = parser.OFPMatch(in_port=in_port, eth_dst=dst, eth_src=src)
# verify if we have a valid buffer_id, if yes avoid to send both
# flow_mod & packet_out
if msg.buffer_id != ofproto.OFP_NO_BUFFER:
self.add_flow(datapath, 1, match, actions, msg.buffer_id)
return
else:
self.add_flow(datapath, 1, match, actions)
data = None
if msg.buffer_id == ofproto.OFP_NO_BUFFER:
data = msg.data
#下发流表项
out = parser.OFPPacketOut(datapath=datapath, buffer_id=msg.buffer_id,
in_port=in_port, actions=actions, data=data)
datapath.send_msg(out)
@set_ev_cls是一个装饰器,当参数表示的事件触发后会执行下面的函数,例:
@set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)
def switch_features_handler(self, ev):
控制器事件event具体见ryu/controller/ofp_event.py,其事件名称是由接收到的报文类型来命名的,名字为Event+报文类型,本例中,控制器收到的是交换机发送的FEATURE_REPLY报文,所以事件名称为EventOFPSwitchFeatures。所以本事件其实就是当控制器接收到FEATURE_REPLY报文触发。
控制器有以下四个状态:
1、HANDSHAKE_DISPATCHER:发送Hello报文并等待对端Hello报文。
2、CONFIG_DISPATCHER:协商版本并发送FEATURE-REQUEST报文。
3、MAIN_DISPATCHER:已收到FEATURE-REPLY报文并发送SET-CONFIG报文。
4、DEAD_DISPATCHER:与对端断开连接。
综上,以上代码说明了当控制器处于CONFIG_DISPATCHER状态并且接受到FEATURE_REPLY报文时,执行switch_features_handler()函数。
Ryu控制器一般是配合Mininet进行网络的实验,在后续讲到Mininet的时候再说。
simple_switch_rest_13.py
除了simple_switch_13.py,有一个和它长得很像的文件,simple_switch_rest_13.py。
唯一的区别就是多了个rest_,rest是啥,我当时第一反应是放松。后来做毕设的时候才意识到这是RESTful软件架构风格。
REST (Representational State Transfer) API,这是Roy Fielding在2000年提出的一种架构风格或设计原则。REST的原则是将系统中的所有事物抽象为URL表示的资源。客户端可以通过 HTTP 请求(如 GET、POST、PUT、DELETE等)来操作资源。服务器收到请求后会根据不同的方法采取不同的响应,最终将数据以XML、JSON等格式返回给客户端。
听起来有点云里雾里的,简单点说其实就是用url的格式来进行资源的访问/进程间的通信。这种设计原则在现在已经很常见了,Ryu也采用的是这样的交互设计原则。
所以说了这么多,这玩意儿是干啥的,它其实就是之前在(一)里面提到的甲方与领导沟通的那套规则–北向协议。
老说南向协议和北向协议啥的,听着很玄乎。
南向协议就是控制器和数据平面沟通的规则。
北向协议就是应用平面和控制平面沟通的规则。
用户可以通过url对流表项进行查询、下发、删除等操作。
simple_switch_rest_13.py可以通过REST API干两件事:
1、获取MAC表
2、注册MAC表
具体的请看
https://blog.csdn.net/weixin_46239293/article/details/115607828
关于北向接口和意图映射的事情会在后续讲到。
交换机信息及流表项的查询
Ryu提供了两种方式来查询交换机信息以及流表项信息:
1、REST API主动查询:
后续讲到北向接口的时候会详细讲述。
2、事件响应查询:
Ryu中提供了很多的事件,如EventOFPPortStatsReply、EventOFPFlowStatsReply等。
EventOFPPortStatsReply在端口查询返回结果时触发。
EventOFPFlowStatsReply在流量查询返回结果时触发。
simple_monitor_13.py中展示了这两个事件的应用:
详细的代码讲解可以看大哥的博客:
https://www.cnblogs.com/ssyfj/p/11755773.html
我这里以我毕设的部分代码为例:
@set_ev_cls(ofp_event.EventOFPPortStatsReply, MAIN_DISPATCHER)
def _port_stats_reply_handler(self, ev):#执行完send_port_stats_request后会调用该函数对查询完的端口信息进行处理
body = ev.msg.body
for port_stat in sorted(body,key=attrgetter('port_no')):
datapathid=ev.msg.datapath.id
no=port_stat.port_no
tx=port_stat.tx_bytes
p=self.port_tx[datapathid]
if no==4294967294:
continue
if no in p.keys():
self.port_tx1[datapathid][no]=tx
else:
self.port_tx[datapathid][no]=tx
def send_port_stats_request(self, datapath):#执行端口信息的查询
ofproto = datapath.ofproto
parser = datapath.ofproto_parser
req = parser.OFPPortStatsRequest(datapath)
datapath.send_msg(req)
如果相对端口的信息进行查询可以调用send_port_stats_request()函数,该函数向指定的交换机发起端口的查询请求,也就是OFPPortStatsRequest。
交换机收到查询请求后进行相应的查询,然后将查询结果返回给控制器,此时会触发事件EventOFPPortStatsReply。开始执行_port_stats_reply_handler()函数,该函数内部的处理是specific的,可以忽略,换成自己的代码就可以,其实就是对查询完的端口信息进行处理。
总结
学习Ryu主要是还得多看app的源码,可以配合大哥的博客学习。
但其实说到这里,Ryu咋用还是没说到,这个得配合后面介绍的Mininet了。
下章会介绍Mininet、OVS,以及Ryu的使用。
https://blog.csdn.net/weixin_44480014/article/details/123261925