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

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

这个练习项目来自《Python基础教程(第2版)》,案例原名为“使用XML-RPC进行文件共享”。

原文是基于Pyhton2.7,其中使用的一些模块在Python3中已经发生改变,这里使用Python3完成这个练习项目 。

练习过程分为两个阶段:

  • 实现基本文件共享功能
  • 实现基于CMD客户端界面的文件分享功能

在开始练习之前,我们先了解一下P2P(Peer to Peer)的基本原理。

P2P原为网络通信技术名词,意思是“对等网络”。

在了解对等网络之前,我们先来看一下网络连接模式中另外一种形式的网络,即客户端/服务器网络(Client/Server)。在客户端/服务器网络中,服务器是网络的核心,而客户端是网络的基础,客户端依靠服务器获得所需要的网络资源,而服务器为客户端提供网络必须的资源。

为了更清楚的理解,我用一张简单的图来表示。

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

而对等网络则是另外一种形式,在对等网络上,各台计算机有相同的功能,无主从之分,一台计算机都是既可作为服务器,设定共享资源供网络中其他计算机所使用,也可以作为客户端获取其他计算机上的共享资源。

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

大家常用的BT下载,就是对等网络的一种具体实现。

当我们使用BT下载软件时,我们的计算机既是服务器又是客户端,不但能够下载自己需要的资源,同时也在上传他人需要的资源(即为其他计算机提供资源共享服务)。

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

在对等网络中,每一台计算机都是一个节点,当一个节点进行资源下载的时候,是如何工作的呢?

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

如上图所示,当节点Peer1进行资源下载时,会先通过广播功能向所有已知节点发出请求,当某个节点(例如Peer2)收到请求,立即对请求进行处理,先对本地资源进行查询。如果找到相应的资源,则回复Peer1节点;如果没有找到可以提供的资源,则通过广播功能向自己的所有已知节点转发Peer1的请求。以此类推。

不过,大家能够想到,这样一层一层的进行请求,几乎是没有尽头的。

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

说明:上图中数字表示访问链长度。

所以,一般会对这个访问链的长度加以限制,例如,只能进行6次广播,如果查询不到资源即终止。

综上所述,每个节点都能够应该具备下图中的功能。

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

了解了P2P的概念以及文件共享的原理,接下来,我们先尝试创建一个简单的服务器和客户端。

这里需要的是xmlrpc模块。

示例代码:(服务器)


1
2
3
4
5
6
7
8
9
10
11
12
1from xmlrpc.server import SimpleXMLRPCServer
2'''
3想要学习Python?Python学习交流群:984632579满足你的需求,资料都已经上传群文件,可以自行下载!
4'''
5server = SimpleXMLRPCServer(('', 6666))  # 创建服务器对象
6
7def twice(x):  # 定义供客户端调用的函数
8    return 2 * x
9
10server.register_function(twice)  # 注册开放给客户端的函数到服务器对象
11server.serve_forever()  # 运行服务器
12

示例代码:(客户端)


1
2
3
4
5
1from xmlrpc.client import ServerProxy
2
3server = ServerProxy('http://219.142.209.7:6666')  # 连接服务器,创建服务器代理对象。
4print(server.twice(6))  # 调用服务器提供的函数,显示输出结果为:12
5

先运行服务器,再运行客户端,我们能够看到显示输出的结果。

了解了服务器的创建与访问,接下来我们就完成一个功能相对完整的服务器,并模拟客户端的一些访问请求。

首先,先编写服务器代码。

一、导入模块

实现上述服务器功能,需要用到多个模块。

每个模块的用途请参考代码中的注释。

示例代码:


1
2
3
4
5
1from xmlrpc.server import SimpleXMLRPCServer  # 用于创建服务器
2from xmlrpc.client import ServerProxy  # 用于向其它节点发出请求
3from urllib.parse import urlparse  # 用于URL解析
4from os.path import join, isfile  # 用于路径处理和文件查询
5

二、定义常量

常量和变量一样,用于保存值。

区别在于,变量的值会在程序中发生改变,而常量的值是固定的。

在Python中常量的命名通常是全大写字母的单词。

这里的常量用于访问链长度限制和表示查询状态等。

示例代码:


1
2
3
4
5
1MAX_HISTORY_LENGTH = 6  # 访问链最大长度
2OK = 1 # 查询状态:正常
3FAIL = 2 # 查询状态:无效
4EMPTY = '' # 空数据
5

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
1class Node:
2    def __init__(self, url, dir_name, secret):
3        self.url = url
4        self.dirname = dir_name
5        self.secret = secret
6        self.known = set()
7
8    def _start(self):  # 定义启动服务器的内部方法
9        pass
10
11    def _handle(self, filename):  # 定义处理请求的内部方法
12        pass
13
14    def _broadcast(self, filename, history):  # 定义广播的内部方法
15        pass
16
17    def query(self, filename, history=[]):  # 定义接受请求的方法
18        pass
19
20    def hello(self, other):  # 定义向添加其它节点到已知节点的方法
21        pass
22
23    def fetch(self, filename, secrt):  # 定义下载的方法
24        pass
25

在上方代码中,类的构造函数不但创建了类的变量保存传入的参数,并且创建了一个已知节点的集合(利用了集合可以去重的特点)。

另外,大家要注意内部方法的名称都是单下划线“_”开头,表示受保护的方法,仅限在模块中的内部调用。

还记得双下划线”__”开头的方法吗?表示是类的私有方法,仅限类中可以调用。

其实,不管单下划线还是双下划线开头的方法,如果你非要在外部调用,也是拦不住的……(前面的教程中提到过)

四、定义启动服务器的方法

在创建服务器对象时,我们将Node类的实例注册到服务器对象,这样就不需要为每个方法进行注册。

示例代码:


1
2
3
4
5
1def _start(self):  # 定义启动服务器的方法
2    server = SimpleXMLRPCServer(('', get_port(self.url)), logRequests=False)
3    server.register_instance(self)  # 注册类的实例到服务器对象
4    server.serve_forever()
5

五、定义处理请求的方法

在这个方法中,我们需要通过请求的文件名称和目录路径组成文件路径,通过文件路径检查文件是否存在。

如果文件不存在返回无效的状态和空数据,否则返回正常的状态和读取的文件数据。

示例代码:


1
2
3
4
5
6
1def _handle(self, filename):  # 定义处理请求的方法
2    file_path = join(self.dirname, filename)  # 获取请求路径
3    if not isfile(file_path):  # 如果路径不是一个文件
4        return FAIL, EMPTY  # 返回无效状态和空数据
5    return OK, open(file_path).read()  # 返回正常状态和读取的文件数据
6

六、定义广播请求的方法

广播请求时,需要遍历已知节点,如果节点被访问过,则继续向下一节点发出请求。

如果被请求的节点发生异常,说明该节点失效,将其从已知节点中移除。

如果被请求的节点有效,返回正常的状态和数据。

如果所有已知节点都未能请求到需要的资源,返回无效的状态和空数据。

示例代码:


1
2
3
4
5
6
7
8
9
10
11
12
13
1def _broadcast(self, filename, history):  # 定义广播的方法
2    for other in self.known.copy():  # 遍历已知节点的列表
3        if other in history:  # 如果已知节点存在于历史记录
4            continue  # 继续下一个已知节点信息
5        try:
6            server = ServerProxy(other)  # 访问非历史记录中的已知节点
7            state, data = server.query(filename, history)  # 向已知节点发出请求
8            if state == OK:  # 如果状态为正常
9                return OK, data  # 返回有效状态和数据
10        except OSError:
11            self.known.remove(other)  # 如果发生异常从已知节点列表中移除节点
12    return FAIL, EMPTY  # 返回无效状态和空数据
13

七、定义接收请求的方法

当服务器接收到请求之后,交由内部处理程序进行处理,查询当前节点的资源状态并读取数据。

如果获取到正常状态,返回状态和数据;否则,向所有已知节点广播请求。

这里要注意,在广播请求之前,要把当前节点的URL存放在历史记录列表中,这样能够避免对当前节点的重复请求,并形成访问链;并且,每一层接收请求处理过后,如果没有获取到资源,也都要将当前节点的URL在再次广播请求前存入历史记录列表。

当访问链长度(即历史记录数量)大于等于限定长度时,要返回无效的状态和空数据,不再广播请求。

示例代码:


1
2
3
4
5
6
7
8
9
10
1def query(self, filename, history=[]):  # 定义接收请求的方法
2    state, data = self._handle(filename)  # 获取处理请求的结果
3    if state == OK:  # 如果是正常状态
4        return state, data  # 返回状态和数据
5    else:  # 否则
6        history.append(self.url)  # 历史记录添加已请求过的节点
7        if len(history) >= MAX_HISTORY_LENGTH:  # 如果历史请求超过6次
8            return FAIL, EMPTY  # 返回无效状态和空数据
9        return self._broadcast(filename, history)  # 返回广播结果
10

八、定义向添加其它节点到已知节点的方法

这个方法比较简单,只需要将其他节点的URL添加到已知节点。

示例代码:

不过要注意,服务器中的每个方法都必须有返回值,否则,会发生错误。错误提示为:“annot marshal None unless allow_none is enabled”,意思是不能返回None值,除非参数allow_none(允许为空)为启用。这个参数allow_none是指ServerProxy类进行实例化时的参数之一。


1
2
3
4
1def hello(self, other):  # 定义向添加其它节点到已知节点的方法
2    self.known.add(other)  # 添加其它节点到已知节点
3    return OK  # 返回值是必须的
4

九、定义下载的方法

为了避免通过未经许可的渠道获取资源,我们需要在实例化节点时设定密钥,并在下载节点资源时验证密钥。

当密钥验证成功,我们通过接收请求的方法进行请求处理,获取到资源状态和读取的数据。

当资源状态正常时,进行文件的创建,将读取到的数据写入到文件中。

示例代码:


1
2
3
4
5
6
7
8
9
10
11
1def fetch(self, filename, secrt):  # 定义下载的方法
2    if secrt != self.secret:  # 如果密钥不匹配
3        return FAIL, EMPTY  # 返回无效状态和空数据
4    state, data = self.query(filename)  # 处理请求获取文件状态与与数据
5    if state == OK:  # 如果返回正常的状态
6        with open(join(self.dirname, filename), 'w') as file:  # 写入模式打开文件
7            file.write(data)  # 将获取到的数据写入文件
8        return OK  # 返回值是必须的
9    else:
10        return FAIL  # 返回值是必须的
11

十、启动服务器

最后,我们编写启动服务器的代码。

示例代码:


1
2
3
4
5
6
7
1if __name__ == '__main__':
2    url = 'http://127.0.0.1:6666'
3    directory = 'NodeFiles01'
4    secret = '123456'
5    node = Node(url, directory, secret)
6    node._start()
7

接下来,编写客户端代码。

一、发出请求

在客户端中发出请求,我们需要准备请求的文件名称和正确的密钥。

然后,通过ServerProxy类创建服务器代理对象,调用接收请求的方法。

示例代码:


1
2
3
4
5
6
7
8
9
10
11
12
1from xmlrpc.client import ServerProxy
2
3filename = 'file.txt'  # 请求的资源文件名称
4
5url1 = 'http://127.0.0.1:7777'  # 请求的服务器URL
6peer1 = ServerProxy(url1)  # 创建服务器代理对象
7print(peer1.query(filename))  # 调用服务器的接收请求方法
8
9url2 = 'http://127.0.0.1:6666'
10peer2 = ServerProxy(url2)
11print(peer2.query(filename))
12

进行这一步测试时,大家需要先启动多个服务器。

如果是本机测试,这些服务器要有不同的端口、目录名称以及密钥,并且在部分目录中放入被请求的文件。

例如:

Node(‘http://127.0.0.1:6666’, ‘NodeFiles01’, ‘123456’)  # 目录中有文件“file.txt”
Node(‘http://127.0.0.1:7777’, ‘NodeFiles02’, ‘654321’)

当我们运行上方客户端代码时,我们会看到结果:

[2, ”]
[1, ‘这是一个用于下载测试的文件!’]

二、添加节点到已知节点

我们将存在被请求文件的节点URL添加到已知节点。

示例代码:


1
2
3
1peer1.hello(url2)
2print(peer1.query(filename))
3

运行上方代码,显示结果为:

[2, ”]
[1, ‘这是一个用于下载测试的文件!’]
[1, ‘这是一个用于下载测试的文件!’]

三、下载文件

下载文件只需要添加一句代码,对用服务器代理对象的fetch()方法。

因为在上一段代码中,我们已经将存有请求文件的节点添加到了peer1的已知节点中,所以peer1节点能够完成文件下载。

示例代码:


1
2
1peer1.fetch(filename, '123456')  # 下载文件
2

 

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

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

2021-10-11 16:36:11

安全经验

安全咨询服务

2022-1-12 14:11:49

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