參考學(xué)習(xí)資料
- SDN啟動(dòng)流程
-
命令解析庫 OSLO
http://lingxiankong.github.io/blog/2014/08/31/openstack-oslo-config/ - python修飾器
- 參考學(xué)習(xí),站在別人的肩膀上
- RYU main函數(shù)
- RYU源碼解讀
本篇文章先從最簡(jiǎn)單的example_switch_l3.py進(jìn)行剖析,之后進(jìn)一步分析整個(gè)RYU框架。
在程序中經(jīng)常出現(xiàn)如下的修飾器:
@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
# install the table-miss flow entry.
match = parser.OFPMatch()
actions = [parser.OFPActionOutput(ofproto.OFPP_CONTROLLER,
ofproto.OFPCML_NO_BUFFER)]
self.add_flow(datapath, 0, match, actions)
def set_ev_cls(ev_cls, dispatchers=None):
def _set_ev_cls_dec(handler):
if 'callers' not in dir(handler):
handler.callers = {}
for e in _listify(ev_cls):
handler.callers[e] = _Caller(_listify(dispatchers), e.__module__)
return handler
return _set_ev_cls_dec
set_ev_cls函數(shù)接收兩個(gè)參數(shù)志膀,第一個(gè)代表需要監(jiān)聽的事件乎芳,第二個(gè)參數(shù)表示該事件在交換機(jī)與控制器交互的哪個(gè)階段發(fā)生有以下四個(gè)取值敛滋,可或烦味。
HANDSHAKE_DISPATCHER = "handshake"
CONFIG_DISPATCHER = "config"
MAIN_DISPATCHER = "main"
DEAD_DISPATCHER = "dead"
若監(jiān)聽到EventOFPSwitchFeatures事件苍凛,就會(huì)觸發(fā)下面的handler函數(shù)徒扶。
上述過程實(shí)際上是:
switch_features_handler=set_ev_cls(ofp_event.EventOFPSwitchFeatures, CONFIG_DISPATCHER)(switch_features_handler)
?上面還是有一點(diǎn)疑問粮彤,具體的返回和調(diào)用關(guān)系,為什么返回_set_ev_cls_dec函數(shù)而set_ev_cls函數(shù)中參數(shù)ev_cls, dispatchers還能被使用姜骡。如何做到函數(shù)的事件驅(qū)動(dòng)調(diào)用导坟。
整個(gè)過程可以描述為:
首先switch_features_handler作為參數(shù)傳遞給 _set_ev_cls_dec函數(shù),先判斷該函數(shù)有沒有callers屬性(因?yàn)橥缓瘮?shù)也可能被其他事件觸發(fā))溶浴,若沒有則添加該屬性乍迄,要注意函數(shù)也是具有屬性的,并且可以在定義外面添加士败。
callers是一個(gè)字典闯两,鍵值key是傳入的事件類對(duì)應(yīng)的列表形式,這里就是[ofp_event.EventOFPSwitchFeatures]谅将,value是一個(gè)_Caller對(duì)象漾狼,該對(duì)象主要就是記下dispatchers,和這個(gè)事件的模塊名饥臂。
_Caller函數(shù)原型為:
class _Caller(object):
"""Describe how to handle an event class.
"""
def __init__(self, dispatchers, ev_source):
"""Initialize _Caller.
:param dispatchers: A list of states or a state, in which this
is in effect.
None and [] mean all states.
:param ev_source: The module which generates the event.
ev_cls.__module__ for set_ev_cls.
None for set_ev_handler.
"""
self.dispatchers = dispatchers
self.ev_source = ev_source
這是第一個(gè)部分逊躁,總結(jié)下來完成的工作就是給switch_features_handler函數(shù)添加了一個(gè)callers字典屬性,里面保存了自己感興趣的事件和dispatcher隅熙。
事件是如何通知該函數(shù)的稽煤,ev這個(gè)類又是如何產(chǎn)生?
上面的問題也是最關(guān)心的問題囚戚,要理解這個(gè)還需要參考以下:
RYU main函數(shù)
RYU源碼解讀
RYU在運(yùn)行 ryu-manage <application>
時(shí)酵熙,實(shí)際上先運(yùn)行以下main函數(shù)。
def main(args=None, prog=None):
try:
CONF(args=args, prog=prog,
project='ryu', version='ryu-manager %s' % version,
default_config_files=['/usr/local/etc/ryu/ryu.conf'])
except cfg.ConfigFilesNotFoundError:
CONF(args=args, prog=prog,
project='ryu', version='ryu-manager %s' % version)
log.init_log()
logger = logging.getLogger(__name__)
if CONF.enable_debugger:
msg = 'debugging is available (--enable-debugger option is turned on)'
logger.info(msg)
else:
hub.patch(thread=True)
if CONF.pid_file:
import os
with open(CONF.pid_file, 'w') as pid_file:
pid_file.write(str(os.getpid()))
app_lists = CONF.app_lists + CONF.app
# keep old behavior, run ofp if no application is specified.
if not app_lists:
app_lists = ['ryu.controller.ofp_handler']
app_mgr = AppManager.get_instance()
app_mgr.load_apps(app_lists)
contexts = app_mgr.create_contexts()
services = []
services.extend(app_mgr.instantiate_apps(**contexts))
webapp = wsgi.start_service(app_mgr)
if webapp:
thr = hub.spawn(webapp)
services.append(thr)
try:
hub.joinall(services)
except KeyboardInterrupt:
logger.debug("Keyboard Interrupt received. "
"Closing RYU application manager...")
finally:
app_mgr.close()
首先就涉及到OSLO模塊的使用驰坊。留以后分析
app_lists從輸入的參數(shù)中獲得匾二,若沒有則指定為ryu.controller.ofp_handler,然后運(yùn)行
app_mgr = AppManager.get_instance()
調(diào)用APPManager類的靜態(tài)方法,改靜態(tài)方法為:
@staticmethod
def get_instance():
if not AppManager._instance:
AppManager._instance = AppManager()
return AppManager._instance
很明顯察藐,獲得一個(gè)實(shí)例皮璧。
接下來,加載應(yīng)用分飞。
app_mgr.load_apps(app_lists)
def load_app(self, name):
mod = utils.import_module(name) #加載這個(gè)模塊悴务,mod代表的是什么結(jié)構(gòu)?
clses = inspect.getmembers(mod,
lambda cls: (inspect.isclass(cls) and
issubclass(cls, RyuApp) and
mod.__name__ ==
cls.__module__))
if clses:
return clses[0][1]
return None
load_app函數(shù)是下面函數(shù)的調(diào)用函數(shù)浸须,app_lists是命令行傳入要運(yùn)行的APP惨寿,可以有多個(gè),通過依次 調(diào)用load_app進(jìn)行加載删窒。加載app為以下幾步,首先調(diào)用utils.import_module(name)
顺囊,該函數(shù)返回整個(gè)模塊的環(huán)境肌索,包括類和方法,然后通過 inspect.getmembers過濾出我們寫的class特碳,也就是ExampleSwitch13诚亚,通過三個(gè)條件刪選:類,RyuApp的子類午乓,所在的文件為主文件(這個(gè)排除了其他地方import進(jìn)來的類)站宗。注意返回的是名字和對(duì)應(yīng)的屬性,也就是類名和對(duì)應(yīng)的類益愈,clses[0][1]表示返回第一個(gè)類梢灭,不是類名,類名是[0][0]蒸其。在官方文檔中也有提到只運(yùn)行APP實(shí)現(xiàn)的第一個(gè)類敏释。
def load_apps(self, app_lists):
app_lists = [app for app
in itertools.chain.from_iterable(app.split(',')
for app in app_lists)]
while len(app_lists) > 0:
app_cls_name = app_lists.pop(0)
context_modules = [x.__module__ for x in self.contexts_cls.values()]
if app_cls_name in context_modules:
continue
LOG.info('loading app %s', app_cls_name)
cls = self.load_app(app_cls_name)
if cls is None:
continue
self.applications_cls[app_cls_name] = cls
services = []
for key, context_cls in cls.context_iteritems():
v = self.contexts_cls.setdefault(key, context_cls)
assert v == context_cls
context_modules.append(context_cls.__module__)
if issubclass(context_cls, RyuApp):
services.extend(get_dependent_services(context_cls))
# we can't load an app that will be initiataed for
# contexts.
for i in get_dependent_services(cls):
if i not in context_modules:
services.append(i)
if services:
app_lists.extend([s for s in set(services)
if s not in app_lists])
可能app_lists有多個(gè)app,先將他們統(tǒng)一為一個(gè)列表摸袁,并且依次加載執(zhí)行钥顽。
以上函數(shù)完成依次加載app,其實(shí)就是找到該APP對(duì)應(yīng)要運(yùn)行的類靠汁,并把該類的名字存到applications_cls中蜂大,key是app的名字,如module.path.module_name
蝶怔,值就是返回要運(yùn)行的類奶浦。
加載的過程中順便加載每個(gè)app依賴的app類,而且把每個(gè)app的context保存到context_cls中添谊。依賴的APP保存在_CONTEXT中财喳,這也是ryu實(shí)現(xiàn)的一種模塊間通信機(jī)制。具體參考RYU模塊間通信
到這一步,除了applications_cls耳高,還完成了context_cls字典的構(gòu)造扎瓶。該字典內(nèi)容和 _CONTEXTS中內(nèi)容一致。是當(dāng)前app所依賴的服務(wù)泌枪,如其他APP概荷。并將該服務(wù)添加在services列表中。
def get_dependent_services(cls):
services = []
for _k, m in inspect.getmembers(cls, _is_method):
if _has_caller(m):
for ev_cls, c in m.callers.items():
service = getattr(sys.modules[ev_cls.__module__],
'_SERVICE_NAME', None)
if service:
# avoid cls that registers the own events (like
# ofp_handler)
if cls.__module__ != service:
services.append(service)
m = sys.modules[cls.__module__]
services.extend(getattr(m, '_REQUIRED_APP', []))
services = list(set(services))
return services
接下來對(duì)依賴的服務(wù)和本身服務(wù)調(diào)用get_dependent_services碌燕,原型如上误证。get_dependent_services函數(shù)獲取其依賴應(yīng)用,最后將所有的依賴services添加到app_lists中修壕。該函數(shù)完成兩個(gè)操作愈捅,一是判斷該依賴項(xiàng)的method中有沒有訂閱事件,如果有就將ev_cls.__module__._SERVICE_NAME(也就是ofp_event._SERVICE_NAME)添加到services中.二是有沒有_REQUIRED_APP慈鸠,實(shí)際中都沒怎么用到蓝谨。
最后將依賴項(xiàng)添加到app_lists中。
create_contexts函數(shù):實(shí)例化context_cls中的context青团,如果這個(gè)context是app譬巫,就實(shí)例化該app。記得在load_apps中沒有管屬于context_cls的app嗎督笆,在這里直接初始化了芦昔。
def create_contexts(self):
for key, cls in self.contexts_cls.items():
if issubclass(cls, RyuApp):
# hack for dpset
context = self._instantiate(None, cls) //初始化app
else:
context = cls() //實(shí)例化context
LOG.info('creating context %s', key)
assert key not in self.contexts
self.contexts[key] = context //加入contexts字典
return self.contexts //返回contexts字典
完成上述后,創(chuàng)建了contexts 字典娃肿,key是依賴項(xiàng)名字咕缎,value是初始化后的依賴項(xiàng)。
返回到main函數(shù)中咸作,接下來運(yùn)行
services = []
services.extend(app_mgr.instantiate_apps(**contexts))
即調(diào)用
def instantiate_apps(self, *args, **kwargs):
for app_name, cls in self.applications_cls.items():
self._instantiate(app_name, cls, *args, **kwargs)
self._update_bricks()
self.report_bricks()
threads = []
for app in self.applications.values():
t = app.start()
if t is not None:
app.set_main_thread(t)
threads.append(t)
return threads
def _instantiate(self, app_name, cls, *args, **kwargs):
# for now, only single instance of a given module
# Do we need to support multiple instances?
# Yes, maybe for slicing.
LOG.info('instantiating app %s of %s', app_name, cls.__name__)
if hasattr(cls, 'OFP_VERSIONS') and cls.OFP_VERSIONS is not None:
ofproto_protocol.set_app_supported_versions(cls.OFP_VERSIONS)
if app_name is not None:
assert app_name not in self.applications
app = cls(*args, **kwargs)
register_app(app)
assert app.name not in self.applications
self.applications[app.name] = app
return app
_instantiate函數(shù)中就開始明朗了锨阿,先設(shè)置OFP_VERSIONS,做必要檢查后運(yùn)行 register_app(app)
记罚。該函數(shù)完成兩件事墅诡,一是applications[app.name] = app該字典的構(gòu)造,比較applications_cls桐智,key是app的名字末早,如module.path.module_name,值就是返回要運(yùn)行的類说庭。而這里然磷,key是類名,而value是實(shí)例化的該類對(duì)象刊驴。二是注冊(cè)該APP姿搜,如下
def register_app(app):
assert isinstance(app, RyuApp)
assert app.name not in SERVICE_BRICKS
SERVICE_BRICKS[app.name] = app
register_instance(app)
完成了重要的數(shù)據(jù)結(jié)構(gòu)SERVICE_BRICKS的構(gòu)造寡润,其中key是要運(yùn)行的類的名字,value是實(shí)例好的該類對(duì)象舅柜。
app.name表示這個(gè)類的名稱梭纹,而app則代表這個(gè)類本身。name這個(gè)屬性在基類RyuApp中有定義致份。
將該APP加入到SERVICE_BRICKS字典中变抽,該字典代表一個(gè)服務(wù)鏈。值得注意的是氮块,只要繼承了基類RyuApp的app绍载,則app.name都是該類的類名,因?yàn)槎x為self.name = self.__class__.__name__
滔蝉,但是ofp_handler中class OFPHandler對(duì)name屬性進(jìn)行了重寫击儡,self.name = 'ofp_event',在后面會(huì)有作用锰提。
def register_instance(i):
for _k, m in inspect.getmembers(i, inspect.ismethod):
# LOG.debug('instance %s k %s m %s', i, _k, m)
if _has_caller(m):
for ev_cls, c in m.callers.items():
i.register_handler(ev_cls, m)
def _has_caller(meth):
return hasattr(meth, 'callers')
def register_handler(self, ev_cls, handler):
assert callable(handler)
self.event_handlers.setdefault(ev_cls, [])
self.event_handlers[ev_cls].append(handler)
和之前的開始連接起來曙痘,這里對(duì)APP的類中每一個(gè)method檢查是否有callers屬性,如果有立肘,就注冊(cè)句柄。最終將事件和對(duì)應(yīng)要觸發(fā)的函數(shù)以鍵值對(duì)形式保存在event_handlers字典中名扛。
至此谅年,生成了兩個(gè)字典,一是 SERVICE_BRICKS[app.name] = app肮韧,代表APP運(yùn)行的類融蹂,一是event_handlers,保存對(duì)應(yīng)事件和觸發(fā)函數(shù)弄企。
再次回到instantiate_apps函數(shù)超燃,還沒有運(yùn)行完呢。接下來是
self._update_bricks()
self.report_bricks()
def _update_bricks(self):
for i in SERVICE_BRICKS.values():
for _k, m in inspect.getmembers(i, inspect.ismethod):
if not hasattr(m, 'callers'):
continue
for ev_cls, c in m.callers.items():
if not c.ev_source:
continue
brick = _lookup_service_brick_by_mod_name(c.ev_source)
if brick:
brick.register_observer(ev_cls, i.name,
c.dispatchers)
# allow RyuApp and Event class are in different module
for brick in SERVICE_BRICKS.values():
if ev_cls in brick._EVENTS:
brick.register_observer(ev_cls, i.name,
c.dispatchers)
這個(gè)函數(shù)很關(guān)鍵拘领,第一步對(duì)APP中的類進(jìn)行檢查是否有callers意乓,如果有說明是修飾過,再進(jìn)一步看c.ev_source约素,回顧
handler.callers[e] = _Caller(_listify(dispatchers), e.__module__)
其中 e表示事件類届良,c.ev_source表示該事件類所在模塊名。其實(shí)c.ev_source如果是的module那么就是ofp_event,而OFPHandler類的名字正好就是'ofp_event'.
這里有一個(gè)技巧圣猎,其實(shí)c.ev_source如果是的module那么就是ofp_event,而OFPHandler類的名字正好就是'ofp_event'(在下文中會(huì)看到其實(shí)就是OpenFlowController),所以這里的brick就是OFPHandler士葫,然后將將每種ev_cls的類型和app名字注冊(cè)到該類中,其實(shí)本質(zhì)上就是OFPHandler作為了一個(gè)消息源送悔。這個(gè)函數(shù)非常重要慢显,將每個(gè)app的handler與消息源OFPHandler建立了聯(lián)系
def lookup_service_brick(name):
return SERVICE_BRICKS.get(name)
def _lookup_service_brick_by_mod_name(mod_name):
return lookup_service_brick(mod_name.split('.')[-1])
所以上面其實(shí)是返回SERVICE_BRICKS.get(‘ofp_event’)爪模,而對(duì)應(yīng)的類為OFPHandler。也就是說荚藻,這里是根據(jù)事件所在的模塊名加載服務(wù)塊屋灌,brick = OFPHandler對(duì)象。
def register_observer(self, ev_cls, name, states=None):
states = states or set()
ev_cls_observers = self.observers.setdefault(ev_cls, {})
ev_cls_observers.setdefault(name, set()).update(states)
上面程序細(xì)節(jié)比較多鞋喇,先看參數(shù)声滥,傳入的en_cls是事件類,即訂閱的事件侦香,name為該類的類名落塑,states為上文中提到的四中狀態(tài)之一。構(gòu)造observers字典罐韩,key是en_cls事件類憾赁,value是一個(gè)字典,過程如下:
>>> a.setdefault(4,{})
{}
>>> a.setdefault(4,{}).setdefault('yuan',set())
set([])
>>> b = a.setdefault(4,{})
>>> b.setdefault('yuan',set())
set([])
>>> a
{1: 'hello', 2: 'nihao', 3: 'world', 4: {'yuan': set([])}}
>>> b.setdefault('yuan',set()).update('abc')
>>> a
{1: 'hello', 2: 'nihao', 3: 'world', 4: {'yuan': set(['a', 'c', 'b'])}}
>>>
總結(jié)來說散吵,構(gòu)造了一個(gè)較為復(fù)雜的observers字典龙考,key是en_cls事件類,value為字典矾睦,其中key為類的名稱晦款,value為一個(gè)集合,其中保存的states枚冗。
整個(gè)函數(shù)_update_bricks函數(shù)對(duì)所有事件進(jìn)行了注冊(cè)缓溅,并生成了一個(gè)observers字典,注冊(cè)在OFPHander類下赁温,表示OFPHander為消息源坛怪。
接下來運(yùn)行self.report_bricks(),如下
def report_bricks():
for brick, i in SERVICE_BRICKS.items():
AppManager._report_brick(brick, i)
def _report_brick(name, app):
LOG.debug("BRICK %s", name)
for ev_cls, list_ in app.observers.items():
LOG.debug(" PROVIDES %s TO %s", ev_cls.__name__, list_)
for ev_cls in app.event_handlers.keys():
LOG.debug(" CONSUMES %s", ev_cls.__name__)
這塊代碼主要負(fù)責(zé)顯示信息股囊⊥嗄洌可以理解,brick代表服務(wù)鏈中的一個(gè)服務(wù)稚疹,也就是一個(gè)要運(yùn)行的類居灯,每個(gè)服務(wù)對(duì)應(yīng)有兩個(gè)字典,一是observers贫堰,另一個(gè)event_handlers穆壕,event_handlers是該類中保存的對(duì)應(yīng)事件和觸發(fā)函數(shù)。運(yùn)行時(shí)會(huì)顯示CONSUMES ev_cls.__name__
啟動(dòng)一個(gè)服務(wù)可能要依賴其他服務(wù)其屏,比如ofp_event是所有APP都需要依賴的服務(wù)喇勋,被依賴的服務(wù)要提供相關(guān)服務(wù),提供的服務(wù)就保存在observers中偎行。
回到instantiate_apps函數(shù)川背,接下來贰拿,所有APP都啟動(dòng)。
threads = []
for app in self.applications.values():
t = app.start()
if t is not None:
app.set_main_thread(t)
threads.append(t)
return threads
def start(self):
"""
Hook that is called after startup initialization is done.
"""
self.threads.append(hub.spawn(self._event_loop))
def _event_loop(self):
while self.is_active or not self.events.empty():
ev, state = self.events.get()
self._events_sem.release()
if ev == self._event_stop:
continue
handlers = self.get_handlers(ev, state)
for handler in handlers:
try:
handler(ev)
except hub.TaskExit:
# Normal exit.
# Propagate upwards, so we leave the event loop.
raise
except:
LOG.exception('%s: Exception occurred during handler processing. '
'Backtrace from offending handler '
'[%s] servicing event [%s] follows.',
self.name, handler.__name__, ev.__class__.__name__)
hub.spawn(self._event_loop)熄云,利用eventlet框架創(chuàng)建一個(gè)協(xié)程膨更,具體細(xì)節(jié)不深究。重點(diǎn)關(guān)注_event_loop函數(shù)缴允。先搞清楚以下兩個(gè)屬性:
self.events = hub.Queue(128)
self._events_sem = hub.BoundedSemaphore(self.events.maxsize)
即調(diào)用hub文件中
Queue = eventlet.queue.LightQueue
BoundedSemaphore = eventlet.semaphore.BoundedSemaphore
查閱官網(wǎng)資料荚守,
class eventlet.queue.LightQueue(maxsize=None)
This is a variant of Queue that behaves mostly like the standard Stdlib_Queue. It differs by not supporting the task_done
or joinmethods, and is a little faster for not having that overhead.
創(chuàng)建隊(duì)列,和 Stdlib_Queue類似练般,但是性能相比較更好矗漾。
calss eventlet.semaphore.BoundedSemaphore(value=1)
A bounded semaphore checks to make sure its current value doesn’t exceed its initial value. If it does, ValueError is raised. In most situations semaphores are used to guard resources with limited capacity. If the semaphore is released too many times it’s a sign of a bug. If not given, value defaults to 1.
release(blocking=True)
Release a semaphore, incrementing the internal counter by one. If the counter would exceed the initial value, raises ValueError. When it was zero on entry and another thread is waiting for it to become larger than zero again, wake up that thread
值得注意的是:
其中一個(gè)特殊的app是opf_handler app,其重寫了start函數(shù)薄料,其實(shí)調(diào)用start后就是啟動(dòng)OpenFlowController類敞贡,OpenFlowController啟動(dòng)后,ryu開始監(jiān)聽來自交換機(jī)的新連接
總結(jié):
整個(gè)RYU程序的啟動(dòng)可以分為兩個(gè)部分摄职,第一部分是APP的加載誊役,上下文環(huán)境的加載,訂閱事件的注冊(cè)與分發(fā)谷市。
第一部分主要分為以下幾個(gè)步驟蛔垢,在一些步驟中會(huì)有比較重要的數(shù)據(jù)結(jié)構(gòu)需要注意,一開始有個(gè)數(shù)據(jù)結(jié)構(gòu)要特別注意迫悠, set_ev_cls修飾的函數(shù)啦桌,表示訂閱事件,會(huì)添加一個(gè)method及皂, handler.callers[e] = _Caller(_listify(dispatchers), e.__module__)
。:
- 1且改、初始化APP管理類验烧,AppManager
- 2、 生成APP服務(wù)列表(并不實(shí)例化)又跛,只是保存在對(duì)應(yīng)字典中碍拆,
self.applications_cls[app_cls_name] = cls
,保存每個(gè)APP的名字和第一個(gè)功能類(滿足是RyuApp子類等條件)- 3、加載每個(gè)app所依賴的模塊慨蓝,保存在
contexts_cls.setdefault(key, context_cls)
- 4感混、實(shí)例化依賴模塊,即contexts_cls中的類。
- 5礼烈、實(shí)例化APPs弧满。需要實(shí)例化的APP保存在applications_cls字典中。創(chuàng)建以下數(shù)據(jù)結(jié)構(gòu)此熬,一是SERVICE_BRICKS[app.name] = app表示服務(wù)鏈庭呜,二是所有訂閱事件集合event_handlers滑进,保存事件和觸發(fā)函數(shù)的字典。三是observers字典募谎,注冊(cè)在OFPHander中扶关,key是en_cls事件類,value為字典数冬,其中key為類的名稱节槐,value為一個(gè)集合,其中保存的states拐纱。
- 6铜异、運(yùn)行每一個(gè)APP,并以協(xié)程方式管理戳玫。要注意的是 OFPHander重寫了start函數(shù)熙掺,在該函數(shù)中啟動(dòng)了控制器類。下一篇文章中做深入分析咕宿。