p2p下载

  • P2P对等式网络又称点对点技术,是无中心服务器、依靠用户群(peers)交换信息的互联网体系,它的作用在于,减低以往网络传输中的节点,以降低数据丢失的风险。
  • 通俗而言,P2P下载就是指数据的传输不再通过服务器,而是网络用户之间直接传递数据。
  • 简版p2p下载流程:

server

本代码使用RPC(Remote Procedure Call)远程过程调用的思路实现P2P下载,并实现绝大部分逻辑,client只是对server做一个封装。

server.py 源码
#-- coding:UTF-8 --
from xmlrpc.server import SimpleXMLRPCServer  # 用于创建服务器
from xmlrpc.client import ServerProxy ,Fault # 用于向其它节点发出请求
from urllib.parse import urlparse  # 用于URL解析
from os.path import join, isfile ,exists , abspath # 用于路径处理和文件查询
from os import mkdir
import sys

MAX_HISTORY_LENGTH = 6  # 访问链最大长度

SimpleXMLRPCServer.allow_reuse_address = 1  # 保证节点服务器重启时能够立即访问
UNHANDLED = 100  # 文件不存在的异常代码
ACCESS_DENIED = 200  # 文件访问受限的异常代码

class UnhandledQuery(Fault):  # 创建自定义异常类
    def __init__(self, message='无法处理请求!'):  # 定义构造方法
        Fault.__init__(self, UNHANDLED, message)  # 重载超类构造方法

class AccessDenied(Fault):  # 创建自定义异常类
    def __init__(self, message='访问资源受限!'):  # 定义构造方法
        Fault.__init__(self, ACCESS_DENIED, message)  # 重载超类构造方法

def inside(dir_path, file_path):  # 定义文件路径检查的方法
    directory = abspath(dir_path)  # 获取共享目录的绝对路径
    file = abspath(file_path)  # 获取请求资源的绝对路径
    return file.startswith(join(directory, ''))  # 返回请求资源的路径是否以共享目录路径开始

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

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):  # 定义启动服务器的方法
        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  # 返回无效状态和空数据
            raise UnhandledQuery  # 抛出文件不存在的异常
        if not inside(self.dirname, file_path):  # 如果请求的资源不是共享目录中的资源
            raise AccessDenied  # 抛出访问资源受限异常
        return open(file_path).read()  # 未发生异常时返回读取的文件数据
        # 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)
                return server.query(filename, history)
            except Fault as f:  # 如果捕获访问故障异常获取异常代码
                if f.faultCode == UNHANDLED:  # 如果是文件不存在异常
                    pass  # 不做任何处理
                else:  # 如果是其它故障异常
                    self.known.remove(other)  # 从已知节点列表中移除节点
            except:  # 如果捕获其它异常(非故障异常)
                self.known.remove(other)  # 从已知节点列表中移除节点
        raise UnhandledQuery  # >>如果已知节点都未能请求到资源<<,抛出文件不存在异常。    

    def query(self, filename, history=[]):  # 定义接受请求的方法
        try:
            return self._handle(filename)
        except UnhandledQuery:  # 如果捕获文件不存在的异常
            history.append(self.url)
            if len(history) >= MAX_HISTORY_LENGTH:
                raise
            return self._broadcast(filename, history)
      
    def hello(self, other):  # 定义向添加其它节点到已知节点的方法
        self.known.add(other)  # 添加其它节点到已知节点
        return OK  # 返回值是必须的

    def fetch(self, filename, secret):  # 定义下载的方法
        if secret != self.secret:  # 如果密钥不匹配
            raise AccessDenied  # 抛出访问资源受限异常
        result = self.query(filename)
        with open(join(self.dirname, filename), 'w') as file:
            file.write(result)
        return 0  # 必须返回非None的值
      
def main(): 
    url, directory, secret = sys.argv[1:]
    node = Node(url, directory, secret)
    node._start()

if __name__ == '__main__':   
    main()

client

客户端测试说明

  • 0.文件准备

① 创建多个url文件,文件内容为其他节点的地址
② 创建多个dir目录
③ 其中任一个目录下创建file文件,供其他节点下载

  • 1.登录: python client.py [urlfile] [dirname] [host:port]
$ python3 client.py url2.txt NodeFiles02 http://127.0.0.1:7777    
>>>
  • 2.下载: >>> fetch [filename]
>>>fetch tt
调用服务器代理对象的下载方法
  • 3.退出: >>> exit
>>>exit
------------------退出程序------------------
源码cilent.py
from xmlrpc.client import ServerProxy, Fault  # 导入服务器代理类和故障类
from random import choice  # 导入随机选取的方法
from string import ascii_lowercase  # 导入小写字母列表对象
from time import sleep  # 导入延迟方法
from server import Node, UNHANDLED  # 导入服务器中的节类和变量
from threading import Thread  # 导入线程类
from cmd import Cmd  # 导入命令类
import sys  # 导入系统模块
import os

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

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

# 因为要使用CMD界面,所以这个类要继承CMD类。
class Client(Cmd):
    prompt = '>>>'  # 重写超类中的命令提示符

    def __init__(self, url_file, dir_name, url):  # 定义构造方法
        Cmd.__init__(self)  # 重载超类的构造方法
        self.secret = random_string(SECRET_LENGTH)  # 创建密钥变量
        node = Node(url, dir_name, self.secret)  # 创建节点对象
        thread = Thread(target=node._start)  # 在独立的线程中启动服务器
        thread.setDaemon(True)  # 将线程设置为守护线程
        thread.start()  # 启动线程
        sleep(HEAD_START)  # 等待服务器启动
        self.server = ServerProxy(url)  # 创建服务器代理对象
        for line in open(url_file):  # 读取URL文件
            self.server.hello(line.strip())  # 添加URL文件中的URL到已知节点集合

    # 类中对应命令的方法命名,都要以“do_”开头。
    def do_fetch(self, filename):  # 定义下载命令的方法
        try:
            self.server.fetch(filename, self.secret)  # 调用服务器代理对象的下载方法
            print('调用服务器代理对象的下载方法')
        except Fault as f:  # 捕获故障异常
            if f.faultCode != UNHANDLED:  # 如果异常代码不是未找到文件
                pass  # 不做处理
            print('找不到文件:', filename)

    def do_exit(self, arg):  # 定义退出命令的方法
        print('------------------退出程序------------------')
        sys.exit()  # 系统退出

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

if __name__ == '__main__':
    print('Download: >>>fetch [filename]')
    print('Exit: >>>exit')
    main()

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

本文代码参考自链接,并将其中python2.7 的部分用python3.7重新实现