>  기사  >  백엔드 개발  >  Python의 Twisted 프레임워크를 사용하여 비차단 다운로드 프로그램을 구축하는 방법에 대한 예제 튜토리얼

Python의 Twisted 프레임워크를 사용하여 비차단 다운로드 프로그램을 구축하는 방법에 대한 예제 튜토리얼

高洛峰
高洛峰원래의
2017-02-03 16:42:581251검색

twisted가 지원하는 최초의 시 서버
Twisted는 대부분 서버 코드 작성에 사용되지만, 처음부터 최대한 간단하게 시작하기 위해 간단한 클라이언트부터 시작하겠습니다.
Twisted의 클라이언트를 사용해 보겠습니다. 소스 코드는 Twisted-client-1/get-poetry.py에 있습니다. 먼저 이전과 같이 세 개의 서버를 열고:

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

클라이언트를 실행합니다:

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 prints가 없는 비차단 모드 클라이언트에 가깝습니다. 동일한 방식으로 작동하기 때문에 이는 놀라운 일이 아닙니다.
이제 소스코드를 자세히 살펴보겠습니다.
참고: Twisted 사용법을 배우기 시작할 때 일부 저수준 Twisted API를 사용하게 됩니다. 이는 Twisted의 추상화 계층을 제거하여 Tiwsted를 내부에서 학습할 수 있도록 하기 위한 것입니다. 하지만 이는 우리가 학습에 사용하는 API가 실제 애플리케이션에서는 보이지 않을 수도 있음을 의미합니다. 이것만 기억하세요. 앞의 코드는 단지 연습일 뿐 실제 소프트웨어 작성의 예가 아닙니다.
보시다시피 PoetrySocket 인스턴스 세트가 먼저 생성됩니다. PoetrySocket이 초기화되면 서버에 연결하기 위한 자체 속성 필드로 네트워크 소켓을 생성하고 비차단 모드를 선택합니다.

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

결국 소켓을 사용하지 않도록 개선할 예정입니다. 추상적인 수준이지만 여기서는 여전히 이를 사용해야 합니다. 소켓을 생성한 후 PoetrySocket은 addReader 메소드를 통해 자신을 리액터에 전달합니다:

# 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는 Twisted의 리액터에 의해 구현된 인터페이스입니다. 따라서 모든 Twisted 리액터에는 위에서 설명한 대로 작동하는 addReader 메서드가 있습니다. 이 메소드 선언에 self 매개변수가 없는 이유는 공용 인터페이스 정의에만 관심이 있고 self 매개변수는 인터페이스 구현의 일부일 뿐이기 때문입니다(호출 시 self 매개변수가 명시적으로 전달되지 않음). 인터페이스 클래스는 기본 클래스로 인스턴스화되거나 구현되지 않습니다.

기술적으로 말하면 IReactorFDSet는 리액터에서 파일 설명자를 수신하는 데만 사용됩니다. 내가 아는 한, 구현된 모든 리액터는 이제 이 인터페이스를 구현합니다.
인터페이스 사용은 단지 문서화를 위한 것이 아닙니다. zope.interface를 사용하면 클래스가 하나 이상의 인터페이스를 구현함을 명시적으로 선언하고 런타임에 이러한 구현을 확인하는 메커니즘을 제공할 수 있습니다. 또한 인터페이스를 직접 구현하지 않는 클래스를 인터페이스와 동적으로 제공할 수 있는 프록시 메커니즘을 제공합니다. 하지만 여기서는 심층적인 연구를 하지 않을 것입니다.
최근 Python에 추가된 인터페이스와 가상 기본 클래스 사이의 유사점을 눈치채셨을 것입니다. 여기서는 이들 간의 유사점과 차이점을 분석하지 않습니다. 관심이 있으시면 Python 프로젝트 창시자인 Glyph가 작성한 이 주제에 관한 기사를 읽어보세요.
문서 설명에 따르면 addReader의 reader 매개변수는 IReadDescriptor 인터페이스를 구현하는 것임을 알 수 있습니다. 이는 PoetrySocket이 동일한 작업을 수행해야 함을 의미합니다.
인터페이스 모듈을 읽으면 다음 코드를 볼 수 있습니다.

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

동시에 PoetrySocket 클래스에 doRead 메소드가 있음을 알 수 있습니다. Twisted의 리액터에서 호출되면 소켓에서 비동기적으로 데이터를 읽습니다. 따라서 doRead는 실제로는 콜백 함수이지만 리액터에 직접 전달되지 않고 이 메소드를 구현한 객체 인스턴스가 전달됩니다. 이는 Twisted 프레임워크의 관례이기도 합니다. 인터페이스를 직접 구현하는 함수를 전달하는 대신 인터페이스를 구현하는 개체를 전달합니다. 이런 방식으로 매개변수를 통해 관련 콜백 함수 세트를 전달할 수 있습니다. 그리고 객체에 저장된 데이터를 통해 콜백 함수 간 통신도 가능합니다.
PoetrySocket에서 다른 콜백 기능을 구현하는 것은 어떻습니까? IReadDescriptor는 IFileDescriptor의 하위 클래스입니다. 즉, IReadDescriptor를 구현하는 사람은 누구나 IFileDescriptor를 구현해야 합니다. 코드를 주의 깊게 읽으면 다음 내용을 볼 수 있습니다:

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来形象地说明这一过程:

Python의 Twisted 프레임워크를 사용하여 비차단 다운로드 프로그램을 구축하는 방법에 대한 예제 튜토리얼

每当回调被激活,就轮到我们的代码将所有能够读的数据读回来然后非阻塞式的停止。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的缓冲区会在瞬间填满,这样一来我们这个客户端与前面那个阻塞式客户端相比就没有什么优势可言了。

추상적으로 클라이언트 구축하기
우선 이 클라이언트에는 실제로 네트워크 포트를 생성하고 그 포트에서 데이터를 수신하는 등의 지루한 코드가 있습니다. Twisted는 우리를 위해 이러한 일상적인 기능을 구현하므로 우리가 새 프로그램을 작성할 때마다 직접 구현할 필요가 없습니다. 이는 크로스 플랫폼인 경우 더 까다로운 세부 사항을 포함하는 비동기 I/O(이전 클라이언트 참조)와 관련된 까다로운 예외 처리 중 일부에서 벗어나기 때문에 특히 유용합니다. 어느 날 오후 여유 시간이 있으면 Twisted의 WIN32 구현 소스 코드를 살펴보고 크로스 플랫폼을 처리하는 데 필요한 작은 스레드가 몇 개 있는지 확인할 수 있습니다.
또 다른 문제는 오류 처리와 관련이 있습니다. 버전 1을 실행하는 Twisted 클라이언트가 제공되지 않는 포트에서 시를 다운로드하면 충돌이 발생합니다. 물론 이 오류를 수정할 수 있지만 아래에서 소개할 Twisted의 API를 통해 이러한 유형의 오류를 처리하는 것이 더 쉽습니다.
마지막으로 해당 클라이언트는 재사용할 수 없습니다. 클라이언트를 통해 시를 다운로드해야 하는 다른 모듈이 있으면 어떻게 되나요? 사람들은 당신의 시가 다운로드되었는지 어떻게 알 수 있나요? 단순히 시를 다운받아 다른 사람에게 전달하는 방식이 아니라 미리 대기 상태로 두는 방식을 사용할 수 없습니다. 이는 실제로 문제이지만 이 섹션에서는 다루지 않을 것입니다. 향후 섹션에서 확실히 다룰 것입니다.
우리는 첫 번째와 두 번째 문제를 해결하기 위해 몇 가지 고급 API와 인터페이스를 사용할 것입니다. Twisted 프레임워크는 많은 추상화 레이어로 느슨하게 구성되어 있습니다. 따라서 Twisted를 학습한다는 것은 각 계층에 사용할 수 있는 API, 인터페이스 및 인스턴스와 같이 이러한 계층이 제공하는 기능을 배우는 것을 의미합니다. 다음으로 Twisted의 가장 중요한 부분을 분석하여 Twisted가 어떻게 구성되어 있는지 더 잘 이해할 것입니다. Twisted의 전체적인 구조에 익숙해지면 새로운 부분을 배우는 것이 훨씬 쉬워질 것입니다.
일반적으로 각 Twisted 추상화는 하나의 특정 개념에만 관련됩니다. 예를 들어, 4부에서 클라이언트가 사용하는 IReadDescriptor는 "바이트를 읽을 수 있는 파일 설명자"의 추상화입니다. 추상화는 인터페이스를 정의하여 추상화를 구현(즉, 인터페이스 구현)하려는 객체의 동작을 지정하는 경우가 많습니다. 새로운 Twisted 추상화 개념을 학습할 때 기억해야 할 가장 중요한 사항은 다음과 같습니다.
대부분의 상위 수준 추상화는 하위 수준 추상화를 기반으로 구축되며 소수는 독립적입니다.
따라서 새로운 Twisted 추상화를 배울 때 그것이 무엇을 하는지, 무엇을 하지 않는지 항상 기억하세요. 특히, 초기 추상화 A가 기능 F를 구현하는 경우 기능 F는 다른 추상화에 의해 구현될 가능성이 없습니다. 또한 다른 추상화에 F 기능이 필요한 경우 F 자체를 구현하는 대신 A를 사용합니다. (보통 B는 A를 상속하거나 A의 인스턴스에 대한 참조를 얻을 수 있습니다.)
네트워크는 매우 복잡하므로 Twisted에는 많은 추상 개념이 포함되어 있습니다. 낮은 수준의 추상화부터 시작하여 Twisted 프로그램의 다양한 부분이 어떻게 구성되어 있는지 더 명확하게 볼 수 있기를 바랍니다.
코어 루프 몸체

우리가 배워야 할 첫 번째 추상화이자 Twisted에서 가장 중요한 추상화는 리액터입니다. Twisted를 통해 구축된 모든 프로그램의 중심에는 프로그램의 레이어 수에 관계없이 항상 프로그램을 지속적으로 실행하는 리액터 루프가 있습니다. 리액터보다 더 기본적인 지원은 없습니다. 실제로 Twisted의 다른 부분(즉, 리액터 루프 제외)은 다음과 같이 이해될 수 있습니다. 이 부분은 모두 X가 리액터 콘텐츠를 더 잘 사용하도록 돕는 데 사용됩니다. 이전 클라이언트처럼 로우레벨 API를 고집할 수도 있지만, 그렇게 고집한다면 우리 스스로 많은 콘텐츠를 구현해야 할 것입니다. 그리고 더 높은 수준에서는 훨씬 적은 양의 코드를 작성할 수 있음을 의미합니다.
그러나 외부에서 문제를 생각하고 처리할 때 리액터의 존재를 잊기 쉽습니다. 일반적인 크기의 Twisted 프로그램에서는 실제로 리액터 API와의 직접적인 상호 작용이 거의 없습니다. 낮은 수준의 추상화에도 마찬가지입니다(즉, 직접적으로 상호 작용하는 경우는 거의 없습니다). 이전 클라이언트에서 사용한 파일 설명자 추상화는 더 높은 수준의 추상화로 훨씬 더 잘 요약되어 실제 Twisted 프로그램에서는 거의 볼 수 없습니다. (아직 내부적으로 사용되고 있어서 우리가 볼 수 없을 뿐입니다.)
파일 디스크립터 추상화 메시지는 문제가 되지 않습니다. Twisted가 비동기 I/O 처리를 주도하게 하여 실제로 해결하려는 문제에 더 집중할 수 있습니다. 그러나 리액터의 경우에는 다르며 결코 사라지지 않습니다. Twisted를 사용하기로 선택했다는 것은 Reactor 패턴을 사용하기로 선택했다는 의미이며 콜백과 멀티태스킹을 사용하는 "대화형" 프로그래밍이 필요하다는 의미입니다.

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

Python의 Twisted 프레임워크를 사용하여 비차단 다운로드 프로그램을 구축하는 방법에 대한 예제 튜토리얼

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

Python의 Twisted 프레임워크를 사용하여 비차단 다운로드 프로그램을 구축하는 방법에 대한 예제 튜토리얼

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

   

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

更多Python의 Twisted 프레임워크를 사용하여 비차단 다운로드 프로그램을 구축하는 방법에 대한 예제 튜토리얼相关文章请关注PHP中文网!

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.