twisted系列教程十三–deferred 中的deferred

Part 13: Deferred All The Way Down

原文:http://krondo.com/blog/?p=2159
作者:dave
译者:notedit
时间:2011.06.26

Introduction

回想一下第十部分的poetry client 5.1,client 用一个deferred 来管理一个callback 链,这个callback 链中调用了一个transformation 引擎,在client 5.1 中,这个引擎是作为一个同步的函数来实现的.

现在我们想写一个新的client,让它利用我们在第十二部分写的transformation service.问题就来了:既然transformation service 是通过网络访问的,我们需要用异步的I/O.这就意味着我们用来请求transformation 的api 是异步的.也就是说我们的try_to_cummingsify callback 会返回一个Deferred 对象

在一个deferred 链中的callback 返回另一个deferred 的时候会发生什么?让我们把第一个deferred 叫做外部的deferred 第二个deferred 叫做内部的deferred.假设在外部的deferred 中callback N 返回了 内部的deferred.这个callback 是在说”我是异步的,我的结果还没有来”.因为外部的deferred 需要调用下一个callback 或者errback,并传递当前callback 的返回值,外部的deferred 需要等待内部的deferred被触发.当然,外部的deferred 也不能是阻塞的,所以,此时外部的deferred暂定callback 链的执行并把控制圈交还给reactor.

外部的deferred 是怎样知道什么时候恢复呢? 很简单,通过给内部的deferred增加一个callback/errback 对.当内部的deferred 被触发的时候,外部的deferred 会恢复执行.假如内部的deferred成功了(例如:它调用了一个被外部的deferred增加的callback),外部的deferred则会继续调用它的N+1callback,假如内部的deferred 失败了,外部的deferred 会调用它的 N+1 errback.

让我们用一张图片来描述这个过程,图片二十八:
图片二十吧

在这张图片中,外部的deferred 有四对callback/errback.当外部的deferred 被触发时,第一个callback 返回了一个deferred(内部的deferred).这时候外部的deferred会停止触发它的callback 链,并把控制权交给reactor(在给内部的deferred 的增加了一对callback/errback 之后).然后,一段时间之后,内部的deferred触发,外部的deferred 也开始恢复运行.注意,外部的deferred 并不会自己触发内部的deferred.那也是不可能的,因为外部的deferred 不会知道什么时候内部的deferred的结果是可用的,或者结果是什么.我们的外部的deferred就是异步的等待内部的deferred 触发.

注意连接callback 和内部的deferred 的那跟线是黑色的而不是绿色或红色.那是因为我们我不知这个callback 是成功还是失败知道内部的deferred触发.直到那时候外部的deferred 才能知道是去调用下一个callback 还是下一个errback.

图片二十九描述了相同的外部的/内部的deferred 触发顺序,不过是站在reactor 的角度:
图片二十九

这个可能是deferred 最难懂的部分,如果你在短时间内不能消化也不要着急.我们会用具体的程序举例说明–twisted-deferred/defer-10.py.这个例子创造了两个外部的deferred,一个带有空白的callbacks,另一个有一个callback 并返回一个内部的deferred.通过学习这个例子你可以搞明白第二个外部的deferred是怎样在内部的deferred 返回的时候停止的,和 在内部的deferred被触发的时候外部的deferred 又是怎样恢复的.

Client 6.0

让我们用我们新学的嵌套的deferred 来重新实现一下我们的poetry client,并用上第十二部分讲到的transformation service,你可以在twisted-client-6/get-poetry.py 找到代码. poetry protocol 和protocol factory 和前一版本的client 都没有变化.但是增加了进行transformation 请求的protocol 和factory.下面是protocol 部分:

class TransformClientProtocol(NetstringReceiver):

    def connectionMade(self):
        self.sendRequest(self.factory.xform_name,
                                         self.factory.poem)

    def sendRequest(self, xform_name, poem):
        self.sendString(xform_name + '.' + poem)

    def stringReceived(self, s):
        self.transport.loseConnection()
        self.poemReceived(s)

    def poemReceived(self, poem):
        self.factory.handlePoem(poem)

使用NetstringReceiver 作为一个基类让这个protocol 相当的简单.只要连接一建立,我们从factory 中取到变形的名字和诗的内容并向server发送一个transform 请求.当我们得到返回的诗,我们把它传递给factory,下面是factory 的代码:

class TransformClientFactory(ClientFactory):

    protocol = TransformClientProtocol

    def __init__(self, xform_name, poem):
        self.xform_name = xform_name
        self.poem = poem
        self.deferred = defer.Deferred()

    def handlePoem(self, poem):
        d, self.deferred = self.deferred, None
        d.callback(poem)

    def clientConnectionLost(self, _, reason):
        if self.deferred is not None:
            d, self.deferred = self.deferred, None
            d.errback(reason)

    clientConnectionFailed = clientConnectionLost

这个factory 是被client 设计的,处理一个transformation 请求,并存储着transform 的名字和这首诗的内容.factory 创造了一个代表了这个transformation请求返回结果的deferred.注意这个factory 是怎样处理两种错误情况的:一个是连接错误的情况一个是还没完全接受到返回值的时候连接就断开的情况.也注意clientConnectionLost 方法就算我们接受诗成功最后也会调用,但是在这种情况下self.deferred 已经被handlepoem 设置为None了.

这个factory 创建了一个deferred 也触发了它,这是一个很好的方法:

一般来说,一个创造了deferred 的对象,也应该负责触发那个deferred

这个”你创造它,你触发它”规则帮助我们保证一个deferred 仅仅被触发一次,也让程序流程更简单一些.

除了这个transform Factory,这里还有一个proxy 类,它隐藏连接transform server 的具体信息:

class TransformProxy(object):
    """
    I proxy requests to a transformation service.
    """

    def __init__(self, host, port):
        self.host = host
        self.port = port

    def xform(self, xform_name, poem):
        factory = TransformClientFactory(xform_name, poem)
        from twisted.internet import reactor
        reactor.connectTCP(self.host, self.port, factory)
        return factory.deferred

这个类提供了一个xform()接口,其他的代码可以用它来发送transformations 请求.所以其他的代码就可以仅仅发送一个transformations 请求然后得到一个deferred,而不用再去关心ip 和端口号.

其他的代码变化的地方还有 try_to_cummingsify callback:

def try_to_cummingsify(poem):
    d = proxy.xform('cummingsify', poem)

    def fail(err):
        print >>sys.stderr, 'Cummingsify failed!'
        return poem

    return d.addErrback(fail)

这个callback 现在返回一个deferred,但是我们不用改变其他的main 函数中的代码.因为try_to_cummingsify 本来就在deferred 的链中,它已经是异步的了,其他的就不用变化了.

你可能会发现我们返回的是d.addErrback(fail) 的结果,这里是用了一些语法糖.addCallback 和 addErrback 都返回原来的deferred.我们也可以写成:

d.addErrback(fail)
return d

Testing out the Client

这个新版的client 和其他的client相比有一些语法上的变化,假如你有一个transformation service 运行在10001 端口,两个poetry server 运行在10002 和 10003 上,你应该这样启动client:

python twisted-client-6/get-poetry.py 10001 10002 10003

你可以这样启动transformation service :

python twisted-server-1/transformedpoetry.py --port 10001

这样启动poetry server:

python twisted-server-1/fastpoetry.py --port 10002 poetry/fascination.txt
python twisted-server-1/fastpoetry.py --port 10003 poetry/science.txt

Wrapping Up

在这一部分我们学习了在一个callback 链中一个deferred 怎样透明的处理其他的deferred.我们可以安全的增加异步的callback到一个外部的deferred 中.这个是非常有用的因为我们的很多函数都要求是异步的.

我们知道deferred 的全部的事情了么? 还没有.还有很重要的一点,我们会在第十四部分讲到.

发表在 python, twisted | 评论关闭

twisted系列教程十二–为server 增加一个service

Part 12: A Poetry Transformation Server

原文:http://krondo.com/blog/?p=2101
作者:dave
译者:notedit
时间:2011.06.25

One More Server

第九部分第十部分我们介绍了关于诗歌的变形引擎的想法,最后我们实现了cummingsifier,我们还让它抛出随机的异常来模拟错误.但是假如这个变形的引擎在另外一台服务器上,提供一种网络的”poetry transformation service”, 那么现在又多出来一种出错的方式:变形引擎挂掉了.

所以在第十二部分我们将要实现一个poetry transformation server,并在下一部分,我们让我们的client 使用一个外部的transformation service,并从中学到Deferred 的一些东西.

Designing the Protocol

到现在为止client 和server 端的交互都是单向的,server 端向client端发送一首诗,而client 什么也向server 发送.但是一个transformation service 是双向的– client 端向server发送一首诗然后服务端发送给client 一首变形后的诗.所以我们需要一个新的protocol来处理这种交互.

在我们实现这个的时候,我们让server 支持多种的变形方式,并让client 可以选择使用哪一种.所以client 需要发送给server两个数据:变形的方式和这首诗的内容.我们的server 会返回变化后的诗.所以呢,我们已经完成了一个简单的Remote Procedure Call.

twisted 包含了几个实现这个问题的几种protocol,包括XML-RPC, Perspective Broker, 和 AMP.

但是呢,用这些已经实现了的protocol 会让我们走的太远了,所以最好我们还是自己实现一个.让我们的client 发送一个如下的字符串:

.

也就是一个变形引擎的名字和这首诗歌的内容中间用一个点连接.我们还会把这个字符串编码成netstring 的形式.server 端会返回已经变形过的诗,也是netstring 的方式.因为netstring 使用了带长度的编码,client 可以检测到是否server端发送了一个完整版本的诗.如果你回想一下的话,我们原来的protocol 很难检测到中途停止发送的情况.
有关protocol 的设计先介绍这些,对于我们来说已经够用了.

The Code

让我们来看一下我们的ransformation server,在 twisted-server-1/transformedpoetry.py,首先,我们定义了一个TransformService 类:

class TransformService(object):

    def cummingsify(self, poem):
        return poem.lower()

这个transform service 目前只支持一种变形–cummingsify.通过一个同名方法.我们还可以增加其他的算法.我们需要注意的是:transformation service 是完全独立于我们之前所说的protocol 的.把protocol 的逻辑和 service 的逻辑分开在twisted 编程中是一个经常用的模式.这样做的话可以通过不同的protocol 来提供相同的transformation service而不用修改太多的代码.

下面让我们看一下protocol factory:

class TransformFactory(ServerFactory):

    protocol = TransformProtocol

    def __init__(self, service):
        self.service = service

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

        if thunk is None: # no such transform
            return None

        try:
            return thunk(poem)
        except:
            return None # transform failed

    def xform_cummingsify(self, poem):
        return self.service.cummingsify(poem)

这个factory 提供了一个变形的方法,一个protocol 可以用它来得到一个poetry transformation.如果没有这个方法相对应的transformation或这个transformation失败了,这个方法会返回None.就像TransformService 一样,这个protocol factory 和protocol 也是相互独立的,但是factory 中的方法protocol 都是可以调用的.

你需要注意的是我们是怎样保护带有xform_前缀地 方法.这是一种在twisted 源码中经常用的模式.这是一种防止客户端的代码直接调用service 对象方法的方法,因为客户端可能传递过来任何的 transform name.

下面我们来看一下protocol 的实现:

class TransformProtocol(NetstringReceiver):

    def stringReceived(self, request):
        if '.' not in request: # bad request
            self.transport.loseConnection()
            return

        xform_name, poem = request.split('.', 1)

        self.xformRequestReceived(xform_name, poem)

    def xformRequestReceived(self, xform_name, poem):
        new_poem = self.factory.transform(xform_name, poem)

        if new_poem is not None:
            self.sendString(new_poem)

        self.transport.loseConnection()

在protocol 的实现中我们利用了Twsited 已经实现的NetstringReceiver protocol.这个基类会负责netstring 的编码和解码,我们所要去做的就是实现 stringReceived 方法.换句话说,stringReceived会被传递进从client 那边接收过来的诗的内容.这个基类也会负责缓冲传进来的数据直到我们有足够的数据去解码出来一首完整的诗.

假如一切都正常的话我们会通过NetstringReceiver 提供的sendString方法发送变过形的诗到客户端.这就是全部要做的工作.main 函数由于没有什么变化,我们在这里就不讲了.

注意我们是怎样通过定义xformRequestReceived 方法利用twisted 模式将传进来的字节流变化成更高级的抽象,最后xformRequestReceived 被传递进两个参数–一个是变形的名字另一个是诗的内容.

A Simple Client

在第十三部分我们会实现一个利用ransformation service 的twisted client.但现在我们仅仅用一段脚本来测试我们的server,代码在twisted-server-1/transform-test.它用netcat程序向server 发送一首诗歌的内容并打印出返回的结果.让我们先开启我们的server:

python twisted-server-1/transformedpoetry.py --port 11000

然后运行我们的测试脚本:

./twisted-server-1/transform-test 11000

你看看到下面的额输出:

15:here is my poem,

Discussion

在这一部分我们介绍了一些新的想法:

  1. 双向的通信
  2. 在twisted 已经提供的protocol 实现上进行编程
  3. 使用一个service 对象来分开功能逻辑和protocol 逻辑

双向通信的结构是非常简单的,读数据和写数据中我们都使用了同样的技术,与之前的server 和client 不同的是我们同时使用了它们两个.当然,一个更复杂的protocol会需要更复杂的代码来处理字节流,这也是我们为什么使用了一个已经存在的protocol 实现.

一但你熟悉了一些基本的protocol 的实现,建议你去看看twisted 自带的一些复杂的协议.你可以从twisted.protocols.basic 模块读起.写一些简单的protocol 是你熟悉twisted 编程的很好的方式.在一些真正twisted 程序中,更有可能去使用一个已经被实现的protocol.

使用一个service对象来把功能逻辑和protocol逻辑分开在twisted 编程中是一个很重要的设计模式,尽管我们今天将的这个变形的service 不是特别重要,你可以想象在一个大型的应用中一个service可能会非常复杂.通过让service 和 protocol 分离,我们可以快速的在不同的protocol上提供相同的service.

图片二十七描述了一个transformation server通过两种不同的protocol 的来进行服务:
图片二十七

尽管我们需要两个protocol factory,它们可能也就protocol 不同.这个两个protocol factory共用一个service 对象,剩下的就是protocol 需要分开实现.这样我们就实现了数据复用.

Looking Ahead
有关我们的transformation server 暂时就讲这么多,在第十三部分,我们会继续改进我们的client,让它可以直接利用我们的service.

发表在 python, twisted | 评论关闭

twisted系列教程十一 — 一个twisted 的服务端

Part 11: Your Poetry is Served

原文:http://krondo.com/blog/?p=2048
作者:dave
译者:notedit
时间:2011.06.23

A Twisted Poetry Server

既然我们已经学了这么多twisted client 的编写,现在让我们来用twisted来重新实现一下我们的poetry server 吧.我们要多谢谢twisted 抽象的普遍性,貌似我们已经学了twisted 的我们需要知道的大部分东西了.看下我们的twisted poetry server twisted-server-1/fastpoetry.py,它被叫做fastpoetry 应为这个server可以尽可能快的发送一首诗.它的代码要比cilent 中的代码要少.

让我们一次分析一小段的代码,首先,PoetryProtocol:

class PoetryProtocol(Protocol):

    def connectionMade(self):
        self.transport.write(self.factory.poem)
        self.transport.loseConnection()

就像client 一样,server 用了一个单独的protocol 实例来管理每一个不同的连接.这里的protocol 是我们server端的poetry protocol.因为我的protocol 是严格的单向的(one-way不知道怎么翻译),server protocol 实例仅仅需要关心发送数据.我们的protocol 需要在连接建立之后立即开始传送数据,所以我们实现了connectionMade 方法,它是一个callback,在一个protocol 实例连接上一个transport之后触发.

我们的connectionMade方法让transport 去做两件事:发送整首诗的内容(self.transport.write)和关闭连接(self.transport.loseConnection).当然,这两个操作都是异步的.所以write方法的调用意味着”最后我会把所有的数据都送出去”,close方法的调用意味着”一但我让你送的数据都送完了再关闭这个连接”.

正如你看到的,protocol 从factory那里接受整首诗歌的内容,就像下面的代码所描述的:

class PoetryFactory(ServerFactory):

    protocol = PoetryProtocol

    def __init__(self, poem):
        self.poem = poem

我们的factory 真正干的工作除了创建PoetryProtocol 的实例以外,也保存了PoetryProtocol 需要的poem 的内容.
注意我们这里实现的是ServerFactory而不是ClientFactory.因为我们的server 是被动的监听连接,我们不需要ClientFactory提供的额外的方法.我们是怎么确定要用 ServerFactory的呢?因为我们要用reactor 的listenTCP 方法,手册中讲到那个方法的factory 参数需要是一个ServerFactory 的实例.

下面是main 函数:

def main():
    options, poetry_file = parse_args()

    poem = open(poetry_file).read()

    factory = PoetryFactory(poem)

    from twisted.internet import reactor

    port = reactor.listenTCP(options.port or 0, factory,
                             interface=options.iface)

    print 'Serving %s on %s.' % (poetry_file, port.getHost())

    reactor.run()

它已经做了三件事情:

  1. 读取我们将要服务的诗的内容
  2. 创建一个PoetryFactory
  3. 用listenTCP去告诉twsited 在一个端口上监听连接,并用我们的factory为每一个连接 创建protocol 的实例

做完这些之后,剩下的要做的事情就是告诉reactor 开始循环.你可以用以前咱们写的client 测试一下.

Discussion

回想一下在五部分的图片八图片九.这些图片描述了一个新的protocol 实例是怎样在twisted 创建一个新的连接之后被创建和初始化的.在server 端twisted 接受一个新来的连接也是采用了同样的原理.那就是为什么 connectTCP 和 listenTCP都需要一个factory 的参数.

在图片九中我们没有描述的是connectionMade callback 会在Protocal 初始化的时候被调用,不管什么情况下都会发生的,但是我们在client 用不到它.我们在client 中用到的一些protocol 方法在server 的protocol 中也不会用到.如果我们想折腾的话,我们可以写一个统一的protocol,又可以用在客户端又可用在服务端,在twisted 中的一些protocol 也确实是这么做的.例如,NetstringReceiver protocol 既可以用来从Transport中读也可以向Transport 中写.

我们暂时就不写一个low-level 的poetry server了,但是我们要想明白这个twisted server到底做了什么.首先,调用listenTCP 让twisted 创建一个监听的socket并把它加入到事件循环里面.这个socket 上的事件并不是告诉你有数据可读了,而是告诉你有一个client 在等待要连上来.

twisted 会自动的接收传过来的连接请求,并创建一个client socket 连接server 和 client.这个client socket也被加入事件循环,然后twisted 创建一个新的transport 和 一个PoetryProtocol 的实例来为这个client socket 服务. 所以Protocol 实例是一直和client socket 连着的,而不是负责监听的socket.

图片二十六描述了这个过程:
图片二十六

在这张图片中有三个client 连接着poetry server.每一个Transport 代表了一个client socket,listening socket 总共创建了四个文件描述符并让select loop 来监测.当一个client 断开跟它关联的Transport,PoetryProtocol 会被间接引用被被垃圾回收器回收.同时PoetryFactory会继续存在只要我们还在监听新的连接到来.

client sockets 和他们关联的python 对象不会活太久如果我们要处理的诗很短的话.但是如果有成千上百的并发的client 的话,我们的poetry server 会垮掉.twisted 本身对同时能处理的连接数量并没有内置的限制.当然,随着你不断增加server 的负载,你会发现你的server最后会撑不住,或者达到了操作系统内部的限制.对于高负载的系统来说,认真的抗压测试是非常重要的.

twisted 对我们能监听的端口的数量也没有限制.实际上,一个twisted 的进程可以监听n多的端口并在不同的端口提供不同的服务.
我们的server 还少很多东西.首先,它没有记录任何的可以帮助我们进行dedug 或者分析网络流浪的日志.在一个是,这个server 不是以守护进程来运行的,这就让它很容易挂掉,比如不小心按了Ctrl-C,或者登出了.我们在将来都会修复这些问题.在第十二部分,我们将会写令一个可以变换诗的server.

发表在 twisted | 评论关闭

twisted系列教程十–可以变化的诗

Part 10: Poetry Transformed

原文:http://krondo.com/blog/?p=1956
作者:dave
译者:notedit
时间:2011.06.22

Client 5.0

现在我们将要想我们的client中加入一些变形逻辑.但是首先我不得不说:我不知道怎样写一个Byronification 引擎,它超出我的能力范围了.做为替代,我会实现一个相对简单的变形–Cummingsifier.Cummingsifier 是可以把一首诗变成令一首cumming风格的诗的算法.下面就是这个算法的实现:

def cummingsify(poem)
    return poem.lower()

不幸的是,这个算法很简单以至于很难失败,所以在client 5.0 版本中在 twisted-client-5/get-poetry.py中,我们用了一个可以随机出现以下结果的算法:

  1. 返回一个正常的结果
  2. 抛出一个GibberishError错误
  3. 抛出一个ValueError 错误

通过这种方法我们模拟了一个有时返回异常的复杂的算法.
在client 5.0 中唯一变化的是poetry_main 函数:

def poetry_main():
    addresses = parse_args()

    from twisted.internet import reactor

    poems = []
    errors = []

    def try_to_cummingsify(poem):
        try:
            return cummingsify(poem)
        except GibberishError:
            raise
        except:
            print 'Cummingsify failed!'
            return poem

    def got_poem(poem):
        print poem
        poems.append(poem)

    def poem_failed(err):
        print >>sys.stderr, 'The poem download failed.'
        errors.append(err)

    def poem_done(_):
        if len(poems) + len(errors) == len(addresses):
            reactor.stop()

    for address in addresses:
        host, port = address
        d = get_poetry(host, port)
        d.addCallback(try_to_cummingsify)
        d.addCallbacks(got_poem, poem_failed)
        d.addBoth(poem_done)

    reactor.run()

当这个程序从server上下载一首诗,还会做下面三件事情中的一件:

  1. 打印出cummingsified 版本的诗
  2. 在输出原来的诗后然后打印"Cummingsify failed!"
  3. 打印出“The poem download failed.”


尽管我们已经有了从多个server上同时下载诗的能力,但是在测试client 5.0 的时候最好只用一个server,并多运行client 几次,并试着在不开server的时候运行client.
让我们画一下在get_poetry中Deferred的callback/errback 链:
图片十九
注意pass-through errback,它传递它接收到的任何的Failure到下一个errback(poem_failed).所以poem_failed 可以处理从get_poetry 和cummingsify 传过来的错误.

让我们分析一下我们的deferred 的不同的触发方法.如果我们想要一个正常的正确返回结果,获取诗的流程应该像图片二十:
图片二十
在这种情况下 没有callback失败,callback一路向下调用.这时候poem_done 会接收到None作为它的结果,因为got_poem 没有返回一个值.假如我们想让后面的callback可以访问到诗的内容,我们可以修改got_poem 并让它明确的返回一首诗.

图片二十一描述了cummingsify 抛出了GibberishError 错误的时候:
图片二十一
因为try_to_cummingsify 抛出了GibberishError 错误,poem_failed 被调用,并接收到Failure 对象.
poem_failed 并没有抛出一个异常,它执行完成后并转向poem_done.我们让poem_failed来处理错误并返回一个None值是一个合理的行为.另一方面,如果我们让poem_failed继续传递这个错误,那poem_done errback 就会被调用.
注意我们这里got_poem 和poem_failed 永远不会失败,所以poem_done errback 永远不会调用.但是加上它是安全的,因为你不知道got_poem 和poem_failed 有没有我们不知道的bug. addBoth 保证了这个函数无论deferred如何被触发都会被调用,addBoth 有点像try/except 中的finally 语句.

现在我们来看一下我们成功下载了一首小诗,但是cummingsify 函数抛出了一个ValueError 的的情况,图片二十二描述了这个过程:
图片二十二

图片二十二跟图片二十是一样的,除了got_poem接收的诗是原来的版本而不是已经转化过的版本.这种变化在try_to_cummingsify callback 中,它用try/except捕捉了ValueError并返回原来版本的诗. deferred 对象一点也感觉不到错误的存在.

最后我们用图片二十三描述一下我们从一个不存在的server上下载一首诗的情况:
图片二十三
根据以前的流程可知,poem_failed 返回None 所以程序流向下一层的poem_done callback.

Client 5.1
在client 5.0 中,我们用了一个普通的try/except 语句来捕捉在try_to_cummingsify callback 中出现的异常,而不是让deferred 首先捕捉它.这种方法本身并没有什么错误,但是我们要知道怎么做才是标准的twisted 范.
让我们假设我们想让deferred 同时捕捉GibberishError 和 ValueError 异常然后把它们交给errback.为了保证能正确处理异常,我们的errback需要去检查错误类型是否是ValueError,假如是的话,返回原来的诗,所以程序流又能返回到callback 并把原来版本的诗打印出来.

但是这里有一个问题:errback 不会得到原来的诗,它只会得到一个Failure 对象.为了让errback能处理这个错误,我们需要做一些变化让errback 能接收到原来的诗.

一个修改的方法就是修改cummingsify 函数,让原来的诗的内容包含在异常中.这个就是我们client 5.1 要完成的功能,代码在 twisted-client-5/get-poetry-1.py.我们把ValueError 变为了一个普通的CannotCummingsify异常,并把原来的诗的内容作为参数.

假如cummingsify 是一个外部模块的函数,那最好的办法就是用另一个可以捕捉除GibberishError之外所有异常的函数包装它,并抛出一个CannotCummingsify 异常.经过这些变化,我们的poetry_main 函数可以变成如下:

def poetry_main():
    addresses = parse_args()

    from twisted.internet import reactor

    poems = []
    errors = []

    def cummingsify_failed(err):
        if err.check(CannotCummingsify):
            print 'Cummingsify failed!'
            return err.value.args[0]
        return err

    def got_poem(poem):
        print poem
        poems.append(poem)

    def poem_failed(err):
        print >>sys.stderr, 'The poem download failed.'
        errors.append(err)

    def poem_done(_):
        if len(poems) + len(errors) == len(addresses):
            reactor.stop()

    for address in addresses:
        host, port = address
        d = get_poetry(host, port)
        d.addCallback(cummingsify)
        d.addErrback(cummingsify_failed)
        d.addCallbacks(got_poem, poem_failed)
        d.addBoth(poem_done)

我们的deferred 的callback/errback 链就成下图了,图片二十四:
图片二十四
检查cummingsify_failed errback:

def cummingsify_failed(err):
    if err.check(CannotCummingsify):
        print 'Cummingsify failed!'
        return err.value.args[0]
    return err

我们用了check 方法来检测绑定在Failure 对象上的异常是否是CannotCummingsify 的实例.假如是的话,我们返回给异常第一个参数并处理这个错误.因为返回的不再是一个Failure 对象,程序流转向callback执行.否则的话我们返回一个Failure 对象,并传递给下一层的errback.

图片二十五描述了当我们遇到一个CannotCummingsify 异常的时候会发生什么:
图片二十五

所以当我们用deferred 的时候,我们可以选择是用try/except 去处理异常,还是让deferred 把错误再路由出去.

Summary
在第十部分我们用deferred 路由错误的功能来改变我们的client,尽管这个例子不是特别真实的,它确实描述了在一个deferred中怎样根据callback 和errback返回的结果来控制程序的流程.
现在我们已经知道了deferred 的所有的事情了吗?还没有.我们还会继续讲解deferred 在未来的部分.但是我们 先绕个路,在第十一部分,实现我们的twisted poetry server.

发表在 twisted | 评论关闭

twisted系列教程九–Deferred 的第二个小插曲

Part 9: A Second Interlude, Deferred

原文:http://krondo.com/blog/?p=1825
作者:dave
译者:notedit
时间:2011.06.21

More Consequence of Callbacks

我们将要再来研究一下callback,尽管我们已经对deferred比较了解而且已经可以写出twisted 风格的异步程序,Deferred 类提供了更多的特色来进行处理一些更复杂的设置.所以我们要想出一些更复杂的设置来看看用callback编程的时候能给我们造成哪些挑战.然后我们会研究deferred是怎样处理这些挑战的.

为了激发我们继续讨论,我们将会向我们的poetry client增加一个新的功能.假设某的蛋痛的科学家发明出了一个特别的算法,这个算法可以将一首诗变成另一首诗.而且我们的导师提供了一个参照的实现,用下面的接口来实现:

class IByronificationEngine(Interface):

    def byronificate(poem):
     """
    Return a new poem like the original, but in the style of Lord Byron.

   Raises GibberishError if the input is not a genuine poem.
    """

就像很多新的软件的软件一样,这个实现有很多bug.这意味着除了注释中说的错误以外,byronificate方法如果遇到一些特殊情况也会抛出随机的异常.我们也会假设我们的引擎运行的足够快,下面是我们对我们的程序的期望:

  1. 试着下载一首小诗
  2. 如果下载失败,告诉用户我们不能完成下载
  3. 如果我们下载完成了,试着用IByronificationEngine进行转化
  4. 如果这个引擎抛出了GibberishError错误,告诉用户我们不能完成下载
  5. 如果这个引擎抛出了另外的异常,则保持原来的诗不变
  6. 如果我们得到了一首诗,打印出来
  7. 结束程序

这个构想是说如果出现了GibberishError 则意味着我们不能得到最终的诗,我们会告诉用户下载失败.这对调试没什么作用,但是我们的用户最想知道的是诗有没有下载成功.另一方面,假如这个引擎由于某些原因出现了失败,我们直接传给用户我们从服务器上接收到的诗.毕竟有些东西总比什么也没有强.

这里是我们程序的同步版本:

try:
    poem = get_poetry(host, port) # synchronous get_poetry
except:
    print >>sys.stderr, 'The poem download failed.'
else:
    try:
        poem = engine.byronificate(poem)
    except GibberishError:
        print >>sys.stderr, 'The poem download failed.'
    except:
        print poem
       # handle other exceptions by using the original poem
    else:
        print poem

sys.exit()

这个代码通过重构还可以更简单一些,但是它已经把要做的描述的很清晰了.我们想把我们的twisted poetry client4.0也改成这个结构,我们会在第十部分完成.现在,我们先来改造我们的client 3.1,client 3.1 没有用deferred.假设我们不用考虑来处理异常,仅仅把got_poem callback 改成这样:

def got_poem(poem):
    poems.append(byron_engine.byronificate(poem))
    poem_done()

在byronificate 方法发生抛出GibberishError 或者其他异常的时候会发生什么? 看看第六部分的图片十一,我们可以看到:

  1. 在factory中,异常会被传递到poem_finished callback,那个方法最终触发callback
  2. 因为poem_finished 不会捕捉异常,它会继续运行至protocol中的poemReceived
  3. 然后运行到connectionLost
  4. 然后就是运行到twisted 内部了,最后停止reactor

根据我们已经学过的,reactor 会捕捉和记录异常而不是崩溃掉,但它肯定不会做的就是告诉用户它不能下载一首诗.reactor 不会了解任何的诗或者GibberishErrors,它只是一般性的一块代码,被用来处理各种各样的网络连接.

现在注意,在上面讲的每一步,异常被传递到越来越具有一般性的代码上.在got_poem 以后的每一步都不适合用来处理异常,这种情况是和同步程序中的异常传播的方法是完全相同的.看一下图片十五,一张同步程序中的调用的堆栈信息:
图片十五
主方法是”high-context”,意味着他对整个程序都了解,为什么它存在,它是让整个程序运转的.典型的例子是,main方法可以利用命令行的参数来表明用户想让这个程序做什么.

连接socket 的方法是”low-context”的.它所知道的就是去连接一个网络地址.它不知道令一边是什么也不知道为什么我们需要连接.connet 是一个很具有一般性的方法–你可以不管你连接的是什么服务.

get_poetry 则在中间,它知道他要获取一首诗,但是不知道如果没有获取到该怎么办.

所以一个被connect抛出的异常会往上抛,从一般性的low-context到具有特殊功能的high-context,直到异常遇到可以处理它的代码.

异常是逐渐往上抛的而不是迭带的寻找”high-context”代码.在一个典型的同步程序中”up the stack ” 和”towards higher-context” 是相同的方向(这里实在不直到怎么翻译好).
现在回想一下我们将要对client 3.1 进行的改造,堆栈信息将会如下-图片十六:
图片十六
问题现在很明显了:在一个callback过程中,reactor(low-context) 会调用”high-context” 的代码,并以此类推.假如一个异常没有被立即的处理,这个异常会向”low-context”代码传递,”low-context” 对异常一无所知,所以这个异常就不会被处理.

一但一个异常被传进到twisted 的内部代码,这个程序就要崩溃掉了.这个异常不会被处理,它顶多会被reactor记录一下.所以当我们不用deferred编程的时候,我们必须认真的处理每一个异常.
因为bug无处不在,我们应该需要在我们的callback外层都包装一个try/except,以便异常可以被很好的处理,errback 也同样需要啊,因为处理错误的代码也可能出错哇.

The Fine Structure of Deferreds

Deferred已经帮我们解决这个问题了,当deferred触发callback 或者errback 的时候,它捕捉任何的异常.换句话说,一个deferred相当于包装在外层的try/except,所以根本不用我们自己写这个包装.但是deferred怎么处理捕捉到的exception?很简单—它把异常(以Failue 的方式)传递给下一个errback.

所以第一个被deferred加入的errback会处理所以向deferred发出错误信号的异常,第二个errback来处理第一个第一个errback或者callback抛出的异常,然后一直下去.

回想一下图片十二,描述了一个带有callback和errback链的deferred.我们假设第一个的callback/errback对为第0层,下一对为第1层,并依次类推.

在第n层,假如callback 或者errback 失败了,则第n+1层的errback会被触发,并传递一个Failure 对象.

通过传递exception,deferred把异常往”higher-context”方向传,这就意味着触发deferred 的callback 或者errback方法对触发者来说不会造成什么异常.所以底层的代码可以安全的触发deferred而不用关心捕捉异常.相反的,高层的代码可以通过向deferred中增加errback来捕捉异常.

在同步的程序中,异常只要被捕捉住就会停止传播.所以errback怎样标明自己已经捕捉驻异常了呢? 很简单,通过不再抛出那个异常.在这种情况下,执行转向callback,在第n层,如果callback 或者errback执行成功了,都会转向第n+1层的callback.

让我们来总结一下deferred 的触发模式:

  1. 一个deferred 包含一个有序的callback/errback 链,按照它们被加入的顺序排列
  2. 在第0层,第一个callback/errback对会被触发当deferred被callback方法触发的时候,第0层的callback会被调用,如果deferred被errback触发则第0层的errback被调用
  3. 如果第n层失败,则第n+1层的errback被调用
  4. 如果第n层成功,则第n+1层的callback调用

可以用下面的图片十七来描述:
图片十七

图中绿色的线表示了当一个callback或者errback成功的时候运行的过程,而红色的线则表示了失败的时候运行的路线.图片十七展示了所有的情况,但一个callback/errback对 只会有一个可以运行,图片十八展示了其中的一个过程:
图片十八

在图片十八中,deferred 的callback方法被调用,并触发了第0层的callback.这个callback成功了然后转向第1层的callback,…….(略去了,整个调用过程一看就明白)

在图片十八中,我们已经指出第3层会成功的执行,但要是在第3层的callback中失败了呢,后面没有errback可以继续处理异常,我们就说错误是没有被处理的.

在同步程序中没有被处理的异常会让程序崩溃掉,在一个不用deferred 的异步程序中,一个没有被处理的异常会被reactor捕捉和记录.在deferred中如果有一个没有被处理的异常会发生什么呢?让我们来看一下.例子在twisted-deferred/defer-unhandled.py,例子中会触发一个只有一个callback 的deferred,并会抛出一个异常,下面是输出:

Finished
Unhandled error in Deferred:
Traceback (most recent call last):
...
--- ---
...
exceptions.Exception: oops

一些要注意的事情:

  1. 最后的print语句运行了,所以这个程序没有因为异常而崩溃掉
  2. 那意味着traceback已经输出了,python 的解释器没有崩溃掉
  3. traceback 的内容告诉了我们deferred对象在哪里捕捉了异常
  4. "unhandled" 的提示在打印出"Finished"以后被输出

所以当你用deferred 的时候,在callback中没有处理的异常仍然会被记下,作为调试用.但是它们不会让程序崩溃掉(实际上异常根本不会到达reactor,deferred会首先捕捉到它们).顺便说一下,”Finished” 比”Unhandled”早输出的原因是”Unhandled”语句直到deferred 被垃圾回收的时候才被输出.我们在将来的章节会讲到.

我们可以在同步程序中用raise 再一次抛出异常,这样做可以重新抛出原来的异常并可以让采取一些措施来处理错误但又不完全处理完.我们在errback中也可以这么做,如果出现下列的一些情况,deferred会认为callback/errback 已经失败:

  1. callback/errback 抛出任何的异常
  2. callback/errback 返回Failure 对象

既然errback 的第一个参数是Failure,errback 可以通过返回这个参数重新抛出异常,然后让后面的程序去捕捉它.

Callbacks and Errbacks, Two by Two
你需要明白的是,你向deferred 中加入的callback/errback 的顺序对于deferred怎样被触发有很大的影响.在一个deferred中,callback 和errback 往往是成对出现的.有四种方法你可以向Deferred中加入callback/errback:

  1. addCallbacks
  2. addCallback
  3. addErrback
  4. addBoth

很明显的,第一个和最后一个向callback/errback链中加入一对,addCallback 方法增加一个明确的callback 和一个隐含的errback,这个隐含的errback仅仅返回它的第一个参数,第一个参数永远是一个Failure 对象.addErrback 同理.

The Deferred Simulator

熟悉deferred 触发它们的callback和errback 是很重要的.twisted-deferred/deferred-simulator.py是一个deferred 模拟器.运行这个程序会让你进入一系列的callback 和errback.你并可以指定它们的返回结果.

Summary
在探讨这么多的callback 之后,我们意识到让exception往上冒泡并不是一个很好的选择,因为callback部分的程序处在low-context 和high-context 之间.Deferred 可以处理这个情况,通过捕捉异常然后把它们送往下层的callback/errback,而不是让异常传递到reactor中去.
我们还学到了正常的返回结果也会向下运行,这样的话错误结果和正常结果就组成了一种交叉调用的模式.

有了这些,我们会在第十部分更新我们的poetry client.

发表在 twisted | 评论关闭

twisted系列教程八–延迟的诗

Part 8: Deferred Poetry

原文:http://krondo.com/blog/?p=1778
作者:dave
译者:notedit
时间:2011.06.19

Client 4.0

既然我们已经对deferred有些了解了,我们可以用deferred 来重写我们的poetry client,你可以在这里找到client 4.0 twisted-client-4/get-poetry.py.

我们的get_poetry 函数不再需要callback 和 errback这两个参数.相反的,它返回一个deferred,我们可以在它上面附加一些callback 和errback.

def get_poetry(host, port):
    """
    Download a poem from the given host and port. This function
    returns a Deferred which will be fired with the complete text of
    the poem or a Failure if the poem could not be downloaded.
    """
    d = defer.Deferred()
    from twisted.internet import reactor
    factory = PoetryClientFactory(d)
    reactor.connectTCP(host, port, factory)
    return d

我们的factory 对象初始化的时候不再传入callback/errback 对,而是传入一个deferred 对象.一但我们得到诗或者我们发现我们不能连接上server,deferred 就会被触发–带着一首诗 或者错误.

class PoetryClientFactory(ClientFactory):

    protocol = PoetryProtocol

    def __init__(self, deferred):
        self.deferred = deferred

    def poem_finished(self, poem):
        if self.deferred is not None:
            d, self.deferred = self.deferred, None
            d.callback(poem)

    def clientConnectionFailed(self, connector, reason):
        if self.deferred is not None:
            d, self.deferred = self.deferred, None
            d.errback(reason)

注意在deferred被触发之后我们释放deferred 的方法.这是一个在twisted源代码中使用的模式,可以帮助我们确保我们不会触发相同的deferred两次.也可以让python 的垃圾回收机更容易的回收资源.

在一次的,我们也不需要修改PoetryProtocol,还有一个需要修改的地方是poetry_main 函数:

def poetry_main():
    addresses = parse_args()

    from twisted.internet import reactor

    poems = []
    errors = []

    def got_poem(poem):
        poems.append(poem)

    def poem_failed(err):
        print >>sys.stderr, 'Poem failed:', err
        errors.append(err)

    def poem_done(_):
        if len(poems) + len(errors) == len(addresses):
            reactor.stop()

    for address in addresses:
        host, port = address
        d = get_poetry(host, port)
        d.addCallbacks(got_poem, poem_failed)
        d.addBoth(poem_done)

    reactor.run()

    for poem in poems:
        print poem

注意我们是怎样利用deferred 的callback 链来重构poem_done. 因为deferred在twisted中应用的这么频繁,所以我们经常用一个字符d 来持有你正在用的deferred对象,如果要做长期的存储对象,比如作为一个对象的属性,就经常被叫做”deferred”.

Discussion

我们的新的client 的get_poetry 和我们之前写的同步的版本的get_poetry 接收相同的参数–poetry server 的地址.同步的版本返回一首诗,而我们的异步版本返回一个deferred.返回deferred对象是twisted 所有特有的,这就指出了deferred的另一种概念:

一个Deferred对象描述了一个"异步的结果"或者说"还没有到来的结果"

我们可以用下面的图片描述这两种编程的模式:
图片十三

通过返回一个deferred,异步的api可以返回用户如下的信息:

我是一个异步的方法.你想让我做的事情我现在还没有做,但是当我做完的时候,我会触发这个deferred的callback链,并传递返回结果.

当然,这个方法本身不会迭代的触发deferred,它已经返回了.而是这个方法已经在返回的结果上动态的装置了一系列的事件,然后最终导致deferred被触发.

所以deferred 实际上是一种时间移位的方法,能让一个函数的返回结果来适应异步模型的需要.一个函数返回一个deferred 意味着这个函数是异步的,一种将来再返回结果的表现,一个结果会延迟的承诺.


一个同步的函数返回deferred也是可能的,技术上讲,deferred 返回一个值意味着这个函数可能是异步的,我们将会看到同步的函数返回deferred 的例子

因为deferred 的行为是很明确和被很多人知道的,如果你写的apis也返回deferred 你的程序会很容易的被理解和被复用的.如果没有deferred,每一个twisted 的程序或者每一个twisted 的组件可能都会有它们自己的唯一的用来处理callback 的方法,这样会增加你的学习成本.

When You’re Using Deferreds, You’re Still Using Callbacks, and They’re Still Invoked by the Reactor

当你一开始学习twisted 的时候,一个经常范的错误就是向deferred中添加很多的callback,特别的,人们经常认为向一个函数中加入足够多的callback它就是异步的了.这会导致你认为你可以在callback中用os.system,它就不是非阻塞的了.

我认为这个错误是因为你还没有真正的理解异步模型.因为典型的twisted 代码用了很多的deferred 而且很少会用到reactor,他导致你认为deferred做了全部的工作.如果你是从一开始就读的这个系列,你就会明白远不是这种情况.尽管twisted 是由很多工作在一起的部分组成的,但实现异步模型的任务是reactor 来完成的.deferred 是个很重要的抽象,但是我们不用它也可以写我们的twisted client.

让我们看一下我们第一个callback被触发时候的堆栈信息.运行twisted-client-4/get-poetry-stack.py(记得运行server),你会看到下面的输出:


File "twisted-client-4/get-poetry-stack.py", line 129, in
poetry_main()
File "twisted-client-4/get-poetry-stack.py", line 122, in poetry_main
reactor.run()

... # some more Twisted function calls

protocol.connectionLost(reason)
File "twisted-client-4/get-poetry-stack.py", line 59, in connectionLost
self.poemReceived(self.poem)
File "twisted-client-4/get-poetry-stack.py", line 62, in poemReceived
self.factory.poem_finished(poem)
File "twisted-client-4/get-poetry-stack.py", line 75, in poem_finished
d.callback(poem) # here's where we fire the deferred

... # some more methods on Deferreds

File "twisted-client-4/get-poetry-stack.py", line 105, in got_poem
traceback.print_stack()

和client 2.0 版本的堆栈信息非常相像,我们可以用图片十四来形象的描述:
图片十四

和我们前一个版本的client非常相像,(看到这张图片你想到了什么,哈哈,作者表达的既幽默又隐讳:for the sake of the children).这张图片没有表达出的一点是:callback 链不会把控制权返回给reactor,直到第二个callback被触发,也就是在第一个callback返回结果之后.


client4.0 和client2.0 中输出的堆栈信息中有一点不同的是,"twisted code " 和"our code" 的界限变得模糊了,自从deferred 的方法之后是真正的twisted code.这种"twisted code" 和 "our code" 的交互在大型的twisted 项目中是非常常见的.

通过在twisted中使用deferred我们已经在callback 链中多增加了几步,但是我们改变异步模型的基本原理.回想一下callback 程序的一些事实:

  1. 在任一时间只有一个callback在运行
  2. 当reactor 在运行的时候,我们的程序暂停
  3. 和上一条相反,当我们的程序在运行的时候,reactor暂停
  4. 如果callback阻塞了,则整个程序都会阻塞

向deferred上多增加一个callback不会改变这些事实.特别地,一个阻塞的callback仍旧会阻塞即使它被加进一个deferred中.所以那个deferred被触发的时候也会阻塞,则整个程序都是阻塞的.我们得出下面的结论:

deferred 是一个管理callbacks 的方法,它们不是一个可以避免阻塞 和 可以把阻塞变成非阻塞的方法

我们可以通过构建一个带有阻塞callback 的deferred来证明最后一点.看一下这个例子:twisted-deferred/defer-block.py.第二个callback用time.sleep 进行阻塞.如果你运行那个程序然后检查print语句的顺序,它非常明确的说明了一个阻塞的callback也会在deferred中阻塞.

Summary

通过返回一个deferred,一个函数告诉用户我是异步的,并提供了一个机制去获得异步的结果.deferred 在twisted 中应用非常广泛,如果你查看twisted 的api,你会发现它.所以熟悉deferred 会给你带来回报的.Client 4.0 是第一个用twisted 范写出来的,使用deferred作为一个异步函数的返回值.还有一些twisted api 可以让我们把它变得更干净些,但是我想它已经描述了怎么用twisted写程序. 最后我们还会用twisted来写我们的server.

但是我们跟deferred还没完.deferred 类 用很短的一段代码提供了很多有特色的api.我们将会讲更多的deferred 的特色,请关注第九部分.

发表在 twisted | 一条评论

twisted系列教程七–小插曲,延迟对象

Part 7: An Interlude, Deferred

原文:http://krondo.com/blog/?p=1682
作者:dave
译者:notedit
时间:2011.06.16

Callbacks and Their Consequences

在第六部分,我们得出这样一个结论:callbacks 是twisted异步编程的一个重要组成部分.callback 是交织在twisted结构中的,而不仅仅是连接reactor 的一种方法.所以用twisted 或者其他的基于reactor 的异步程序,就意味着把我们的程序组织成被reator触发的callback链.
甚至像我们的简单的get_poetry方法都需要两个callback,一个用来处理正常的返回结果,另一个用来处理错误.作为一个twisted 的程序员,我们将来会大量的用到它们.我们应该花费点时间想想使用callback 的最好的方法,还有我们将会遇到那些陷阱.

看一下下面的一段代码,在client 3.1 版本中:

def got_poem(poem):
    print poem
    reactor.stop()

def poem_failed(err):
    print >>sys.stderr, 'poem download failed'
    print >>sys.stderr, 'I am terribly sorry'
    print >>sys.stderr, 'try again later?'
    reactor.stop()

get_poetry(host, port, got_poem, poem_failed)

reactor.run()

完成的功能很简单:

  1. 如果得到了诗,打印出来
  2. 如果没有得到诗,打印一个错误
  3. 在上面两种情况之后,结束程序

如果是同步的类似的程序应该像下面这样:

try:
    poem = get_poetry(host, port)
   # the synchronous version of get_poetry
except Exception, err:
    print >>sys.stderr, 'poem download failed'
    print >>sys.stderr, 'I am terribly sorry'
    print >>sys.stderr, 'try again later?'
    sys.exit()
else:
    print poem
    sys.exit()

callback 就像 else 代码块,而errback 就像except 代码块.也就意味着,异步程序的触发errback就像抛出一个异常而触发callback就像走正常的程序流.

这两个版本代码之间的不同之处是什么?在同步的版本中,python 的解释器会保证只要get_poetry 抛出任何异常,except块就会运行.如果我们能相信python解释器能正确运行代码,我就能相信出错处理部分的代码能运行.

和异步程序相比:poem_failed errback 会被我们的代码触发–PoetryClientFactory中的clientConnectionFailed,是我们而不是python 在控制出错假如出错的话,所以我们要确保要去处理每一个会出错的情况,并触发相应的errback.否则的话我们的程序将会等待一个永远不会来的callback然后卡在那里.

这个是同步程序和异步程序的令一个区别.在同步程序中,如果我们不去捕捉出现的异常,python 解释器会帮我们捕捉它然后程序崩溃掉并告诉我们出现了什么错误.但是假如我们忘记抛出一个异步的错误,我们的程序就会不停停止,过起了什么也不直到的性福生活.

很明显的,在异步程序中处理异常是很重要的,也是很棘手的.在异步程序中出错处理比处理正常的结果更重要,因为事情出错的方法远比正常运行的方法多.在用twisted过程中,忘记处理错误是一经常犯的错误.

对上面的额同步程序来说:else 代码块和except 代码块都会只运行一次,python 解释器并不会忽然的决定要运行它们两个,或者一时高兴,运行了else 代码块27次.

但是在异步程序中,callback 和errback 都是由我们来控制的,我们可能会犯错.我们可能既调用了callback 也调用了errback,或者调用了callback 27次.然后调用我们写的get_poetry 的那个娃可就要悲剧了. 虽然twisted 中没有明确说,但就像同步程序中的try/except 一样,在twisted 中,我们要调用callback 一次 或者调用errback 一次.运行get_poetry一次,我们或者得到诗歌或者没有得到.

试着想一下如果你去调试一个发起了三个poetry请求并调用了七个callback 和两个errback 的程序, 你可以从哪里下手呢? 你可能会结束你的callbacks 和errbacks 然后去监测在一个get_poetry请求中他们什么时候被触发了两次.

还有一点就是:两个版本的get_poetry都有一些重复的代码.异步的版本调用了两次reactor.stop同步的版本调用了两次sys.exit.我们可以把同步版本的重构成这样:

...
try:
    poem = get_poetry(host, port) # the synchronous version of get_poetry
except Exception, err:
    print >>sys.stderr, 'poem download failed'
    print >>sys.stderr, 'I am terribly sorry'
    print >>sys.stderr, 'try again later?'
else:
    print poem

sys.exit()

异步的版本可以重构吗?我们对此还不太确定,因为我们异步的get_poetry callback 和errback是两个不同的函数,难道你想让我们把它简化成一个callback?

ok,下面是我们对使用callback进行编程的一些看法:

  1. 调用errback 非常重要.因为errback就是except语句,用户需要依赖它们,它们不是可选的
  2. 不要在错误的时间触发callback和在正确的时间触发它们一样重要,callback 和errback 是互赤,只能运行一个
  3. 在使用callback的时候,代码是不容易重构的


我们在将来的章节仍会讲callbacks,但现在我们要去看有没有一个抽象可以很好的管理callbacks.

The Deferred

callback在异步程序中使用的非常多,但就我们发现如果要正确的使用它们是很困难的.twisted 的开发者们创造了一个叫做Deferred 的抽象可以帮助我们来处理callbacks.Deferred 类在twisted.internet.defer中定义.

一个deferred 一对callback 链,一个是用来处理正确结果的,另一个是用来处理出错结果的.一个新的deferred 含有两个空的链.我们可以通过增加callbacks 和 errbacks 来填充这两条链,然后用一个正常的结果或者异常来触发deferred.触发deferred 会按照callback或errback被加入进去的顺序调用它们.图片十二描述了带有callback/errback 链的deferred 实例.
图片十二

让我们测试一下,因为deferred 没有用到reactor,我们不用开启一个循环,我们的第一个deferred 的例子是 twisted-deferred/defer-1.py:

from twisted.internet.defer import Deferred

def got_poem(res):
    print 'Your poem is served:'
    print res

def poem_failed(err):
    print 'No poetry for you.'

d = Deferred()

# add a callback/errback pair to the chain
d.addCallbacks(got_poem, poem_failed)

# fire the chain with a normal result
d.callback('This poem is short.')

print "Finished"

这段代码新建了一个新的deferred,然后用addCallbacks加入了一对callback/errback.然后callback触发了正常结果的callback.当然这里并没有一个callback 链,只有一个callback.但不管怎么样,运行这个代码然后输出如下:

Your poem is served:
This poem is short.
Finished

非常简单,下面是需要注意的一些东西:

  1. 就像在client 3.1我们用的callback/errback,我们向deferred 中加入的callback一次接收一个参数,或者一个正常的结果或者一个错误的结果.实际上deferred支持callback和errback带有多个参数,但是最少一个.但第一个参数永远是callback 或者errback
  2. 我们增加callbacks和errbacks的时候是一对对的
  3. callback方法用一个正常的结果触发deferred
  4. 看一下输出的顺序,我们可以看到触发defferred之后立即调用了callback.这里根本没有异步,因为没有用reactor.


让我们看另一个例子在twisted-deferred/defer-2.py,这次触发deferred 的errback:

from twisted.internet.defer import Deferred
from twisted.python.failure import Failure

def got_poem(res):
    print 'Your poem is served:'
    print res

def poem_failed(err):
    print 'No poetry for you.'

d = Deferred()

# add a callback/errback pair to the chain
d.addCallbacks(got_poem, poem_failed)

# fire the chain with an error result
d.errback(Failure(Exception('I have failed.')))

print "Finished"

在我们运行之后有如下输出:

No poetry for you.
Finished

所以要触发errback 链只要调用errback方法 就可以了,这个方法参数是一个错误的返回结果.就像之前的callback一样,errback在触发之后立即被调用了.

在上一个例子中,我们传递一个Failure对象给errback,这样没什么问题.但是deferred 会自动把普通的Exception 转变为Failures对象,我们在twisted-deferred/defer-3.py可以看到:

from twisted.internet.defer import Deferred

def got_poem(res):
    print 'Your poem is served:'
    print res

def poem_failed(err):
    print err.__class__
    print err
    print 'No poetry for you.'

d = Deferred()

# add a callback/errback pair to the chain
d.addCallbacks(got_poem, poem_failed)

# fire the chain with an error result
d.errback(Exception('I have failed.'))

在这里我们传递给errback 一个正常的Exception.我们得到如下的输出:


twisted.python.failure.Failure
[Failure instance: Traceback (failure with no frames): : I have failed.
]
No poetry for you.

这就意味着当我们用deferred 的时候,我们可以用正常的Exception,deferred 会自动的为我们转为Failure对象.defferred 会保证每一个errback被触发的时候都会被传入一个Failure 实例.

我尝试着按了callback 的按钮也尝试着按了errback 的按钮.就像任何一个好的工程师一样,你可能想要一遍一遍的按它们.为了让代码更短一些,我们让callback和errback 都是同一个函数.记住它们返回的内容不同,一个是正常的结果,一个是错误.代码在这里twisted-deferred/defer-4.py:

from twisted.internet.defer import Deferred
def out(s): print s
d = Deferred()
d.addCallbacks(out, out)
d.callback('First result')
d.callback('Second result')
print 'Finished'

你会得到如下的输出:

First result
Traceback (most recent call last):
...
twisted.internet.defer.AlreadyCalledError

灰常有意思,defferred 不会让我们多次触发正常的callback.实际上,不管如何defferred 都不让人触发两次,下面的例子会证明这一点:

  1. twisted-deferred/defer-4.py
  2. twisted-deferred/defer-5.py
  3. twisted-deferred/defer-6.py
  4. twisted-deferred/defer-7.py


注意最后的print 语句都没有运行到. 当我们用defferred来管理我们的callbacks 的时候,我们就不会范既调用callback 又调用errback 的错误.我们可以试一下,但是deferred会抛出异常来提醒我们.

deferred 可以帮助我们重构异步的代码吗?让我们看一下例子twisted-deferred/defer-8.py:

import sys

from twisted.internet.defer import Deferred

def got_poem(poem):
    print poem
    from twisted.internet import reactor
    reactor.stop()

def poem_failed(err):
    print >>sys.stderr, 'poem download failed'
    print >>sys.stderr, 'I am terribly sorry'
    print >>sys.stderr, 'try again later?'
    from twisted.internet import reactor
    reactor.stop()

d = Deferred()

d.addCallbacks(got_poem, poem_failed)

from twisted.internet import reactor

reactor.callWhenRunning(d.callback, 'Another short poem.')

reactor.run()

这个是我们原始的代码.注意我们在启动reactor之后,用callWhenRunning来触发defferred.我们利用callWhenRunning 接收额外的参数然后传递给callback.在twisted 中有很多注册callback 的api都遵守这个规则,包括吧callback 加进deferred 的api.

callback 和 errback 都可以停止reactor,既然defferred 支持callback 链和errback链,我们可以这些普通代码重组进链的第二层,具体的代码在这里twisted-deferred/defer-9.py:

import sys

from twisted.internet.defer import Deferred

def got_poem(poem):
    print poem

def poem_failed(err):
    print >>sys.stderr, 'poem download failed'
    print >>sys.stderr, 'I am terribly sorry'
    print >>sys.stderr, 'try again later?'

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

d = Deferred()

d.addCallbacks(got_poem, poem_failed)
d.addBoth(poem_done)

from twisted.internet import reactor

reactor.callWhenRunning(d.callback, 'Another short poem.')

reactor.run()

addBoth 方法向callback和errback 链中加入了相同的函数,不论如果我们完成了重构.

Summary
在这一部分我们分析了callback 程序,知道了一些可能潜在的问题.我们也看到了Defferred是怎样帮助我们写代码的:

  1. 我们不能无视errbacks.deferred内置对errback 的支持
  2. 多次触发callback可能导致很难调试的bug,Deferred只能被触发一次,你可以把他想象成try/except
  3. 用deferred,我们可以通过向链中增加新的callback和errback,并在各callback 和errback中移动代码完成重构


我们跟deferred 还没完,它的很多原理我们还没有讲,但对于我们的poetry client已经够用了.继续等我们的第八部分吧.我要先去吃饭了.

发表在 twisted | 评论关闭

twisted系列教程六–继续重构twisted poetry client

Part 6: And Then We Took It Higher

原文:http://krondo.com/blog/?p=1595
作者:dave
译者:notedit
时间:2011.06.16

Poetry for Everyone

我们已经在我们的client取得了很大的进步,我们的2.0版本已经试用了Transports,Protocols 和Protocols Factories.但是仍有很多可以提升的地方.2.0 版本的client版本仅仅可以在命令行下载诗.这是因为PoetryClientFactory 不仅仅负责下载诗,也负责在下载完的时候停掉这个程序.这对一个Protocol Factory类来说太奇怪了,它应该只用来创建PoetryProtocols 和 收集已经运行下载完的诗.

我们需要一种可以把这首诗交给我们代码的方法,但是你必须先获取这首诗,在一个同步的程序中我们可以这样做:

def get_poetry(host, post):
    """Return a poem from the poetry server at the given host and port."""

但是当然的,我们不能在这里这样做.上面的代码会阻塞直到这首诗被完全的接收,否则的话它不会像它注释中说明的一样.但是这是一个reactive 程序,网络阻塞可以被它很好的解决.我们需要一个可以在诗下载完的时候通知我们的代码,以及在下载时不阻塞的方法.这个问题就像twisted 遇到的问题一样,twisted 需要在一个socket 可以进行I/O 的时候告诉我们.twisted 用callback 的方法很好的解决了这个问题,所以我们也可以这样用:

def get_poetry(host, port, callback):
    """
    Download a poem from the given host and port and invoke

      callback(poem)

    when the poem is complete.
    """

现在我们有了一个可以让twisted 使用的api 了,让我们继续.


就像我以前说的,我们有时会不按照twisted 的方式的写代码,上面的写法就不是twisted 的写法,我们会在第七部分和八部分用twisted 的方式来改写它.用最简单的方式开始写代码可以让我们更深入的理解.

Client 3.0
你可以看到我们的poety client 3.0 版本在twisted-client-3/get-poetry.py,这个版本有一个get_poetry 方法的实现:

def get_poetry(host, port, callback):
    from twisted.internet import reactor
    factory = PoetryClientFactory(callback)
    reactor.connectTCP(host, port, factory)

需要注意的是我们传递callback 给PoetryClientFactory,factory 用这个callback传递诗:

class PoetryClientFactory(ClientFactory):

    protocol = PoetryProtocol

    def __init__(self, callback):
        self.callback = callback

    def poem_finished(self, poem):
        self.callback(poem)

现在的factory 比client 2.1 版本的简单多了,因为factory 不用再去关心停掉reactor 了,也少了捕捉错误的代码,我们一会会加上的.而 PoetryProtocol 则不需要做任何改变,我们可以重用它.:

class PoetryProtocol(Protocol):

    poem = ''

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

    def connectionLost(self, reason):
        self.poemReceived(self.poem)

    def poemReceived(self, poem):
        self.factory.poem_finished(poem)

在这些改变之后,get_poetry,PoetryClientFactory,PoetryProtocol都可以完全重用了.它们都只负责下载诗.所有的初始化,关闭reactor 的逻辑代码全部在我们的主函数poetry_main:

def poetry_main():
    addresses = parse_args()

    from twisted.internet import reactor

    poems = []

    def got_poem(poem):
        poems.append(poem)
        if len(poems) == len(addresses):
            reactor.stop()

    for address in addresses:
        host, port = address
        get_poetry(host, port, got_poem)

    reactor.run()

    for poem in poems:
        print poem

我们可以把这些可以重用的部分都放到一个模块中,然后任何人都可以获取诗了.^_^.

顺便说一下,在你实际的测试client 3.0 的时候,你可以重新配置一下poetry server 让它一次多输出一些数据块.

Discussion
我们可以把诗传递的过程用图片十一 形象化:
图片十一

图片十一是值得多想想的,到现在为止我们描述的callback 链是以我们自己写的代码终止的.但是当你用twisted或者其他的reactive 系统 写程序的时候,我们的callback 链会出现一小段代码callback另一小段代码的情况.也就是说,reactive 类型的程序在到达我们写的代码的时候不会停止,它会不断callback 下去.

在你选择twisted 的时候,请把下面的话记到你的心里.当你做了这个决定之后:

我要用twisted了啦啦啦

你也要做这个决定:

我将要把我的程序构造成由reactor触发的一系列的callback

也许你现在不会大声地将它说出来,但twisted 就是这样的.twisted 就是这样工作的.

可能大多数的python程序是同步的而且大多数的python 模块也是同步的.假如我们正在写同步(原文这里写的是同步,我怀疑有点问题,应该是异步)的程序然后忽然意识到我们需要获取诗,我们可以使用get_poetry 函数,就像下面的写法:

...
import poetrylib # I just made this module name up
poem = poetrylib.get_poetry(host, port)
...

然后我们继续,假如不久以后我们根本不需要诗,然后就可以删除上面的两行,对所有的程序都不会造成什么影响.但是假如我们正在写一个同步的程序然后决定用twisted 版本的额get_poetry,我们就需要用callbacks 来重构我们的程序.我们可能会对代码改动很多.我并不是说重写代码是一个错误,根据我们的需求去重构代码是很有意义的.但它不会只增加几行代码那么简单.简单来讲,同步的和异步的程序不能混合在一起.

如果你对twisted 和异步编程了解不是很多,我还是建议你开始研究大型的twisted 程序代码库之前自己先实现几个简单的demo. 这样的话你会在没有其他复杂的干扰下找到twisted 的感觉.假如你的程序已经是异步的,和twisted结合起来就会相对简单.twisted 和 pyGTKpyQT 就结合的很好.

When Things Go Wrong
在client 3.0 版本中我们不再监测当连接服务器时出现的错误,就像在client 1.0 版本中的那样.假如我们让client 3.0 从一个不存在的server上下载诗的话,client 3.0 不会崩溃掉而是在原地不停的等待.clientConnectionFailed callback 仍旧会被调用,但是ClientFactory中的默认clientConnectionFailed 什么也不做,所以got_poem callback 永远不会被调用,reactor 永远不会停,然后我们又成功的写了另一个什么也不做的程序.

很明显的我们需要来处理这个错误,但是在哪里呢?错误信息通过clientConnectionFailed 被传递到factory,所以我们从这里开始,但是这个factory 应该是可用的,正常的处理错误的方法应该依据factory被调用的地方的上下文来处理.在一些程序中,接收不到诗歌可能会是一个灾难,在令一些程序中,我们仍旧可以继续运行(这里是在说twisted 的容错性比较强).

换句话说,当你用get_poetry 的时候需要知道什么时候会出错,不仅仅是什么时候是对的.在一个同步的系统中,get_poetry 会抛出一个异常,然后用一个try/except 进行捕捉,但是在一个reactive 的系统里,错误信息也必须以异步的方式传递.毕竟我们直到get_poetry 返回的时候我们才能发现连接错误了,下面是一种可能的情况:

def get_poetry(host, port, callback):
    """
    Download a poem from the given host and port and invoke

      callback(poem)

    when the poem is complete. If there is a failure, invoke:

      callback(None)

    instead.
    """

通过监测callback 的参数,client可以确定是否我们最终得到了一首诗.这样就可以防止我们的程序永远运行下去,但是还是会有一些小问题,当你向callback传递None的时候,并不能概括到所有的出错信息,而且twisted 的一些api 也会默认的返回None,所以这里我们要用err 参数来替代None,err中可以包含具体的出错信息.就像下面的一样:

def get_poetry(host, port, callback):
    """
    Download a poem from the given host and port and invoke

      callback(poem)

    when the poem is complete. If there is a failure, invoke:

      callback(err)

    instead, where err is an Exception instance.
    """

如果这里用一个异常就基本上和我们的同步程序一样了.现在我们可以从异常中获取出错的信息.正常的,在我们在平常的python代码中如果遇到了异常我们会输出traceback供我们调试用.
请记住在我们的callback被触发的时候我们并不想要一个traceback.我们真正想要的是在出现异常的地方的exception实例 和 当时的traceback.
twisted 包含了一个叫做Failure 的抽象,failure是Exception 和 traceback 的封装.Failure 文档描述了怎样创建一个failure.通过传递给callback一个Failure对象,我们可以很好的保护traceback 信息.

twisted-failure/failure-examples.py中有一些Failure 对象的用法,它演示了Failure 是怎样保护traceback信息的,即使在一个except 代码块上下文之外.我们现在不会在怎样建立Failure 实例上花费很多时间,在第七部分,我们会看到Failure 的用法.
第三个版本的get_poetry:

def get_poetry(host, port, callback):
    """
    Download a poem from the given host and port and invoke

      callback(poem)

    when the poem is complete. If there is a failure, invoke:

      callback(err)

    instead, where err is a twisted.python.failure.Failure instance.
    """

在这个版本后,我们在得到异常的时候还能同时得到一个traceback 记录.

我们已经快完成了,但是还有一个问题.处理错误和处理正常的结果看起来是一种很奇怪的行为.一般来说,我们会对出错和正常结果做出完全不同的操作.在同步的系统中我们会用try/except 语句来分别处理正确的和错误的结果:

try:
    attempt_to_do_something_with_poetry()
except RhymeSchemeViolation:
    # the code path when things go wrong
else:
    # the code path when things go so, so right baby

如果我们也想保持这种错误处理的方式,我们需要让错误处理走另一条路径.在异步程序中分出一个路径意味着多出一个callback.

def get_poetry(host, port, callback, errback):
    """
    Download a poem from the given host and port and invoke

      callback(poem)

    when the poem is complete. If there is a failure, invoke:

      errback(err)

    instead, where err is a twisted.python.failure.Failure instance.
    """

Client 3.1
client 3.1 在twisted-client-3/get-poetry-1.py 中,变化还是非常直观的, PoetryClientFactory 会获得一个callback 和一个errback,在clientConnectionFailed中调用了errback.

class PoetryClientFactory(ClientFactory):

    protocol = PoetryProtocol

    def __init__(self, callback, errback):
        self.callback = callback
        self.errback = errback

    def poem_finished(self, poem):
        self.callback(poem)

    def clientConnectionFailed(self, connector, reason):
        self.errback(reason)

既然clientConnectionFailed已经接收到一个包含错误信息的Failure 对象,我们把它传给errback 就可以了.

其他的改变就很小了,我们就略去不讲了,你可以在不启动server 的情况下测试client 3.1:

python twisted-client-3/get-poetry-1.py 10004

你会看到下面的一些输出:

Poem failed: [Failure instance: Traceback (failure with no frames): : Connection was refused by other side: 111: Connection refused.
]

输出是从我们的poem_failed errback 输出的,在这种情况下,twisted 仅仅传递给我们一个Exception 而不是抛出,所以我们在这里不会得到一个traceback.但是一个traceback 不是必须的,因为这个地方不是一个bug.只是twisted 告诉我们,我们不能连接到那个地址.

Summary
下面是我们从第六部分学到的:

  1. 我们为twisted程序写的api必须是异步的
  2. 我们不能将同步的代码和异步的代码混合
  3. 在我们的代码中不能不用callback,就像twisted 那样
  4. 我们不得不用callback去处理错误

是不是意味着我们用twisted 写的每一个api都要包含callback和errback 两个参数?这样听起来可不是太美好.幸运的是twisted 已经用一个抽象把这两个参数消除掉了并带来一些新的特性.我们将在第七部分讲到.

发表在 twisted | 评论关闭

twisted系列教程五–改进twisted poetry client

Part 5: Twistier Poetry
原文:http://krondo.com/blog/?p=1522
作者:dave
译者:notedit
时间:2011.06.15

Abstract Expressionism
在第四部分我们写了我们的第一个twisted client.它工作的非常好,但仍旧有提升的空间

首先,这个twisted client 包含了一些比如创建sockets 和从这些sockets 中接收数据的细节.twisted 已经对这些操作提供了支持,所以我们没有必要自己再去实现.这个特别有用因为异步的I/O在处理异常方面需要一些技巧,如果需要支持多平台的话需要更多的技巧.如果你有一个空闲的下午,你可以在twisted 源码中搜索以下win32,你可以看到因为平台问题到底引入了多少问题.
另外一个问题就是出错处理,试着在没有启动server的情况下运行twisted client, 它会崩溃掉,我们会修复现在的twisted client,用twisted 的api 你会发现这很容易.
最后,这个client 不是特别可以复用.另外一个模块怎样来用我们的client? “calling” module 是怎样知道我们什么时候执行完下载的? 我们不能仅仅写一个函数直接返回结果,那样会引起阻塞.这些不是我们今天要全部解决的问题,我们会在后面的几篇教程中解决最后一个问题.

我们今天要解决是是第一个和第二个问题,主要是通过一些高级的api 和一些接口.twisted 框架是抽象层松耦合的,学习twisted 就是要学习这些层能提供什么.比如,,每一层能提供什么api,接口,还有什么实现是可用的.我们不会详细的讲解每一层的抽象,我们仅仅去看一些最重要的碎片去理解twisted是怎样被组合起来的.一但你对twisted 的结构全貌了解了,你再去学习twisted 的其他的部分就会容易很多.

一般来说,每一个twisted 的抽象都跟一个特别的概念相关.比如, 第四部分中的twisted client 使用的 IReadDescriptor 就是一个抽象–”可以读取内容的文件描述符”.一个twisted 的抽象被一个接口定义,这个接口指定了一个被这个抽象实例化了的对象应该有什么样的表现.你要记住的是当你学一个twisted 的抽象时:

大多数的高层的抽象是用底层的建立起来的,而不是替换它们

所以当你学习一个新的twisted 抽象时,记住它做了什么和没做什么.特别的,对于一些早期的抽象,可能抽象A 实现了特色F,然后F再也没有被其他的抽象实现.当然,假如抽象B需要特色F,它用使用A而不是再去实现一遍F.
网络是个复杂的主题,因此twisetd 中包含很多种抽象.从低级的抽象开始,我们可以清晰的得到一个结构–它们在一个正常运行的系统中是怎样结合在一起的.
Loopiness in the Brain
reactor是我们目前接触的最重要的抽象,也是twisetd中最重要的抽象.在一个twisted程序的中间,不管你的程序有多少层,总有一个reactor 循环在来维持着这个程序不停的运行.twisted 中的其他的抽象都不会提供这样的功能.twisted 的其他的部分都可以被看做可以让我们更容易使用reactor 的物体,比如做一个web服务或者查询一个sql语句.可能我们还会用一个些底层的api,就像twisetd client 1.0,但是这就需要我们自己去实现更多的内容.如果用高级api则就意味着我们需要写更少的代码.

但是在我们写twisted 外层的东西的时候我们经常忘了reactor 的存在,在一个正常的twisted 的应用中,只有很少的一部分会直接用到reactor api.对于一些底层的抽象来说也是这样.我们在twisted client 1.0 中的使用的文件描述符抽象就被高级的抽象包装,以至于在实际的twisted程序中并不会被用到.

就文件描述符来说,这并不是一个问题.让twisted 来处理异步的I/O 可以让我们专注于我们要解决的问题. 对于reactor 就不一样了,reactor 不会不出现,当你选择使用twisted时候你已经选择了reactor模式,这就意味着 reactor模式的代码会用callback 和 多任务合作. 如果你想用好twisted,你必须时候想着reactor 的存在,我们会在第六章讲关于这个的更多的内容,现在你要记住的是:

图片五图片六是最重要的两张图片

我们将会继续使用图片来说明新的概念,但这两张图片你需要印到你的心里.这两张图片确实在我心里在我用twisted写代码的时候.

在我们进入代码之前,还有三个抽象需要我们去了解:Transports,Protocols和Protocol Factories.

Transports

Transport 抽象在twisted interfaces 模块中被ITransport 定义,一个twisted transport 代表了一个可以接收和发送数据的连接.在我们的client 中tranport 就想一个tcp 连接.但是twisted也支持UNIX PIPESUDP socket 类型的I/O. transport 抽象代表了任何一个它们其中的一种连接,并处理每种连接带来的各种异步I/O的细节.

如果你查看了ITransport 中定义的方法,你不会发现有用来接收数据的方法.那是因为transport 被用来从连接中异步的读数据,然后把读到的数据传递通过callback传递给我们.所以transport 的写相关的方法为了防止阻塞并不是立即把数据交给我们.告诉transport 去写数据意味着:在不产生阻塞的情况下尽快的把数据传送出去.

我们并不会实现我们自己的transport 或者创建一个,而是我们用twsited 已经帮我们实现的transport,reactor 在创建连接的时候会帮我们创建transport.

Protocols

twsited protocol 是在interfaces模块中被IProtocol 定义.Protocol 对象实现了protocol,也就是说,一个twisted Protocol 的实现应该是实现了一个特别的网络协议,就像FTP或者IMAP或者其他的我们自己实现的协议.我们的poetry protocol,也是一种协议,在连接建立之后开始传送数据,连接断开表示诗已经传送完毕.

严格的来说,每一个twisted Protocol 对象的实例都为一个特别的连接实现一个协议.所以我们程序的每一个连接都会需要一个Protocol 的实例.这就让Protocol 实例自然而然的成为了存储协议状态和部分累积数据(因为在异步io中我们每一次接收的数据块不是固定大小的)的地方.

所以Protocol 实例是怎样知道它是在为哪一个连接负责? 假如我们看一下IProtocol 的定义,我们将会看到makeConnection.这个方法是一个callback,twisted代码会调用它,一个transport 实例作为参数.transport 会被连接和协议用到.

twisted 已经包含了大量的协议的实现.你可以看到一些简单的协议在twisted.protocols.basic,在你想写一个新的协议之前你最好去查找一下源代码看有没有已经实现的协议符合你的要求.如果没有的话,去实现你自己的协议也是很简单的事情.

Protocol Factories

每一个连接需要它自己的Protocol,这个Protocol 应该是我们实现的一个类的实例.既然我们让twisted 来处理创建连接,twisted 需要一种当一个连接被建立时就会有一个合适的protocol为它准备好的方法. 创建Protocol实例的任务就交给Protocol Factories.

也许你已经猜到,Protocol Factory API 被IProtocolFactory 定义,也在interfaces 模块中.Protocol Factories 是工场设计模式的一个例子. buildProtocol 在每次调用的时候会返回一个Protocol 的实例,这个就是twisted 用来为每个新的连接新建protocol 的方法.

Get Poetry 2.0: First Blood.0
现在让我们来看一下Twisted poery client 的2.0 版本.代码在twisted-client-2/get-poetry.py.你可以运行它然后它的输出和1.0 版本的很像.这个也会是最后一个会打印任务号的client.到现在为止你应该对多任务交互运行和每一次只会读取一小块数据比较清楚了.我们仍旧会用打印语句来告诉你现在运行到什么地方了,但是在不久的将来这些代码不会这么冗长.
在client 2.0 中,socket 已经消失了.我甚至不用导入socket 模块,并且我们永远不会再提到socket,以及文件描述符了.我们用以下的代码让reactor 去创造连接:

factory = PoetryClientFactory(len(addresses))

from twisted.internet import reactor

for address in addresses:
    host, port = address
    reactor.connectTCP(host, port, factory)

我们可以来看一下connectTCP,前面的两个参数是自解释的,第三个是PoetryClientFactory 的实例,这个是poetry client 的的Protocol Factory,传递给reactor,然后twisted 就可以创造PoetryProtocol.

注意一下我们实现Factory 或 Protocol 的时候并不是乱写的,不像PoetrySocket对象,我们是继承了twisted 提供的twisted.internet.protocol twisted.internet.protocol.Factory.但我们这里是用的ClientFactory
clientfactory 是专门为client 准备的(client 是建立连接,而server 是在等待连接).
我们也利用twsited factory 实现了buildProtocol,我们在子类中调用了父类中的buildProtocol方法:

def buildProtocol(self, address):
    proto = ClientFactory.buildProtocol(self, address)
    proto.task_num = self.task_num
    self.task_num += 1
    return proto

基类是怎样知道去建立那些protocol呢?注意我们已经在PoetryClientFactory 中设置protocol 属性了:

class PoetryClientFactory(ClientFactory):

    task_num = 1

    protocol = PoetryProtocol # tell base class what proto to build

Factory 基类通过我们设置的protocol 属性来实现buildProtocol,并在新建立的protocol实例上设置factory属性,这时的factory是ProtocolFactory 的引用.这个过程可以用图片八来表明:
图片八
就像我们在上面提到的,factory 做为Protocol 的属性,这样被创造出来的Protocol 就可以通过factory来进行共享状态.由于factory 是被我们自己写的代码控制的(相对于被twisted 控制的部分),拥有相同的factory 属性可以让protocol 对象把运行时的状态交给我们写的代码.(这两段是根据自己的理解翻译过来的), 在第六部分我们会看到具体的应用.

注意,在factory 做Protocol 属性的时候,这时的factory 是 Protocol Factory 的一个实例,factory 的protocol 属性是Protocol 类的引用而不是一个实例的引用,因为一个factory会创建多个protocol实例.

Protocol 结构 用transport 连接一个protocol 的第二个阶段是用makeConnection方法.我们没有必要自己实现这个方法因为twisted 的基类提供了一个默认的实现.默认的,makeConnection有一个引用了Transport 的叫做transport的属性,并设置了connected属性为True,图片九描述了这个过程:
图片九

一但初始完这些,Protocol 就可以干点真正的工作了–把底层的数据流转化为高级的协议信息数据流,处理数据的主要的方法为dataReceived,我们的客户端是这样实现的:

def dataReceived(self, data):
    self.poem += data
    msg = 'Task %d: got %d bytes of poetry from %s'
    print  msg % (self.task_num, len(data), self.transport.getHost())

每一次dataReceived 被调用,我们就会以字符串的形式得到一个新的字节序列.因为是异步IO操作,我们不清楚我们会得到多少数据,所以我们会进行缓冲直到有一个完整的protocol 消息.在我们的client 中,诗不会停止直到连接断开,所以我们不停的像我们的.poem属性中增加字节.
我们在Transport中使用getHost 方法来区别这些数据来自哪一个server,我们以前的client中也是这么做的.否则我们的代码没有必要用Tansport.因为我们不用向server端送出任何数据.

让我们快速的看一下当dataReceived 被调用的时候发生了什么?在我们的client 2.0相同目录下,有一个client叫做 twisted-client-2/get-poetry-stack.py,与2.0 版本不同的是dataReceived 被改成了这样:

def dataReceived(self, data):
    traceback.print_stack()
    os._exit(0)

这个改变会让程序打印出堆栈信息,然后退出.你可以运行它像这样

python twisted-client-2/get-poetry-stack.py 10000

将会有如下的输出:

File "twisted-client-2/get-poetry-stack.py", line 125, in
poetry_main()

... # I removed a bunch of lines here

File ".../twisted/internet/tcp.py", line 463, in doRead # Note the doRead callback
return self.protocol.dataReceived(data)
File "twisted-client-2/get-poetry-stack.py", line 58, in dataReceived
traceback.print_stack()

你会看到我们在twised client 1.0 中用到的doRead callback .就像我们之前说的,twisted 用底层的抽象来建立上一层的抽象,并不是替换掉它们.所以在这个过程中仍就会有一个IReadDescriptor的实现在起作用,它仅仅被twisted 实现了而不是我们自己的代码.如果你好奇的话,twisted 的实现在 twisted.internet.tcp 中.如果你跟踪源代码,你会发现有一些对象实现了IWriteDescriptor 和 ITransport.所以Transport 最终是IReadDescriptor 的表现形式.我们可以用一张图片来形象化dataReceived, 图片十:
图片十

一但一首小诗完成了下载,PoetryProtocol 对象会通知PoetryClientFactory:

def connectionLost(self, reason):
    self.poemReceived(self.poem)

def poemReceived(self, poem):
    self.factory.poem_finished(self.task_num, poem)

connectionLost callback 被触发当transport 的连接断开之后,reason 参数是一个twisted.python.failure.Failure对象带着额外的信息来告诉我们这个连接是正常的断开还是由于错误.我们的client 忽略这个值假设我们已经接收到整个的诗.
factory 会停掉reactor 在所有的诗被处理完之后.再一次的假设我们的的程序唯一做的事情就是下载诗,这样就造成我们的PoetryClientFactory 对象不可重用,我们会解决这个问题在下一部分.但是要注意poem_finished callback 是怎样记录剩余没有完成的诗的数量.

...
    self.poetry_count -= 1

    if self.poetry_count == 0:
        ...

假如我们正在写一个多进程程序,每一首诗在一个不同的进程中进行,我们应该保护这部分的代码用一个锁防止两个进程或多个进程同时调用poem_finished.否则的话 我们可能会关闭reactor 两次.但是在一个reactive 的系统中我们不需要这样.reactor 一次只能运行一个callback,所以这种情况不会发生.

我们的新的client 在处理错误的方面比client 1.0 更优雅.下面是PoetryClientFactory 类中的出错处理:

def clientConnectionFailed(self, connector, reason):
    print 'Failed to connect to:', connector.getDestination()
    self.poem_finished()

注意这个callback 是在factory里,不是在protocol 里.因为protocol 是在连接建立之后才被创建的.factory 会获取到信息当一个连接不能被建立的时候.

A simpler client
尽管我们的client 已经很简单了,我们可以让它更简单如果不用任务号的话,这个是简化了的2.1 版本:twisted-client-2/get-poetry-simple.py

Wrapping Up
client 2.0 版本使用了twisted 用户比较熟悉的抽象.假如我们需要一个命令行的client,打印出诗然后退出,我们可以停在这里然后让我们的程序停止.但是假如我们需要一些可复用的代码,一些可以用在一个大型系统里面的代码,不光用来下载诗还用来做其他的事情,我们仍旧有很多工作要去做.在第六部分我们将会近一步的尝试.

发表在 twisted | 评论关闭

twised系列教程四–twisted Poetry client

Part 4: Twisted Poetry

原文:http://krondo.com/blog/?p=1445
作者:dave
译者:notedit
blog:www.notedit.com
时间:2011.06.14

我们第一个twisted client
尽管twisted 经常被用来写server端的,但client往往会比较简单些,我们就以最简单的client 开始.源代码在twisted-client-1/get-poetry.py,首先开启server:

python blocking-server/slowpoetry.py --port 10000 poetry/ecstasy.txt
 --num-bytes 30
python blocking-server/slowpoetry.py --port 10001 poetry/fascination.txt
python blocking-server/slowpoetry.py --port 10002 poetry/science.txt

然后运行client:

python twisted-client-1/get-poetry.py 10000 10001 10002

你会看到如下的输出:

Task 1: got 60 bytes of poetry from 127.0.0.1:10000
Task 2: got 10 bytes of poetry from 127.0.0.1:10001
Task 3: got 10 bytes of poetry from 127.0.0.1:10002
Task 1: got 30 bytes of poetry from 127.0.0.1:10000
Task 3: got 10 bytes of poetry from 127.0.0.1:10002
Task 2: got 10 bytes of poetry from 127.0.0.1:10001
...
Task 1: 3003 bytes of poetry
Task 2: 623 bytes of poetry
Task 3: 653 bytes of poetry
Got 3 poems in 0:00:10.134220

就像我们先前写的异步的client一样,他们实质上做了相同的事情.让我们看一下源码看一下它是怎样工作的,把代码在你的编辑器中打开,以至于你知道我们在讨论什么.

就像在第一部分说的,我们刚开始接触twisted会尽量的用他的底层的api,这样我们更能了解twisted底层的东西.]
也就是说我们一开始用的一些底层api 在往后的项目中可能不会用到.我们只是学习练习twisted的用法.

这个twisted client 建立了几个PoetrySocket 对象.一个PoetrySocket 创造一个socket,连接一个server,然后设置非阻塞模式:

self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect(address)
self.sock.setblocking(0)

最后我们会将代码抽象到好像没有用到socket,不过现在还是要用到socket. 在我们创造一个连接之后,PoetrySocket 通过addReader 函数把它自己传递给reactor:

# tell the Twisted reactor to monitor this socket for reading
from twisted.internet import reactor
reactor.addReader(self)

这个函数交给twisted一个你想要监测的文件描述符. 为什么我传递给twisted 的是PoetySocket 对象而不是文件描述符和callback? twisted 是怎样知道怎样操作PoetySocket 的?相信我吧,我已经观察过了. 打开twisted.internet.interfaces模块,并跟随我.
Twisted Interfaces
在twisted 中有很多叫做接口的子模块.每一个模块定义了一些 接口类.在twisted 8.0 版本中, zope.interface是这些类的基础,但是这个包对我们不是特别重要,我们关心的是这些接口的子类,就像我们将要看到的这个.

作为一个python程序员你一定熟悉Duck Typing,一个对象的类型不是被它在类结构的中地位决定的,而是被它实现的公共的接口定义的(这里翻译的不太准确,建议看原文).因此两个实现了相同的公共接口的对象,他们是相同类型的.

你可以在twisted.internet.interfaces 中找到 addReader 的定义.它在 IReactorFDSet 接口中定义:

def addReader(reader):
    """
    I add reader to the set of file descriptors to get read events for.

    @param reader: An L{IReadDescriptor} provider that will be checked for
                   read events until it is removed from the reactor with
                   L{removeReader}.

    @return: C{None}.
    """

IReactorFDSet 是twisted reactors 实现的众多接口中的一个.因此任何的twisted reactor 对象都有一个addReader 的方法. 这个方法的声明并没有一个self 参数,因为它只是被定义为一个接口.接口对象永远不会被实例化或用作基类.

  1. 技术上讲,IReactorFDSet仅仅被reactor实现,被用来等待文件描述符, 据我所知,IReactorFDSet 已经包含了reactor 的所有实现
  2. zope.interface 允许你明确的声明一个类实现了一个或多个接口,并提供相应的机制在运行时来检查这些声明. 也支持适配器,可以动态的为一个对象提供一个它本身不支持的接口.感兴趣的同学移步这里
  3. 你可能注意到了接口和抽象的基类的相同之处,都是对python 语言的扩展 ,我们在这里不讲他们之间的相同点和不同点.如果你感兴趣的话你可以读读 essay

根据上面讲的,addReader 方法的参数reader 应该实现IReadDescriptor接口.这就意味着我们的PoetrySocket 对象要实现这个接口.
我们可以找到IReadDescriptor 接口的定义:

class IReadDescriptor(IFileDescriptor):

    def doRead():
        """
        Some data is available for reading on your descriptor.
        """

你会发现在我们的PoetrySocket 中doRead 方法的实现.当它被twised 的reactor调用时,它会从socket 中异步地读取数据.可见doRead 确实是一个callback,我们并没有把它直接传递给reactor,我们把它包装进PoetrySocket.这在twisted framework 中是个习惯–不是传递一个函数而是传递一个实现了某些接口的对象. 这样就允许我们一次可以传递多个callback,而且可以让多个callback可以通过一个对象中的共享数据互相通信.

PoetrySocket 对象的其他方法实现了什么?注意IReadDescriptor是IFileDescriptor子类,这就意味着实现了IReadDescriptor 的对象也必须实现IFileDescriptor,如果你看一下IFileDescriptor,你会发现:

class IFileDescriptor(ILoggingContext):
    """
    A file descriptor.
    """

    def fileno():
        ...

    def connectionLost(reason):
        ...

我省略了一部分注释,但是你可以从方法名中看到它们的用途:fileno 会返回我们想监听的文件描述符,connectionLost会在连接丢失的时候被访问.你会发现我们的PoetrySocket 也实现了这两个方法.

最后,IFileDescriptor继承至ILoggingContext,所以我们也要实现logPrefix callback,你可以在interfaces 模块中获取更详细的信息.

注意:你可能注意到doRead 会返回一个特殊值来表明什么时候这个socket被关闭.我是怎样知道这样做的呢?
一般来说,如果不这样做就不会工作,我查看了其他的实现了相同接口的方法.你不得不记住:有时候一些软件
的文档是不正确的.(略)

More on Callbacks
我们的新的twisetd client 和我们原先写的异步的client 确实很相像.两个client全都建立它们自己的socket,然后从这些socket 中异步的读数据.不同的是twsited cliet 不用自己实现select loop– 它用 twisted 的reactor 来实现.

doRead callback 是最重要的一个,twisted 通过它告诉我们socket 中已经有数据准备好了,我们可以把它形象化成图七
 图片七
每一次callback被触发的时候,我们就尽可能的读数据然后停止(非阻塞的),就像我们在第三部分讲的,twisted 不能防止我们的代码是阻塞的.我们可以那样做一下然后看会发生什么. 在跟我们的twisted client 相同目录下有一个twisted-client-1/get-poetry-broken.py,这个client 跟咱们的twisted client 有两点不一样:

  1. 这个client 没有设置为非阻塞
  2. doRead方法不停的读数据直到一个socket被关闭

下面运行这个client:

python twisted-client-1/get-poetry-broken.py 10000 10001 10002

你将会得到以下的输出:

Task 1: got 3003 bytes of poetry from 127.0.0.1:10000
Task 3: got 653 bytes of poetry from 127.0.0.1:10002
Task 2: got 623 bytes of poetry from 127.0.0.1:10001
Task 1: 3003 bytes of poetry
Task 2: 623 bytes of poetry
Task 3: 653 bytes of poetry
Got 3 poems in 0:00:10.132753

输出跟我们以前的twisted client 不太一样,这是因为这个broken-client 为阻塞的.在callback中通过使用一个阻塞的操作,把我们以前的异步twisted程序编程了同步的twisted程序.我们得到了一个复杂的select 循环,并没有从异步中得到什么好处.
twisted 提供给我的多任务的处理方式是合作,twisted 会告诉什么时候去读什么时候去写,但是我们必须在尽可能处理多的数据情况下又要防止阻塞.此外我们必须避免其他的阻塞的操作,比如os.system.假如我们有一个很耗费cpu资源的任务,这就要求我们把它拆成小的交替的任务,以便i/o操作不会造成阻塞.
注意我们的broken- client仍旧会工作,但就是不能利用异步I/O 的效率,你可能仍旧注意到我们的broken-client 仍会比单纯的同步阻塞client 要快,那是因为broken -client 在一开始的时候就连接到所有的server,server端会立刻送出数据,操作系统会缓冲这些数据,即使我们没有读取它们,broken-client 会有效的获取到其他server传来的数据,即使broken-client在一段时间内只有一个在读.
这种buffer 的把戏只会在只有小量数据的时候管用,如果数据量很大的的话broken-client 会和我们以前的同步的client相差不多.
wrapping up
我对于我们的第一个twisted client 没有太多可说的,你应该注意到 connectionLostcallback在处理完所有的socket之后 关闭了这个reactor.这里并没有用到什么技巧,因为client 假设我们在下载完诗后不会再做其他的事情,我们还可以看到其他的两个底层的reactor api ,removeReader 和 getReaders.
在reactor api 中也还有Writer 相关的方法,被用来向文件描述符中写一些东西,你可以在interfaces 中获取更多信息.reader 和 writer 相关的api 会被twisted 分开写,因为select loop 会分开这两中事件.

在第五部分,我们将会用一些高级的api来写我们的twisted poetry.学习更多的接口和api. 谢谢 ~~~~

发表在 python, twisted | 评论关闭