本篇文章是最近开发「Todo-List」应用的
P2P数据传输功能的经验分享!
前言
一般用户与用户间的数据交互是咋样的呢?
用户A要发送数据给用户B,那么用户A需要先将数据发送给服务器,服务器接收数据并存储后,再中转给用户B。
唯一数据源与逻辑中心] <-- 数据传输 --> C1[客户端A] S <-- 数据传输 --> C2[客户端B] S <-- 数据传输 --> C3[...] style S fill:#ffcccc end
也就是经典的「客户端-服务器」模式。
这种方式最常见于各种商业化运作的企业,可以有效管理和掌控用户数据。但相对于个体开发者应用反倒可能是劣势。
-
有些个体开发者应用并不需要用户间经常性的数据交互,那么要为此配置一台24h在线的服务器做交互,还是有点成本的;
-
另外,如果采用这种模式,数据交互的稳定性很大程度上取决于服务器资源和带宽,服务器挂了,整个交互就瘫痪了。
可能有人会问:可不可以不经过服务器,直接用户和用户之间进行交互呢?这就是今天要介绍的 P2P 模式。
P2P传输(Peer-to-Peer,点对点传输) 无需依赖中心服务器进行中转或调度,参与者之间直接共享资源。
兼具客户端与服务端功能] <--> 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连接。
好啦,以上就是今天分享的内容啦。感谢阅读,如果觉得对你有帮助,记得三连哦!!!