对twisted诗歌服务器的总结和笔记

差不多两个月之前的时候看过一段时间的twisted源码和诗歌服务器的教程,但是当时的笔记都记在笔记本,两个月之后想要再用的时候印象又已经模糊了。况且当时对于事件驱动和异步回调的理解没有现在深,系统地看一遍教程记一下twisted和defer怎样一步步产生的。个人感觉这个比当时看tcp差错控制的演进还是要简单一点。。
第一步,用2to3 -w 将所有代码转换成python3格式

1.Twisted 理论基础
首先介绍了一下多线程和异步模型
在多线程程序中,对于停止某个线程启动另外一个线程,其决定权并不在程序员手里而在操作系统那里,因此,程序员在编写程序过程中必须要假设在任何时候一个线程都有可能被停止而启动另外一个线程。相反,在异步模型中,一个任务要想运行必须显式放弃当前运行的任务的控制权。这也是相比多线程模型来说,最简洁的地方。

个人理解:简洁的意思是如果你能手动控制当前任务放弃控制权,能避免一些糟糕的情况(比如说死锁?消费者生产者问题。。)。虽然这里看起来需要手动添加到哪个位置时防止,增加了代码量,但是有效避免出错及为了处理出错而增加的代码量。(多线程并不适合逻辑复杂的任务)(协程与异步,协程既有异步又有同步)

异步模型要比同步模型复杂得多。程序员必须将任务组织成序列来交替的小步完成。因此,若其中一个任务用到另外一个任务的输出,则依赖的任务(即接收输出的任务)需要被设计成为要接收系列比特或分片而不是一下全部接收。

因此,就要问了,为什么还要使用异步模型呢? 在这儿,我们至少有两个原因。首先,如果有一到两个任务需要完成面向人的接口,如果交替执行这些任务,系统在保持对用户响应的同时在后台执行其它的任务。因此,虽然后台的任务可能不会运行的更快,但这样的系统可能会受欢迎的多。
然而,有一种情况下,异步模型的性能会高于同步模型,有时甚至会非常突出,即在比较短的时间内完成所有的任务。这种情况就是任务被强行等待或阻塞,如图4所示:

据我了解,第一种情况的典型代表就是图形界面,其中事件驱动是很广泛使用的设计模式,第二种的代表就是爬虫了。

因此一个异步程序只有在没有任务可执行时才会出现"阻塞",这也是为什么异步程序被称为非阻塞程序的原因。
一个网络服务是异步模型的典型代表

2.异步编程模式与Reactor初探
先实现了一个阻塞的服务器blocking-server/slowpoetry.py和一个阻塞的客户端blocking-client/get-poetry.py
自然效率很低
然后使用一个异步客户端async-client/get-poetry.py,提升了效率。这个客户端并没有用到twisted
核心代码:

            while True:
                try:
                    new_data = sock.recv(1024)
                except socket.error, e:
                    if e.args[0] == errno.EWOULDBLOCK:
                        # this error code means we would have
                        # blocked if the socket was blocking.
                        # instead we skip to the next socket
                        break
                    raise
                else:
                    if not new_data:
                        break
                    else:
                        data += new_data

1用来进行通信的Socket方法是非阻塞模的,这是通过调用setblocking(0)来实现的。
2select模块中的select方法是用来识别其监视的socket是否有完成数据接收的,如果没有它就处于阻塞状态。
3当从服务器中读取数据时,会尽量多地从Socket读取数据直到它阻塞为止,然后读下一个Socket接收的数据(如果有数据接收的话)。

我们的异步模式的客户端必须要有一个循环体来保证我们能够同时监视所有的socket端。这样我们就能在一次循环体中处理尽可能多的数据。
这个利用循环体来等待事件发生,然后处理发生的事件的模型非常常见,而被设计成为一个模式:reactor模式。

说起这个圈,其实我想到让服务生打电话叫自己起床这么个有名的事件驱动的比喻。事实上,服务生一直在不断地检查:是否天亮了,如果天亮了就打电话,没有天亮就继续检查是否天亮。并不是轮询消失了,而是轮询由我变成了另一个人(服务生),所以事件驱动有个别名叫事件轮询。但是,要注意在twisted里,只有一个主线程,不管是轮询还是业务逻辑,都只有一个线程。

扫描二维码关注公众号,回复: 4543536 查看本文章

一个真正reactor模式的实现是需要实现循环独立抽象出来并具有如下的功能:
1监视一系列与你I/O操作相关的文件描述符(description)
2不停地向你汇报那些准备好的I/O操作的文件描述符

感觉就像是一个老板的秘书,报告重要事件。

3.初识Twisted
用twisted的方式实现前面的内容

from twisted.internet import reactor
reactor.run()

正常情况下,我们需要给出事件循环或者文件描述符来监视I/O(连接到某个服务器上,比如说我们那个诗歌服务器)。
值得注意的是,这里并不是一个在不停运行的简单循环。如果你在桌面上有个CPU性能查看器,可以发现这个循环体不会带来任何性能损失。实际上,这个reactor被卡住在第二部分图5的最顶端,等待永远不会到来的事件发生(更具体点说是一个调用select函数,却没有监视任何文件描述符)。
注意,此时进程是属于阻塞态的(linux下进程状态显示为S),不消耗任何系统资源。
为了避免CPU空转,可以引进了一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理,不过两者的本质是一样的)。这个代理比较厉害,可以同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,于是我们的程序就会轮询一遍所有的流(于是我们可以把“忙”字去掉了)。
在这里插入图片描述
这一段还需要深入理解。为什么他被“卡”在最顶端,是真的“卡住了”?
reactor是单例模式,只能存在一个,并且只要import就会创建
若使用其它的reactor,需要在引入twisted.internet.reactor前安装它。

from twited.internet import pollreactor
pollreactor.install()
from twisted.internet import reactor
reactor.run()

通过调用reactor的callWhenRunning函数,并传给它一个我们想调用函数的引用来实现hello函数的调用。
由于Twisted循环是独立于我们的代码,我们的业务代码与reactor核心代码的绝大多数交互都是通过使用Twisted的APIs回调我们的业务函数来实现的。
reactor和我们的业务函数是在一个线程里的,我之前认为轮询是由另一个线程来完成(可能其他事件驱动的程序可能是这样的,比如服务生打电话,但reactor不是。)

很多标准的Python方法没有办法转换为非阻塞方式。例如,os.system中的很多方法会在子进程完成前一直处于阻塞状态,这也就是它工作的方式。所以当你使用Twisted时,避开使用os.system。
避免在自己的函数里写会阻塞的代码

reactor.stop() # 退出
reactor.callLater(1, self.count)

注册一个1秒后运行的函数,这个方法和之前Tk的方法名字都一样啊
你可以将超时作为图5中循环等待中的一种事件来看待。
Twisted的callLater机制并不为硬实时系统提供任何时间上的保证。

捕获它,Twisted
reactor并不会因为回调函数中出现失败(虽然它会报告异常)而停止运行。而是会继续执行下一个函数

4.由twisted支持的客户端
前面一节讲解了twisted的一些最基础语法。这一节开始分析两个twisted程序,一个是服务器,一个是客户端
第一个twisted支持的诗歌服务器
先讲解客户端:
twisted-client-1/get-poetry.py
这个文件有一个类PoetrySocket,其中有fileno,connectLost,doRead等方法,这些都是内置方法。当触发了对应的事件的时候reactor会自动调用他们。
addReader将自己传递给 reactor,给reactor指定监控的文件描述符。
doRead方法。当其被Twisted的reactor调用时,就会采用异步的方式从socket中读取数据。(IReadDescriptor)
fileno返回我们想监听的文件描述符,connectionLost是当连接关闭时被调用。(IFileDescriptor)
logPrefix(ILoggingContext)

doRead等其实就是一个回调函数,只是没有直接将其传递给reactor,而是传递一个实现此方法的对象实例。这也是Twisted框架中的惯例—不是直接传递实现某个接口的函数而是传递实现它的对象。这样我们通过一个参数就可以传递一组相关的回调函数。而且也可以让回调函数之间通过存储在对象中的数据进行通信。

twisted-client-1/get-poetry-broken.py 没有选择非阻塞的写法。效率和一开始的客户端没有区别

应该禁止使用其它各类的阻塞函数,如os.system中的函数。除此之外,当我们遇到计算型的任务(长时间占用CPU),最好是将任务切成若干个部分执行以让I/O操作尽可能地执行。

其他:两个低层reactor的APIs:removeReader和getReaders。

5.由Twisted扶持的客户端
三个新概念:
Transports, Protocols, Protocol Factories
一个Twisted的Transport代表一个可以收发字节的单条连接。对于我们的诗歌下载客户端而言,就是对一条TCP连接的抽象。但是Twisted也支持诸如Unix中管道和UDP。
Protocol实例是存储协议状态与间断性(由于我们是通过异步I/O方式以任意大小来接收数据的)接收并累积数据的地方。
factory是用来给不同protocol之间交换信息,一个protocol可以有不同factory
reactor run之前创建过程,Factory生成 protocol
reactor run之前
在Protocol创立的第二步便是通过makeConnection与一个Transport联系起来。
点击加入

处理接收到数据的主要方法是dataReceived

def dataReceived(self, data):
    self.poem += data

twisted-client-2/get-poetry.py 实现了上面的一切
twisted-client-2/get-poetry-stack.py 打印一个运行时的跟踪堆栈
IReadDescriptor实际上就是变相的Transport类

当transport的连接关闭时,conncetionLost回调会被激活。reason参数是一个twisted.python.failure.Failure的实例对象,其携带的信息能够说明连接是被安全的关闭还是由于出错被关闭的。

6.更加"抽象"的运用Twisted
客户端版本2仅仅比较初步地使用了Protocol和Factory,3优化了设计
使Factory只做好两件事,1、生成一个PoetryProtocol的实例 2、收集下载完毕的诗歌的工作
由get_poetry负责创建工厂和connectTCP
Factory用一个回调来将下载完毕的诗歌传回去。
这样子更好地复用这三个类,而开启和关闭reactor都在main函数中完成。

开始讨论异常问题
在一个异步交互的程序中,错误信息也必须异步的传递出去。
1、使用Exception
2、使用一个异常跟踪栈
所以,我们需要写一个处理错误信息的回调函数

7.小插曲 Deferred
defer解决回调异常处理问题:
client3-1中的poem_failed错误回调是由我们自己的代码激活并调用的,即PoetryClientFactory的clientConnectFailed函数。是我们自己而不是Python来确保当出错时错误处理代码能够执行。因此我们必须保证通过调用携带Failure对象的errback来处理任何可能的错误。
否则,我们的程序就会因为等待一个永远不会出现的回调而止步不前。

在异步程序中处理错误信息比处理正常的信息要重要的多,这是因为错误会以多种方式出现,而正确的结果出现的方式是唯一的。当使用Twisted编程时忘记处理异常是一个常犯的错误。

在程序中出现错误后,如果没有捕捉到,reactor都会一直运行下去,不会终止。
deferred不允许别人激活它两次
先使用d = defer.Deferred() 创建一个回调链,再使用addcallback和addboth往里面塞东西,最后
reactor.callWhenRunning(d.callback, ‘Another short poem.’)
可以调用reactor的callWhenRunning函数来激活deferred。再给callWhenRunning函数一个额外的参数传给回调函数。

小实验:
在defer-9.py中加入一行

def poem_done(_):  # done是回调链最后一环
    a = 1/0
    from twisted.internet import reactor
    reactor.stop()

依然会导致reactor无法结束的错误。

如果

def got_poem(poem):  # 接着执行done
    print(poem)
    a=1/0

系统不会报错但会正常结束。接着在done里面加入

def poem_done(_):
    print(_)
    from twisted.internet import reactor
    reactor.stop()

系统正常结束,且会报错误
应该保证最后addBoth的代码简单,这样不会出错。

8.使用Deferred的诗歌下载客户端
实现一个使用defer的客户端4.0:
之前3.0版本是直接使用回调,这样如果的话,出错就会GG,所以要使用deferred的d.callback调用回调
所以要先把d传入工厂,接着在poem_finish回调之后在工厂里调用callback

d, self.deferred = self.deferred, None  # 销毁其引用。这样做可以保证我们不会激活一个deferred两次。

讨论:
同步函数也能返回一个deferred,因此严格来说,返回deferred只能说可能是异步的。我们会在将来的例子中会看到同步函数返回deferred。

9.第二个小插曲,deferred
在3.1的基础上进行讲解,如果没有正确捕获异常会发生什么:
这个异常会传播到工厂中的poem_finished回调,即激活got_poem的方法
由于poem_finished并没有捕获这个异常,因此其会传递到protocol中的poemReceive函数
然后来到connectionLost函数,仍然在protocol中
然后就来到Twisted的核心区,最后止步于reactor。

在got_poem后面任何一步都没有可能以符合我们客户端的具体要求来处理异常的机会。
这段内容详细介绍了defered和异常处理。
在这里插入图片描述
在正常顺序调用时,最底层的是connect函数,get_poem的调用顺序显然应该在connect之前。
在回调情况下,最底层的是got_poem函数,它发生了错误之后,反而会传递到connect,所以:
由于bug可能存在于我们代码中的每个角落,因此我们必须将每个回调都放入try/except中,这样一来所有的异常都才有可能被捕获。这对于我们的errback同样适用,因为errback中也可能含有bugs。
回调链的执行顺序
上图为回调链一个可能的的执行顺序

未处理的defer异常(最后一步出现错误):
最后一个print函数成功执行,意味着程序并没有因为出现未处理异常而崩溃。
其只是将跟踪栈打印出来,而没有宕掉解释器
跟踪栈的内容告诉我们deferred在何处捕获了异常
"Unhandle"的字符在"Finished"之后出现。

之所以出现第4条是因为,这个消息只有在deferred被垃圾回收时才会打印出来。

Callbacks与Errbacks,总会成对出现
addCallbacks
addCallback
addErrback
addBoth
很明显的是,第一个与第四个是向链中添加函数对。当然中间两个也向链中添加函数对。addCallback向链中添加一个显式的callback函数与一个隐式的"pass-through"函数(实在想不出一个对应的词)。一个pass-through函数只是虚设的函数,只将其第一个参数返回。由于errback回调函数的第一个参数是Failure,因此一个"path-through"的errback总是执行"失败",即将异常传给下个errback回调。

deferred模拟器
这部分内容,没有译。其主要是帮助理解deferred,但你会发现,读其中的代码twisted-deferred/deferred-simulator.py ,可以更好的理解deferred。主要是我还没有理解,嘿嘿。所以就不知为不知吧。

10.增强defer功能的客户端
增强defer客户端5.0
在获得诗歌之后进行新的处理逻辑
如果上游的链返回了一个None,控权会进入到下一级的callback链中。

5.1版本改写:
之前在5.0版本中try_to_cummingsify函数依然使用了try/except语句,可以将其改写成使用defered的形式。
让deferred来捕获 GibberishError 与ValueError 异常在这里插入图片描述

总结:控制权在deferred的回调链中交错传递具体方向依赖于返回值的类型。

11.改进诗歌下载服务器(一)
服务器端使用Protocol来管理连接(transport)
实现源码:twisted-server-1/fastpoetry.py
Protocol是从Factory中获得诗歌内容的:
除了创建PoetryProtocol, 工厂仅有的工作是存储要发送的诗歌。

*connectionMade都会在Protocol初始化时被调用,只是我们在客户端处没有使用这个方法。
并且我们在客户端的portocal中实现的方法也没有在服务器中用到。因此,如果我们有这个需要,可以创建一个共享的PoetryProtocol供客户端与服务器端同时使用。*这样可以实现向服务器推送数据。
每一个客户端连接都有一个transport,代表一个client socket,加上listening socket总共是四个被select循环监听的文件描述符(file descriptor).

12.改进诗歌下载服务器(二)
实现一个诗歌样式转换服务器
样式转换服务需要两者进行双向交互-客户端将原始式样的诗歌发送给服务器,然后服务器转换格式并将其返回给对应的客户端。因此,我们需要使用或自己实现一个协议来实现这种交互。
客户端需要向服务器端发送两部分信息:转换方式与诗歌原始内容。服务器只是将转换格式之后的诗歌发送给客户端。这里使用到了简单的运程调用。
远程过程调用:
Twisted支持若干种能解决这个问题的协议:XML-RPC, Perspective Broker, AMP。
什么是netstring
twisted-server-1/transformedpoetry.py
格式转换服务与具体协议的实现是完全分离的。将协议逻辑与服务逻辑分开是Twisted编程中常见的模式。这样做可以通过多种协议实现同一种服务,以增加代码的重用性。

    def transform(self, xform_name, poem):
        thunk = getattr(self, 'xform_%s' % (xform_name,), None)

通过xfomr_前缀式方法来获取服务方法。
考虑到客户端可以发送任意的transform方法名,这是一种防止客户端蓄意使用恶性代码来让服务器端执行的方法。这种方法也提供了实现由服务提供具体协议代理的机制。
NetStringProtocol实现不错

客户端实现
twisted-server-1/transform-test

总结:
双向通信
基于Twisted已有的协议实现新协议
将协议实现与服务功能实现独立分开

13.使用Deferred新功能实现新客户端
twisted-deferred/defer-10.py
展示了deferred最复杂的功能,一个外层defer,一个内层defer,一开始外层先执行,当外层defer执行到内层时,暂停,当内层被激活时开始继续执行执行内层完毕,然后转向外层。
在这里插入图片描述

客户端版本6.0
使用新学的deferred嵌套来重写我们的客户端来使用由服务器提供的样式转换服务。
这里之所以要使用双重嵌套是因为我们要实现两个服务,第一步,从两个服务器下载诗歌,随后将这两首诗歌传给诗歌转换服务器并得到最终结果,我们把诗歌传给转换服务器时,reactor会运行,此时在进行等待转换后的诗歌到来(这一步如果作为一个单独的任务拆分出来是需要自己的deferred的。)。

14.Deferred用于同步环境
twisted-deferred/defer-11.py
第一组例子:
演示了deferred可以在返回之前就激活。可以在一个已经激活的deferred上添加回调处理函数。一个非常值得注意的是:已经被激活的deferred可以立即激活新添加的回调处理函数。

第二组例子:
演示了deferred中的pause与unpause函数的功能,pause可以暂停一个已经激活的deferred对其回调链上回调的激活,unpause可以解除暂停。这个机制类似于“当Deferred回调链上的回调函数又返回Deferred时,Deferred暂停自己”。

d = Deferred()
print('Pausing, then firing deferred.')
d.pause()
d.callback(0)

print('Adding callbacks.')
d.addCallback(callback_1)
d.addCallback(callback_2)
d.addCallback(callback_3)

print('Unpausing the deferred.')
d.unpause()

代理 1.0版本
一个有缓存的代理服务器版本:
该服务器既作为服务器向客户端请求提供本地缓存的诗歌,同时也要作为向外部诗歌下载服务器提出下载请求的客户端,因此其有两套协议/工厂,一套实现服务器角色,另一套实现客户端角色。
考虑到客户端端发送请求来时,缓存代理可能会将本地缓冲的诗歌取出返回,也有可能需要异步等待外部诗歌下载服务器的回复。如此一来,就会出现这样的情景:客户端发送来的请求,缓存代理处理请求可能是同步也可能是异步。
要解决这个需要,就用到了之前的特性:可以在返回Deferred前就激活。
两套协议/工厂,一套实现服务器角色,另一套实现客户端角色。
这里主要的问题是返回值不确定,
maybeDeferred函数解决了这个问题。如果返回值不是defer,它会把返回值打包为一个已经激活的defer传入回调链中。

d = maybeDeferred(self.factory.service.get_poem)

或者直接用succeed手动打包

return succeed(self.poem)  # 这里直接返回一个deferred

Deferred可以在激活后添加新的回调也间接说明了我们在第九部分twisted-deferred/defer-unhandled.py(提到的,deferred中会在最后一个回调中遇到未处理异常,并在此deferred被垃圾回收(即其已经没有任何外界引用)时才将该异常的情况打印出来。即deferred会在其销毁前一直持有异常,等待可能还会添加进来的回调来处理。

15、16测试和进程守护跳过,考虑到这两章对理解deferred并没有帮助。

17.构造"回调"的另一种方法
为什么generators 是创建回调的候选方法.
以生成器本身的角度看问题:

生成器函数在被循环调用之前并没有执行(使用 next 方法)
一旦生成器开始运行,它将一直执行直到返回"循环"(使用 yield)
生成器与循环总是交错进行,一个运行,一个就不运行:
当循环中运行其他代码时(如 print 语句),生成器则没有运行
当生成器运行时, 则循环没有运行(等待生成器返回前它被"阻滞"了)
一旦生成器将控制交还到循环,再启动可能需要等待任意时间(其间任意量的代码可能被执行)

这与异步系统中的回调工作方式非常类似.
把 while 循环视作 reactor, 把生成器视作一系列由 yield 语句分隔的回调函数.
注意( 所有的回调分享相同的局部变量名空间, 而且名空间在不同回调中保持一致.)

一次激活多个生成器:
twisted-intro/inline-callbacks/gen-3.py
理解生成器的send和throw方法

对比defer,如果我们需要使用内外层的defer的时候, 可以用生成器抛出一个deferred的方式来解决。
在等被抛出的(内层deferred)执行完毕后将结果传回生成器内继续执行。参见inline-callbacks/inline-callbacks-1.py的代码

使用 deferred或者inllineCallbacks,我们的回调仍然一次调用一个回调
inlineCallbacks特性:
当我们调用一个用 inlineCallbacks 修饰的函数时,不需要自己调用 send 或 throw 方法.修饰符会帮助我们处理细节,并确保生成器运行到结束(假设它不抛出异常).
如果我们从生成器yield一个非deferred值,它将以 yield 生成的值立即重启.
如果我们从生成器yield一个 deferred,它不会重启除非此 deferred 运行结束.如果 deferred 成功返回,则 yield 的结果就是 deferred 的结果.如果 deferred 失败了,则 yield 会抛出异常. 注意这个异常仅仅是一个普通的 Exception 对象,而不是 Failure,我们可以在 yield 外面用 try/except 块捕获它们.
(此例中用callLater 在一小段时间之后去激发 deferred。虽然这是一种将非阻塞延迟放入回调链的实用方法,但通常我们会生成一个 deferred,它是被生成器中其他的异步操作(如 get_poetry)返回的.这句没有看懂)
当我们实际调用经过inline装饰器装饰的函数时,返回一个deferred,这个deferred在生成器完全结束(或抛出异常)后才被激发.

阅读inline-callbacks/inline-callbacks-2.py,最终装饰过的函数通过returnValue返回值或者一个异常返回。

客户端7.0:

12.7的一些看法
需要注意javascript也是单线程的事件驱动
defer和promise之间的关系
由于python的很多库都是同步的,在用twisted写程序的时候存在很多坑,比如读取mysql数据库这种操作。
事实上inlinecallback装饰器已经接近协程的写法了,scrapy中大部分的写法都是如此。

生成器的惰性特征:所谓惰性计算,是函数式编程语言的一种特性,简单来说就是对于一个值,只有当用到它的时候才去计算,这个思想来自代数运算(对于很多未知数的方程组,事实上可以消掉一些不用求的未知数)。惰性运算的缺点是运行速度缓慢(每次用到一个值,编译器都会循环往上问:你有没有被求出来。。)
所以,考虑生成器表达式和列表表达式的不同行为,使用生成器是用空间换时间的一种策略。进一步参考后文的 惰性不是迟缓: Twisted和Haskell

猜你喜欢

转载自blog.csdn.net/kekefen01/article/details/84189501