生动的SDN基础内容介绍(三)--Ryu控制器

  • 控制器
  • Ryu的目录
  • Ryu的学习
  • simple_switch_13.py
  • simple_switch_rest_13.py
  • 交换机信息及流表项的查询
  • 总结


控制器

之前介绍完了南向协议OpenFlow,这次说一说Ryu。因为毕设的时候师兄推荐了Ryu,再考虑到Python方便开发,我也就继续用Ryu了。但是后续发现好像支持Ryu开发的框架相较Floodlight、OpenDaylight、ONOS没那么多(但也可能只是我没找到)。

首先非常强烈推荐这位大哥的博客:

当时学习的时候多亏了这位大哥,大哥博客里面有很多关于SDN的实验,讲解的很细致。

其次要强烈推荐Ryu官方的手册:
https://ryu.readthedocs.io/en/latest/ofproto_v1_3_ref.html 有不懂的就翻手册。

Ryu的目录

Ryu的安装网上有很多介绍了,这里就不说了。

我用的是Ubuntu20.04,Ryu4.34的目录(ryu/ryu)如下所示:

基于python语言sdn的控制语言 python sdn控制器_网络


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介绍中显示的匹配域:

基于python语言sdn的控制语言 python sdn控制器_python_02

4、ofproto:
这里有两类文件,一类是协议的数据结构定义,另外一类是协议的解析,即处理的函数。
如ofproto_v1_3.py是1.3版本的OpenFlow协议数据结构的定义,而ofproto_v1_3_parser.py则定义了1.3版本的协议编码和解码。

5、topology:

这里定义了交换机的数据结构和一些event。

可以看到这里的Switches class将交换机基本的内容已经定义好了:

基于python语言sdn的控制语言 python sdn控制器_网络_03


定义了可支持的OpenFlow的版本、时间、链路、主机、端口等。

6、contrib:
存放了开源社区贡献者的代码。

7、cmd:
为控制器的执行创建环境,执行命令行的命令。

Ryu的学习

Ryu的目录里有一个app目录,这里存放了开发者给我们写好了的一些app。

simple_switch_13.py

这里最要关注的是simple_switch_13.py,这个文件很适合刚接触Ryu的人来熟悉Ryu。
它基于OpenFlow1.3协议实现了简单的学习交换机,为啥叫学习呢,因为最开始交换机不知道去往MAC1的数据包该往哪儿发,然后接受到了一次之后就把要转发的端口记录下来了,下次就知道去MAC1的数据包要从哪个端口走了。其实就是实现了平常咱们用的二层交换机的功能。

关于这个文件的源码讲解,这篇博客还是很好的:

当然还有大哥的这篇:

我把我自己的一些注释写在下面的代码里

# 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表
具体的请看

关于北向接口和意图映射的事情会在后续讲到。

交换机信息及流表项的查询

Ryu提供了两种方式来查询交换机信息以及流表项信息:
1、REST API主动查询:
后续讲到北向接口的时候会详细讲述。
2、事件响应查询:
Ryu中提供了很多的事件,如EventOFPPortStatsReply、EventOFPFlowStatsReply等。
EventOFPPortStatsReply在端口查询返回结果时触发。
EventOFPFlowStatsReply在流量查询返回结果时触发。

simple_monitor_13.py中展示了这两个事件的应用:
详细的代码讲解可以看大哥的博客:

我这里以我毕设的部分代码为例:

@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的使用。