本文内容全部出自《Python基础教程》第二版,在此分享自己的学习之路。
______欢迎转载:http://www.cnblogs.com/Marlowes/p/5538341.html______
Created on Marlowes
本章将会给读者展示一些例子,这些例子会使用多种Python的方法编写一个将网络(比如因特网)作为重要组成部分的程序。Python是一个很强大的网络编程工具,这么说有很多原因,首先,Python内有很多针对常见网络协议的库,在库顶部可以获得抽象层,这样就可以集中精力放在程序的逻辑处理上,而不是停留在网络实现的细节中。其次,虽然现在还没有现成可用的代码来处理各种协议格式,但使用Python很容易写出这样的代码,因为Python在处理字节流的各种模式方面很擅长(在学习处理文本文件的各种方式时应有所体会了)。
使用Python提供了如此丰富的网络工具,所以对于Python的网络编程我只进行简要介绍。可以在本书的其他地方找到一些例子。第十五章包括面向Web的网络编程得相关讨论,后面的章节中有很多项目使用了网络模块来完成工作。如果想要了解更多Python中的网络编程,我衷心推荐John Goerzen的Foundations of Python Network Programming,这本书对这个主题讨论得很详细。
在下面的部分中,我会先大体介绍Python标准库中可用的一些网络模块。然后讨论SocketsServer类和它的“朋友们”,接着是同时能处理多个连接的各种方法。最后看看Twisted框架——Python中一个丰富、成熟的、用于编写网络应用程序的框架。
注:如果计算机上安装了防火墙,那么它可能会在每次开始运行网络程序时发出警告。它还可能会阻止程序连接到网络。这时应该配置防火墙来允许Python完成工作。如果防火墙有一个交互接口(比如Windows XP的防火墙),那么只要在请求的时候允许连接就行了。注意,任何连接到网络的软件都有潜在的风险,即使(尤其)是你自己写的软件也不例外。
14.1 少数几个网络设计模块
在标准库中有很多与网络有关的模块,在其他地方还有更多。除了那些明确处理网络事务的模块外,还有很多模块(比如用于在网络传输中处理各种形式的数据编码的模块)被认为是网络相关的。模块部分的讨论限定于如下的几个模块。
14.1.1 socket模块
在网络编程中的一个基本组件就是套接字(socket)。套接字基本上是两个端点的程序之间的“信息通道”。程序可能分布在不同的机器上(通过网络连接),通过套接字相互发送信息。在Python中的大多数的网络编程都隐藏了socket模块的基本细节,不直接和套接字交互。
套接字包括两个:服务器套接字和客户机套接字。在创建一个服务器套接字后,让它等待连接。这样它就在某个网络地址处(IP地址和一个端口号的组合)监听。直到有客户端套接字连接。连接完成后,两者就可以进行交互了。
处理客户端套接字通常比处理服务器套接字更容易,因为服务器必须准备随时处理客户端的连接,同时还要处理多个连接,而客户机只是简单地连接,完成事务,断开连接。在这章后面,我会使用socketserver类族和Twisted框架来处理服务器端的编程。
一个套接字就是socket模块中的socket类的一个实例。它的实例化需要3个参数:第1个参数是地址族(默认是socket.AF_INET);第2个参数是流(socket.SOCK_STREAM,默认值)或数据报(sock.SOCK_DGRAM)套接字(还有其他套接字,例如原始套接字SOCK_RAW等);第3个参数是使用的协议(默认是0,使用默认值即可)。对于一个普通的套接字,不需要提供任何参数。
服务器端套接字使用bind方法后,再调用listen方法去监听某个特定的地址。客户端套接字使用connect方法连接到服务器。在connect方法中使用的地址与服务器在bind方法中的地址相同(在服务器端,能实现很多功能,比如使用函数socket.gethostname得到当前主机名)。在这种情况下,一个地址就是一个格式为(host,port)的元组,其中host是主机名(比如www.example.com),port是端口号(一个整数)。listen方法只有一个参数,即服务器未处理的连接的长度(即允许排队等待的连接数目,这些连接在禁用之前等待)。
服务器端套接字开始监听后,它就可以接受客户端的连接。这个步骤使用accept方法来完成。这个方法会阻塞(等待)直到客户端连接,然后该方法就返回一个格式为(client,address)的元组,client是一个客户端套接字,address是一个前面解释过的地址。服务器在处理完与该客户端的连接后,再次调用accept方法开始等待下一个连接。这个过程通常都是在一个无限循环中实现的。
注:这种形式的服务器端编程称为阻塞或者是同步网络编程。14.3节会介绍一些非阻塞或者叫异步网络编程的例子,例子中还使用了线程来同时处理多个客户机。
套接字有两个方法:send和recv(用于接收),用于传输数据。可以使用字符串参数调用send以发送数据,用一个所需的(最大)字节数做参数调用recv来接受数据。如果不能确定使用哪个数字比较好,那么1024是个很好的选择。
代码清单14-1和代码清单14-2展示了一个最简单的客户机/服务器程序。如果在同一台机器上运行它们(先启动服务器),服务器会打印出发现一个连接的信息。然后客户机打印出从服务器端接收到的信息。可以在服务器运行时同时运行多个客户机。通过服务器端所在机器的实际主机名来代替客户端调用gethostname所得到的主机名,就可以让两个运行在不同机器上的程序通过网络连接起来。
注:使用端口号一般是被限制的。在Linux或者UNIX系统中,需要有系统管理员的权限才能使用1024以下的端口。这些低于1024的端口号被用于标注服务,比如端口80用于Web服务(如果你有的话)。如果用Ctrl+C停止了一个服务,可能要等上一段时间才能使用同一个端口号(否则可能会得到“地址正在使用”的错误信息)。
# 代码清单14-1 一个小型服务器 import socket s = socket.socket() host = socket.gethostname() port = 1234 s.bind((host, port)) s.listen(5) while True: c, addr = s.accept() print "Got connection from", addr c.send("Thank you for connecting") c.close() # 代码清单14-2 一个小型客户机 import socket s = socket.socket() host = socket.gethostname() port = 1234 s.connect((host, port)) print s.recv(1024)
可以在Python库参考文档(http://python.org/doc/lib/module_socket.html)和Gordon McMillan的Socket Programming HOWTO(http://doc.python.org/dev/howto/sockets.html)中找到更多的关于socket模块的内容。
14.1.2 urllib和urllib2模块
在能使用的各种网络函数库中,功能最强大的可能是urllib和urllib2了。通过它们在网络上访问文件,就好像访问本地电脑上的文件一样。通过一个简单的函数调用,几乎可以把任何URL所指向的东西用作程序的输入。想象一下,如果将这两个模块与re模块结合使用的效果:可以下载Web页面,提取信息,以及自动生成报告等。
这两个模块的功能都差不多,但urllib2更好一些。如果只使用简单的下载,urllib就足够了。如果需要使用HTTP验证(HTTP authentication)或cookie,或者要为自己的协议编写扩展程序的话,那么urllib2是更好的选择。
1.打开远程文件
可以像打开本地文件一样打开远程文件,不同之处是只能使用读模式。使用来自urllib模块的urlopen,而不是open(或file):
>>> from urllib import urlopen >>> webpage = urlopen("http://www.python.org")
如果电脑连接到了网络,变量webpage现在应该包含一个链接到http://www.python.org网页的类文件对象。
注:如果想要试验urllib但现在没有连接网络,可以用以file开头的URL访问本地文件,比如file:c:\text\somefile.txt(记得要对"\"进行转义)。
urlopen返回的类文件对象支持close、read、readline和readlines方法,当然也支持迭代。
假设想要提取在前面打开的Python页中"About"链接的(相对)URL,那么就可以用正则表达式来实现(有关正则表达式的更多内容请参见第十章的re模块部分)。
>>> import re >>> text = webpage.read() >>> m = re.search(‘<a href="([^"]+)" .*?>about</a>‘, text, re.IGNORECASE) >>> m.group(1) ‘/about/‘
注:如果网页内容发生了变化,读者需要自行修改正则表达式。
2.获取远程文件
函数urlopen返回一个能从中读取数据的类文件对象。如果希望urllib为你下载文件并在本地中存储一个文件的副本,那么可以使用urlretrieve。urlretrieve返回一个元组(filename, headers)而不是类文件对象,filename是本地文件的名字(由urllib自动创建),headers包含一些远程文件的信息(我在这里忽略headers,如果要了解更多的相关信息,请在urllib的标准库文档中查找urlretrieve)如果想要为下载的副本指定文件名,可以在urlretrieve函数的第2个参数中给出。
urlretrieve(‘http://www.python.org‘, ‘C:\\python_webpage.html‘)
这个语句获取Python的主页并把它存储在文件C:\python_webpage.html中。如果没有指定文件名,文件就会放在临时的位置,用open函数可以打开它,但如果完成了对它的操作,就可以删除它以节省硬盘空间。要清理文件时,可以调用urlcleanup函数,但不要提供参数,该函数会负责清理工作。
一些功能
除了通过URL读取和下载文件,urllib还提供了一些函数操作URL本身(下面假设你知道URL和CGI的基础知识),这些函数如下。
? quote(string[, safe]):它返回一个字符串,其中所有的特殊字符(这些字符在URL中有特殊含义)都已被对URL友好的字符所代替(就像用%7E代替了~)。如果想在URL中使用一个包含特殊字符的字符串,这个函数就很有用。safe字符串值包含乐不应该采用这种方式编码的字符。默认是"/"。
? quote_plus(string[, safe]):功能和quote差不多,但用+代替空格。
? unquote(string):和quote相反。
? unquote_plus(string):和quote_plus相反。
? urlencode(query[, doseq]):把映射(比如字典)或者包含连个元素的元组的序列——(key, value)这种形式——转换成URL格式编码的字符串,这样的字符串可以在CGI查询中使用(Python文档中有更多的信息)。
14.1.3 其他模块
就像在前面提过的,除了在本章提及的模块外,Python库和其他地方还有很多和网络有关的模块,表14-1列出了Python标准库中一些和网络相关的模块。如表格中说明的,本书的其他章节会讨论其中一些模块。
表14-1 标准库中一些与网络相关的模块
asynchat asyncore的增强版本(参看第二十四章)
asyncore 异步套接字处理程序(参看第二十四章)
cgi 基本的CGI支持(请参看第十五章)
Cookie Cookie对象操作,主要用于服务器
cookielib 客户端cookie支持
email E-mail消息支持(包括MIME)
ftplib FTP客户端模块
gopherlib gopher客户端模块
httplib HTTP客户端模块
imaplib IMAP4客户端模块
mailbox 读取几种邮箱的格式
mailcap 通过mailcap文件访问MIME配置
mhlib 访问MH邮箱
nntplib NNTP客户端模块(参看第二十三章)
poplib POP客户端模块
robotparser 支持解析Web服务器的robot文件
SimpleXMLRPCServer 一个简单的XML-RPC服务器(参看第二十七章)
smtpd SMTP服务器端模块
smtplib SMTP客户端模块
telnetlib Telnet客户端模块
urlparse 支持解释URL
xmlrpclib XML-RPC的客户端支持(参看第二十七章)
14.2 SocketServer和它的朋友们
正如在前面的socket模块部分看到的一样,写一个简单的套接字服务器真的不是很难。如果想实现超出基础的应用,那么,最好还是寻求些帮助。SocketServer模块是标准库中很多服务器框架的基础,这些服务器框架包括BaseHTTPServer、SimpleHTTPServer、CGIHTTPServer、SimpleXMLRPCServer和DocXMLRPCServer,所有的这些服务器框架都为基础服务器增加了特定的功能。
SocketServer包含了4个基本的类:针对TCP流式套接字的TCPServer;针对UDP数据报套接字的UDPServer;以及针对性不强的UnixStreamServer和UnixDatagramServer。通常可能很少用到后3个。
如果要编写一个使用SocketServer框架的服务器,可能会将大部分代码放在一个请求处理程序(request handler)中。每当服务器接收到一个请求(来自客户端的连接)时,就会实例化一个请求处理程序,并且它的各种处理方法(handler method)会在处理请求时被调用。具体调用哪个方法取决于特定的服务器和使用的处理程序类(handler class)。还可以把它们子类化,使得服务器调用自定义的处理程序集。BaseRequestHandler类把所有的操作都放到了处理器的一个叫做handler的方法中,这个方法会被服务器调用。然后这个方法就会访问属性self.request中的客户端套接字。如果使用的是流(如果使用的是TCPServer,这就是可能的),那么可以使用StreamRequestHandler类,创建了其他两个新属性,self.rfile(用于读取)和self.wfile(用于写入)。然后就能使用这类文件对象和客户机进行通信。
SocketServer框架中的其他类实现了对HTTP服务器的基本支持。其中包括运行CGI脚本,也包括对XML RPC(在第二十七章讨论)的支持。
代码清单14-3展示了代码清单14-1中的小型服务器的SocketServer版本。它能和代码清单14-2中的客户机协同工作。注意,StreamRequestHandler在连接被处理完后会负责关闭连接。还要注意使用‘‘表示的是服务器正在其上运行的机器的主机名。
# 代码清单14-3 一个基于SocketServer的小型服务器 from SocketServer import TCPServer from SocketServer import StreamRequestHandler class Handler(StreamRequestHandler): def handle(self): addr = self.request.getpeername() print "Got connecting from", addr self.wfile.write("Thank you for connection") server = TCPServer(("", 1234), Handler) server.serve_forever()
在Python库参考文档(http://python.org/doc/lib/modules-SocketServer.html)和John Goerzen的The Foundations of Python Network Programming中可以找到关于SocketServer框架的更多信息。
14.3 多个连接
到目前为止讨论的服务器解决方案都是同步的:即一次只能连接一个客户机并处理它的请求。如果每个请求只是花费很少的时间,比如,一个完整的聊天会话,那么同时能处理多个连接就很重要。
有3种主要的方法能实现这个目的:分叉(forking)、线程(threading)以及异步I/O(asynchronous I/O)。通过对SocketServer服务器(如代码清单14-4和代码清单14-5所示)使用混入类(mix-in class),派生进程和线程很容易处理。即使要自己实现它们,这些方法也很容易使用。它们确实有缺点:分叉占据资源,并且如果有太多的客户端时分叉不能很好分叉(尽管如此,对于合理数量的客户端,分叉在现代的UNIX或者Linux系统中是很高效的,如果有一个多CPU系统,那效率会更高);线程处理能导致同步问题。我在这里不会深入讨论这些问题的任何细节(我也不会深入讨论多线程),但我在接下来的几节中会向你展示这些技术。
什么是分叉和线程处理
或许你不知道什么是分叉和线程处理,这里做一些说明。分叉是一个UNIX术语:当分叉一个进程(一个运行的程序)时,基本上是复制了它,并且分叉后的两个进程都从当前的执行点继续运行,并且每个进程都有自己的内存副本(比如变量)。一个进程(原来的那个)成为父进程,另一个(复制的)成为子进程。如果你是一个科幻小说迷,可以把它们想象成并行宇宙(parallel universe)。分叉操作在时间线(timeline)上创建了一个分支,最后得到了两个独立存在的进程。幸好进程可以判断哪个是原进程哪个是子进程(通过查看fork函数的返回值)。因此它们所执行的操作不同(如果相同,那么还有什么意义?两个进程都会做同样的工作,会使你自己的电脑停顿)。
在使用一个分叉的服务器中,每一个客户端连接都要利用分叉创造一个子进程。父进程继续监听新的连接,同时子进程处理客户端。当客户端的请求结束时,子进程就退出了。因为分叉的进程是并行运行的,客户端之间不必互相等待。
因为分叉有点耗费资源(每个分叉出来的进程都需要自己的内存),这就出现了另一种方法:线程。线程是轻量级的进程或子进程,所有的线程都存在于相同的(真正的)进程中,共享内存。资源消耗的下降伴随着一个缺陷:因为线程共享内存,所以必须确保它们的变量不会冲突,或者是在同一时间修改同一内容,这就会造成混乱。这些问题都可以归结为同步问题。在现代操作系统中(除了Windows,它不支持分叉),分叉实际是很快的,现代的硬件能比以往更好地处理资源消耗。如果不想被同步问题所困扰,分叉是一个很好的选择。
能避免这类并行问题最好不过了。本章后面会看到基于select函数的其他解决方法。避免线程和分叉的另外一种方法是转换到Stackless Python(http://stackless.com),一个为了能够在不同的上下文之间快速、方便切换而设计的Python版本。它支持一个叫做微线程(microthreads)的类线程的并行形式,微线程比真线程的伸缩性要好。比如,Stackless Python微线程已经被用在星战前夜在线(EVE Online,http://www.eve-online.com)来为成千上万的使用者服务。
异步I/O在底层的实现有点难度。基本的机制是select模块的select函数(14.3.2节会介绍),这是非常难处理的。幸好存在更高的层次处理异步I/O的框架,这就为处理一个强大可伸缩的机制提供了一个简单的、抽象的接口。包括在标准库中的这种类型的基本框架由asyncore和asynchat模块组成,这会在第二十四章讨论。Twisted(在本章的最后讨论)是一个非常强大的异步网络编程框架。
14.3.1 使用SocketServer进行分叉和线程处理
使用SocketServer框架创建分叉或者线程服务器太简单了,几乎不需要解释。代码清单14-4和代码清单14-5展示了如何让代码清单14-3中的服务器使用分叉和线程。如是handle方法要花很长时间完成,那么分叉和线程行为就很有用。注意,Windows不支持分叉。
# 代码清单14-4 使用了分叉技术的服务器 from SocketServer import TCPServer, ThreadingMixIn, StreamRequestHandler class Server(ThreadingMixIn, TCPServer): pass class Handler(StreamRequestHandler): def handle(self): address = self.request.getpeername() print "Got connection from", address self.wfile.write("Thank you for connecting") server = Server(("", 1234), Handler) server.serve_forever() # 代码清单14-5 使用了线程处理的服务器 from SocketServer import TCPServer, ThreadingMixIn, StreamRequestHandler class Server(ThreadingMixIn, TCPServer): pass class Handler(StreamRequestHandler): def handle(self): address = self.request.getpeername() print "Got connection from", address self.wfile.write("Thank you for connecting") server = Server(("", 1234), Handler) server.serve_forever()
14.3.2 带有select和poll的异步I/O
当一个服务器与一个客户端通信时,来自客户端的数据可能不是连续的。如果使用分叉或线程处理,那就不是问题。当一个程序在等待数据,另一个并行的程序可以继续处理它们自己的客户端。另外的处理方法是只处理在给定时间内真正要进行通信的客户端。不需要一直监听,只要监听(或读取)一会儿,然后把它放到其他客户端的后面。
这是asyncore/asynchat(参见第二十四章)框架和Twisted框架(参加下一节)从采用的方法,这种功能的基础是select函数,如果poll函数可用,那也可以是它,这两个函数都来自select模块。这两个函数中,poll的伸缩性更好,但它只能在UNIX系统中使用(这就是说,在Windows中不可用)。
select函数需要3个序列作为它的必选参数,此外还有一个可选的以秒为单位的超时时间作为第4个参数。这些序列是文件描述符整数(或者是带有返回这样整数的fileno方法的对象)。这些就是我们等待的连接。3个序列用于输入、输出以及异常情况(错误以及类似的东西)。如果没有给定超时时间,select会阻塞(也就是处于等待状态),直到其中的一个文件描述符已经为行动做好了准备;如果给定了超时时间,select最多阻塞给定的超时时间,如果给定的超时时间是0,那么就给出一个连续的poll(即不阻塞)。select的返回值是3个序列(也就是一个长度为3的元组),每个代表相应参数的一个活动子集。比如,返回的第1个序列是一个输入文件描述符的序列,其中有一些可以读取的东西。
序列能包含文件对象(在Windows中行不通)或套接字。代码清单14-6展示了一个使用select的为很多连接服务的服务器(注意,服务器套接字本身被提供给select,这样select就能在准备接受一个新的连接时发出通知)。服务器是个简单的记录器(logger),它输出(在本地)来自客户机的所有数据。可以使用Telnet(或者写一个简单的基于套接字的客户机来为它提供数据)连接它来进行测试。尝试用多个Telnet去连接来验证服务器能同时为多个客户端服务(服务器的日志会记录其中两个客户端的输入信息)。
# 代码清单14-6 使用了select的简单服务器 import socket import select s = socket.socket() host = socket.gethostname() port = 1234 s.bind((host, port)) s.listen(5) inputs = [s] while True: rs, ws, es = select.select(inputs, [], []) for r in rs: if r is s: c, address = s.accept() print "Got connection from", address inputs.append(c) else: try: data = r.recv(1024) disconnected = not data except socket.error: disconnected = True if disconnected: print r.getpeername(), "disconnected" inputs.remove(r) else: print data
poll方法使用起来比select简单。在调用poll时,会得到一个poll对象。然后就可以使用poll对象的register方法注册一个文件描述符(或者是带有fileno方法的对象)。注册后可以使用unregister方法移除注册的对象。注册了一些对象(比如套接字)以后,就可以调用poll方法(带有一个可选的超时时间参数)并得到一个(fd, event)格式列表(可能是空的),其中fd是文件描述符,event则告诉你发生了什么。这是一个位掩码(bitmask),意思是它是一个整数,这个整数的每个位对应不同的时间。那些不同的事件是select模块的常量,在表14-2中会进行解释,为了验证是否设置了一个给定位(也就是说,一个给定的时间是否发生了),可以使用按位与操作符(&):
if event & select.POLLIN: ...
表14-2 select模块中的polling事件常量
POLLIN 读取来自文件描述符的数据
POLLPRI 读取来自文件描述符的紧急数据
POLLOUT 文件描述符已经准备好数据,写入时不会发生阻塞
POLLERR 与文件描述符有关的错误情况
POLLHUP 挂起,连接丢失
POLLNVAL 无效请求,连接没有打开
代码清单14-7中的程序时代码清单14-6中的服务器的重写,现在使用poll来代替select。注意我添加了一个从文件描述符(ints)到套接字对象的映射(fdmap)。
# 代码清单14-7 使用poll的简单服务器 import socket import select s = socket.socket() host = socket.gethostname() port = 1234 s.bind((host, port)) fdmap = {s.fileno(): s} s.listen(5) p = select.poll() p.register(s) while True: events = p.poll() for fd, event in events: if fd == s.fileno(): c, address = s.accept() print "Got connection from", address p.register(c) fdmap[c.fileno()] = c elif event & select.POLLIN: data = fdmap[fd].recv(1024) if not data: # Not data -- disconnected print fdmap[fd].getpeername(), "disconnected" p.unregister(fd) del fdmap[fd] else: print data
更多信息
在Python库参考文档(http://python.org/doc/lib/module-select.html)中可以找到更多的关于select和poll的信息。也可以阅读标准库中的asyncore和asynchat模块的源代码(位于Python安装程序的asyncore.py和asynchat.py文件中)。
14.4 Twisted
来自于Twisted Matrix实验室(http://twistedmatrix.com)的Twisted是一个事件驱动的Python网络框架,原来是为网络游戏开发的,现在被所有类型的网络软件使用。在Twisted中,需要实现事件处理程序,这很像在GUI工具包(参见第十二章)中做的那样。实际上,Twisted能很好地和几个常见的GUI工具包(Tk、GTK、Qt以及wxWidgets)协同工作。本节会介绍一些基本概的概念并且展示如何使用Twisted来做一些相对简单的网络编程。掌握了基本概念以后,就能根据Twisted的文档去做一些更高级的网络编程。Twisted是一个非常丰富的框架,并且支持Web服务器、客户机、SSH2、SMTP、POP3、IMAP4、AIM、ICQ、IRC、MSN、Jabber、NNTP和DNS,等等。
14.4.1 下载并安装Twisted
安装Twisted很容易。首先访问Twisted Matrix的网页(http://twistedmatrix.com),然后点击下载链接。如果使用的是Windows的话,那么下载与Python版本对应的Windows安装程序,如果使用的是其他的系统那就下载源代码档案文件(如果使用了包管理器,比如Portage、RPM、APT、Fink或者MacPorts,那就可以直接下载并安装Twisted)。Windows安装程序是无须说明的按步进行的安装向导,他可能会花些时间解压缩和编译,你要做的就是等待。为了安装源代码档案文档,首先要解压缩(使用tar,然后根据下载的是哪种类型的档案文件来决定使用gunzip还是bunzip2)然后运行Distutils脚本:
python setup.py install
应该可以使用Twisted了。
14.4.2 编写Twisted服务器
这章之前编写的基本套接字服务器是显式的。其中的一些有很清楚的事件循环,用来查找新的连接和新数据,而基于SocketServer的服务器有一个隐式的循环,在循环中服务器查找连接并为每个连接创建一个处理程序,但处理程序在要读数据时必须是显式的。Twisted(以及在第二十四章讨论的asyncore/asynchat框架)使用一个事件甚至多个基于事件的方法。要编写基本的服务器,就要实现处理比如新客户端连接、新数据到达以及一个客户端断开连接等事件的事件处理程序。具体的类能通过基本类尽力更精炼的事件,比如包装“数据到达”事件、收集数据直到新的一行,然后触发“一行数据到达”的事件。
注:有一个类似Twisted特征的内容我在本节没有处理,那就是延迟和延迟执行,请参考Twisted文档以了解更多信息(比如可以看Twisted文档中的HOWTO页中名为"Deferreds are beautiful"的教程)。
事件处理程序在一个协议(protocol)中定义;在一个新的连接到达时,同样需要一个创建这种协议对象的工厂(factory),但如果只是想要创建一个通用的协议类的示例,那么就可以使用Twisted自带的工厂。factory类在twisted.internet.protocol模块中。当编写自己的协议时,要使用和超类一样的模块中的protocol。得到了一个连接后,事件处理程序connectionMade就会被调用;丢失了一个连接后,connectionLost就会被调用。来自客户端的数据是通过处理程序dataReceived接收的。当然不能使用事件处理策略来把数据发回到客户端,如果要实现此功能,可以使用对象self.transort,这个对象有一个write方法,也有一个包含客户机地址(主机名和端口号)的client属性。
代码清单14-8包含代码清单14-6和代码清单14-7中服务器的Twisted版本。希望读者也会觉得Twisted版本更简单、更易读。这里只涉及一点设置,必须实例化factory,还要设置它的protocol属性,这样它在和客户机通信时就知道使用什么协议(自定义协议)。然后就开始在给定的端口处使用工厂进行监听,这个工厂要通过实例化协议对象来准备处理连接。程序使用的是reactor中的listenTCP函数来监听,最后通过调用同一个模块中的run函数启动服务器。
# 代码清单14-8 使用Twisted的简单服务器 from twisted.internet import reactor from twisted.internet.protocol import Protocol, Factory class SimpleLogger(Protocol): def connectionMade(self): print "Got connection from", self.transport.client def connectionLost(self, reason): print self.transport.client, "disconnected" def dataReceived(self, data): print data factory = Factory() factory.protocol = SimpleLogger reactor.listenTCP(1234, factory) reactor.run()
如果用Telnet连接到此服务器并进行测试的话,那么每行可能只输出一个字符取决于缓冲或类似的东西。当然可以使用sys.stdout.write来代替print。但在很多情况下可能更喜欢每次得到一行,而不是任意的数据。编写一个处理这种情况的自定义协议很容易,实际上已经有一个现成的类了。twisted.protocols.basic模块中包含一个有用的预定义协议,是LineReceiver。它实现了dataReceived并且只要收到了一整行就调用事件处理程序lineReceived。
注:如果要在接受数据时做些事情,可以使用由LineReveiver定义的叫做rawDataReceived的事件处理程序,也可以使用lineReceived,但它依赖于dataReceived的实现LineReceiver。
转换协议只需要很少的工作。代码清单14-9展示了转换的结果。如果在运行服务器时查看输出结果,会看到换行符被去掉了,换句话说,使用print不再提供两倍的换行符。
# 代码清单14-9 一个使用了LineReceiver协议改进的记录服务器 from twisted.internet import reactor from twisted.internet.protocol import Factory from twisted.protocol.basic import LineReceiver class SimpleLogger(LineReceiver): def connectionMade(self): print "Got connection from", self.transport.client def connectionLost(self, reason): print self.transport.client, "disconnected" def lineReceived(self, line): print line factory = Factory() factory.protocol = SimpleLogger reactor.listenTCP(1234, factory) reactor.run()
关于Twisted框架的内容有很多我没有讲到。如果想要学习更多知识,应该查看在线的文档,文档可以在Twisted的网站(http://twistedmatrix.com)上找到。
14.5 小结
本章介绍了Python中网络编程中的一些方法。究竟选择什么方法取决于程序特定的需要和开发者的偏好。选择了某种方法后,就需要了解具体方法的更多内容。本章介绍的内容如下。
? 套接字和socket模块:套接字程序(进程)之间进行通信的信息通道,可能会通过网络来通信。socket模块给提供了对客户端和服务器端套接字的低级访问功能。服务器端套接字会在指定的地址监听客户端的连接,而客户机是直接连接的。
? urllib和urllib2:这些模块可以在给出数据源的URL时让从不同的服务器读取和下载数据。urllib模块是一个简单一些的实现,而urllib2是可扩展的,而且很强大。两者都通过urlopen等简单的函数来工作。
? SocketServer框架:这是一个同步的网络服务器基类。位于标准库中,使用它可以很容易地编写服务器。它甚至用CGI支持简单的Web服务(HTTP)。如果想同时处理多个连接,可以使用分叉和线程来处理混入类。
? select和poll:这两个函数让你可以考虑一组连接并且能找出已经准备好读取或者写入的连接。
? Twisted:这是来自Twisted Matrix实验室的框架,支持绝大多数的网络协议,它内容丰富并且很复杂,尽管很庞大,有的习惯用语却不太容易记,但它的基本用法简单、直观。Twisted框架是异步的,因此它在伸缩性和效率方面表现的很好。如果能使用Twisted,它可能很多自定义网络应用程序的最佳选择。
14.5.1 本章的新函数
本章涉及的新函数如表14-3所示。
表14-3 本章的新函数
urllib.urlopen(url[, data[, proxies]]) 通过URL打开一个类文件对象
urllib.urlretrieve(url[, fname[, hook[, data]]]) 通过URL下载一个文件
urllib.quote(string[, safe]) 引用特定的URL字符
urllib.quote_plus(string[, safe]) 和quote相同,但是将空格引用为+
urllib.unquote(string) 和quote相反
urllib.unquote_plus(string) 和quote_plus相反
urllib.urlencode(query[, doseq]) 在CGI请求中使用的编码映射
select.select(iseq, oseq, eseq[, timeout]) 找出准备好读取/写入的套接字
select.poll() 为polling套接字创建一个poll对象
reactor.listenTCP(port, factory) Twisted函数,监听连接
reactor.run() Twisted函数,主服务器循环
14.5.2 接下来学什么
所有关于网络程序设计的内容都介绍完了吗?不可能,下一章会处理网络世界中的一个非常具体并且更为人们熟悉的实体:Web。