Heim  >  Artikel  >  Backend-Entwicklung  >  Ein Beispiel-Tutorial zum Erstellen eines nicht blockierenden Download-Programms mit dem Twisted-Framework von Python

Ein Beispiel-Tutorial zum Erstellen eines nicht blockierenden Download-Programms mit dem Twisted-Framework von Python

高洛峰
高洛峰Original
2017-02-03 16:42:581251Durchsuche

Der erste von Twisted unterstützte Poesieserver
Obwohl Twisted hauptsächlich zum Schreiben von Servercode verwendet wird, beginnen wir mit einem einfachen Client, um von Anfang an so einfach wie möglich zu beginnen.
Versuchen wir es mit dem Client von Twisted. Der Quellcode befindet sich in Twisted-Client-1/get-poetry.py. Öffnen Sie zunächst wie zuvor drei 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

und führen Sie den Client aus:

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

Sie sehen die Befehlszeile des Clients druckt:

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

, was dem entspricht, was unser nicht blockierender Modus-Client ohne Twisted druckt. Dies ist nicht überraschend, da sie auf die gleiche Weise funktionieren.
Lassen Sie uns nun einen genaueren Blick auf den Quellcode werfen.
Hinweis: Wenn wir anfangen, Twisted zu verwenden, werden wir einige Twisted-APIs auf niedriger Ebene verwenden. Dies geschieht, um die Abstraktionsschicht von Twisted zu entfernen, sodass wir Tiwsted von innen heraus lernen können. Dies bedeutet jedoch, dass die APIs, die wir beim Lernen verwenden, möglicherweise nicht in tatsächlichen Anwendungen zu sehen sind. Denken Sie daran: Der vorangehende Code ist nur eine Übung und kein Beispiel für das Schreiben echter Software.
Wie Sie sehen können, wird zunächst eine Reihe von PoetrySocket-Instanzen erstellt. Wenn PoetrySocket initialisiert wird, erstellt es einen Netzwerk-Socket als eigenes Attributfeld, um eine Verbindung zum Server herzustellen, und wählt den nicht blockierenden Modus aus:

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

Irgendwann werden wir auf „Nicht verwenden“ umsteigen es. Socket-Abstraktionsebene, aber wir müssen es hier immer noch verwenden. Nach dem Erstellen des Sockets übergibt sich PoetrySocket über die Methode addReader an den Reaktor:

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

Diese Methode stellt Twisted einen Dateideskriptor zur Verfügung, um die zu sendenden Daten zu überwachen. Warum übergeben wir Twisted nicht einen Dateideskriptor oder eine Rückruffunktion, sondern eine Objektinstanz? Und es gibt in Twisted keinen Code, der sich auf diesen Poesiedienst bezieht. Woher weiß er also, wie er mit unserer Objektinstanz interagieren soll? Vertrauen Sie mir, ich habe es mir angesehen, öffne das Modul „twisted.internet.interfaces“ und finde gemeinsam mit mir heraus, was los ist.

Twisted-Schnittstelle
In Twisted gibt es viele Untermodule, die als Schnittstellen bezeichnet werden. Jede definiert eine Reihe von Schnittstellenklassen. Seit Version 8.0 verwendet Twisted zope.interface als Basisklasse für diese Klassen. Auf die Details gehen wir hier aber nicht näher ein. Uns interessieren nur die Twisted-Unterklassen, die Sie hier sehen.
Einer der Hauptzwecke der Verwendung von Schnittstellen ist die Dokumentation. Als Python-Programmierer müssen Sie sich mit Duck Typing auskennen. (Python-Philosophie: „Wenn es wie eine Ente aussieht und wie eine Ente klingt, können Sie es wie eine Ente behandeln.“ Daher strebt die Schnittstelle von Python-Objekten danach, einfach und einheitlich zu sein, ähnlich den schnittstellenorientierten Programmierideen in anderen Sprachen.) Lesen Sie „twisted.internet .interfaces“, um die addReader-Definition der Methode zu finden. Ihre Definition finden Sie in 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 ist eine Schnittstelle, die vom Reaktor von Twisted implementiert wird. Daher verfügt jeder Twisted-Reaktor über eine addReader-Methode, die wie oben beschrieben funktioniert. Der Grund, warum diese Methodendeklaration keinen Self-Parameter hat, liegt darin, dass sie sich nur um eine öffentliche Schnittstellendefinition kümmert und der Self-Parameter nur Teil der Schnittstellenimplementierung ist (beim Aufruf wird kein Self-Parameter explizit übergeben). Schnittstellenklassen werden niemals als Basisklassen instanziiert oder implementiert.

Technisch gesehen wird IReactorFDSet vom Reaktor nur zum Abhören von Dateideskriptoren verwendet. Soweit ich weiß, implementieren inzwischen alle implementierten Reaktoren diese Schnittstelle.
Die Verwendung von Schnittstellen dient nicht nur der Dokumentation. Mit zope.interface können Sie explizit deklarieren, dass eine Klasse eine oder mehrere Schnittstellen implementiert, und einen Mechanismus zum Überprüfen dieser Implementierungen zur Laufzeit bereitstellen. Es bietet auch einen Proxy-Mechanismus, der eine Klasse dynamisch bereitstellen kann, die keine Schnittstelle direkt mit der Schnittstelle implementiert. Aber wir werden hier keine detaillierte Untersuchung durchführen.
Vielleicht ist Ihnen die Ähnlichkeit zwischen Schnittstellen und den kürzlich zu Python hinzugefügten virtuellen Basisklassen aufgefallen. Wir werden hier nicht die Ähnlichkeiten und Unterschiede zwischen ihnen analysieren. Wenn Sie interessiert sind, können Sie einen Artikel zu diesem Thema lesen, der von Glyph, dem Gründer des Python-Projekts, verfasst wurde.
Aus der Beschreibung des Dokuments geht hervor, dass der Reader-Parameter von addReader die IReadDescriptor-Schnittstelle implementiert. Das bedeutet, dass unser PoetrySocket dasselbe tun muss.
Wenn wir das Schnittstellenmodul lesen, sehen wir den folgenden Code:

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

Gleichzeitig sehen Sie, dass es in unserer PoetrySocket-Klasse eine doRead-Methode gibt. Wenn es vom Twisted-Reaktor aufgerufen wird, liest es asynchron Daten aus dem Socket. Daher ist doRead eigentlich eine Rückruffunktion, die jedoch nicht direkt an den Reaktor übergeben wird, sondern eine Objektinstanz, die diese Methode implementiert. Dies ist auch die Konvention im Twisted-Framework: Anstatt die Funktion, die eine Schnittstelle implementiert, direkt zu übergeben, übergeben Sie das Objekt, das sie implementiert. Auf diese Weise können wir eine Reihe verwandter Rückruffunktionen über einen Parameter übergeben. Und es ist auch möglich, zwischen Rückruffunktionen über im Objekt gespeicherte Daten zu kommunizieren.
Wie wäre es mit der Implementierung anderer Rückruffunktionen in PoetrySocket? Beachten Sie, dass IReadDescriptor eine Unterklasse von IFileDescriptor ist. Dies bedeutet, dass jeder, der IReadDescriptor implementiert, IFileDescriptor implementieren muss. Wenn Sie den Code sorgfältig lesen, sehen Sie Folgendes:

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

我将文档描述省略掉了,但这些函数的功能从字面上就可以理解:fileno返回我们想监听的文件描述符,connectionLost是当连接关闭时被调用。你也看到了,PoetrySocket实现了这些方法。
最后,IFileDescriptor继承了ILoggingContext,这里我不想再展现其源码。我想说的是,这就是为什么我们要实现一个logPrefix回调函数。你可以在interface模块中找到答案。
注意:你也许注意到了,当连接关闭时,在doRead中返回了一个特殊的值。我是如何知道的?说实话,没有它程序是无法正常工作的。我是在分析Twisted源码中发现其它相应的方法采取相同的方法。你也许想好好研究一下:但有时一些文档或书的解释是错误的或不完整的。

更多关于回调的知识
我们使用Twisted的异步客户端和前面的没有使用Twisted的异步客户非常的相似。两者都要连接它们自己的socket,并以异步的方式从中读取数据。最大的区别在于:使用Twisted的客户端并没有使用自己的select循环-而使用了Twisted的reactor。 doRead回调函数是非常重要的一个回调。Twisted调用它来告诉我们已经有数据在socket接收完毕。我可以通过图7来形象地说明这一过程:

Ein Beispiel-Tutorial zum Erstellen eines nicht blockierenden Download-Programms mit dem Twisted-Framework von Python

每当回调被激活,就轮到我们的代码将所有能够读的数据读回来然后非阻塞式的停止。Twisted是不会因为什么异常状况(如没有必要的阻塞)而终止我们的代码。那么我们就故意写个会产生异常状况的客户端看看到底能发生什么事情。可以在twisted-client-1/get-poetry-broken.py中看到源代码。这个客户端与你前面看到的同样有两个异常状况出现:
这个客户端并没有选择非阻塞式的socket
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的缓冲区会在瞬间填满,这样一来我们这个客户端与前面那个阻塞式客户端相比就没有什么优势可言了。

Abstrakter Aufbau des Clients
Zuallererst verfügt dieser Client tatsächlich über so langweiligen Code wie das Erstellen eines Netzwerkports und das Empfangen von Daten am Port. Twisted soll diese Routinefunktionen für uns implementieren und uns ersparen, sie jedes Mal selbst implementieren zu müssen, wenn wir ein neues Programm schreiben. Dies ist besonders nützlich, da es uns von einigen der kniffligen Ausnahmebehandlungen befreit, die mit asynchroner E/A verbunden sind (siehe den vorherigen Client), die kniffligere Details beinhalten, wenn es plattformübergreifend sein soll. Wenn Sie eines Nachmittags Freizeit haben, können Sie sich den Quellcode der WIN32-Implementierung von Twisted ansehen und sehen, wie viele kleine Threads es für die plattformübergreifende Handhabung gibt.
Ein weiteres Problem hängt mit der Fehlerbehandlung zusammen. Wenn der Twisted-Client, auf dem Version 1 ausgeführt wird, Gedichte von einem Port herunterlädt, der nicht bedient wird, stürzt er ab. Natürlich können wir diesen Fehler beheben, aber es ist einfacher, diese Art von Fehlern über die APIs von Twisted zu handhaben, die wir weiter unten vorstellen.
Schließlich kann dieser Client nicht wiederverwendet werden. Was ist, wenn es ein anderes Modul gibt, das Gedichte über unseren Kunden herunterladen muss? Woher wissen die Leute, dass Ihre Gedichte heruntergeladen wurden? Wir können nicht einfach eine Methode verwenden, um ein Gedicht herunterzuladen und es dann an andere weiterzugeben, sondern sie vorher in einem Wartezustand belassen. Dies ist in der Tat ein Problem, aber wir werden es in diesem Abschnitt nicht ansprechen – es wird definitiv in einem zukünftigen Abschnitt behandelt.
Wir werden einige High-Level-APIs und -Schnittstellen verwenden, um das erste und zweite Problem zu lösen. Das Twisted-Framework besteht lose aus vielen Abstraktionsschichten. Daher bedeutet das Erlernen von Twisted auch, zu lernen, welche Funktionen diese Schichten bieten, beispielsweise welche APIs, Schnittstellen und Instanzen für jede Schicht verfügbar sind. Als nächstes analysieren wir die wichtigsten Teile von Twisted, um ein besseres Gefühl dafür zu bekommen, wie Twisted organisiert ist. Sobald Sie mit der Gesamtstruktur von Twisted vertraut sind, wird Ihnen das Erlernen neuer Teile viel leichter fallen.
Im Allgemeinen bezieht sich jede Twisted-Abstraktion nur auf ein bestimmtes Konzept. Beispielsweise verwendet der Client in Teil 4 IReadDescriptor, eine Abstraktion von „einem Dateideskriptor, der Bytes lesen kann“. Eine Abstraktion spezifiziert häufig das Verhalten von Objekten, die die Abstraktion implementieren (d. h. die Schnittstelle implementieren) möchten, indem sie eine Schnittstelle definieren. Beim Erlernen neuer Twisted-Abstraktionskonzepte ist Folgendes zu beachten:
Die meisten Abstraktionen auf hoher Ebene basieren auf Abstraktionen auf niedriger Ebene, und nur wenige sind unabhängig.
Wenn Sie also eine neue verdrehte Abstraktion lernen, denken Sie immer daran, was sie tut und was nicht. Insbesondere wenn eine frühe Abstraktion A Merkmal F implementiert, ist es unwahrscheinlich, dass Merkmal F von einer anderen Abstraktion implementiert wird. Wenn außerdem eine andere Abstraktion die F-Funktion erfordert, wird A verwendet, anstatt F selbst zu implementieren. (Normalerweise kann B A erben oder einen Verweis auf eine Instanz von A erhalten)
Das Netzwerk ist sehr komplex, daher enthält Twisted viele abstrakte Konzepte. Indem wir mit einer Abstraktion auf niedriger Ebene beginnen, hoffen wir, klarer zu sehen, wie die verschiedenen Teile eines Twisted-Programms organisiert sind.
Kernschleifenkörper

Die erste Abstraktion, die wir lernen müssen, und die wichtigste in Twisted, ist der Reaktor. Im Zentrum jedes mit Twisted erstellten Programms befindet sich, egal wie viele Ebenen Ihr Programm hat, immer eine Reaktorschleife, die dafür sorgt, dass das Programm kontinuierlich läuft. Es gibt keine grundlegendere Unterstützung als den Reaktor. Tatsächlich können andere Teile von Twisted (mit Ausnahme der Reaktorschleife) auf diese Weise verstanden werden: Sie werden alle verwendet, um X bei der besseren Nutzung des Reaktorinhalts zu unterstützen. Obwohl es möglich ist, darauf zu bestehen, die Low-Level-APIs wie beim vorherigen Client zu verwenden, müssen wir, wenn wir darauf bestehen, viele Inhalte selbst implementieren. Und auf einer höheren Ebene bedeutet das, dass wir viel weniger Code schreiben können.
Aber wenn man Probleme von außen betrachtet und damit umgeht, vergisst man leicht die Existenz eines Reaktors. In jedem Twisted-Programm üblicher Größe wird es tatsächlich nur sehr wenig direkte Interaktion mit Reaktor-APIs geben. Das Gleiche gilt für Abstraktionen auf niedriger Ebene (d. h. wir interagieren selten direkt mit ihnen). Die Dateideskriptor-Abstraktion, die wir im vorherigen Client verwendet haben, wird durch Abstraktionen auf höherer Ebene so gut zusammengefasst, dass wir sie in echten Twisted-Programmen selten antreffen. (Sie werden immer noch intern verwendet, wir können sie einfach nicht sehen)
Was die Dateideskriptor-Abstraktionsnachricht betrifft, ist dies kein Problem. Überlassen Sie Twisted das Ruder der asynchronen E/A-Verarbeitung, damit wir uns mehr auf das Problem konzentrieren können, das wir tatsächlich lösen möchten. Aber beim Reaktor ist es anders, es wird nie verschwinden. Wenn Sie sich für die Verwendung von Twisted entscheiden, bedeutet dies, dass Sie sich für die Verwendung des Reactor-Musters entscheiden und dass Sie eine „interaktive“ Programmierung mit Rückrufen und Multitasking benötigen.

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
因此每个连接需要一个自己的Protocol,而且这个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为我们创建一个PoetryProtocol实例。
值得注意的是,从一开始我们既没有实现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)。这个过程如图

正如我们提到的那样,位于Protocol对象内的factory属性字段允许在都由同一个factory产生的Protocol之间共享数据。由于Factories都是由用户代码来创建的(即在用户的控制中),因此这个属性也可以实现Protocol对象将数据传递回一开始初始化请求的代码中来,这将在第六部分看到。
值得注意的是,虽然在Protocol中有一个属性指向生成其的Protocol Factory,在Factory中也有一个变量指向一个Protocol类,但通常来说,一个Factory可以生成多个Protocol。
在Protocol创立的第二步便是通过makeConnection与一个Transport联系起来。我们无需自己来实现这个函数而使用Twisted提供的默认实现。默认情况是,makeConnection将Transport的一个引用赋给(Protocol的)transport属性,同时置(同样是Protocol的)connected属性为True

Ein Beispiel-Tutorial zum Erstellen eines nicht blockierenden Download-Programms mit dem Twisted-Framework von Python

一旦初始化到这一步后,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的回调过程:

Ein Beispiel-Tutorial zum Erstellen eines nicht blockierenden Download-Programms mit dem Twisted-Framework von Python

一旦诗歌下载完成,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()

   

注意,回调是在工厂内部而不是协议内部实现。由于协议是在连接建立后才创建的,而工厂能够在连接未能成功建立时捕获消息。

更多Ein Beispiel-Tutorial zum Erstellen eines nicht blockierenden Download-Programms mit dem Twisted-Framework von Python相关文章请关注PHP中文网!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn