三、使用低级的socket通信 |
4、使用socket选项
socket对象的getpeername()和getsockname()方法都返回包含一个IP地址和端口的二元组(这个二元组的形式就像你传递给connect和bind的)。getpeername返回所连接的远程socket的地址和端口,getsockname返回关于本地socket的相同信息。
在默认情况下,socket是阻塞式的,意思就是socket的方法的调用在任务完成之前是不会返回的。例如,如果存储向外发送的数据的缓存已满,你又企图发送更多的数据,那么你对send的调用将被阻塞直到它能够将更多的数据放入缓存。你可以通过调用setblocking(flag)方法(其中flag取值是0,setblocking(0))来改变这个默认行为,以使socket为非阻塞式。当socket为非阻塞式的时候,如果所做的动作将导致阻塞,将会引起error异常。下面一段代码将试图不断地接受新的连接并使用函数processRequest来处理。如果一个新连接无效,它将间隔半秒再试。另一方法是在你的监听socket上调用select或poll来检测一个新的连接的到达。
别的socket的选项可以使用setsockopt(level,name,value)和getsockopt(level,name[,buflen])方法来设置和获取。socket代表了一个协议栈的不同层,level参数指定了选项应用于哪一层。level的取值以SOL_开头(SOL_SOCKET,SOL_TCP等等)。name表明你涉及的是哪个选项。对于value,如果该选项要求数值的值,value只能传入数字值。你也可以传递入一个缓存(一个字符串),但你必须使用正确的格式。对getsockopt,不指定buflen参数意味你要求一个数字值,并返回这个值。如果你提供了buflen,getsockopt返回代表一个缓存的字符串,它的最大长度是buflen的字节数。下面的例子设置了一个socket的用于发送的缓存尺寸为64KB:
>>> s=socket(AF_INET,SOCK_STREAM)
>>> s.setsockopt(SOL_SOCKET,SO_SNDBUF,65535)
要得到一个包在被路由丢弃前所能有的生命周期(TTL)和跳数,你可以使用如下代码:
>>> s.getsockopt(SOL_IP,IP_TTL)
32
5、数值转换
由于不同平台的字节顺序不一样,所以当在网络中传输数据时我们使用标准的网络字节顺序。nthol(x)和ntohs(x)函数要求一个网络字节顺序的数值并把它转换为当前主机字节顺序的相同数值,而htonl(x)和htons(x)则相反:
>>> import.socket
>>> socket.htons(20000) #转换为一个16位的值
8270
>>> socket.htonl(20000) #转换为一个32位的值
541982720
>>> socket.ntohl(541982720)
20000
SocketServers模块为一组socket服务类定义了一个基类,这组类压缩和隐藏了监听、接受和处理进入的socket连接的细节。
1、SocketServers家族
TCPServer和UDPServer都是SocketServer的子类,它们分别处理TCP和UDP信息。
注意:SocketServer也提供UnixStreamServer(TCPServer的子类)和UNIXdatagramServer(UDPServer的子类),它们都如同其父类一样除了在创建监听socket时使用AF_UNIX代替了AF_INET。
默认情况下,socket服务一次处理一个连接,但是你可以使用ThreadingMixIN和ForkingMixIn类来创建任一SocketServer的线程和子进程。实际上,SocketServer模块提供了一些对些有用的类来解决你的麻烦,它们是:ForkingUDPServer、ForkingTCPServer、ThreadingUDPServer、ThreadingTCPServer、ThreadingUnixStreamServer和ThreadingUnixDatagramServer。
SocketServer以通常的方法处理进入的连接;要使它更有用,你应该提供你自己的请求处理器类给它以便它传递一个socket去处理。SocketServer模块中的BaseRequestHandler类是所有请求处理器的父类。假设,例如你需要写一个多线程的电子邮件服务器,首先你要创建一个MailRequestHandler,它是BaseRequestHandler的子类,然后把它传递给一个新创建的SocketServer:
import SocketServer
...#创建你的MailRequestHandler
addr=('220.172.20.6',25) #监听的地址和端口
server=SocketServer.ThreadingTCPServer(addr,MailRequestHandler)
server.serve_forever()
每次一个新的连接到来时,这个server创建一个新的MailRequestHandler实例并调用它的handle()方法来处理这个新的请求。因为server继承自ThreadingTCPServer,对于每个新的请求它都启动一个单独的线程来处理这个请求,以便于多个请求能够被同时处理。如果用handle_request()代替server_forever,它将一个一个的处理连接请求。server_forever 只是反复调用handle_request而已。
一般来说,你只需使用socket服务之一,但是如果你需要创建你自己的子类的话,你可以覆盖我们下面提到的方法来定制它。
当服务被第一次创建的时候,__init__函数调用server_bind()方法来绑定监听socket(self.socket)到正确的地址(self.server_address)。然后调用server_activate()来激活这个服务(默认情况下,调用socket的listen方法)。
这个socket服务不做任何事情直到调用了handle_request或serve_forever方法。handle_request调用get_request()去等待和接收一个新的socket连接,然后调用verify_request(request,client_address)去看服务是否会处理这个连接(你可以在访问控制中使用这个,默认情况下verify_request总是返回true)。如果会处理这个请求,handle_request然后调用process_request(request,client_address),如果process_request(request,client_address)导致一个异常的话,将调用handle_error(request,client_address)。默认情况下,process_request简单地调用finish_request(request,client_address);子进程和线程类覆盖了这个行为去开始一新的进程或线程,然后调用finish_request。finish_request实例化一个新的请求处理器,请求处理器轮流调用它们的handle()方法。
当SocketServer创建一个新的请求处理器时,它传递给这个处理器的__init__函数的self变量,以便于这个处理器能够访问关于这个服务的信息。
SocketServer的fileno()方法返回监听socket的文件描述符。address_family成员变量指定了监听socket的socket族(如AF_INET),server_address包含了监听socket被绑定到的地址。socket变量包含监听socket自身。
2、请求处理器
请求处理器有setup()、handle()和finish()方法,你可以覆盖它们来定制你自己的行为。一般情况下,你只需要覆盖handle方法。BaseRequestHandler的__init__函数调用setup()方法来做初始化的工作,handle()服务于请求,finish()用于执行清理工作,如果handle或setup导致一个异常,finish不会被调用。记住,你的请求处理器会为每个请求创建一个新的实例。
request成员变量有关于流(TCP)服务的最近接受的socket;对于数据报服务,它是一个包含进入消息和监听socket的元组。client_address包含发送者的地址,server有对SocketServer的一个引用(通过这你可以访问它的成员,如server_address)。
下面的例子实现了一个EchoRequestHandler,这作为一个服务端它将客户端所发送的数据再发送回客户端:
>>> import SocketServer
>>> class EchoRequestHandler(SocketServer.BaseRequestHandler):
... def handle(self):
... print 'Got new connection!'
... while 1:
... mesg=self.request.recv(1024)
... if not msg:
... break
... print 'Received:',msg
... self.request.send(msg)
... print 'Done with connection'
>>> server=SocketServer.ThreadingTCPServer(('127.0.0.1',12321),EchoReuestHandler)
>>> server.handle_request() #执行后将等待连接
Got new connection!
Received: Hello!
Received: I like Tuesdays!
Done with connection
打开另一个Python解释器作为客户端,然后执行如下代码:
>>> from socket import *
>>> s=socket(AF_INET,SOCK_STREAM)
>>> s.connect(('120.0.0.1',12321))
>>> s.send('Hello!')
6
>>> print s.recv(1024)
Hello!
>>> s.send('I like Tuesdays!')
16
>>> print s.recv(1024)
I like Tuesdays!
>>> s.close()
SocketServer模块也定义了BaseRequestHandler的两个子类:StreamRequestHandler和DatagramRequestHandler。它们覆盖了setup和finish方法并创建了两个文件对象rfile和wfile,你可以用这两个文件对象来向客户端读写数据,从而代替使用socket方法。
一、使用socket
网络编程中最基本的部分就是socket(套接字)。socket有两种:服务端socket和客户端 socket。在你创建了一个服务端socket之后,你告诉它去等待连接。然后它将监听某个网络地址(形如:xxx.xxx.xxx.xxx:xxx) 直到客户端连接。然后这两端就可以通信了。
处理客户端socket通常比处理服务端socket要容易一点,因为服务端必须时刻准备处理来自客户端的连接,并且它必须处理多个连接,而客户端只需要简单的连接,然后做点什么,然后断开连接。
实例化一个socket时,可以指定三个参数:地址系列(默认为socket.AF_INET)、流socket(这是个默认值: socket.SOCK_STREAM)或数据报socket(socket.SOCK_DGRAM)、协议(默认值是0)。对于简单的socket,你可以不指定任何参数而全部使用默认值。
服务端socket在使用bind方法之后调用listen方法去监听一个给定的地址。然后,客户端socket就可以通过使用connect方法(connect方法所使用的地址参数与bind相同)去连接服务端。listen方法要求一个参数,这个参数就是等待连接队列中所能包含的连接数。
一旦服务端socket调用了listen方法,就进入了临听状态,然后通常使用一个无限的循环:1、开始接受客房端的连接,这通过调用accept方法来实现。调用了这个方法后将处于阻塞状态(等待客户端发起连接)直到一个客户端连接,连接后,accept返回形如(client,address)的一个元组,其中client是一个用于与客户端通信的socket,address是客户端的形如xxx.xxx.xxx.xxx:xxx的地址;2、然后服务端处理客户端的请求;3、处理完成之后又调用1。
关于传输数据,socket有两个方法:send和recv。send使用字符串参数发送数据;recv参数是字节数,表示一次接受的数据量,如果你不确定一次该接受的数据量的话,最好使用1024。
下面给出一个最小的服务器/客户机的例子:
服务端:
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()
客户端:
import socket
s = socket.socket()
host = socket.gethostname()
port = 1234
s.connect((host, port))
print s.recv(1024)
注意:如果你使用Ctrl-C来停止服务端的话,如果再次使用相同的端口可能需要等待一会儿。
二、使用SocketServer
SocketServer模块简单化了编写网络服务器的工作。
它提供了四个基本的服务类:TCPServer(使用TCP协议)、UDPServer(使用数据报)、UnixStreamServer、
UnixDatagramServer。UnixStreamServer和UnixDatagramServer用于类Unix平台。
这四个类处理请求都使用同步的方法,也就是说,在下一个请求处理开始之前当前的请求处理必须已完成
。
用SocketServer创建一个服务器需要四步:
1、通过子类化BaseRequestHandler类和覆盖它的handle()方法来创建一个请求处理器类,用于处理进来
的请求;
2、实例化服务类如TCPServer,并传递给它参数:服务器地址和请求处理器类;
3、调用服务实例对象的handle_request()或serve_forever()方法去处理请求。
下面使用SocketServer用同步的方法写一个最简单的服务器:
from SocketServer import TCPServer, StreamRequestHandler
#第一步。其中StreamRequestHandler类是BaseRequestHandler类的子类,它为流socket定义了
#rfile和wfile方法
class Handler(StreamRequestHandler):
def handle(self):
addr = self.request.getpeername()
print 'Got connection from', addr
self.wfile.write('Thank you for connecting')
#第二步。其中''代表运行服务器的主机
server = TCPServer(('', 1234), Handler)
#第三步。serve_forever()导致进入循环状态
server.serve_forever()
非阻塞或异步编程
例如,对于一个聊天室来说,因为有多个连接需要同时被处理,所以很显然,阻塞或同步的方法是不合适的,这就像买票只开了一个窗口,佷多人排队等一样。那么我们如何解决这个问题呢?主要有三种方法:forking、threading、异步I/O。 Forking和threading的方法非常简单,通过使用SocketServer服务类的min-in类就可以实现。forking只适用于类Unix平台;threading需要注意内存共享的问题。 异步I/O如果底层的方法来实现是有点困难的。要简单点,我们可以考虑使用标准库中的框架或Twisted(Twisted是一个非常强大的异步网络编程的框架)。 一、用ScoketServer实现Forking和threading 下面我们使用两个例子来分别创建forking服务器和threading服务器。 Forking 服务器: from SocketServer import TCPServer, ForkingMixIn, StreamRequestHandler class Server(ForkingMixIn, TCPServer): pass class Handler(StreamRequestHandler): def handle(self): addr = self.request.getpeername() print 'Got connection from', addr self.wfile.write('Thank you for connecting') server = Server(('', 1234), Handler) server.serve_forever() threading服务器: from SocketServer import TCPServer, ThreadingMixIn, StreamRequestHandler class Server(ThreadingMixIn, TCPServer): pass class Handler(StreamRequestHandler): def handle(self): addr = self.request.getpeername() print 'Got connection from', addr self.wfile.write('Thank you for connecting') server = Server(('', 1234), Handler) server.serve_forever() 二、使用select实现异步I/O 所谓异步I/O,打个比方,就是如果一大群人都想你听他说话,那么你就给他们每人一分钟的时间说,大家轮流说,没说完的待会儿轮到时再继续说。也就是一个时间片的方法。 要实现异步I/O,我们可以通过使用框架asyncore/asynchat或Twisted,它们都是基于select函数或poll函数(poll只适于类Unix系统)的。select和poll函数都来自select模块。 select函数要求三个必须序列作为参数和一个可选的以秒为单位的超时值。序列中是表示文件描述符的整数值,它们是我们要等待的连接。这三个序列是关于输入、输出和异常条件的。如果超时值没有给出的话,select将处于阻塞状态(也就是等待)直到有文件描述符准备动作。如果超时值给出了,那么select只阻塞给定的时间。如果超时值是0的话,那么将不阻塞。select返回的值是一个由三个序列组成的元组,它们分别代表相应参数的活动的子集。例如,第一个序列返回的是用于读的输入文件描述符构成的序列。 序列可以包含文件对象(不适于Windows)或socket。下面这个例子创建一个使用select去服务几个连接的服务器(注意:服务端的socket自身也提供给了select,以便于它能够在有新的连接准备接受时发出信号通知)。这个服务器只是简单地打印接受自客户端的数据。你可以使用telnet(或写一个基于socket的简单的客户端)来连接测试它。 select server import socket, 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, addr = s.accept() print 'Got connection from', addr 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 三、Twisted Twisted是针对Python的一个事件驱动的网络框架,最初是为了网络游戏而开发的,但是现在被应用于各类网络软件。用Twisted,你可以实现事件处理器,非常类似用GUI工具包(Tk, GTK, Qt, wxWidgets)。这部分我将介绍一些基本的概念和演示如何使用Twisted来做一些相对简单的网络编程。Twisted是非常强大的框架并提供了大量的支持,如:Web服务器和客户端、SSH2, SMTP, POP3, IMAP4, AIM, ICQ, IRC, MSN,Jabber, NNTP, DNS等等。 早先我们所写的基于socket的服务器,它们都有一个显示的事件循环:寻找新的连接和新的数据;基于SocketServer的服务器有一个隐含的循环:寻找连接和为连接创建处理器。但时处理器仍然时显示的读数据。 而Twisted使用了更多的基于事件的方式。要写一个基本的服务器,你要实现事件处理器,它处理诸如一个新的客户端连接、新的数据到达和客户端连接中断等情况。在Twisted中,你的事件处理器定义在一个protocol中;你也需要一个factory,当一个新的连接到达时它能够构造这个protocol对象,但是如果你仅仅想创建一个自定义的Protocol类的实例的话,你可以使用来自Twisted的factory,Factory类在模块twisted.internet.protocol中。当你写你的protocol时,使用twisted.internet.protocol模块中的Protocol作为你的父类。当你得到一个连接时,事件处理器connectionMade被调用;当你丢失了一个连接时,connectionLost被调用。从客户端接受数据使用处理器dataReceived。但是你不能使用事件处理策略向客户端发送数据;要向客户端发送数据,你可以使用self.transport,它有一个write方法。它也有一个client属性,其中包含了客户端的地址(主机名和端口)。 下面这个例子是一个Twisted版的服务器。其中实例化了Factory并设置了它的protocol属性以便它知道使用哪个protocol与客户端通信(这就是所谓的你的自定义protocol)。然后你使用factory开始监听指定的端口,factory通过实例化的protocol对象处理连接。监听使用reactor模块中的listenTCP函数。最后,你通过调用reactor模块中的run函数来开始服务器。 from twisted.internet import reactor from twisted.internet.protocol import Protocol, Factory # 定义你Protocol类 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() # 设置factory的protocol属性以便它知道使用哪个protocol与客户端通信(这就是所谓的你的自定义 # protocol) factory.protocol = SimpleLogger # 监听指定的端口 reactor.listenTCP(1234, factory) # 开始运行主程序 reactor.run() 为你的处理目的而写一个自定义的protocol是很容易的。模块twisted.protocols.basic中包含了几个有用的已存在的protocol,其中的LineReceiver执行dataReceived并在接受到了一个完整的行时调用事件处理器lineReceived。如果当你在接受数据时除了使用lineReceived,还要做些别的,那么你可以使用LineReceiver定义的名为rawDataReceived事件处理器。下面是一使用LineReceiver的服务器例子: from twisted.internet import reactor from twisted.internet.protocol import Factory from twisted.protocols.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()
urllib和urllib2
urllib和urllib2的工作大同小异,它们让你能够通过网络访问文件,就像访问自己电脑上的一样。通过简单的函数调用,URL所定位的资源就可以被你作为输入使用到你的程序中。如果再配以re模块,那么你就能够下载Web页面、提取信息、自动创建你所寻找的东西的报告。 urllib2更流行一些。对于简单的下载任务,urllib比较好。如果你需要HTTP验证或cookies,或你想写一些扩展去处理你自己的协议的话,那么urllib2是正确的选择。 一、打开远程文件 打开远程文件的操作和本地差不多,不同的是只能使用读模式,并且使用urllib模块的urlopen: >>> from urllib import urlopen >>> webpage=urlopen('http://www.python.org') 如果你在线的话,变量webpage现在就包含了一个关联Web页: http://www.python.org 的文件类对象。 注意:如果你当前没有联网,而你又想练习一下urllib的话,你可以用如下形式访问本地文件: localpage=urlopen(r'file:c:/test.txt') 由urlopen返回的文件类对象支持close,read,readline,readlines等方法。 下面的代码抽取出了Python官方主页中“Documentation”链接的URL: >>> import re >>> text = webpage.read() >>> m = re.search('<a href="([^"]+)">Documentation</a>', text, re.IGNORECASE) >>> m.group(1) 'http://docs.python.org/' 二、获取远程文件 urlopen函数给你一个文件类对象,你可以读取它。如果你使用urlib时只关心下载文件并存储一个复本到本地文件的话,你可以使用urlretrieve替而代之。urlretrieve返回一个元组(filename, headers),filename是本地文件(复本)的名字(它由urllib自动创建),headers包含关于远程文件的一些信息。 如果你想为复本指定一个名字的话,你可以提供第二个参数: urlretrieve('http://www.python.org', 'C://python_webpage.html') 这将获取Python官方主页并存储到本地C:/python_webpage.html中。如果你不指定复本的文件名,那么文件将放到一个临时的地方,你能够使用open函数打开它,如果你要清除这些临时的复本,你可以调用urlcleanup函数而不带任何参数,它将为你完成清除工作。 |