个人主页:创客白泽 - CSDN博客
系列专栏:《Python开源项目实战》
热爱不止于代码,热情源自每一个灵感闪现的夜晚。愿以开源之火,点亮前行之路。
希望大家多多支持,我们一起进步!
如果文章对你有帮助的话,欢迎 点赞 评论 收藏 ⭐️ 加关注+分享给更多人哦
在局域网环境下快速传输大文件一直是办公场景的刚需。本文介绍一款基于PyQt5+Socket开发的高颜值文件共享工具,具有以下特点:
def scan_network(self):
# 使用并发Ping扫描(支持Windows/Linux双平台)
with concurrent.futures.ThreadPoolExecutor() as executor:
futures = {executor.submit(self.ping_host, ip): ip for ip in all_hosts}
# ...结果实时更新UI...
def _send_file(self, ip, file_path, password):
# 断点续传实现逻辑
offset = struct.unpack("!Q", offset_data)[0]
with open(file_path, 'rb') as f:
f.seek(offset) # 定位到断点位置
# 安装依赖库
pip install -r requirements.txt
# 所需库:
# PyQt5==5.15.7
# psutil==5.8.0
if __name__ == "__main__":
app = QApplication(sys.argv)
window = FileSharingApp()
window.show()
sys.exit(app.exec_())
def ping_host(self, ip):
# 跨平台ping命令适配
param = '-n' if platform.system() == 'Windows' else '-c'
command = ['ping', param, '1', '-w', '1000', ip]
# 使用subprocess避免阻塞主线程
output = subprocess.run(command, capture_output=True, text=True)
# 解析延迟(Windows/Linux不同输出格式)
if 'TTL=' in output.stdout: # Windows
latency = int(output.stdout.split('time=')[1].split('ms')[0])
关键技术点:
subprocess.run()
实现非阻塞调用# 接收方处理逻辑
if os.path.exists(temp_path):
received = os.path.getsize(temp_path) # 获取已接收字节数
client_socket.sendall(struct.pack("!Q", received)) # 告知发送方偏移量
# 发送方定位文件指针
f.seek(offset) # 跳转到断点位置
设计亮点:
.tmp
临时文件避免传输中断导致数据丢失!Q
确保偏移量精确传输import sys
import os
import socket
import threading
import time
import hashlib
import pickle
import struct
import json
import platform
import subprocess
import concurrent.futures
import psutil
from datetime import datetime
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QSplitter, QTreeWidget, QTreeWidgetItem, QLabel, QLineEdit,
QPushButton, QTextEdit, QFileDialog, QMessageBox, QInputDialog,
QSpinBox, QProgressBar, QTabWidget, QStatusBar, QFrame,
QHeaderView)
from PyQt5.QtCore import Qt, QTimer, QSize
from PyQt5.QtGui import QFont, QIcon
class FileSharingApp(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle(" 局域网文件共享工具")
self.setGeometry(100, 100, 1200, 800)
# 初始化变量
self.password = "123456"
self.hosts = {}
self.connections = {}
self.server_running = False
self.discovery_running = False
self.file_transfers = {}
self.retry_attempts = 3
self.retry_delay = 2
self.max_threads = 4
# 创建UI
self.init_ui()
# 确保接收目录存在
os.makedirs("received_files", exist_ok=True)
# 加载配置
self.load_config()
# 启动服务
self.start_server()
self.start_discovery()
# 设置定时器
self.setup_timers()
def init_ui(self):
"""初始化用户界面"""
main_widget = QWidget()
main_layout = QVBoxLayout()
main_widget.setLayout(main_layout)
self.setCentralWidget(main_widget)
# 主分割器
main_splitter = QSplitter(Qt.Vertical)
main_layout.addWidget(main_splitter)
# 上部区域
upper_splitter = QSplitter(Qt.Horizontal)
main_splitter.addWidget(upper_splitter)
# 左侧 - 网络主机发现
self.setup_host_discovery_ui(upper_splitter)
# 右侧 - 已连接主机
self.setup_connection_ui(upper_splitter)
# 下部区域
self.setup_transfer_log_ui(main_splitter)
# 状态栏
self.setup_status_bar()
# 设置初始大小
main_splitter.setSizes([600, 200])
upper_splitter.setSizes([400, 600])
def setup_host_discovery_ui(self, parent):
"""设置网络主机发现UI"""
frame = QFrame()
frame.setFrameShape(QFrame.StyledPanel)
layout = QVBoxLayout()
frame.setLayout(layout)
label = QLabel("️ 网络主机发现")
label.setStyleSheet("font-size: 14px; font-weight: bold;")
layout.addWidget(label)
self.host_tree = QTreeWidget()
self.host_tree.setHeaderLabels(["IP地址", "主机名称", "最后在线", "信号强度"])
self.host_tree.setColumnCount(4)
self.host_tree.header().setSectionResizeMode(QHeaderView.ResizeToContents)
layout.addWidget(self.host_tree)
# 主机操作按钮
btn_layout = QHBoxLayout()
buttons = [
(" 连接选中主机", self.connect_to_host),
("➕ 手动连接主机", self.manual_connect),
(" 重新扫描网络", self.rescan_network),
("️ 清除列表", self.clear_host_list)
]
for text, callback in buttons:
btn = QPushButton(text)
btn.clicked.connect(callback)
btn_layout.addWidget(btn)
layout.addLayout(btn_layout)
parent.addWidget(frame)
def setup_connection_ui(self, parent):
"""设置已连接主机UI"""
frame = QFrame()
frame.setFrameShape(QFrame.StyledPanel)
layout = QVBoxLayout()
frame.setLayout(layout)
label = QLabel(" 已连接主机")
label.setStyleSheet("font-size: 14px; font-weight: bold;")
layout.addWidget(label)
self.conn_tree = QTreeWidget()
self.conn_tree.setHeaderLabels(["IP地址", "主机名称", "状态", "延迟"])
self.conn_tree.setColumnCount(4)
self.conn_tree.header().setSectionResizeMode(QHeaderView.ResizeToContents)
layout.addWidget(self.conn_tree)
# 文件传输部分
self.setup_file_transfer_ui(layout)
# 密码设置部分
self.setup_password_settings_ui(layout)
parent.addWidget(frame)
def setup_file_transfer_ui(self, parent_layout):
"""设置文件传输UI"""
frame = QFrame()
layout = QVBoxLayout()
frame.setLayout(layout)
label = QLabel(" 文件传输")
label.setStyleSheet("font-size: 14px; font-weight: bold;")
layout.addWidget(label)
# 文件选择
file_layout = QHBoxLayout()
file_layout.addWidget(QLabel("选择文件:"))
self.file_path = QLineEdit()
file_layout.addWidget(self.file_path)
self.browse_btn = QPushButton(" 浏览...")
self.browse_btn.clicked.connect(self.browse_file)
file_layout.addWidget(self.browse_btn)
layout.addLayout(file_layout)
# 密码和发送
pass_layout = QHBoxLayout()
pass_layout.addWidget(QLabel("密码:"))
self.file_password = QLineEdit()
self.file_password.setEchoMode(QLineEdit.Password)
self.file_password.setText(self.password)
pass_layout.addWidget(self.file_password)
self.send_btn = QPushButton(" 发送文件")
self.send_btn.clicked.connect(self.send_file)
pass_layout.addWidget(self.send_btn)
layout.addLayout(pass_layout)
parent_layout.addWidget(frame)
def setup_password_settings_ui(self, parent_layout):
"""设置密码和配置UI"""
frame = QFrame()
layout = QVBoxLayout()
frame.setLayout(layout)
label = QLabel(" 密码与设置")
label.setStyleSheet("font-size: 14px; font-weight: bold;")
layout.addWidget(label)
# 密码更新
pass_layout = QHBoxLayout()
pass_layout.addWidget(QLabel("新密码:"))
self.new_password = QLineEdit()
self.new_password.setEchoMode(QLineEdit.Password)
pass_layout.addWidget(self.new_password)
pass_layout.addWidget(QLabel("确认密码:"))
self.confirm_password = QLineEdit()
self.confirm_password.setEchoMode(QLineEdit.Password)
pass_layout.addWidget(self.confirm_password)
self.update_pass_btn = QPushButton(" 更新密码")
self.update_pass_btn.clicked.connect(self.update_password)
pass_layout.addWidget(self.update_pass_btn)
layout.addLayout(pass_layout)
# 其他设置
settings_layout = QHBoxLayout()
settings_layout.addWidget(QLabel("传输线程:"))
self.thread_spin = QSpinBox()
self.thread_spin.setRange(1, 16)
self.thread_spin.setValue(self.max_threads)
settings_layout.addWidget(self.thread_spin)
settings_layout.addWidget(QLabel("重试次数:"))
self.retry_spin = QSpinBox()
self.retry_spin.setRange(1, 10)
self.retry_spin.setValue(self.retry_attempts)
settings_layout.addWidget(self.retry_spin)
self.save_settings_btn = QPushButton(" 保存设置")
self.save_settings_btn.clicked.connect(self.save_settings)
settings_layout.addWidget(self.save_settings_btn)
layout.addLayout(settings_layout)
parent_layout.addWidget(frame)
def setup_transfer_log_ui(self, parent):
"""设置传输状态和日志UI"""
tabs = QTabWidget()
# 传输状态标签页
transfer_tab = QWidget()
transfer_layout = QVBoxLayout()
transfer_tab.setLayout(transfer_layout)
label = QLabel(" 传输状态")
label.setStyleSheet("font-size: 14px; font-weight: bold;")
transfer_layout.addWidget(label)
self.transfer_tree = QTreeWidget()
self.transfer_tree.setHeaderLabels(["ID", "文件名", "目标IP", "进度", "状态", "速度"])
self.transfer_tree.setColumnCount(6)
self.transfer_tree.header().setSectionResizeMode(QHeaderView.ResizeToContents)
transfer_layout.addWidget(self.transfer_tree)
# 传输控制按钮
btn_layout = QHBoxLayout()
buttons = [
("⏸️ 暂停传输", self.pause_transfer),
("▶️ 继续传输", self.resume_transfer),
("❌ 取消传输", self.cancel_transfer),
(" 清除已完成", self.clear_completed)
]
for text, callback in buttons:
btn = QPushButton(text)
btn.clicked.connect(callback)
btn_layout.addWidget(btn)
transfer_layout.addLayout(btn_layout)
tabs.addTab(transfer_tab, " 传输状态")
# 日志标签页
log_tab = QWidget()
log_layout = QVBoxLayout()
log_tab.setLayout(log_layout)
label = QLabel(" 操作日志")
label.setStyleSheet("font-size: 14px; font-weight: bold;")
log_layout.addWidget(label)
self.log_text = QTextEdit()
self.log_text.setReadOnly(True)
log_layout.addWidget(self.log_text)
tabs.addTab(log_tab, " 操作日志")
parent.addWidget(tabs)
def setup_status_bar(self):
"""设置状态栏"""
status_bar = QStatusBar()
self.setStatusBar(status_bar)
self.status_label = QLabel()
self.update_status_text()
status_bar.addWidget(self.status_label, 1)
# 网络状态指示器
self.network_status = QLabel(" 网络: 检测中...")
status_bar.addPermanentWidget(self.network_status)
# 定时更新网络状态
self.network_timer = QTimer(self)
self.network_timer.timeout.connect(self.update_network_status)
self.network_timer.start(5000)
def setup_timers(self):
"""设置各种定时器"""
# 清理旧主机定时器
self.cleanup_timer = QTimer(self)
self.cleanup_timer.timeout.connect(self.cleanup_old_hosts)
self.cleanup_timer.start(10000) # 每10秒清理一次
# 更新传输状态定时器
self.status_timer = QTimer(self)
self.status_timer.timeout.connect(self.update_transfer_status)
self.status_timer.start(1000) # 每秒更新一次
# 更新连接状态定时器
self.connection_timer = QTimer(self)
self.connection_timer.timeout.connect(self.update_connection_status)
self.connection_timer.start(3000) # 每3秒更新一次
# 以下是网络发现和扫描功能 --------------------------------------
def get_local_network_info(self):
"""获取本地网络信息,包括IP和子网掩码"""
try:
# 获取所有网络接口信息
interfaces = psutil.net_if_addrs()
for interface, addrs in interfaces.items():
for addr in addrs:
if addr.family == socket.AF_INET and not addr.address.startswith('127.'):
# 获取子网掩码
netmask = addr.netmask
if netmask:
# 计算网络地址
network = self.calculate_network(addr.address, netmask)
return network, netmask
return None, None
except Exception as e:
self.log_message(f"⚠️ 获取网络信息失败: {str(e)}")
return None, None
def calculate_network(self, ip, netmask):
"""计算网络地址"""
ip_parts = list(map(int, ip.split('.')))
mask_parts = list(map(int, netmask.split('.')))
network_parts = [ip_parts[i] & mask_parts[i] for i in range(4)]
return '.'.join(map(str, network_parts))
def get_all_hosts_in_network(self, network, netmask):
"""获取网络中的所有可能主机IP"""
network_parts = list(map(int, network.split('.')))
mask_parts = list(map(int, netmask.split('.')))
# 计算主机位数
host_bits = sum([bin(x).count('0') - 1 for x in mask_parts])
num_hosts = 2 ** host_bits - 2 # 减去网络地址和广播地址
# 生成所有可能的IP
base_ip = network_parts.copy()
hosts = []
for i in range(1, num_hosts + 1):
host_ip = base_ip.copy()
host_ip[3] += i
# 处理进位
for j in range(3, 0, -1):
if host_ip[j] > 255:
host_ip[j] = 0
host_ip[j-1] += 1
hosts.append('.'.join(map(str, host_ip)))
return hosts
def ping_host(self, ip):
"""ping指定主机,返回是否在线和延迟"""
try:
# Windows系统使用'-n'参数,Linux/Unix使用'-c'
param = '-n' if platform.system().lower() == 'windows' else '-c'
count = '1'
timeout = '1000' # 毫秒
# 构建ping命令
command = ['ping', param, count, '-w', timeout, ip]
# 执行ping命令
output = subprocess.run(command, capture_output=True, text=True)
# 解析输出结果
if platform.system().lower() == 'windows':
if 'TTL=' in output.stdout:
# 提取延迟时间
time_line = [line for line in output.stdout.split('\n') if 'time=' in line][0]
latency = int(float(time_line.split('time=')[1].split('ms')[0]))
return True, latency
else:
if '1 received' in output.stdout:
# 提取延迟时间
time_line = [line for line in output.stdout.split('\n') if 'time=' in line][0]
latency = int(float(time_line.split('time=')[1].split(' ms')[0]))
return True, latency
return False, -1
except Exception as e:
self.log_message(f"⚠️ ping {ip} 失败: {str(e)}")
return False, -1
def scan_network(self):
"""扫描网络中的在线主机"""
self.log_message(" 正在扫描网络...")
# 获取本地网络信息
network, netmask = self.get_local_network_info()
if not network or not netmask:
self.log_message("❌ 无法确定本地网络信息")
return
# 获取所有可能的IP
all_hosts = self.get_all_hosts_in_network(network, netmask)
self.log_message(f" 扫描范围: {network}/{netmask} (共{len(all_hosts)}个IP)")
# 使用线程池并发ping
with concurrent.futures.ThreadPoolExecutor(max_workers=self.max_threads) as executor:
futures = {executor.submit(self.ping_host, ip): ip for ip in all_hosts}
for future in concurrent.futures.as_completed(futures):
ip = futures[future]
try:
is_online, latency = future.result()
if is_online:
try:
# 尝试获取主机名
hostname = socket.gethostbyaddr(ip)[0]
except:
hostname = "未知"
# 更新主机列表
self.hosts[ip] = {
'name': hostname,
'last_seen': time.time(),
'latency': latency
}
# 更新UI
self.update_host_tree()
except Exception as e:
self.log_message(f"⚠️ 扫描 {ip} 时出错: {str(e)}")
self.log_message(f"✅ 扫描完成,发现 {len(self.hosts)} 台在线主机")
def start_discovery(self):
"""启动主机发现服务"""
self.discovery_running = True
self.discovery_thread = threading.Thread(target=self.run_discovery, daemon=True)
self.discovery_thread.start()
self.log_message(" 主机发现服务已启动 (使用Ping扫描)")
def run_discovery(self):
"""运行主机发现主循环"""
while self.discovery_running:
self.scan_network()
# 每30秒扫描一次
time.sleep(30)
def update_host_tree(self):
"""更新主机树显示"""
self.host_tree.clear()
for ip, info in self.hosts.items():
last_seen_str = time.strftime("%H:%M:%S", time.localtime(info['last_seen']))
# 信号强度指示
if info['latency'] < 0:
signal = "❌ 离线"
elif info['latency'] < 50:
signal = " 强"
elif info['latency'] < 150:
signal = " 中"
else:
signal = " 弱"
item = QTreeWidgetItem([ip, info['name'], last_seen_str, signal])
# 根据延迟设置颜色
if info['latency'] > 150:
item.setForeground(3, Qt.red)
elif info['latency'] > 0:
item.setForeground(3, Qt.darkYellow)
else:
item.setForeground(3, Qt.green)
self.host_tree.addTopLevelItem(item)
# 以下是文件传输和连接管理功能 --------------------------------------
def update_status_text(self):
"""更新状态栏文本"""
hostname = socket.gethostname()
ip = self.get_local_ip()
self.status_label.setText(f" 就绪 | 主机: {hostname} | IP: {ip} | 线程: {self.max_threads}")
def update_network_status(self):
"""更新网络状态指示"""
online_hosts = len(self.hosts)
if online_hosts > 0:
self.network_status.setText(f" 网络: 良好 (发现{online_hosts}台主机)")
else:
self.network_status.setText(" 网络: 无连接")
def get_local_ip(self):
"""获取本地IP地址"""
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(("8.8.8.8", 80))
ip = s.getsockname()[0]
s.close()
return ip
except:
return "127.0.0.1"
def log_message(self, message):
"""记录日志消息"""
timestamp = datetime.now().strftime("%H:%M:%S")
self.log_text.append(f"[{timestamp}] {message}")
def browse_file(self):
"""浏览选择文件"""
file_path, _ = QFileDialog.getOpenFileName(self, "选择文件", "", "所有文件 (*.*)")
if file_path:
self.file_path.setText(file_path)
def update_password(self):
"""更新密码"""
new_pass = self.new_password.text()
confirm_pass = self.confirm_password.text()
if not new_pass:
QMessageBox.warning(self, "警告", "密码不能为空!")
return
if new_pass != confirm_pass:
QMessageBox.warning(self, "警告", "两次输入的密码不一致!")
return
self.password = new_pass
self.file_password.setText(new_pass)
self.log_message(f" 密码已更新为: {new_pass}")
QMessageBox.information(self, "成功", "密码更新成功!")
self.new_password.clear()
self.confirm_password.clear()
self.save_config()
def save_settings(self):
"""保存设置"""
try:
self.max_threads = self.thread_spin.value()
self.retry_attempts = self.retry_spin.value()
self.save_config()
self.log_message(f"⚙️ 设置已保存: 线程数={self.max_threads}, 重试次数={self.retry_attempts}")
self.update_status_text()
QMessageBox.information(self, "成功", "设置保存成功!")
except Exception as e:
QMessageBox.critical(self, "错误", f"保存设置时出错: {str(e)}")
def load_config(self):
"""加载配置"""
try:
if os.path.exists("config.json"):
with open("config.json", "r") as f:
config = json.load(f)
self.password = config.get("password", self.password)
self.max_threads = config.get("max_threads", self.max_threads)
self.retry_attempts = config.get("retry_attempts", self.retry_attempts)
self.retry_delay = config.get("retry_delay", self.retry_delay)
# 更新UI控件
self.file_password.setText(self.password)
self.thread_spin.setValue(self.max_threads)
self.retry_spin.setValue(self.retry_attempts)
except Exception as e:
self.log_message(f"⚠️ 加载配置失败: {str(e)}")
def save_config(self):
"""保存配置"""
try:
config = {
"password": self.password,
"max_threads": self.max_threads,
"retry_attempts": self.retry_attempts,
"retry_delay": self.retry_delay
}
with open("config.json", "w") as f:
json.dump(config, f)
except Exception as e:
self.log_message(f"⚠️ 保存配置失败: {str(e)}")
def start_server(self):
"""启动文件传输服务器"""
self.server_running = True
self.server_thread = threading.Thread(target=self.run_server, daemon=True)
self.server_thread.start()
self.log_message("️ 文件传输服务器已启动 (端口: 12345)")
def run_server(self):
"""运行服务器主循环"""
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
try:
server_socket.bind(('', 12345))
server_socket.listen(5)
while self.server_running:
try:
client_socket, addr = server_socket.accept()
threading.Thread(target=self.handle_client, args=(client_socket, addr), daemon=True).start()
except Exception as e:
if self.server_running:
self.log_message(f"❌ 服务器错误: {str(e)}")
finally:
server_socket.close()
def handle_client(self, client_socket, addr):
"""处理客户端连接"""
ip = addr[0]
transfer_id = None
try:
# 认证阶段
auth_data = client_socket.recv(1024)
if not auth_data:
return
auth_info = pickle.loads(auth_data)
host_name = auth_info.get('hostname', '未知')
password = auth_info.get('password', '')
if password != self.password:
self.log_message(f" 来自 {ip} 的连接尝试使用无效密码")
client_socket.sendall(b"AUTH_FAIL")
return
client_socket.sendall(b"AUTH_OK")
self.connections[ip] = client_socket
self.log_message(f" 已连接: {host_name} ({ip})")
self.update_connection_tree()
# 接收文件信息
file_info_data = client_socket.recv(1024)
if not file_info_data:
return
file_info = pickle.loads(file_info_data)
file_id = file_info['file_id']
filename = file_info['filename']
filesize = file_info['filesize']
file_hash = file_info['hash']
chunk_size = file_info.get('chunk_size', 4096)
# 处理文件名冲突
save_path = os.path.join("received_files", filename)
os.makedirs("received_files", exist_ok=True)
counter = 1
base, ext = os.path.splitext(filename)
while os.path.exists(save_path):
save_path = os.path.join("received_files", f"{base}_{counter}{ext}")
counter += 1
# 检查是否有未完成的传输
received = 0
temp_path = save_path + ".tmp"
if os.path.exists(temp_path):
received = os.path.getsize(temp_path)
self.log_message(f" 发现未完成传输: {filename}, 已接收: {self.format_size(received)}")
# 告诉客户端从哪个位置继续
client_socket.sendall(struct.pack("!Q", received))
transfer_id = f"{ip}_{file_id}"
self.file_transfers[transfer_id] = {
'filename': filename,
'size': filesize,
'received': received,
'status': 'transferring',
'start_time': time.time(),
'last_update': time.time(),
'speed': 0
}
# 开始接收文件
hasher = hashlib.sha256()
with open(temp_path, 'ab') as f:
while received < filesize:
try:
# 接收块长度信息
chunk_info = client_socket.recv(8)
if not chunk_info:
break
chunk_len = struct.unpack("!Q", chunk_info)[0]
chunk = b''
remaining = chunk_len
# 设置超时
client_socket.settimeout(30.0)
# 接收实际数据
start_time = time.time()
while remaining > 0:
part = client_socket.recv(remaining)
if not part:
break
chunk += part
remaining -= len(part)
if len(chunk) != chunk_len:
self.log_message(f"⚠️ 数据包不完整: {len(chunk)}/{chunk_len}字节")
# 请求重传
client_socket.sendall(struct.pack("!Q", received))
continue
# 写入文件并更新哈希
f.write(chunk)
hasher.update(chunk)
received += len(chunk)
# 更新传输状态
now = time.time()
time_elapsed = now - self.file_transfers[transfer_id]['last_update']
if time_elapsed > 0:
bytes_elapsed = received - self.file_transfers[transfer_id]['received']
self.file_transfers[transfer_id]['speed'] = bytes_elapsed / time_elapsed
self.file_transfers[transfer_id]['received'] = received
self.file_transfers[transfer_id]['last_update'] = now
# 每接收1MB发送一次确认
if received % (1024*1024) == 0:
client_socket.sendall(struct.pack("!Q", received))
except socket.timeout:
self.log_message(f"⚠️ 接收超时,等待重传...")
# 发送当前接收位置
client_socket.sendall(struct.pack("!Q", received))
continue
except Exception as e:
self.log_message(f"❌ 接收数据时出错: {str(e)}")
break
# 传输完成,验证文件
if received == filesize and hasher.hexdigest() == file_hash:
os.rename(temp_path, save_path)
self.log_message(f"✅ 成功接收文件: {save_path} ({self.format_size(filesize)})")
client_socket.sendall(b"FILE_OK")
self.file_transfers[transfer_id]['status'] = 'completed'
else:
self.log_message(f"❌ 文件校验失败: {filename}")
client_socket.sendall(b"FILE_FAIL")
self.file_transfers[transfer_id]['status'] = 'failed'
except Exception as e:
self.log_message(f"❌ 处理客户端 {ip} 时出错: {str(e)}")
if transfer_id in self.file_transfers:
self.file_transfers[transfer_id]['status'] = 'failed'
finally:
client_socket.close()
if ip in self.connections:
del self.connections[ip]
self.update_connection_tree()
def cleanup_old_hosts(self):
"""清理过期的主机记录"""
current_time = time.time()
to_remove = []
for ip, info in self.hosts.items():
if current_time - info['last_seen'] > 30: # 30秒无响应视为离线
to_remove.append(ip)
for ip in to_remove:
del self.hosts[ip]
if to_remove:
self.update_host_tree()
def update_connection_tree(self):
"""更新连接树"""
self.conn_tree.clear()
for ip, sock in list(self.connections.items()):
try:
hostname = socket.gethostbyaddr(ip)[0]
except:
hostname = "未知"
# 测量延迟
latency = self.measure_latency(ip)
if latency < 0:
status = "❌ 断开"
try:
sock.close()
except:
pass
del self.connections[ip]
continue
else:
status = "✅ 已连接"
item = QTreeWidgetItem([ip, hostname, status, f"{latency}ms"])
# 根据延迟设置颜色
if latency > 150:
item.setForeground(2, Qt.red)
item.setForeground(3, Qt.red)
elif latency > 50:
item.setForeground(2, Qt.darkYellow)
item.setForeground(3, Qt.darkYellow)
else:
item.setForeground(2, Qt.green)
item.setForeground(3, Qt.green)
self.conn_tree.addTopLevelItem(item)
def measure_latency(self, ip):
"""测量主机延迟"""
try:
start_time = time.time()
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2)
sock.connect((ip, 12345))
sock.close()
return int((time.time() - start_time) * 1000) # 毫秒
except:
return -1 # 表示无法测量
def update_connection_status(self):
"""定时更新连接状态"""
for ip in list(self.connections.keys()):
latency = self.measure_latency(ip)
if latency < 0: # 连接已断开
try:
self.connections[ip].close()
except:
pass
del self.connections[ip]
self.log_message(f"⚠️ 连接断开: {ip}")
self.update_connection_tree()
def update_transfer_status(self):
"""更新传输状态"""
self.transfer_tree.clear()
current_time = time.time()
for transfer_id, transfer in list(self.file_transfers.items()):
file_id = transfer_id.split('_')[-1]
filename = transfer['filename']
target_ip = transfer_id.split('_')[0]
received = transfer['received']
total = transfer['size']
# 计算进度和速度
progress = 0
if total > 0:
progress = min(100, int(received * 100 / total))
# 计算传输速度
time_elapsed = current_time - transfer['start_time']
if time_elapsed > 0:
speed = received / time_elapsed # 字节/秒
else:
speed = 0
# 格式化速度
if speed > 1024*1024:
speed_str = f"{speed/(1024*1024):.1f} MB/s"
elif speed > 1024:
speed_str = f"{speed/1024:.1f} KB/s"
else:
speed_str = f"{speed:.1f} B/s"
# 状态文本
status = transfer['status']
if status == 'transferring':
status_text = f"传输中 ({progress}%)"
elif status == 'completed':
status_text = "✅ 已完成"
elif status == 'failed':
status_text = "❌ 失败"
elif status == 'paused':
status_text = "⏸️ 已暂停"
elif status == 'canceled':
status_text = " 已取消"
else:
status_text = status
item = QTreeWidgetItem([
file_id[:6],
filename,
target_ip,
f"{progress}%",
status_text,
speed_str
])
# 设置进度条
progress_bar = QProgressBar()
progress_bar.setValue(progress)
progress_bar.setAlignment(Qt.AlignCenter)
# 根据状态设置颜色
if status == 'completed':
progress_bar.setStyleSheet("QProgressBar::chunk { background-color: green; }")
elif status in ['failed', 'canceled']:
progress_bar.setStyleSheet("QProgressBar::chunk { background-color: red; }")
elif status == 'paused':
progress_bar.setStyleSheet("QProgressBar::chunk { background-color: orange; }")
self.transfer_tree.setItemWidget(item, 3, progress_bar)
self.transfer_tree.addTopLevelItem(item)
# 更新传输速度
transfer['speed'] = speed
def connect_to_host(self):
"""连接选中的主机"""
selected = self.host_tree.selectedItems()
if not selected:
QMessageBox.warning(self, "警告", "请先选择一个主机")
return
ip = selected[0].text(0)
self.connect_to_ip(ip)
def manual_connect(self):
"""手动连接主机"""
ip, ok = QInputDialog.getText(self, "手动连接", "请输入目标IP地址:")
if ok and ip:
self.connect_to_ip(ip)
def connect_to_ip(self, ip):
"""连接到指定IP"""
if ip in self.connections:
QMessageBox.information(self, "信息", f"已经连接到 {ip}")
return
try:
# 先测量延迟
latency = self.measure_latency(ip)
if latency < 0:
QMessageBox.critical(self, "错误", f"无法连接到 {ip}")
return
# 建立连接
client_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client_socket.settimeout(5)
client_socket.connect((ip, 12345))
# 认证
auth_info = {
'hostname': socket.gethostname(),
'password': self.password
}
client_socket.sendall(pickle.dumps(auth_info))
response = client_socket.recv(1024)
if response == b"AUTH_OK":
self.connections[ip] = client_socket
self.update_connection_tree()
self.log_message(f"✅ 成功连接到 {ip} (延迟: {latency}ms)")
else:
client_socket.close()
QMessageBox.critical(self, "错误", "密码错误或连接被拒绝")
self.log_message(f"❌ 连接 {ip} 失败: 认证失败")
except socket.timeout:
QMessageBox.critical(self, "错误", f"连接 {ip} 超时")
self.log_message(f"⌛ 连接 {ip} 失败: 连接超时")
except Exception as e:
QMessageBox.critical(self, "错误", f"无法连接到 {ip}: {str(e)}")
self.log_message(f"❌ 连接 {ip} 失败: {str(e)}")
def rescan_network(self):
"""重新扫描网络"""
self.log_message(" 正在重新扫描网络...")
self.hosts = {}
self.update_host_tree()
self.scan_network()
def clear_host_list(self):
"""清除主机列表"""
self.hosts = {}
self.update_host_tree()
self.log_message(" 主机列表已清除")
def send_file(self):
"""发送文件"""
selected = self.conn_tree.selectedItems()
if not selected:
QMessageBox.warning(self, "警告", "请先选择一个连接")
return
file_path = self.file_path.text()
if not file_path:
QMessageBox.warning(self, "警告", "请先选择要发送的文件")
return
if not os.path.isfile(file_path):
QMessageBox.warning(self, "警告", "文件不存在")
return
password = self.file_password.text()
if not password:
QMessageBox.warning(self, "警告", "请输入密码")
return
ip = selected[0].text(0)
if ip not in self.connections:
QMessageBox.critical(self, "错误", "连接已断开")
return
# 在单独的线程中发送文件
threading.Thread(
target=self._send_file,
args=(ip, file_path, password),
daemon=True
).start()
def _send_file(self, ip, file_path, password):
"""实际发送文件的实现"""
file_id = str(int(time.time() * 1000))
transfer_id = f"{ip}_{file_id}"
try:
# 检查连接是否仍然有效
if ip not in self.connections:
self.log_message(f"❌ 连接已断开: {ip}")
return
sock = self.connections[ip]
# 获取文件信息
filename = os.path.basename(file_path)
filesize = os.path.getsize(file_path)
# 计算文件哈希
self.log_message(f" 正在计算文件哈希...")
hasher = hashlib.sha256()
with open(file_path, 'rb') as f:
while chunk := f.read(4096):
hasher.update(chunk)
file_hash = hasher.hexdigest()
# 准备文件信息
file_info = {
'file_id': file_id,
'filename': filename,
'filesize': filesize,
'hash': file_hash,
'password': password,
'chunk_size': 4096
}
# 发送文件信息
sock.sendall(pickle.dumps(file_info))
# 获取偏移量(用于断点续传)
offset_data = sock.recv(8)
if not offset_data or len(offset_data) != 8:
self.log_message(f"❌ 接收偏移量失败: {filename}")
return
offset = struct.unpack("!Q", offset_data)[0]
# 初始化传输记录
self.file_transfers[transfer_id] = {
'filename': filename,
'size': filesize,
'sent': offset,
'status': 'transferring',
'start_time': time.time(),
'last_update': time.time(),
'speed': 0
}
# 开始传输文件
with open(file_path, 'rb') as f:
f.seek(offset)
chunk_size = 4096
total_sent = offset
attempts = 0
last_active_time = time.time()
while total_sent < filesize:
# 检查传输状态
if self.file_transfers[transfer_id]['status'] == 'paused':
time.sleep(1)
continue
elif self.file_transfers[transfer_id]['status'] == 'canceled':
self.log_message(f" 传输已取消: {filename}")
return
# 检查连接是否超时
if time.time() - last_active_time > 30: # 30秒无活动
self.log_message(f"⚠️ 连接超时,尝试重新连接...")
try:
sock.shutdown(socket.SHUT_RDWR)
sock.close()
except:
pass
# 重新连接
try:
new_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
new_sock.settimeout(10)
new_sock.connect((ip, 12345))
# 重新认证
auth_info = {'hostname': socket.gethostname(), 'password': password}
new_sock.sendall(pickle.dumps(auth_info))
response = new_sock.recv(1024)
if response == b"AUTH_OK":
sock = new_sock
self.connections[ip] = sock
# 重新发送文件信息
sock.sendall(pickle.dumps(file_info))
offset_data = sock.recv(8)
offset = struct.unpack("!Q", offset_data)[0]
f.seek(offset)
total_sent = offset
last_active_time = time.time()
continue
except Exception as e:
self.log_message(f"❌ 重新连接失败: {str(e)}")
break
# 读取并发送数据块
chunk = f.read(chunk_size)
if not chunk:
break
try:
# 发送块长度
sock.sendall(struct.pack("!Q", len(chunk)))
# 发送实际数据
sock.sendall(chunk)
total_sent += len(chunk)
self.file_transfers[transfer_id]['sent'] = total_sent
attempts = 0
last_active_time = time.time()
# 更新传输速度
now = time.time()
time_elapsed = now - self.file_transfers[transfer_id]['last_update']
if time_elapsed > 0:
bytes_elapsed = total_sent - self.file_transfers[transfer_id]['sent']
self.file_transfers[transfer_id]['speed'] = bytes_elapsed / time_elapsed
self.file_transfers[transfer_id]['last_update'] = now
except Exception as e:
attempts += 1
if attempts > self.retry_attempts:
self.log_message(f"❌ 发送失败超过重试次数: {str(e)}")
self.file_transfers[transfer_id]['status'] = 'failed'
return
self.log_message(f"⚠️ 发送失败: {str(e)} (尝试 {attempts}/{self.retry_attempts})")
time.sleep(self.retry_delay)
f.seek(total_sent)
# 传输完成,等待确认
response = sock.recv(1024)
if response == b"FILE_OK":
# 验证文件哈希
sock.sendall(b"VERIFY_HASH")
local_hash = hashlib.sha256(open(file_path, 'rb').read()).hexdigest()
sock.sendall(local_hash.encode())
verify_response = sock.recv(1024)
if verify_response == b"HASH_MATCH":
self.log_message(f"✅ 文件校验成功: {filename}")
self.file_transfers[transfer_id]['status'] = 'completed'
else:
self.log_message(f"❌ 文件校验失败: {filename}")
self.file_transfers[transfer_id]['status'] = 'hash_failed'
else:
self.log_message(f"❌ 文件传输失败: {filename}")
self.file_transfers[transfer_id]['status'] = 'failed'
except Exception as e:
self.log_message(f"❌ 发送文件到 {ip} 时出错: {str(e)}")
if transfer_id in self.file_transfers:
self.file_transfers[transfer_id]['status'] = 'failed'
if ip in self.connections:
del self.connections[ip]
self.update_connection_tree()
def pause_transfer(self):
"""暂停传输"""
selected = self.transfer_tree.selectedItems()
if not selected:
return
for item in selected:
transfer_id = f"{item.text(2)}_{item.text(0)}"
if transfer_id in self.file_transfers and self.file_transfers[transfer_id]['status'] == 'transferring':
self.file_transfers[transfer_id]['status'] = 'paused'
self.log_message(f"⏸️ 已暂停传输: {item.text(1)}")
def resume_transfer(self):
"""继续传输"""
selected = self.transfer_tree.selectedItems()
if not selected:
return
for item in selected:
transfer_id = f"{item.text(2)}_{item.text(0)}"
if transfer_id in self.file_transfers and self.file_transfers[transfer_id]['status'] == 'paused':
self.file_transfers[transfer_id]['status'] = 'transferring'
self.log_message(f"▶️ 已继续传输: {item.text(1)}")
def cancel_transfer(self):
"""取消传输"""
selected = self.transfer_tree.selectedItems()
if not selected:
return
for item in selected:
transfer_id = f"{item.text(2)}_{item.text(0)}"
if transfer_id in self.file_transfers:
self.file_transfers[transfer_id]['status'] = 'canceled'
self.log_message(f" 已取消传输: {item.text(1)}")
def clear_completed(self):
"""清除已完成传输"""
to_remove = []
for transfer_id, transfer in self.file_transfers.items():
if transfer['status'] in ['completed', 'failed', 'canceled', 'hash_failed']:
to_remove.append(transfer_id)
for transfer_id in to_remove:
del self.file_transfers[transfer_id]
if to_remove:
self.log_message(f" 已清除 {len(to_remove)} 个传输记录")
def format_size(self, size):
"""格式化文件大小"""
for unit in ['B', 'KB', 'MB', 'GB']:
if size < 1024:
return f"{size:.1f} {unit}"
size /= 1024
return f"{size:.1f} TB"
def closeEvent(self, event):
"""关闭窗口事件处理"""
self.server_running = False
self.discovery_running = False
# 关闭所有连接
for sock in self.connections.values():
try:
sock.close()
except:
pass
# 保存配置
self.save_config()
# 停止所有定时器
self.cleanup_timer.stop()
self.status_timer.stop()
self.connection_timer.stop()
self.network_timer.stop()
event.accept()
if __name__ == "__main__":
app = QApplication(sys.argv)
# 设置应用程序样式
app.setStyle('Fusion')
# 设置应用程序图标
if hasattr(sys, '_MEIPASS'):
icon_path = os.path.join(sys._MEIPASS, 'icon.ico')
else:
icon_path = 'icon.ico' if os.path.exists('icon.ico') else None
if icon_path and os.path.exists(icon_path):
app.setWindowIcon(QIcon(icon_path))
# 创建并显示主窗口
window = FileSharingApp()
window.show()
# 运行应用程序
sys.exit(app.exec_())
版权声明:本文代码采用MIT开源协议,转载需注明作者及原文链接。
互动区:欢迎在评论区分享你的改进建议!(精选留言送CSDN周边礼品)