8. Python脚本学习实战笔记八 使用XML-RPC进行文件共享
本篇名言:“也许你太平凡,没有令粉蝶断魂的容颜;也许你太常见,没有昙花一现的娇艳;但你却默默伫立,无怨无悔;装点京华,月月年年。”
XML-RPC是一种用于远程调用过程的协议。可以用普通的套接字编程来实现,可能性能更好,但是XML-RPC非常简单易用,而且让代码大大简化。
1. 需求
P2P文件共享程序是在不同计算机上的程序交换文件。P2P交互内,任何节点peer都可以是链接到其他节点。在这样一个由节点组成的虚拟网络中,是没有*节点的,这样网络会更强壮。
P2P系统构建会有很多问题,但是大多数P2P系统都有聪明的方法来组织自己的结构。
学习使用满足如下即可:
l 节点介绍自己给其他节点
l 向节点请求文件
l 为避免循环以及长距离请求,需要提供请求历史记录
l 程序必须有连接到节点并且将自己表示为可信任参与者的方法
l 必须提供用户界面
用Python实现其实并不是那么的困难的。
2. 工具及准备
使用的主要模块是xmlrpclib,SimpleXMLPRCServer。共享界面可以使用标准库中的cmd模块,以获得一些非常有限的并行效果。此外还有threading模块,urlparse模块。
3. 初次实现
先做个试验,启动两个交互式的Python解释器。
第一个输入以下内容:
>>> from SimpleXMLRPCServer importSimpleXMLRPCServer
>>>s=SimpleXMLRPCServer(("",4242))
>>> def twice(x):
... return x*2
...
>>> s.register_function(twice)
>>> s.serve_forever()
另一个输入如下:
>>> from xmlrpclib importServerProxy
>>>s=ServerProxy('http://localhost:4242')
>>> s.twice(2)
4
此时第一个解释器会输出如下:
127.0.0.1 - - [06/Aug/2015 23:00:05]"POST /RPC2 HTTP/1.1" 200 -
程序每个节点需要两方面包括:Node要维护什么信息,必须执行什么操作。
代码实现如下:
fromxmlrpclibimport ServerProxy
fromos.pathimport join, isfile
fromSimpleXMLRPCServerimportSimpleXMLRPCServer
fromurlparseimport urlparse
importsys
MAX_HISTORY_LENGTH = 6
OK = 1
FAIL = 2
EMPTY = ''
defgetPort(url):
'Extracts the port from a URL'
name = urlparse(url)[1]
parts = name.split(':')
return int(parts[-1])
classNode:
"""
A node in a peer-to-peer network.
"""
def__init__(self,url, dirname, secret):
self.url = url
self.dirname = dirname
self.secret = secret
self.known = set()
defquery(self,query, history=[]):
"""
Performs a query for a file, possiblyasking other known Nodes for
help. Returns the file as a string.
"""
code, data = self._handle(query)
if code == OK:
return code, data
else:
history = history + [self.url]
if len(history) >= MAX_HISTORY_LENGTH:
return FAIL, EMPTY
returnself._broadcast(query, history)
defhello(self,other):
"""
Used to introduce the Node to otherNodes.
"""
self.known.add(other)
return OK
deffetch(self,query, secret):
"""
Used to make the Node find a file anddownload it.
"""
if secret !=self.secret:return FAIL
code, data = self.query(query)
if code == OK:
f = open(join(self.dirname,query),'w')
f.write(data)
f.close()
return OK
else:
return FAIL
def_start(self):
"""
Used internally to start the XML-RPCserver.
"""
s = SimpleXMLRPCServer(("", getPort(self.url)),logRequests=False)
s.register_instance(self)
s.serve_forever()
def_handle(self,query):
"""
Used internally to handle queries.
"""
dir = self.dirname
name = join(dir, query)
ifnot isfile(name):return FAIL, EMPTY
return OK, open(name).read()
def_broadcast(self,query, history):
"""
Used internally to broadcast a query toall known Nodes.
"""
for otherinself.known.copy():
if otherin history:continue
try:
s = ServerProxy(other)
code, data = s.query(query,history)
if code == OK:
return code, data
except:
self.known.remove(other)
return FAIL, EMPTY
defmain():
url, directory, secret = sys.argv[1:]
n = Node(url, directory, secret)
n._start()
if __name__ =='__main__': main()
创建两个文件夹files1和files2,
在files2文件中放入test.txt文件。。
在第一个节点运行
Python simple_node.py http://localhost:4242 files1 secret1
在第二个节点运行
Python simple_node.py http://localhost:4243 files2 secret2
然后运行如下代码:
>>> from xmlrpclib import *
>>>mypeer=ServerProxy('http://localhost:4242')
>>>code,data=mypeer.query('test.txt')
>>> code
2
Code为2表示获取失败。
然后从节点二获取如下:
>>>otherpeer=ServerProxy('http://localhost:4243')
>>>code,data=otherpeer.query('test.txt')
>>> code
1
>>> data
'ok'
把第一个节点介绍给第二个节点
>>> mypeer.hello('http://localhost:4243')
1
然后获取,就可以从第二个节点上得到test.txt文件。
>>> mypeer.query('test.txt')
[1, 'ok']
把第二个节点的文件下载到第一个节点上
>>>mypeer.fetch('test.txt','secret1')
1
在files1文件夹中查看,存在test.txt文件。
4. 重构
重启一个Node的话,会提示已经在使用的错误信息;需要实现人性化界面;返回状态代码不方便,不会检查文件是否在文件目录中。
使用cmd模块的Cmd类实现。
可以继承Cmd来创建命令行界面,然后对所有想处理的命令foo实现do_foo方法。
需要每个客户端和自己的节点关联起来,如果在所有节点同步前不能做任何事那么就会非常不友好。可以通过启动独立的线程来解决这个问题。
使用异常来替换if语句检查返回的代码。
使用os.path模块来实现判断给定的文件是否能在给定的目录中找到。
重构分解成两个文件:
Client.py如下:
from xmlrpclib import ServerProxy, Fault
from cmd import Cmd
from random import choice
from string import lowercase
from server import Node, UNHANDLED
from threading import Thread
from time import sleep
import sys
HEAD_START = 0.1 # Seconds
SECRET_LENGTH = 100
def randomString(length):
"""
Returns a random string ofletters with the given length.
"""
chars = []
letters = lowercase[:26]
while length > 0:
length -= 1
chars.append(choice(letters))
return ''.join(chars)
class Client(Cmd):
"""
A simple text-based interfaceto the Node class.
"""
prompt = '> '
def __init__(self, url,dirname, urlfile):
"""
Sets the url, dirname,and urlfile, and starts the Node
Server in a separatethread.
"""
Cmd.__init__(self)
self.secret =randomString(SECRET_LENGTH)
n = Node(url, dirname,self.secret)
t =Thread(target=n._start)
t.setDaemon(1)
t.start()
# Give the server ahead start:
sleep(HEAD_START)
self.server =ServerProxy(url)
for line inopen(urlfile):
line = line.strip()
self.server.hello(line)
def do_fetch(self, arg):
"Call the fetchmethod of the Server."
try:
self.server.fetch(arg, self.secret)
except Fault, f:
if f.faultCode !=UNHANDLED: raise
print"Couldn't find the file", arg
def do_exit(self, arg):
"Exit theprogram."
sys.exit()
def do_hello(self, arg):
print"hello",arg
do_EOF = do_exit #End-Of-File is synonymous with 'exit'
def main():
urlfile, directory, url =sys.argv[1:]
client = Client(url,directory, urlfile)
client.cmdloop()
if __name__ == '__main__': main()
Server.py文件如下:
from xmlrpclib import ServerProxy, Fault
from os.path import join, abspath, isfile
from SimpleXMLRPCServer import SimpleXMLRPCServer
from urlparse import urlparse
import sys
SimpleXMLRPCServer.allow_reuse_address = 1
MAX_HISTORY_LENGTH = 6
UNHANDLED = 100
ACCESS_DENIED = 200
class UnhandledQuery(Fault):
"""
An exception thatrepresents an unhandled query.
"""
def __init__(self,message="Couldn't handle the query"):
Fault.__init__(self,UNHANDLED, message)
class AccessDenied(Fault):
"""
An exception that is raisedif a user tries to access a
resource for which he orshe is not authorized.
"""
def __init__(self,message="Access denied"):
Fault.__init__(self,ACCESS_DENIED, message)
def inside(dir, name):
"""
Checks whether a given filename lies within a given directory.
"""
dir = abspath(dir)
name = abspath(name)
returnname.startswith(join(dir, ''))
def getPort(url):
"""
Extracts the port numberfrom a URL.
"""
name = urlparse(url)[1]
parts = name.split(':')
return int(parts[-1])
class Node:
"""
A node in a peer-to-peernetwork.
"""
def __init__(self, url,dirname, secret):
self.url = url
self.dirname = dirname
self.secret = secret
self.known = set()
def query(self, query,history=[]):
"""
Performs a query for afile, possibly asking other known Nodes for
help. Returns the fileas a string.
"""
try:
return self._handle(query)
except UnhandledQuery:
history = history +[self.url]
if len(history)>= MAX_HISTORY_LENGTH: raise
returnself._broadcast(query, history)
def hello(self, other):
"""
Used to introduce theNode to other Nodes.
"""
self.known.add(other)
return 0
def fetch(self, query,secret):
"""
Used to make the Nodefind a file and download it.
"""
if secret != self.secret:raise AccessDenied
result =self.query(query)
f =open(join(self.dirname, query), 'w')
f.write(result)
f.close()
return 0
def _start(self):
"""
Used internally tostart the XML-RPC server.
"""
s =SimpleXMLRPCServer(("", getPort(self.url)), logRequests=False)
s.register_instance(self)
s.serve_forever()
def _handle(self, query):
"""
Used internally tohandle queries.
"""
dir = self.dirname
name = join(dir, query)
if not isfile(name):raise UnhandledQuery
if not inside(dir,name): raise AccessDenied
returnopen(name).read()
def _broadcast(self, query,history):
"""
Used internally tobroadcast a query to all known Nodes.
"""
for other inself.known.copy():
if other inhistory: continue
try:
s =ServerProxy(other)
returns.query(query, history)
except Fault, f:
if f.faultCode== UNHANDLED: pass
else:self.known.remove(other)
except:
self.known.remove(other)
raise UnhandledQuery
def main():
url, directory, secret =sys.argv[1:]
n = Node(url, directory,secret)
n._start()
if __name__ == '__main__': main()
执行如下2个客户端:
python client.py urls.txt files2 http://localhost:4243
python client.py urls.txt files1 http://localhost:4242
在urls.txt文件中输入:
E:\>python client.py urls.txt files1http://localhost:4242
> fetch files2.txt
>
这个files2.txt文件如果再files1文件夹中没找到,会自动到files2文件夹中进行寻找。