(转) Twisted 第四部分: 由Twisted支持的诗歌客户端

第一个twisted支持的诗歌服务器html

尽管Twisted大多数状况下用来写服务器代码,为了一开始尽可能从简单处着手,咱们首先从简单的客户端讲起。python

让咱们来试试使用Twisted的客户端。源码在twisted-client-1/get-poetry.py。首先像前面同样要开启三个服务器:react

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

而且运行客户端:git

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

和咱们的没有使用Twisted的非阻塞模式客户端打印的内容接近。这并不奇怪,由于它们的工做方式是同样的。github

下面,咱们来仔细研究一下它的源代码。编程

注意:正如我在第一部分说到,咱们开始学习使用Twisted时会使用一些低层TwistedAPIs。这样作是为揭去Twisted的抽象层,这样咱们就能够从内向外的来学习Tiwsted。可是这就意味着,咱们在学习中所使用的APIs在实际应用中可能都不会见到。记住这么一点就行:前面这些代码只是用做练习,而不是写真实软件的例子。缓存

可民看到,首先建立了一组PoetrySocket的实例。在PoetrySocket初始化时,其建立了一个网络socket做为本身的属性字段来链接服务器,而且选择了非阻塞模式:服务器

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一个文件描述符或回调函数而是一个对象实例?而且Twisted内部没有任何与这个诗歌服务相关的代码,它怎么知道该如何与咱们的对象实例交互?相信我,我已经查看过了,打开twisted.internet.interfaces模块,和我一块儿来搞清楚是怎么回事。


Twisted接口

twisted内部有不少被称做接口的子模块。每一个都定义了一组接口类。因为在8.0版本中,Twisted使用zope.interface做为这些类的基类。但咱们这里并不来讨论它其中的细节。咱们只关心其在Twisted的子类,就是你看到的那些。

使用接口的核心目的之一就是文档化。做为一个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是一个Twistedreactor实现的接口。所以任何一个Twistedreactor都会一个 addReader的方法,如同上面描述的同样工做。这个方法声明之因此没有self参数是由于它仅仅关心一个公共接口定义,self参数仅仅是接口实现时的一部分(在调用它时,也没有显式地传入一个self参数)。接口类永远不会被实例化或做为基类来继承实现。

注意1:技术上讲,IReactorFDSet只会由reactor实现用来监听文件描述符。具我所知,如今全部已实现reactor都会实现这个接口。

注意2:使用接口并不只仅是为了文档化。zope.interface容许你显式地来声明一个类实现一个或多个接口,并提供运行时检查这些实现的机制。一样也提供代理这一机制,它能够动态地为一个没有实现某接口的类直接提供该接口。但咱们这里就不作深刻学习了。

注意3:你可能已经注意到接口与最近添加到Python中虚基类的类似性了。这里咱们并不去分析它们之间的类似性与差别。若你有兴趣,能够读读Ptyhon项目的创始人Glyph写的一篇关于这个话题的文章

根据文档的描述能够看出,addReaderreader参数是要实现IreadDescriptor接口的。这也就意味咱们的PoetrySocket也必须这样作。

阅读接口模块咱们能够看到下面这段代码:

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

同时你会看到在咱们的PoetrySocket类中有一个doRead方法。当其被Twistedreactor调用时,就会采用异步的方式从socket中读取数据。所以,doRead其实就是一个回调函数,只是没有直接将其传递给reactor,而是传递一个实现此方法的对象实例。这也是Twisted框架中的惯例—不是直接传递实现某个接口的函数而是传递实现它的对象。这样咱们经过一个参数就能够传递一组相关的回调函数。并且也可让回调函数之间经过存储在对象中的数据进行通讯。

那在PoetrySocket中实现其它的回调函数呢?注意到IReadDescriptorIFileDescriptor的一个子类。这也就意味任何一个实现IReadDescriptor都必须实现IFileDescriptor。如果你仔细阅读代码会看到下面的内容:

class IFileDescriptor(ILoggingContext):
    """
    A file descriptor.
    """
    def fileno():
        ...
    def connectionLost(reason):
        …

将文档描述省略掉了,但这些函数的功能从字面上就能够理解:fileno返回咱们想监听的文件描述符,connectionLost是当链接关闭时被调用。你也看到了,PoetrySocket实现了这些方法。

最后,IFileDescriptor继承了ILooggingContext,这里我不想再展示其源码。我想说的是,这就是为何咱们要实现一个logPrefix回调函数。你能够在interface模块中找到答案。

注意:你也许注意到了,当链接关闭时,在doRead中返回了一个特殊的值。我是如何知道的?说实话,没有它程序是没法正常工做的。我是在分析Twisted源码中发现其它相应的方法采起相同的方法。你也许想好好研究一下:但有时一些文档或书的解释是错误的或不完整的。所以可能当你搞清楚怎么回事时,咱们已经完成第五部分了呵呵。


更多关于回调的知识

咱们使用Twisted的异步客户端和前面的没有使用Twisted的异步客户很是的类似。二者都要链接它们本身的socket,并以异步的方式从中读取数据。最大的区别在于:使用Twisted的客户端并无使用本身的select循环-而使用了Twistedreactor

doRead回调函数是很是重要的一个回调。Twisted调用它来告诉咱们已经有数据在socket接收完毕。我能够经过图7来形象地说明这一过程:

 

                                                                                                                                                                       第四部分:由Twisted支持的诗歌客户端

7 doRead回调过程


每当回调被激活,就轮到咱们的代码将全部可以读的数据读回来而后非阻塞式的中止。正如咱们第三部分说的那样,Twisted是不会由于什么异常情况(如没有必要的阻塞)而终止咱们的代码。那么咱们就故意写个会产生异常情况的客户端看看到底能发生什么事情。能够在twisted-client-1/get-poetry-broken.py中看到源代码。这个客户端与你前面看到的一样有两个异常情况出现:

1.这个客户端并不没有选择非阻塞式的socket

2.doRead回调方法在socket关闭链接前一直在不停地读socket

如今让咱们运行一下这个客户端:

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

可能除了任务的完成顺序不太一致外,和我先阻塞式客户端是同样的。这是由于这个客户端是一个阻塞式的。

因为使用了阻塞式的链接,就将咱们的非阻塞式客户端变成了阻塞式的客户端。这样一来,咱们尽管遭受了使用select的复杂但却没有享受到其带来的异步优点。

像诸如Twisted这样的事件循环所提供的多任务的能力是须要用户的合做来实现的。Twisted会告诉咱们何时读或写一个文件描述符,但咱们必需要尽量高效而没有阻塞地完成读写工做。一样咱们应该禁止使用其它各种的阻塞函数,如os.system中的函数。除此以外,当咱们遇到计算型的任务(长时间占用CPU),最好是将任务切成若干个部分执行以让I/O操做尽量地执行。

你也许已经注意到这个客户端所花费的时间少于先前那个阻塞的客户端。这是因为这个在一开始就与全部的服务创建链接,因为服务是一旦链接创建就当即发送数据,并且咱们的操做系统会缓存一部分发送过来但尚读不到的数据到缓冲区中(缓冲区大小是有上限的)。所以就明白了为何前面那个会慢了:它是在完成一个后再创建下一个链接并接收数据。

但这种小优点仅仅在小数据量的状况下才会得以体现。若是咱们下载三首20M个单词的诗,那时OS的缓冲区会在瞬间填满,这样一来咱们这个客户端与前面那个阻塞式客户端相比就没有什么优点可言了。


结束语

我没有过多地解释此部分第一个客户端的内容。你可能注意到了,connectionLost函数会在没有PoetrySocket等待诗歌后关闭reactor。因为咱们的程序除了下载诗歌不提供其它服务,因此才会这样作。但它揭示了两个低层reactorAPIsremoveReadergetReaders

还有与咱们客户端使用的ReadersAPIs类同的WritersAPIs,它们采用相同的方式来监视咱们要发送数据的文件描述符。能够经过阅读interfaces文件来获取更多的细节。读和写有各自的APIs是由于select函数须要分开这两种事件(读或写能够进行的文件描述符)。固然了,能够等待即能读也能写的文件描述符。


第五部分,咱们将使用Twisted的高层抽象方式实现另一个客户端,而且学习更多的Twisted的接口与APIs

相关文章
相关标签/搜索