对于任意一款控制器,想要快速了解其开发机制,从转发模块入手无疑是最佳的学习方式。RYU通过App的形式提供了一系列功能模块,其中包括使用了OpenFlow作为控制协议的二层交换机控制模块simple_switch_13.py。
想要理解simple switch的控制逻辑,首先要掌握传统网络下二层交换机的转发学习表工作原理。
对于每个二层交换机,都会维护一个mac地址表,用于记录mac地址和物理端口的映射关系。默认状态下,mac地址表为空,当交换机在端口A收到一个数据帧时,首先检查帧头中的源mac地址(mac_src),记录mac_src到端口A的映射关系,表示交换机可以通过端口A找到mac_src对应的主机。对于帧头中的目的mac地址(mac_dst),如果没有记录mac_dst与端口的映射关系,则会通过广播的方式将数据帧转发出去。
假如每个交换机都通过一个唯一的switch_id来标识,那么对于整个二层网络,我们可以得到一个由(switch_id,mac)到端口号(port_num)的映射表。实例:如果存在映射关系(sw1,mac1)→(port_1),那么当sw1收到目的mac地址为mac1的数据帧时,交换机将会从port_1把数据帧转发出去。simple switch模块的核心就是实现了上述映射表的记录,记录在mac_to_port字典中。
switch_feature_handler函数:
@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)
该函数用于对交换机配置table-miss流表项,实现交换机初始化,table-miss流表项匹配所有数据包,优先级为0,交换机会将匹配到table-miss流表项的数据包发送给控制器,形成packet_in事件。代码中,OFPMatch和OFPActionOutput对应了OpenFlow交换机流表规定的匹配字段和指定端口转发的动作字段,具体实现详见ofproto_v1_3_parser.py
add_flow函数:
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)
通过向交换机发送flow_mod消息配置流表。其中datapath表示交换机实例,priority是优先级,注意flow_mod消息中hard_timeout和idle_timeout缺省值为0,表示流表永久存活,可以自行设置其他整数值来为流表项设置生存时间,单位是秒。
packet_in_handler函数:
@set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)
def _packet_in_handler(self, ev):
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'] # 从in_port得到的该数据帧
pkt = packet.Packet(msg.data)
eth = pkt.get_protocols(ethernet.ethernet)[0] # 获取二层帧头
if eth.ethertype == ether_types.ETH_TYPE_LLDP:
return
dst = eth.dst
src = eth.src
dpid = datapath.id # 交换机的id
self.mac_to_port.setdefault(dpid, {}) # 数据结构格式:{dpid: {mac: port}}
self.logger.info("packet in %s %s %s %s", dpid, src, dst, in_port)
self.mac_to_port[dpid][src] = in_port # 记录映射信息
if dst in self.mac_to_port[dpid]:
out_port = self.mac_to_port[dpid][dst] # 查找目的mac对应的端口号
else:
out_port = ofproto.OFPP_FLOOD # 如果没有找到,则广播
actions = [parser.OFPActionOutput(out_port)]
if out_port != ofproto.OFPP_FLOOD:
match = parser.OFPMatch(in_port=in_port, eth_dst=dst, eth_src=src)
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)
packet_in_handler函数通过@set_ev_cls(ofp_event.EventOFPPacketIn, MAIN_DISPATCHER)标签来注册对packet_in事件的监听。对于SDN控制器应用来说,packet_in是最常见的程序入口,通过解析包头字段,获取地址、端口号、包类型等信息。simple_switch的packet_in_handler函数中解析了以太网帧头,得到了源、目的mac地址,记录交换机id、源mac地址到收包端口号。如果交换机记录了到目的mac地址的端口映射,则从对应端口转发出去,否则指定转发端口为FLOOD。
Packetin消息:用于标记缓存在交换机中的数据报文id,如报文被action上送到控制器中maxlen字段或者table_miss消息限制长度,而通过bufferid将报文缓存在交换机中,以便被另外两种消息来调用;
Packetout消息:用于控制器将原先buffer在交换机中的报文,通过Packetout个形式从交换机的某个物理口送出去;
Flowmod消息:如果flowmod中带有bufferid,那么说明这个flowmod需要做两件事情,第一是正常下发一条flow,其次是把交换机中先前buffer的那个数据报文,Packetout到table来匹配一次下的这条flow;注意以上两个指令都是通过这个带有bufferid的消息执行的,不需要控制器另外下packet_out消息,这种设计思路是非常巧妙的。
大家如果有希望了解的控制器源码,欢迎在评论区进行推荐。