最新消息:欢迎光临 魔力 • Python!大家可以点开导航菜单中的【学习目录】,这个目录类似图书目录,更加方便学习!

练习项目15:P2P在线文件共享(一)

Python教程 小楼一夜听春语 10670浏览 0评论

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

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

练习过程分为两个阶段:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

这里需要的是xmlrpc模块。

示例代码:(服务器)

from xmlrpc.server import SimpleXMLRPCServer

server = SimpleXMLRPCServer(('', 6666))  # 创建服务器对象

def twice(x):  # 定义供客户端调用的函数
    return 2 * x

server.register_function(twice)  # 注册开放给客户端的函数到服务器对象
server.serve_forever()  # 运行服务器

示例代码:(客户端)

from xmlrpc.client import ServerProxy

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

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

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

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

一、导入模块

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

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

示例代码:

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

二、定义常量

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

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

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

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

示例代码:

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

三、定义获取端口号的函数

当客户端需要下载资源时,实际上需要先对当前节点服务器的共享资源进行查询,所以,我们在创建服务器时,需要在参数中传入当前节点的URL以供查询,并且要根据这个URL中所提供的端口号,创建服务器对象。

所以,我们需要一个函数,能够获取URL中端口号。

示例代码:

def get_port(url):  # 定义获取端口号的函数
    result = urlparse(url)[1]  # 解析并获取URL中的[域名:端口号]
    port = result.split(':')[-1]  # 获取以":"进行分割后的最后一组
    return int(port)  # 转换为整数后返回

三、定义节点类(Node)

节点的类中包含一个节点的所有功能。

示例代码:

class Node:
    def __init__(self, url, dir_name, secret):
        self.url = url
        self.dirname = dir_name
        self.secret = secret
        self.known = set()

    def _start(self):  # 定义启动服务器的内部方法
        pass

    def _handle(self, filename):  # 定义处理请求的内部方法
        pass

    def _broadcast(self, filename, history):  # 定义广播的内部方法
        pass

    def query(self, filename, history=[]):  # 定义接受请求的方法
        pass

    def hello(self, other):  # 定义向添加其它节点到已知节点的方法
        pass

    def fetch(self, filename, secrt):  # 定义下载的方法
        pass

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

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

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

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

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

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

示例代码:

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

五、定义处理请求的方法

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

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

示例代码:

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

六、定义广播请求的方法

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

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

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

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

示例代码:

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

七、定义接收请求的方法

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

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

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

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

示例代码:

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

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

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

示例代码:

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

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

九、定义下载的方法

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

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

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

示例代码:

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

十、启动服务器

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

示例代码:

if __name__ == '__main__':
    url = 'http://127.0.0.1:6666'
    directory = 'NodeFiles01'
    secret = '123456'
    node = Node(url, directory, secret)
    node._start()

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

一、发出请求

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

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

示例代码:

from xmlrpc.client import ServerProxy

filename = 'file.txt'  # 请求的资源文件名称

url1 = 'http://127.0.0.1:7777'  # 请求的服务器URL
peer1 = ServerProxy(url1)  # 创建服务器代理对象
print(peer1.query(filename))  # 调用服务器的接收请求方法

url2 = 'http://127.0.0.1:6666'
peer2 = ServerProxy(url2)
print(peer2.query(filename))

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

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

例如:

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

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

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

二、添加节点到已知节点

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

示例代码:

peer1.hello(url2)
print(peer1.query(filename))

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

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

三、下载文件

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

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

示例代码:

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

本节练习源代码:【点此下载

转载请注明:魔力Python » 练习项目15:P2P在线文件共享(一)

头像
发表我的评论
取消评论

表情

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网站 (可选)

网友最新评论 (2)

  1. 头像
    ConnectionRefusedError: [WinError 10061] 由于目标计算机积极拒绝,无法连接
    哥斯拉吐泡泡5年前 (2019-10-24)回复
    • 头像
      废话,主机和路由器上的防火墙要设置成允许入站才行啊(默认都是不允许的)
      Kahr4年前 (2021-03-14)回复