Python 的 Twisted 事件驱动的网络引擎框架
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:http://blog.csdn.net/crisschan
概述
Twisted 是用 Python 实现的基于事件驱动的网络引擎框架。Twisted 支持许多常见的传输及应用层协议,包括 TCP、UDP、SSL/TLS、HTTP、IMAP、SSH、IRC 以及 FTP。
优越性
使用基于事件驱动的编程模型,而不是多线程模型。
跨平台:为主流操作系统平台暴露出的事件通知系统提供统一的接口。
“内置电池”的能力:提供流行的应用层协议实现,因此 Twisted 马上就可为开发人员所用。
符合 RFC 规范,已经通过健壮的测试套件证明了其一致性。
能很容易的配合多个网络协议一起使用。
可扩展。
事件驱动编程
事件驱动编程是一种编程范式,这里程序的执行流由外部事件来决定。它的特点是包含一个事件循环,当外部事件发生时使用回调机制来触发相应的处理。另外两种常见的编程范式是(单线程)同步以及多线程编程。事件驱动模型也就是我们常说的观察者,或者发布-订阅模型;理解它的几个关键点:
一种对象间的一对多的关系;最简单的如交通信号灯,信号灯是目标(一方),行人注视着信号灯(多方);
当目标发送改变(发布),观察者(订阅者)就可以接收到改变;
观察者如何处理(如行人如何走,是快走/慢走/不走,目标不会管的),目标无需干涉;所以就松散耦合了它们之间的关系。
下图表现了单线程、多线程和事件驱动的关系:
在单线程同步模型中,任务按照顺序执行。如果某个任务因为 I/O 而阻塞,其他所有的任务都必须等待,直到它完成之后它们才能依次执行。这种明确的执行顺序和串行化处理的行为是很容易推断得出的。如果任务之间并没有互相依赖的关系,但仍然需要互相等待的话这就使得程序不必要的降低了运行速度。
在多线程版本中,这 3 个任务分别在独立的线程中执行。这些线程由操作系统来管理,在多处理器系统上可以并行处理,或者在单处理器系统上交错执行。这使得当某个线程阻塞在某个资源的同时其他线程得以继续执行。与完成类似功能的同步程序相比,这种方式更有效率,但程序员必须写代码来保护共享资源,防止其被多个线程同时访问。多线程程序更加难以推断,因为这类程序不得不通过线程同步机制如锁、可重入函数、线程局部存储或者其他机制来处理线程安全问题,如果实现不当就会导致出现微妙且令人痛不欲生的 bug。
在事件驱动版本的程序中,3 个任务交错执行,但仍然在一个单独的线程控制中。当处理 I/O 或者其他昂贵的操作时,注册一个回调到事件循环中,然后当 I/O 操作完成时继续执行。回调描述了该如何处理某个事件。事件循环轮询所有的事件,当事件到来时将它们分配给等待处理事件的回调函数。这种方式让程序尽可能的得以执行而不需要用到额外的线程。事件驱动型程序比多线程程序更容易推断出行为,因为程序员不需要关心线程安全问题。
什么情况可以使用事件驱动
程序中有许多任务
任务之间高度独立
在等待事件到来时,某些任务会阻塞。
一句话,不需要同步处理的多任务处理就可以使用事件驱动了。
Twisted
Twisted 中的客户端和服务器是用 Python 开发的,采用了一致性的接口。这使得开发新的客户端和服务器变得很容易实现,可以在客户端和服务器之间共享代码,在协议之间共享应用逻辑,以及对某个实现的代码做测试。Twisted 采用了 Reactor 设计模式,其核心就是 Reactor 的事件循环。Reactor 可以感知网络、文件系统以及定时器事件。它等待然后处理这些事件,从特定于平台的行为中抽象出来,并提供统一的接口,使得在网络协议栈的任何位置对事件做出响应都变得简单。
下面以获取一个 URL 页面代码为例,同步调用方式如下:
import getPage
def processPage(page):
print page
def logError(error):
print error
def finishProcessing(value):
print "Shutting down..."
exit(0)
url = "http://google.com"
try:
page = getPage(url)
processPage(page)
except Error, e:
logError(error)
finally:
finishProcessing()
一异步的方式获取如下:
from twisted.internet import reactor
import getPage
def processPage(page):
print page
finishProcessing()
def logError(error):
print error
finishProcessing()
def finishProcessing(value):
print "Shutting down..."
reactor.stop()
url = "http://google.com"
# getPage takes: url,
# success callback, error callback
getPage(url, processPage, logError)
reactor.run()
从上面异步代码段可以看出,如果编写丢失了 reactor.stop()就会进入死循环,Twisted 应对这种复杂性的方式是新增一个称为 Deferred(延迟)的对象。
Deferreds
Deferred 对象以抽象化的方式表达了一种思想,即结果还尚不存在。它同样能够帮助管理产生这个结果所需要的回调链。Deferred 对象包含一对回调链,一个是针对操作成功的回调,一个是针对操作失败的回调。初始状态下 Deferred 对象的两条链都为空。在事件处理的过程中,每个阶段都为其添加处理成功的回调和处理失败的回调。当一个异步结果到来时,Deferred 对象就被“激活”,那么处理成功的回调和处理失败的回调就可以以合适的方式按照它们添加进来的顺序依次得到调用。
异步版 URL 获取器采用 Deferred 代码片段如下:
from twisted.internet import reactor
import getPage
def processPage(page):
print page
def logError(error):
print error
def finishProcessing(value):
print "Shutting down..."
reactor.stop()
url = "http://google.com"
deferred = getPage(url) # getPage returns a Deferred
deferred.addCallbacks(success, failure)
deferred.addBoth(stop)
reactor.run()
Deferred 对象创建时包含两个添加回调的阶段。第一阶段,addCallbacks 将 processPage 和 logError 添加到它们各自归属的回调链中。然后 addBoth 再将 finishProcessing 同时添加到这两个回调链上。Deferred 对象只能被激活一次,如果试图重复激活将引发一个异常.用图解的方式来看,回调链应该如图所示:
Transports
Transports 代表网络中两个通信结点之间的连接。Transports 负责描述连接的细节,比如连接是面向流式的还是面向数据报的,流控以及可靠性。TCP、UDP 和 Unix 套接字可作为 transports 的例子。Transports 实现了 ITransports 接口
write 以非阻塞的方式按顺序依次将数据写到物理连接上
writeSequence 将一个字符串列表写到物理连接上
loseConnection 将所有挂起的数据写入,然后关闭连接
getPeer 取得连接中对端的地址信息
getHost 取得连接中本端的地址信息
Protocols
Protocols 描述了如何以异步的方式处理网络中的事件。HTTP、DNS 以及 IMAP 是应用层协议中的例子。Protocols 实现了 IProtocol 接口,它包含如下的方法:
makeConnection 在 transport 对象和服务器之间建立一条连接
connectionMade 连接建立起来后调用
dataReceived 接收数据时调用
connectionLost 关闭连接时调用
详细的 reactor、transport、protocols 例子
运行服务器端脚本将启动一个 TCP 服务器,监听端口 8000 上的连接。服务器采用的是 Echo 协议,数据经 TCP transport 对象写出。运行客户端脚本将对服务器发起一个 TCP 连接,回显服务器端的回应然后终止连接并停止 reactor 事件循环。这里的 Factory 用来对连接的双方生成 protocol 对象实例。两端的通信是异步的,connectTCP 负责注册回调函数到 reactor 事件循环中,当 socket 上有数据可读时通知回调处理。
服务器部分
from twisted.internet import protocol, reactor
class Echo(protocol.Protocol):
def dataReceived(self, data):
# As soon as any data is received, write it back
self.transport.write(data)
class EchoFactory(protocol.Factory):
def buildProtocol(self, addr):
return Echo()
reactor.listenTCP(8000, EchoFactory())
reactor.run()
客户端部分
from twisted.internet import reactor, protocol
class EchoClient(protocol.Protocol):
def connectionMade(self):
self.transport.write("hello, world!")
def dataReceived(self, data):
print "Server said:", data
self.transport.loseConnection()
def connectionLost(self, reason):
print "connection lost"
class EchoFactory(protocol.ClientFactory):
def buildProtocol(self, addr):
return EchoClient()
def clientConnectionFailed(self, connector, reason):
print "Connection failed - goodbye!"
reactor.stop()
def clientConnectionLost(self, connector, reason):
print "Connection lost - goodbye!"
reactor.stop()
reactor.connectTCP("localhost", 8000, EchoFactory())
reactor.run()
Applications
Twisted 是用来创建具有可扩展性、跨平台的网络服务器和客户端的引擎。Applications 基础组件包含 4 个主要部分:服务(Service)、应用(Application)、配置管理(通过 TAC 文件和插件)以及 twistd 命令行程序。为了说明这个基础组件,我们将上一节的 Echo 服务器转变成一个应用。
Service
IService 接口下实现的可以启动和停止的组件。Twisted 自带有 TCP、FTP、HTTP、SSH、DNS 等服务以及其他协议的实现。其中许多 Service 都可以注册到单独的应用中。IService 接口的核心是:
startService 启动服务。可能包含加载配置数据,设定数据库连接或者监听某个端口
stopService 关闭服务。可能包含将状态保存到磁盘,关闭数据库连接或者停止监听端口
Application
Application 是处于最顶层的 Service,代表了整个 Twisted 应用程序。Service 需要将其自身同 Application 注册,然后就可以用下面我们将介绍的部署工具 twistd 搜索并运行应用程序。我们将创建一个可以同 Echo Service 注册的 Echo 应用。
TAC 文件
当在一个普通的 Python 文件中管理 Twisted 应用程序时,需要由开发者负责编写启动和停止 reactor 事件循环以及配置应用程序的代码。在 Twisted 的基础组件中,协议的实现都是在一个模块中完成的,需要使用到这些协议的 Service 可以注册到一个 Twisted 应用程序配置文件中(TAC 文件)去,这样 reactor 事件循环和程序配置就可以由外部组件来进行管理。
要将我们的 Echo 服务器转变成一个 Echo 应用,我们可以按照以下几个简单的步骤来完成:
将 Echo 服务器的 Protocol 部分移到它们自己所归属的模块中去。
在 TAC 文件中:
- 创建一个 Echo 应用。
- 创建一个 TCPServer 的 Service 实例,它将使用我们的 EchoFactory,然后同前面创建的应用完成注册。
管理 reactor 事件循环的代码将由 twistd 来负责,我们下面会对此进行讨论。这样,应用程序的代码就变成这样了:
echo.py 文件:
from twisted.internet import protocol, reactor
class Echo(protocol.Protocol):
def dataReceived(self, data):
self.transport.write(data)
class EchoFactory(protocol.Factory):
def buildProtocol(self, addr):
return Echo()
twistd
twistd(读作“twist-dee”)是一个跨平台的用来部署 Twisted 应用程序的工具。它执行 TAC 文件并负责处理启动和停止应用程序。作为 Twisted 在网络编程中具有“内置电池”能力的一部分,twistd 自带有一些非常有用的配置标志,包括将应用程序转变为守护进程、定义日志文件的路径、设定特权级别、在 chroot 下运行、使用非默认的 reactor,甚至是在 profiler 下运行应用程序。
我们可以像这样运行这个 Echo 服务应用:
twistd –y echo_server.tac
在这个简单的例子里,twistd 将这个应用程序作为守护进程来启动,日志记录在 twistd.log 文件中。启动和停止应用后,日志文件内容如下:
2011-11-19 22:23:07-0500 [-] Log opened.
2011-11-19 22:23:07-0500 [-] twistd 11.0.0 (/usr/bin/python 2.7.1) starting up.
2011-11-19 22:23:07-0500 [-] reactor class: twisted.internet.selectreactor.SelectReactor.
2011-11-19 22:23:07-0500 [-] echo.EchoFactory starting on 8000
2011-11-19 22:23:07-0500 [-] Starting factory <echo.EchoFactory instance at 0x12d8670>
2011-11-19 22:23:20-0500 [-] Received SIGTERM, shutting down.
2011-11-19 22:23:20-0500 [-] (TCP Port 8000 Closed)
2011-11-19 22:23:20-0500 [-] Stopping factory <echo.EchoFactory instance at 0x12d8670>
2011-11-19 22:23:20-0500 [-] Main loop terminated.
2011-11-19 22:23:20-0500 [-] Server Shut Down.
通过使用 Twisted 框架中的基础组件来运行服务,这么做使得开发人员能够不用再编写类似守护进程和记录日志这样的冗余代码了。这同样也为部署应用程序建立了一个标准的命令行接口。
#Plugins
对于运行 Twisted 应用程序的方法,除了基于 TAC 文件外还有一种可选的方法,这就是插件系统。TAC 系统可以很方便的将 Twisted 预定义的服务同应用程序配置文件注册,而插件系统能够方便的将用户自定义的服务注册为 twistd 工具的子命令,然后扩展应用程序的命令行接口。
在使用插件系统时:
由于只有 plugin API 需要保持稳定,这使得第三方开发者能很容易地扩展软件。
插件发现能力已经集成到系统中了。插件可以在程序首次运行时加载并保存,每次程序启动时会重新触发插件发现过程,或者也可以在程序运行期间反复轮询新插件,这使得在程序已经启动后我们还可以判断是否有新的插件安装上了。
当使用 Twisted 插件系统来扩展软件时,我们要做的就是创建 IPlugin 接口下实现的对象并将它们放到一个特定的位置中,这里插件系统知道该如何去找到它们。
我们已经将 Echo 服务转换为一个 Twisted 应用程序了,而将其转换为一个 Twisted 插件也是非常简单直接的。在我们之前的 Echo 模块中,除了包含有 Echo 协议和 EchoFactory 的定义之外,现在我们还要添加一个名为 twistd 的目录,其中还包含着一个名为 plugins 的子目录,这里正是我们需要定义 echo 插件的地方。通过这个插件,我们可以启动一个 echo 服务,并将需要使用的端口号作为参数指定给 twistd 工具。
from zope.interface import implements
from twisted.python import usage
from twisted.plugin import IPlugin
from twisted.application.service import IServiceMaker
from twisted.application import internet
from echo import EchoFactory
class Options(usage.Options):
optParameters = [["port", "p", 8000, "The port number to listen on."]]
class EchoServiceMaker(object):
implements(IServiceMaker, IPlugin)
tapname = "echo"
description = "A TCP-based echo server."
options = Options
def makeService(self, options):
"""
Construct a TCPServer from a factory defined in myproject.
"""
return internet.TCPServer(int(options["port"]), EchoFactory())
serviceMaker = EchoServiceMaker()
现在,我们的 Echo 服务器将作为一个服务选项出现在 twistd –help 的输出中。运行 twistd echo –port=1235 将在端口 1235 上启动一个 Echo 服务器。
Twisted 还带有一个可拔插的针对服务器端认证的模块 twisted.cred,插件系统常见的用途就是为应用程序添加一个认证模式。我们可以使用 twisted.cred 中现成的 AuthOptionMixin 类来添加针对各种认证的命令行支持,或者是添加新的认证类型。比如,我们可以使用插件系统来添加基于本地 Unix 密码数据库或者是基于 LDAP 服务器的认证方式。
twistd 工具中附带有许多 Twisted 所支持的协议插件,只用一条单独的命令就可以完成启动服务器的工作了。这里有一些通过 twistd 启动服务器的例子:
twistd web –port 8080 –path .
这条命令将在 8080 端口启动一个 HTTP 服务器,在当前目录中负责处理静态和动态页面请求。
twistd dns –p 5553 –hosts-file=hosts
这条命令在端口 5553 上启动一个 DNS 服务器,解析指定的文件 hosts 中的域名,这个文件的内容格式同/etc/hosts 一样。
sudo twistd conch –p tcp:2222
这条命令在端口 2222 上启动一个 SSH 服务器。ssh 的密钥必须独立设定。
twistd mail –E –H localhost –d localhost=emails
这条命令启动一个 ESMTP POP3 服务器,为本地主机接收邮件并保存到指定的 emails 目录下。
我们可以方便的通过 twistd 来搭建一个用于测试客户端功能的服务器,但它同样是可装载的、产品级的服务器实现。
在部署应用程序的方式上,Twisted 通过 TAC 文件、插件以及命令行工具 twistd 的部署方式已经获得了成功。但是有趣的是,对于大多数大型 Twisted 应用程序来说,部署它们仍然需要重写一些这类管理和监控组件;Twisted 的架构并没有对系统管理员的需求呈现出太多的友好性。这也反映了一个事实,那就是对于系统管理员来说 Twisted 历来就没有太多架构可言,而这些系统管理员才是部署和维护应用程序的专家。在这方面,Twisted 在未来架构设计的决策上需要更积极的征求这类专家级用户的反馈意见。
关注我,关注测试
版权声明: 本文为 InfoQ 作者【陈磊@Criss】的原创文章。
原文链接:【http://xie.infoq.cn/article/684cdce7e09e18bf24d50b224】。未经作者许可,禁止转载。
评论