Twisted 专家级编程(一)
一、Twisted 事件驱动编程简介
Twisted 是一个强大的、经过充分测试的、成熟的并发网络库和框架。正如我们将在本书中看到的,十多年来,许多项目和个人都使用了它,并取得了巨大的成效。
同时,Twisted 是大型的、复杂的、古老的。它的词典里充斥着奇怪的名字,比如“反应堆”、“协议”、“终点”和“延迟”。这些描述了一种哲学和架构,这种哲学和架构让具有多年 Python 经验的新手和老手都感到困惑。
两个基本的编程范例通知了 Twisted 的 API 万神殿:事件驱动编程和异步编程。JavaScript 的兴起和asyncio在 Python 标准库中的引入使这两者进一步成为主流,但是这两种范式都没有完全主导 Python 编程,以至于仅仅了解这种语言就使它们变得熟悉。它们仍然是为中级或高级程序员保留的专门主题。
本章和下一章将介绍事件驱动和异步编程背后的动机,然后展示 Twisted 如何使用这些范例。它们为后面探索真实世界 Twisted 程序的章节奠定了基础。
我们将从探索 Twisted 环境之外的事件驱动编程的本质开始。一旦我们了解了事件驱动编程的定义,我们将会看到 Twisted 如何提供软件抽象来帮助开发人员编写清晰有效的事件驱动程序。我们还将沿途停下来了解这些抽象的一些独特部分,如接口,并探索它们是如何在 Twisted 的网站上记录的。
在这一章结束时,你将知道 Twisted 的术语:协议、传输、反应器、消费者和生产者。这些概念构成了 Twisted 事件驱动编程方法的基础,了解这些概念对于用 Twisted 编写有用的软件是必不可少的。
关于 Python 版本的说明
Twisted 本身支持 Python 2 和 3,所以本章中的所有代码示例都可以在 Python 2 和 3 上运行。Python 3 是未来,但 Twisted 的部分优势在于其丰富的协议实现历史;出于这个原因,即使您从未编写过代码,也要熟悉在 Python 2 上运行的代码,这一点很重要。
什么是事件驱动编程?
一个事件是导致一个事件驱动程序执行一个动作的东西。这个宽泛的定义允许许多程序被理解为事件驱动的;例如,考虑一个根据用户输入打印Hello或World!的简单程序:
import sys
line = sys.stdin.readline().strip()
if line == "h":
print("Hello")
else:
print("World")
超过标准输入的输入行的可用性是一个事件。我们的程序在sys.stdin.readline()暂停,它要求操作系统允许用户输入一个完整的行。直到收到一个,我们的计划才能取得进展。当操作系统接收到输入,Python 的内部机制确定它是一行时,sys.stdin.readline()通过将数据返回给它来恢复我们的程序。这次恢复是推动我们计划向前发展的事件。那么,即使这个简单的程序也可以理解为一个事件驱动的程序。
多个事件
接收单个事件然后退出的程序不会从事件驱动的方法中受益。然而,一次可以发生多件事情的程序更自然地围绕事件来组织。图形用户界面就意味着这样一个程序:在任何时候,用户都可能点击一个按钮,从菜单中选择一个项目,滚动一个文本小部件,等等。
这是我们之前的程序的一个版本,带有 Tkinter GUI:
from six.moves import tkinter
from six.moves.tkinter import scrolledtext
class Application(tkinter.Frame):
def __init__ (self, root):
super(Application,self). __init__ (root)
self.pack()
self.helloButton = tkinter.Button(self,
text="Say Hello",
command=self.sayHello)
self.worldButton = tkinter.Button(self,
text="Say World",
command=self.sayWorld)
self.output = scrolledtext.ScrolledText(master=self)
self.helloButton.pack(side="top")
self.worldButton.pack(side="top")
self.output.pack(side="top")
def outputLine(self, text):
self.output.insert(tkinter.INSERT, text+ '\n')
def sayHello(self):
self.outputLine("Hello")
def sayWorld(self):
self.outputLine("World")
应用(tkinter。Tk())。主循环()
这个版本的程序为用户提供了两个按钮,每个按钮都可以生成一个独立的点击事件。这与我们之前的程序不同,在我们之前的程序中,只有sys.stdin.readline可以生成单个“生产线就绪”事件。
我们通过将事件处理程序与每一个相关联来处理每个按钮事件可能发生的情况。Tkinter 按钮接受一个可调用的command以在被点击时调用。当标有“Say Hello”的按钮生成一个点击事件时,该事件驱动我们的程序调用Application.sayHello,如图 1-1 所示。这反过来将由Hello组成的一行输出到一个可滚动的文本小部件。同样的过程也适用于标有“Say Hello”和Application.sayWorld的按钮。
图 1-1
我们的 Tkinter GUI 应用在一系列点击“说你好”和“说世界”之后
我们的Application类继承的tkinter.Frame的mainloop方法,等待绑定到它的按钮生成一个事件,然后运行相关的事件处理程序。在每个事件处理程序运行之后,tkinter.Frame.mainloop再次开始等待新的事件。一个监视事件源并分派其相关处理程序的循环是典型的事件驱动程序,被称为事件循环。
这些概念是事件驱动编程的核心:
-
事件表示某件事情已经发生,程序应该对此做出反应。在我们的两个例子中,事件自然地对应于程序输入,但是正如我们将看到的,它们可以表示导致我们的程序执行一些动作的任何东西。
-
事件处理程序构成了程序对事件的反应。有时一个事件的处理程序仅仅由一系列代码组成,就像我们的
sys.stdin.readline例子一样,但是更多的时候它被一个函数或方法封装,就像我们的tkinter例子一样。 -
一个事件循环等待事件并调用与每个事件相关的事件处理程序。不是所有的事件驱动程序都有事件循环;我们的例子没有,因为它只响应单个事件。然而,大多数类似于我们的
tkinter例子,它们在最终退出之前处理许多事件。这类程序使用事件循环。
多路复用和解复用
事件循环等待事件的方式影响了我们编写事件驱动程序的方式,所以我们必须仔细研究一下。考虑我们的tkinter例子及其两个按钮;mainloop中的事件循环必须等到用户至少点击了一个按钮。一个简单的实现可能如下所示:
def mainloop(self):
while self.running:
ready = [button for button in self.buttons if button.hasEvent()]
if ready:
self.dispatchButtonEventHandlers(ready)
mainloop不断地为新事件轮询每个按钮,只为那些准备好事件的按钮分派事件处理程序。当没有事件准备好时,程序没有进展,因为没有采取需要响应的动作。事件驱动程序必须在这些不活动期间暂停执行。
在我们的mainloop例子中,while 循环暂停它的程序,直到其中一个按钮被点击,并且sayHello或sayWorld应该运行。除非用户使用鼠标的速度超乎寻常的快,否则这个循环大部分时间都花在检查没有被点击的按钮上。这被称为忙等待,因为程序正在积极地忙等待。
像这样的繁忙等待会暂停程序的整体执行,直到它的一个事件源报告一个事件,因此它足以作为一种暂停事件循环的机制。
驱动我们的实现忙碌等待的内部列表理解提出了一个关键问题:发生了什么事情吗?答案来自于ready变量,这个变量包含了在一个地方被点击过的所有按钮。ready的真决定了事件循环问题的答案:当ready为空因而为假时,没有按钮被点击,所以什么也没发生。然而,当它是真的时,至少有一个被点击了,所以一些事情已经发生了。
构建ready的列表理解将许多独立的输入合并成一个。这被称为多路复用,而从单个合并的输入中分离出不同输入的逆过程被称为解复用。列表理解将我们的按钮复用到ready中,而dispatchButtonEventHandlers方法通过调用每个事件的处理程序将它们解复用出来。
现在,我们可以通过精确描述事件循环等待事件的方式来完善我们对事件循环的理解:
- 一个事件循环通过将事件源复用到一个输入中来等待事件。当该输入指示事件已经发生时,事件循环将其解复用为组成输入,并调用与每个输入相关联的事件处理程序。
我们的mainloop复用器浪费了大部分时间来轮询没有被点击的按钮。并非所有多路复用器都如此低效。tkinter.Frame.mainloop的实际实现使用了一个类似的多路复用器来轮询所有的小部件,除非操作系统提供了更有效的原语。为了提高效率,mainloop的多路复用器利用了计算机检查 GUI 部件的速度比人与它们互动的速度更快的洞察力,并插入了一个sleep调用,使整个程序暂停几毫秒。这允许程序被动地花费部分忙等待循环,而不是主动地什么都不做,以可忽略的延迟为代价节省 CPU 时间和能量。
虽然 Twisted 可以与图形用户界面集成,并且事实上对tkinter有特殊的支持,但它本质上是一个网络引擎。套接字,而不是按钮,是网络中的基本对象,操作系统公开了用于复用套接字事件的有效原语。Twisted 的事件循环使用这些原语来等待事件。要理解 Twisted 的事件驱动编程方法,我们必须理解这些套接字和这些多路复用网络原语之间的交互。
select复用器
它的历史,它的兄弟姐妹,和它的目的
几乎所有现代操作系统都支持select多路复用器。select之所以得名,是因为它能够获取一个套接字列表,并且只“选择”那些具有准备好处理的事件的套接字。
诞生于 1983 年,那时计算机的能力远不及现在。因此,它的接口使它无法以最高效率运行,尤其是在复用大量套接字时。每个操作系统家族都提供了自己的、更高效的多路复用器,比如 BSD 的kqueue和 Linux 的epoll,但是没有两个能够互操作。幸运的是,它们的原理与select非常相似,我们可以从select的原理中归纳出它们的行为。我们将使用select来探究这些插座多路复用器的行为。
select和插座
下面的代码省略了错误处理,并将在实践中出现的许多边缘情况下中断。它只是作为一种教学工具。不要在实际应用中使用它。用 Twisted 代替。 Twisted 力求正确处理错误和边缘情况;这也是它的实现如此复杂的部分原因。
排除了免责声明,让我们开始一个交互式 Python 会话,并为select创建套接字以进行多路传输:
>>> import socket
>>> listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> listener.bind(('127.0.0.1', 0))
>>> listener.listen(1)
>>> client = socket.create_connection(listener.getsockname())
>>> server, _ = listener.accept()
对套接字 API 的完整解释超出了本书的范围。事实上,我们期望我们讨论的部分将引导您选择 Twisted!然而,前面的代码包含了比无关细节更基本的概念:
-
listener-该插座可以接受输入连接。它是一个互联网(socket.AF_INET)和 TCP (socket.SOCK_STREAM)套接字,客户端可以通过内部的、仅限本地的网络接口(通常有一个127.0.0.1地址)和操作系统(0)随机分配的端口进行访问。这个监听器可以为一个传入的连接执行必要的设置,并对其进行排队,直到我们读取它为止(listen(1))。 -
这个插座是一个输出连接。Python 的
socket.create_connection函数接受一个代表要连接的监听套接字的(host, port)元组,并返回一个与之连接的套接字。因为我们的监听套接字在同一个进程中,并被命名为listener,所以我们可以用listener.getsockname()检索它的主机和端口。 -
server-服务器的传入连接。一旦client连接到我们的主机和端口,我们必须接受来自listener的长度为 1 的队列的连接。listener.accept返回一个(socket, address)元组;我们只需要套接字,所以我们丢弃了地址。一个真正的程序可能会记录地址或使用它来跟踪连接度量。我们通过套接字的listen方法将监听队列设置为 1,在我们调用accept并允许create_connection返回之前,监听队列为我们保存这个套接字。
client和server是同一个 TCP 连接的两端。已建立的 TCP 连接没有“客户端”和“服务器”的概念;我们的client套接字与我们的server套接字具有相同的读、写或关闭连接的特权:
>>> data = b"xyz"
>>> client.sendall(data)
>>> server.recv(1024) == data
True
>>> server.sendall(data)
>>> client.recv(1024) == data
True
套接字事件的方式和原因
在幕后,操作系统为每个 TCP 套接字维护读写缓冲区,以考虑网络的不可靠性以及以不同速度读写的客户端和服务器。如果server暂时无法接收数据,我们通过的b"xyz"``client.sendall将保留在其写缓冲区中,直到server再次变为活动状态。类似地,如果我们太忙而没有时间调用client.recv来接收发送的b"xyz" server.sendall,那么client'的读缓冲区会一直保存它,直到我们有时间接收它。我们传递的数字recv表示我们愿意从读缓冲区中移除的最大数据量。如果读缓冲区的数据小于最大值,如我们的例子所示,recv将从缓冲区中移除所有的数据并返回。
我们的套接字的双向性意味着两种可能的事件:
-
一个可读事件,这意味着套接字有一些可用的东西。当数据到达套接字的接收缓冲区时,连接的服务器套接字会生成该事件,因此在可读事件后调用
recv将立即返回该数据。断开由无数据的recv表示。按照惯例,当我们可以accept一个新的连接时,一个监听套接字会生成这个事件。 -
一个可写事件,这意味着套接字的写缓冲区中有可用空间。这是一个微妙的问题:只要套接字从服务器接收到对数据的确认,它在网络上传输的速度比我们将数据添加到发送缓冲区的速度快,它就保持可写。
select的界面反映了这些可能的事件。它最多接受四个参数:
-
监控可读事件的套接字序列;
-
监控可写事件的套接字序列;
-
监控“异常事件”的套接字序列在我们的例子中,不会发生异常事件,所以我们总是在这里传递一个空列表;
-
一个可选的超时。这是
select等待其中一个监视器套接字生成事件的秒数。省略这个参数将导致select永远等待。
我们可以询问select我们的套接字刚刚生成的事件:
>>> import select
>>> maybeReadable = [listener, client, server]
>>> maybeWritable = [client, server]
>>> readable, writable, _ = select.select(maybeReadable, maybeWritable, [], 0)
>>> readable
[]
>>> writable == maybeWritable and writable == [client, server]
True
我们通过提供超时 0 来指示select不要等待任何新事件。如上所述,我们的client和server套接字可能是可读或可写的,而我们的listener只能接受传入的连接,只能是可读的。
如果我们忽略了超时,select会暂停我们的程序,直到它所监控的一个套接字变得可读或可写。这种执行的暂停类似于多路复用的 busy-wait,它在我们上面的天真的mainloop实现中轮询所有的按钮。
调用select 多路复用套接字比繁忙等待更有效,因为操作系统只有在至少一个事件已经生成时才会恢复我们的程序;在内核内部,一个事件循环,与我们的select相似,等待来自网络硬件的事件,并将它们分派给我们的应用。
处理事件
select返回一个包含三个列表的元组,顺序与其参数相同。迭代每个返回的列表解复用 select的返回值。我们的套接字都没有生成可读的事件,尽管我们已经将数据写入了client和server;我们之前对recv的调用清空了它们的读取缓冲区,自从我们接受server以来,没有新的连接到达listener。然而,client和server都生成了一个可写事件,因为它们的发送缓冲区中有可用空间。
从client向server发送数据导致server生成一个可读事件,因此select将其放入readables列表:
>>> client.sendall(b'xyz')
>>> readable, writable, _ = select.select(maybeReadable, maybeWritable, [], 0)
>>> readable == [server]
True
有趣的是,writable列表再次包含了我们的client和server插座:
>>> writable == maybeWritable and writable == [client, server]
True
如果我们再次调用select,我们的server插座将再次位于readable,我们的client和server插座将再次位于writable。原因很简单:只要数据保留在套接字的读缓冲区中,它就会连续生成一个可读事件,只要套接字的写缓冲区中还有空间,它就会生成一个可写事件。我们可以通过recv调用发送到server的数据client并再次调用select来确认新事件:
>>> server.recv(1024) == b'xyz'
True
>>> readable, writable, _ = select.select(maybeReadable, maybeWritable, [], 0)
>>> readable
[]
>>> writable == maybeWritable and writable == [client, server]
True
清空server的读缓冲区导致它停止生成可读事件,而client和server继续生成可写事件,因为它们的写缓冲区还有空间。
带有select的事件循环
我们现在知道了select如何复用套接字:
-
不同的套接字生成可读或可写的事件,以指示事件驱动的程序应该接受传入的数据或连接,或者写入传出的数据。
-
select通过监视套接字的可读或可写事件来复用套接字,暂停程序,直到至少生成一个事件或可选的超时时间已过。 -
套接字继续生成可读和可写事件,直到导致这些事件的环境发生变化:具有可读数据的套接字发出可读事件,直到其读缓冲区被清空;侦听套接字发出可读事件,直到所有传入连接都被接受;并且可写套接字发出可写事件,直到其写缓冲区被填满。
有了这些知识,我们可以围绕select勾画一个事件循环:
import select
class Reactor(object):
def __init__ (self):
self._readers = {}
self._writers = {}
def addReader(self, readable, handler):
self._readers[readable] = handler
def addWriter(self, writable, handler):
self._writers[writable] = handler
def removeReader(self, readable):
self._readers.pop(readable,None)
def removeWriter(self, writable):
self._writers.pop(writable,None)
def run(self):
while self._readers or self._writers:
r, w, _ = select.select(list(self._readers), list(self._writers), [])
for readable in r:
self._readersreadable
for writable in w:
if writable in self._writers:
self._writerswritable
我们称我们的事件循环为反应器,因为它对套接字事件做出反应。我们可以请求我们的Reactor用addReader调用套接字上的可读事件处理程序,用addWriter调用可写事件处理程序。事件处理程序接受两个参数:反应器本身和生成事件的套接字。
run方法中的循环将我们的套接字与select进行多路复用,然后在产生读事件的套接字和产生写事件的套接字之间解复用结果。每个可读套接字的事件处理程序首先运行。然后,事件循环在运行其事件处理程序之前,检查每个可写套接字是否仍注册为编写器。这种检查是必要的,因为关闭的连接表示为读取事件,所以之前立即运行的读取处理程序可能会从读取器和写入器中移除关闭的套接字。当它的可写事件处理程序运行时,关闭的套接字将从_writers字典中移除。
事件驱动的客户端和服务器
这个简单的事件循环足以实现一个不断向服务器写入数据的客户端。我们将从事件处理程序开始:
def accept(reactor, listener):
server, _ = listener.accept()
reactor.addReader(server, read)
def read(reactor, sock):
data = sock.recv(1024)
if data:
print("Server received", len(data),"bytes.")
else:
sock.close()
print("Server closed.")
reactor.removeReader(sock)
DATA=[b"*", b"*"]
def write(reactor, sock):
sock.sendall(b"".join(DATA))
print("Client wrote", len(DATA)," bytes.")
DATA.extend(DATA)
accept函数通过接受传入连接并请求反应器监控可读事件来处理侦听套接字上的可读事件。这些由read函数处理。
read函数通过尝试从套接字的接收缓冲区接收固定数量的数据来处理套接字上的可读事件。任何接收到的数据的长度都会被打印出来——记住,传递给recv的数据量代表返回的字节数的上限。如果在已经生成可读事件的套接字上没有接收到数据,那么连接的另一端已经关闭了它的套接字,并且read函数通过关闭它的套接字端并将其从由反应器监视可读事件的套接字集中删除来做出响应。关闭套接字释放其操作系统资源,同时将其从反应器中移除确保了select多路复用器不会试图监控永远不会再次活动的套接字。
write函数将一系列星号(*)写入生成写事件的套接字。每次成功写入后,数据量都会翻倍。这模拟了真实网络应用的行为,这些应用不会始终如一地向一个连接写入相同数量的数据。考虑一个 web 浏览器:一些传出的请求包含用户键入的少量表单数据,而另一些请求将一个大文件上传到远程服务器。
注意,这些是模块级函数,而不是我们的Reactor类中的方法。相反,通过将它们注册为读取器或写入器,它们与反应器相关联,因为 TCP 套接字只是套接字的一种,我们必须处理它们的事件的方式不同于我们处理其他套接字事件的方式。然而,select的工作方式是一样的,不管它被赋予什么样的套接字,所以在它返回的套接字列表上运行事件处理程序的逻辑应该被Reactor类封装。稍后我们将会看到封装和它所隐含的接口对事件驱动程序有多重要。
我们现在可以建立一个listener和一个client,并允许事件循环驱动连接的接受和从client到服务器套接字的数据传输。
import socket
listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listener.bind(('127.0.0.1',0))
listener.listen(1)
client = socket.create_connection(listener.getsockname())
loop = Reactor()
loop.addWriter(client, write)
loop.addReader(listener, accept)
loop.run()
运行该程序会显示成功和失败:
Client wrote 2 bytes.
Server received 2 bytes.
Client wrote 4 bytes.
Server received 4 bytes.
Client wrote 8 bytes.
Server received 8 bytes.
...
Client wrote 524288 bytes.
Server received 1024 bytes.
Client wrote 1048576 bytes.
Server received 1024 bytes.
^CTraceback (most recent call last):
File "example.py", line 53, in <module>
loop.run()
File "example.py", line 25, in run
writeHandler(self, writable)
File "example.py", line 33, in write
sock.sendall(b"".join(DATA))
KeyboardInterrupt
成功是显而易见的:数据从客户机套接字传递到服务器。这个行为遵循由accept、read和write事件处理程序设计的路径。正如所料,客户机首先向服务器发送两个字节的b'*',服务器依次接收这两个字节。
客户机和服务器的同时性展示了事件驱动编程的威力。我们的 GUI 应用可以响应来自两个不同按钮的事件,而这个小型网络服务器现在可以响应来自客户端或服务器的事件,允许我们在一个进程中同时处理两者。select的多路复用能力在我们的程序事件循环中提供了一个单点,在这里它可以响应任何一个事件。
失败也是显而易见的:在一定次数的重复之后,我们的程序会冻结,直到它被键盘中断。这个失败的线索存在于我们程序的输出中;过了一会儿,客户机发送的数据量是服务器接收的数据量的许多倍,KeyboardInterrupt的回溯直接导致我们的write处理程序的sock.sendall调用。
我们的客户机使我们的服务器不堪重负,结果是客户机试图发送的大部分数据都留在它的套接字的发送缓冲区中。当在发送缓冲区中没有剩余空间的套接字上调用时,sendall的默认行为是暂停或阻塞程序。现在,如果sendall阻塞了而没有阻塞,并且我们的事件循环被允许运行,那么套接字就不会以可写的形式出现,阻塞的sendall调用也不会运行;然而,我们不能保证一个给定的发送调用会写足够的内容来填满一个套接字的发送缓冲区,这样sendall就不会阻塞,写处理程序会运行到完成,而select会阻止进一步的写操作,直到缓冲区耗尽。网络的本质是我们只有在这样的问题发生后才知道它。
到目前为止,我们报道的所有事件都促使我们的程序做一些事情。它们都不能促使它停止做某事。我们需要一种新的活动。
非阻塞输入输出
知道何时停止
默认情况下,套接字会阻止一个程序开始一项操作,直到远端执行某项操作,该操作才能完成。在这种情况下,我们可以通过请求操作系统使其非阻塞来使套接字发出一个事件。
让我们回到交互式 Python 会话,再次构建一个client和server套接字之间的连接。这一次,我们将使客户端成为非阻塞的,并尝试向其写入无限的数据流。
>>> import socket
>>> listener = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
>>> listener.bind(( '127.0.0.1',0))
>>> listener.listen(1)
>>> client=socket.create_connection(listener.getsockname())
>>> server, _ = listener.accept()
>>> client.setblocking(False)
>>> while True: client.sendall(b"*"*1024)
...
Traceback (most recent call last):
File"<stdin>", line1, in <module>
BlockingIOError: [Errno11] Resource temporarily unavailable
我们再次填充了client的发送缓冲区,但是sendall没有暂停进程,而是引发了一个异常。Python 2 和 3 中的异常类型有所不同;在这里,我们展示了 Python 3 的BlockingIOError,而在 Python 2 中,它将是更一般的socket.error。在 Python 的两个版本中,异常的errno属性将被设置为errno.EAGAIN:
>>> import errno, socket
>>> try:
... while True: client.sendall(b"*"*1024)
... except socket.error as e:
... print(e.errno == errno.EAGAIN)
True
该异常表示操作系统生成的事件,指示我们应该停止写入。这几乎足以修复我们的客户端和服务器。
跟踪状态
然而,处理这个异常需要我们回答一个新问题:我们试图写入套接字发送缓冲区的数据有多少?不回答这个问题,我们就无法知道我们实际上发送了什么数据,也不知道我们无法用非阻塞套接字编写正确的程序。例如,网络浏览器必须跟踪它上传了多少文件,否则就有可能在传输过程中破坏文件内容。
在生成成为我们的异常的EAGAIN事件之前,client.sendall可以在它的写缓冲区中放置任意数量的字节。我们必须从套接字对象的sendall方法切换到send方法,该方法返回写入套接字发送缓冲区的数据量。我们可以用我们的server插座来证明这一点:
>>> server.setblocking(False)
>>> try:
... while True: print(server.send(b"*" * 1024))
... except socket.error as e:
... print("Terminated with EAGAIN:", e.errno == errno.EAGAIN)
1024
1024
...
1024
952
Terminated with EAGAIN:True
我们将server标记为非阻塞,这样当它的发送缓冲区已满时,它会生成一个EAGAIN事件。然后while循环调用server.send。返回 1024 的调用已经将所有提供的字节写入套接字的发送缓冲区。最终套接字的写缓冲区被填满,一个代表EAGAIN事件的异常终止了循环。然而,循环终止前最后一次成功的send调用返回 952,这里 send 简单地丢弃了剩余的 72 个字节。这就是所谓的短写。阻塞套接字也会发生这种情况!因为当发送缓冲区中没有可用空间时它们会阻塞,而不是引发异常,sendall可以并且确实包含一个循环,该循环检查底层send调用的返回值并重新调用它,直到所有数据都被发送完。
在这种情况下,套接字的发送缓冲区不是 1024 的倍数,因此我们无法在到达EAGAIN之前将偶数个send调用的数据放入。然而,在现实世界中,套接字的发送缓冲区会根据网络中的条件改变大小,应用会通过连接发送不同数量的数据。使用非阻塞 I/O 的程序,比如我们假设的 web 浏览器,必须定期处理这样的短写。
我们可以使用send的返回值来确保我们将所有数据写入连接。我们维护自己的缓冲区,其中包含我们想要写入的数据。每次select为该套接字发出一个可写事件时,我们试图send当前在缓冲区中的数据;如果send调用在没有引发EAGAIN的情况下完成,我们会记录返回的数量,并从我们的缓冲区的开头删除该数量的字节,因为send从它传递的字节序列的开头将数据写入发送缓冲区。另一方面,如果send引发了一个EAGAIN异常,表明发送缓冲区已满,无法接受更多数据,我们就让缓冲区保持原样。我们以这种方式进行,直到我们自己的缓冲区为空,此时我们知道所有的数据都已经放在了套接字的发送缓冲区中。之后,由操作系统将它发送到连接的接收端。
我们现在可以通过将它的write函数分成一个初始化数据写入的函数和一个在send之上管理缓冲区的对象来修复我们简单的客户机-服务器示例:
import errno
import socket
class BuffersWrites(object):
def __init__ (self, dataToWrite, onCompletion):
self._buffer = dataToWrite
self._onCompletion = onCompletion
def bufferingWrite(self, reactor, sock):
if self._buffer:
try:
written = sock.send(self._buffer)
except socket.error as e:
if e.errno != errno.EAGAIN:
raise
return
else:
print("Wrote", written,"bytes")
self._buffer = self._buffer[written:]
if not self._buffer:
reactor.removeWriter(sock)
self._onCompletion(reactor, sock)
DATA=[b"*", b"*"]
def write(reactor, sock):
writer = BuffersWrites(b"".join(DATA), onCompletion=write)
reactor.addWriter(sock, writer.bufferingWrite)
print("Client buffering", len(DATA),"bytes to write.")
DATA.extend(DATA)
BuffersWrites的初始化器的第一个参数是它将写入的字节,它将其用作缓冲区的初始值,而它的第二个参数onCompletion是一个可调用的对象。顾名思义,当提供的数据被完全写入时,onCompletion将被调用。
bufferingWrite方法的签名是我们对适合传递Reactor.addWriter的可写事件处理程序的期望。如上所述,它试图将任何缓冲的数据send到它传递的套接字,保存返回的指示写入量的数字。如果send引发一个EAGAIN异常,bufferingWrite抑制它并返回;否则,它会传播异常。在这两种情况下。self._buffer保持不变。
如果send成功,从self._buffer的开始处切掉与写入量相等的字节数,然后bufferingWrite返回。例如,如果send调用只写了 1024 个字节中的 952 个,self_buffer将包含最后的 73 个字节。
最后,如果缓冲区是空的,那么所有请求的数据都已经被写入,没有工作留给BuffersWrites实例去做。它请求反应器停止监视其套接字的可写事件,然后调用onCompletion,因为提供给它的数据已经被完全写入。注意,这个检查发生在独立于第一个if self._buffer语句的if语句中。前面的代码可能已经运行并清空了缓冲区;如果最终代码在附加到if self._buffer语句的else块中,它将不会运行,直到下一次反应器在这个套接字上检测到可写事件。为了简化资源管理,我们在这个方法中执行检查。
除了现在它通过它的bufferingWrite方法将数据委托给BuffersWrites之外,write函数看起来与我们之前的版本相似。最值得注意的是,write将本身传递给BuffersWrites作为onCompletion调用。这通过间接递归创建了与先前版本相同的循环效果。write从不直接调用自己,而是将自己传递给我们的反应器最终调用的对象。这种间接方式允许此序列继续进行,而不会溢出调用堆栈。
通过这些修改,我们的客户机-服务器程序不再阻塞。相反,它失败的另一个原因是:最终,DATA变得太大,不适合你的计算机的可用内存!下面是作者电脑中的一个例子:
Client buffering 2 bytes to write.
Wrote 2 bytes
Client buffering 4 bytes to write.
Server received 2 bytes.
Wrote 4 bytes
...
Client buffering 2097152 bytes to write.
Server received 1024 bytes.
Wrote 1439354 bytes
Server received 1024 bytes.
Server received 1024 bytes.
....
Wrote 657798 bytes
Server received 1024 bytes.
Server received 1024 bytes.
....
Client buffering 268435456 bytes to write.
Traceback (most recent call last):
File "example.py", line 76, in <module>
loop.run()
File "example.py", line 23, in run
writeHandler(self, writable)
File "example.py", line 57, in bufferingWrite
self._onCompletion(reactor, sock)
File "example.py", line 64, in write
DATA.extend(DATA)
MemoryError
状态使程序变得复杂
尽管存在这个问题,但我们已经成功编写了一个事件驱动的网络程序,它使用非阻塞 IO 来控制套接字写入。然而,代码是混乱的:从write到BuffersWrites,然后是反应器,最后回到write的间接性模糊了出站数据的逻辑流,很明显,实现任何比简单的星号流更复杂的东西都将涉及扩展特设类和接口,超出它们的断点。例如,我们如何处理MemoryError?我们的方法无法扩展到实际应用中。
管理传输和协议的复杂性
使用非阻塞 I/O 编程无疑是复杂的。UNIX 权威 W. Richard Stevens 在其开创性的 Unix 网络编程系列的第一卷中写下了以下内容:
- 但是,考虑到结果代码的复杂性,使用非阻塞 I/O 编写应用值得吗?答案是否定的。
( UNIX 网络编程,第 1 卷。第二版。第 446 页
我们代码的复杂性似乎证明史蒂文斯是正确的。然而,正确的抽象可以将复杂性封装在一个可管理的接口中。我们的例子已经有了可重用的代码:任何写入套接字的新代码单元都需要使用核心逻辑BuffersWrites。我们已经封装了写非阻塞套接字的复杂性。基于这种认识,我们可以区分两个概念领域:
-
传输 :
BuffersWrites管理将输出写入非阻塞套接字的过程,而不考虑该输出的内容。它可以发送照片,或者音乐,或者任何我们可以想象的东西,只要它可以用字节来表达。BuffersWrites是一个传输,因为它是字节的一种传输方式。传输封装了从套接字读取数据以及接受新连接的过程。它代表我们程序中动作的原因,是我们程序自身动作的接收者。 -
协议:我们的示例程序用一个简单的算法生成数据,并且仅仅计算它接收到的数据。更复杂的程序可能会生成网页或将语音电话处理成文本。只要它们能接收和发送字节,它们就能与我们所描述的传输协同工作。它们还可以控制传输行为,比如在收到无效数据时关闭活动连接。电信领域描述了像这样的规则,这些规则定义了如何通过 ?? 协议 ?? 来交换数据。一个协议,然后定义如何产生和处理输入和输出。它封装了我们程序的效果。
反应器:使用传输
我们从改变我们的Reactor开始,在运输方面工作:
import select
class Reactor(object):
def __init__ (self):
self._readers = set()
self._writers = set()
def addReader(self, transport):
self._readers.add(transport)
def addWriter(self, transport):
self._writers.add(transport)
def removeReader(self, readable):
self._readers.discard(readable)
def removeWriter(self, writable):
self._writers.discard(writable)
def run(self):
while self._readers or self._writers:
r, w, _ = select.select(self._readers,self._writers, [])
for readable in r:
readable.doRead()
for writable in w:
if writable in self._writers:
writable.doWrite()
我们的可读和可写事件处理程序以前是函数,现在是传输对象上的方法:doRead和doWrite。此外,反应堆不再跟踪插座-它直接select的运输。从反应堆的角度来看,传输接口包括:
-
doRead, -
doWrite, -
使传输的状态对
select可见的东西:一个fileno()方法,返回一个数字,select理解为对套接字的引用。
传输:使用协议
接下来,我们将通过回到我们的read和write函数来考虑一个协议实现。read职能有两个职责:
-
计算套接字上接收的字节数。
-
响应关闭的连接。
write 函数只有一个职责:对要写入的数据进行排队。
由此我们可以勾画出一个Protocol界面的初稿:
class Protocol(object):
def makeConnection(self, transport):
...
def dataReceived(self, data):
...
def connectionLost(self, exceptionOrNone):
...
我们将read的两个职责分成了两个方法:dataReceived和connectionLost。前者的签名是不言自明的,而后者接收一个参数:如果连接因为那个异常而被关闭(例如,因为ECONNRESET),则接收一个异常对象;如果连接在没有异常的情况下被关闭(例如,因为被动关闭而具有空读取),则接收None。注意,我们的协议接口缺少一个write方法。这是因为写入数据,包括传输字节,属于传输的范畴。因此,Protocol实例必须能够访问代表底层网络连接的传输,并且它将有一个write方法。两者之间的关联通过makeConnection发生,它接受一个传输作为它的参数。
为什么不将传输作为参数传递给Protocol的初始化器呢?一个单独的方法可能看起来笨拙,但它给我们提供了更大的灵活性;例如,您可以想象这个方法将如何允许我们引入Protocol缓存。此外,我们将看到,因为传输调用协议的dataReceived和connectionLost方法,所以它也必须与协议相关联。如果我们的Transport和Protocol类都需要它们的初始化式中的对等体,那么我们将会有一个循环关系来阻止它们被实例化。我们选择让我们的Protocol通过一个单独的方法接受它的传输来打破这个循环,因为它提供了灵活性。
用协议和传输打乒乓
这足以让我们编写一个更复杂的协议来实现这个新接口。我们之前的客户机-服务器示例只是让客户机向服务器发送越来越多的字节序列;我们可以增加这个,这样两个字节来回发送,直到一个可选的最大值,超过最大值的发送方关闭连接。
class PingPongProtocol(object):
def __init__ (self, identity, maximum=None):
self._identity = identity
self._received = 0
self._maximum = maximum
def makeConnection(self, transport):
self.transport = transport
self.transport.write(b'*')
def dataReceived(self, data):
self._received += len(data)
if self._maximum is not None and self._received >= self._maximum:
print(self._identity,"is closing the connection")
self.transport.loseConnection()
else:
self.transport.write(b'*')
print(self._identity,"wrote a byte")
def connectionLost(self, exceptionOrNone):
print(self._identity,"lost the connection:", exceptionOrNone)
初始化器接受一个用于标识协议实例的identity字符串,以及在终止连接之前可选的最大数据量。makeConnection将PingPongProtocol与其传输相关联,并通过发送单个字节开始交换。dataReceived记录接收到的数据量;如果总量超过可选的最大值,它告诉传输失去连接,或者等同于断开连接。否则,它通过发回一个字节来继续交换。最后,connectionLost在连接的协议方关闭时打印一条消息。
描述了一组行为,其复杂性远远超出了我们之前在非阻塞客户端-服务器应用方面的尝试。同时,它的实现反映了它之前的散文描述,而没有陷入非阻塞 I/O 的细节。我们已经能够增加我们的应用的复杂性,同时降低其独特的 I/O 管理的复杂性。我们将回头探讨这一问题的结果,但可以说,缩小我们的关注范围允许我们消除程序特定领域的复杂性。
在写入Transport之前,我们不能使用PingPongProtocol。然而,我们可以写一份Transport界面的初稿:
class Transport(object):
def __init__ (self, sock, protocol):
...
def doRead(self):
...
def doWrite(self):
...
def fileno(self):
...
def write(self):
...
def loseConnection(self):
...
Transport初始化器的第一个参数是实例包装的套接字。这加强了Transport'对现在Reactor所依赖的套接字的封装。第二个参数是协议,当新数据可用时将调用其dataReceived,当连接关闭时将调用其connectionLost。doRead和doWrite方法与我们上面列举的反应器端传输接口相匹配。新方法fileno也是这个接口的一部分;一个正确实现了fileno方法的对象可以传递给select。我们将把对我们的Transport的fileno的调用代理到它所包装的套接字,使得从select的角度来看这两者无法区分。
write方法提供了接口,我们的Protocol依赖这个接口发送输出数据。我们还添加了loseConnection,一个新的Protocol端 API,它启动套接字的关闭,并代表我们的被动关闭connectionLost方法的主动关闭端。
我们可以通过在我们的read函数中吸收BuffersWrites和套接字处理来实现这个接口:
import errno
class Transport(object):
def __init__ (self, reactor, sock, protocol):
self._reactor = reactor
self._socket = sock
self._protocol = protocol
self._buffer = b "
self._onCompletion = lambda:None
def doWrite(self):
if self._buffer:
try:
written = self._socket.send(self._buffer)
except socket.error as e:
if e.errno != errno.EAGAIN:
self._tearDown(e)
return
else:
print("Wrote", written,"bytes")
self._buffer = self._buffer[written:]
if not self._buffer:
self._reactor.removeWriter(self)
self._onCompletion()
def doRead(self):
data=self._socket.recv(1024)
if data:
self._protocol.dataReceived(data)
else:
self._tearDown(None)
def fileno(self):
return self._socket.fileno()
def write(self, data):
self._buffer += data
self._reactor.addWriter(self)
self.doWrite()
def loseConnection(self):
if self._buffer:
def complete():
self.tearDown(None)
self._onCompletion = complete
else:
self._tearDown(None)
def _tearDown(self, exceptionOrNone):
self._reactor.removeWriter(self)
self._reactor.removeReader(self)
self._socket.close()
self._protocol.connectionLost(exceptionOrNone)
def activate(self):
self._socket.setblocking(False)
self._protocol.makeConnection(self)
self._reactor.addReader(self)
self._reactor.addWriter(self)
doRead和doWrite反映了先前示例中的插座操作read和write功能以及BuffersWrites。doRead还将任何接收到的数据代理到协议的dataReceived方法,或者在接收到空读取时调用其connectionLost方法。最后,fileno通过使Transport s select能够完成Reactor要求的接口。
write方法像以前一样缓冲写操作,但是它不是将写操作委托给一个单独的类,而是调用它的兄弟doWrite方法将缓冲区刷新到套接字。如果缓冲区为空,对loseConnection的调用通过以下方式断开连接:
-
从反应器中移除运输工具;
-
关闭底层套接字以将其资源释放回操作系统;
-
向协议的
connectionLost发送None以表明由于被动关闭而导致连接丢失。
如果缓冲区不为空,则有数据要写入,因此loseConnection用一个闭包覆盖_onCompletion,该闭包按照与上述相同的过程断开连接。与BuffersWrites一样,Transport._onCompletion只有当我们的写缓冲区中的所有字节都被刷新到底层套接字时才会被调用。loseConnection对_onCompletion的使用确保了底层连接保持打开,直到所有数据都被写入。_onCompletion的默认值在Transport的初始化器中被设置为一个 lambda,没有任何效果。这确保了对write的多个调用可以重用底层连接。这些write和loseConnection的实现一起实现了Protocol所需的传输接口。
最后,activate通过以下方式激活传输:
-
为非阻塞 I/O 准备包装的套接字;
-
通过
Protocol.makeConnection将Transport实例传递给它的协议; -
最后向反应器注册传输。
这通过包装连接生命周期的开始完成了Transport对其套接字的封装,其中结束已经被loseConnection封装。
在Protocol允许我们通过PingPongProtocol扩展我们的关注点并将行为添加到我们的应用中的地方,Transport已经围绕套接字的输入输出生命周期缩小了它的范围。reactor——我们的事件循环——从它们的起始套接字检测和调度事件,而协议包含我们需要的事件处理程序。Transport通过将套接字事件转换为协议方法调用并在这些方法调用之间实施控制流来进行协调;例如,它确保一个协议的makeConnection在它生命的开始被调用,而loseConnection在结束时被调用。这是对我们的特定客户机-服务器示例的另一个改进;我们将套接字的控制流完全集中在Transport内,而不是分散在不相关的函数和对象上。
具有协议和传输的客户端和服务器
我们可以通过定义一个子类型Listener来展示Transport的通用性,该子类型接受传入的连接并将它们与一个唯一的PingPongProtocol实例相关联:
class Listener(Transport):
def activate(self):
self._reactor.addReader(self)
def doRead(self):
server, _ = self._socket.accept()
protocol = PingPongProtocol("Server")
Transport(self._reactor, server, protocol).activate()
侦听套接字不发出可写事件,所以我们覆盖了activate来只添加传输作为读取器。我们可读的事件处理程序doRead,必须接受一个新的客户端连接和协议,然后用一个激活的Transport将两者绑定在一起。
现在已经为由协议和传输提供支持的客户机-服务器示例做好了准备:
listenerSock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
listenerSock.bind(('127.0.0.1',0))
listenerSock.listen(1)
clientSock = socket.create_connection(listenerSock.getsockname())
loop = Reactor()
Listener(loop, listenerSock, None).activate()
Transport(loop, clientSock, PingPongProtocol("Client", maximum=100)).activate()
loop.run()
两者将交换单个字节,直到客户端收到其最大值 100,之后客户端关闭连接:
Server wrote a byte
Client wrote a byte
Wrote 1 bytes
Server wrote a byte
Wrote 1 bytes
Client wrote a byte
Wrote 1 bytes
Server wrote a byte
Wrote 1 bytes
Client wrote a byte
Wrote 1 bytes
Server wrote a byte
Server wrote a byte
Client is closing the connection
Client lost the connection: None
Server lost the connection: None
Twisted 和反应器、协议和传输
我们已经走了很长一段路:从select开始,我们围绕一个事件循环及其处理程序开发了一组接口,它们清晰地划分了职责。我们的Reactor驱动我们的程序,Transport将套接字事件分派给在Protocol上定义的应用级处理程序。
我们的反应堆、运输机和协议显然是玩具。例如,socket.create_connection阻塞,我们还没有调查任何非阻塞的替代方案。事实上,create_connection暗示的底层 DNS 解析可能会自己阻塞!
然而,作为概念,它们已经可以认真使用了。反应器、传输和协议是 Twisted 事件驱动架构的基础。正如我们所看到的,它们的体系结构反过来又依赖于 I/O 多路复用和无阻塞的现实,从而使 Twisted 能够高效地运行。
然而,在我们探索 Twisted 本身之前,我们将把我们的例子作为一个整体来考虑,以评估事件驱动编程的优点和缺点。
事件驱动编程的价值
W.Richard Stevens 关于非阻塞 I/O 复杂性的告诫是对我们所探索的事件驱动编程范例的重要批评。然而,这不是唯一的缺点:我们的事件驱动范例在高 CPU 负载下表现不佳。
编写指数增长的字节序列的客户机-服务器示例自然会消耗大量内存,但也会消耗大量 CPU。原因是其缓冲区管理的幼稚:套接字根本不能接受大于特定大小的数据块。每次我们调用send的时候,调用send会把它复制到内核控制的内存位置。然后写入数据的一部分,然后我们切掉缓冲区的前端;因为bytes在 Python 中是不可变的,这意味着另一个副本。如果我们试图发送 N 个字节,我们将复制缓冲区一次,然后两次,一次又一次,直到 N。因为每次复制都意味着遍历缓冲区,所以这个过程的时间复杂度为 O( n 2 )。
Twisted 自己的缓冲机制性能更好,但代价是复杂性超出了事件驱动编程的可读介绍。然而,并不是所有计算要求高的任务都那么容易改进:蒙特卡罗模拟必须重复进行统计分析和随机采样;比较排序算法必须比较序列中的每一对元素;等等。
我们的事件驱动程序都执行多种逻辑行为——我们有一个客户端和一个服务器在一个进程中通信。这种通信同时发生*:在暂停并允许服务器取得少量进展之前,连接的客户端取得少量进展。客户端和服务器在任何时候都不是并行运行的*,就像它们在独立的 Python 解释器中一样,也许是在通过网络连接的独立计算机上。当我们简单的缓冲区管理执行一个冗长的拷贝时,在这个过程完成之前不会有任何进展,而如果客户机和服务器运行在不同的计算机上,服务器可以接受新的连接,而客户机则费力地来回移动字节。如果我们在我们的过程中运行一个计算要求很高的算法,我们的反应器不能调用select来发现新的事件以对其做出反应,直到这个算法完成之后。**
**因此,事件驱动编程不适合计算要求高的任务。幸运的是,许多任务对输入和输出的要求比计算更高。网络服务器就是一个典型的例子;一个聊天服务器可能有成千上万的用户,但是在任何时候只有一小部分是活跃的(而且通常在你寻求帮助的时候是不活跃的!).因此,事件驱动编程仍然是网络中一个强大的范例。
事件驱动编程有一个特别的优势,可以弥补这个缺点:它强调原因和效果。一个事件的产生代表一个原因,而那个事件的处理程序代表预期的结果。
我们在Transport和Protocol中对这种划分进行了整理:传输表示动作的原因——一些输入或套接字输出——而协议封装了效果。我们的PingPongProtocol通过一个清晰的接口与其传输进行交互,该接口将处理程序暴露给更高级别的事件——原因——比如传入字节的到达或连接的结束。然后,它从这些原因中产生效果,这又可能导致新的原因,例如将数据写入传输。两者之间的区别是由各自的接口决定的。
这意味着我们可以用一个传输替换另一个,并通过调用表示预期效果的方法来模拟我们的协议的执行。这将我们的客户机-服务器的核心变成了一个可测试的代码单元。
考虑一个构建在BytesIO上的传输实现,它只实现了Transport接口的Protocol端:
import io
class BytesTransport(object):
def __init__ (self, protocol):
self.protocol = protocol
self.output = io.BytesIO()
def write(self, data):
self.output.write(data)
def loseConnection(self):
self.output.close()
self.protocol.connectionLost(None)
我们可以用它来为我们的PingPongProtocol编写一个单元测试套件:
import unittest
class PingPongProtocolTests(unittest.TestCase):
def setUp(self):
self.maximum = 100
self.protocol = PingPongProtocol("client", maximum=self.maximum)
self.transport = BytesTransport(self.protocol)
self.protocol.makeConnection(self.transport)
def test_firstByteWritten(self):
self.assertEqual(len(self.transport.output.getvalue()), 1)
def test_byteWrittenForByte(self):
self.protocol.dataReceived(b"*")
self.assertEqual(len(self.transport.output.getvalue()), 2)
def test_receivingMaximumLosesConnection(self):
self.protocol.dataReceived(b"*" * self.maximum)
self.assertTrue(self.transport.output.closed)
这个测试断言了我们为PingPongProtocol设置的需求,而没有设置任何套接字或执行任何实际的 I/O。我们可以测试我们程序的效应,而没有具体的原因。相反,我们通过用字节调用我们的协议实例的dataReceived方法来模拟可读事件,而协议通过在我们的字节传输上调用write来生成可写事件,并通过调用loseConnection来生成关闭请求。
Twisted 努力分开原因和结果。如前所述,最明显的好处是可测试性。为事件驱动的 Twisted 程序编写全面的测试更容易,因为协议和传输之间有基本的区别。事实上,Twisted 将责任之间的这种区别作为设计中的一门深刻课程,从而产生了其庞大且有时晦涩难懂的词汇。让这么多东西明确地分离对象需要大量的名称。
我们现在准备用 Twisted 编写一个事件驱动的程序。我们将会遇到我们在玩具例子中遇到的同样的设计问题,而编写这些玩具的经验将会阐明 Twisted 为解决这些问题所提供的策略。
Twisted 的现实世界
我们从实现我们的PingPongProtocol客户机和服务器开始探索 Twisted:
from twisted.internet import protocol, reactor
class PingPongProtocol(protocol.Protocol):
def __init__ (self):
self._received = 0
def connectionMade(self):
self.transport.write(b'*')
def dataReceived(self, data):
self._received += len(data)
if self.factory._maximum is not None and self._received >= self.factory._maximum:
print(self.factory._identity, "is closing the connection")
self.transport.loseConnection()
else:
self.transport.write(b'*')
print(self.factory._identity,"wrote a byte")
def connectionLost(self, exceptionOrNone):
print(self.factory._identity,"lost the connection:", exceptionOrNone)
class PingPongServerFactory(protocol.Factory):
protocol = PingPongProtocol
_identity = "Server"
def __init__ (self, maximum=None):
self._maximum = maximum
class PingPongClientFactory(protocol.ClientFactory):
protocol = PingPongProtocol
_identity = "Client"
def __init__ (self, maximum=None):
self._maximum = maximum
listener=reactor.listenTCP(port=0,
factory=PingPongServerFactory(),
interface='127.0.0.1')
address = listener.getHost()
reactor.connectTCP(host=address.host,
port=address.port,
factory=PingPongClientFactory(maximum=100))
reactor.run()
我们的PingPongProtocol类与我们的玩具实现几乎相同。有三个变化:
-
我们继承自
twisted.internet.protocol.Protocol。这个类提供了重要功能的有用的默认实现。在最初设计 Twisted 的传输和协议时,继承是一种流行的代码重用方法。围绕公共和私有 API 的困难以及关注点的分离已经正确地导致了它的受欢迎程度的下降。对继承缺点的完整讨论超出了本章的范围,但是我们不建议编写依赖继承的新 API! -
我们用
connectionMade替换了makeConnection,这是一个事件处理程序,当底层连接就绪时,它会 Twisted 调用。Twisted 的Protocol类为我们实现了makeConnection,并留下了connectionMade作为我们可以填充的存根。实际上,我们不希望改变传输与协议的关联方式,但是我们经常希望代码在连接就绪后立即运行。这个处理程序提供了这样做的方法。 -
最大字节数和协议标识不再是实例变量;相反,它们是新的
factory实例变量的属性。
协议工厂协调协议的创建及其与传输的绑定。这是我们第一个 Twisted 将责任本地化到类的例子。协议工厂有两种基本类型:服务器和客户端。顾名思义,一个管理服务器端协议的创建,而另一个管理客户端协议的创建。两者都通过不带参数地调用它们的protocol属性来创建协议实例。这就是为什么PingPongProtocol的初始化器不接受参数。
PingPongServerFactory子类化twisted.internet.protocol.Factory并将它的_identity属性设置为"Server."它的初始化器接受反应器作为参数和可选的最大值。然后,它依赖其超类的实现来创建其协议的实例——在类级别设置为PingPongProtocol——并将它们与自身相关联。这就是为什么PingPongProtocol实例有一个factory属性:Factory默认为我们创建。
PingPongClientFactory子类化twisted.internet.protocol.ClientFactory,并将其_identity属性设置为"Client.",其他方面与PingPongServerFactory相同。
工厂为存储所有协议实例共享的状态提供了一个方便的地方。因为协议实例对于连接来说是唯一的,所以当连接存在时,它们就不再存在,并且不能自己保持状态。因此,像我们的最大允许值和我们的协议客户机或服务器标识字符串这样的设置转移到它们的工厂遵循 Twisted 中的一个常见模式。
reactor公开了listenTCP和connectTCP方法,它们将工厂与服务器和客户端连接相关联。listenTCP返回一个Port对象,其getHost方法类似于socket.getsockname。然而,它不是返回一个元组,而是返回一个twisted.internet.address.IPv4Address的实例,该实例又具有方便的host和port属性。
最后,我们通过调用run来启动reactor,就像我们对玩具实现所做的那样。迎接我们的是类似于我们的玩具实现打印的输出:
Client wrote a byte
Server wrote a byte
Client wrote a byte
Server wrote a byte
Client wrote a byte
Server wrote a byte
Client wrote a byte
Server wrote a byte
Client is closing the connection
Client lost the connection: [Failure instance: ...: Connection was closed cleanly.
]
Server lost the connection: [Failure instance: ...: Connection was closed cleanly.
]
抛开传递给connectionLost的Failure对象(我们将在 Twisted 中讨论异步编程)不谈,这个输出似乎证明了我们的新实现的行为与旧实现的行为相匹配。
然而,通过修改我们的协议测试,我们可以做得比比较输出更好:
from twisted.trial import unittest
from twisted.test.proto_helpers import StringTransportWithDisconnection, MemoryReactor
class PingPongProtocolTests(unittest.SynchronousTestCase):
def setUp(self):
self.maximum = 100
self.reactor = MemoryReactor()
self.factory = PingPongClientFactory(self.reactor,self.maximum)
self.protocol = self.factory.buildProtocol(address.IPv4Address(
"TCP","localhost",1234))
self.transport = StringTransportWithDisconnection()
self.protocol.makeConnection(self.transport)
self.transport.protocol = self.protocol
def test_firstByteWritten(self):
self.assertEqual(len(self.transport.value()), 1)
def test_byteWrittenForByte(self):
self.protocol.dataReceived(b"*")
self.assertEqual(len(self.transport.value()), 2)
def test_receivingMaximumLosesConnection(self):
self.protocol.dataReceived(b"*" * self.maximum)
self.assertFalse(self.transport.connected)
Twisted 有自己的测试基础设施,我们将在异步编程的讨论中涉及到它;现在,我们可以将SynchronousTestCase视为等同于标准库的unittest.TestCase。我们的setUp方法现在构建了一个MemoryReactor赝品,它代替了我们真正的反应堆。它将其传递给PingPongClientFactory,然后通过调用从ClientFactory继承的buildProtocol方法构建一个PingPongProtocol客户端。这又需要一个地址参数,为此我们提供了另一个假参数。然后我们使用 Twisted 的内置StringTransportWithDisconnection,它的行为和接口与我们的 toy BytesTransport实现一致。Twisted 称之为StringTransport,因为在编写它的时候,所有发布的 Python 版本都有一个默认的字符串类型bytes。在 Python 3 的世界中,StringTransport已经成为一个误称,因为它仍然必须以字节为单位工作。
我们的测试方法调整到StringTransportWithDisconnection的接口:value返回写入的内容,而connected在协议调用loseConnection时变成False。
客户端和服务器的 Twisted 实现清楚地表明了 Twisted 和我们的示例代码之间的相似之处:反应器多路复用来自套接字的事件,并通过传输将它们分派给协议,然后协议可以通过它们的传输创建新的事件。
虽然这种动态形成了 Twisted 的事件驱动架构的核心,并通知其设计决策,但它是相对较低的级别。许多程序从不实现自己的Protocol子类。接下来,我们转向一种事件,它是许多 Twisted 程序中直接使用的模式和 API 的基础。
时间上的事件
到目前为止,我们看到的所有事件都源于输入,比如用户点击按钮或新数据到达套接字。程序必须经常安排动作在未来某个时间点运行,与任何输入分开。考虑一个心跳:每 30 秒左右,网络应用将向其连接写入一个字节,以确保远程终端不会因为不活动而关闭它们。
Twisted 提供了一个底层接口,通过reactor.callLater来安排未来的行动。我们通常不直接调用这个 API,但是现在将这样做来解释它是如何工作的。
from twisted.internet import reactor
reactor.callLater(1.5, print,"Hello from the past.")
reactor.run()
reactor.callLater接受数字延迟和可调用。当调用 callable 时,任何其他位置或关键字参数都会传递给它。运行该程序将不会产生任何输出,直到大约 1.5 秒后,此时Hello from the past将会出现。
reactor.callLater返回一个可以取消的DelayedCall实例:
from twisted.internet import reactor
call = reactor.callLater(1.5, print,"Hello from the past.")
call.cancel()
reactor.run()
这个程序没有输出,因为DelayedCall在反应器运行它之前就被取消了。
显然reactor.callLater发出一个事件,表明指定的时间已经过去,并运行它作为该事件的处理程序接收的可调用程序。然而,这种情况发生的机制还不太清楚。
幸运的是,实现基本上很简单,这也说明了为什么延迟只是近似值。回想一下,select接受一个可选的超时参数。当我们希望select立即告诉我们已经生成了什么事件,而不是等待新的事件时,我们用 0 作为超时来调用它。除了基于套接字的事件之外,我们现在可以使用这个超时来复用基于时间的事件:为了确保我们的DelayedCall运行,我们可以调用select,超时时间等于应该调度的下一个DelayedCall的延迟,也就是时间上最近的那个。
想象一个包含以下内容的程序:
reactor.callLater(2, functionB)
reactor.callLater(1, functionA)
reactor.callLater(3, functionC)
reactor.run()
reactor 将DelayedCall记录在一个 min-heap 中,按照它计划运行的挂钟时间排序:
def callLater(self, delay, f,*args,**kwargs):
self._pendingCalls.append((time.time()+delay, f, args, kwargs)
heapq.heapify(self._pendingCalls)
如果第一个reactor.callLater发生在时间 t ,并且每个调用不占用时间,那么在所有三个调用之后,pendingCalls将如下所示:
[
(t+1, <DelayedCall: functionA>),
(t+2, <DelayedCall: functionB>),
(t+3, <DelayedCall: functionB>),
]
向堆中添加一个元素的时间复杂度为 O(log n ),因此重复的callLater调用的总最坏情况时间复杂度为 O( n log n )。如果反应器改为排序_pendingCalls,重复的callLater调用将取 O(n)* O(nlogn)= O(n2)。
现在,在反应堆进入select,之前,它检查是否有任何未决的DelayedCalls;如果有,它提取堆的顶部元素,并使用其目标运行时间和当前时间之差作为select的超时。然后,在处理任何套接字事件之前,它从堆中弹出每个时间已过的元素并运行它,跳过取消的调用。如果没有未决的DelayCall,反应器调用select,超时None,表示没有超时。
class Reactor(object):
...
def run(self):
while self.running:
if self._pendingCalls:
targetTime, _ = self._pendingCalls[0]
delay=targetTime-time.time()
else:
targetTime = None
r, w, _ = select.select(self.readers,self.writers, [], targetTime)
now = time.time()
while self._pendingCalls and (self._pendingCalls[0][0] <= now):
targetTime, (f, args, kwargs) = heapq.heappop()
if not call.cancelled:
f(*args,**kwargs)
...
在我们的三个reactor.callLater调用中,functionA的延迟最短,因此位于pendingCalls堆的顶部。如果我们的反应器的run循环随后立即开始(即,也在时间 t ),那么delay变量将为( t + 1) - t = 1,并且select调用将在不超过一秒钟后返回。现在,time.time返回 t + 1,所以functionA的DelayedCall,从而functionA运行。然而,functionB和functionC的DelayedCall仍然留在将来,因此内部while循环结束,过程再次开始。
该实现揭示了为什么DelayedCall在延迟过后不立即运行:它们的调用取决于它们在pendingCalls堆中的位置以及前面的DelayedCall需要多长时间来完成。如果functionA运行的时间超过一秒钟,functionB就会运行得比截止时间晚。这对于延迟相同时间的DelayedCall s 来说尤其可能。
使用LoopingCall重复事件
足以实现我们的心跳。我们可以定义一个用自身调用callLater的函数,然后通过直接调用该函数一次来启动间接递归:
def f(reactor, delay)
reactor.callLater(delay, f, reactor, delay)
f(reactor,1.0)
这是可行的,但是很笨拙。在对f的初始呼叫之后,我们不能访问代表对f的下一次呼叫的DelayedCall,所以如果对方终止连接,我们不能轻易取消它。我们可以手动跟踪这些呼叫,但幸运的是,Twisted 提供了一个方便的包装器callLater,为我们处理这一切:twisted.internet.task.LoopingCall。下面是一个使用LoopingCall来实现心跳的协议:
from twisted.internet import protocol, task
class HeartbeatProtocol(protocol.Protocol):
def connectionMade(self):
self._heartbeater = task.LoopingCall(self.transport.write, b"*")
self._heartbeater.clock = self.factory._reactor
self._heartbeater.start(interval=30.0)
def connectionLost(self):
self._heartbeater.stop()
class HeartbeatProtocolFactory(protocol.Factory):
protocol = HeartbeatProtocol
def __init__ (self, reactor):
self._reactor = reactor
该协议创建了一个新的LoopingCall实例,它将在连接建立时向协议的传输写入一个星号。然后它用工厂的反应堆替换了LoopingCall的时钟;我们很快就会看到,这种间接方式有助于测试。最后,该协议以 30 秒的间隔启动LoopingCall,这样大约每 30 秒它就会用一个星号调用transport.write。LoopingCall从什么时候开始计时 30 秒?它是从 0 开始计数,在这种情况下应该立即调用它的函数,还是从 1 开始计数,在这种情况下应该等待整整 30 秒?答案取决于程序员。LoopingCall.start的第二个可选参数now决定了该函数是应该作为对start的调用的一部分被调用,还是在一个完整的间隔过去之后被调用。它默认为True,所以我们的心跳会立即向传输写一个星号。
从工厂取回反应堆使得HeartbeatProtocol和PingPongProtocol一样容易测试:
from twisted.trial import unittest
from twisted.internet import main, task
from twisted.test.proto_helpers import StringTransportWithDisconnection
class HeartbeatProtocolTests(unittest.SynchronousTestCase):
def setUp(self):
self.clock = task.Clock()
self.factory = HeartbeatProtocolFactory(self.clock)
self.protocol = self.factory.buildProtocol(address.IPv4Address(
"TCP","localhost",1234))
self.transport = StringTransportWithDisconnection()
self.protocol.makeConnection(self.transport)
self.transport.protocol = self.protocol
def test_heartbeatWritten(self):
self.assertEqual(len(self.transport.value()), 1)
self.clock.advance(60)
self.assertEqual(len(self.transport.value()), 2)
def test_lostConnectionStopsHeartbeater(self):
self.assertTrue(self.protocol._heartbeater.running)
self.protocol.connectionLost(main.CONNECTION_DONE)
self.assertFalse(self.protocol._heartbeater.running)
HeartbeatProtocolTest.setUp与PingPongProtocolTests.setUp几乎相同,除了它用twisted.internet.task.Clock代替MemoryReactor。Clock,顾名思义,提供了一个反应器的时间相关接口的实现。最重要的是,它有一个callLater方法:
>>> from twisted.internet.task import Clock
>>> clock = Clock()
>>> clock.callLater(1.0, print,"OK")
因为它们旨在单元测试中使用,Clock实例自然没有自己的select循环。我们可以通过调用advance来模拟select超时的终止:
>>> clock.advance(2)
OK
test_heartbeatWritten调用advance使其协议的LoopingCall写入一个字节。这类似于PingPongProtocolTests.test_byteWrittenForByte对其协议的dataReceived的调用;两者都模拟了反应堆在这些测试之外管理的事件的发生。
Twisted 的事件驱动编程方法依赖于清晰描述的接口,如Protocol和Clock的接口。然而,到目前为止,我们都认为每个接口的本质是理所当然的:我们怎么知道Clock或MemoryReactor可以取代测试套件中的真实反应器呢?我们可以通过探索 Twisted 用来管理其接口的工具来回答这个问题。
与 zope.interface 的事件接口
Twisted 使用一个名为zope.interface的包来形式化它的内部接口,包括那些描述它的事件驱动范例的接口。
Zope 是一个古老但仍然活跃的项目,已经产生了几个 web 应用框架,其中最老的是在 1998 年首次公开发布的。许多技术起源于 Zope,并被用于其他项目。Twisted 使用 Zope 的接口包来定义它的接口。
对zope.interface的完整解释超出了本书的范围。然而,接口在测试和文档中起着重要的作用,所以我们通过研究前面例子中使用的 Twisted 类的接口来介绍它们。
我们首先询问Clock的一个实例,它提供了什么接口:
>>> from twisted.internet.task import Clock
>>> clock = Clock()
>>> from zope.interface import providedBy
>>> list(providedBy(clock))
[<InterfaceClass twisted.internet.interfaces.IReactorTime>]
首先,我们创建一个Clock的实例。然后我们从zope.interface包中检索providedBy;因为 Twisted 本身依赖于zope.interface,所以我们可以在交互会话中使用它。在我们的Clock实例上调用providedBy会返回它提供的接口的一个 iterable。
与其他语言的接口不同,zope.interface的接口可以是实现的或提供的。符合接口的单个对象提供该接口,而创建那些提供接口的对象实现该接口。这种微妙的区别与 Python 的“鸭子打字”相匹配。一个接口定义可能描述一个call方法,并因此应用于一个用def或lambda创建的函数对象。这些语法元素不能被标记为我们接口的实现者,但是功能对象本身可以说是提供了它。
一个接口是zope.interface.Interface的子类,它使用一个特殊的 API 来描述所需的方法和它们的签名以及属性。下面是我们的Clock提供的twisted.internet.interfaces.IReactorTime接口的摘录:
class IReactorTime(Interface):
"""
Time methods that a Reactor should implement.
"""
def callLater(delay, callable,*args,**kw):
"""
Call a function later.
@type delay: C{float}
@param delay: the number of seconds to wait.
@param callable: the callable object to call later.
@param args: the arguments to call it with.
@param kw: the keyword arguments to call it with.
@return: An object which provides L{IDelayedCall} and can be used to
cancel the scheduled call, by calling its C{cancel()} method.
It also may be rescheduled by calling its C{delay()} or
C{reset()} methods.
"""
注意,callLater“方法”没有self参数。这是接口不能实例化的结果。它也没有主体,而是通过只提供一个 docstring 来满足 Python 的函数定义语法。不像抽象类,比如那些由标准库的abc模块提供的,它们也不能包含任何实现代码。相反,它们只是作为描述对象功能子集的标记而存在。
Zope 提供了一个名为verifyObject的助手,如果一个对象没有提供接口,它会抛出一个异常:
>>> from zope.interface.verify import verifyObject
>>> from twisted.internet.interfaces import IReactorTime
>>> verifyObject(IReactorTime, clock)
True
>>> verifyObject(IReactorTime, object()))
Traceback (most recent call last):
File"<stdin>", line1, in <module>
...
zope.interface.exceptions.DoesNotImplement: An object does not implement interface<Interface
我们可以用这个来确认反应器提供了与Clock实例相同的IReactorTime接口:
>>> from twisted.internet import reactor
>>> verifyObject(IReactorTime, reactor) True
稍后当我们编写自己的接口实现时,我们将回到verifyObject。不过现在,只要知道我们可以在任何依赖IReactorTime.callLater的地方用Clock实例替换反应器就足够了。一般来说,如果我们知道一个对象所提供的接口包含了我们所依赖的方法或属性,我们就可以用其他提供相同接口的对象来替换这个对象。虽然我们可以用providedBy交互地发现一个对象提供的接口,但是 Twisted 的在线文档对接口有特殊的支持。图 1-2 描述了 Twisted 网站上Clock的文档。
图 1-2
twisted.internet.task.Clock文档。虚线框突出显示了到IReactorTime接口的链接。
由Clock类实现的接口在虚线矩形中突出显示。单击每一个都可以看到该接口的文档,其中包括所有已知实现者和提供者的列表。如果您知道对象是什么,那么您可以通过访问它的文档来确定它的接口。
我们接下来讨论一个问题,这个问题的 Twisted 解决方案涉及到定义接口的实现者。
事件驱动程序中的流量控制
PingPongProtocol不同于我们为上一个非 Twisted 事件驱动的示例编写的流协议:PingPongProtocol中的每一端都写入一个字节来响应接收到的字节,而流协议让客户端向服务器发送越来越大的字节序列,当服务器不堪重负时暂停其写入。调整发送方的写入速率以匹配接收方的读取速率被称为流量控制。
当与事件驱动的编程相结合时,非阻塞 I/O 使我们能够编写在任何给定时间可以响应许多不同事件的程序。同步 I/O,就像我们看到的根据sendall实现的流客户端协议,暂停或阻塞我们的程序,阻止它做任何事情,直到 I/O 操作完成。虽然这增加了并发性的难度,但它使流控制变得更加容易:超过其读取器速度的编写器会被操作系统暂停,直到读取器接受挂起的数据。在我们的流客户端中,这导致了死锁,因为慢速读者运行在由于写得太快而暂停的同一进程中,因此永远无法跟上。更常见的情况是,读取器和写入器运行在不同的进程中,如果不是在不同的机器上,它们的同步、阻塞 I/O 自然提供了流控制。
然而,在网络应用中很少遇到简单的阻塞 I/O。即使是最简单的也必须为每个连接同时管理两件事:数据通信和与每个 I/O 操作相关的超时。Python 的socket模块允许程序员在recv和sendall操作上设置这些超时,但在幕后这是通过调用带有超时的select来实现的!
我们有实现流控制所必需的事件。select通知我们可写事件,而EAGAIN表明套接字的发送缓冲区已满,从而间接表明接收者不堪重负。我们可以组合这些来暂停和恢复写入程序,并实现类似于阻塞 I/O 所提供的流控制。
流控制与生产者和消费者纠缠在一起
Twisted 的流量控制系统有两个组成部分:生产者和消费者。生产者通过调用消费者的write方法向消费者写入数据。消费者包装生产者;每个消费者可以与一个生产者相关联。这种关系确保了消费者可以访问其生产者,因此它可以通过调用生产者的某些方法来调节数据流,从而对生产者施加反压力。常见的传输,比如绑定到像我们的PingPongProtocol这样的协议的 TCP 传输,既可以是消费者也可以是生产者。
我们通过重新实现我们预先 Twisted 的流客户端示例来探索生产者和消费者之间的交互。
推动生产者
我们从客户的制作人开始:
from twisted.internet.interfaces import IPushProducer
from twisted.internet.task import LoopingCall
from zope.interface import implementer
@implementer(IPushProducer)
class StreamingProducer(object):
INTERVAL=0.001
def __init__ (self, reactor, consumer):
self._data = [b"*", b"*"]
self._loop = LoopingCall(self._writeData, consumer.write)
self._loop.clock = reactor
def resumeProducing(self):
print("Resuming client producer.")
self._loop.start(self.INTERVAL)
def pauseProducing(self):
print("Pausing client producer.")
self._loop.stop()
def stopProducing(self):
print("Stopping client producer.")
if self._loop.running:
self._loop.stop()
def _writeData(self, write):
print("Client producer writing", len(self._data),"bytes.")
write(b"".join(self._data))
self._data.extend(self._data)
我们的生产者StreamingProducer,实现twisted.internet.interfaces.IPushProducer。该接口描述了不断向其消费者写入数据直到暂停的生产者。StreamingProducer上的以下方法满足IPushProducer接口:
-
resumeProducing:恢复或启动向消费者写入数据的过程。因为我们的实现通过在每次写入后将一个字节序列加倍来生成数据,所以它需要某种类型的循环来向其消费者提供连续的流。简单的while循环是行不通的:如果不将控制权交还给反应器,程序就不能处理新的事件,直到循环终止。事件驱动的程序(如 web 浏览器)在大文件上传期间会有效地暂停其执行。StreamingProducer通过一个LoopingCall实例将写循环委托给反应器来避免这种情况,因此它的resumeProducing方法启动了那个LoopingCall。一毫秒的间隔是任意低的。我们的生产者不能比这更快地写入数据,所以间隔是延迟的来源,一毫秒可以接受地最小化它。 -
pauseProducing:暂停向消费者写入数据的过程。消费者称这表明它已经不堪重负,无法接受更多的数据。在我们的实现中,停止底层的LoopingCall就足够了。当底层资源可以接受更多数据时,消费者可以稍后调用resumeProducing。这个resumeProducing和pauseProducing调用的循环构成了流量控制。 -
stopProducing:这终止了数据的产生。这与pauseProducing不同,因为在调用stopProducing之后,消费者再也不能调用resumeProducing来接收更多的数据。最明显的是,当一个套接字连接被关闭时,它被调用。StreamingProducer的实现与pauseProducing方法的唯一不同之处在于,它必须首先检查循环调用是否正在运行。这是因为当生产者已经暂停时,消费者可能请求不再写入数据。更复杂的推送生产者将执行额外的清理;例如,从一个文件传输数据的生产者需要在这里关闭该文件,以将其资源释放回操作系统。
请注意,IPushProducer并没有指定它的实现者如何向消费者写入数据,甚至如何访问数据。这使得界面更加灵活,但也使其更难实现。StreamingProducer遵循一种典型的模式,在其初始化器中接受消费者。我们将很快介绍完整的消费者接口,但是现在,知道消费者必须提供一个write方法就足够了。
我们可以测试StreamingProducer实现了IPushProducer的预期行为:
from twisted.internet.interfaces import IPushProducer
from twisted.internet.task import Clock
from twisted.trial import unittest
from zope.interface.verify import verifyObject
class FakeConsumer(object):
def __init__ (self, written):
self._written = written
def write(self, data):
self._written.append(data)
class StreamingProducerTests(unittest.TestCase):
def setUp(self):
self.clock = Clock()
self.written = []
self.consumer = FakeConsumer(self.written)
self.producer = StreamingProducer(self.clock,self.consumer)
def test_providesIPushProducer(self):
verifyObject(IPushProducer,self.producer)
def test_resumeProducingSchedulesWrites(self):
self.assertFalse(self.written)
self.producer.resumeProducing()
writeCalls = len(self.written)
self.assertEqual(writeCalls,1)
self.clock.advance(self.producer.INTERVAL)
newWriteCalls = len(self.written)
self.assertGreater(newWriteCalls, writeCalls)
def test_pauseProducingStopsWrites(self):
self.producer.resumeProducing()
writeCalls = len(self.written)
self.producer.pauseProducing()
self.clock.advance(self.producer.INTERVAL)
self.assertEqual(len(self.written), writeCalls)
def test_stopProducingStopsWrites(self):
self.producer.resumeProducing()
writeCalls = len(self.written)
self.producer.stopProducing()
self.clock.advance(self.producer.INTERVAL)
self.assertEqual(len(self.written), writeCalls)
FakeConsumer接受一个列表,每个write调用都会将收到的数据附加到该列表中。这允许测试套件断言StreamingProducer已经在预期的时候调用了它的消费者的write方法。
test_providesIPushProducer确保StreamingProducer定义了IPushProducer要求的方法。如果没有,这个测试将通过zope.interface.exceptions.DoesNotImplement失败。像这样断言实现满足其接口的测试在开发和重构中是一个有用的高通过滤器。
test_resumeProducingSchedulesWrites断言调用resumeProducing意味着向消费者写入数据,并且每次经过指定的时间间隔,都会写入更多的数据。test_pauseProducingStopsWrites和test_stopProducingStopsWrites都断言相反的情况:调用pauseProducing和stopProducing防止在每个间隔过去后发生进一步的写操作。
顾客
StreamingProducer放出数据却无处安放。为了完成我们的流媒体客户端,我们需要一个消费者。StreamingProducer的初始化器清楚地表明,消费者的接口必须提供一个write方法,概述表明,额外的消费者方法管理与生产者的交互。twisted.internet.interfaces.IConsumer要求实施者实施三种方法:
-
write:接受来自生产者的数据。这是在我们上面的测试中由FakeConsumer提供的唯一方法,因为它是IConsumer接口IPushProducer调用的唯一部分。 -
registerProducer:这将生产者与消费者关联起来,确保它可以调用生产者的resumeProducing和pauseProducing来调节数据流,调用stopProducing来终止数据流。这接受了两个论点:生产者和一面streaming旗帜。我们稍后将解释这第二个论点的目的;现在,知道我们的流媒体客户端会将此设置为True就足够了。 -
这就把生产者和消费者分开了。一个消费者可能在其一生中接受来自多个生产者的数据;再考虑一个 web 浏览器,它可能通过单个连接向服务器上传多个文件。
IConsumer实现者和传输者都公开了write方法,这并不是巧合;如上所述,绑定到连接协议的 TCP 传输是一个消费者,我们可以向其注册一个StreamingProducer实例。我们可以修改我们的PingPongProtocol示例,在成功连接后用其底层传输注册StreamingProducer:
from twisted.internet import protocol, reactor
from twisted.internet.interfaces import IPushProducer
from twisted.internet.task import LoopingCall
from zope.interface import implementer
@implementer(IPushProducer)
class StreamingProducer(object):
INTERVAL=0.001
def __init__ (self, reactor, consumer):
self._data = [b"*", b"*"]
self._loop = LoopingCall(self._writeData, consumer.write)
self._loop.clock = reactor
def resumeProducing(self):
print("Resuming client producer.")
self._loop.start(self.INTERVAL)
def pauseProducing(self):
print("Pausing client producer.")
self._loop.stop()
def stopProducing(self):
print("Stopping client producer.")
if self._loop.running:
self._loop.stop()
def _writeData(self, write):
print("Client producer writing", len(self._data),"bytes.")
write(b"".join(self._data))
self._data.extend(self._data)
class StreamingClient(protocol.Protocol):
def connectionMade(self):
streamingProducer = StreamingProducer(
self.factory._reactor,self.transport)
self.transport.registerProducer(streamingProducer,True)
streamingProducer.resumeProducing()
class ReceivingServer(protocol.Protocol):
def dataReceived(self, data):
print("Server received", len(data),"bytes.")
class StreamingClientFactory(protocol.ClientFactory):
protocol = StreamingClient
def __init__ (self, reactor):
self._reactor = reactor
class ReceivingServerFactory(protocol.Factory):
protocol = ReceivingServer
listener = reactor.listenTCP(port=0,
factory=ReceivingServerFactory(),
interface='127.0.0.1')
address = listener.getHost()
reactor.connectTCP(host=address.host,
port=address.port,
factory=StreamingClientFactory(reactor))
reactor.run()
StreamingClient协议创建一个StreamingProducer,然后向其传输注册。如前所述,registerProducer的第二个参数是True。然而,注册一个生产者并不会自动恢复它,所以我们必须通过调用resumeProducing来开始StreamingProducer的写循环。注意,StreamingClient从不调用它的生产者的stopProducing:当反应堆发出断开信号时,transports 代表它们的协议调用这个。
运行此命令会产生如下输出:
Resuming client producer.
Client producer writing 2 bytes.
Server received 2 bytes.
Client producer writing 4 bytes.
Server received 4 bytes.
Client producer writing 8 bytes.
Server received 8 bytes.
...
Client producer writing 524288 bytes.
Pausing client producer.
Server received 65536 bytes.
Server received 65536 bytes.
Server received 65536 bytes.
Server received 65536 bytes.
Resuming client producer.
Client producer writing 1048576 bytes.
Pausing client producer.
...
最终,程序将消耗所有可用的内存,从而构成一个成功的流量控制实验。
拉动生产者
存在第二个生产者接口:twisted.internet.interfaces.IPullProducer。不像IPushProducer,它只在它的resumeProducing方法被调用时写给它的消费者。这就是IConsumer.registerProducer的第二个论点的目的:IPullProducer s 要求streaming为False。不写IPullProducer s!大多数传输的行为类似于套接字,并生成可写事件,从而消除了对类似StreamingProducer的写循环的需要。当数据必须手动从源中抽出时,编写和测试LoopingCall反而更容易。
摘要
我们已经看到事件驱动编程如何将程序分成事件和它们的处理程序。程序发生的任何事情都可以被建模为事件:来自用户的输入、通过套接字接收的数据,甚至是时间的流逝。一个事件循环使用一个多路复用器来等待任何可能发生的事件,为那些已经发生的事件运行适当的处理程序。操作系统提供底层接口,比如select,来复用网络套接字 I/O 事件。使用select的事件驱动网络编程在使用非阻塞时最为有效,它为send和recv等操作生成事件,指示程序应该停止运行事件处理程序。
由非阻塞套接字发出的停止事件会导致没有正确抽象的复杂代码。协议和传输在原因和结果之间划分程序代码:传输将读取、写入和停止事件翻译成协议可以响应的更高级原因,依次生成新事件。协议和传输之间的这种责任划分允许实现事件处理程序,通过用内存中的假货替换传输,可以很容易地测试这些事件处理程序。稍后,我们将看到协议-传输分离的其他实际好处。
协议、传输和反应器——事件循环的名称——是 Twisted 运行的基础,并贯穿其整体架构。Twisted 的反应器可以对非 I/O 事件做出反应,比如时间的流逝。测试这些并不比测试协议更困难,因为反应堆,像传输一样,在内存中有假货。Twisted 形式化了反应器和其他对象必须通过zope.interface实现的接口。通过确定一个对象提供了什么接口,就有可能选择一个适合测试的替代品,它保证是等价的,因为它提供了相同的接口。Twisted 的在线文档使得在 Python 会话中发现接口比检查活动对象更容易。
接口的一个实际应用是 Twisted 对事件驱动的网络编程难以解决的问题的解决方案:流量控制。IPushProducer和IConsumer定义了一组行为,允许流数据的接收者在不堪重负时暂停数据源。
这个介绍足以解释 Twisted 中事件驱动编程的核心原则。然而,还有更多:在下一章,我们将了解 Twisted 如何通过允许程序处理尚未计算的值来进一步简化事件驱动编程。**
二、Twisted 异步编程简介
前一章从基本原则中推导出 Twisted 的事件驱动架构。Twisted 程序和所有事件驱动的程序一样,以增加数据流控制的难度为代价,使并发变得更容易。当事件驱动程序发送的数据超过接收方的处理能力时,它不会通过阻塞 I/O 来自动暂停执行。确定这种情况何时发生以及如何处理是程序的责任。
通信方之间的数据流动方式也会影响数据在单个程序中的流动方式。因此,组成一个事件驱动应用的不同组件的策略不同于阻塞程序中使用的策略。
事件处理程序和合成
考虑一个不是事件驱动的程序,它使用阻塞 I/O 来执行网络操作:
def requestField(url, field):
results = requests.get(url).json()
return results[field]
requestField用requests HTTP 库检索一个 URL,将响应的主体解码为 JSON,然后从结果字典中返回所请求的field属性的值。requests使用阻塞 I/O,所以对requestField的调用会暂停整个程序,直到 HTTP 请求所需的网络操作完成。因此,该函数可以假设在它返回之前,results将可用于操作。这个函数的调用者可以做出同样的假设,因为requestField会阻塞它们,直到它计算出结果:
def someOtherFunction(...):
...
url = calculateURL(...)
value = requestField(url, 'someInteger')
return value + 1
x = someOtherFunction(...)
在requestField从 JSON 响应中检索到 URL 并提取出someInteger属性的值之前,someOtherFunction和顶级x赋值都无法完成。这是一种合成 : someOtherFunction调用requestField来完成自己执行的一部分。我们可以通过显式函数组合使这一点更清楚:
def someOtherFunction(value):
return value + 1
x = someOtherFunction(requestField(calculateURL(...), 'someInteger'))
这段代码用嵌套的函数调用替换了someOtherFunction的局部变量,但在其他方面是等效的。
功能组合是组织程序的基本工具。它允许一个程序被分解,或者分解成独立的单元,形成一个整体,其行为与未分解的版本完全匹配。这提高了可读性、可重用性和可测试性。
不幸的是,事件处理程序不能像someOtherFunction、requestField和calculateURL那样构成。考虑一个假设的非阻塞版本的requestField:
def requestField(url, field):
??? = nonblockingGet(url)
在非阻塞版本的requestField中,什么可以取代????这是一个很难回答的问题,因为nonblockingGet不会暂停程序的执行来完成构成对url的 HTTP 请求的网络操作;相反, requestField外的一个事件循环复用可读和可写的事件,只要有可能就调用事件处理程序发送和接收数据。没有明显的方法可以从我们假设的nonblockingGet函数中返回event handlers'值。
幸运的是,通过将事件处理程序表示为函数,我们可以利用函数组合的通用性将事件驱动的程序分解成独立的组件。让我们假设假设的nonblockingGet函数本身接受一个事件处理函数作为参数,当请求的完成事件发生时,它调用这个函数。这个较高级别的事件将由较低级别的事件合成,类似于我们在第一章中看到的传输为了它们的协议而发出一个connectionLost事件的方式。然后我们可以重写requestField来利用这个新论点:
def requestField(url, field):
def onCompletion(response):
document = json.loads(response)
value = response[field]
nonblockingGet(url, onCompletion=onCompletion)
onCompletion是一个回调,或者是一个可调用的对象,作为一个参数传递给一些其他可调用的对象,这些对象执行一些期望的操作。当该操作完成时,用一些相关的参数调用回调。在这种情况下,nonblockingGet在其 HTTP 请求解析为完整的响应对象时调用其onCompletion回调。在前一章的BuffersWrites实现中,我们看到了一个等价的onCompletion回调;在那里,当所有缓冲的数据都被写入套接字时,它被调用。
回调在内部组合*,而其他函数,比如上面的someOtherFunction示例,在外部组合*;在可调用程序的执行过程中,值可用于回调,从而获得所需的结果,而不是从该可调用程序返回。**
**与nonblockingGet提取事件驱动的 HTTP 请求代码的方式相同,requestField可以通过接受自己的回调提取字段的使用方式。我们将让requestField接受一个useField回调,然后让onCompletion回调调用它:
def requestField(url, field, useField):
def onCompletion(response):
document = json.loads(response)
value = response[field]
useField(value)
nonblockingGet(url, onCompletion=onCompletion)
我们可以通过将someOtherFunction作为useField回调来编写一个事件驱动的程序,它相当于我们的阻塞 I/O 版本:
def someOtherFunction(useValue):
url = calculateURL(...)
def addValue(value):
useValue(value + 1)
requestField(url,"someInteger", useField=addValue)
反过来,someOtherFunction也必须通过接受其自己的回调来进行内部合成,这与之前在外部合成的calculateURL不同。这种回调驱动的方法足以编写任何程序;事实上,在计算机科学的研究中,回调可以被细化为称为延续的控制流原语,并被用于一种称为延续传递风格的技术中,在这种技术中,函数通过调用它们的延续并产生结果来终止。延续传递风格已经在各种语言编译器中使用,以支持程序分析和优化。
尽管延续传递式的理论很强大,但读起来和写起来都很笨拙。此外,外部构图——如requestField和calculateURL——和内部构图——如requestField和useField——彼此之间没有明显的构图。例如,很难想象calculateURL如何会被视为回调。最后,错误处理是一个关键的因果关系;想象一下我们将如何以延续传递的方式处理异常!在这个例子中,我们有意省略了任何错误处理,以保持代码足够短,便于阅读。
幸运的是,异步编程提供了一个强大的抽象,简化了事件处理程序的组成,并解决了这些问题。
什么是异步编程?
我们最初的requestField实现是同步,因为整个程序的执行是随着时间的推移线性进行的。例如,给定对request.get的两个调用,第一个将在第二个之前完成。同步编程是一种适用于阻塞 I/O 的常见范例。包括 Python 在内的大多数编程语言都默认通过阻塞 I/O 来启用同步操作。
我们的事件驱动requestField的延续传递风格是一种异步编程:当通过nonblockingGet回调的逻辑流暂停直到必要的数据可用时,整个程序的执行继续。两个独立的nonblockingGet调用的执行将交错进行,没有保证它们完成的顺序;一个比另一个早开始并不保证它会先完成。这就是并发的定义。
利用非阻塞 I/O 的事件驱动程序必然是异步的,因为所有的 I/O 操作都是基于可以在任何时间以任何顺序到达的事件进行的。值得注意的是,异步程序不需要事件驱动的 I/O;不同的平台基于完全不同的原语提供 I/O 和调度模式。例如,Windows 提供了 I/O 完成端口(IOCP ),它通知程序请求操作的完成,而不是执行操作的机会。例如,请求 IOCP 基础设施在套接字上执行读取的程序将被通知读取完成的时间和数据。Twisted 以其 IOCP 反应器的形式对此提供了一些支持,但就我们的目的而言,我们可以将异步编程理解为事件驱动范式的脱节和零碎执行的结果,就像同步编程是阻塞 I/O 的结果一样
未来值的占位符
事件驱动程序中的回调模糊了控制流,因为它们在内部组成了*;它们不是将值返回给调用者,而是将结果转发给作为参数接收的回调。这导致了应用逻辑和控制流的混合,使重构变得困难,并且在错误发生点和对错误感兴趣的代码之间出现了脱节。*
引入一个表示尚未计算的值的对象允许回调在外部被组合*。考虑一下当允许返回这种占位符时,我们的非阻塞requestField示例是如何变化的:
def requestField(url, field):
def onCompletion(response):
document = json.load(response)
return jsonDoc[field]
placeholder = nonblockingGet(url)
return placeholder.addCallback(onCompletion)
nonblockingGet现在返回一个占位符,这个占位符不是响应,而是一个容器,当响应准备好时,它将被放入这个容器。没有操作的容器不会提供太多好处,所以这个占位符接受它在值准备好时调用的回调。我们没有将onCompletion直接传递给nonblockingGet,,而是将其作为回调附加到占位符nonblockingGet的返回中。内部onCompletion回调的实现现在可以返回值——从 JSON 文档中提取的字段——该值将成为后续回调的参数。
requestField现在可以暂时删除自己的回调参数,并将占位符返回给someOtherFunction,后者可以添加自己的回调:
def someOtherFunction(...):
url = calculateURL(...)
def addValue(value)
return value + 1
placeholder = requestField(url,"someInteger")
return placeHolder.addCallback(addValue)
我们的占位符值并没有完全消除回调。相反,它提供了一个控制流抽象,将回调定位到它们的起始范围,这样它们就可以在外部被组合。当多个回调处理一个异步结果时,这变得更加清晰。考虑以下内部编写的回调:
def manyCallbacks(url, useValue, ...):
def addValue(result):
return divideValue(result + 2)
def divideValue(result):
return multiplyValue(result // 3)
def multiplyValue(result):
return useValue(result * 4)
requestField(url, "someInteger", onCompletion=addValue)
控制从addValue流向divideValue,最后从multiplyValue退出,进入由manyCallbacks的调用者提供的useValue回调。改变三个内部回调的顺序需要重写每一个。然而,占位符对象将该顺序移出每个回调:
def manyCallbacks(url, ...):
def addValue(result):
return result + 2
def divideValue(result):
return result // 3
def multiplyValue(result):
return result * 4
placeholder = requestField(url, "someInteger")
placeholder.addCallback(addValue)
placeholder.addCallback(divideValue)
placeholder.addCallback(multiplyValue)
return placeholder
divideValue不再直接依赖于multiplyValue,所以可以在multiplyValue之前移动,甚至不需要改变它或者multiplyValue就可以移除。
回调的实际组合发生在placeholder对象中,其核心实现非常简单。我们将我们的占位符类命名为Deferred,因为它代表一个延迟值——一个尚未准备好的值:
class Deferred(object):
def __init__ (self):
self._callbacks = []
def addCallback(self, callback):
self._callbacks.append(callback)
def callback(self, result):
for callback in self._callbacks:
result = callback(result)
当结果可用时,Deferred实例的创建者调用callback。每个回调用当前结果调用,其返回值成为传递给下一个回调的结果。这就是上面的onCompletion如何将 HTTP 响应变成唯一感兴趣的 JSON 字段。
由Deferred的for循环施加的控制流足以依次调用每个回调,但是不能比内部合成的回调更好地处理异常。解决这个问题需要添加某种分支逻辑来检测异常并将异常重新路由到它们的目的地。
异步异常处理
同步 Python 代码用try和except处理异常:
def requestField(url):
response = requests.get(url).content
try:
return response.decode('utf-8')
except UnicodeDecodeError:
# Handle this case
通过addCallback方法添加到Deferred的回调在没有异常发生时运行,因此是try块的异步等价物。我们可以通过为except块引入一个类似的回调来添加错误处理,该回调接受异常作为它的参数。像这样被异常调用的回调被称为错误返回。
同步代码可以通过省略try和except来选择让异常向上传播到它的调用者。然而,Deferred的控制流将允许回调引发的异常从for循环向上返回到Deferred.callback的调用者。这将是放置异常处理的错误位置,因为为Deferred提供值的代码不知道添加回调的代码想要的错误处理行为。将这种错误处理封装在我们传递给Deferred s 的errbacks中,允许那些Deferred s 在正确的时间调用它们,而不是麻烦Deferred.callback的调用者。
然后,在回调链的每一步,循环必须捕捉任何异常,并将其转发给下一个 errback。因为每个步骤都可能调用回调或错误返回,所以我们的callbacks列表将更改为包含(callback、errback)对:
def passthrough(obj):
return obj
class Deferred(object):
def __init__ (self):
self._callbacks = []
def addCallback(self, callback):
self._callbacks.append((callback, passthrough))
def addErrback(self, errback):
self._callbacks.append((passthrough, errback))
def callback(self, result):
for callback, errback in self._callbacks:
if isinstance(result,BaseException):
handler = errback
else:
handler = callback
try:
result = handler(result)
except BaseExceptionas e:
result = e
循环的每次迭代都检查当前结果。异常被传递给下一个 errback,而其他的都像以前一样被传递给下一个回调。由 errback 或回调引发的任何异常都将成为链中下一个 errback 要处理的结果。这就产生了下面的Deferred代码:
someDeferred = Deferred()
someDeferred.addCallback(callback)
someDeferred.addErrback(errback)
someDeferred.callback(value)
相当于这个同步代码:
try:
callback(value)
except BaseExceptionas e:
errback(e)
错误返回通过返回异常来传播异常,并通过返回任何不是而不是异常的值来抑制异常。下面的Deferred代码过滤掉 ValueErrors,同时让所有其他异常传播到下一个 errback:
def suppressValueError(exception):
if not isinstance(exception, ValueError):
return exception
someDeferred.addErrback(suppressValueError)
当isinstance(exception, ValueError)评估为True时suppressValueError隐式返回None,因此Deferred回调循环中的异常检查将None传递到下一个回调。每隔一个异常从suppressValueError返回,进入for循环,并继续下一个 errback。总的效果相当于下面的同步代码:
try:
callback(value)
except ValueError:
pass
当我们考虑它可能遇到异常的两个地方时,Deferred的新控制流的一个方便的结果变得明显:
-
在
Deferred的回调列表中的任何回调都可能引发异常。例如,我们的manyCallback函数的回调序列中的一个错误可能导致addValue返回None,在这种情况下divideValue将引发一个TypeError。 -
将实际值传递给
Deferred的callback方法的代码可能会引发一个异常。例如,想象一下,nonblockingGet试图将 HTTP 响应的主体解码为 UTF-8,并使用结果回调一个Deferred。如果主体包含非 UTF 8 字节序列,将引发一个UnicodeDecodeError。这种异常意味着实际值永远无法计算,这是Deferred的 errbacks 应该知道的错误情况。
Deferred现在处理这两种情况;第一个问题可以通过在一个try块中运行每个回调和错误返回来解决,而第二个问题可以通过捕捉异常并将其转发给Deferred.callback来解决。考虑一个 HTTP 协议实现,它试图用 UTF-8 解码的响应体调用Deferred的回调:
class HTTP(protocol.Protocol):
def dataReceived(self, data):
self._handleData(data)
if self.state == "BODY_READY":
try:
result = data.decode('utf-8')
except Exceptionas e:
result = e
self.factory.deferred.callback(e)
class HTTPFactory(protocol.Factory)
protocol = HTTP
def __init__ (self, deferred):
self.deferred = deferred
def nonblockingGet(url):
deferred = Deferred()
factory = HTTPFactory(deferred)
...
return deferred
这是因为Deferred的for循环通过检查当前结果的性质来开始每次迭代。第一次通过循环,结果是无论调用者提供什么callback;在对Exception进行编码的情况下,上面的代码向callback提供了那个异常。
异常处理现在可以在 errbacks 中本地化,就像应用逻辑在回调中本地化一样。这允许我们将同步异常控制流转换为异步异常控制流。此代码:
def requestField(url, field):
results = requests.get(url).json()
return results[field]
def manyOperations(url):
result = requestField(url, field)
try:
result += 2
result //= 3
result *= 4
except TypeError:
return -1
return result
变成了这样的代码:
def manyCallbacks(url):
def addValue(result):
return result + 2
def divideValue(result):
return result // 3
def multiplyValue(result):
return result * 4
def onTypeError(exception):
if isinstance(exception,TypeError):
return -1
else:
return exception
deferred = requestField(url, "someInteger")
deferred.addCallback(addValue)
deferred.addCallback(divideValue)
deferred.addCallback(multiplyValue)
deferred.addErrback(onTypeError)
return deferred
Twisted 提供了一个Deferred实现,它的 API 是这里显示的 API 的超集;正如我们将在下一节中看到的,真正的Deferred自己组成,并提供额外的功能,如超时和取消。然而,在其核心,它的行为与我们的玩具实现相匹配。
Twisted 的延期介绍
了解 Twisted 的Deferred的最好方法是在 Python 会话中使用它。我们将从从twisted.internet.defer导入开始:
>>> from twisted.internet.defer import Deferred
回收
像我们的玩具实现一样,twisted.internet.defer.Deferred的addCallback方法接受一个回调来添加到实例的回调列表中。与我们的实现不同,Twisted 还接受将传递给回调的位置和关键字参数:
>>> d = Deferred()
>>> def cbPrint(result, positional, **kwargs):
... print("result =", result, "positional =", positional,
... "kwargs =", kwargs)
...
>>> d.addCallback(cbPrint, "positional", keyword=1) is d
True
>>> d.callback("result")
result = result positional = positinal, kwargs = {'keyword': 1}
我们创建一个名为 d 的Deferred,添加cbPrint作为回调,然后用"result". d回调 d,将它传递给cbPrint作为它的第一个位置参数,而传递给d.addCallback的附加参数作为它的剩余参数。
注意,d.addCallback返回 d 本身,这允许像
d.addCallback(...).addCallback(...).addCallback(...).
现在d已经用一个值回调了,它不能再被回调:
>>> d.callback("whoops")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "site-packages/twisted/internet/defer.py", line 459, in callback
self._startRunCallbacks(result)
File "site-packages/twisted/internet/defer.py", line 560, in _startRunCallbacks
raise AlreadyCalledError
twisted.internet.defer.AlreadyCalledError
这是因为Deferred s 记得他们被召回的价值:
>>> d2 = Deferred()
>>> d2.callback("the result")
<Deferred at 0x12345 current result: 'the result'>
Deferreds存储结果的事实提出了一个问题:当有结果的Deferred被添加了回调时会发生什么?
>>> d2.addCallback(print)
the result
一旦将print作为回调添加到d2中,它就会运行。一个有结果的Deferreds立即运行添加给它的回调。人们很容易想象Deferred s 总是代表一个尚不可用的值。然而,假设这一点的代码是错误的,并且是令人沮丧的错误的来源。请考虑以下几点:
class ReadyOK(twisted.internet.protocol.Protocol):
def connectionMade(self):
someDeferred = someAPI()
def checkAndWriteB(ignored):
self.transport.write(b"OK\n")
someDeferred.addCallback(checkAndWriteB)
self.transport.write(b"READY\n")
顾名思义,这个ReadyOK协议应该用一条READY线路来迎接新的连接,只写OK,当someAPI回叫它的Deferred时就断开。当someDeferred直到connectionMade返回后才被回调时,READY才会出现在OK之前,但这并不能保证;如果someAPI返回someDeferred一个结果,那么OK出现在READY之前。这种预期行顺序的颠倒会破坏正确要求先发送READY的客户端。
这种情况下的解决方法是将self.transport.write(b"READY\n") 移到 someDeferred = someAPI()之前。您可能需要类似地重新组织您自己的代码,以确保结果的Deferreds不违反不变量。
错误和失败
Deferreds也有 errbacks 来处理由回调和调用代码提供的异常Deferred.callback。我们首先考虑第一种情况:
>>> d3 = Deferred()
>>> def cbWillFail(number):
... 1 / number
...
>>> d3.addCallback(cbWillFail)
<Deferred at 0x123456>
>>> d3.addErrback(print)
<Deferred at 0x123456>
>>> d3.callback(0)
[Failure instance: Traceback: <class 'ZeroDivisionError'>: division by zero
<stdin>:1:<module>
site-packages/twisted/internet/defer.py:459:callback
site-packages/twisted/internet/defer.py:567:_startRunCallbacks
--- <exception caught here> ---
site-packages/twisted/internet/defer.py:653:_runCallbacks
<stdin>:2:cbWillFail
]
d3 Deferred有一个将 1 除以其参数的回调函数,内置的print函数作为 errback,因此回调函数引发的任何异常都将出现在我们的交互会话中。用 0 回调d3自然会产生一个ZeroDivisionError,但也会产生其他东西:一个失败实例。请注意,Failure字符串表示是用括号([。。。]).errback 打印的是单个故障,而不是有一个故障的list!
Python 2 中的异常对象不包含回溯或其他关于其来源的信息。为了提供尽可能多的上下文,Twisted 引入了Failures作为记录回溯的异步异常的容器类型。在except块中构造的Failure吸收活动异常及其回溯:
>>> from twisted.python.failure import Failure
>>> try:
... 1 /0
... except:
... f = Failure()
...
>>> f
<twisted.python.failure.Failure builtins.ZeroDivisionError: division by zero>
>>> f.value ZeroDivisionError('division by zero',)
>>> f.getTracebackObject()
<traceback object at 0x1234567>
>>> print(f.getTraceback())Traceback (most recent call last):
--- <exception caught here> ---
File "<stdin>", line 2, in <module>
builtins.ZeroDivisionError: division by zero
Failure实例在其value属性下存储实际的异常对象,并以几种不同的方式使回溯本身可用。
还有一些方便的方法,可以在出错时轻松地与它们交互。check方法接受多个异常类,并返回属于Failure的异常或None的一个:
>>> f.check(ValueError)
>>> f.check(ValueError, ZeroDivisionError)
<class 'ZeroDivisionError'>
Failure.trap的行为类似于check,除了当Failure的异常与任何提供的异常类都不匹配时,它会重新引发异常。这允许 errbacks 复制过滤 except 子句的行为:
>>> d4 = Deferred()
>>> def cbWillFail(number):
... 1 / 0
...
>>> def ebValueError(failure):
... failure.trap(ValueError):
... print("Failure was ValueError")
...
>>> def ebTypeErrorAndZeroDivisionError(failure):
... exceptionType = failure.trap(TypeError, ZeroDivisionError):
... print("Failure was", exceptionType)
...
>>> d4.addCallback(cbWillFail)
<Deferred at 0x12345678>
>>> d4.addErrback(ebValueError)
<Deferred at 0x12345678>
>>> d4.addErrback(ebTypeErrorAndZeroDivisionError)
<Deferred at 0x12345678>
>>> d4.callback(0)
Failure was <class 'ZeroDivisionError'>
ebValueError和ebTypeErrorAndZeroDivisionError的功能类似于同步代码中的两个块:
try:
1/0
except ValueError:
print("Failure was ValueError")
except (TypeError,ZeroDivisionError) as e: exceptionType = type(e)
print("Failure was", exceptionType)
最后,Deferreds可以被提供一个Failure或者可以从当前异常合成一个。
用一个Failure实例回调一个Deferred开始执行它的错误返回。someDeferred.callback(Failure())因此类似于通过我们的玩具实现的callback异常。
Deferreds还要暴露一个errback方法。传递这个Failure实例与传递callback实例具有相同的效果;然而,不带参数调用Deferred.errback会导致失败,从而很容易捕获异步处理的异常:
>>> d5 = Deferred()
>>> d5.addErrback(print)
<Deferred at 0x12345678>
>>> try:
... 1/0
... except:
... d.errback()
...
[Failure instance: Traceback:< class 'ZeroDivisionError'>: division by zero
---<exception caught here>---
<stdin>:2:<module>
]
撰写延期
是一个控制流抽象,支持回调和错误的组合。他们还和自己一起作曲,这样一个Deferred可以侍候一个Deferred。
考虑一个名为outerDeferred的Deferred,它有以下回调序列,其中一个返回innerDeferred,它有自己的回调:
>>> outerDeferred = Deferred()
>>> def printAndPassThrough(result, *args):
... print("printAndPassThrough",
... " ".join(args), "received", result)
... return result
...
>>> outerDeferred.addCallback(printAndPassThrough, '1')
<Deferred at 0x12345678>
>>> innerDeferred = Deferred()
>>> innerDeferred.addCallback(printAndPassThrough, '2', 'a')
<Deferred at 0x123456789>
>>> innerDeferred.addCallback(printAndPassThrough, '2', 'b')
<Deferred at 0x123456789>
>>> def returnInnerDeferred(result, number):
... print("returnInnerDeferred #", number, "received", result)
... print("Returning innerDeferred...")
... return innerDeferred
...
>>> outerDeferred.addCallback(returnInnerDeferred, '2')
<Deferred at 0x12345678>
>>> outerDeferred.addCallback(printAndPassThrough, '3')
<Deferred at 0x12345678>
>>> outerDeferred.addCallback(printAndPassThrough, '4')
<Deferred at 0x12345678>
回调outerDeferred清楚地调用了标识符为 1 的printAndPassThrough回调,但是当控制到达returnInnerDeferred时会发生什么呢?
我们可以用图 2-1 中执行流程的可视化表示来回答这个问题。
图 2-1
outerDeferred和innerDeferred之间的执行和数据流。执行遵循虚线箭头,而数据流遵循实线箭头。
标有 A 的方框代表开始outerDeferred回调循环的outerDeferred.callback( ' result ' )调用,而虚线和实线箭头分别表示执行和数据的流向。
标识符为1的第一个回调函数—printAndPassThrough—接收‘result’作为第一个参数,并打印出一条消息。因为它返回' ',outerDeferred用相同的对象调用下一个回调。returnInnerDeferred打印它的标识符和一条它正在返回的消息innerDeferred在这样做之前:
>>> outerDeferred.callback("result")
printAndPassThrough 1 received result
returnInnerDeferred 2 received result
Returning innerDeferred...
outerDeferred内部的回调循环检测到returnInnerDeferred返回了一个Deferred而不是一个实际值,并且暂停自己的回调循环,直到innerDeferred解析为一个值。图 2-1 中的虚线箭头表示执行已经转移到innerDeferred处,outerDeferred的repr也是如此:
>>> outerDeferred
<Deferred at 0x12345678 waiting on Deferred at 0x123456789>
标有 B 的方框代表继续执行的innerDeferred.callback( ' result ' )调用。自然地,innerDeferred自己的回调,printAndPassThrough的标识符2 a和2 b,现在运行。
一旦innerDeferred已经运行了它所有的回调,执行返回到outerDeferred的回调循环,其中printAndPassThrough的3和4用innerDeferred最后一次回调返回的值执行。
>>> innerDeferred.callback('inner result')
printAndPassThrough 2 a received inner result
printAndPassThrough 2 b received inner result
printAndPassThrough 3 received inner result
printAndPassThrough 4 received inner result
实际上,printAndPassThrough 3和4变成了innerDeferred的回调。如果任何innerDeferred自己的回调返回Deferred s,它的回调循环将以与outerDeferred相同的方式暂停。
从回调(以及错误返回)返回Deferreds的能力允许在外部组合返回Deferreds的函数:
def copyURL(sourceURL, targetURL):
downloadDeferred = retrieveURL(sourceURL)
def uploadResponse(response):
return uploadToURL(targetURL, response)
return downloadDeferred.addCallback(uploadResponse)
copyURL使用两个假设的 API:retrieveURL,它检索一个 URL 的内容;和uploadToURL,它上传数据到一个目标 URL。添加到由retrieveURL返回的Deferred中的uploadResponse回调使用来自源 URL 的数据调用uploadResponse,并返回结果Deferred。记住一个Deferred的addCallback返回相同的实例,所以copyURL返回downloadDeferred给它的调用者。
copyURL的用户首先等待下载,然后等待上传。copyURL的实现组合返回Deferred的函数,就像它组合回调函数一样,没有任何特殊用途的 API。
Twisted 的Deferred s 的基本接口允许用户在外部编写回调、错误返回和Deferred s,简化了异步程序的构建。
异步程序可以从外部组合它们的事件处理程序,这并不是唯一的方法。自从 Twisted 的Deferred问世以来的近 20 年里,Python 已经开发了语言级机制来暂停和恢复特殊类型的函数。
生成器和内联回调
产量
Python 从 2.5 版本开始支持生成器。生成器是在它们的主体中使用一个yield表达式的函数和方法。调用生成器会返回一个可迭代的生成器对象。迭代执行生成器主体,直到下一个yield表达式,此时执行暂停,迭代器计算出yield表达式的操作数。
考虑以下生成器的执行:
>>> def generatorFunction():
... print("Begin")
... yield 1
... print("Continue")
... yield 2
...
>>> g = generatorFunction()
>>> g
<generator object generatorFunction at 0x12345690>
>>> result = next(g)
Begin
>>> result
1
被调用时返回一个新的生成器对象。请注意,generatorFunction的身体还没有跑起来。内置的next函数推进了一个迭代器;推进生成器对象g开始执行generatorFunction的主体,将Begin输出到我们的交互式 Python 会话中。执行在到达第一个yield表达式时暂停,提供给yield的值成为next调用的返回值。再次调用next继续执行发生器,直到它到达第二个yield:
>>> nextResult = next(g)
Continue
>>> nextResult
2
再次调用next恢复生成器。这一次它的整个身体都被处决了。没有进一步的yields可以暂停,所以生成器对象不能为后续的next调用提供另一个值。根据 Python 的迭代协议,在 generato 对象上调用next会引发StopIteration来表明它已经被耗尽:
>>> next(g)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
因此,生成器遵循与任何其他迭代器相同的 API:要么像上面那样通过对next的显式调用返回值,要么像在for循环中那样通过隐式调用返回值,而StopIteration异常表明不能再返回更多的值。然而,生成器实现的不仅仅是迭代 API。
派遣
发生器可以接收值,也可以发送它们。yield操作数可以出现在赋值语句的右边。通过将值传递给生成器的send方法,可以使生成器暂停的yield表达式计算出某个值。给定生成器gPrime中的以下yield表达式:
def gPrime():
a = yield 4
gPrime.send(5)导致赋值右侧的yield计算为5,这样生成器中的代码就相当于:
def gPrime():
a = 5
结果,发电机本地变量a取值为 5。与此同时,gPrime().send(5)调用推进生成器,并评估为4。让我们通过检查一个完全工作的例子及其在图 2-2 中的可视化来更详细地探索send的控制流。
图 2-2
执行和数据流入流出receivingGenerator。执行向下移动,而数据流沿着实线箭头。
>>> def receivingGenerator():
... print("Begin")
... x = 1
... y = yield x
... print("Continue")
... z = yield x + y
... print(x + y + z)
...
>>> g = receivingGenerator()
>>> result = next(g) # A Begin
>>> result
1
>>> nextResult = g.send(2) # B
Continue
>>> nextResult
3
>>> g.send(3) # C
6
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
我们从next开始执行receivingGenerator,与我们开始执行generatorFunction的方式相同;生成器必须总是通过迭代一次来启动。图 2-2 中标有 A 的方框表示对next的初始调用。和以前一样,g一直运行,直到暂停在它的第一个yield表达式上,这个next调用计算那个yield的操作数。因为该操作数是被赋值为 1 的局部变量x,所以next调用的值为 1。从yield x出来的黑色箭头,穿过方框 A ,在数值1穿过next离开发生器时追踪该数值。
现在,发电机已经启动,我们可以使用 send 再次恢复它,如框 B 所示。g. send(2)将值2传递给生成器,生成器将其赋给变量y。执行继续,经过print("Continue"),直到在下一个yield暂停。这里的操作数是表达式x + y,其计算结果为 3,并通过g.send(2)返回。从x + y穿过框 B 的黑色箭头显示结果 3 采用的退出路径。
由框 C 表示的调用g.send(3),将 3 发送到生成器并再次继续执行,将x + y + z = 6 打印到会话。然而,生成器不能像以前一样暂停执行,因为在receivingGenerator中没有进一步的yield表达式。因为生成器遵循迭代协议,所以当耗尽时它们抛出StopIteration;g.send(3)因此引发 StopIteration,而不是计算一个值,如图 2-2 所示,并在示例代码中演示。
扔
正如send允许将值传递给生成器一样,throw允许在生成器中引发异常。考虑以下代码:
>>> def failingGenerator():
... try:
... value = yield
... except ValueError:
... print("Caught ValueError")
...
>>> tracebackG = failingGenerator()
>>> next(tracebackG)
>>> tracebackG.throw(TypeError())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 3, in failingGenerator
TypeError
>>> catchingG = failingGenerator()
>>> next(catchingG)
>>> catchingG.throw(ValueError())
Caught ValueError
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
failingGenerator将其yield表达式包装在一个try块中,该块的except捕获ValueError,然后打印一条消息。所有其他异常都传递回调用者。
我们通过调用failingGenerator并将其命名为tracebackG来创建一个新的生成器。我们像往常一样先给next打个电话。注意failingGenerator的yield缺少一个操作数;Python 用None表示值的缺失,所以next计算为None(当函数返回None时,迭代 Python 会话不打印None)。在生成器内部,第一个yield本身评估为None,因为next不能向生成器发送任何值。因此,g.send(None)相当于next(g)。当我们研究协程时,这种等价将变得非常重要。
接下来,我们通过throw方法将TypeError扔进tracebackG。发生器恢复到其yield表达式,但是yield 没有计算出一个值,而是提高了throw传递的TypeError。结果回溯在failingGenerator内终止。从回溯中不太清楚的是TypeError从tracebackG.throw上升。这是有意义的:调用throw导致了生成器的恢复,这又引发了TypeError,未处理的异常返回调用堆栈是很自然的。
一个名为catchingG的新生成器演示了当failingGenerator的except方块遇到ValueError时会发生什么。正如所料,yield引发了传递给throw的异常,正如 Python 的异常处理所料,except块捕获了ValueError并输出了它的消息。然而,没有进一步的yield来暂停发电机,所以这次throw产生一个StopIteration来指示failingGenerator的耗尽。
带内联回调的异步编程
生成器暂停和恢复执行对应于Deferred执行回调和错误返回:
-
当到达一个
yield表达式时,生成器暂停其执行,而当一个返回另一个Deferred时,Deferred暂停其回调和出错; -
暂停的生成器可以通过它的
send方法用一个值恢复,而等待另一个Deferred的Deferred在那个Deferred解析为一个值时恢复执行它的回调; -
暂停的生成器可以通过它的
throw方法接收和捕获异常,而等待另一个Deferred的Deferred在那个Deferred解决了异常时继续执行它的 errbacks。
通过比较以下两个代码示例,我们可以看到这些等效性的作用:
def requestFieldDeferred(url, field):
d = nonblockingGet(url)
def onCompletion(response):
document = json.load(response)
return jsonDoc[field]
def onFailure(failure):
failure.trap(UnicodeDecodeError)
d.addCallack(onCompletion)
d.addErrback(onFailure)
return d
def requestFieldGenerator(url, field):
try:
document = yield nonblockingGet(url)
except UnicodeDecodeError:
pass
document = json.load(response)
return jsonDoc[field]
requestFieldDeferred给nonblockingGet的响应Deferred附加一个回调,将响应解码为 JSON 并提取一个属性,以及一个 errback,只隐藏UnicodeDecodeError s
requestFieldGenerator反而产生nonblockingGet的Deferred。然后,当响应可用时,生成器可以恢复响应,或者如果发生异常,则恢复异常。callback 和 errback 都被移到了调用nonblockingGet的同一个作用域中。将函数体移入调用者被称为内联。
我们不能像写的那样使用requestFieldGenerator: Python 2 不允许生成器返回值,我们需要一个包装器来接受yield ed Deferred,并在Deferred解析为值或异常时安排调用生成器的send或throw。
Twisted 在twisted.internet.defer.inlineCallbacks中提供了这个包装器。它修饰返回生成器的可调用程序,并在每个产出的Deferred解析为一个值或异常时调用send和throw。反过来,调用修饰的生成器函数或方法的调用者会收到一个Deferred,而不是一个生成器对象。这确保了期望Deferreds的现有 API 与inlineCallbacks无缝协作。
这是我们用inlineCallbacks装饰的requestFieldGenerator:
from twisted.internet import defer
@defer.inlineCallbacks
def requestFieldGenerator(url, field):
try:
document = yield nonblockingGet(url)
except UnicodeDecodeError:
pass
document = json.load(response)
defer.returnValue(jsonDoc[field])
def someCaller(url, ...):
requestFieldDeferred = requestFieldGenerator(url,"someProperty")
...
returnValue函数抛出一个包含其参数的特殊异常;inlineCallbacks捕捉到这个,并安排用那个值回调requestFieldGenerator。Python 3 中的一个return语句引发了一个等价的异常,inlineCallbacks也会捕获它,所以在只在 Python 3 下运行的代码中returnValue是不必要的。
通过将代码从回调和错误返回到单个局部范围,生成器使得异步 Twisted 程序读起来就像同步的一样。短程序尤其受益于随之而来的函数定义的减少和更清晰的控制流。
发电机用熟悉来换取新的困难。最关键的是,生成器函数或方法的调用者不可能知道返回的生成器对象是使用用send发送给它的值,还是默默地忽略它。例如,这两个发生器提供相同的接口:
def listensToSend():
a = 1
b = yield a
print(a+b)
def ignoresSend():
a = 1
yield a
print(a)
意外地用ignoreSend替换listensToSend会导致一个难以诊断的细微错误。两者都是有效的 Python 代码,适用于不同的环境:listensToSend允许带值的恢复,使其适用于inlineCallbacks,而ignoreSend只是产生一个值,适合于对文件中的行进行操作的处理管道。Python 生成器 API 模糊了这两个不同的用例。
幸运的是,Python 3 的最新版本提供了为inlineCallbacks风格的生成器量身定制的新语法。
Python 中的协同程序
在计算机科学中,生成器是协同程序的一个特例,它可以挂起自己,并将执行传递给任何其他协同程序,当它们接收到返回值时继续执行。我们的inlineCallbacks修饰生成器类似于协程,因为它可以产生和接收值,但是它不像协程,它不能像调用任何其他函数那样直接调用另一个生成器。相反,它需要inlineCallbacks内部的机器来代表它将执行任务交给另一个生成器。这个机器管理执行代码的请求,并将结果返回给请求者,被称为蹦床。为了理解其中的原因,想象一下执行就好像在不同的发生器之间反弹inlineCallbacks。
产自的协同程序
Python 3.3 引入了一种新的语法,允许一个生成器直接将其执行委托给另一个生成器:yield from。以下 Python 3.3+专用代码演示了从另一个生成器生成的生成器的行为:
图 2-3
执行和数据流入流出e和f。执行向下移动,而数据流沿着实线箭头。
>>> def e():
... a = yield 1
... return a + 2
...
>>> def f():
... print("Begin f")
... c = yield from e()
... print(c)
...
>>> g = f()
>>> g.send(None)
Begin f
1
>>> g.send(2)
4
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
生成器e的行为与上一节中描述的生成器函数完全一样:如果我们调用它,我们将通过调用它的next(或者传递它的send方法None)来启动返回的生成器,这将返回 1,它的yield的操作数;然后,我们可以用send将值传递回生成器,它会将操作数返回给下一个yield表达式或者返回给 return 语句(记住,在 Python 3 中,生成器可以返回值)。
由f 返回的生成器g从产生由e返回的生成器,暂停以允许子生成器执行。对g发出的next、send和throw调用被代理到底层的e生成器,因此生成器g看起来是一个e生成器。在图 2-3 中,方框 A 表示开始执行g的初始g.send(None)。执行通过f()的yield from移动到由e()返回的生成器,暂停在e主体内的yield表达式上,该表达式将1发送回g.send(None)。
当子生成器终止时,用yield from将执行委托给另一个生成器的生成器重新获得控制。图 2-3 中的框 B 表示对g.send(2)的第二次调用,该调用通过暂停的f生成器将值2传递给子生成器e,子生成器恢复并将2赋给变量a。执行进行到return语句,并且e子生成器以值4退出。现在f在其yield from表达式的左侧重新开始,并将接收到的4赋给变量c。在print调用之后,没有进一步的yield或yield from表达式,因此f()终止,导致g.send(2)引发StopIteration错误。
这种语法不需要像inlineCallbacks这样的蹦床将调用从一个生成器分派到另一个生成器,因为它允许生成器直接将执行委托给其他生成器。有了yield from,Python 生成器的行为就像真正的协程一样。
协同程序异步和等待
不幸的是,yield from仍然遭受着和yield一样的不确定性:接受值和忽略值的生成器对于调用代码来说是一样的。Python 以后的版本通过在区分协程的yield from:async和await之上引入新的语法特性来解决这种歧义。
当应用于一个函数或方法定义时,async标记将那个函数或方法变成一个协程:
>>> async def function(): pass
...
>>> c = function()
>>> c
<coroutine object function at 0x9876543210>
与生成器不同,协程不能迭代:
>>> list(function())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'coroutine' object is not iterable
>>> next(function())
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: 'coroutine' object is not iterable
像生成器一样,协程有 send 和 throw 方法,调用者可以用这些方法恢复它们:
>>> function().send(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
>>> function().throw(Exception)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 1, in function
Exception
协程可以等待其他协程,语义与从其他生成器生成的生成器相同:
>>> async def returnsValue(value):
... return 1
...
>>> async def awaitsCoroutine(c):
... value = await c
... print(value)
...
>>> awaitsCoroutine(returnsValue(1)).send(None)
1
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
这种行为展示了协同程序合成的先决条件,但是await做一些立即返回值的事情并不能激发它们在异步编程中的使用。我们需要能够向一个暂停的协程发送一个任意值,但是因为async和await的目的是呈现一个与普通生成器不兼容的 API,我们既不能像yield from那样await一个普通生成器,也不能像yield那样省略它的操作数:
>>> def plainGenerator():
... yield 1
...
>>> async def brokenCoroutineAwaitsGenerator():
... await plainGenerator()
...
>>> brokenCoroutineAwaitsGenerator().send(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in brokenCoroutineAwaitsGenerator
TypeError: object generator can't be used in 'await' expression
>>> async def brokenCoroutineAwaitsNothing():
... await
File "<stdin>", line 2
await
^
SyntaxError: invalid syntax
为了学习如何用值恢复协程,我们回到yield from。我们之前的例子为yield from提供了另一个生成器,因此对包装生成器的send和throw方法的调用被代理到内部生成器。可能有许多生成器,每个生成器都通过yield from将执行委托给继任者,但是在底层,必须有一些东西产生向上的价值。例如,考虑一组五个发电机,如图 2-4 所示。
图 2-4
一堆发电机。g1到g4已经向下委托g5执行。
>>> def g1(): yield from g2
...
>>> def g2(): yield from g3
...
>>> def g3(): yield from g4
...
>>> def g4(): yield from g5
...
>>> def g5(): yield 1
g1、g2、g3和g4不能取得任何进展,直到g5产生一个将从g4传播到g1的值。g5不必是发电机,但是;如下例所示,yield from只需要一个可迭代对象来推进它的生成器:
>>> def yieldsToIterable(o):
... print("Yielding from object of type", type(o))
... yield from o
...
>>> list(yieldsToIterable(range(3)))
Yielding from object of type <class 'range'>
[0, 1, 2]
yieldsToIterable将执行委托给它的参数,在本例中是一个range对象。通过构建一个列表来迭代yieldsToIterable生成器,演示了range对象就像生成器一样接管迭代。
用async def定义的协程与yield from共享它们的实现,因此通过适当的步骤,它们也可以await特殊类型的可迭代程序和生成器。
与前面的例子显示的相反,只要生成器被用types.coroutine装饰器标记为协程,它们就可以被等待。使用这种修饰生成器的协程接收该生成器的返回值:
>>> import types
>>> @types.coroutine
... def makeBase():
... return (yield "hello from a base object")
...
>>> async def awaitsBase(base):
... value = await base
... print("From awaitsBase:", value)
...
>>> awaiter = awaitsBase(makeBase())
>>> awaiter.send(None)
'hello from a base object'
>>> awaiter.send("the result")
From awaits base: the result
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
StopIteration
用send(None)启动awaitsBase协程跳转到base生成器的yield语句,并遵循生成器的典型执行路径,返回"hello from base object."现在协程已将执行委托给base,因此send("the result")用该字符串恢复base。base立即returns这个值,这导致协程的await解析到它的值。
如果 Iterable 对象实现了一个返回迭代器的特殊的__await__方法,也可以等待它。这个迭代器的最终值——也就是说,无论它最后产生什么或者包装在一个StopIteration异常中——都将成为传递给await的结果。一个符合这个接口的物体被说成是类未来。当我们稍后探索asyncio时,我们将看到它的Future提供了这个接口,因此授予它它们的名字。
一个类似未来的对象的简单实现演示了控制流:
class FutureLike(object):
_MISSING="MISSING"
def __init__(self):
self.result = self._MISSING
def __next__(self):
if self.result is self._MISSING:
return self
raise StopIteration(self.result)
def __iter__(self):
return self
def __await__(self):
return iter(self)
async def awaitFutureLike(obj):
result = await obj
print(result)
obj = FutureLike()
coro = awaitFutureLike(obj)
assert coro.send(None) is obj
obj.result = "the result"
try:
coro.send(None)
except StopIteration:
pass
FutureLike的实例是可迭代的,因为它们的__iter__方法返回一个本身具有__next__方法的对象。在这种情况下,迭代一个FutureLike实例将一遍又一遍地产生同一个实例,直到它的result属性被设置,这时它将引发一个包含该值的StopIteration异常。这相当于来自发电机的returning值。
FutureLike的实例也是类似未来的,因为它们的__await__方法返回一个迭代器,所以awaitFutureLike可以await一个FutureLike的实例。通常,协程从send(None)开始。这将返回awaitFutureLike协程await s 的FutureLike实例,这是我们传递给它的同一个实例。设置FutureLike对象的result属性允许我们通过将其 await 解析为一个值来恢复协程,协程接收结果,打印结果,然后以一个StopIteration异常终止。
注意,第二个coro.send调用也将None传递给协程。协程,await类未来对象解析为这些对象的迭代器提供的最后一个值。它们仍然必须被恢复以利用这些值,但是它们必然会忽略它们的send方法的参数。
Twisted 提供了一个可适应的对象和一个协程适配器,这样协程和现有的 API 就可以无缝地交互。正如我们所见,协程与asyncio是完全分离的,所以我们在本节讨论的 Twisted API 不足以集成两者。我们将在下一章学习必要的附加 API。
等待延期
从 Twisted 16.4.0 开始,延迟是类似未来的对象,提供了一致的__next__、__iter__和__await__方法。这允许我们用一个Deferred替换前面代码中的FutureLike:
from twisted.internet.defer import Deferred
async def awaitFutureLike(obj):
result = await obj
print(result)
obj = Deferred()
coro = awaitFutureLike(obj)
assert coro.send(None) is obj
obj.callback("the result")
try:
coro.send(None)
except StopIteration:
pass
awaiting一个Deferred解析为Deferred在其正常回调和错误返回处理循环后做的任何事情:
>>> from twisted.internet.defer import Deferred
>>> import operator
>>> d = Deferred()
>>> d.addCallback(print, "was received by a callback")
<Deferred at 0x7eff85886160>
>>> d.addCallback(operator.add, 2)
<Deferred at 0x7eff85886160>
>>> async def awaitDeferred():
... await d
...
>>> g = awaitDeferred()
>>> g.send(None)
<Deferred at 0x7eff85886160>
>>> d.callback(1)
1 was received by a callback
>>> g.send(None)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in awaitDeferred
File "twisted/src/twisted/internet/defer.py", line 746, in send
raise result.value
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'
我们的Deferred的print回调运行,但返回None,导致第二次回调失败,当它试图向第一个参数添加 2 时,返回TypeError。恢复的协程因此失败,并且TypeError存储在Deferred中。
在这种情况下,协程和Deferreds的组合暴露了一个 bug,但是测试的代码路径表明错误和数据在两者之间自然地流动。
允许我们在协程中调用 Twisted 的 API,但是如果我们想让 Twisted 的 API 使用我们的协程呢?
使用 ensureDeferred 延迟的协同例程
Twisted 可以用Deferreds包装协程,允许期望Deferreds的 API 接受协程。
twisted.internet.defer. ensureDeferred接受一个协程对象并返回一个 Deferred,当协程返回一个:
>>> from twisted.internet.defer import Deferred, ensureDeferred
>>> async def asyncIncrement(d):
... x = await d
... return x + 1
...
>>> awaited = Deferred()
>>> addDeferred = ensureDeferred(asyncIncrement(awaited))
>>> addDeferred.addCallback(print)
<Deferred at0x12345>
>>> awaited.callback(1)
2
>>>
我们的协程asyncIncrement awaits是一个解析为一个数字的对象,然后返回这个数字和 1 的和。我们用ensureDeferred将它转换成一个Deferred,分配给addDeferred,然后给它添加一个print回调。回调asyncIncrement等待的awaited Deferred依次回调ensureDeferred返回的addDeferred Deferred,而不需要我们调用send。换句话说,addDeferred的行为与手动构建的Deferred相同。异常传播也以同样的方式工作:
>>>from twisted.internet.defer import Deferred, ensureDeferred
>>> async def asyncAdd(d):
... x = await d
... return x + 1
...
>>>awaited = Deferred()
>>>addDeferred = ensureDeferred(asyncAdd(awaited))
>>>addDeferred.addErrback(print)
Unhandled error in Deferred:
<Deferred at0x7eff857f0470>
>>>awaited.callback(None)
[Failure instance: Traceback:< class 'TypeError'>: ...
...
<stdin>:3:asyncAdd
]
协同程序比Deferred管理的回调更类似于同步代码,Twisted 使得使用协同程序变得足够容易,以至于你可能会怀疑Deferred是否会带来麻烦。一个显而易见的答案是它们已经被使用过了;许多 Twisted 的代码使用了Deferred,所以即使你很少使用它们,你仍然需要熟悉它们。不使用协程的另一个原因是,您必须编写在 Python 2 上运行的代码。随着 Python 2 寿终正寝,这已经不再是一个问题,PyPy,一个替代的 Python 运行时,它的实时(JIT)编译器可以极大地提高纯 Python 代码的速度,扩展了它们对 Python 3 的支持。
然而,为什么 Twisted 的Deferreds在一个后协同程序的世界里仍然有价值,还有一些不太明显但更持久的原因。
多路传输延迟
如果我们想要两个异步操作的结果,其中一个可能在另一个之前完成,会发生什么?例如,假设我们编写一个程序,同时发出两个 HTTP 请求:
def issueTwo(url1, url2):
urlDeferreds = [retrieveURL(url1), retrieveURL(url2)]
...
协程会让我们依次等待每一个:
async def issueTwo(url1, url2):
urlDeferreds = [retrieveURL(url1), retrieveURL(url2)]
for d in urlDeferreds:
result = await d
doSomethingWith(result)
当issueTwo await完成其中一个的时候,反应堆将继续回收url1和url2;等待url1的回收完成并不妨碍反应堆回收url2。这种并发性确实是异步和事件驱动编程的要点!
然而,随着操作变得更加复杂,这种效率变得不那么重要了。假设我们只想要首先检索的 URL。我们不能只使用await来写一个fastestOfTwo协程,因为我们不知道先给哪个await。只有反应器知道指示协程值准备就绪的底层事件何时发生,并且如果我们只有协程,事件循环将不得不暴露同步原语,该原语同时等待多个协程并检查是否所有协程都已完成。
幸运的是,无需特殊的反应堆级同步机制,多个Deferreds就可以轻松复用成单个Deferred。最简单地说,twisted.internet.defer.DeferredList是一个Deferred,它接受一个延期列表,并在所有这些Deferreds都有值时回调自己。
考虑以下代码:
>>> from twisted.internet.defer import Deferred, DeferredList
>>> url1 = Deferred()
>>> url2 = Deferred()
>>> urlList = DeferredList([url1, url2])
>>> urlList.addCallback(print)
<Deferred at 0x123456>
>>> url2.callback("url2")
>>> url1.callback("url1")
[(True, "url1)", (True, "url2")]
DeferredList urlList 包装了两个url1和url2 Deferreds,并有一个print函数作为自己的回调函数。该回调仅在url1和url2都被回调后运行,因此urlList与上面的issueTwo协程中的全有或全无同步相匹配。
第一个线索是DeferredList更强大的特性集在于它返回给回调函数的list。每个元素是一个长度为 2 的tuple;第二个元素显然是传入的list中同一索引处的Deferred的值,所以索引 0 的第二个元组成员是"url1",对应于索引 0 处的url1 Deferred。
tuple中的第一项表示Deferred是否成功终止。url1和url2都解析为字符串,而不是Failures,因此结果列表中相应的索引将True作为它们的第一个元素。
导致至少一个DeferredList的Deferreds失败演示了Failures是如何通信的:
>>> succeeds = Deferred()
>>> fails = Deferred()
>>> listOfDeferreds = DeferredList([succeeds, fails])
>>> listOfDeferreds.addCallback(print)
<Deferred at 0x1234567>
>>> fails.errback(Exception())
>>> succeeds.callback("OK")
[(True, 'OK'), (False, <twisted.python.failure.Failure builtins.Exception: >)]
现在,返回列表中的第二个元组将False作为其第一个元素,并将代表导致其Deferred失败的Exception的Failure作为其第二个项目。
这个特殊的(success,value or Failure)对列表通过使用Failures的回溯捕获工具保留了所有可能的信息。作为这种方法带来的灵活性的一个例子,DeferredList的用户可以在一次回调中轻松地过滤聚合结果。
有了DeferredList的基本行为,我们可以研究允许我们实现fastestOfTwo : fireOnOneCallback的附加特性。
当list中的任何一个Deferreds有值时,fireOnOneCallback选项指示DeferredList回调自己:
>>> noValue = Deferred()
>>> getsValue = Deferred()
>>> waitsForOne = DeferredList([noValue, getsValue], fireOnOneCallback=True)
>>> waitsForOne.addCallback(print)
<Deferred at 0x12345678>
>>> getsValue.callback("the value") ('the value', 1)
现在,当只有getsValue Deferred解析为一个值时,waitsForOne的print回调就会运行。传递给回调函数的值DeferredList也是一个长度为 2 的tuple,但是这一次,第一项是对应的Deferred解析到的值,而第二项是它在列表中的索引。getsValue用"the value,"回调,它是我们传递给DeferredList的列表中的第二个项目,所以回调接收("the value," 1)作为结果。
我们现在可以实现fastestOfTwo:
def fastestOfTwo(url1, url2):
def extractValue(valueAndIndex):
value, index = valueAndIndex
return value
urlList = DeferredList([retrieveURL(url1), retrieveURL(url2)],
fireOnOneCallback=True,
fireOnOneErrback=True)
return urlList.addCallback(extractValue)
DeferredList也允许用fireOnOneErrback模拟多路传输错误。在第一个错误时触发DeferredList并展开它的值是一种常见的模式,Twisted 在twisted.internet.defer.gatherResults中提供了一个方便的包装器:
>>> from twisted.internet.defer import Deferred, gatherResults
>>> d1, d2 = Deferred(), Deferred()
>>> results = gatherResults([d1, d2])
>>> results.addCallback(print)
<Deferred at 0x123456789>
>>> d1.callback(1)
>>> d2.callback(2)
>>> [1, 2]
>>> d1, d2 = Deferred(), Deferred()
>>> fails = gatherResults([d1, d2])
>>> fails.addErrback(print)
<Deferred at 0x1234567890>
>>> d1.errback(Exception())
[[Failure instance: Traceback ...: <class 'Exception'>: ]]
回想一下,Failure的__str__方法返回一个以[]开始和结束的字符串,因此打印出的失败出现了两组括号:一组来自其__str__,另一组来自其包含的list。
还要注意的是gatherResults等待所有成功Deferreds,所以它不能用于fastestOfTwo
DeferredList和gatherResults提供了允许复杂行为但隐含分支的高级 APIs 每个选项的输出取决于它们自己的选项和它们包装的Deferred的输出之间的相互作用。任何一个方面的改变都可能导致意想不到的输出,从而产生令人不快的 bug。
这超出了Deferred s 的一般间接性:因为Deferred.callback几乎总是由反应器调用,而不是由间接操纵套接字的代码用户代码调用,所以在异常的来源和它的最终原因之间可能存在差距。
Twisted 通过提供对测试的特殊支持解决了异步代码固有的困难。
测试延期
在前一章中,我们看到 Twisted 的trial.unittest包提供了一个SynchronousTestCase,它的 API 模仿了unittest.TestCase的。事实上,SynchronousTestCase的 API 是unittest.TestCase的超集,它的附加特性的重要部分涉及到关于Deferred的断言
我们可以通过为上一节定义的fastestOfTwo函数编写测试来探索这些特性。首先,我们将把它一般化,接受任意两个Deferreds,而不是检索 URL 本身:
def fastestOfTwo(d1, d2):
def extractValue(valueAndIndex):
value, index = valueAndIndex
return value
urlList = DeferredList([d1, d2],
fireOnOneCallback=True,
fireOnOneErrback=True)
return urlList.addCallback(extractValue)
我们可以为这个新版本的fastestOfTwo编写的第一个测试断言,当它的两个Deferreds都没有解析为值时,它返回的Deferred没有解析为值:
from twisted.internet import defer
from twisted.trial import unittest
class FastestOfTwoTests(unittest.SynchronousTestCase):
def test_noResult(self):
d1 = defer.Deferred()
self.assertNoResult(d1)
d2=defer.Deferred()
self.assertNoResult(d2)
self.assertNoResult(fastestOfTwo(d1, d2))
顾名思义,synchronoustestcase . assertnoresult 断言它所传递的延迟没有结果,这是一个很有价值的工具,可以确保执行符合您的预期。
然而,当它们确实有结果时,是最有用的。在fastestOfTwo的情况下,我们期望返回的Deferred取两个Deferreds中第一个的值:
def test_resultIsFirstDeferredsResult(self):
getsResultFirst = defer.Deferred()
neverGetsResult = defer.Deferred()
fastestDeferred = fastestOfTwo(getsResultFirst, neverGetsResult)
self.assertNoResult(fastestDeferred)
result = "the result"
getsResultFirst.callback(result)
actualResult = self.successResultOf(fastestDeferred)
self.assertIs(result, actualResult)
SynchronousTestCase.successResultOf要么返回Deferred的当前结果,要么导致其测试失败。我们的测试在用它回调getsResultFirst之后,用它从fastestDeferred中提取"the result",这样测试可以断言fastestOfTwo确实返回了第一个可用的结果。
注意,在我们回调getsResultFirst之前,我们仍然断言fastestOfTwo返回的Deferred没有结果。鉴于test_noResult已经做出了这个断言,这看起来可能是多余的,但是请记住,在您的代码添加回调或错误返回之前,可以回调Deferred s。在这种情况下,fastestOfTwo可能会错误地返回一个已经用the result回调的Deferred,而忽略传入的Deferred s,然而我们的测试仍然会通过。这在如此简单的代码中是不太可能的,但是当 a Deferred得到结果时,关于的隐含假设可能会潜入代码中,导致测试忽略 bug。断言Deferred实际上处于给定的状态是一种好的做法,而不是假设这样以避免这些错误,并且更好的做法是针对已经有结果的Deferred和没有结果的Deferred来测试您的代码。
我们可以添加一个测试,断言即使在Deferred已经触发时fastestOfTwo也能工作:
def test_firedDeferredIsFirstResult(self):
result = "the result"
fastestDeferred = fastestOfTwo(defer.Deferred(),
defer.succeed(result))
actualResult = self.successResultOf(fastestDeferred)
self.assertIs(result, actualResult)
twisted.internet.defer.succeed函数接受一个参数并返回一个Deferred,这个参数会立即被回调,所以fastestOfTwo的第二个参数是一个Deferred,在任何fastestOfTwo运行之前,它已经被用the result回调了。
为了完整起见,我们还可以测试当fastestOfTwo收到两个已经被回调的Deferreds时会发生什么:
def test_bothDeferredsFired(self):
first = "first"
second = "second"
fastestDeferred = fastestOfTwo(defer.succeed(first),
defer.succeed(second))
actualResult = self.successResultOf(fastestDeferred)
self.assertIs(first, actualResult)
底层的DeferredList将其内部处理回调按顺序添加到其列表中的每个Deferreds中。有了fireOnOneCallback=True,列表中最早有结果的Deferred回调代表列表的Deferred。在我们的测试中,我们期望first是回调fastestDeferred的值。
错误处理是测试的关键部分,所以我们对fastestDeferred的测试也应该测试它如何处理Failure。为了保持测试简短,我们将只展示在Deferred被传递到fastestOfTwo之前失败的情况:
def test_failDeferred(self):
class ExceptionType(Exception):
pass
fastestDeferred = fastestOfTwo(defer.fail(ExceptionType()),
defer.Deferred())
failure = self.failureResultOf(fastestDeferred)
failure.trap(defer.FirstError)
failure.value.subFailure.trap(ExceptionType)
像SynchronousTestCase.successResultOf,SynchronousTestCase.failureResultOf从一个Deferred返回当前的Failure;如果Deferred还没有被调用或者没有-Failure结果,failureResultOf导致测试失败。
因为返回的对象是一个Failure,所以我们可以在 errbacks 中使用的所有方法和属性在我们的测试中都是可用的。DeferredList用fireOnOneErrback=True将失败包装在twisted.internet.defer.FirstError异常中,所以我们在测试中使用了trap这种类型;如果Failure包装了任何其他异常,trap将再次引发它。导致FirstError的底层Failure在其subFailure属性上是可访问的,并且由于我们传入了ExceptionType的一个实例,我们trap断言第一个Deferred由于预期的原因而失败。
使用successResultOf和failureResultOf的assertNoResult鼓励使用关于Deferred状态的显式假设来编写测试。正如fastestOfTwo所展示的,即使是对Deferred的简单使用也必须进行隐式排序依赖和错误处理测试。这些也是协程和任何其他并发原语的关注点。Twisted 的测试套件自然拥有在Deferred环境中处理常见并发问题的最佳工具。
摘要
这一章通过解释事件处理程序是一种_callback_的方式,继承了上一章未完成的事件驱动编程。由于延续传递式的理论力量,非常复杂的程序可以用回调来表达。回调通过直接调用其他回调来传递值,而不是返回给它们的调用者。我们将这种组合命名为内部组合,因为它发生在每个回调的主体中。
内部组合使得维护回调驱动的程序变得困难:每个回调都必须知道它的后继者的名字和签名,这样它才能调用它。对一系列回调进行重新排序或消除一个回调可能需要修改几个回调。一个解决方案在于异步编程的范例,它允许程序在所有输入准备好之前继续运行。代表异步结果的占位符值可以收集回调,然后在真实值可用时运行它们。这个占位符允许回调返回值,从而在外部组合*,反过来使得逻辑单元不知道它们是如何以及在哪里被使用的。使用这些异步占位符的事件驱动代码可以像非回调驱动代码一样被分解。*
*Twisted 的异步占位符值是Deferred。我们看到Deferred在一个循环中运行它们的回调,将一个的结果传递给下一个,并在任何异常时调用错误处理程序或错误返回。?? 内部的这个处理循环使它们成为强大的控制流抽象 ??。
控制流抽象的一个重要部分是以不同的方式响应不同的错误。Twisted 的Failure类捕获回溯信息以及引发的异常,并公开允许 errbacks 过滤和重新引发异常的实用方法。我们看到了回调和错误返回如何完全代表使用try和except的同步代码。
就像回调允许组合一样,它们自己组合。当回调或错误返回一个Deferred时,该回调或错误自身的Deferred暂停其执行,直到新的Deferred完成。这意味着返回Deferred的函数和方法可以被用作回调和错误返回,而不需要开发人员做任何特别的努力。
尽管Deferred功能强大,但它们并不是组成异步动作的唯一方式。Python 的生成器可以暂停它们的执行,并在从外部来源接收到值后继续执行。这个控制流映射到由延迟器提供的控制流,回调和错误可以通过使用inlineCallbacks转移到生成器中。
然而,生成器是不明确的,因为它们可能表示简单的迭代器或类似于Deferred的控制流。Python 3.5 增加了对协程的特殊支持,这些协程是以控制流为中心的生成器,可以通过将执行委托给其他协程来挂起自己,而不需要inlineCallbacks。协程可以await直接 TwistedDeferred的,可以用ensureDeferred变成Deferred的。这些 API 允许 Twisted 无缝地使用协程。
不是所有的程序都可以用协程直接表达:我们的fastestOfTwo例子需要同时等待两件事情。幸运的是,DeferredList,一个建立在Deferreds之上的抽象,允许 Twisted 去复用的异步结果。
Twisted 还特别支持测试Deferreds。SynchronousTestCase提供了assertNoResult、successResultOf和failureResultOf,允许测试对Deferred s 的状态做出精确的断言。影响所有原语(协程、生成器和Deferred s)的并发问题可以用这套工具进行测试。****