Python基础教程书籍案例:P2P在线文件共享(使用XML-RPC进行文件共享)【二】

释放双眼,带上耳机,听听看~!

这一篇教程,我们对服务器进行优化,并且创建CMD客户端,通过命令进行访问。

Python基础教程书籍案例:P2P在线文件共享(使用XML-RPC进行文件共享)【二】

先不用头疼,实现过程并不复杂。

一、服务器的再次实现

首先,我们先对服务器代码进行优化。

在之前的代码中,请求文件有正常和失效两个状态,处理请求时会返回不同的状态值和相应的数据。

在新版本中,正常的状态只是返回数据,失效的状态我们抛出故障异常,包括:文件不存在异常和文件访问受限异常。

文件不存在比较好理解,那么,什么是文件访问受限呢?

首先,下载时如果提供的密钥不匹配,则应该抛出文件访问受限的异常。

另外,我们是通过路径访问文件,允许访问的空间是我们设定的共享目录,除了这个目录,硬盘上的其它目录都是不允许访问的。

如果有节点发来的请求是当前节点共享目录之外的目录,此时也要返回文件访问受限的异常。

例如,在上一个版本的服务器运行时,我们在客户端可以通过类似”../file.txt”的文件名获取服务器指定共享目录上一级目录中的其它资源。

我们要杜绝这样的问题发生。

所以,在新版本中,我们主要做如下改动:

  • 将状态变量取消,添加两个异常状态代码的变量,分别代表文件不存在和文件访问受限;
  • 创建两个自定义异常的类;
  • 处理请求时,如果捕获自定义异常,抛出异常。

示例代码:(优化部分代码带有注释)


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
1from xmlrpc.server import SimpleXMLRPCServer
2from xmlrpc.client import ServerProxy, Fault  # 导入故障异常类Fault
3from os.path import isfile, abspath, join  # 导入绝对路径的方法abspath
4from urllib.parse import urlparse
5import sys  # 导入系统模块
6'''
7想要学习Python?Python学习交流群:984632579满足你的需求,资料都已经上传群文件,可以自行下载!
8'''
9SimpleXMLRPCServer.allow_reuse_address = 1  # 保证节点服务器重启时能够立即访问
10MAX_HISTORY_LENGTH = 6
11
12UNHANDLED = 100  # 文件不存在的异常代码
13ACCESS_DENIED = 200  # 文件访问受限的异常代码
14
15class UnhandledQuery(Fault):  # 创建自定义异常类
16    def __init__(self, message='无法处理请求!'):  # 定义构造方法
17        Fault.__init__(self, UNHANDLED, message)  # 重载超类构造方法
18
19class AccessDenied(Fault):  # 创建自定义异常类
20    def __init__(self, message='访问资源受限!'):  # 定义构造方法
21        Fault.__init__(self, ACCESS_DENIED, message)  # 重载超类构造方法
22
23def inside(dir_path, file_path):  # 定义文件路径检查的方法
24    directory = abspath(dir_path)  # 获取共享目录的绝对路径
25    file = abspath(file_path)  # 获取请求资源的绝对路径
26    return file.startswith(join(directory, ''))  # 返回请求资源的路径是否以共享目录路径开始
27
28def get_port(url):
29    result = urlparse(url)[1]
30    port = result.split(':')[-1]
31    return int(port)
32
33class Node:
34    def __init__(self, url, dir_name, secret):
35        self.url = url
36        self.dirname = dir_name
37        self.secret = secret
38        self.known = set()
39
40    def _start(self):
41        server = SimpleXMLRPCServer(('', get_port(self.url)), logRequests=False)
42        server.register_instance(self)
43        server.serve_forever()
44
45    def _handle(self, filename):
46        file_path = join(self.dirname, filename)
47        if not isfile(file_path):  # 如果路径不是一个文件
48            raise UnhandledQuery  # 抛出文件不存在的异常
49        if not inside(self.dirname, file_path):  # 如果请求的资源不是共享目录中的资源
50            raise AccessDenied  # 抛出访问资源受限异常
51        return open(file_path).read()  # 未发生异常时返回读取的文件数据
52
53    def _broadcast(self, filename, history):
54        for other in self.known.copy():
55            if other in history:
56                continue
57            try:
58                server = ServerProxy(other)
59                return server.query(filename, history)
60            except Fault as f:  # 如果捕获访问故障异常获取异常代码
61                if f.faultCode == UNHANDLED:  # 如果是文件不存在异常
62                    pass  # 不做任何处理
63                else:  # 如果是其它故障异常
64                    self.known.remove(other)  # 从已知节点列表中移除节点
65            except:  # 如果捕获其它异常(非故障异常)
66                self.known.remove(other)  # 从已知节点列表中移除节点
67        raise UnhandledQuery  # 如果已知节点都未能请求到资源,抛出文件不存在异常。
68
69    def query(self, filename, history=[]):
70        try:
71            return self._handle(filename)
72        except UnhandledQuery:  # 如果捕获文件不存在的异常
73            history.append(self.url)
74            if len(history) >= MAX_HISTORY_LENGTH:
75                raise
76            return self._broadcast(filename, history)
77
78    def hello(self, other):
79        self.known.add(other)
80        return 0  # 必须返回非None的值
81
82    def fetch(self, filename, secret):
83        if secret != self.secret:  # 如果密钥不匹配
84            raise AccessDenied  # 抛出访问资源受限异常
85        result = self.query(filename)
86        with open(join(self.dirname, filename), 'w') as file:
87            file.write(result)
88        return 0  # 必须返回非None的值
89
90def main():  # 定义主程序函数,用于测试当前代码是否正常。
91    url, directory, secret = sys.argv[1:]  # 获取通过命令行终端输入参数
92    node = Node(url, directory, secret)  # 创建节点对象
93    node._start()  # 启动节点服务器
94
95if __name__ == '__main__':
96    main()  # 运行主程序
97

在上方代码中加入了一个main()函数,这个函数主要用于当前服务器代码的测试。

实际上,节点的创建以及服务器的启动,我们会放到客户端的代码中。

就好像我们打开一个BT软件的客户端,服务器也同时启动一样。

二、客户端的实现

客户端中我们要实现以下功能:

  • 创建一定长度并且随机的下载密钥;
  • 启动节点服务器;
  • 加载节点文件中的URL到已知节点;
  • 能够执行下载、退出的命令。
  • 使用CMD命令行界面。

1、导入一些必须的模块。

示例代码:


1
2
3
4
5
6
7
8
9
1from xmlrpc.client import ServerProxy, Fault  # 导入服务器代理类和故障类
2from random import choice  # 导入随机选取的方法
3from string import ascii_lowercase  # 导入小写字母列表对象
4from time import sleep  # 导入延迟方法
5from pserver import Node, UNHANDLED  # 导入服务器中的节类和变量
6from threading import Thread  # 导入线程类
7from cmd import Cmd  # 导入命令类
8import sys  # 导入系统模块
9

2、定义两个变量。

因为服务器启动之后,客户端要从XML-PRC进行连接,为了保证服务器启动在连接之前,我们在启动服务器之后添加一个延时,再进行连接。这个延时的时长,通过变量来设定。

另外,下载密钥的长度,我们也通过变量来设定。

示例代码:


1
2
3
1HEAD_START = 0.1 # 等待服务器启动时长
2SECRET_LENGTH = 10 # 密钥长度
3

3、定义生成随机密钥的函数。

生成指定长度的随机密钥(小写字母的随机组合),我们要定义一个函数。

示例代码:


1
2
3
4
5
6
7
1def random_string(length):  # 定义随机密钥的函数
2    secret = ''
3    while length > 0:
4        length -= 1
5        secret += choice(ascii_lowercase)  # 随机获取小写字母叠加到变量
6    return secret
7

4、定义客户端类。

因为要使用CMD界面,所以这个类要继承CMD类。

然后,类中对应命令的方法命名,都要以“do_”开头。

另外,还要注意,服务器如果直接在当前线程中启动,会影响客户端功能无法运行,所以,要把服务器在独立的线程中启动,并且将线程设置为守护线程。守护线程表示这个线程不重要,主线程退出时,不用等待这个线程运行结束。我们知道服务器的运行是死循环,在不发生异常时,永远不会运行结束,如果不将其设置为守护进程,就会导致主线程无法退出。

如果想更好地理解守护线程的作用,可以在运行客户端时开关守护线程的设置,执行exit命名来比较区别。

示例代码:


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
1'''
2想要学习Python?Python学习交流群:984632579满足你的需求,资料都已经上传群文件,可以自行下载!
3'''
4class Client(Cmd):
5    prompt = '>>>'  # 重写超类中的命令提示符
6
7    def __init__(self, url_file, dir_name, url):  # 定义构造方法
8        Cmd.__init__(self)  # 重载超类的构造方法
9        self.secret = random_string(SECRET_LENGTH)  # 创建密钥变量
10        node = Node(url, dir_name, self.secret)  # 创建节点对象
11        thread = Thread(target=node._start)  # 在独立的线程中启动服务器
12        thread.setDaemon(True)  # 将线程设置为守护线程
13        thread.start()  # 启动线程
14        sleep(HEAD_START)  # 等待服务器启动
15        self.server = ServerProxy(url)  # 创建服务器代理对象
16        for line in open(url_file):  # 读取URL文件
17            self.server.hello(line.strip())  # 添加URL文件中的URL到已知节点集合
18
19    def do_fetch(self, filename):  # 定义下载命令的方法
20        try:
21            self.server.fetch(filename, self.secret)  # 调用服务器代理对象的下载方法
22        except Fault as f:  # 捕获故障异常
23            if f.faultCode != UNHANDLED:  # 如果异常代码不是未找到文件
24                pass  # 不做处理
25            print('找不到文件:', filename)
26
27    def do_exit(self, arg):  # 定义退出命令的方法
28        print('------------------退出程序------------------')
29        sys.exit()  # 系统退出
30

5、定义主程序函数。

示例代码:


1
2
3
4
5
1def main():  # 定义主程序函数
2    urlfile, dir_name, url = sys.argv[1:]  # 获取通过命令行输入的参数
3    client = Client(urlfile, dir_name, url)  # 创建客户端对象
4    client.cmdloop()  # 启动命令行循环执行
5

6、运行客户端代码。

示例代码:


1
2
3
1if __name__ == '__main__':
2    main()
3

完成以上代码的编写之后,我们再做一些准备。

以启动3个客户端为例,准备内容如下所述。

1、创建多个共享目录,并准备共享文件。

在项目文件夹中创建不同的文件夹作为不同URL的共享目录:NodeFiles01、NodeFiles02、NodeFiles03。

然后,在NodeFiles01文件夹中创建一个“file.txt”的测试文件。

2、创建多个URL文件。

URL文件是作为每个节点的已知节点,对应启动的不同客户端。

所以,在项目文件夹中,URL文件文件也需要准备3个:url1.txt、url2.txt、url3.txt。

文件内容(url1.txt):

http://127.0.0.1:7777

http://127.0.0.1:8888

文件内容(url2.txt):

http://127.0.0.1:6666

http://127.0.0.1:8888

文件内容(url3.txt):

http://127.0.0.1:7777

完成以上准备之后,我们进行测试。

在命令行终端中,通过命令启动3个客户端。

pclient.py url1.txt NodeFiles01 http://127.0.0.1:6666
pclient.py url2.txt NodeFiles02 http://127.0.0.1:7777
pclient.py url3.txt NodeFiles03 http://127.0.0.1:8888

然后,在第3个端口号为“8888”的客户端中,我们执行下载命令。

如果在“NodeFiles03”文件夹中出现了“file.txt”文件,则说明测试成功。

而下载过程是“8888”端口的客户端先在当前节点的共享目录中查找请求的文件,发现文件不存在后,向已知节点“7777”端口的服务器发出了请求,而在“7777”端口服务器的共享目录也没有请求的文件,所以“7777”端口的服务器对已知节点(url2.txt中的URL)广播请求,“6666”端口的节点的服务器接收到请求进行处理,找到了请求的文件,将文件数据读取返回。

给TA打赏
共{{data.count}}人
人已打赏
安全经验

Google Adsense(Google网站联盟)广告申请指南

2021-10-11 16:36:11

安全经验

安全咨询服务

2022-1-12 14:11:49

个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索