还在申请云服务器来传输数据嘛?试试P2P直连吧

本篇文章是最近开发「Todo-List」应用的 P2P 数据传输功能的经验分享!

前言

一般用户与用户间的数据交互是咋样的呢?

用户A要发送数据给用户B,那么用户A需要先将数据发送给服务器,服务器接收数据并存储后,再中转给用户B。

flowchart LR subgraph A [C/S 模式] direction TB S[中央服务器
唯一数据源与逻辑中心] <-- 数据传输 --> C1[客户端A] S <-- 数据传输 --> C2[客户端B] S <-- 数据传输 --> C3[...] style S fill:#ffcccc end

也就是经典的「客户端-服务器」模式。

这种方式最常见于各种商业化运作的企业,可以有效管理和掌控用户数据。但相对于个体开发者应用反倒可能是劣势。

  • 有些个体开发者应用并不需要用户间经常性的数据交互,那么要为此配置一台24h在线的服务器做交互,还是有点成本的;

  • 另外,如果采用这种模式,数据交互的稳定性很大程度上取决于服务器资源和带宽,服务器挂了,整个交互就瘫痪了。

可能有人会问:可不可以不经过服务器,直接用户和用户之间进行交互呢?这就是今天要介绍的 P2P 模式。


P2P传输(Peer-to-Peer,点对点传输) 无需依赖中心服务器进行中转或调度,参与者之间直接共享资源

flowchart LR subgraph A [P2P 模式] direction TB P1[参与者A
兼具客户端与服务端功能] <--> P2[参与者B
兼具客户端与服务端功能] P2 <--> P3[...] P3 <--> P1 end

通过这种无中心化的方式,不再依赖任何单一节点,从个体开发者来说,开发和维护非单机简单应用,成本可以是0的。

就以我最近开发的「Todo-List」应用为例,数据基本是单机应用数据,唯一需要进行交互的就桌面端应用和手机端应用的数据同步,完全可以采用这种模式进行数据同步的。

目前打通了局域网 P2P 数据传输问题,还在考虑端口要不要持续保持开启状态,从而实现两端自动化数据同步;以及实现内网穿透,在公网也能进行数据传输(这个有点上难度,尤其是国内多层NAT的情况下)......


交互逻辑

来说说具体实现吧------局域网内的 P2P 大体的交互是这样的:

存在P2P服务器端分享数据,P2P客户端接收数据

  • P2P服务器在局域网内暴露端口号和 IP

  • P2P客户端扫描局域网内暴露指定端口号的设备,并发现P2P服务器

  • P2P客户端和P2P服务器建立连接,并进行数据传输


代码实现

代码实现的话,在理解思路后其实可以使用 AI 辅助编码的。这里提供「Todo-List」基于 AI 精简后的 python 版本代码实现。

p2p_server

python 复制代码
"""
P2P服务器模块 - 负责监听并共享数据
精简版本:移除防火墙管理,专注于核心监听和共享功能
"""
import socket
import threading
import json
import struct
from typing import Optional, Tuple, Callable, Any


class P2PServer:
    """P2P服务器,用于在局域网内共享数据"""

    def __init__(self, port: int = 5000):
        """
        初始化P2P服务器

        Args:
            port: 监听端口,默认5000
        """
        self.port = port
        self.server_socket: Optional[socket.socket] = None
        self.is_running = False
        self.on_data_request_callback: Optional[Callable] = None

        # 数据共享函数(默认返回空数据)
        self._shared_data = {}

    def start(self, shared_data: Optional[dict] = None) -> Tuple[bool, str]:
        """
        启动P2P服务器

        Args:
            shared_data: 要共享的数据字典

        Returns:
            (是否成功, 消息) 元组
        """
        if self.is_running:
            return False, "服务器已在运行中"

        # 设置共享数据
        if shared_data:
            self._shared_data = shared_data

        try:
            # 创建TCP服务器socket
            self.server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            self.server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
            self.server_socket.bind(('0.0.0.0', self.port))
            self.server_socket.listen(5)  # 最多5个等待连接
            self.is_running = True

            # 启动监听线程
            listen_thread = threading.Thread(
                target=self._listen_for_connections,
                daemon=True
            )
            listen_thread.start()

            local_ip = self.get_local_ip()
            print(f"[P2P服务器] ✓ 服务器启动成功")
            print(f"[P2P服务器]   监听地址: 0.0.0.0:{self.port}")
            print(f"[P2P服务器]   本机IP: {local_ip}")

            return True, f"服务器启动成功,本机IP: {local_ip}"

        except OSError as e:
            error_msg = f"端口 {self.port} 被占用: {e}"
            print(f"[P2P服务器] ✗ {error_msg}")
            return False, error_msg
        except Exception as e:
            error_msg = f"服务器启动失败: {e}"
            print(f"[P2P服务器] ✗ {error_msg}")
            return False, error_msg

    def set_data_request_callback(self, callback: Callable):
        """
        设置数据请求回调函数

        Args:
            callback: 当收到数据请求时调用的函数,返回要共享的数据字典
        """
        self.on_data_request_callback = callback

    def set_shared_data(self, data: dict):
        """
        设置要共享的数据

        Args:
            data: 要共享的数据字典
        """
        self._shared_data = data
        print(f"[P2P服务器] 共享数据已更新: {len(data)} 项")

    def stop(self) -> Tuple[bool, str]:
        """
        停止服务器

        Returns:
            (是否成功, 消息) 元组
        """
        if not self.is_running:
            return False, "服务器未运行"

        print(f"[P2P服务器] 正在停止服务器...")

        self.is_running = False

        if self.server_socket:
            try:
                self.server_socket.close()
            except:
                pass
            self.server_socket = None

        print(f"[P2P服务器] ✓ 服务器已停止")
        return True, "服务器已停止"

    def _listen_for_connections(self):
        """
        监听客户端连接(在独立线程中运行)
        """
        while self.is_running:
            try:
                # 设置超时以便定期检查is_running
                self.server_socket.settimeout(1.0)
                client_socket, address = self.server_socket.accept()

                print(f"[P2P服务器] 收到来自 {address[0]}:{address[1]} 的连接")

                # 为每个客户端创建处理线程
                client_thread = threading.Thread(
                    target=self._handle_client,
                    args=(client_socket, address),
                    daemon=True
                )
                client_thread.start()

            except socket.timeout:
                # 超时正常,继续循环
                continue
            except Exception as e:
                if self.is_running:
                    print(f"[P2P服务器] 接受连接错误: {e}")
                break

    def _handle_client(self, client_socket: socket.socket, address: tuple):
        """
        处理客户端连接

        Args:
            client_socket: 客户端socket连接
            address: 客户端地址 (ip, port)
        """
        try:
            # 接收数据长度头(4字节)
            length_data = self._receive_all(client_socket, 4)
            if not length_data:
                return

            data_length = struct.unpack('!I', length_data)[0]

            # 接收实际数据
            data_bytes = self._receive_all(client_socket, data_length)
            if not data_bytes:
                return

            # 解析JSON数据
            data = json.loads(data_bytes.decode('utf-8'))
            data_type = data.get('type', 'unknown')
            print(f"[P2P服务器] 收到 {address[0]} 的请求: {data_type}")

            # 处理数据请求
            if data_type == 'request_data':
                # 获取要共享的数据
                if self.on_data_request_callback:
                    shared_data = self.on_data_request_callback()
                else:
                    shared_data = self._shared_data

                if shared_data:
                    # 发送共享数据
                    self._send_data(client_socket, shared_data)
                    response = {'status': 'success', 'message': '数据发送成功'}
                else:
                    response = {'status': 'error', 'message': '没有可共享的数据'}

                # 发送响应
                self._send_response(client_socket, response)

            else:
                # 处理其他类型的数据
                print(f"[P2P服务器] 收到 {data_type} 类型数据: {data}")
                response = {'status': 'success', 'message': '数据接收成功'}
                self._send_response(client_socket, response)

        except json.JSONDecodeError:
            print(f"[P2P服务器] 数据格式错误")
            response = {'status': 'error', 'message': '数据格式错误'}
            self._send_response(client_socket, response)
        except Exception as e:
            print(f"[P2P服务器] 处理客户端错误: {e}")
            try:
                response = {'status': 'error', 'message': str(e)}
                self._send_response(client_socket, response)
            except:
                pass
        finally:
            client_socket.close()

    def _receive_all(self, sock: socket.socket, length: int) -> Optional[bytes]:
        """
        接收指定长度的数据

        Args:
            sock: socket连接
            length: 期望接收的数据长度

        Returns:
            接收到的字节数据,失败返回None
        """
        data = bytearray()
        while len(data) < length:
            try:
                chunk = sock.recv(length - len(data))
                if not chunk:
                    return None
                data.extend(chunk)
            except:
                return None
        return bytes(data)

    def _send_data(self, sock: socket.socket, data: dict):
        """
        发送数据(长度 + 内容)

        Args:
            sock: socket连接
            data: 要发送的数据字典
        """
        data_str = json.dumps(data, ensure_ascii=False)
        data_bytes = data_str.encode('utf-8')
        length_data = struct.pack('!I', len(data_bytes))  # 4字节长度头
        sock.sendall(length_data + data_bytes)

    def _send_response(self, sock: socket.socket, response: dict):
        """
        发送响应

        Args:
            sock: socket连接
            response: 响应数据字典
        """
        response_str = json.dumps(response, ensure_ascii=False)
        response_bytes = response_str.encode('utf-8')
        length_data = struct.pack('!I', len(response_bytes))
        sock.sendall(length_data + response_bytes)

    def get_local_ip(self) -> str:
        """
        获取本机IP地址

        Returns:
            本机IP地址字符串
        """
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            local_ip = s.getsockname()[0]
            s.close()
            return local_ip
        except:
            return "127.0.0.1"

    def get_status(self) -> dict:
        """
        获取服务器状态

        Returns:
            服务器状态字典
        """
        return {
            'is_running': self.is_running,
            'port': self.port,
            'local_ip': self.get_local_ip(),
            'has_callback': self.on_data_request_callback is not None,
            'shared_data_size': len(self._shared_data)
        }

p2p_client

python 复制代码
"""
P2P客户端模块 - 负责发现和接收共享数据
精简版本:移除防火墙管理,专注于核心发现和接收功能
"""
import socket
import json
import struct
import threading
from typing import Optional, List, Tuple


class P2PClient:
    """P2P客户端,用于扫描局域网并接收共享数据"""

    def __init__(self, port: int = 5000):
        """
        初始化P2P客户端

        Args:
            port: P2P通信端口,默认5000
        """
        self.port = port

    def scan_devices(self, timeout: float = 3.0) -> List[Tuple[str, str]]:
        """
        扫描局域网内可用的P2P设备

        Args:
            timeout: 扫描超时时间(秒),默认3秒

        Returns:
            设备列表,每个设备为 (ip, hostname) 元组
        """
        devices = []

        try:
            # 获取本机IP和网段
            local_ip = self.get_local_ip()
            if not local_ip or local_ip == "127.0.0.1":
                print("[P2P客户端] 无法获取有效的本机IP地址")
                return devices

            # 解析网段(例如: 192.168.1.0/24)
            ip_parts = local_ip.split('.')
            network_base = f"{ip_parts[0]}.{ip_parts[1]}.{ip_parts[2]}"

            # 扫描网段内的设备
            threads = []
            results = []

            print(f"[P2P客户端] 开始扫描网段: {network_base}.0/24, 本机IP: {local_ip}")

            # 创建扫描线程
            for i in range(1, 255):
                target_ip = f"{network_base}.{i}"
                thread = threading.Thread(
                    target=self._check_device,
                    args=(target_ip, timeout, results, local_ip)
                )
                thread.start()
                threads.append(thread)

            # 等待所有线程完成
            for thread in threads:
                thread.join()

            print(f"[P2P客户端] 扫描完成,发现 {len(results)} 个设备")
            devices = [(ip, f"设备_{ip}") for ip in results]

        except Exception as e:
            print(f"[P2P客户端] 扫描设备错误: {e}")

        return devices

    def _check_device(self, ip: str, timeout: float, results: list, local_ip: str):
        """
        检查指定IP是否开放了P2P服务

        Args:
            ip: 目标IP地址
            timeout: 连接超时时间
            results: 存储结果的列表
            local_ip: 本机IP地址(用于跳过自身)
        """
        # 跳过本机IP
        if ip == local_ip:
            return

        try:
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(timeout)

            # 尝试连接目标端口
            result = sock.connect_ex((ip, self.port))
            if result == 0:
                results.append(ip)
                print(f"[P2P客户端] 发现设备: {ip}")

            sock.close()
        except:
            pass

    def receive_data(self, ip: str, timeout: int = 30) -> Optional[dict]:
        """
        从指定设备接收数据

        Args:
            ip: 目标设备IP地址
            timeout: 超时时间(秒),默认30秒

        Returns:
            接收到的数据字典,失败返回None
        """
        try:
            print(f"[P2P客户端] 正在连接 {ip}:{self.port}...")

            # 建立TCP连接
            sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            sock.settimeout(timeout)
            sock.connect((ip, self.port))

            # 发送数据请求
            request = json.dumps({'type': 'request_data'})
            self._send_data(sock, request)

            # 接收数据长度(4字节)
            length_data = self._receive_all(sock, 4)
            if not length_data:
                sock.close()
                return None

            data_length = struct.unpack('!I', length_data)[0]
            print(f"[P2P客户端] 接收数据长度: {data_length} bytes")

            # 接收数据内容
            data_bytes = self._receive_all(sock, data_length)
            if not data_bytes:
                sock.close()
                return None

            # 解析JSON数据
            data = json.loads(data_bytes.decode('utf-8'))

            # 接收响应确认
            response_length_data = self._receive_all(sock, 4)
            if response_length_data:
                response_length = struct.unpack('!I', response_length_data)[0]
                response_data = self._receive_all(sock, response_length)
                if response_data:
                    response = json.loads(response_data.decode('utf-8'))
                    if response.get('status') == 'success':
                        print(f"[P2P客户端] 数据接收成功")
                    else:
                        print(f"[P2P客户端] 数据接收失败: {response.get('message')}")

            sock.close()
            return data

        except socket.timeout:
            print(f"[P2P客户端] 连接超时: {ip}")
            return None
        except ConnectionRefusedError:
            print(f"[P2P客户端] 连接被拒绝: {ip}")
            return None
        except Exception as e:
            print(f"[P2P客户端] 接收数据错误: {e}")
            return None

    def _send_data(self, sock: socket.socket, data: str):
        """
        发送数据(长度 + 内容)

        Args:
            sock: socket连接
            data: 要发送的字符串数据
        """
        data_bytes = data.encode('utf-8')
        length_data = struct.pack('!I', len(data_bytes))  # 4字节长度头
        sock.sendall(length_data + data_bytes)

    def _receive_all(self, sock: socket.socket, length: int) -> Optional[bytes]:
        """
        接收指定长度的数据

        Args:
            sock: socket连接
            length: 期望接收的数据长度

        Returns:
            接收到的字节数据,失败返回None
        """
        data = bytearray()
        while len(data) < length:
            try:
                chunk = sock.recv(length - len(data))
                if not chunk:
                    return None
                data.extend(chunk)
            except:
                return None
        return bytes(data)

    def get_local_ip(self) -> str:
        """
        获取本机IP地址

        Returns:
            本机IP地址字符串
        """
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("8.8.8.8", 80))
            local_ip = s.getsockname()[0]
            s.close()
            return local_ip
        except:
            return "127.0.0.1"

p2p_test_example

实际测验时,需要开启两个测试类,一个模拟服务端、一个模拟客户端;另外如果是真实环境使用,需要额外考虑防火墙问题,这里就不做展开了

python 复制代码
"""
P2P示例代码 - 演示如何使用精简版P2P库
"""
import time
from p2p_server import P2PServer
from p2p_client import P2PClient


def run_server_example():
    """运行服务器示例"""
    print("=== P2P服务器示例 ===")

    # 创建服务器实例
    server = P2PServer(port=5000)

    # 设置要共享的数据
    shared_data = {
        'device_name': '我的设备',
        'device_type': '电脑',
        'timestamp': time.time(),
        'message': '你好,这是共享的数据!',
        'data': {
            'item1': '值1',
            'item2': '值2',
            'item3': [1, 2, 3, 4, 5]
        }
    }

    # 启动服务器
    success, message = server.start(shared_data)
    if not success:
        print(f"服务器启动失败: {message}")
        return

    print(f"服务器状态: {server.get_status()}")

    try:
        # 保持服务器运行
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        print("\n正在停止服务器...")
        server.stop()


def run_client_example():
    """运行客户端示例"""
    print("=== P2P客户端示例 ===")

    # 创建客户端实例
    client = P2PClient(port=5000)

    # 扫描局域网设备
    print("正在扫描局域网设备...")
    devices = client.scan_devices(timeout=2.0)

    if not devices:
        print("未发现任何设备")
        return

    print(f"发现 {len(devices)} 个设备:")
    for i, (ip, name) in enumerate(devices, 1):
        print(f"  {i}. {name} ({ip})")

    # 从第一个设备接收数据
    if devices:
        target_ip = devices[0][0]
        print(f"\n正在从 {target_ip} 接收数据...")

        data = client.receive_data(target_ip, timeout=10)

        if data:
            print(f"接收到数据:")
            for key, value in data.items():
                if isinstance(value, dict):
                    print(f"  {key}: {len(value)} 项")
                else:
                    print(f"  {key}: {value}")
        else:
            print("数据接收失败")


def run_full_example():
    """运行完整示例(需要在两个终端分别运行)"""
    print("P2P系统完整示例")
    print("=" * 40)
    print("1. 启动服务器(共享数据)")
    print("2. 启动客户端(接收数据)")
    print("3. 退出")

    choice = input("请选择 (1/2/3): ").strip()

    if choice == '1':
        run_server_example()
    elif choice == '2':
        run_client_example()
    else:
        print("退出")


if __name__ == "__main__":
    run_full_example()

结语

当然,P2P直连也不是万能的,只适用于特定场景,大多数商业化场景下还是要考虑C/S连接,或者其变体B/S连接。

好啦,以上就是今天分享的内容啦。感谢阅读,如果觉得对你有帮助,记得三连哦!!!

相关推荐
黄宝康2 小时前
sublimetext 运行python程序
开发语言·python
开心猴爷2 小时前
iOS 代码混淆在项目中的方式, IPA 级保护实践记录
后端
matlabgoodboy3 小时前
程序代做python代编程matlab定制代码编写C++代写plc设计java帮做
c++·python·matlab
魅影骑士00103 小时前
柯里化函数
后端·设计模式
副露のmagic3 小时前
更弱智的算法学习 day34
python·学习
AllFiles3 小时前
用Python turtle画出标准五星红旗,原来国旗绘制有这么多数学奥秘!
python
JOEH603 小时前
🛡️ 微服务雪崩救星:Sentinel 限流熔断实战,3行代码搞定高可用!
后端·全栈
亲爱的非洲野猪3 小时前
Java线程池深度解析:从原理到最佳实践
java·网络·python
用户1377940499933 小时前
基于遗传算法实现自动泊车+pygame可视化
python