剖析Python的Twisted框架的核心特性
一.reactor
twisted的核心是reactor,而提到reactor不可避免的是同步/异步,阻塞/非阻塞,在Dave的第一章概念性介绍中,对同步/异步的界限有点模糊,关于同步/异步,阻塞/非阻塞可参见知乎讨论。而关于proactor(主动器)和reactor(反应堆),这里有一篇推荐博客有比较详细的介绍。
就reactor模式的网络IO而言,应该是同步IO而不是异步IO。而Dave第一章中提到的异步,核心在于:显式地放弃对任务的控制权而不是被操作系统随机地停止,程序员必须将任务组织成序列来交替的小步完成。因此,若其中一个任务用到另外一个任务的输出,则依赖的任务(即接收输出的任务)需要被设计成为要接收系列比特或分片而不是一下全部接收。
显式主动地放弃任务的控制权有点类似协程的思考方式,reactor可看作协程的调度器。reactor是一个事件循环,我们可以向reactor注册自己感兴趣的事件(如套接字可读/可写)和处理器(如执行读写操作),reactor会在事件发生时回调我们的处理器,处理器执行完成之后,相当于协程挂起(yield),回到reactor的事件循环中,等待下一个事件来临并回调。reactor本身有一个同步事件多路分解器(SynchronousEventDemultiplexer),可用select/epoll等机制实现,当然twistedreactor的事件触发不一定是基于IO,也可以由定时器等其它机制触发。
twisted的reactor无需我们主动注册事件和回调函数,而是通过多态(继承特定类,并实现所关心的事件接口,然后传给twistedreactor)来实现。关于twisted的reactor,有几个需要注意的地方:
twisted.internet.reactor是单例模式,每个程序只能有一个reactor;
尽量在reactor回调函数尽快完成操作,不要执行阻塞任务,reactor本质是单线程,用户回调代码与twisted代码运行在同一个上下文,某个回调函数中阻塞,会导致reactor整个事件循环阻塞;
reactor会一直运行,除非通过reactor.stop()显示停止它,但一般调用reactor.stop(),也就意味着应用程序结束;
二.twisted简单使用
twisted的本质是reactor,我们可以使用twisted的底层API(避开twisted便利的高层抽象)来使用reactor:
#示例一twisted底层API的使用 fromtwisted.internetimportreacto fromtwisted.internetimportmain fromtwisted.internet.interfacesimportIReadDescriptor importsocket classMySocket(IReadDescriptor): def__init__(self,address): #连接服务器 self.address=address self.sock=socket.socket(socket.AF_INET,socket.SOCK_STREAM) self.sock.connect(address) self.sock.setblocking(0) #telltheTwistedreactortomonitorthissocketforreading reactor.addReader(self) #接口:告诉reactor监听的套接字描述符 deffileno(self): try: returnself.sock.fileno() exceptsocket.error: return-1 #接口:在连接断开时的回调 defconnectionLost(self,reason): self.sock.close() reactor.removeReader(self) #当应用程序需要终止时调用: #reactor.stop() #接口:当套接字描述符有数据可读时 defdoRead(self): bytes='' #尽可能多的读取数据 whileTrue: try: bytesread=self.sock.recv(1024) ifnotbytesread: break else: bytes+=bytesread exceptsocket.error,e: ife.args[0]==errno.EWOULDBLOCK: break returnmain.CONNECTION_LOST ifnotbytes: returnmain.CONNECTION_DONE else: #在这里解析协议并处理数据 printbytes
示例一可以很清晰的看到twisted的reactor本质:添加监听描述符,监听可读/可写事件,当事件来临时回调函数,回调完成之后继续监听事件。
需要注意:
套接字为非阻塞,如果为阻塞则失去了reactor的意义
我们通过继承IReadDescriptor来提供reactor所需要的接口
通过reactor.addReader将套接字类加入reactor的监听对象中
main.CONNECTION_LOST是twisted预定义的值,通过这些值它我们可以一定程度控制下一步回调(类似于模拟一个事件)
但是上面的MySocket类不够好,主要有以下缺点:
需要我们自己去读取数据,而不是框架帮我们读好,并处理异常
网络IO和数据处理混为一块,没有剥离开来
三.twisted抽象
twisted在reactor的基础上,建立了更高的抽象,对一个网络连接而言,twisted建立了如下三个概念:
Transports:网络连接层,仅负责网络连接和读/写字节数据
Protocols:协议层,服务业务相关的网络协议,将字节流转换成应用所需数据
ProtocolFactories:协议工厂,负责创建Protocols,每个网络连接都有一个Protocols对象(因为要保存协议解析状态)
twisted的这些概念和erlang中的ranch网络框架很像,ranch框架也抽象了Transports和Protocols概念,在有新的网络连接时,ranch自动创建Transports和Protocols,其中Protocols由用户在启动ranch时传入,是一个实现了ranch_protocolbehaviour的模块,Protocols初始化时,会收到该连接对应的Transports,如此我们可以在Protocols中处理字节流数据,按照我们的协议解析并处理数据。同时可通过Transports来发送数据(ranch已经帮你读取了字节流数据了)。
和ranch类似,twisted也会在新连接到达时创建Protocols并且将Transport传入,twisted会帮我们读取字节流数据,我们只需在dataReceived(self,data)接口中处理字节流数据即可。此时的twisted在网络IO上可以算是真正的异步了,它帮我们处理了网络IO和可能遇到的异常,并且将网络IO和数据处理剥离开来,抽象为Transports和Protocols,提高了程序的清晰性和健壮性。
#示例二twisted抽象的使用 fromtwisted.internetimportreactor fromtwisted.internet.protocolimportProtocol,ClientFactory classMyProtocol(Protocol): #接口:Protocols初始化时调用,并传入Transports #另外twisted会自动将Protocols的factory对象成员设为ProtocolsFactory实例的引用 #如此就可以通过factory来与MyProtocolFactory交互 defmakeConnection(self,trans): print'makeconnection:gettransport:',trans print'myfactoryis:',self.factory #接口:有数据到达 defdataReceived(self,data): self.poem+=data msg='Task%d:got%dbytesofpoetryfrom%s' printmsg%(self.task_num,len(data),self.transport.getPeer()) #接口:连接断开 defconnectionLost(self,reason): #连接断开的处理 classMyProtocolFactory(ClientFactory): #接口:通过protocol类成员指出需要创建的Protocols protocol=PoetryProtocol#tellbaseclasswhatprototobuild def__init__(self,address): self.poetry_count=poetry_count self.poems={}#tasknum->poem #接口:在创建Protocols的回调 defbuildProtocol(self,address): proto=ClientFactory.buildProtocol(self,address) #在这里对proto做一些初始化.... returnproto #接口:连接Server失败时的回调 defclientConnectionFailed(self,connector,reason): print'Failedtoconnectto:',connector.getDestination() defmain(address): factory=MyClientFactory(address) host,port=address #连接服务端时传入ProtocolsFactory reactor.connectTCP(host,port,factory) reactor.run()
示例二要比示例一要简单清晰很多,因为它无需处理网络IO,并且逻辑上更为清晰,实际上ClientFactory和Protocol提供了更多的接口用于实现更灵活强大的逻辑控制,具体的接口可参见twisted源代码。
四.twistedDeferred
twistedDeferred对象用于解决这样的问题:有时候我们需要在ProtocolsFactory中嵌入自己的回调,以便Protocols中发生某个事件(如所有Protocols都处理完成)时,回调我们指定的函数(如TaskFinished)。如果我们自己来实现回调,需要处理几个问题:
如何区分回调的正确返回和错误返回?(我们在使用异步调用时,要尤其注意错误返回的重要性)
如果我们的正确返回和错误返回都需要执行一个公共函数(如关闭连接)呢?
如果保证该回调只被调用一次?
Deferred对象便用于解决这种问题,它提供两个回调链,分别对应于正确返回和错误返回,在正确返回或错误返回时,它会依次调用对应链中的函数,并且保证回调的唯一性。
d=Deferred() #添加正确回调和错误回调 d.addCallbacks(your_ok_callback,your_err_callback) #添加公共回调函数 d.addBoth(your_common_callback) #正确返回将依次调用your_ok_callback(Res)->common_callback(Res) d.callback(Res) #错误返回将依次调用your_err_callback(Err)->common_callback(Err) d.errback(Err) #注意,对同一个Defered对象,只能返回一次,尝试多次返回将会报错
twisted的defer是异步的一种变现方式,可以这么理解,他和thread的区别是,他是基于时间event的。
有了deferred,即可对任务的执行进行管理控制。防止程序的运行,由于等待某项任务的完成而陷入阻塞停滞,提高整体运行的效率。
Deferred能帮助你编写异步代码,但并不是为自动生成异步或无阻塞的代码!要想将一个同步函数编程异步函数,必须在函数中返回Deferred并正确注册回调。
五.综合示例
下面的例子,你们自己跑跑,我上面说的都是一些个零散的例子,大家对照下面完整的,走一遍。twisted理解其实却是有点麻烦,大家只要知道他是基于事件的后,慢慢理解就行了。
#coding:utf-8 #xiaorui.cc fromtwisted.internetimportreactor,defer fromtwisted.internet.threadsimportdeferToThread importos,sys fromtwisted.pythonimportthreadable;threadable.init(1) deferred=deferToThread.__get__ importtime deftodoprint_(result): printresult defrunning(): "Printsafewdotsonstdoutwhilethereactorisrunning." #sys.stdout.write(".");sys.stdout.flush() print'.' reactor.callLater(.1,running) @deferred defsleep(sec): "Ablockingfunctionmagicallyconvertedinanon-blockingone." print'startsleep%s'%sec time.sleep(sec) print'\nendsleep%s'%sec return"ok" deftest(n,m): print"funtest()isstart" m=m vals=[] keys=[] foriinxrange(m): vals.append(i) keys.append('a%s'%i) d=None foriinxrange(n): d=dict(zip(keys,vals)) print"funtest()isend" returnd if__name__=="__main__": #one sleep(10).addBoth(todoprint_) reactor.callLater(.1,running) reactor.callLater(3,reactor.stop) print"gogo!!!" reactor.run() #two aa=time.time() de=defer.Deferred() de.addCallback(test) reactor.callInThread(de.callback,10000000,100) printtime.time()-aa print"我这里先做别的事情" printde print"gogoend"