Xây dựng hệ thống chia sẻ tệp từ xa với XML-RPC trong Python

XML-RPC là giao thức gọi hàm từ xa dựa trên XML và HTTP, cho phép các ứng dụng giao tiếp qua mạng một cách đơn giản. Trong bài này, ta sẽ xây dựng một hệ thống P2P chia sẻ tệp sử dụng XML-RPC — tương tự mô hình của các công cụ như BitTorrent hay eDonkey.

1. Thử nghiệm cơ bản

Tạo file server_basic.py:

from xmlrpc.server import SimpleXMLRPCServer

def square_value(val):
    return val ** 2

server = SimpleXMLRPCServer(('localhost', 4242), allow_none=True)
server.register_function(square_value, 'square')
server.serve_forever()

Sau đó, mở terminal khác và chạy:

from xmlrpc.client import ServerProxy

proxy = ServerProxy('http://localhost:4242')
print(proxy.square(6))  # Kết quả: 36

2. Xây dựng server chia sẻ tệp

Tạo file file_server.py:

# -*- coding: utf-8 -*-
import sys
import os
from xmlrpc.server import SimpleXMLRPCServer
from xmlrpc.client import ServerProxy, Fault
from urllib.parse import urlparse

SimpleXMLRPCServer.allow_reuse_address = True

MAX_HOPS = 5
ERR_NOT_FOUND = 100
ERR_FORBIDDEN = 200

class FileNotFound(Fault):
    def __init__(self, msg="File not found or unhandled"):
        super().__init__(ERR_NOT_FOUND, msg)

class ForbiddenAccess(Fault):
    def __init__(self, msg="Access denied to this resource"):
        super().__init__(ERR_FORBIDDEN, msg)

def is_within_directory(base_dir, target_path):
    base = os.path.abspath(base_dir)
    target = os.path.abspath(target_path)
    return os.path.commonpath([base, target]) == base

def extract_port(url):
    parsed = urlparse(url)
    if ':' in parsed.netloc:
        return int(parsed.netloc.split(':')[-1])
    return 80

class PeerNode:
    def __init__(self, node_url, shared_dir, auth_key):
        self.url = node_url
        self.dir = shared_dir
        self.key = auth_key
        self.peers = set()

    def search(self, filename, visited=None):
        if visited is None:
            visited = []
        try:
            return self._local_lookup(filename)
        except FileNotFound:
            visited.append(self.url)
            if len(visited) > MAX_HOPS:
                raise
            return self._propagate_search(filename, visited)

    def connect(self, peer_url):
        self.peers.add(peer_url)
        return True

    def retrieve(self, filename, key):
        if key != self.key:
            raise ForbiddenAccess()
        content = self.search(filename)
        filepath = os.path.join(self.dir, filename)
        with open(filepath, 'wb') as f:
            f.write(content.data)
        return True

    def _run_server(self):
        port = extract_port(self.url)
        rpc_server = SimpleXMLRPCServer(('localhost', port), logRequests=False)
        rpc_server.register_instance(self)
        rpc_server.serve_forever()

    def _local_lookup(self, filename):
        full_path = os.path.join(self.dir, filename)
        if not os.path.isfile(full_path):
            raise FileNotFound()
        if not is_within_directory(self.dir, full_path):
            raise ForbiddenAccess()
        with open(full_path, 'rb') as f:
            import xmlrpc.client
            return xmlrpc.client.Binary(f.read())

    def _propagate_search(self, filename, trail):
        for peer in list(self.peers):
            if peer in trail:
                continue
            try:
                remote = ServerProxy(peer)
                result = remote.search(filename, trail)
                return result
            except Fault as e:
                if e.faultCode != ERR_NOT_FOUND:
                    self.peers.discard(peer)
            except Exception:
                self.peers.discard(peer)
        raise FileNotFound()

def launch_node():
    if len(sys.argv) != 4:
        print("Usage: python file_server.py <url> <directory> <secret>")
        sys.exit(1)
    url, folder, secret = sys.argv[1:]
    node = PeerNode(url, folder, secret)
    node._run_server()

if __name__ == '__main__':
    launch_node()

3. Tạo client điều khiển bằng dòng lệnh

Tạo file file_client.py:

# -*- coding: utf-8 -*-
import sys
import threading
import time
import random
import string
from cmd import Cmd
from xmlrpc.client import ServerProxy, Fault

from file_server import PeerNode, ERR_NOT_FOUND

INIT_DELAY = 0.1
KEY_LEN = 32

def generate_token(length):
    chars = string.ascii_lowercase
    return ''.join(random.choice(chars) for _ in range(length))

class FileClient(Cmd):
    prompt = 'p2p> '

    def __init__(self, config_file, local_dir, self_url):
        super().__init__()
        self.auth_key = generate_token(KEY_LEN)
        node = PeerNode(self_url, local_dir, self.auth_key)
        thread = threading.Thread(target=node._run_server)
        thread.daemon = True
        thread.start()
        time.sleep(INIT_DELAY)
        self.proxy = ServerProxy(self_url)
        with open(config_file, 'r') as f:
            for line in f:
                peer_url = line.strip()
                if peer_url:
                    self.proxy.connect(peer_url)

    def do_get(self, filename):
        """Tải tệp từ mạng P2P: get <filename>"""
        try:
            self.proxy.retrieve(filename, self.auth_key)
            print(f"Đã tải '{filename}' thành công.")
        except Fault as e:
            if e.faultCode == ERR_NOT_FOUND:
                print(f"Không tìm thấy tệp: {filename}")
            else:
                raise

    def do_quit(self, arg):
        """Thoát chương trình"""
        print("Đang thoát...")
        return True

    do_exit = do_quit

def main():
    if len(sys.argv) != 4:
        print("Usage: python file_client.py <peer_list.txt> <local_dir> <self_url>")
        sys.exit(1)
    peer_list, local_folder, my_url = sys.argv[1:]
    client = FileClient(peer_list, local_folder, my_url)
    client.cmdloop()

if __name__ == '__main__':
    main()

4. Hướng dẫn chạy thử

  1. Tạo thư mục nodeAnodeC.
  2. Trong nodeC, tạo file sample.txt với nội dung bất kỳ.
  3. Tạo file peersA.txt trong thư mục gốc, ghi vào: http://localhost:4243
  4. Tạo file peersC.txt (có thể để trống hoặc ghi ngược lại URL của node A).
  5. Mở terminal thứ nhất, chạy:
    python file_client.py peersA.txt nodeA http://localhost:4242
  6. Mở terminal thứ hai, chạy:
    python file_client.py peersC.txt nodeC http://localhost:4243
  7. Ở terminal thứ nhất, gõ: get sample.txt → ban đầu sẽ báo lỗi vì chưa biết node C.
  8. Khởi động lại client A (hoặc cập nhật danh sách peer trong code), sau đó thử lại → file sẽ được tải về thư mục nodeA.

Thẻ: python xml-rpc p2p file-sharing Distributed-System

Đăng vào ngày 13 tháng 7 lúc 19:36