分类: LINUX
2011-12-09 08:15:16
Twisted网络编程必备(一)
0.1 为什么使用Twisted?
如果你并不准备使用Twisted,你可能有很多异议。为什么使用Twisted而不是其他网络函数库或框架?如下是一些充分的理由:
·基于Python
Twisted是使用Python编写的,强壮的、面向对象的解释性语言。Python使它的爱好者充满热情。使用Python编程是一种乐趣,易于编写、易于阅读、易于运行。因为Python是跨平台的,所以可以运行Twisted程序在Linux、Windows、Unix和MAC等等系统上。
·异步和事件驱动的
同步网络编程函数库留给开发者一个痛苦的抉择:要么允许程序在网络通信时失去响应,或者引入非常复杂的线程。Twisted是基于事件的,异步网络通信框架允许编写的程序在处理事件时保持相应,却不需要使用线程。
·多功能
Twisted包括大量的功能。Email、WEB、news、chat、DNS、SSH、Telnet、RPC、数据库存取或者更多。所有的都为你准备好了。
·灵活
Twisted提供了高层类允许快速开始。而且并没有感到受限。如果需要高级功能,或者需要自定义网络协议,也是可以的。你可以实现自己的网络协议,控制每一个字节。
·开放源代码
Twisted是免费的。它包含源代码,按照函数库协议发行。并且欢迎在你的程序中使用Twisted,不必支付任何费用和担心法律问题。如果希望知道一个对象的运行原理也可以直接看源码。如果你开发出了自己的新功能和扩展,欢迎与我们共享。
·社区支持
Twisted有一个活跃的社区包含开发者和用户。如果你发现了问题,也可以在邮件列表上找到很多开发者来帮助你。查看第一章的寻找FAQ一节。或者进入#twisted这个IRC频道,来与开发者进行在线交流。
·易于整合的平台
一个Twisted程序可以在多种服务之间共享数据,可以整合很多工作。比如可以编写SMTP到XMLRPC的代理,一个通过SSH来更新网站的服务,或者一个包含NNTP接口的WEB讨论组。如果需要在平台之间交换数据,Twisted是个很好的选择。:if expand("%") == ""|browse confirm w|else|confirm w|endif
0.2 本书包含的内容
本书并不是讲解Twisted框架中的每一个类,而是关注于更加真实的例子。本书也会帮助你理解Twisted中使用的关键技术和设计模式。如下是主要内容列表:
·安装Twisted
第一章讲解了下载和安装还有平台相关扩展库。
·使用TCP连接
第二章告诉你如何使用Twisted建立TCP连接,包括服务器和客户端。
·WEB客户端和服务器
第三、四章讲解了如何使用Twisted工作于WEB之上。第三章模拟WEB客户端。第四章编写了一个示例HTTP服务器,实现了基本的等级管理和响应功能。
·WEB服务和RPC
Twisted内置支持了多种WEB服务和远程调用方法。第五章讲解了如何在REST架构下建立应用。也讲解了如何编写XMLRPC和SOAP客户端和服务器,和如何将Python对象转换为网络连接。
·认证
管理用户和认证权限是很多程序的核心功能之一。第六章讨论了实现方法,并提供了在多种不同应用中具有很好移植性的框架。
·电子邮件客户端和服务器
第七、八章给出了电子邮件支持。第七章的例子展示了如何编写SMTP、POP3、IMAP客户端来收发电子邮件。第八章展示了如何构建SMTP、POP3、IMAP服务器。
·Usenet新闻组
第九章讨论了使用NNTP协议的Usenet客户端和服务器。并展示了如何阅读和发送消息,如何运行NNTP服务器,如何使用NNTP作为其他方式的接口,如RSS等。
·SSH客户端和服务器
第十章讲解了SSH支持。例举了编写SSH服务器的程序,并提供了远程可用的Python解释器。这一章也展示了如何编写SSH客户端来执行远程命令。
·运行和管理Twisted程序
第十一章展示了如何使用Twisted工具运行和管理应用程序,比如作为后台精灵线程,使用setuid和chroot限制权限,和写入日志文件。
1.0 快速开始
在你使用Twisted进行开发之前,你应该下载并安装。这一章讲解在各种操作系统下的安装过程。也包括将Twisted实用工具加入到路径和熟悉文档。其他问题可以到Twisted社区提问。
1.1 安装Twisted
首先应该下载Twisted用于安装。可以到下载。为了安装附加属性,还可以安装另外两个可选软件包。
Twisted网络编程必备(二)
1.1.1 下面该如何做?
到下载最新版本的Twisted。然后安装PyOpenSSL(一个Python开源OpenSSL库),这个软件包用于给Twisted提供加密传输支持(SSL)。最后,安装PyCrypto,一个包含了Python安全算法的包,用于提供SSH支持。这些软件包的下载地址在后面各个节中指定。
为了使用Twisted并不一定要安装PyOpenSSL和PyCrypto。但是不安装这些,将无法使用Twisted的SSL和SSH功能,其他功能还是可用的。
1.1.1.1 Windows
到下载Windows版Python2.4安装包。这个二进制包包含了Twisted的核心功能,包括大量的扩展包和协议支持。如果需要安装更多的例子,可以到下载"Twisted Dependencies for Windows"。这个安装包已经包含了PyOpenSSL和PyCrypto。
这些页面有可能已经移动到其他页面中了,并且增加了一些新功能。如果找不到这些连接,可以尝试从访问。
如果一次下载了这三个软件包,运行Twisted安装程序。这是一个小的安装包,可以自动检测Python安装路径,下一步运行PyOpenSSL和PyCrypto安装包。这些都是很简单的,也只需要很少的几秒钟。
当编译Twisted模块时,可能需要一点时间,进度条会显示当前状态。解包也会耗费几分钟。
证实是否安装成功的方法是导入twisted包。
$ python
... ...
>>> import twisted
>>>
如果"import twisted"执行成功而没有错误,则已经安装好了Twisted。
1.1.1.2 MAC OS X, Linux, 与BSD
很多Linux和BSD的发行版,包括Debian、Ubuntu、Gentoo、Red Hat、FreeBSD等等已经预先安装了Twisted了,而且PyOpenSSL和PyCrypto也是可用的。可以检查你的OS是否被Twisted下载页所提及,或者搜索已经安装包历史可以看到。如果已经安装了,最好确保版本是Twisted 2.0或更高,而不是1.3的release版本。
如果没有预安装Twisted,则需要下载源码来安装。
1.2 从源码安装
如果你的操作系统上没有预安装Twisted,则需要从源代码安装。不必担心,从源码安装对于Python也很简单。
1.2.1 下面如何做
首先,下载完整的Twisted源码包。附带的包可以让Twisted功能更多。运行本书的大多数例子都需要完整的安装。
一旦下载了软件包,就解压到工作目录。
$ tar -xjvf ~/downloads/TwistedSumo-2005-03-22.tar.bz2
下一步进入解压后的目录。Twisted依赖于zope.interface包,而这个包已经附带在了Twisted Sumo发行包中了。
解压ZopeInterface压缩包:
$ tar -xzvf ZopeInterface-3.0.1.tgz
进入ZopeInterface目录,运行命令"python setup.py install"。这个命令将会构建和安装zope.interface包
到python安装目录的lib/site-packages/twisted目录。这里是需要Administrator/root权限的。所以使用su或sudo来增加权限级别。
$ cd ZopeInterface-3.0.1
$ python setup.py install
一旦zope.interface安装完成,就可以安装Twisted了。在Twisted目录下,运行"python setup.py install"。这个命令将会编译Twisted的C模块,并安装Twisted:
$ cd TwistedSumo-2005-03-22
$ python setup.py install
如果你已经安装了多个版本的Python,确保Twisted安装在你正在运行的python命令对应的那个版本上。书的例子可以运行在Python 2.3或更高版本。检查你的Python版本通过"python -V"。
祝贺,你已经成功安装了Twisted。现在测试一下是否安装成功,运行Python提示符:
$ python
>>> import twisted
>>>
如果"import twisted"语句执行成功则Twisted安装成功了。
下一步,下载最新版本的PyOpenSSL()。PyOpenSSL运行在OpenSSL库之上,所以你需要确保OpenSSL已经安装了。Mac OS X已经默认安装了OpenSSL,并包含了头文件,Linux和BSD的大部分发行版也默认包含了这些。
如果你正巧倒霉安装了一个默认没有OpenSSL库的发行版,则需要下载并安装源码包,从这里()。
为了安装PyOpenSSL,需要执行如下步骤。首先解压下载文件:
$ tar -zxvf PyOpenSSL-0.6.tar.gz
然后,选择PyOpenSSL库并运行"python setup.py install",使用root用户。
$ cd PyOpenSSL-0.6
$ python setup.py install
当安装完成了,确保OpenSSL包是可用的,可以用导入来测试:
$ python
>>> import OpenSSL
>>> import twisted.internet.ssl
>>> twisted.internet.ssl.SSL
如果没有看到任何错误,则已经为Twisted成功的安装了SSL支持。
最后一个需要安装的包是PyCrypto。PyCrypto是Python的加密工具集,是A.M.Kuchling开发的一个包含了众多加密算法实现的包。Twisted使用PyCrypto来支持SSH连接。
从()来下载,并解压:
$ tar -xzvf pycrypto-2.0.tar.gz
运行很熟悉的"python setup.py install"来安装,在PyCrypto的目录中。
$ cd pycrypto-2.0
$ python setup.py install
确保这个包是否安装成功可以用导入测试。你可以测试twisted.conch.ssh.transport模块可以使用PyCrypto的RSA实现:
$ python
>>> import Crypto
>>> import twisted.conch.ssh.transport
>>> twisted.conch.ssh.transport.RSA
如果是如上的显示,则你已经成功的安装了PyCrypto。在这个是时候,你就可以利用Twisted,并且使用SSL和SSH功能了
Twisted网络编程必备(三)
1.3 添加Twisted实用工具到PATH路径参数
Twisted包含了一些实用工具脚本。为了方便使用,应该将他们添加到PATH可以找到的地方。
1.3.1 下面如何做
一般来说,所有添加到PATH的路径都是可以被OS找到的。按照如下步骤来做。
1.3.1.1 Windows
Twisted的使用工具将会安装到Python的scripts目录(典型为c:\Python23\scripts)。Twisted包含了一些菜单设置程序,可以通过Programs->Twisted->Twisted Command Prompt。来进入。使用这个菜单入口可以使用提示符模式来运行实用工具,而且已经自动设置好了PATH路径了。
1.3.1.2 Linux
Twisted的使用工具将会安装到python二进制目录的相同文件夹(可能是/usr/bin或/usr/local/bin),所以需要将这个路径添加到PATH路径。
1.3.1.3 Mac OS X
如果正在使用Mac OS X 2.3 (jaguar)或更新版本,Twisted已经默认安装在了(/System/Library/Frameworks/Python.framework/Versions/Current/bin。将这个路径添加到PATH变量。
$ set PATH=$PATH:/System/Library/Frameworks/Python.framework/Versions/Current/bin
1.4 使用Twisted的文档
Twisted包含了一些不同类型的文档,包括扩展API文档、HOWTO文档、快速入门和例子程序。十分推荐尽快熟悉这些文档,并在开发过程中提供帮助。
1.4.1 下面如何做
Twisted的文档是在网站上在线观看的。一个完整的API文档在(http://twistedmatrix.com/documents/current/api)。为了编程,你需要多次使用这个文档。这份API文档是由代码自动生成的,所用的工具是Twisted的一个组件叫做lore。
Twisted是由一些子工程构成的,每个子工程包含自己的文档。例如,核心模块文档在(http://twistedmatrix.com/projects/core/documentation),而关于WEB模块的文档在(http://twistedmatrix.com/projects/web/documentation)。在主页有进入每一个子工程的链接。
每个子工程都包含以下类型的文档:
·HOWTOs
描述特殊功能和如何使用这些功能。并不覆盖所有的只是,但可以提供对特定任务的快速入门。包含在HOWTOs中的快速入门(tutorial)叫做"Twisted From Scratch",讲解如何开发一个应用程序,并扩展的讲解一些高级内容。
·Examples
简短而特别的Twisted代码,有点像HOWTOs,但是可以在使用中对特定功能提供有效的帮助。
·Manual pages
这是HTML版本的manpages,描述了如何使用Twisted实用工具。
1.4.2 关于
如果想要浏览文档而不通过浏览器,那么可以直接使用pydoc工具来浏览。pydoc并不能展示Twisted文档中所有的标志,当仍然很有用。Figure 1-1展示了pydoc展示twisted.web.http的信息。
当然,真的想要理解Twisted的工作,可以阅读Twisted的源代码。因为有些模块为了性能而使用了C语言来写,除此之外可以到site-packages/twisted目录中查看。或者打开appropriate.py文件。
1.5 为自己的问题寻找答案
大多数问题可以从文档中自己找到答案,或者可以从社区得到答案。
1.5.1 下面如何做
有一些有益的社区资源可以提供帮助。首先是邮件列表。"twisted-python"列表是提供一般讨论的。"twisted-web"列表提供了WEB应用程序的讨论。如果问题提错了地方,会被自动转到另外一个讨论组。可以在如下地址申请讨论组:
第二,可以在#twisted和#twisted.web这两个IRC频道讨论问题()。这些频道是一直开放且有趣的,问题可以很及时的得到回答。如果不要求及时得到答复,最好还是到邮件列表讨论,这种方式提供更加广泛的关注,并且可以让更多的人分享。
最后的可用资源是Planet Twisted社区。地址在,这个站点的weblog提供了大量的知识。这是一种很好的获取知识的方式,这也提供了RSS订阅方式。
2. 建立简单的客户端和服务器
使用Twisted进行开发,需要学习如何使用新的类和对象。这些类是Twisted的核心,你将会在你的应用中使用这些类。它们提供了平滑的学习曲线,理解如何使用他们,将会使得使用Twisted进行开发更加简便。
这一章展示了如何编写简单的客户端和服务器。并介绍Twisted简单的类和工作流程,并展示如何使用它们。
Twisted网络编程必备(四)
2.1 启动Twisted的事件循环
Twisted是事件驱动的框架。这意味着不再使用特定序列来处理程序逻辑,而是通过被动调用一些函数来实现。
例如,GUI程序里面使用的"button pressed"事件。设计者不需要事先确定事件何时发生,
只需要编写事件的响应函数即可。这就是所谓的事件处理器(event handler)。
每个事件驱动的框架都包含了一个特殊的函数叫做事件循环(event loop)。每次启动,事件循环都会立即运行。
这个函数运行时等待事件的发生。当事件发生时,事件循环函数会自动触发相关的事件处理函数。
使用事件循环需要改变一些顺序编程的习惯。一旦开始了事件循环,你将无法控制程序的执行顺序,
只会自动响应事件。所以,你需要为程序设计事件处理器。也就是事件发生时的响应。
在Twisted中,有一种特殊的对象用于实现事件循环。这个对象叫做reactor。可以把反应器(reactor)想象为
Twisted程序的中枢神经。除了分发事件循环之外,反应器还做很多重要的工作:定时任务、线程、
建立网络连接、监听连接。为了让反应器可以正常工作,需要启动事件循环。
2.1.1 下面如何做
启动反应器是很简单的,从twisted.internet模块导入reactor对象。然后调用reactor.run()来启动反应器的事件循环。
下例展示了代码:
from twisted.internet import reactor
print 'Running the reactor ...'
reactor.run()
print 'Reactor stopped.'
反应器将会一直运行,直到接到停止的通知,可以按下Ctrl+C来退出事件循环,并终止程序:
$ python runreactor.py
Running the reactor ...
^CReactor stopped.
这是一个简洁的例子。尽管反应器运行着,但是没有做任何事情。下面的例子提供了更多有趣的事情,
介绍了反应器的callLater方法。reactor.callLater方法用于设置定时事件。这个方法在用于定时在未来执行。
比如一个函数已经是一个事件处理器了,这种事件会在一定时间之后启动。下面的例子,
函数调用了一定时间之后被调用:
from twisted.internet import reactor
import time
def printTime():
print 'Current time is',time.strftime("%H:%M:%S")
def stopReactor():
print "Stopping reactor"
reactor.stop()
reactor.callLater(1,printTime)
reactor.callLater(2,printTime)
reactor.callLater(3,printTime)
reactor.callLater(4,printTime)
reactor.callLater(5,stopReactor)
print 'Running the reactor ...'
reactor.run()
print 'Reactor stopped.'
运行这个程序,可以看到如下结果:
$ python calllater.py
Running the reactor ...
Current time is 10:33:44
Current time is 10:33:45
Current time is 10:33:46
Current time is 10:33:47
Stopping reactor
Reactor stopped.
2.1.2 它是如何工作的
例子2-1简介了导入与执行reactor.run()来启动事件循环。反应器将会一直保持执行直到按下Ctrl-C,
尽管这一段时间什么都不做。在这时,反应器停止了,程序执行到代码的最后一行,并打印出反应器已经停止的消息。
第二个例子使用reactor.callLater函数定时执行函数。reactor.callLater函数包含两个必须参数,
等待的秒数,和需要调用的函数。在设置了定时函数调用之后,控制就回到了反应器的事件循环reactor.run()中了。
Tip:你可以传递附加的参数和键值对到reactor.callLater中,这些用于调用指定的函数。
例如,reactor.callLater(1,func,True,x="Hello"),将会最终调用func(True,x="Hello"),在一秒钟之后。
第一个定时函数是printTime(),简单显示了当前时间。第五个定时函数是stopReactor(),
其中调用了reactor.stop(),导致了反应器退出了事件循环。这也是为什么不需要按下Ctrl-C而自动退出了事件循环的原因。
Tip:在这种情况下仍然可以按下Ctrl-C来手动停止事件循环。
注意事件的发生顺序,反应器按照给定的时间来调用指定的函数。一旦反应器开始运行,
反应器就会控制事件循环,并在指定时间调用函数。反应器在被告知停止之前会一直运行,
直到reactor.stop()调用。一旦反应器停止了,程序将继续处理最后一行,显示反应器停止的消息。
Tip:在实际应用中,reactor.callLater是常用于超时处理和定时事件。
可以设置函数按照指定的时间间隔来执行关闭非活动连接或者保存内存数据到硬盘。
2.1.3 建立(establishing)一个TCP连接
所有的网络应用程序都必须做一个简单的步骤,开启一个连接。接着可以发送邮件、
传递文件、看电影等等,但是在所有这些之前,必须在电脑之间建立一个连接。
本节讲解了如何使用reactor开启TCP连接。
2.1.4 下面如何做
调用reactor.connectTCP()方法打开一个TCP连接,传递一个ClientFactory对象作为第三个参数。
ClientFactory对象等待连接被建立,然后创建一个Protocol对象来管理连接中的数据流。
下面的例子2-3展示了如何在电脑和Internet之间建立一个连接():
from twisted.internet import reactor,protocol
class QuickDisconnectedProtocol(protocol.Protocol):
def connectionMade(self):
print "Connected to %s."%self.transport.getPeer().host
self.transport.loseConnection()
class BasicClientFactory(protocol.ClientFactory):
protocol=QuickDisconnectedProtocol
def clientConnectionLost(self,connector,reason):
print 'Lost connection: %s'%reason.getErrorMessage()
reactor.stop()
def clientConnectionFailed(self,connector,reason):
print 'Connection failed: %s'%reason.getErrorMessage()
reactor.stop()
reactor.connectTCP('',80,BasicClientFactory())
reactor.run()
当运行这个例子的时候,可以看到如下输出:
$ python connection.py
Connected to
Lost connection: Connection was closed cleanly.
除非你的电脑没有在线。在这种情况下,会看到如下错误信息:
$ python connection.py
Connection failed: DNS lookup failed: address '' not found.
2.1.5 它是如何工作的
这里有两个主要的类用于作为客户端工作,ClientFactory和Protocol。
这些类被设计成处理连接中所有可能运到的事件:成功建立连接、连接失败、连接断开、数据传送等等。
ClientFactory和Protocol有严格的不同。ClientFactory的工作是管理连接事件,
并且创建Protocol对象处理每一个成功的连接。一旦连接建立,Protocol对象就接管下面的工作了,
包括收发数据和决定是否关闭连接。
Tip:名字"Factory"在ClientFactory是来自于Protocol的请求,响应每一个成功的连接。
例子2-3定义了自定义的Protocol叫做QuickDisconnectProtocol,继承自protocol.Protocol。
它重载了一个方法connectMade。这个方法连接成功时运行,在reactor刚刚成功建立了连接,
然后ClientFactory创建了QuickDisconnectProtocol的实例时。有如他的名字,
QuickDisconnectProtocol对象在打印信息之后就马上关闭了。
Tip:Protocol对象有一个属性叫做transport,包含了当前活动连接对象。
BasicClientFactory是继承自protocol.ClientFactory的类。
它首先设置了类变量protocol为QuickDisconnectProtocol。这个类的实例被创建用于管理成功的连接。
BasicClientFactory重载了ClientFactory的两个方法,clientConnectionLost和clientConnectionFailed。
这两个方法是事件处理器。clientConnectionFailed在反应器无法建立连接时被调用。
clientConnectionLost在建立的连接被关闭或断开时调用。
告知反应器建立TCP连接,按照例子2-3的方法是调用reactor.connectTCP:
reactor.connectTCP('',80,BasicClientFactory())
这一行告知反应器建立一个TCP连接到服务器的80端口,通过BasicClientFactory来管理连接。
Twisted网络编程必备(五)
2.2 使用非同步的方式工作的结果
除了反应器reactor之外,Deferred可能是最有用的Twisted对象。你可能在Twisted程序中多次用到Deferred,
所有有必要了解它是如何工作的。Deferred可能在开始的时候引起困惑,但是它的目的是简单的:
保持对非同步活动的跟踪,并且获得活动结束时的结果。
Deferred可以按照这种方式说明:可能你在饭店中遇到过这个问题,如果你在等待自己喜欢的桌子时,
在一旁哼哼小曲。带个寻呼机是个好主意,它可以让你在等待的时候不至于孤零零的站在那里而感到无聊。
你可以在这段时间出去走走,到隔壁买点东西。当桌子可用时,寻呼机响了,这时你就可以回到饭店去你的位置了。
一个Deferred类似于这个寻呼机。它提供了让程序查找非同步任务完成的一种方式,
而在这时还可以做其他事情。当函数返回一个Deferred对象时,说明获得结果之前还需要一定时间。
为了在任务完成时获得结果,可以为Deferred指定一个事件处理器。
2.2.1 下面如何做?
当编写一个启动非同步操作的函数时,返回一个Deferred对象。当操作完成时,调用Deferred的callback方法来返回值。
如果操作失败,调用Deferred.errback函数来跑出异常。例子2-4展示了程序使用Deferred使用非同步操作的连接例子,
连接一个服务器的端口。
当调用一个可以返回Deferred的函数时,使用Deferred.addCallback方法指定返回结果时调用的函数。
使用Deferred.addErrback方法指定执行发生异常时调用的函数。
connectiontester.py
from twisted.internet import reactor,defer,protocol
class CallbackAndDisconnectProtocol(protocol.Protocol):
def connectionMade(self):
self.factory.deferred.callback("Connected!")
self.transport.loseConnection()
class ConnectionTestFactory(protocol.ClientFactory):
protocol=CallbackAndDisconnectProtocol
def __init__(self):
self.deferred=defer.Deferred()
def clientConnectionFailed(self,connector,reason):
self.deferred.errback(reason)
def testConnect(host,port):
testFactory=ConnectionTestFactory()
reactor.connectTCP(host,port,testFactory)
return testFactory.deferred
def handleSuccess(result,port):
print "Connect to port %i"%port
reactor.stop()
def handleFailure(failure,port):
print "Error connecting to port %i: %s"%(
port,failure.getErrorMessage())
reactor.stop()
if __name__=="__main__":
import sys
if not len(sys.argv)==3:
print "Usage: connectiontest.py host port"
sys.exit(1)
host=sys.argv[1]
port=int(sys.argv[2])
connecting=testConnect(host,port)
connecting.addCallback(handleSuccess,port)
connecting.addErrback(handleFailure,port)
reactor.run()
运行这个脚本并加上服务器名和端口这两个参数会得到如下输出:
$ python connectingtest.py oreilly.com 80
Connecting to port 80.
或者,如果连接的端口是关闭的:
$ python connectiontest.py oreilly.com 81
Error connecting to port 81: Connection was refused by other side: 22: Invalid argument.
或者连接的服务器并不存在:
$ python connectiongtest.py fakesite 80
Error connecting to port 80: DNS lookup failed: address 'fakesite' not found.
2.2.2 它是如何工作的?
类ConnectionTestFactory是ClientFactory的子类,并且含有Deferred对象作为属性。
当连接完成时,CallbackAndDisconnectProtocol的connectionMade方法会被调用。
connectionMade会调用self.factory.deferred.callbakc加上任意值标识调用成功。
如果连接失败,ConnectionTestFactory的clientConnectionFailed方法会被调用。
第二个参数clientConnectionFailed的reason是twisted.python.failure.Failure对象,
封装了异常,并描述了异常发生的原因。clientConnectionFailed传递了Failure对象
到self.deferred.errback来标识操作失败。
testConnecn函数需要两个参数host和port。用于创建叫做testFactory的ConnectionTestFactory的类实例,
并且传递给reactor.connectTCP参数host和port。当连接成功时返回testFactory.deferred属性,
就是用于跟踪的Deferred对象。
例子2-4夜展示了两个事件处理器函数:handleSuccess和handleFailure。当从命令行运行时,
它们需要host和port两个参数来调用testConnect,指定了结果Deferred到变量connecting。
此时可以使用connecting.addCallback和connecting.addErrback来设置事件处理器函数。
在每一种情况下,传递port作为附加参数。因为扩展的参数或者关键字参数将会由addCallback或
addErrback传递给事件处理器,这将会在调用handleSuccess和handleFailure时得到端口参数作为第二个参数。
Tip:当函数返回Deferred时,可以确定事件调用了Deferred的callback或者errback方法。否则代码将永远等待结果。
在调用了testConnect和设置事件处理器之后,控制劝将会交给reactor.run()。
依赖于testConnect的执行成功与否,将会调用handleSuccess或者handleFailure,打印西那个关的信息并停止反应器。
2.2.3 关于
是否需要保持一串的Deferred呢?有时确实需要同时保持多个非同步任务,且并非同时完成。
例如,可能需要建立一个端口扫描器运行testConnect函数来对应一个范围的端口。
为了实现这个,使用DeferredList对象,作为例子2-5,来挂历众多的Deferred。
from twisted.internet import reactor,defer
from connectiontester import testConnect ##上面调用的connectiontester.py
def handleAllResules(results,ports):
for port,resultInfo in zip(ports,results):
success,result=resultInfo
if success:
print 'Connected to port %i' % port
reactor.stop()
import sys
host=sys.argv[1]
ports=range(1,201)
testers=[testConnect(host,port) for port in ports]
defer.DeferredList(testers,consumeErrors=True).addCallback(handleAllResults,ports)
reactor.run()
运行portscan.py脚本并传递host参数。将会自动扫描1-200端口并报告结果。
$ python portscan.py localhost
Connected to port 22
Connected to port 23
Connected to port 25
... ...
例子2-5使用了Python的列表(list)的形式来创建一个列表存储由testConnect()返回的Deferred对象的列表。
每一个testConnect()使用host参数,并测试1-200端口。
testers=[testConnect(host,port) for port in ports]
例子2-5使用列表方式包装了Deferred对象成为DeferredList对象。
DeferredList将会跟踪所有的Deferred对象的结果并传递作为首参数。
当它们都完成了的时候,将会回调按照(success,result)的格式。在Deferred完成时,
第一个回传值是TRue和第二个参数是Deferred传回结果。如果执行失败,
则第一个参数是False且第二个参数是Failure对象包装的异常。consumeErrors键设置为True时告知
DeferredList完成了吸收Deferred的所有错误。否则将会看到弹出的错误信息。
当DeferredList完成时,results将会被传给handleAllResults函数,列表中的每个元素都会被扫描。
handleAllResults使用了zip函数来匹配每个结果的端口。对每一个端口如果有成功的连接则打印信息。
最终反应器会结束并结束程序。
Twisted网络编程必备(六)
2.3 发送和接收数据
一旦TCP连接被建立,就可以用于通讯。程序可以发送数据到另一台计算机,或者接收数据。
2.3.1 下面如何做?
使用Protocol的子类可以发送和接收数据。重载dataReceived方法用于控制数据的接收,
仅在接收到数据时才被调用。使用self.transport.write来发送数据。
例子2-6包括了DataForwardingProtocol的类,可以将接收到的数据写入self.output。
这个实现创建了简单的应用程序,类似于netcat,将收到的数据传递到标准输出,用于打印接收的数据到标准输出。
from twisted.internet import stdio,reactor,protocol
from twisted.protocols import basic
import re
class DataForwardingProtocol(protocol.Protocol):
def __init__(self):
self.output=None
self.normalizeNewLines=False
def dataReceived(self,data):
if self.normalizeNewLines:
data=re.sub(r"(\r\n|\n)","\r\n",data)
if self.output:
self.output.write(data)
class StdioProxyProtocol(DataForwardingProtocol):
def connectionMade(self):
inputForwarder=DataForwardingProtocol()
inputForwarder.output=self.transport
inputForwarer.normalizeNewLines=True
stdioWarpper=stdio.StandardIO(inputForwarder)
self.output=stdioWarpper
print "Connected to server. Press Ctrl-C to close connection."
class StdioProxyFactory(protocol.ClientFactory):
protocol=StdioProxyProtocol
def clientConnectionLost(self,transport,reason):
reactor.stop()
def clientConnectionFailed(self,transport,reason):
print reason.getErrorMessage()
reactor.stop()
if __name__=='__main__':
import sys
if not len(sys.argv)==3:
print "Usage: %s host port "__file__
sys.exit(1)
reactor.connectTCP(sys.argv[1],int(sys.argv[2]),StdioProxyFactory())
reactor.run()
运行dataforward.py并传入host和port两个参数。一旦连接就可以将所有来自服务器的消息送回服务器,所有接收的数据也同时显示在屏幕上。例如可以手动连接一个HTTP服务器,并发送HTTP请求到oreilly.com:
$ python dataforward.py oreilly.com 80
Connected to server. Press Ctrl-C to close connection.
HEAD / HTTP/1.0
Host: oreilly.com
<--空行
HTTP/1.1 200 OK
......
2.3.2 它们是如何工作的?
例子2-6开始于定义类DataForwardingProtocol。这个协议用于接受数据并存入self.output,这个属性可以用write方法访问,如同self.output.write。DataForwardingOutputProtocol包含一个叫做normalizeNewLines的属性。如果这个属性设置为True,将会由Unix风格的\n换行改变为\r\n这种常见的网络换行方式。
作为DataForwardingProtocol的子类StdioProxyProtocol类具体负责工作。一旦连接被建立,将会创建一个叫做inputForwarder的DataForwardingProtocol实例,并设置输出为self.transport。然后包装twisted.internet.stdio.StandardIO的实例inputForwarder,以标准IO的方式代替网络连接。这一步对所有的通过StdioProxyProtocol方式的网络连接都有效。最终设置StdioProxyProtocol的output属性到值stdioWarpper,所以数据的接收工作定义到了标准输出。
协议定义之后,很容易定义StdioProxyFactory,将其protocol属性设置为StdioProxyProtocol,并且处理反应器的停止和连接、失败工作。调用reactor.connectTCP连接,然后依靠reactor.run()来控制事件循环。
2.4 接受客户端的连接
前面的实验都是讲解如何作为客户端进行连接。Twisted也同样适合于编写网络服务器,用于等待客户端连接。下面的实验将会展示如何编写Twisted服务器来接受客户端连接。
2.4.1 下面如何做?
创建一个Protocol对象来定义服务器行为。创建一个ServerFactory对象并引用Protocol,并传递给reactor.listenTCP。例子2-7展示了简单的echo服务器,简单的返回客户端发来的信息。
from twisted.internet import reactor,protocol
from twisted.protocols import baisc
class EchoProtocol(basic.LineReceiver):
def lineReceived(self,line):
if line=='quit':
self.sendLine("Goodbye.")
self.transport.loseConnection()
else:
self.sendLine("You said: "+line)
class EchoServerFactory(protocol.ServerFactory):
protocol=EchoProtocol
if __name__=="__main__":
port=5001
reactor.listenTCP(port,EchoServerFactory())
reactor.run()
当这个例子运行时,将会在5001端口监听,并报告已经建立的连接。
$python echoserver.py
Server running, Press Ctrl-C to stop.
Connection from 127.0.0.1
Connection from 127.0.0.1
在另一个终端,使用netcat、telnet或者dataforward.py(例子2-6)来连接服务器。将会返回键入的例子。输入quit来关闭连接。
$ python dataforward.py localhost 5001
Connected to server. Press Ctrl-C to close connection.
hello
You said: hello
twisted is fun
You said: twisted is fun
quit
Goodbye.
2.4.2 它们是如何工作的?
Twisted服务器使用相同的Protocol类作为客户端。为了复用,EchoProtocol继承了twisted.protocols.basic.LineReceiver,作为Protocol.LineReceiver的轻量级实现,可以自动按照获得的行来产生处理。当EchoProtocol接收到一个行时,就会返回收到的行,除非遇到了'quit'将退出。
下一步,定义了EchoServerFactory类。EchoServerFactory继承自ServerFactory,作为ClientFactory的服务器端,并设置了EchoProtocol为protocol属性。EchoServerFactory的实例作为第二个参数传递给reactor.listenTCP,第一个参数是端口号。
Twisted网络编程必备(七)
3.0 WEB客户端
大部分上网活动都是通过WEB浏览器来访问WEB的。所以通过HTTP协议制作客户端来访问WEB是很有意义的。这一章讲解如何使用twisted.web.client模块来操作互联网资源,包括下载页面,使用HTTP认证,上传文件,使用HTTP字段等。
3.1 下载网页
最简单和常用的任务莫过于通过WEB客户端来下载网页了。客户端连接服务器,发送HTTP的GET请求,接收包含网页的HTTP响应。
3.1.1 下面如何做?
可以依靠Twisted内置的协议来快速进入工作。twisted.web包包含了完整的HTTP实现,免去自行开发Protocol和ClientFactory的工作。在未来,它还将包括建立HTTP请求的工具函数。获取一个网页,使用twisted.web.client.getPage。例子3-1是webcat.py脚本,用于通过URL获得网页。
from twisted.web import client
from twisted.internet import reactor
import sys
def printPage(data):
print data
reactor.stop()
def printError(failure):
print >> sys.stderr, "Error: ", failure.getErrorMessage()
reactor.stop()
if len(sys.argv)==2:
url=sys.argv[1]
client.getPage(url).addCallback(printPage).addErrback(printError)
reactor.run()
else:
print "Usage: webcat.py
给webcat.py脚本一个URL,将会显示网页代码:
$ python webcat.py
传递一个URL,并返回一个Deferred对象用于下载完成时的回调
"""
tmpfd,tempfilename=tempfile.mkstemp()
os.close(tmpfd)
return client.downloadPage(url,tempfilename).addCallback(returnFilename,tempfilename)
def returnFilename(result,filename):
return filename
if __name__=='__main__':
import sys,os
from twisted.internet import reactor
def printFile(filename):
for line in file(filename,'r+b'):
sys.stdout.write(line)
os.unlink(filename) #删除文件
reactor.stop()
def printError(failure):
print >> sys.stderr, "Error: ",failure.getErrorMessage()
reactor.stop()
if len(sys.argv)==2:
url=sys.argv[1]
downloadToTempFile(url).addCallback(printFile).addErrback(printError)
reactor.run()
else:
print "Usage: %s
函数downloadToTempFile在调用twisted.web.client.downloadPage时返回Deferred对象。downloadToTempFile还添加了returnFilename作为Deferred的回调,将临时文件名作为附加参数。这意味着当downloadToTempFile返回时,reactor将会调用returnFileName作为downloadToTempFile的首个参数,而文件名作为第二个参数。
例子3-2注册了另外两个回调函数到downloadToTempFile。记住downloadToTempFile返回的Deferred已经包含了returnFilename作为回调处理器。因此,在结果返回时,returnFilename将会首先被调用。这个函数的结果被用于调用printFile。
3.2 存取受到密码保护的页面
有些网页需要认证。如果你正在开发HTTP客户端应用,那么最好准备好处理这些,并在需要时给出用户名和密码。
3.2.1 下面如何做?
如果一个HTTP请求以401的状态码执行失败,则是需要认证的。这时传递用户提供的登录用户名和密码到Authorization字段,有如例子3-3中那样:
from twisted.web import client,error as weberror
from twisted.internet import reactor
import sys,getpass,base64
def printPage(data):
print data
reactor.stop()
def checkHTTPError(failure,url):
failure.trap(weberror.Error)
if failure.value.status=='401':
print >> sys.stderr,failure.getErrorMessage()
#要求用户名和密码
username=raw_input("User name: ")
password=getpass.getpass("Password: ")
basicAuth=base64.encodestring("%s:%s"%(username,password))
authHeader="Basic "+basicAuth.strip()
#尝试再次获取页面,已经加入验证信息
return client.getPage(url,headers={"Authorization":authHeader})
else:
return failure
def printError(failure):
print >> sys.stderr, 'Error: ',failure.getErrorMessage()
reactor.stop()
if len(sys.argv)==2:
url=sys.argv[1]
client.getPage(url).addErrback(
checkHTTPError,url).addCallback(
printPage).addErrback(
printError)
reactor.run()
else:
print "Usage: %s
运行webcat3.py并传递URL作为附加参数,将会尝试下载页面。如果收到了401错误,则会询问用户名和密码之后自动重试:
$ python webcat3.py
401 Authorization Required
User name: User
Password: <输入密码>
... ...
3.2.2 它们如何工作?
这个例子使用了扩展的错误处理器。它首先给client.getPage添加了Deferred,在添加printPage和printError之前。这样做给了checkHTTPError以机会在其他处理器之前来处理client.getPage的错误。
作为一个errback的错误处理器,checkHTTPError将被twisted.python.failure.Failure对象调用。Failure对象封装了跑出的异常,记录了异常时的traceback,并添加了几个有用的方法。checkHTTPError开始使用了Failure.trap方法来证实异常是twisted.web.error.Error类型的。如果不是,trap将会重新抛出异常,退出当前函数并允许错误传递到下一个errback处理器,printError。
然后,checkHTTPError检查HTTP响应状态码.failure.value是一个Exception对象,并且是twisted.web.error.Error的对象,已知的status属性包含了HTTP响应状态码。如果状态码不是401,则返回原始错误,通过把错误传递给printError。
如果状态码是401,checkHTTPError会执行。它会提示输入用户名和密码,并编码成HTTP的Authorization头字段。然后调用client.getPage,返回Deferred对象。这将导致一些很cool的事情发生,反应器等待第二次调用结果,然后调用printPage或者printError来处理结果。实际上,checkHTTPError将会提示"通过另外一个Deferred来处理错误,等待Deferred的结果,并且在另外一行中进行事件处理"。这个技术是很强大的,并且可以在Twisted程序中多次使用。
最终的结果有如前面的例子:使用printPage或者printError来输出结果。当然,如果初始化请求成功了,则不需要认证,checkHTTPError将不会被调用,返回的结果也将直接调用printPage。
Twisted网络编程必备(八)
3.3 上传文件
以用户的观点来说,没有什么简单的方法使得上传文件到网页。使用HTML表单选择文件并按下提交按钮才可以上传。正因为如此很多站点提供了难用的上传方式。有时当你需要上传文件而又不能使用浏览器时。可能需要自己开发程序来上传照片、基于WEB的文件管理系统等等。下面的例子展示了使用Twisted开发HTTP客户端来上传文件的方法。
3.3.1 下面如何做?
首先,对键值对编码,并将上传文件编入multipart/form-data的MIME文件。Python和Twisted都无法提供简便的方法来实现,但你可以也可以不需要太大努力的自己实现一个。然后传递已编码的表单数据到formdata键,并传递给client.getPage或client.downloadPage,并使用HTTP的POST方法。然后就可以执行getPage或downloadPage来获得HTTP响应了。例子3-4(validate.py)展示了如何按照W3C标准上传文件,并保存响应到本地文件,然后显示在用户浏览器中。
from twisted.web import client
import os,tempfile,webbrowser,random
def encodeForm(inputs):
"""
传递一个字典参数inputs并返回multipart/form-data字符串,包含了utf-8编码的
数据。键名必须为字符串,值可以是字符串或文件对象。
"""
getRandomChar=lambda:chr(random.choice(range(97,123)))
randomChars=[getRandomChar() for x in range(20)]
boundary="---%s---"%''.join(randomChars)
lines=[boundary]
for key,val in inputs.items():
header='Content-Disposition: form-data; name="%s"'%key
if hasattr(val,'name'):
header+=';filename="%s"'%os.path.split(val.name)[1]
lines.append(header)
if hasattr(val,'read'):
lines.append(val.read())
else:
lines.append(val.encode('utf-8'))
lines.append('')
lines.append(boundary)
return "\r\n".join(lines)
def showPage(pageData):
#将数据写入临时文件并在浏览器中显示
tmpfd,tmp=tempfile.mkstemp('.html')
os.close(tmpfd)
file(tmp,'w+b').write(pageData)
webbrowser.open('file://'+tmp)
reactor.stop()
def handleError(failure):
print "Error: ",failure.getErrorMessage()
reactor.stop()
if __name__=='__main__':
import sys
from twisted.internet import reactor
filename=sys.argv[1]
fileToCheck=file(filename)
form=encodeForm({'uploaded_file':fileToCheck})
postRequest=client.getPage(
'',
method="POST",
headers={'Content-Type':'multipart/form-data; charset=utf-8',
'Content-Length':str(len(form))},
postdata=form)
postRequest.addCallback(showPage).addErrback(handleError)
reactor.run()
运行validate.py脚本并在第一个参数给出HTML文件名:
$ python validate.py test.html
一旦发生了错误,必须获得validation的在浏览器中的错误报告。
3.3.2 它们是如何做的?
按照W3C标准页面,例如包含表单如下:
这样可以建立一个HTTP客户端发送与浏览器相同的数据。函数encodeForm输入一个字典包含了以键值对方式引入的上传文件对象,并返回已经按照multipart/form-data的MIME编码文档的字符串。当validate.py运行时,打开特定文件作为第一个参数并传递encodeForm作为'uploaded_file'的值。这将会返回待提交的有效数据。
validate.py然后使用client.getPage来提交表单数据,传递头部字段包括Content-Length和Content-Type。showPage回调处理器得到返回的数据并写入临时文件。然后使用Python的webbrowser模块调用默认浏览器来打开这个文件。
3.4 检测网页更新
当使用流行的HTTP应用打开一个RSS收集时,将会自动下载最新的RSS(或者是Atom)的blog更新。RSS收集定期下载最新的更新,典型值是一个小时。这个机制可以减少浪费大量的带宽。因为内容很少更新,所以客户端常常重复下载相同数据。
为了减少浪费网络资源,RSS收集(或者其他请求页面的方式)推荐使用条件HTTP GET请求。通过在HTTP请求的头部字段添加条件,客户端可以告知服务器仅仅返回已经看过的时间之后的更新。当然,一种条件是在上次下载后是否修改过。
3.4.1 下面如何做?
首次下载网页时保持跟踪头部字段。查找ETag头部字段,这定义了网页的修正版本,或者是Last-Modified字段,给出了网页修改时间。下次请求网页时,发送头部If-None-Match加上ETag值,或者If-Modified-Since加上Last-Modified值。如果服务器支持条件GET请求,如果网页没有修改过则返回304 Unchanged响应。
getPage和downloadPage函数是很方便的,不过对于控制条件请求是不可用的。可以使用稍微低层次的HTTPClientFactory接口来实现。例子3-5演示了HTTPClientFactory测试网页是否更新。
from twisted.web import client
class HTTPStatusChecker(client.HTTPClientFactory):
def __init__(self,url,headers=None):
client.HTTPClientFactory.__init__(self,url,headers=headers)
self.status=None
self.deferred.addCallback(
lambda data: (data,self.status,self.response_headers))
def noPage(self,reason): #当返回非200响应时
if self.status=='304' #页面未曾改变
client.HTTPClientFactory.page(self,'')
else:
client.HTTPClientFactory.noPage(self,reason)
def checkStatus(url,contextFactory=None,*args,**kwargs):
scheme,host,port,path=client._parse(url)
factory=HTTPStatusChecker(url,*args,**kwargs)
if scheme=='https':
from twisted.internet import ssl
if contextFactory is None:
contextFactory=ssl.ClientContextFactory()
reactor.connectSSL(host,port,factory,contextFactory)
else:
reactor.connectTCP(host,port,factory)
return factory.deferred
def handleFirstResult(result,url):
data,status,headers=result
nextRequestHeaders={}
eTag=headers.get('etag')
if eTag:
nextRequestHeaders['If-None-Match']=eTag[0]
modified=headers.get('last-modified')
if modified:
nextRequestHeaders['If-Modified-Since']=modified[0]
return checkStatus(url,headers=nextRequestHeaders).addCallback(
handleSecondResult)
def handleSecondResult(result):
data,status,headers=result
print 'Second request returned status %s:'%status,
if status=='200':
print 'Page changed (or server does not support conditional request).'
elif status=='304':
print 'Page is unchanged.'
else:
print 'Unexcepted Response.'
reactor.stop()
def handleError(failure):
print 'Error',failure.getErrorMessage()
reactor.stop()
if __name__=='__main__':
import sys
from twisted.internet import reactor
url=sys.argv[1]
checkStatus(url).addCallback(
handleFirstResult,url).addErrback(
handleError)
reactor.run()
运行updatecheck.py脚本并传入URL作为首参数。它首先尝试下载WEB页面,使用条件GET。如果返回了304错误,则表示服务器未更新网页。常见的条件GET适用于静态文件,如RSS种子,但不可以是动态生成页面,如主页。
$ python updatecheck.py
Second request returned status 304: Page is unchanged
$ python updatecheck.py
Second request returned status 200: Page changed
(or server does not support conditional requests).
3.4.2 它们如何做?
HTTPStatusChecker类是client.HTTPClientFactory的子类。它负责两件重要的事情。在初始化时使用lambda添加回调函数到self.deferred。这个匿名函数将会在结果传递到其他处理器之前截获self.deferred。这将会替换结果(已下载数据)为包含更多信息的元组:数据、HTTP状态码、self.response_headers,这是一个响应字段的字典。
HTTPStatusChecker也重载了noPage方法,就是由HTTPClientFactory调用的成功的响应码。如果响应码是304(未改变状态码),则noPage方法调用HTTPClientFactory.page替换原来的noPage方法,表示成功的响应。如果执行成功了,HTTPStatusChecker的noPage同样返回重载的HTTPClientFactory中的noPage方法。在这个时候,它提供了304响应来替换错误。
checkStatus函数需要一个被twisted.web.client._parse转换过的URL参数。它看起来像是URL的一部分,可以从中获取主机地址和使用HTTP/HTTPS协议(TCP/SSL)。下一步,checkStatus创建一个HTTPStatusChecker工厂对象,并打开连接。所有这些代码对twisted.web.client.getPage都是很简单的,并推荐使用修改过的HTTPStatusChecker工厂来代替HTTPClientFactory。
当updatecheck.py运行时,会调用checkStatus,设置handleFirstResult作为回调事件处理器,按照循序,第二次请求会使用If-None-Match或If-Modified-Since条件头字段,设置handleSecondResult作为回调函数处理器。handleSecondResult函数报告服务器是否返回304错误,然后停止反应器。
handleFirstResult实际上返回的是handleSecondResult的deferred结果。这允许printError,就是被指定给checkStatus的错误事件处理器,用于所有在第二次请求中调用checkStatus的其他错误。
3.5 监视下载进度
以上的例子暂时还没有办法监视下载进度。当然,Deferred可以在下载完成时返回结果,但有时你需要监视下载进度。
Twisted网络编程必备(九)
3.5.1 下面如何做?
再次,twisted.web.client没能提供足够的实用函数来控制进度。所以定义一个client.HTTPDownloader的子类,这个工厂类常用于下载网页到文件。通过重载一对方法,你可以保持对下载进度的跟踪。webdownload.py脚本是3-6例子,展示了如何使用:
from twisted.web import client
class HTTPProgressDownloader(client.HTTPDownloader):
def gotHeaders(self,headers):
if self.status=='200':
if headers.has_key('content-length'):
self.totalLength=int(headers['content-length'][0])
else:
self.totalLength=0
self.currentLength=0.0
print ''
return client.HTTPDownloader.gotHeaders(self,headers)
def pagePart(self,data):
if self.status=='200':
self.currentLength+=len(data)
if self.totalLength:
percent="%i%%"%(
(self.currentLength/self.totalLength)*100)
else:
percent="%dK"%(self.currentLength/1000)
print "\033[1FProgress: "+percent
return client.HTTPDownloader.pagePart(self,data)
def downloadWithProgress(url,file,contextFactory=None,*args,**kwargs):
scheme,host,port,path=client._parse(url)
factory=HTTPProgressDownloader(url,file,*args,**kwargs)
if scheme=='https':
from twisted.internet import ssl
if contextFactory is None:
contextFactory=ssl.ClientContextFactory()
reactor.connectSSL(host,port,factory,contextFactory)
else:
reactor.connectTCP(host,port,factory)
return factory.deferred
if __name__=='__main__':
import sys
from twisted.internet import reactor
def downloadComplete(result):
print "Download Complete."
reactor.stop()
def downloadError(failure):
print "Error: ",failure.getErrorMessage()
reactor.stop()
url,outputFile=sys.argv[1:]
downloadWithProgress(url,outputFile).addCallback(
downloadComplete).addErrback(
downloadError)
reactor.run()
运行webdownload.py脚本并加上URL和存储文件名这两个参数。作为命令工作会打印出下载进度的更新。
$ python webdownload.py oreilly.html
Progress: 100% <- 下载过程中的更新
Download Complete.
如果WEB服务器并不返回Content-Length头字段,则无法计算下载进度。在这种情况下,webdownload.py打印已经下载的KB数量。
$ python webdownload.py slashdot.html
Progress: 60K <- 下载过程中更新
Download Complete.
3.5.2 它们如何工作?
HTTPProgressDownloader是client.HTTPDownloader的子类。重载了gotHeaders方法并检查Content-Length头字段用于计算下载总量。它还同时提供重载了pagePart方法,在每次接收到一块数据时发生,用来保持对下载量的跟踪。
每次有数据到达时,HTTPProgressDownloader打印进度报告。字符串\033[1F是一个终端控制序列,可以确保每次替换掉当前行。这个效果看起来可以在原位置更新。
downloadWithProgress函数包含了例子3-5中相同的代码用来转换URL,创建HTTPProgressDownloader工厂对象,并初始化连接。downloadComplete和downloadError是用于打印消息和停止反应器的简单回调函数。
4.0 WEB服务器
即使是很保守的说,现在的很多软件是基于WEB开发的。人们将大量时间花费在WEB浏览器上面,包括阅读HTML页面、电子邮件、管理日志、进入数据库的记录、更新Wiki页面和写weblog。
即使你不打算写严格的WEB应用,WEB界面也更加容易提供适合于跨平台的UI。在你的应用中包含轻量级的WEB服务器将会提供更多的附属功能。这一章将会展示如何使用Twisted开发一个WEB服务器,并介绍你一些构建WEB应用的方法。当然还提供了HTTP代理服务器。
这一章提供了一些HTTP协议的常识。这些一些构建WEB服务器所必须的知识。实际上,本书所涉及的HTTP知识还远远不够,还需要如《HTTP: The Definitive Guide》等等的书,还有不可替代的HTTP的RFC文档RFC2616()。
Twisted网络编程必备(十)
4.1 响应HTTP请求
HTTP是一个简单的协议接口。客户端发送请求,服务器端发送响应,然后关闭连接。你可以自己实现一个HTTP的Protocol来练习接收连接,读取请求,并发送HTTP格式的响应。
4.1.1 下面如何做?
每个HTTP请求开始于单行的HTTP方法,紧接着是URL,然后是HTTP版本。随后是一些行的头字段。一个空行标志着头字段的结束。头字段后面就是请求主体,例如提交的HTML表单数据。
如下是一个HTTP请求的例子。这个请求询问服务器通过GET方法获取资源,并使用HTTP版本1.1。
GET /index.html HTTP/1.1
Host:
服务器响应的第一行告知客户端响应的HTTP版本和状态码。有如请求一样,响应包含了头字段,并用空行隔开消息主体。如下是HTTP响应的例子:
HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 17
Connection: Close
Hello HTTP world!
设置简单的HTTP服务器,需要写一个Protocol允许客户端连接。查找空行标志着头字段的结束。然后发送HTTP响应,例子4-1展示了简单的HTTP实现。
from twisted.protocols import basic
from twisted.internet import protocol,reactor
class HttpEchoProtocol(basic,LineReceiver):
def __init__(self):
self.lines=[]
self.gotRequest=False
def lineReceived(self,line):
self.lines.append(line)
if not line and not self.gotRequest:
self.sendResponse()
self.gotRequest=True
def sendResponse(self):
responseBody="You said: \r\n\r\n"+"\r\n".join(self.lines)
self.sendLine("HTTP/1.0 200 OK")
self.sendLine("Content-Type: text/plain")
self.sendLine("Content-Length: %i"%len(responseBody))
self.sendLine("")
self.transport.write(responseBody)
self.transport.loseConnection()
f=protocol.ServerFactory()
f.protocol=HttpEchoProtocol
reactor.listenTCP(8000,f)
reactor.run()
运行webecho.py脚本启动服务器。你可以看到服务器运行在。你可以获得请求的回显,即原请求的报文。
4.1.2 它们如何工作?
HTTPEchoProtocol懂得如何响应每一个请求。从客户端收到的数据将会存储在self.lines中。当看到一个空行时,可以知道头字段结束了。它发送回一个HTTP响应。第一行包含了HTTP版本和状态码,在这种情况下,200是成功("OK"是附加的便于阅读的状态码描述)。下一对行是Content-Type和Content-Length头字段,用于告知客户端内容格式和长度。HTTPEchoProtocol发送一个空行来结束头字段,然后发送响应主体,回显客户端请求报文。
4.2 解析HTTP请求
HTTPEchoProtocol类提供了有趣的HTTP入门,但是距离使用功能还很遥远。它并不去分析请求头和资源位置,HTTP方法也只支持一种。如果想要建立一个真正的WEB服务器,你可以用一种更好的方式来解析和响应请求。下面的实现展示这些。
4.2.1 下面如何做?
写twisted.web.http.Request的子类并重载process方法来处理当前请求。Request对象已经包含了process需要调用的HTTP请求所有信息,所以只需要决定如何响应。例子4-2展示了如何运行基于http.Request的HTTP服务器。
from twisted.web import http
class MyRequestHandler(http.Request):
pages={
'/':'Home
Home Page',
'/test':'Test
Test Page',
}
def process(self):
if self.pages.has_key(self.path):
self.write(self.pages[self.path])
else:
self.setResponseCode(http.NOT_FOUND)
self.write("Not Found
Sorry, no such page.")
self.finish()
class MyHttp(http.HTTPChannel):
requestFactory=MyRequestHandler
class MyHttpFactory(http.HTTPFactory):
protocol=MyHttp
if __name__=='__main__':
from twisted.internet import reactor
reactor.listenTCP(8000,MyHttpFactory())
reactor.run()
运行requesthandler.py将会在8000端口启动一个WEB服务器。可以同时阅览主页()和测试页/test()。如果你指定了想要访问其他页面,将会得到错误信息。
Twisted网络编程必备(十一)
4.2.2 它们如何工作?
http.Request类解析了进入的HTTP请求并提供了制造响应的接口。在例子4-2中,MyRequestHandler是http.Request的一个子类提供了process方法。process方法将会被请求调用并接收。有责任在产生一个响应之后调用self.finish()来指出响应完成了。MyRequestHandler使用path属性来寻找请求路径。并在pages字典中指定匹配路径。如果匹配成功,MyRequestHandler使用write方法发送响应文本到响应。
注意write仅在写入响应的部分实体主体时才使用,而不是在生成原始HTTP响应时。setResponseCode方法可以用于改变HTTP状态码。twisted.web.http模块提供了所有HTTP状态码的定义,所以可用http.NOT_FOUND来代替404错误。
Tip:Request.setResponseCode带有一个可选的第二个参数,一个易读的状态消息。你可以感觉到很方便于twisted.web.http模块包含了内置列表描述普通的状态码,也就是缺省使用的。
Request类也提供setHeader方法来添加响应头字段。MyRequestHandler使用setHeader来设置Content-Type头为text/html,这个设置告诉浏览器对响应主体使用HTML格式。
twisted.web.http模块提供了两种附加类允许将Request的子类转换成功能性的WEB服务器。HTTPChannel类是一个Protocol,可以创建Request对象来应付每个连接。创建HTTPChannel时使用你的Request子类,重载requestFactory类属性。HTTPFactory是一个ServerFactory,添加附加特性,包含日志方法加上Request对象,这种日志格式包括Apache和其他多种日志格式。
4.3 处理POST数据和HTML表单
前面的实验展示了通过客户端请求生成静态HTML。这个实验展示了允许书写代码控制响应的生成,并且处理HTML表单提交的数据。
4.3.1 下面如何做?
写一个函数来处理Request对象并产生响应。设置字典来映射每一个可用路径到WEB站点来让函数出路路径请求。使用Request.args字典存取提交的HTML表单数据。例子4-3展示了生成单页HTML表单的WEB服务器,另一个页面是通过表单数据显示的。
from twisted.web import http
def renderHomePage(request):
colors='red','blue','green'
flavors='vanilla','chocolate','strawberry','coffee'
request.write("""
""")
request.finish()
def handlePost(request):
request.write("""
Form Data
""")
for key,values in request.args.items():
request.write("%s
"%key)
request.write("")
")
for value in values:
request.write("
request.write("
request.write("""
""")
request.finish()
class FunctionHandleRequest(http.Request):
pageHandlers={
'/':renderHomePage,
'/posthandler':handlePost,
}
def process(self):
self.setHeader("Content-Type","text/html")
if self.pageHandlers.has_key(self.path):
handler=self.pageHandlers[self.path]
handler(self)
else:
self.setResponseCode(http.NOT_FOUND)
self.write("Not Found
Sorry, no such page.")
self.finish()
class MyHttp(http.HTTPChannel):
requestFactory=FunctionHandledRequest
class MyHttpFactory(http.HTTPFactory):
protocol=MyHttp
if __name__=='__main__':
from twisted.internet import reactor
reactor.listenTCP(8000,MyHttpFactory())
reactor.run()
运行formhandler.py脚本。将会在8000端口运行WEB服务器。进入可以找到表单主页。按照如下填写一些字段信息。
然后点击提交按钮,你的浏览器将会发送表单数据到页面formhandler使用HTTP的POST请求。当它接受到表单数据时,formhandler回展示提交过的字段和值。
Twisted网络编程必备(十二)
4.3.2 它们是如何工作的?
例子4-3定义了两个函数来处理请求,renderHomePage和handlePost。FunctionHandleRequest是Request的子类,其属性pageHandler定义了路径映射功能。process方法查找路径,并尝试在pageHandlers中匹配路径。如果匹配成功,则FunctionHandleRequest传递自身到匹配函数,并且由对方负责处理;如果匹配失败,则返回404 Not Found响应。
renderHomePage函数设置处理器到/,站点的根路径。它生成的HTML表单将会提交数据到页面/formhandler。这个处理器函数/formhandler是handlePost,将会响应页面列表并提交数据。handlePost遍历值Request.args,这个字典属性包含了请求提交的所有数据。
Tip:在这种情况下,发送的表单数据在HTTP POST请求的主体中。当请求发送HTTP GET时,Request.args将会包含所有提交的URI查询字段值。你可以修改这个行为,通过改变表单生成器renderHomePage的method属性,从POST到GET,重启服务器,就可以重新提交表单。
一个HTML表单可以有多个字段具有相同的名字。例如,表单4-3中允许选中多个复选框,所有的名字都是flavor。不像很多其他的框架,http.Request并不对你隐藏什么:代替了映射字段名到字符串,Request.args映射每个字段值到列表。如果你知道要取哪一个值,那么可以只获取列表的第一个值。
4.4 管理资源等级
WEB应用中的路径通常使用分级目录管理。例如如下URL:
这里可以很清楚的看出等级划分。页面/people/charles是/people的子页面,而页面/people/charles/contact是/people/charles的子页面。等级中的每个页面都有特定的意义:/people/charles是一个人,而/people/charles/contact是一项数据,charles的数据。
WEB服务器的缺省行为是将PATH等级映射到磁盘上的文件。客户端的每次请求都是对应特定的资源,服务器查找文件并定位磁盘路径,然后将文件内容或者可执行文件的执行结果作为响应。在WEB应用中可以人为的生成一个对应路径文件的内容。例如,你的数据并没有存储在磁盘上,而是在一个关系数据库或者另一个服务器上。或者你想要在请求时自动创建资源。类似这种情况,最好的办法是为等级浏览创建自己的逻辑。
编写自己的资源管理逻辑可以帮助你管理安全。而不是打开WEB服务器的整个目录,你可以有选择的控制哪些文件是可以访问的。
4.4.1 下面如何做?
twisted.web.resource和twisted.web.static还有twisted.web.server模块提供了比twisted.web.http.Resource更高层次的请求管理类,你可以使用这些来设置一个WEB服务器来处理多种逻辑等级的资源。例子4-4使用了这些类来建立十六进制颜色代码。请求资源/color/hex,hex是十六进制的颜色代码,你可以得到一个背景为#hex的页面。对应每一种可能出现的颜色可能,服务器动态创建资源。
from twisted.web import resource,static,server
class ColorPage(resource.Resource):
def __init__(self,color):
self.color=color
def render(self,request):
return """
This is #%s.
""" % (self.color, self.color, self.color)
class ColorRoot(resource.Resource):
def __init__(self):
resource.Resource.__init__(self)
self.requestedColors=[]
self.putChild('',ColorIndexPage(self.requestColors))
def render(self,request):
# redirect /color -> /color/
request.redirect(request.path+'/')
return 'please use /colors/ instead.'
def getChild(self,path,request):
if path not in self.requestedColors:
self.requestedColors.append(path)
return ColorPage(path)
class ColorIndexPage(resource.Resource):
def __init__(self,requestColorsList):
resource.Resource.__init__(self)
self.requestedColors=requestedColorsList
def render(self,request):
request.write("""
Colors
To see a color, enter a url like
/color/ff0000.
Colors viewed so far:
""")
for color in self.requestedColors:
request.write(
"
color, color, color))
request.write("""
""")
return ""
class HomePage(resource.Resource):
def render(self,request):
return """
Colors Demo
What's here:
"""
if __name__=='__main__':
from twisted.internet import reactor
root=resource.Resource()
root.putChild('',HomePage())
root.putChild('color',ColorRoot())
root.putChild('styles.css',static.File('styles.css'))
site=server.Site(root)
reactor.listenTCP(8000,site)
reactor.run()
例子4-4引用了静态文件。所以需要在resourcetree.py脚本目录下创建一个styles.css文件。内容如下:
body {
font-family: Georgia, Times, serif;
font-size: 11pt;
}
h1 {
margin: 10px 0;
padding: 5px;
background-color: black;
color: white;
}
a {
font-family: monospace;
}
p {
padding: 10px;
}
运行resourcetree.py脚本,将会在8000端口启动一个WEB服务器。下面是服务器全部可用路径:
/ 主页
/css 虚拟的CSS资源
/css/styles.css 静态文件styles.css
/colors/ 颜色查看页面
/colors/hexcolor 按照背景色为#hexcolor的页面
尝试通过来访问,将会得到背景色为#00abef的页面,大约是亮蓝色。
可以随便试试其他颜色。同样可以进入,选择可选答案。
Twisted网络编程必备(十三)
4.4.2 它们如何工作?
例子4.4从twisted.web包中引入了几个类:resource.Resource、static.File、server.Site。每个resource.Resource对象做两件事。首先,定义请求的资源如何处理。第二,定义请求子资源的Resource对象。
例如查看类ColorRoot。在这个类的实例稍后被加入了/colors这个等级资源。初始化时,ColorRoot使用putChild方法插入ColorIndexPage这个资源座位''资源。这意味着所有对/colors/的请求都由ColorIndexPage对象来处理。
你可以把他们想象为等价的,但是/stuff和/stuff/是不同的。浏览器在解释相对路径时,对是否加上斜线的处理方法是不同的。在第一个例子中,对"otherpage"的请求会解释为"",在第二个例子中解释为""。
如果你不清楚(explicit)服务器代码,这个问题可能会再次郁闷你。最好是预先设计好是否需要在URI末尾加上斜线,并重定向请求。Resource类将会简化这些操作。如果设置了addSlash属性为True,一个Resource会自动在找不到对应资源时自动在URL末尾添加斜线来再次查找。
4.4 管理资源等级
4.6 运行HTTP代理服务器
除了HTTP服务器和客户端以外,twisted.web还包含了HTTP代理服务器的支持。一个代理服务器是一个服务器和一个客户端。他接受来自客户端的请求(作为服务器)并将他们转发到服务器(作为客户端)。然后将响应发送回客户端。HTTP代理服务器可以提供很多有用的服务:缓存、过滤和使用情况报告。下面的例子展示了如何使用Twisted构建一个HTTP代理服务器。
4.6.1 下面如何做?
twisted.web包包含了twisted.web.proxy,这个模块包含了HTTP代理服务器。例子4-7构建了一个简单的代理服务器。
from twisted.web import proxy,http
from twisted.internet import reactor
from twisted.python import log
import sys
log.startLogging(sys.stdout)
class ProxyFactory(http.HTTPFactory):
protocol=proxy.Proxy
reactor.listenTCP(8001,ProxyFactory())
reactor.run()
运行simpleproxy.py脚本将会在8001端口启动代理服务器。在浏览器中设置这个代理服务器可以作为代理进行测试。对log.startLogging的调用将会把HTTP日志信息记录在stdout中,并可以直接查看。
$ python simpleproxy.py
2005/06/13 00:22 EDT [-] Log opened.
2005/06/13 00:22 EDT [-] __main__.ProxyFactory starting on 8001
... ...
这虽然给出了一个代理服务器,但是实际上没什么用处。例子4-8提供了更多的功能,可以跟踪最常使用的网页。
import sgmllib.re
from twisted.web import proxy,http
import sys
from twisted.python import log
log.startLogging(sys.stdout)
WEB_PORT=8000
PROXY_PORT=8001
class WordParser(sgmllib.SGMLParser):
def __init__(self):
sgmllib.SGMLParser.__init__(self)
self.chardata=[]
self.inBody=False
def start_body(self,attrs):
self.inBody=True
def end_body(self):
self.inBody=False
def handle_data(self,data):
if self.inBody:
self.chardata.append(data)
def getWords(self):
#解出单词
wordFinder=re.compile(r'\w*')
words=wordFinder.findall("".join(self.chardata))
words=filter(lambda word: word.strip(), words)
print "WORDS ARE", words
return words
class WordCounter(object):
ignoredWords="the a of in from to this that and or but is was be can could i you they we at".split()
def __init__(self):
self.words=()
def addWords(self,words):
for word in words:
word=word.lower()
if not word in self.ignoredWords:
currentCount=self.words.get(word,0)
self.words[word]=currentCount+1
class WordCountProxyClient(proxy.ProxyClient):
def handleHeader(self,key,value):
proxy.ProxyClient.handleHeader(self,key,value)
if key.lower()=="content-type":
if value.split(';')[0]=='text/html':
self.parser=WordParser()
def handleResponsePart(self,data):
proxy.ProxyClient.handleResponsePart(self,data)
if hasattr(self,'parser'):
self.parser.feed(data)
def handleResponseEnd(self):
proxy.ProxyClient.handleResponseEnd(self)
if hasattr(self,'parser'):
self.parser.close()
self.father.wordCounter.addWords(self.parser.getWords())
del(self.parser)
class WordCountProxyClientFactory(proxy.ProxyClientFactory):
def buildProtocol(self,addr):
client=proxy.ProxyClientFactory.buildProtocol(self,addr)
#升级proxy.proxyClient对象到WordCountProxyClient
client.__class__=WordCountProxyClient
return client
class WordCountProxyRequest(proxy.ProxyRequest):
protocols={'http':WordCountProxyClientFactory)
def __init__(self,wordCounter,*args):
self.wordCounter=wordCounter
proxy.ProxyRequest.__init__(self,*args)
class WordCountProxy(proxy.Proxy):
def __init__(self,wordCounter):
self.wordCounter=wordCounter
proxy.Proxy.__init__(self)
def requestFactory(self,*args)
return WordCountProxyRequest(self.wordCounter,*args)
class WordCountProxyFactory(http.HTTPFactory):
def __init__(self,wordCount):
self.wordCounter=wordCounter
http.HTTPFactory.__init__(self)
def buildProtocol(self,addr):
protocol=WordCountProxy(self.wordCounter)
return protocol
#使用WEB接口展示记录的接口
class WebReportRequest(http.Request):
def __init__(self,wordCounter,*args):
self.wordCounter=wordCounter
http.Request.__init__(self,*args)
def process(self):
self.setHeader("Content-Type",'text/html')
words=self.wordCounter.words.items()
words.sort(lambda(w1,c1),(w2,c2): cmp(c2,c1))
for word,count in words:
self.write("
self.finish()
class WebReportChannel(http.HTTPChannel):
def __init__(self,wordCounter):
self.wordCounter=wordCounter
http.HTTPChannel.__init__(self)
def requestFactory(self,*args):
return WebReportRequest(self.wordCounter,*args)
class WebReportFactory(http.HTTPFactory):
def __init__(self,wordCounter):
self.wordCounter=wordCounter
http.HTTPFactory.__init__(self)
def buildProtocol(self,addr):
return WebReportChannel(self.wordCounter)
if __name__=='__main__':
from twisted.internet import reactor
counter=WordCounter()
prox=WordCountProxyFactory(counter)
reactor.listenTCP(PROXY_PORT,prox)
reactor.listenTCP(WEB_PORT,WebReportFactory(counter))
reactor.run()
运行wordcountproxy.py将浏览器的代理服务器设置到8001端口。浏览其他站点,或者访问,将可以看到访问过的站点的单词频率。
4.6.2 它是如何工作的?
在例子4-8中有很多个类,但大多数是连接用的。只有很少的几个做实际工作。最开始的两个类WordParser和WordCounter,用于从HTML文档中解出单词符号并计算频率。第三个类WordCountProxy客户端包含了查找HTML文档并调用WordParser的任务。
因为代理服务器同时作为客户端和服务器,所以需要使用大量的类。有一个ProxyClientFactory和ProxyClient,提供了Factory/Protocol对来支持客户端连接。为了响应客户端的连接,需要使用ProxyRequest,它是HTTPFactory的子类,还有Proxy,是http.HTTPChannel的子类。它们对建立一个普通的HTTP服务器是有必要的:HTTPFactory使用Proxy作它的协议(Protocol),而代理Proxy HTTPChannel使用ProxyRequest作为它的RequestFactory。如下是客户端发送请求的事件处理流程:
·客户端建立到代理服务器的连接。这个连接被HTTPFactory所处理。
·HTTPFactory.buildProtocol会创建一个Proxy对象用来对客户端发送和接收数据。
·当客户端发送请求时,Proxy创建ProxyRequest来处理。
·ProxyRequest查找客户端请求的服务器。并创建ProxyClientFactory并调用reactor.connectTCP来通过factory连接服务器。
·一旦ProxyClientFactory连接到服务器,就会创建ProxyClient这个Protocol对象来发送和接收数据。
·ProxyClient发送原始请求。作为响应,它将客户端发来的请求发送给服务器。这是通过调用self.father.transport.write实现的。self.father是一个Proxy对象,正在管理着客户端连接对象。
这是一大串类,但实际上是分工明确的将工作由一个类传递到另一个类。但这是很重要的。为代理模块的每一个类提供一个子类,你可以完成每一步的控制。
整个例子4-8中最重要的一个技巧。ProxyClientFactory类有一个buildProtocol方法,可以包装好,供ProxyClient作为Protocol。它并不提供任何简单的方法来提供你自己的ProxyClient子类。解决办法是使用Python的__class__属性来替换升级ProxyClient对象来放回ProxyClientFactory.buildProtocol,这些将ProxyClient改变为WordCountProxyClient。
作为代理服务器的附加功能。例子4-8提供了标准的WEB服务器,在8000端口,可以显示从代理服务器来的当前单词统计数量。由此可见,在你的应用中包含一个内嵌的HTTP服务器是多么的方便,这种方式可以很好的提供给远程来显示相关状态信息。