基于 XML - RPC 的文件共享系统:从命令行到 GUI 的实现
1. 第二版实现概述
第一版实现存在诸多缺陷,例如:
- 停止并重启节点时,可能出现端口已被占用的错误。
- 在交互式 Python 解释器中, xmlrpclib 接口不够友好。
- 返回码使用不便,若文件未找到,使用自定义异常会更自然和 Pythonic。
- 节点未检查返回的文件是否在指定文件目录内,可能导致非法访问。
2. 问题解决
- 端口占用问题 :将
SimpleXMLRPCServer的allow_reuse_address属性设置为True,代码如下:
SimpleXMLRPCServer.allow_reuse_address = 1
若不想直接修改该类,可创建子类。
3. 客户端接口
客户端接口使用 cmd 模块的 Cmd 类。通过子类化 Cmd 创建命令行界面,并为每个要处理的命令实现 do_foo 方法。该方法接收命令行其余部分作为唯一参数(字符串形式)。
例如,在命令行输入 say hello , do_say 方法将以字符串 'hello' 作为唯一参数被调用。 Cmd 子类的提示符由 prompt 属性决定。
客户端接口实现的命令有:
- fetch :用于下载文件。
- exit :用于退出程序。
在构造函数中,为使每个客户端关联一个对等节点,将节点在单独线程中启动,代码如下:
from threading import Thread
n = Node(url, dirname, self.secret)
t = Thread(target=n._start)
t.start()
为确保服务器在使用 XML - RPC 连接前完全启动,使用 time.sleep 让其有启动时间。之后,遍历 URL 文件中的所有行,使用 hello 方法将服务器介绍给其他节点。同时,使用 randomString 函数生成客户端和节点共享的随机密钥。
4. 异常处理
不再返回表示成功或失败的代码,而是假设成功,失败时抛出异常。在 XML - RPC 中,异常(或“错误”)由数字标识。这里,普通失败(未处理的请求)和请求拒绝(访问被拒)分别选择数字 100 和 200。异常是 xmlrpclib.Fault 的子类,在服务器中抛出时,会以相同的 faultCode 传递给客户端。
5. 文件名验证
使用 os.path 模块确保文件名验证的平台独立性。具体步骤如下:
1. 从目录名和文件名创建绝对路径。
2. 使用 os.path.join 将目录名与空文件名连接,确保以文件分隔符结尾。
3. 检查绝对文件名是否以绝对目录名开头,若是,则文件在目录内。
6. 第二版实现测试
运行程序的命令为:
python client.py urls.txt directory http://servername.com:4242
其中, urls.txt 文件每行包含一个已知对等节点的 URL,第二个参数指定要共享文件的目录,最后一个参数是对等节点的 URL。运行命令后会出现提示符 > ,尝试获取不存在的文件时,会输出错误信息。
7. 代码实现
以下是服务器和客户端的代码:
服务器代码(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 that represents an unhandled query.
"""
def __init__(self, message="Couldn't handle the query"):
Fault.__init__(self, UNHANDLED, message)
class AccessDenied(Fault):
"""
An exception that is raised if a user tries to access a
resource for which he or she is not authorized.
"""
def __init__(self, message="Access denied"):
Fault.__init__(self, ACCESS_DENIED, message)
def inside(dir, name):
"""
Checks whether a given file name lies within a given directory.
"""
dir = abspath(dir)
name = abspath(name)
return name.startswith(join(dir, ''))
def getPort(url):
"""
Extracts the port number from a URL.
"""
name = urlparse(url)[1]
parts = name.split(':')
return int(parts[-1])
class Node:
"""
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()
def query(self, query, history=[]):
"""
Performs a query for a file, possibly asking other known Nodes for
help. Returns the file as a string.
"""
try:
return self._handle(query)
except UnhandledQuery:
history = history + [self.url]
if len(history) >= MAX_HISTORY_LENGTH: raise
return self._broadcast(query, history)
def hello(self, other):
"""
Used to introduce the Node to other Nodes.
"""
self.known.add(other)
return 0
def fetch(self, query, secret):
"""
Used to make the Node find 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 to start the XML-RPC server.
"""
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)
if not isfile(name): raise UnhandledQuery
if not inside(dir, name): raise AccessDenied
return open(name).read()
def _broadcast(self, query, history):
"""
Used internally to broadcast a query to all known Nodes.
"""
for other in self.known.copy():
if other in history: continue
try:
s = ServerProxy(other)
return s.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()
客户端代码(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 of letters 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 interface to the Node class.
"""
prompt = '> '
def __init__(self, url, dirname, urlfile):
"""
Sets the url, dirname, and urlfile, and starts the Node
Server in a separate thread.
"""
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 a head start:
sleep(HEAD_START)
self.server = ServerProxy(url)
for line in open(urlfile):
line = line.strip()
self.server.hello(line)
def do_fetch(self, arg):
"Call the fetch method 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 the program."
print
sys.exit()
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()
8. 进一步探索
可从以下方面改进和扩展系统:
- 添加缓存 :节点通过 query 调用中继文件时,同时存储文件,下次有人请求相同文件时可更快响应。可设置缓存最大大小并移除旧文件。
- 使用线程或异步服务器 :可在不等待其他节点回复的情况下向多个节点求助,节点后续通过 reply 方法回复。
- 允许更高级查询 :如查询文本文件内容。
- 更广泛使用 hello 方法 :发现新对等节点时,将其介绍给已知的所有对等节点,或思考更巧妙的发现新对等节点的方法。
- 学习 REST 哲学 :REST 是分布式系统的新兴替代方案,可参考 REST 介绍 。
- 使用 xmlrpclib.Binary 包装文件 :使非文本文件传输更安全。
- 阅读 SimpleXMLRPCServer 代码 :查看 DocXMLRPCServer 类和 libxmlrpc 中的多调用扩展。
9. 为文件共享系统添加 GUI
在现有文件共享系统基础上添加 GUI 客户端,可使程序更易用。
10. 具体目标
- 允许用户输入文件名并提交给服务器的
fetch方法。 - 列出服务器文件目录中当前可用的文件。
11. 所需工具
除之前使用的工具外,需要 wxPython 工具包,代码基于 wxPython 2.6 版本开发。
12. 准备工作
确保之前的文件共享系统已正常运行,并安装可用的 GUI 工具包。
13. 第一版实现
客户端子类化 wx.App ,GUI 相关设置在 OnInit 方法中完成,步骤如下:
1. 创建标题为 “File Sharing Client” 的窗口。
2. 创建文本字段并赋值给 self.input ,同时创建 “Fetch” 按钮,设置按钮大小并绑定事件处理程序。
3. 使用盒式布局管理器将文本字段和按钮添加到窗口。
4. 显示窗口并返回 True 表示 OnInit 成功。
事件处理程序与之前的 do_fetch 方法类似,从文本字段获取查询内容,调用服务器节点的 fetch 方法,若查询未处理则打印错误信息。
以下是简单 GUI 客户端代码:
from xmlrpclib import ServerProxy, Fault
from server import Node, UNHANDLED
from client import randomString
from threading import Thread
from time import sleep
from os import listdir
import sys
import wx
HEAD_START = 0.1 # Seconds
SECRET_LENGTH = 100
class Client(wx.App):
"""
The main client class, which takes care of setting up the GUI and
starts a Node for serving files.
"""
def __init__(self, url, dirname, urlfile):
"""
Creates a random secret, instantiates a Node with that secret,
starts a Thread with the Node's _start method (making sure the
Thread is a daemon so it will quit when the application quits),
reads all the URLs from the URL file and introduces the Node to
them.
"""
super(Client, self).__init__()
self.secret = randomString(SECRET_LENGTH)
n = Node(url, dirname, self.secret)
t = Thread(target=n._start)
t.setDaemon(1)
t.start()
# Give the server a head start:
sleep(HEAD_START)
self.server = ServerProxy(url)
for line in open(urlfile):
line = line.strip()
self.server.hello(line)
def OnInit(self):
"""
Sets up the GUI. Creates a window, a text field, and a button, and
lays them out. Binds the submit button to self.fetchHandler.
"""
win = wx.Frame(None, title="File Sharing Client", size=(400, 45))
bkg = wx.Panel(win)
self.input = input = wx.TextCtrl(bkg);
submit = wx.Button(bkg, label="Fetch", size=(80, 25))
submit.Bind(wx.EVT_BUTTON, self.fetchHandler)
hbox = wx.BoxSizer()
hbox.Add(input, proportion=1, flag=wx.ALL | wx.EXPAND, border=10)
hbox.Add(submit, flag=wx.TOP | wx.BOTTOM | wx.RIGHT, border=10)
vbox = wx.BoxSizer(wx.VERTICAL)
vbox.Add(hbox, proportion=0, flag=wx.EXPAND)
bkg.SetSizer(vbox)
win.Show()
return True
def fetchHandler(self, event):
"""
Called when the user clicks the 'Fetch' button. Reads the
query from the text field, and calls the fetch method of the
server Node. If the query is not handled, an error message is
printed.
"""
query = self.input.GetValue()
try:
self.server.fetch(query, self.secret)
except Fault, f:
if f.faultCode != UNHANDLED: raise
print "Couldn't find the file", query
通过以上步骤,我们实现了从命令行到 GUI 的文件共享系统,并且可以根据进一步探索的建议不断完善系统。
基于 XML - RPC 的文件共享系统:从命令行到 GUI 的实现
14. 第一版 GUI 实现的不足
虽然第一版 GUI 客户端实现了基本的文件获取功能,但它只完成了部分任务,还应该列出服务器文件目录中可用的文件。为了实现这一功能,需要对服务器(Node)进行扩展。
15. 服务器扩展思路
为了让服务器能够提供文件列表,我们可以在 Node 类中添加一个新的方法,用于返回指定目录下的文件列表。以下是扩展后的 Node 类代码:
from xmlrpclib import ServerProxy, Fault
from os.path import join, abspath, isfile, listdir
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 that represents an unhandled query.
"""
def __init__(self, message="Couldn't handle the query"):
Fault.__init__(self, UNHANDLED, message)
class AccessDenied(Fault):
"""
An exception that is raised if a user tries to access a
resource for which he or she is not authorized.
"""
def __init__(self, message="Access denied"):
Fault.__init__(self, ACCESS_DENIED, message)
def inside(dir, name):
"""
Checks whether a given file name lies within a given directory.
"""
dir = abspath(dir)
name = abspath(name)
return name.startswith(join(dir, ''))
def getPort(url):
"""
Extracts the port number from a URL.
"""
name = urlparse(url)[1]
parts = name.split(':')
return int(parts[-1])
class Node:
"""
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()
def query(self, query, history=[]):
"""
Performs a query for a file, possibly asking other known Nodes for
help. Returns the file as a string.
"""
try:
return self._handle(query)
except UnhandledQuery:
history = history + [self.url]
if len(history) >= MAX_HISTORY_LENGTH: raise
return self._broadcast(query, history)
def hello(self, other):
"""
Used to introduce the Node to other Nodes.
"""
self.known.add(other)
return 0
def fetch(self, query, secret):
"""
Used to make the Node find 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 to start the XML-RPC server.
"""
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)
if not isfile(name): raise UnhandledQuery
if not inside(dir, name): raise AccessDenied
return open(name).read()
def _broadcast(self, query, history):
"""
Used internally to broadcast a query to all known Nodes.
"""
for other in self.known.copy():
if other in history: continue
try:
s = ServerProxy(other)
return s.query(query, history)
except Fault, f:
if f.faultCode == UNHANDLED: pass
else: self.known.remove(other)
except:
self.known.remove(other)
raise UnhandledQuery
def list_files(self):
"""
Returns a list of files in the server's directory.
"""
return listdir(self.dirname)
def main():
url, directory, secret = sys.argv[1:]
n = Node(url, directory, secret)
n._start()
if __name__ == '__main__': main()
在上述代码中,我们添加了 list_files 方法,该方法使用 os.listdir 函数返回指定目录下的文件列表。
16. 客户端更新
为了显示服务器文件目录中的文件列表,我们需要更新客户端代码。以下是更新后的客户端代码:
from xmlrpclib import ServerProxy, Fault
from server import Node, UNHANDLED
from client import randomString
from threading import Thread
from time import sleep
from os import listdir
import sys
import wx
HEAD_START = 0.1 # Seconds
SECRET_LENGTH = 100
class Client(wx.App):
"""
The main client class, which takes care of setting up the GUI and
starts a Node for serving files.
"""
def __init__(self, url, dirname, urlfile):
"""
Creates a random secret, instantiates a Node with that secret,
starts a Thread with the Node's _start method (making sure the
Thread is a daemon so it will quit when the application quits),
reads all the URLs from the URL file and introduces the Node to
them.
"""
super(Client, self).__init__()
self.secret = randomString(SECRET_LENGTH)
n = Node(url, dirname, self.secret)
t = Thread(target=n._start)
t.setDaemon(1)
t.start()
# Give the server a head start:
sleep(HEAD_START)
self.server = ServerProxy(url)
for line in open(urlfile):
line = line.strip()
self.server.hello(line)
def OnInit(self):
"""
Sets up the GUI. Creates a window, a text field, a button, and a list box, and
lays them out. Binds the submit button to self.fetchHandler.
"""
win = wx.Frame(None, title="File Sharing Client", size=(400, 300))
bkg = wx.Panel(win)
self.input = input = wx.TextCtrl(bkg);
submit = wx.Button(bkg, label="Fetch", size=(80, 25))
submit.Bind(wx.EVT_BUTTON, self.fetchHandler)
self.file_list = file_list = wx.ListBox(bkg)
self.update_file_list()
hbox = wx.BoxSizer()
hbox.Add(input, proportion=1, flag=wx.ALL | wx.EXPAND, border=10)
hbox.Add(submit, flag=wx.TOP | wx.BOTTOM | wx.RIGHT, border=10)
vbox = wx.BoxSizer(wx.VERTICAL)
vbox.Add(hbox, proportion=0, flag=wx.EXPAND)
vbox.Add(file_list, proportion=1, flag=wx.ALL | wx.EXPAND, border=10)
bkg.SetSizer(vbox)
win.Show()
return True
def fetchHandler(self, event):
"""
Called when the user clicks the 'Fetch' button. Reads the
query from the text field, and calls the fetch method of the
server Node. If the query is not handled, an error message is
printed.
"""
query = self.input.GetValue()
try:
self.server.fetch(query, self.secret)
except Fault, f:
if f.faultCode != UNHANDLED: raise
print "Couldn't find the file", query
def update_file_list(self):
"""
Updates the file list in the GUI.
"""
try:
files = self.server.list_files()
self.file_list.Clear()
for file in files:
self.file_list.Append(file)
except Exception as e:
print "Error updating file list:", e
def main():
urlfile, directory, url = sys.argv[1:]
client = Client(url, directory, urlfile)
client.MainLoop()
if __name__ == '__main__': main()
在更新后的代码中,我们添加了一个 wx.ListBox 用于显示文件列表,并在 OnInit 方法中调用 update_file_list 方法来初始化文件列表。 update_file_list 方法通过调用服务器的 list_files 方法获取文件列表,并将其显示在 ListBox 中。
17. 运行和测试
更新后的客户端和服务器代码可以按照以下步骤运行:
1. 启动服务器:
python server.py http://localhost:4242 /path/to/files secret
- 启动客户端:
python client.py urls.txt /path/to/download http://localhost:4242
运行客户端后,你将看到一个包含文本字段、“Fetch” 按钮和文件列表的窗口。你可以在文本字段中输入文件名,点击 “Fetch” 按钮下载文件,同时文件列表会显示服务器目录中的可用文件。
18. 总结
通过以上步骤,我们成功地为文件共享系统添加了一个 GUI 客户端,并实现了文件列表的显示功能。这个 GUI 客户端使得文件共享系统更加易用,降低了用户的使用门槛。同时,我们也看到了如何在现有代码基础上进行扩展和改进,以满足新的需求。
19. 未来展望
虽然我们已经实现了一个基本的 GUI 客户端,但仍然有很多可以改进和扩展的地方:
- 界面优化 :可以进一步美化界面,提高用户体验,例如添加更多的按钮、菜单和图标。
- 错误处理 :可以增强错误处理功能,为用户提供更详细的错误信息。
- 多线程优化 :在客户端和服务器中使用多线程或异步编程,提高系统的性能和响应速度。
- 安全性增强 :可以添加更多的安全机制,如加密传输、身份验证等,保护用户的文件安全。
通过不断地改进和扩展,我们可以打造一个更加完善、安全和易用的文件共享系统。
20. 流程图:文件共享系统流程
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px
A([启动服务器]):::startend --> B(初始化 Node):::process
B --> C(启动 XML - RPC 服务器):::process
D([启动客户端]):::startend --> E(初始化客户端):::process
E --> F(启动 Node 线程):::process
F --> G(等待服务器启动):::process
G --> H(读取 URL 文件):::process
H --> I(向服务器介绍其他节点):::process
J(用户输入文件名):::process --> K(点击 Fetch 按钮):::process
K --> L(客户端调用服务器 fetch 方法):::process
L --> M{文件是否存在}:::decision
M -->|是| N(服务器返回文件):::process
N --> O(客户端保存文件):::process
M -->|否| P(客户端显示错误信息):::process
Q(用户查看文件列表):::process --> R(客户端调用服务器 list_files 方法):::process
R --> S(服务器返回文件列表):::process
S --> T(客户端显示文件列表):::process
21. 表格:系统功能对比
| 功能 | 命令行客户端 | 第一版 GUI 客户端 | 更新后 GUI 客户端 |
|---|---|---|---|
| 文件获取 | 支持 | 支持 | 支持 |
| 文件列表显示 | 不支持 | 不支持 | 支持 |
| 用户体验 | 较差 | 一般 | 较好 |
超级会员免费看

被折叠的 条评论
为什么被折叠?



