抽象地构建客户端html
在第四部分中,咱们构建了第一个使用Twisted的客户端。它确实能很好地工做,但仍有提升的空间。python
首先是,这个客户端居然有建立网络端口并接收端口处的数据这样枯燥的代码。Twisted理应为咱们实现这些例程性功能,免得咱们每次写一个新的程序时都要去本身实现。Twisted这样作也将咱们从像异步I/O操做中包括许多像异常处理这样的细节处理解放出来。更多的细节处理存在于多平台上运行咱们的代码中。若是你那个下午有空,能够翻翻Twisted的WIN32实现源代码,看看里面有多少小针线是来处理跨平台的。react
另外一问题是与错误处理有关。当运行版本1的Twisted客户端来从并无提供服务的端口上下载诗歌时,它就会崩溃。咱们是能够修正这个错误,但经过下面咱们要介绍Twisted的APIs来处理这些类型的错误会更简单。git
最后,那个客户端也不能复用。若是有另外一个模块须要经过咱们的客户端下载诗歌呢?人家怎么知道你的诗歌已经下载完毕?咱们不能用一个方法简单地将一首诗下载完成后再传给人家,而在以前让人家处于等待状态。这确实是一个问题,但咱们不许备在这个部分解决这个问题—在将来的部分中必定会解决这个问题。github
咱们将会使用一些高层次的APIs和接口来解决第1、二个问题。Twisted框架是由众多抽象层松散地组合起来的。所以,学习Twisted也就意味着须要学习这些层都提供什么功能,例如每层都有哪些APIs,接口和实例可供使用。接下来咱们会经过剖析Twisted最最重要的部分来更好地感觉一下Twisted都是怎么组织的。一旦你对Twisted的整个结构熟悉了,学习新的部分会简单多了。数据库
通常来讲,每一个Twisted的抽象都只与一个特定的概念相关。例如,第四部分中的客户端使用的IReadDescriptor,它就是“一个能够读取字节的文件描述符”的抽象。一个抽象每每会经过定义接口来指定那些想实现个抽象(也就是实现这个接口)对象的形为。在学习新的Twisted抽象概念时,最须要谨记的就是:编程
多数高层次抽象都是在低层次抽象的基础上创建的,不多有另立门户的。缓存
所以,你在学习新的Twisted抽象概念时,始终要记住它作什么和不作什么。特别是,若是一个早期的抽象A实现了F特性,那么F特性不太可能再由其它任何抽象来实现。另外,若是另一个抽象须要F特性,那么它会使用A而不是本身再去实现F。(一般的作法,B可能会经过继承A或得到一个指向A实例的引用)安全
网络很是的复杂,所以Twisted包含不少抽象的概念。经过从低层的抽象讲起,咱们但愿能更清楚起看到在一个Twisted程序中各个部分是怎么组织起来的。服务器
核心的循环体
第一个咱们要学习的抽象,也是Twisted中最重要的,就是reactor。在每一个经过Twisted搭建起来的程序中心处,无论你这个程序有多少层,总会有一个reactor循环在不中止地驱动程序的运行。再也没有比reactor提供更加基础的支持了。实际上,Twisted的其它部分(即除了reactor循环体)能够这样理解:它们都是来辅助X来更好地使用reactor,这里的X能够是提供Web网页、处理一个数据库查询请求或其它更加具体内容。尽管坚持像上一个客户端同样使用低层APIs是可能的,但若是咱们执意那样作,那么咱们必需本身来实现很是多的内容。而在更高的层次上,意味着咱们能够少写不少代码。
可是当在外层思考与处理问题叶。很容易就忘记了reactor的存在了。在任何一个常见大小的Twisted程序中 ,确实不多会有直接与reactor的APIs交互。低层的抽象也是同样(即咱们不多会直接与其交互)。咱们在上一个客户端中用到的文件描述符抽象,就被更高层的抽象更好的概括而至于咱们不多会在真正的Twisted程序中遇到。(他们在内部依然在被使用,只是咱们看不到而已)
至于文件描述符抽象的消息,这并非一个问题。让Twisted掌舵异步I/O处理,这样咱们就能够更加关注咱们实际要解决的问题。但对于reactor不同,它永远都不会消失。当你选择使用Twisted,也就意味着你选择使用Reactor模式,而且意味着你须要使用回调与多任务合做的“交互式”编程方式。若是你想正确地使用Twisted,你必须牢记reactor的存在。咱们将在第六部分更加详细的讲解部份内容。可是如今要强调的是:
图5与图6是这个系列中最最重要的图
咱们还将用图来描述新的概念,但这两个图是须要你牢记在脑海中的。能够这样说,我在写Twisted程序时一直想着这两张图。
在咱们付诸于代码前,有三个新的概念须要阐述清楚:Transports,Protocols,Protocol Factoies
Transports
Transports抽象是经过Twisted中interfaces模块中ITransport接口定义的。一个Twisted的Transport表明一个能够收发字节的单条链接。对于咱们的诗歌下载客户端而言,就是对一条TCP链接的抽象。可是Twisted也支持诸如Unix中管道和UDP。Transport抽象能够表明任何这样的链接并为其表明的链接处理具体的异步I/O操做细节。
若是你浏览一下ITransport中的方法,可能找不到任何接收数据的方法。这是由于Transports老是在低层完成从链接中异步读取数据的许多细节工做,而后经过回调将数据发给咱们。类似的原理,Transport对象的写相关的方法为避免阻塞也不会选择当即写咱们要发送的数据。告诉一个Transport要发送数据,只是意味着:尽快将这些数据发送出去,别产生阻塞就行。固然,数据会按照咱们提交的顺序发送。
一般咱们不会本身实现一个Transport。咱们会去实现Twisted提供的类,即在传递给reactor时会为咱们建立一个对象实例。
Protocols
Twisted的Protocols抽象由interfaces模块中的IProtocol定义。也许你已经想到,Protocol对象实现协议内容。也就是说,一个具体的Twisted的Protocol的实现应该对应一个具体网络协议的实现,像FTP、IMAP或其它咱们本身规定的协议。咱们的诗歌下载协议,正如它表现的那样,就是在链接创建后将全部的诗歌内容所有发送出去而且在发送完毕后关闭链接。
严格意义上讲,每个Twisted的Protocols类实例都为一个具体的链接提供协议解析。所以咱们的程序每创建一条链接(对于服务方就是每接受一条链接),都须要一个协议实例。这就意味着,Protocol实例是存储协议状态与间断性(因为咱们是经过异步I/O方式以任意大小来接收数据的)接收并累积数据的地方。
所以,Protocol实例如何得知它为哪条链接服务呢?若是你阅读IProtocol定义会发现一个makeConnection函数。这是一个回调函数,Twisted会在调用它时传递给其一个也是仅有的一个参数,即就是Transport实例。这个Transport实例就表明Protocol将要使用的链接。
Twisted包含不少内置能够实现不少通用协议的Protocol。你能够在twisted.protocols.basic
中
找到一些稍微简单点的。在你尝试写新Protocol时,最好是看看Twisted源码是否是已经有现成的存在。若是没有,那实现一个本身的协议是很是好的,正如咱们为诗歌下载客户端作的那样。
Protocol Factories
所以每一个链接须要一个本身的Portocol,并且这个Protocol是咱们本身定义类的实例。因为咱们会将建立链接的工做交给Twisted来完成,Twisted须要一种方式来为一个新的链接制定一个合适的协议。制定协议就是Protocol Factories的 工做了。
也许你已经猜到了,Protocol Factory的API由IProtocolFactory来定义,一样在interfaces
模块中。Protocol Factory就是Factory模式的一个具体实现。buildProtocol方法在每次被调用时返回一个新Protocol实例。它就是Twisted用来为新链接建立新Protocol实例的方法。
诗歌下载客户端2.0:第一滴心血
好吧,让咱们来看看由Twisted支持的诗歌下载客户端2.0。源码能够在这里twisted-client-2/get-poetry.py
。你能够像前面同样运行它,并获得相同的输出。这也是最后一个在接收到数据时打印其任务的客户端版本了。到如今为止,对于全部Twisted程序都是交替执行任务并处理相对较少数量数据的,应该很清晰了。咱们依然经过print函数来展现在关键时刻在进行什么内容,但未来客户端不会在这样繁锁。
在第二个版本中,sockets不会再出现了。咱们甚至不须要引入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类的实例对象。这是一个专门针对诗歌下载客户端的Protocol Factory,将它传递给reactor可让Twisted为咱们建立一个PeotryProtocol实例。
值得注意的是,从一开始咱们既没有实现Factory也没有去实现Protocol,不像在前面那个客户端中咱们去实例化咱们PoetrySocket类。咱们只是继承了Twisted在twisted.internet.protocol
中提供的基类
。
Factory
的基类是
twisted.internet.protocol.Factory
,
但咱们使用客户端专用(即不像服务器端那样监听一个链接,而是主动建立一个链接)的
ClientFactory
子类来继承。
咱们一样利用了
Twisted
的
Factory
已经实现了
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的实现buildProtocol过程是:安装(建立一个实例)咱们设置在protocol变量上的Protocol类与在这个实例(此处即PoetryProtocol的实例)的factory属性上设置一个产生它的Factory的引用(此处即实例化PoetryProtocol的PoetryClientFactory)。这个过程如图8所示:
图8:Protocol的生成过程
正如咱们提到的那样,位于Protocol对象内的factory属性字段容许在都由同一个factory产生的Protocol之间共享数据。因为Factories都是由用户代码来建立的(即在用户的控制中),所以这个属性也能够实现Protocol对象将数据传递回一开始初始化请求的代码中来,这将在第六部分看到。
值得注意的是,虽然在Protocol中有一个属性指向生成其的Protocol Factory,在Factory中也有一个变量指向一个Protocol类,但一般来讲,一个Factory能够生成多个Protocol。
在Protocol创立的第二步即是经过makeConnection与一个Transport联系起来。咱们无需本身来实现这个函数而使用Twisted提供的默认实现。默认状况是,makeConnection将Transport的一个引用赋给(Protocol的)transport属性,同时置(一样是Protocol的)connected属性为True,正如图9描述的同样:
图9:Protocol遇到其Transport
一旦初始化到这一步后,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())
每次dateReceved被调用就意味着咱们获得一个新字符串。因为与异步I/O交互,咱们不知道能接收到多少数据,所以将接收到的数据缓存下来直到完成一个完整的协议规定格式的消息。在咱们的例子中,诗歌只有在链接关闭时才下载完毕,所以咱们只是不断地将接收到的数据添加到咱们的.poem属性字段中。
注意咱们使用了Transport的getHost方法来取得数据来自的服务器信息。咱们这样作只是与前面的客户端保持一致。相反,咱们的代码没有必要这样作,由于咱们没有向服务器发送任何消息,也就没有必要知道服务器的信息了。
咱们来看一下dataReceved运行时的快照。在2.0版本相同的目录下有一个twisted-client-2/get-poetry-stack.py。它与2.0版本的不一样之处只在于:
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() 看见没,有咱们在1.0版本客户端的doRead回调函数。咱们前面也提到过,Twisted在创建新抽象层进会使用已有的实现而不是另起炉灶。所以必然会有一个IReadDescriptor的实例在辛苦的工做,它是由Twisted代码而非咱们本身的代码来实现。若是你表示怀疑,那么就看看twisted.internet.tcp中的实现吧。若是你浏览代码会发现,由同一个类实现了IWriteDescriptor与ITransport。所以 IreadDescriptor实际上就是变相的Transport类。能够用图10来形象地说明dateReceived的回调过程:
图10:dateReceived回调过程
一旦诗歌下载完成,PoetryProtocol就会通知它的PooetryClientFactory:
def connectionLost(self, reason): self.poemReceived(self.poem) def poemReceived(self, poem): self.factory.poem_finished(self.task_num, poem)
当transport的链接关闭时,conncetionLost回调会被激活。reason参数是一个twisted.python.failure.Failure
的实例对象,其携带的信息可以说明链接是被安全的关闭仍是因为出错被关闭的。咱们的客户端因认为老是能完整地下载完诗歌而忽略了这一参数。
工厂会在全部的诗歌都下载完毕后关闭reactor。再次重申:咱们代码的工做就是用来下载诗歌-这意味咱们的PoetryClientFactory缺乏复用性。咱们将在下一部分修正这一缺陷。值得注意的是,poem_finish回调函数是如何经过跟踪剩余诗歌数的:
... self.poetry_count -= 1 if self.poetry_count == 0: …
若是咱们采用多线程以让每一个线程分别下载诗歌,这样咱们就必须使用一把锁来管理这段代码以避免多个线程在同一时间调用poem_finish。可是在交互式体系下就没必要担忧了。因为reactor只能一次启用一个回调。
新的客户端实如今处理错误上也比先前的优雅的多,下面是PoetryClientFactory处理错误链接的回调实现代码:
def clientConnectionFailed(self, connector, reason): print 'Failed to connect to:', connector.getDestination() self.poem_finished()
注意,回调是在工厂内部而不是协议内部实现。因为协议是在链接创建后才建立的,而工厂可以在链接未能成功创建时捕获消息。
结束语:
版本2的客户端使用的抽象对于那些Twisted高手应该很是熟悉。若是仅仅是为在命令行上打印出下载的诗歌这个功能,那么咱们已经完成了。但若是想使咱们的代码可以复用,可以被内嵌在一些包含诗歌下载功能并能够作其它事情的大软件中,咱们还有许多工做要作,咱们将在第六部分讲解相关内容。