mirror of
https://github.com/duma520/Iperf_GUI.git
synced 2026-10-08 09:54:56 -08:00
1984 lines
81 KiB
Python
1984 lines
81 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
iperf3 图形化工具
|
||
完整支持所有 iperf3 选项
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import json
|
||
import subprocess
|
||
import threading
|
||
import time
|
||
import re
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
|
||
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
||
QHBoxLayout, QGridLayout, QGroupBox, QLabel,
|
||
QLineEdit, QComboBox, QPushButton, QTextEdit,
|
||
QCheckBox, QSpinBox, QDoubleSpinBox, QProgressBar,
|
||
QSplitter, QTabWidget, QMessageBox, QFileDialog,
|
||
QStatusBar, QSizePolicy, QHeaderView, QTableWidget,
|
||
QTableWidgetItem)
|
||
from PyQt5.QtCore import (Qt, QThread, pyqtSignal, QSettings, QTimer,
|
||
QSize, QRect, QMetaObject, Q_ARG, QByteArray)
|
||
from PyQt5.QtGui import QFont, QIcon, QTextCursor, QColor
|
||
|
||
# 配置文件路径
|
||
CONFIG_FILE = "iperf3_gui_config.json"
|
||
|
||
class Iperf3Worker(QThread):
|
||
"""iperf3工作线程"""
|
||
|
||
# 信号定义
|
||
output_received = pyqtSignal(str)
|
||
test_completed = pyqtSignal(dict)
|
||
progress_updated = pyqtSignal(int, str)
|
||
error_occurred = pyqtSignal(str)
|
||
|
||
def __init__(self, params):
|
||
super().__init__()
|
||
self.params = params
|
||
self.is_running = True
|
||
self.process = None
|
||
|
||
def run(self):
|
||
"""执行iperf3测试"""
|
||
try:
|
||
# 构建命令参数
|
||
cmd = ["iperf3"]
|
||
|
||
if self.params["mode"] == "server":
|
||
cmd.extend(["-s"])
|
||
if self.params["server_port"]:
|
||
cmd.extend(["-p", str(self.params["server_port"])])
|
||
if self.params["one_off"]:
|
||
cmd.append("-1")
|
||
if self.params["daemon"]:
|
||
cmd.append("-D")
|
||
if self.params["pid_file"]:
|
||
cmd.extend(["-I", self.params["pid_file"]])
|
||
if self.params["server_bitrate_limit"]:
|
||
cmd.extend(["--server-bitrate-limit", self.params["server_bitrate_limit"]])
|
||
if self.params["idle_timeout"]:
|
||
cmd.extend(["--idle-timeout", str(self.params["idle_timeout"])])
|
||
if self.params["server_max_duration"]:
|
||
cmd.extend(["--server-max-duration", str(self.params["server_max_duration"])])
|
||
if self.params["rsa_private_key_path"]:
|
||
cmd.extend(["--rsa-private-key-path", self.params["rsa_private_key_path"]])
|
||
if self.params["authorized_users_path"]:
|
||
cmd.extend(["--authorized-users-path", self.params["authorized_users_path"]])
|
||
if self.params["time_skew_threshold"]:
|
||
cmd.extend(["--time-skew-threshold", str(self.params["time_skew_threshold"])])
|
||
if self.params["use_pkcs1_padding"]:
|
||
cmd.append("--use-pkcs1-padding")
|
||
else: # client mode
|
||
cmd.extend(["-c", self.params["server_ip"]])
|
||
if self.params["server_port"]:
|
||
cmd.extend(["-p", str(self.params["server_port"])])
|
||
|
||
# 通用参数
|
||
if self.params["protocol"] == "udp":
|
||
cmd.append("-u")
|
||
if self.params["bandwidth"]:
|
||
cmd.extend(["-b", self.params["bandwidth"]])
|
||
if self.params["udp_counters_64bit"]:
|
||
cmd.append("--udp-counters-64bit")
|
||
if self.params["repeating_payload"]:
|
||
cmd.append("--repeating-payload")
|
||
|
||
# 连接选项
|
||
if self.params["connect_timeout"]:
|
||
cmd.extend(["--connect-timeout", str(self.params["connect_timeout"])])
|
||
if self.params["bind_dev"]:
|
||
cmd.extend(["--bind-dev", self.params["bind_dev"]])
|
||
if self.params["bind_host"]:
|
||
cmd.extend(["-B", self.params["bind_host"]])
|
||
|
||
# 测试参数
|
||
if self.params["reverse"]:
|
||
cmd.append("-R")
|
||
if self.params["bidir"]:
|
||
cmd.append("--bidir")
|
||
|
||
if self.params["time"]:
|
||
cmd.extend(["-t", str(self.params["time"])])
|
||
if self.params["bytes"]:
|
||
cmd.extend(["-n", self.params["bytes"]])
|
||
if self.params["blockcount"]:
|
||
cmd.extend(["-k", self.params["blockcount"]])
|
||
|
||
if self.params["parallel"]:
|
||
cmd.extend(["-P", str(self.params["parallel"])])
|
||
|
||
if self.params["window_size"]:
|
||
cmd.extend(["-w", self.params["window_size"]])
|
||
|
||
if self.params["mss"]:
|
||
cmd.extend(["-M", str(self.params["mss"])])
|
||
if self.params["no_delay"]:
|
||
cmd.append("-N")
|
||
|
||
if self.params["length"]:
|
||
cmd.extend(["-l", self.params["length"]])
|
||
|
||
if self.params["cport"]:
|
||
cmd.extend(["--cport", self.params["cport"]])
|
||
|
||
if self.params["interval"]:
|
||
cmd.extend(["-i", str(self.params["interval"])])
|
||
|
||
if self.params["format"]:
|
||
cmd.extend(["--format", self.params["format"]])
|
||
|
||
# 高级选项
|
||
if self.params["pacing_timer"]:
|
||
cmd.extend(["--pacing-timer", self.params["pacing_timer"]])
|
||
if self.params["omit"]:
|
||
cmd.extend(["-O", str(self.params["omit"])])
|
||
if self.params["title"]:
|
||
cmd.extend(["-T", self.params["title"]])
|
||
if self.params["extra_data"]:
|
||
cmd.extend(["--extra-data", self.params["extra_data"]])
|
||
if self.params["get_server_output"]:
|
||
cmd.append("--get-server-output")
|
||
if self.params["dont_fragment"]:
|
||
cmd.append("--dont-fragment")
|
||
|
||
# 网络选项
|
||
if self.params["ipv4_only"]:
|
||
cmd.append("-4")
|
||
if self.params["ipv6_only"]:
|
||
cmd.append("-6")
|
||
if self.params["tos"]:
|
||
cmd.extend(["-S", str(self.params["tos"])])
|
||
if self.params["dscp"]:
|
||
cmd.extend(["--dscp", str(self.params["dscp"])])
|
||
|
||
# 性能选项
|
||
if self.params["zerocopy"]:
|
||
cmd.append("-Z")
|
||
if self.params["skip_rx_copy"]:
|
||
cmd.append("--skip-rx-copy")
|
||
|
||
# 认证选项
|
||
if self.params["username"]:
|
||
cmd.extend(["--username", self.params["username"]])
|
||
if self.params["rsa_public_key_path"]:
|
||
cmd.extend(["--rsa-public-key-path", self.params["rsa_public_key_path"]])
|
||
|
||
# 文件传输
|
||
if self.params["file"]:
|
||
cmd.extend(["-F", self.params["file"]])
|
||
|
||
# CPU亲和性
|
||
if self.params["affinity"]:
|
||
cmd.extend(["-A", self.params["affinity"]])
|
||
|
||
# 输出选项
|
||
if self.params["json_output"]:
|
||
cmd.append("-J")
|
||
if self.params["json_stream"]:
|
||
cmd.append("--json-stream")
|
||
if self.params["json_stream_full_output"]:
|
||
cmd.append("--json-stream-full-output")
|
||
if self.params["logfile"]:
|
||
cmd.extend(["--logfile", self.params["logfile"]])
|
||
if self.params["forceflush"]:
|
||
cmd.append("--forceflush")
|
||
if self.params["timestamps"]:
|
||
if self.params["timestamps_format"]:
|
||
cmd.append(f"--timestamps={self.params['timestamps_format']}")
|
||
else:
|
||
cmd.append("--timestamps")
|
||
if self.params["rcv_timeout"]:
|
||
cmd.extend(["--rcv-timeout", str(self.params["rcv_timeout"])])
|
||
|
||
if self.params["verbose"]:
|
||
cmd.append("-V")
|
||
if self.params["debug"]:
|
||
if self.params["debug_level"]:
|
||
cmd.append(f"--debug={self.params['debug_level']}")
|
||
else:
|
||
cmd.append("-d")
|
||
if self.params["version"]:
|
||
cmd.append("-v")
|
||
|
||
self.output_received.emit(f"执行命令: {' '.join(cmd)}\n")
|
||
|
||
# 执行iperf3
|
||
self.process = subprocess.Popen(
|
||
cmd,
|
||
stdout=subprocess.PIPE,
|
||
stderr=subprocess.STDOUT,
|
||
text=True,
|
||
encoding='utf-8',
|
||
errors='ignore',
|
||
bufsize=1,
|
||
universal_newlines=True
|
||
)
|
||
|
||
# 读取输出
|
||
for line in iter(self.process.stdout.readline, ''):
|
||
if not self.is_running:
|
||
break
|
||
|
||
if line.strip():
|
||
self.output_received.emit(line)
|
||
|
||
# 解析进度信息
|
||
progress_info = self.parse_progress(line)
|
||
if progress_info:
|
||
self.progress_updated.emit(*progress_info)
|
||
|
||
# 等待进程结束
|
||
return_code = self.process.wait()
|
||
|
||
if return_code == 0:
|
||
self.test_completed.emit({"status": "success", "code": return_code})
|
||
else:
|
||
self.error_occurred.emit(f"测试失败,返回码: {return_code}")
|
||
|
||
except FileNotFoundError:
|
||
self.error_occurred.emit("未找到iperf3程序,请确保已安装iperf3")
|
||
except Exception as e:
|
||
self.error_occurred.emit(f"执行错误: {str(e)}")
|
||
|
||
def parse_progress(self, line):
|
||
"""从输出行解析进度信息"""
|
||
# 匹配iperf3进度输出(示例:[ 5] 0.00-1.00 sec 1.10 GBytes 9.42 Gbits/sec)
|
||
pattern = r'\[\s*\d+\]\s+[\d\.]+-([\d\.]+)\s+sec'
|
||
match = re.search(pattern, line)
|
||
if match:
|
||
current_time = float(match.group(1))
|
||
if self.params.get("time"):
|
||
total_time = self.params["time"]
|
||
progress = int((current_time / total_time) * 100)
|
||
return min(progress, 100), f"测试进度: {current_time}/{total_time}秒"
|
||
|
||
# 尝试匹配bytes传输进度
|
||
bytes_pattern = r'\[\s*\d+\]\s+[\d\.]+\s+Bytes\s+([\d\.]+)\s+[KMG]?bits/sec'
|
||
bytes_match = re.search(bytes_pattern, line)
|
||
if bytes_match:
|
||
return 50, "数据传输中..."
|
||
|
||
return None
|
||
|
||
def stop(self):
|
||
"""停止测试"""
|
||
self.is_running = False
|
||
if self.process:
|
||
self.process.terminate()
|
||
self.process.wait()
|
||
|
||
class SettingsManager:
|
||
"""设置管理器"""
|
||
|
||
def __init__(self, config_file=CONFIG_FILE):
|
||
self.config_file = config_file
|
||
self.settings = self.load_settings()
|
||
|
||
def load_settings(self):
|
||
"""加载设置"""
|
||
default_settings = {
|
||
# 基础设置
|
||
"server_ip": "127.0.0.1",
|
||
"server_port": "5201",
|
||
"protocol": 0,
|
||
"mode": 0,
|
||
"time": 10,
|
||
"parallel": 1,
|
||
"bandwidth": "",
|
||
"window_size": "",
|
||
"mss": 0,
|
||
"length": "",
|
||
"bytes": "",
|
||
"blockcount": "",
|
||
"interval": 1.0,
|
||
"format": 0,
|
||
|
||
# 高级选项
|
||
"reverse": False,
|
||
"bidir": False,
|
||
"json_output": False,
|
||
"verbose": False,
|
||
"debug": False,
|
||
"debug_level": "",
|
||
"version": False,
|
||
"one_off": False,
|
||
"daemon": False,
|
||
|
||
# 服务器选项
|
||
"server_bitrate_limit": "",
|
||
"idle_timeout": 0,
|
||
"server_max_duration": 0,
|
||
"rsa_private_key_path": "",
|
||
"authorized_users_path": "",
|
||
"time_skew_threshold": 0.0,
|
||
"use_pkcs1_padding": False,
|
||
|
||
# 客户端选项
|
||
"connect_timeout": 0,
|
||
"bind_dev": "",
|
||
"bind_host": "",
|
||
"cport": "",
|
||
"pacing_timer": "",
|
||
"omit": 0,
|
||
"title": "",
|
||
"extra_data": "",
|
||
"get_server_output": False,
|
||
"udp_counters_64bit": False,
|
||
"repeating_payload": False,
|
||
"dont_fragment": False,
|
||
|
||
# 网络选项
|
||
"ipv4_only": False,
|
||
"ipv6_only": False,
|
||
"tos": 0,
|
||
"dscp": 0,
|
||
"no_delay": False,
|
||
|
||
# 性能选项
|
||
"zerocopy": False,
|
||
"skip_rx_copy": False,
|
||
"rcv_timeout": 0,
|
||
|
||
# 认证选项
|
||
"username": "",
|
||
"rsa_public_key_path": "",
|
||
|
||
# 文件选项
|
||
"file": "",
|
||
"pid_file": "",
|
||
"logfile": "",
|
||
|
||
# 输出选项
|
||
"json_stream": False,
|
||
"json_stream_full_output": False,
|
||
"forceflush": False,
|
||
"timestamps": False,
|
||
"timestamps_format": "",
|
||
|
||
# 系统选项
|
||
"affinity": "",
|
||
|
||
# 窗口设置
|
||
"geometry": None,
|
||
"window_state": None
|
||
}
|
||
|
||
try:
|
||
if os.path.exists(self.config_file):
|
||
with open(self.config_file, 'r', encoding='utf-8') as f:
|
||
loaded = json.load(f)
|
||
# 合并设置,保留默认值用于新增字段
|
||
for key in default_settings:
|
||
if key in loaded:
|
||
# 处理不同类型的值
|
||
if loaded[key] is None:
|
||
continue
|
||
elif isinstance(loaded[key], (int, float, bool, str)):
|
||
# 对于数值字段,确保是正确类型
|
||
if key in ["connect_timeout", "idle_timeout", "server_max_duration",
|
||
"omit", "tos", "dscp", "rcv_timeout"]:
|
||
if isinstance(loaded[key], str):
|
||
if loaded[key].strip() == "":
|
||
default_settings[key] = 0
|
||
else:
|
||
try:
|
||
default_settings[key] = int(loaded[key])
|
||
except:
|
||
default_settings[key] = 0
|
||
else:
|
||
default_settings[key] = int(loaded[key])
|
||
elif key in ["time_skew_threshold", "interval"]:
|
||
if isinstance(loaded[key], str):
|
||
if loaded[key].strip() == "":
|
||
default_settings[key] = 0.0 if key == "time_skew_threshold" else 1.0
|
||
else:
|
||
try:
|
||
default_settings[key] = float(loaded[key])
|
||
except:
|
||
default_settings[key] = 0.0 if key == "time_skew_threshold" else 1.0
|
||
else:
|
||
default_settings[key] = float(loaded[key])
|
||
elif key in ["time", "parallel", "mss"]:
|
||
if isinstance(loaded[key], str):
|
||
if loaded[key].strip() == "":
|
||
default_settings[key] = 10 if key == "time" else (1 if key == "parallel" else 0)
|
||
else:
|
||
try:
|
||
default_settings[key] = int(loaded[key])
|
||
except:
|
||
default_settings[key] = 10 if key == "time" else (1 if key == "parallel" else 0)
|
||
else:
|
||
default_settings[key] = int(loaded[key])
|
||
elif key in ["protocol", "mode", "format"]:
|
||
if isinstance(loaded[key], str):
|
||
# 兼容旧版本字符串格式
|
||
if key == "protocol":
|
||
if loaded[key].lower() == "tcp":
|
||
default_settings[key] = 0
|
||
elif loaded[key].lower() == "udp":
|
||
default_settings[key] = 1
|
||
else:
|
||
default_settings[key] = 0
|
||
elif key == "mode":
|
||
if loaded[key].lower() == "client":
|
||
default_settings[key] = 0
|
||
elif loaded[key].lower() == "server":
|
||
default_settings[key] = 1
|
||
else:
|
||
default_settings[key] = 0
|
||
elif key == "format":
|
||
format_map = {"": 0, "k": 0, "m": 1, "g": 2, "t": 3}
|
||
default_settings[key] = format_map.get(loaded[key].lower(), 0)
|
||
else:
|
||
default_settings[key] = int(loaded[key])
|
||
else:
|
||
default_settings[key] = loaded[key]
|
||
except json.JSONDecodeError:
|
||
print(f"JSON配置文件损坏,使用默认设置")
|
||
try:
|
||
os.remove(self.config_file)
|
||
except:
|
||
pass
|
||
except Exception as e:
|
||
print(f"加载设置失败: {e}")
|
||
|
||
return default_settings
|
||
|
||
def save_settings(self, settings):
|
||
"""保存设置"""
|
||
try:
|
||
# 处理window_state,如果是QByteArray则转换为十六进制字符串
|
||
settings_to_save = settings.copy()
|
||
if isinstance(settings_to_save.get("window_state"), QByteArray):
|
||
settings_to_save["window_state"] = settings_to_save["window_state"].toHex().data().decode()
|
||
|
||
with open(self.config_file, 'w', encoding='utf-8') as f:
|
||
json.dump(settings_to_save, f, indent=2, ensure_ascii=False)
|
||
return True
|
||
except Exception as e:
|
||
print(f"保存设置失败: {e}")
|
||
return False
|
||
|
||
def get(self, key, default=None):
|
||
"""获取设置值"""
|
||
return self.settings.get(key, default)
|
||
|
||
def set(self, key, value):
|
||
"""设置值"""
|
||
self.settings[key] = value
|
||
|
||
def save_all(self):
|
||
"""保存所有设置"""
|
||
return self.save_settings(self.settings)
|
||
|
||
class Iperf3GUI(QMainWindow):
|
||
"""主窗口类"""
|
||
|
||
def __init__(self):
|
||
super().__init__()
|
||
self.settings_manager = SettingsManager()
|
||
self.worker = None
|
||
self.init_ui()
|
||
self.load_settings()
|
||
self.setup_connections()
|
||
|
||
def init_ui(self):
|
||
"""初始化界面"""
|
||
# 设置窗口属性
|
||
self.setWindowTitle("iperf3 网络性能测试工具 - 完整版")
|
||
self.setMinimumSize(1200, 800)
|
||
|
||
# 尝试设置图标
|
||
icon_path = "icon.ico"
|
||
if os.path.exists(icon_path):
|
||
self.setWindowIcon(QIcon(icon_path))
|
||
|
||
# 创建中心部件
|
||
central_widget = QWidget()
|
||
self.setCentralWidget(central_widget)
|
||
|
||
# 主布局
|
||
main_layout = QVBoxLayout(central_widget)
|
||
main_layout.setContentsMargins(10, 10, 10, 10)
|
||
main_layout.setSpacing(10)
|
||
|
||
# 创建标签页
|
||
self.tab_widget = QTabWidget()
|
||
main_layout.addWidget(self.tab_widget)
|
||
|
||
# 创建测试配置标签页
|
||
self.create_test_tab()
|
||
|
||
# 创建高级选项标签页
|
||
self.create_advanced_tab()
|
||
|
||
# 创建服务器选项标签页
|
||
self.create_server_tab()
|
||
|
||
# 创建结果标签页
|
||
self.create_result_tab()
|
||
|
||
# 创建历史记录标签页
|
||
self.create_history_tab()
|
||
|
||
# 创建底部控制区域
|
||
self.create_bottom_controls(main_layout)
|
||
|
||
# 创建状态栏
|
||
self.status_bar = QStatusBar()
|
||
self.setStatusBar(self.status_bar)
|
||
self.status_bar.showMessage("就绪")
|
||
|
||
# 进度条
|
||
self.progress_bar = QProgressBar()
|
||
self.status_bar.addPermanentWidget(self.progress_bar)
|
||
self.progress_bar.hide()
|
||
|
||
# 应用样式
|
||
self.apply_styles()
|
||
|
||
def create_test_tab(self):
|
||
"""创建测试配置标签页"""
|
||
test_tab = QWidget()
|
||
self.tab_widget.addTab(test_tab, "基础配置")
|
||
|
||
layout = QVBoxLayout(test_tab)
|
||
layout.setSpacing(15)
|
||
|
||
# 基础设置组
|
||
basic_group = QGroupBox("基础设置")
|
||
basic_layout = QGridLayout()
|
||
basic_layout.setSpacing(10)
|
||
|
||
# 测试模式
|
||
basic_layout.addWidget(QLabel("测试模式:"), 0, 0)
|
||
self.mode_combo = QComboBox()
|
||
self.mode_combo.addItems(["客户端", "服务器"])
|
||
basic_layout.addWidget(self.mode_combo, 0, 1)
|
||
|
||
# 服务器地址
|
||
basic_layout.addWidget(QLabel("服务器地址:"), 0, 2)
|
||
self.server_ip_edit = QLineEdit()
|
||
self.server_ip_edit.setPlaceholderText("仅在客户端模式下需要")
|
||
basic_layout.addWidget(self.server_ip_edit, 0, 3)
|
||
|
||
# 端口
|
||
basic_layout.addWidget(QLabel("端口:"), 1, 0)
|
||
self.port_edit = QLineEdit()
|
||
self.port_edit.setText("5201")
|
||
basic_layout.addWidget(self.port_edit, 1, 1)
|
||
|
||
# 协议
|
||
basic_layout.addWidget(QLabel("协议:"), 1, 2)
|
||
self.protocol_combo = QComboBox()
|
||
self.protocol_combo.addItems(["TCP", "UDP"])
|
||
basic_layout.addWidget(self.protocol_combo, 1, 3)
|
||
|
||
# 测试时间
|
||
basic_layout.addWidget(QLabel("测试时间(秒):"), 2, 0)
|
||
self.time_spin = QSpinBox()
|
||
self.time_spin.setRange(1, 3600)
|
||
self.time_spin.setValue(10)
|
||
self.time_spin.setSuffix(" 秒")
|
||
basic_layout.addWidget(self.time_spin, 2, 1)
|
||
|
||
# 并行流
|
||
basic_layout.addWidget(QLabel("并行流:"), 2, 2)
|
||
self.parallel_spin = QSpinBox()
|
||
self.parallel_spin.setRange(1, 128)
|
||
self.parallel_spin.setValue(1)
|
||
basic_layout.addWidget(self.parallel_spin, 2, 3)
|
||
|
||
# 带宽限制
|
||
basic_layout.addWidget(QLabel("带宽限制:"), 3, 0)
|
||
self.bandwidth_combo = QComboBox()
|
||
self.bandwidth_combo.addItems(["", "1M", "10M", "100M", "1G", "10G", "100G"])
|
||
self.bandwidth_combo.setEditable(True)
|
||
self.bandwidth_combo.setPlaceholderText("例如: 100M, 1G")
|
||
basic_layout.addWidget(self.bandwidth_combo, 3, 1)
|
||
|
||
# 窗口大小
|
||
basic_layout.addWidget(QLabel("窗口大小:"), 3, 2)
|
||
self.window_combo = QComboBox()
|
||
self.window_combo.addItems(["", "32K", "64K", "128K", "256K", "512K", "1M", "2M", "4M", "8M"])
|
||
self.window_combo.setEditable(True)
|
||
self.window_combo.setPlaceholderText("例如: 64K, 1M")
|
||
basic_layout.addWidget(self.window_combo, 3, 3)
|
||
|
||
basic_group.setLayout(basic_layout)
|
||
layout.addWidget(basic_group)
|
||
|
||
# 传输设置组
|
||
transfer_group = QGroupBox("传输设置")
|
||
transfer_layout = QGridLayout()
|
||
transfer_layout.setSpacing(10)
|
||
|
||
# 传输数据量
|
||
transfer_layout.addWidget(QLabel("传输数据量:"), 0, 0)
|
||
self.data_amount_edit = QLineEdit()
|
||
self.data_amount_edit.setPlaceholderText("例如: 100M, 1G")
|
||
transfer_layout.addWidget(self.data_amount_edit, 0, 1)
|
||
|
||
# 块数量
|
||
transfer_layout.addWidget(QLabel("块数量:"), 0, 2)
|
||
self.blockcount_edit = QLineEdit()
|
||
self.blockcount_edit.setPlaceholderText("例如: 1000, 10000")
|
||
transfer_layout.addWidget(self.blockcount_edit, 0, 3)
|
||
|
||
# 缓冲区长度
|
||
transfer_layout.addWidget(QLabel("缓冲区长度:"), 1, 0)
|
||
self.length_combo = QComboBox()
|
||
self.length_combo.addItems(["", "64K", "128K", "256K", "512K", "1M"])
|
||
self.length_combo.setEditable(True)
|
||
self.length_combo.setPlaceholderText("例如: 128K")
|
||
transfer_layout.addWidget(self.length_combo, 1, 1)
|
||
|
||
# 报告间隔
|
||
transfer_layout.addWidget(QLabel("报告间隔(秒):"), 1, 2)
|
||
self.interval_spin = QDoubleSpinBox()
|
||
self.interval_spin.setRange(0.1, 10.0)
|
||
self.interval_spin.setSingleStep(0.1)
|
||
self.interval_spin.setValue(1.0)
|
||
self.interval_spin.setSuffix(" 秒")
|
||
transfer_layout.addWidget(self.interval_spin, 1, 3)
|
||
|
||
# 单位格式
|
||
transfer_layout.addWidget(QLabel("单位格式:"), 2, 0)
|
||
self.format_combo = QComboBox()
|
||
self.format_combo.addItems(["自动", "K", "M", "G", "T"])
|
||
transfer_layout.addWidget(self.format_combo, 2, 1)
|
||
|
||
# MSS
|
||
transfer_layout.addWidget(QLabel("MSS(最大段大小):"), 2, 2)
|
||
self.mss_spin = QSpinBox()
|
||
self.mss_spin.setRange(0, 9000)
|
||
self.mss_spin.setSpecialValueText("默认")
|
||
self.mss_spin.setSuffix(" 字节")
|
||
transfer_layout.addWidget(self.mss_spin, 2, 3)
|
||
|
||
transfer_group.setLayout(transfer_layout)
|
||
layout.addWidget(transfer_group)
|
||
|
||
# 占位弹簧
|
||
layout.addStretch()
|
||
|
||
def create_advanced_tab(self):
|
||
"""创建高级选项标签页"""
|
||
advanced_tab = QWidget()
|
||
self.tab_widget.addTab(advanced_tab, "高级选项")
|
||
|
||
layout = QVBoxLayout(advanced_tab)
|
||
layout.setSpacing(15)
|
||
|
||
# 客户端高级选项组
|
||
client_group = QGroupBox("客户端高级选项")
|
||
client_layout = QGridLayout()
|
||
client_layout.setSpacing(10)
|
||
|
||
# 连接超时
|
||
client_layout.addWidget(QLabel("连接超时(ms):"), 0, 0)
|
||
self.connect_timeout_spin = QSpinBox()
|
||
self.connect_timeout_spin.setRange(0, 300000)
|
||
self.connect_timeout_spin.setSpecialValueText("默认")
|
||
self.connect_timeout_spin.setSuffix(" ms")
|
||
client_layout.addWidget(self.connect_timeout_spin, 0, 1)
|
||
|
||
# 绑定设备
|
||
client_layout.addWidget(QLabel("绑定设备:"), 0, 2)
|
||
self.bind_dev_edit = QLineEdit()
|
||
self.bind_dev_edit.setPlaceholderText("例如: eth0, wlan0")
|
||
client_layout.addWidget(self.bind_dev_edit, 0, 3)
|
||
|
||
# 绑定主机
|
||
client_layout.addWidget(QLabel("绑定主机:"), 1, 0)
|
||
self.bind_host_edit = QLineEdit()
|
||
self.bind_host_edit.setPlaceholderText("IP地址或主机名")
|
||
client_layout.addWidget(self.bind_host_edit, 1, 1)
|
||
|
||
# 客户端端口
|
||
client_layout.addWidget(QLabel("客户端端口:"), 1, 2)
|
||
self.cport_edit = QLineEdit()
|
||
self.cport_edit.setPlaceholderText("指定客户端端口")
|
||
client_layout.addWidget(self.cport_edit, 1, 3)
|
||
|
||
# 省略时间
|
||
client_layout.addWidget(QLabel("省略时间(秒):"), 2, 0)
|
||
self.omit_spin = QSpinBox()
|
||
self.omit_spin.setRange(0, 100)
|
||
self.omit_spin.setSpecialValueText("不省略")
|
||
self.omit_spin.setSuffix(" 秒")
|
||
client_layout.addWidget(self.omit_spin, 2, 1)
|
||
|
||
# 标题
|
||
client_layout.addWidget(QLabel("输出标题:"), 2, 2)
|
||
self.title_edit = QLineEdit()
|
||
self.title_edit.setPlaceholderText("输出行前缀")
|
||
client_layout.addWidget(self.title_edit, 2, 3)
|
||
|
||
# 额外数据
|
||
client_layout.addWidget(QLabel("额外数据:"), 3, 0)
|
||
self.extra_data_edit = QLineEdit()
|
||
self.extra_data_edit.setPlaceholderText("包含在JSON中的数据")
|
||
client_layout.addWidget(self.extra_data_edit, 3, 1)
|
||
|
||
client_group.setLayout(client_layout)
|
||
layout.addWidget(client_group)
|
||
|
||
# 网络选项组
|
||
network_group = QGroupBox("网络选项")
|
||
network_layout = QGridLayout()
|
||
network_layout.setSpacing(10)
|
||
|
||
# IP版本
|
||
network_layout.addWidget(QLabel("IP版本:"), 0, 0)
|
||
self.ip_version_combo = QComboBox()
|
||
self.ip_version_combo.addItems(["自动", "仅IPv4", "仅IPv6"])
|
||
network_layout.addWidget(self.ip_version_combo, 0, 1)
|
||
|
||
# TOS
|
||
network_layout.addWidget(QLabel("TOS(Type of Service):"), 0, 2)
|
||
self.tos_spin = QSpinBox()
|
||
self.tos_spin.setRange(0, 255)
|
||
self.tos_spin.setSpecialValueText("默认")
|
||
network_layout.addWidget(self.tos_spin, 0, 3)
|
||
|
||
# DSCP
|
||
network_layout.addWidget(QLabel("DSCP:"), 1, 0)
|
||
self.dscp_spin = QSpinBox()
|
||
self.dscp_spin.setRange(0, 63)
|
||
self.dscp_spin.setSpecialValueText("默认")
|
||
network_layout.addWidget(self.dscp_spin, 1, 1)
|
||
|
||
network_group.setLayout(network_layout)
|
||
layout.addWidget(network_group)
|
||
|
||
# 性能选项组
|
||
perf_group = QGroupBox("性能选项")
|
||
perf_layout = QGridLayout()
|
||
perf_layout.setSpacing(10)
|
||
|
||
# 定时器
|
||
perf_layout.addWidget(QLabel("定时器(μs):"), 0, 0)
|
||
self.pacing_timer_edit = QLineEdit()
|
||
self.pacing_timer_edit.setPlaceholderText("例如: 1000")
|
||
perf_layout.addWidget(self.pacing_timer_edit, 0, 1)
|
||
|
||
# 接收超时
|
||
perf_layout.addWidget(QLabel("接收超时(ms):"), 0, 2)
|
||
self.rcv_timeout_spin = QSpinBox()
|
||
self.rcv_timeout_spin.setRange(0, 300000)
|
||
self.rcv_timeout_spin.setSpecialValueText("默认")
|
||
self.rcv_timeout_spin.setSuffix(" ms")
|
||
perf_layout.addWidget(self.rcv_timeout_spin, 0, 3)
|
||
|
||
perf_group.setLayout(perf_layout)
|
||
layout.addWidget(perf_group)
|
||
|
||
# 选项组
|
||
options_group = QGroupBox("选项")
|
||
options_layout = QGridLayout()
|
||
options_layout.setSpacing(10)
|
||
|
||
# 第一行复选框
|
||
self.reverse_check = QCheckBox("反向测试")
|
||
options_layout.addWidget(self.reverse_check, 0, 0)
|
||
|
||
self.bidir_check = QCheckBox("双向测试")
|
||
options_layout.addWidget(self.bidir_check, 0, 1)
|
||
|
||
self.no_delay_check = QCheckBox("禁用Nagle算法")
|
||
options_layout.addWidget(self.no_delay_check, 0, 2)
|
||
|
||
self.dont_fragment_check = QCheckBox("不分片(DF标志)")
|
||
options_layout.addWidget(self.dont_fragment_check, 0, 3)
|
||
|
||
# 第二行复选框
|
||
self.zerocopy_check = QCheckBox("零拷贝")
|
||
options_layout.addWidget(self.zerocopy_check, 1, 0)
|
||
|
||
self.skip_rx_copy_check = QCheckBox("跳过接收拷贝")
|
||
options_layout.addWidget(self.skip_rx_copy_check, 1, 1)
|
||
|
||
self.udp_counters_64bit_check = QCheckBox("UDP 64位计数器")
|
||
options_layout.addWidget(self.udp_counters_64bit_check, 1, 2)
|
||
|
||
self.repeating_payload_check = QCheckBox("重复负载")
|
||
options_layout.addWidget(self.repeating_payload_check, 1, 3)
|
||
|
||
# 第三行复选框
|
||
self.get_server_output_check = QCheckBox("获取服务器输出")
|
||
options_layout.addWidget(self.get_server_output_check, 2, 0)
|
||
|
||
self.one_off_check = QCheckBox("单次连接")
|
||
options_layout.addWidget(self.one_off_check, 2, 1)
|
||
|
||
self.daemon_check = QCheckBox("守护进程模式")
|
||
options_layout.addWidget(self.daemon_check, 2, 2)
|
||
|
||
self.use_pkcs1_padding_check = QCheckBox("使用PKCS1填充")
|
||
options_layout.addWidget(self.use_pkcs1_padding_check, 2, 3)
|
||
|
||
options_group.setLayout(options_layout)
|
||
layout.addWidget(options_group)
|
||
|
||
# 占位弹簧
|
||
layout.addStretch()
|
||
|
||
def create_server_tab(self):
|
||
"""创建服务器选项标签页"""
|
||
server_tab = QWidget()
|
||
self.tab_widget.addTab(server_tab, "服务器选项")
|
||
|
||
layout = QVBoxLayout(server_tab)
|
||
layout.setSpacing(15)
|
||
|
||
# 服务器限制组
|
||
limits_group = QGroupBox("服务器限制")
|
||
limits_layout = QGridLayout()
|
||
limits_layout.setSpacing(10)
|
||
|
||
# 服务器比特率限制
|
||
limits_layout.addWidget(QLabel("服务器比特率限制:"), 0, 0)
|
||
self.server_bitrate_limit_edit = QLineEdit()
|
||
self.server_bitrate_limit_edit.setPlaceholderText("例如: 100M, 1G")
|
||
limits_layout.addWidget(self.server_bitrate_limit_edit, 0, 1)
|
||
|
||
# 空闲超时
|
||
limits_layout.addWidget(QLabel("空闲超时(秒):"), 0, 2)
|
||
self.idle_timeout_spin = QSpinBox()
|
||
self.idle_timeout_spin.setRange(0, 3600)
|
||
self.idle_timeout_spin.setSpecialValueText("无限制")
|
||
self.idle_timeout_spin.setSuffix(" 秒")
|
||
limits_layout.addWidget(self.idle_timeout_spin, 0, 3)
|
||
|
||
# 服务器最大持续时间
|
||
limits_layout.addWidget(QLabel("服务器最大持续时间(秒):"), 1, 0)
|
||
self.server_max_duration_spin = QSpinBox()
|
||
self.server_max_duration_spin.setRange(0, 86400)
|
||
self.server_max_duration_spin.setSpecialValueText("无限制")
|
||
self.server_max_duration_spin.setSuffix(" 秒")
|
||
limits_layout.addWidget(self.server_max_duration_spin, 1, 1)
|
||
|
||
# 时间偏差阈值
|
||
limits_layout.addWidget(QLabel("时间偏差阈值(秒):"), 1, 2)
|
||
self.time_skew_threshold_spin = QDoubleSpinBox()
|
||
self.time_skew_threshold_spin.setRange(0.0, 100.0)
|
||
self.time_skew_threshold_spin.setSpecialValueText("默认")
|
||
self.time_skew_threshold_spin.setSuffix(" 秒")
|
||
limits_layout.addWidget(self.time_skew_threshold_spin, 1, 3)
|
||
|
||
limits_group.setLayout(limits_layout)
|
||
layout.addWidget(limits_group)
|
||
|
||
# 认证选项组
|
||
auth_group = QGroupBox("认证选项")
|
||
auth_layout = QGridLayout()
|
||
auth_layout.setSpacing(10)
|
||
|
||
# 用户名
|
||
auth_layout.addWidget(QLabel("用户名:"), 0, 0)
|
||
self.username_edit = QLineEdit()
|
||
auth_layout.addWidget(self.username_edit, 0, 1)
|
||
|
||
# RSA私钥路径
|
||
auth_layout.addWidget(QLabel("RSA私钥路径:"), 0, 2)
|
||
self.rsa_private_key_path_edit = QLineEdit()
|
||
self.rsa_private_key_path_edit.setPlaceholderText("服务器私钥文件路径")
|
||
auth_layout.addWidget(self.rsa_private_key_path_edit, 0, 3)
|
||
|
||
# RSA公钥路径
|
||
auth_layout.addWidget(QLabel("RSA公钥路径:"), 1, 0)
|
||
self.rsa_public_key_path_edit = QLineEdit()
|
||
self.rsa_public_key_path_edit.setPlaceholderText("客户端公钥文件路径")
|
||
auth_layout.addWidget(self.rsa_public_key_path_edit, 1, 1)
|
||
|
||
# 授权用户路径
|
||
auth_layout.addWidget(QLabel("授权用户路径:"), 1, 2)
|
||
self.authorized_users_path_edit = QLineEdit()
|
||
self.authorized_users_path_edit.setPlaceholderText("用户配置文件路径")
|
||
auth_layout.addWidget(self.authorized_users_path_edit, 1, 3)
|
||
|
||
auth_group.setLayout(auth_layout)
|
||
layout.addWidget(auth_group)
|
||
|
||
# 文件选项组
|
||
file_group = QGroupBox("文件选项")
|
||
file_layout = QGridLayout()
|
||
file_layout.setSpacing(10)
|
||
|
||
# 文件传输
|
||
file_layout.addWidget(QLabel("文件传输:"), 0, 0)
|
||
self.file_edit = QLineEdit()
|
||
file_button = QPushButton("浏览...")
|
||
file_button.clicked.connect(self.browse_file)
|
||
file_layout.addWidget(self.file_edit, 0, 1)
|
||
file_layout.addWidget(file_button, 0, 2)
|
||
|
||
# PID文件
|
||
file_layout.addWidget(QLabel("PID文件:"), 1, 0)
|
||
self.pid_file_edit = QLineEdit()
|
||
pid_file_button = QPushButton("浏览...")
|
||
pid_file_button.clicked.connect(self.browse_pid_file)
|
||
file_layout.addWidget(self.pid_file_edit, 1, 1)
|
||
file_layout.addWidget(pid_file_button, 1, 2)
|
||
|
||
# 日志文件
|
||
file_layout.addWidget(QLabel("日志文件:"), 2, 0)
|
||
self.logfile_edit = QLineEdit()
|
||
logfile_button = QPushButton("浏览...")
|
||
logfile_button.clicked.connect(self.browse_logfile)
|
||
file_layout.addWidget(self.logfile_edit, 2, 1)
|
||
file_layout.addWidget(logfile_button, 2, 2)
|
||
|
||
file_group.setLayout(file_layout)
|
||
layout.addWidget(file_group)
|
||
|
||
# 系统选项组
|
||
system_group = QGroupBox("系统选项")
|
||
system_layout = QGridLayout()
|
||
system_layout.setSpacing(10)
|
||
|
||
# CPU亲和性
|
||
system_layout.addWidget(QLabel("CPU亲和性:"), 0, 0)
|
||
self.affinity_edit = QLineEdit()
|
||
self.affinity_edit.setPlaceholderText("例如: 0 或 0,1")
|
||
system_layout.addWidget(self.affinity_edit, 0, 1)
|
||
|
||
# 时间戳格式
|
||
system_layout.addWidget(QLabel("时间戳格式:"), 0, 2)
|
||
self.timestamps_format_edit = QLineEdit()
|
||
self.timestamps_format_edit.setPlaceholderText("strftime格式,例如: %H:%M:%S")
|
||
system_layout.addWidget(self.timestamps_format_edit, 0, 3)
|
||
|
||
system_group.setLayout(system_layout)
|
||
layout.addWidget(system_group)
|
||
|
||
# 输出选项组
|
||
output_group = QGroupBox("输出选项")
|
||
output_layout = QGridLayout()
|
||
output_layout.setSpacing(10)
|
||
|
||
# 复选框
|
||
self.json_check = QCheckBox("JSON输出")
|
||
output_layout.addWidget(self.json_check, 0, 0)
|
||
|
||
self.json_stream_check = QCheckBox("JSON流输出")
|
||
output_layout.addWidget(self.json_stream_check, 0, 1)
|
||
|
||
self.json_stream_full_output_check = QCheckBox("完整JSON流输出")
|
||
output_layout.addWidget(self.json_stream_full_output_check, 0, 2)
|
||
|
||
self.verbose_check = QCheckBox("详细输出")
|
||
output_layout.addWidget(self.verbose_check, 0, 3)
|
||
|
||
self.version_check = QCheckBox("显示版本")
|
||
output_layout.addWidget(self.version_check, 1, 0)
|
||
|
||
self.debug_check = QCheckBox("调试模式")
|
||
output_layout.addWidget(self.debug_check, 1, 1)
|
||
|
||
self.forceflush_check = QCheckBox("强制刷新输出")
|
||
output_layout.addWidget(self.forceflush_check, 1, 2)
|
||
|
||
self.timestamps_check = QCheckBox("时间戳")
|
||
output_layout.addWidget(self.timestamps_check, 1, 3)
|
||
|
||
# 调试级别
|
||
output_layout.addWidget(QLabel("调试级别:"), 2, 0)
|
||
self.debug_level_combo = QComboBox()
|
||
self.debug_level_combo.addItems(["1", "2", "3", "4"])
|
||
self.debug_level_combo.setCurrentIndex(3)
|
||
output_layout.addWidget(self.debug_level_combo, 2, 1)
|
||
|
||
output_group.setLayout(output_layout)
|
||
layout.addWidget(output_group)
|
||
|
||
# 占位弹簧
|
||
layout.addStretch()
|
||
|
||
def create_result_tab(self):
|
||
"""创建结果标签页"""
|
||
result_tab = QWidget()
|
||
self.tab_widget.addTab(result_tab, "测试结果")
|
||
|
||
layout = QVBoxLayout(result_tab)
|
||
|
||
# 结果文本区域
|
||
self.result_text = QTextEdit()
|
||
self.result_text.setReadOnly(True)
|
||
self.result_text.setFont(QFont("Consolas", 10))
|
||
layout.addWidget(self.result_text)
|
||
|
||
# 结果统计区域
|
||
stats_group = QGroupBox("统计信息")
|
||
stats_layout = QGridLayout()
|
||
|
||
self.stats_labels = {}
|
||
stats_fields = [
|
||
("带宽:", "bandwidth"),
|
||
("抖动:", "jitter"),
|
||
("丢包:", "loss"),
|
||
("时间:", "duration"),
|
||
("数据量:", "bytes"),
|
||
("包数量:", "packets"),
|
||
("重传:", "retransmits"),
|
||
("发送者:", "sender"),
|
||
("接收者:", "receiver")
|
||
]
|
||
|
||
for i, (label_text, key) in enumerate(stats_fields):
|
||
row = i // 3
|
||
col = (i % 3) * 2
|
||
stats_layout.addWidget(QLabel(label_text), row, col)
|
||
value_label = QLabel("--")
|
||
value_label.setStyleSheet("font-weight: bold; color: #2c3e50;")
|
||
stats_layout.addWidget(value_label, row, col + 1)
|
||
self.stats_labels[key] = value_label
|
||
|
||
stats_group.setLayout(stats_layout)
|
||
layout.addWidget(stats_group)
|
||
|
||
def create_history_tab(self):
|
||
"""创建历史记录标签页"""
|
||
history_tab = QWidget()
|
||
self.tab_widget.addTab(history_tab, "历史记录")
|
||
|
||
layout = QVBoxLayout(history_tab)
|
||
|
||
# 历史记录表格
|
||
self.history_table = QTableWidget()
|
||
self.history_table.setColumnCount(10)
|
||
self.history_table.setHorizontalHeaderLabels([
|
||
"时间", "模式", "协议", "服务器", "带宽", "抖动", "丢包", "持续时间", "并行流", "备注"
|
||
])
|
||
self.history_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||
layout.addWidget(self.history_table)
|
||
|
||
# 操作按钮
|
||
button_layout = QHBoxLayout()
|
||
|
||
self.clear_history_btn = QPushButton("清空历史")
|
||
self.export_history_btn = QPushButton("导出记录")
|
||
self.load_history_btn = QPushButton("加载历史")
|
||
self.save_result_btn = QPushButton("保存结果")
|
||
|
||
button_layout.addWidget(self.clear_history_btn)
|
||
button_layout.addWidget(self.export_history_btn)
|
||
button_layout.addWidget(self.load_history_btn)
|
||
button_layout.addWidget(self.save_result_btn)
|
||
button_layout.addStretch()
|
||
|
||
layout.addLayout(button_layout)
|
||
|
||
def create_bottom_controls(self, parent_layout):
|
||
"""创建底部控制区域"""
|
||
control_layout = QHBoxLayout()
|
||
|
||
# 开始/停止按钮
|
||
self.start_btn = QPushButton("开始测试")
|
||
self.start_btn.setMinimumHeight(40)
|
||
self.start_btn.setStyleSheet("""
|
||
QPushButton {
|
||
background-color: #27ae60;
|
||
color: white;
|
||
font-weight: bold;
|
||
border-radius: 5px;
|
||
padding: 10px;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #219653;
|
||
}
|
||
QPushButton:disabled {
|
||
background-color: #95a5a6;
|
||
}
|
||
""")
|
||
|
||
self.stop_btn = QPushButton("停止测试")
|
||
self.stop_btn.setMinimumHeight(40)
|
||
self.stop_btn.setEnabled(False)
|
||
self.stop_btn.setStyleSheet("""
|
||
QPushButton {
|
||
background-color: #e74c3c;
|
||
color: white;
|
||
font-weight: bold;
|
||
border-radius: 5px;
|
||
padding: 10px;
|
||
}
|
||
QPushButton:hover {
|
||
background-color: #c0392b;
|
||
}
|
||
QPushButton:disabled {
|
||
background-color: #95a5a6;
|
||
}
|
||
""")
|
||
|
||
# 保存/加载按钮
|
||
self.save_btn = QPushButton("保存配置")
|
||
self.load_btn = QPushButton("加载配置")
|
||
self.reset_btn = QPushButton("重置配置")
|
||
self.help_btn = QPushButton("帮助")
|
||
|
||
# 添加到布局
|
||
control_layout.addWidget(self.start_btn)
|
||
control_layout.addWidget(self.stop_btn)
|
||
control_layout.addStretch()
|
||
control_layout.addWidget(self.save_btn)
|
||
control_layout.addWidget(self.load_btn)
|
||
control_layout.addWidget(self.reset_btn)
|
||
control_layout.addWidget(self.help_btn)
|
||
|
||
parent_layout.addLayout(control_layout)
|
||
|
||
def setup_connections(self):
|
||
"""设置信号连接"""
|
||
# 按钮连接
|
||
self.start_btn.clicked.connect(self.start_test)
|
||
self.stop_btn.clicked.connect(self.stop_test)
|
||
self.save_btn.clicked.connect(self.save_config)
|
||
self.load_btn.clicked.connect(self.load_config)
|
||
self.reset_btn.clicked.connect(self.reset_config)
|
||
self.clear_history_btn.clicked.connect(self.clear_history)
|
||
self.export_history_btn.clicked.connect(self.export_history)
|
||
self.load_history_btn.clicked.connect(self.load_history)
|
||
self.save_result_btn.clicked.connect(self.save_result)
|
||
self.help_btn.clicked.connect(self.show_help)
|
||
|
||
# 模式切换
|
||
self.mode_combo.currentTextChanged.connect(self.on_mode_changed)
|
||
|
||
# 协议切换
|
||
self.protocol_combo.currentTextChanged.connect(self.on_protocol_changed)
|
||
|
||
def browse_file(self):
|
||
"""浏览文件"""
|
||
file_path, _ = QFileDialog.getOpenFileName(
|
||
self, "选择文件", "", "所有文件 (*)"
|
||
)
|
||
if file_path:
|
||
self.file_edit.setText(file_path)
|
||
|
||
def browse_pid_file(self):
|
||
"""浏览PID文件"""
|
||
file_path, _ = QFileDialog.getSaveFileName(
|
||
self, "选择PID文件", "", "所有文件 (*)"
|
||
)
|
||
if file_path:
|
||
self.pid_file_edit.setText(file_path)
|
||
|
||
def browse_logfile(self):
|
||
"""浏览日志文件"""
|
||
file_path, _ = QFileDialog.getSaveFileName(
|
||
self, "选择日志文件", "", "日志文件 (*.log);;所有文件 (*)"
|
||
)
|
||
if file_path:
|
||
self.logfile_edit.setText(file_path)
|
||
|
||
def show_help(self):
|
||
"""显示帮助"""
|
||
help_text = """
|
||
iperf3 网络性能测试工具 - 完整版
|
||
|
||
支持所有 iperf3 选项:
|
||
|
||
基础选项:
|
||
- -c/-s: 客户端/服务器模式
|
||
- -p: 端口号
|
||
- -t: 测试时间
|
||
- -P: 并行流数量
|
||
- -u: UDP协议
|
||
- -b: 带宽限制
|
||
|
||
高级选项:
|
||
- -w: TCP窗口大小
|
||
- -M: TCP MSS大小
|
||
- -N: 禁用Nagle算法
|
||
- -R: 反向测试
|
||
- --bidir: 双向测试
|
||
- -4/-6: IPv4/IPv6
|
||
|
||
服务器选项:
|
||
- -D: 守护进程模式
|
||
- --server-bitrate-limit: 服务器带宽限制
|
||
- --idle-timeout: 空闲超时
|
||
|
||
认证选项:
|
||
- --username: 用户名
|
||
- --rsa-private-key-path: RSA私钥路径
|
||
|
||
更多信息请参考 iperf3 官方文档。
|
||
"""
|
||
|
||
QMessageBox.information(self, "帮助", help_text)
|
||
|
||
def on_mode_changed(self, mode):
|
||
"""处理模式切换"""
|
||
is_client = (mode == "客户端")
|
||
self.server_ip_edit.setEnabled(is_client)
|
||
self.time_spin.setEnabled(is_client)
|
||
self.bandwidth_combo.setEnabled(is_client)
|
||
self.reverse_check.setEnabled(is_client)
|
||
self.bidir_check.setEnabled(is_client)
|
||
self.data_amount_edit.setEnabled(is_client)
|
||
self.blockcount_edit.setEnabled(is_client)
|
||
|
||
def on_protocol_changed(self, protocol):
|
||
"""处理协议切换"""
|
||
is_udp = (protocol == "UDP")
|
||
self.udp_counters_64bit_check.setEnabled(is_udp)
|
||
self.repeating_payload_check.setEnabled(is_udp)
|
||
|
||
def start_test(self):
|
||
"""开始测试"""
|
||
if self.worker and self.worker.isRunning():
|
||
QMessageBox.warning(self, "警告", "测试正在进行中")
|
||
return
|
||
|
||
# 验证输入
|
||
if not self.validate_input():
|
||
return
|
||
|
||
# 准备参数
|
||
params = self.get_test_params()
|
||
|
||
# 创建并启动工作线程
|
||
self.worker = Iperf3Worker(params)
|
||
self.worker.output_received.connect(self.append_output)
|
||
self.worker.test_completed.connect(self.on_test_completed)
|
||
self.worker.progress_updated.connect(self.update_progress)
|
||
self.worker.error_occurred.connect(self.on_test_error)
|
||
|
||
# 更新界面状态
|
||
self.start_btn.setEnabled(False)
|
||
self.stop_btn.setEnabled(True)
|
||
self.progress_bar.show()
|
||
self.progress_bar.setValue(0)
|
||
self.status_bar.showMessage("测试进行中...")
|
||
|
||
# 清空结果
|
||
self.result_text.clear()
|
||
for label in self.stats_labels.values():
|
||
label.setText("--")
|
||
|
||
# 开始测试
|
||
self.worker.start()
|
||
|
||
def stop_test(self):
|
||
"""停止测试"""
|
||
if self.worker and self.worker.isRunning():
|
||
self.worker.stop()
|
||
self.worker.wait()
|
||
self.status_bar.showMessage("测试已停止")
|
||
|
||
self.reset_ui_state()
|
||
|
||
def validate_input(self):
|
||
"""验证输入"""
|
||
mode = self.mode_combo.currentText()
|
||
|
||
if mode == "客户端":
|
||
server_ip = self.server_ip_edit.text().strip()
|
||
if not server_ip:
|
||
QMessageBox.warning(self, "输入错误", "请输入服务器地址")
|
||
self.server_ip_edit.setFocus()
|
||
return False
|
||
|
||
port = self.port_edit.text().strip()
|
||
if port and not port.isdigit():
|
||
QMessageBox.warning(self, "输入错误", "端口必须是数字")
|
||
self.port_edit.setFocus()
|
||
return False
|
||
|
||
return True
|
||
|
||
def get_test_params(self):
|
||
"""获取测试参数"""
|
||
params = {
|
||
# 基础参数
|
||
"mode": "server" if self.mode_combo.currentText() == "服务器" else "client",
|
||
"server_ip": self.server_ip_edit.text().strip(),
|
||
"server_port": self.port_edit.text().strip() or "5201",
|
||
"protocol": "udp" if self.protocol_combo.currentText() == "UDP" else "tcp",
|
||
"time": self.time_spin.value(),
|
||
"parallel": self.parallel_spin.value(),
|
||
"bandwidth": self.bandwidth_combo.currentText().strip(),
|
||
"window_size": self.window_combo.currentText().strip(),
|
||
"mss": self.mss_spin.value() if self.mss_spin.value() > 0 else "",
|
||
"length": self.length_combo.currentText().strip(),
|
||
"bytes": self.data_amount_edit.text().strip(),
|
||
"blockcount": self.blockcount_edit.text().strip(),
|
||
"interval": self.interval_spin.value(),
|
||
"format": {"自动": "", "K": "k", "M": "m", "G": "g", "T": "t"}[self.format_combo.currentText()],
|
||
|
||
# 高级选项
|
||
"reverse": self.reverse_check.isChecked(),
|
||
"bidir": self.bidir_check.isChecked(),
|
||
"connect_timeout": self.connect_timeout_spin.value() if self.connect_timeout_spin.value() > 0 else "",
|
||
"bind_dev": self.bind_dev_edit.text().strip(),
|
||
"bind_host": self.bind_host_edit.text().strip(),
|
||
"cport": self.cport_edit.text().strip(),
|
||
"pacing_timer": self.pacing_timer_edit.text().strip(),
|
||
"omit": self.omit_spin.value() if self.omit_spin.value() > 0 else "",
|
||
"title": self.title_edit.text().strip(),
|
||
"extra_data": self.extra_data_edit.text().strip(),
|
||
"get_server_output": self.get_server_output_check.isChecked(),
|
||
"udp_counters_64bit": self.udp_counters_64bit_check.isChecked(),
|
||
"repeating_payload": self.repeating_payload_check.isChecked(),
|
||
"dont_fragment": self.dont_fragment_check.isChecked(),
|
||
|
||
# 网络选项
|
||
"ipv4_only": self.ip_version_combo.currentText() == "仅IPv4",
|
||
"ipv6_only": self.ip_version_combo.currentText() == "仅IPv6",
|
||
"tos": self.tos_spin.value() if self.tos_spin.value() > 0 else "",
|
||
"dscp": self.dscp_spin.value() if self.dscp_spin.value() > 0 else "",
|
||
"no_delay": self.no_delay_check.isChecked(),
|
||
|
||
# 性能选项
|
||
"zerocopy": self.zerocopy_check.isChecked(),
|
||
"skip_rx_copy": self.skip_rx_copy_check.isChecked(),
|
||
"rcv_timeout": self.rcv_timeout_spin.value() if self.rcv_timeout_spin.value() > 0 else "",
|
||
|
||
# 认证选项
|
||
"username": self.username_edit.text().strip(),
|
||
"rsa_public_key_path": self.rsa_public_key_path_edit.text().strip(),
|
||
|
||
# 服务器选项
|
||
"one_off": self.one_off_check.isChecked(),
|
||
"daemon": self.daemon_check.isChecked(),
|
||
"server_bitrate_limit": self.server_bitrate_limit_edit.text().strip(),
|
||
"idle_timeout": self.idle_timeout_spin.value() if self.idle_timeout_spin.value() > 0 else "",
|
||
"server_max_duration": self.server_max_duration_spin.value() if self.server_max_duration_spin.value() > 0 else "",
|
||
"rsa_private_key_path": self.rsa_private_key_path_edit.text().strip(),
|
||
"authorized_users_path": self.authorized_users_path_edit.text().strip(),
|
||
"time_skew_threshold": self.time_skew_threshold_spin.value() if self.time_skew_threshold_spin.value() > 0 else "",
|
||
"use_pkcs1_padding": self.use_pkcs1_padding_check.isChecked(),
|
||
|
||
# 文件选项
|
||
"file": self.file_edit.text().strip(),
|
||
"pid_file": self.pid_file_edit.text().strip(),
|
||
"logfile": self.logfile_edit.text().strip(),
|
||
|
||
# 输出选项
|
||
"json_output": self.json_check.isChecked(),
|
||
"json_stream": self.json_stream_check.isChecked(),
|
||
"json_stream_full_output": self.json_stream_full_output_check.isChecked(),
|
||
"verbose": self.verbose_check.isChecked(),
|
||
"debug": self.debug_check.isChecked(),
|
||
"debug_level": self.debug_level_combo.currentText() if self.debug_check.isChecked() else "",
|
||
"version": self.version_check.isChecked(),
|
||
"forceflush": self.forceflush_check.isChecked(),
|
||
"timestamps": self.timestamps_check.isChecked(),
|
||
"timestamps_format": self.timestamps_format_edit.text().strip(),
|
||
|
||
# 系统选项
|
||
"affinity": self.affinity_edit.text().strip()
|
||
}
|
||
|
||
return params
|
||
|
||
def append_output(self, text):
|
||
"""追加输出文本"""
|
||
self.result_text.moveCursor(QTextCursor.End)
|
||
self.result_text.insertPlainText(text)
|
||
self.result_text.ensureCursorVisible()
|
||
|
||
# 尝试解析结果
|
||
self.parse_results(text)
|
||
|
||
def parse_results(self, text):
|
||
"""解析测试结果"""
|
||
try:
|
||
lines = text.strip().split('\n')
|
||
for line in lines:
|
||
line = line.strip()
|
||
|
||
# 解析带宽
|
||
if 'Gbits/sec' in line:
|
||
match = re.search(r'([\d\.]+)\s+Gbits/sec', line)
|
||
if match:
|
||
self.stats_labels['bandwidth'].setText(f"{match.group(1)} Gbps")
|
||
elif 'Mbits/sec' in line:
|
||
match = re.search(r'([\d\.]+)\s+Mbits/sec', line)
|
||
if match:
|
||
self.stats_labels['bandwidth'].setText(f"{match.group(1)} Mbps")
|
||
elif 'Kbits/sec' in line:
|
||
match = re.search(r'([\d\.]+)\s+Kbits/sec', line)
|
||
if match:
|
||
self.stats_labels['bandwidth'].setText(f"{match.group(1)} Kbps")
|
||
|
||
# 解析抖动
|
||
if 'ms' in line and ('jitter' in line.lower() or 'Jitter' in line):
|
||
match = re.search(r'[Jj]itter\s*[\:=]\s*([\d\.]+)\s*ms', line)
|
||
if match:
|
||
self.stats_labels['jitter'].setText(f"{match.group(1)} ms")
|
||
|
||
# 解析丢包
|
||
if 'loss' in line.lower():
|
||
match = re.search(r'([\d\.]+)%', line)
|
||
if match:
|
||
self.stats_labels['loss'].setText(f"{match.group(1)}%")
|
||
|
||
# 解析重传
|
||
if 'retransmits' in line.lower():
|
||
match = re.search(r'([\d\.]+)', line)
|
||
if match:
|
||
self.stats_labels['retransmits'].setText(f"{match.group(1)}")
|
||
|
||
# 解析数据量
|
||
if 'Bytes' in line and 'bits/sec' in line:
|
||
match = re.search(r'([\d\.]+)\s+Bytes', line)
|
||
if match:
|
||
bytes_value = float(match.group(1))
|
||
if bytes_value > 1e9:
|
||
self.stats_labels['bytes'].setText(f"{bytes_value/1e9:.2f} GB")
|
||
elif bytes_value > 1e6:
|
||
self.stats_labels['bytes'].setText(f"{bytes_value/1e6:.2f} MB")
|
||
elif bytes_value > 1e3:
|
||
self.stats_labels['bytes'].setText(f"{bytes_value/1e3:.2f} KB")
|
||
else:
|
||
self.stats_labels['bytes'].setText(f"{bytes_value} Bytes")
|
||
|
||
except Exception as e:
|
||
print(f"解析结果错误: {e}")
|
||
|
||
def update_progress(self, progress, message):
|
||
"""更新进度"""
|
||
self.progress_bar.setValue(progress)
|
||
self.status_bar.showMessage(message)
|
||
|
||
def on_test_completed(self, result):
|
||
"""测试完成处理"""
|
||
self.status_bar.showMessage("测试完成")
|
||
self.reset_ui_state()
|
||
|
||
# 保存到历史记录
|
||
self.save_to_history()
|
||
|
||
# 显示完成消息
|
||
if result["status"] == "success":
|
||
QMessageBox.information(self, "完成", "测试完成")
|
||
|
||
def on_test_error(self, error_message):
|
||
"""测试错误处理"""
|
||
self.append_output(f"\n错误: {error_message}\n")
|
||
self.status_bar.showMessage("测试出错")
|
||
self.reset_ui_state()
|
||
QMessageBox.critical(self, "错误", error_message)
|
||
|
||
def reset_ui_state(self):
|
||
"""重置UI状态"""
|
||
self.start_btn.setEnabled(True)
|
||
self.stop_btn.setEnabled(False)
|
||
self.progress_bar.hide()
|
||
|
||
def save_result(self):
|
||
"""保存测试结果"""
|
||
if not self.result_text.toPlainText().strip():
|
||
QMessageBox.warning(self, "警告", "没有测试结果可保存")
|
||
return
|
||
|
||
file_path, _ = QFileDialog.getSaveFileName(
|
||
self, "保存测试结果", "", "文本文件 (*.txt);;所有文件 (*)"
|
||
)
|
||
|
||
if file_path:
|
||
try:
|
||
with open(file_path, 'w', encoding='utf-8') as f:
|
||
f.write("iperf3 测试结果\n")
|
||
f.write("=" * 50 + "\n\n")
|
||
f.write(f"测试时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
|
||
f.write(f"测试模式: {self.mode_combo.currentText()}\n")
|
||
f.write(f"协议: {self.protocol_combo.currentText()}\n")
|
||
f.write(f"服务器: {self.server_ip_edit.text()}\n")
|
||
f.write(f"端口: {self.port_edit.text()}\n\n")
|
||
f.write("测试结果:\n")
|
||
f.write("=" * 50 + "\n")
|
||
f.write(self.result_text.toPlainText())
|
||
|
||
QMessageBox.information(self, "成功", f"测试结果已保存到: {file_path}")
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "错误", f"保存失败: {str(e)}")
|
||
|
||
def save_config(self):
|
||
"""保存配置"""
|
||
config = {
|
||
# 基础设置
|
||
"server_ip": self.server_ip_edit.text(),
|
||
"server_port": self.port_edit.text(),
|
||
"protocol": self.protocol_combo.currentIndex(),
|
||
"mode": self.mode_combo.currentIndex(),
|
||
"time": self.time_spin.value(),
|
||
"parallel": self.parallel_spin.value(),
|
||
"bandwidth": self.bandwidth_combo.currentText(),
|
||
"window_size": self.window_combo.currentText(),
|
||
"mss": self.mss_spin.value(),
|
||
"length": self.length_combo.currentText(),
|
||
"bytes": self.data_amount_edit.text(),
|
||
"blockcount": self.blockcount_edit.text(),
|
||
"interval": self.interval_spin.value(),
|
||
"format": self.format_combo.currentIndex(),
|
||
|
||
# 高级选项
|
||
"reverse": self.reverse_check.isChecked(),
|
||
"bidir": self.bidir_check.isChecked(),
|
||
"connect_timeout": self.connect_timeout_spin.value(),
|
||
"bind_dev": self.bind_dev_edit.text(),
|
||
"bind_host": self.bind_host_edit.text(),
|
||
"cport": self.cport_edit.text(),
|
||
"pacing_timer": self.pacing_timer_edit.text(),
|
||
"omit": self.omit_spin.value(),
|
||
"title": self.title_edit.text(),
|
||
"extra_data": self.extra_data_edit.text(),
|
||
"get_server_output": self.get_server_output_check.isChecked(),
|
||
"udp_counters_64bit": self.udp_counters_64bit_check.isChecked(),
|
||
"repeating_payload": self.repeating_payload_check.isChecked(),
|
||
"dont_fragment": self.dont_fragment_check.isChecked(),
|
||
|
||
# 网络选项
|
||
"ipv4_only": self.ip_version_combo.currentIndex() == 1,
|
||
"ipv6_only": self.ip_version_combo.currentIndex() == 2,
|
||
"tos": self.tos_spin.value(),
|
||
"dscp": self.dscp_spin.value(),
|
||
"no_delay": self.no_delay_check.isChecked(),
|
||
|
||
# 性能选项
|
||
"zerocopy": self.zerocopy_check.isChecked(),
|
||
"skip_rx_copy": self.skip_rx_copy_check.isChecked(),
|
||
"rcv_timeout": self.rcv_timeout_spin.value(),
|
||
|
||
# 认证选项
|
||
"username": self.username_edit.text(),
|
||
"rsa_public_key_path": self.rsa_public_key_path_edit.text(),
|
||
|
||
# 服务器选项
|
||
"one_off": self.one_off_check.isChecked(),
|
||
"daemon": self.daemon_check.isChecked(),
|
||
"server_bitrate_limit": self.server_bitrate_limit_edit.text(),
|
||
"idle_timeout": self.idle_timeout_spin.value(),
|
||
"server_max_duration": self.server_max_duration_spin.value(),
|
||
"rsa_private_key_path": self.rsa_private_key_path_edit.text(),
|
||
"authorized_users_path": self.authorized_users_path_edit.text(),
|
||
"time_skew_threshold": self.time_skew_threshold_spin.value(),
|
||
"use_pkcs1_padding": self.use_pkcs1_padding_check.isChecked(),
|
||
|
||
# 文件选项
|
||
"file": self.file_edit.text(),
|
||
"pid_file": self.pid_file_edit.text(),
|
||
"logfile": self.logfile_edit.text(),
|
||
|
||
# 输出选项
|
||
"json_output": self.json_check.isChecked(),
|
||
"json_stream": self.json_stream_check.isChecked(),
|
||
"json_stream_full_output": self.json_stream_full_output_check.isChecked(),
|
||
"verbose": self.verbose_check.isChecked(),
|
||
"debug": self.debug_check.isChecked(),
|
||
"debug_level": self.debug_level_combo.currentText(),
|
||
"version": self.version_check.isChecked(),
|
||
"forceflush": self.forceflush_check.isChecked(),
|
||
"timestamps": self.timestamps_check.isChecked(),
|
||
"timestamps_format": self.timestamps_format_edit.text(),
|
||
|
||
# 系统选项
|
||
"affinity": self.affinity_edit.text()
|
||
}
|
||
|
||
self.settings_manager.settings.update(config)
|
||
if self.settings_manager.save_all():
|
||
QMessageBox.information(self, "成功", "配置已保存")
|
||
else:
|
||
QMessageBox.warning(self, "错误", "保存配置失败")
|
||
|
||
def load_config(self):
|
||
"""加载配置"""
|
||
# 基础设置
|
||
self.server_ip_edit.setText(str(self.settings_manager.get("server_ip", "")))
|
||
self.port_edit.setText(str(self.settings_manager.get("server_port", "5201")))
|
||
|
||
# 下拉框
|
||
self.protocol_combo.setCurrentIndex(int(self.settings_manager.get("protocol", 0)))
|
||
self.mode_combo.setCurrentIndex(int(self.settings_manager.get("mode", 0)))
|
||
self.format_combo.setCurrentIndex(int(self.settings_manager.get("format", 0)))
|
||
|
||
# 数值设置
|
||
self.time_spin.setValue(int(self.settings_manager.get("time", 10)))
|
||
self.parallel_spin.setValue(int(self.settings_manager.get("parallel", 1)))
|
||
self.mss_spin.setValue(int(self.settings_manager.get("mss", 0)))
|
||
self.interval_spin.setValue(float(self.settings_manager.get("interval", 1.0)))
|
||
|
||
# 文本设置
|
||
self.bandwidth_combo.setCurrentText(str(self.settings_manager.get("bandwidth", "")))
|
||
self.window_combo.setCurrentText(str(self.settings_manager.get("window_size", "")))
|
||
self.length_combo.setCurrentText(str(self.settings_manager.get("length", "")))
|
||
self.data_amount_edit.setText(str(self.settings_manager.get("bytes", "")))
|
||
self.blockcount_edit.setText(str(self.settings_manager.get("blockcount", "")))
|
||
|
||
# 高级选项 - 修复这里,添加安全转换
|
||
try:
|
||
self.connect_timeout_spin.setValue(int(self.settings_manager.get("connect_timeout", 0)))
|
||
except (ValueError, TypeError):
|
||
self.connect_timeout_spin.setValue(0)
|
||
|
||
self.bind_dev_edit.setText(str(self.settings_manager.get("bind_dev", "")))
|
||
self.bind_host_edit.setText(str(self.settings_manager.get("bind_host", "")))
|
||
self.cport_edit.setText(str(self.settings_manager.get("cport", "")))
|
||
self.pacing_timer_edit.setText(str(self.settings_manager.get("pacing_timer", "")))
|
||
|
||
try:
|
||
self.omit_spin.setValue(int(self.settings_manager.get("omit", 0)))
|
||
except (ValueError, TypeError):
|
||
self.omit_spin.setValue(0)
|
||
|
||
self.title_edit.setText(str(self.settings_manager.get("title", "")))
|
||
self.extra_data_edit.setText(str(self.settings_manager.get("extra_data", "")))
|
||
|
||
# 网络选项
|
||
ip_version = 0
|
||
if self.settings_manager.get("ipv4_only", False):
|
||
ip_version = 1
|
||
elif self.settings_manager.get("ipv6_only", False):
|
||
ip_version = 2
|
||
self.ip_version_combo.setCurrentIndex(ip_version)
|
||
|
||
try:
|
||
self.tos_spin.setValue(int(self.settings_manager.get("tos", 0)))
|
||
except (ValueError, TypeError):
|
||
self.tos_spin.setValue(0)
|
||
|
||
try:
|
||
self.dscp_spin.setValue(int(self.settings_manager.get("dscp", 0)))
|
||
except (ValueError, TypeError):
|
||
self.dscp_spin.setValue(0)
|
||
|
||
# 服务器选项
|
||
self.server_bitrate_limit_edit.setText(str(self.settings_manager.get("server_bitrate_limit", "")))
|
||
|
||
try:
|
||
self.idle_timeout_spin.setValue(int(self.settings_manager.get("idle_timeout", 0)))
|
||
except (ValueError, TypeError):
|
||
self.idle_timeout_spin.setValue(0)
|
||
|
||
try:
|
||
self.server_max_duration_spin.setValue(int(self.settings_manager.get("server_max_duration", 0)))
|
||
except (ValueError, TypeError):
|
||
self.server_max_duration_spin.setValue(0)
|
||
|
||
try:
|
||
self.time_skew_threshold_spin.setValue(float(self.settings_manager.get("time_skew_threshold", 0.0)))
|
||
except (ValueError, TypeError):
|
||
self.time_skew_threshold_spin.setValue(0.0)
|
||
|
||
# 认证选项
|
||
self.username_edit.setText(str(self.settings_manager.get("username", "")))
|
||
self.rsa_private_key_path_edit.setText(str(self.settings_manager.get("rsa_private_key_path", "")))
|
||
self.rsa_public_key_path_edit.setText(str(self.settings_manager.get("rsa_public_key_path", "")))
|
||
self.authorized_users_path_edit.setText(str(self.settings_manager.get("authorized_users_path", "")))
|
||
|
||
# 文件选项
|
||
self.file_edit.setText(str(self.settings_manager.get("file", "")))
|
||
self.pid_file_edit.setText(str(self.settings_manager.get("pid_file", "")))
|
||
self.logfile_edit.setText(str(self.settings_manager.get("logfile", "")))
|
||
|
||
# 系统选项
|
||
self.affinity_edit.setText(str(self.settings_manager.get("affinity", "")))
|
||
self.timestamps_format_edit.setText(str(self.settings_manager.get("timestamps_format", "")))
|
||
|
||
# 性能选项
|
||
try:
|
||
self.rcv_timeout_spin.setValue(int(self.settings_manager.get("rcv_timeout", 0)))
|
||
except (ValueError, TypeError):
|
||
self.rcv_timeout_spin.setValue(0)
|
||
|
||
# 复选框
|
||
self.reverse_check.setChecked(bool(self.settings_manager.get("reverse", False)))
|
||
self.bidir_check.setChecked(bool(self.settings_manager.get("bidir", False)))
|
||
self.get_server_output_check.setChecked(bool(self.settings_manager.get("get_server_output", False)))
|
||
self.udp_counters_64bit_check.setChecked(bool(self.settings_manager.get("udp_counters_64bit", False)))
|
||
self.repeating_payload_check.setChecked(bool(self.settings_manager.get("repeating_payload", False)))
|
||
self.dont_fragment_check.setChecked(bool(self.settings_manager.get("dont_fragment", False)))
|
||
self.no_delay_check.setChecked(bool(self.settings_manager.get("no_delay", False)))
|
||
self.zerocopy_check.setChecked(bool(self.settings_manager.get("zerocopy", False)))
|
||
self.skip_rx_copy_check.setChecked(bool(self.settings_manager.get("skip_rx_copy", False)))
|
||
|
||
# 服务器复选框
|
||
self.one_off_check.setChecked(bool(self.settings_manager.get("one_off", False)))
|
||
self.daemon_check.setChecked(bool(self.settings_manager.get("daemon", False)))
|
||
self.use_pkcs1_padding_check.setChecked(bool(self.settings_manager.get("use_pkcs1_padding", False)))
|
||
|
||
# 输出复选框
|
||
self.json_check.setChecked(bool(self.settings_manager.get("json_output", False)))
|
||
self.json_stream_check.setChecked(bool(self.settings_manager.get("json_stream", False)))
|
||
self.json_stream_full_output_check.setChecked(bool(self.settings_manager.get("json_stream_full_output", False)))
|
||
self.verbose_check.setChecked(bool(self.settings_manager.get("verbose", False)))
|
||
self.debug_check.setChecked(bool(self.settings_manager.get("debug", False)))
|
||
self.version_check.setChecked(bool(self.settings_manager.get("version", False)))
|
||
self.forceflush_check.setChecked(bool(self.settings_manager.get("forceflush", False)))
|
||
self.timestamps_check.setChecked(bool(self.settings_manager.get("timestamps", False)))
|
||
|
||
# 调试级别
|
||
debug_level = str(self.settings_manager.get("debug_level", "4"))
|
||
index = self.debug_level_combo.findText(debug_level)
|
||
if index >= 0:
|
||
self.debug_level_combo.setCurrentIndex(index)
|
||
|
||
QMessageBox.information(self, "成功", "配置已加载")
|
||
|
||
# 更新模式相关控件状态
|
||
self.on_mode_changed(self.mode_combo.currentText())
|
||
self.on_protocol_changed(self.protocol_combo.currentText())
|
||
|
||
def reset_config(self):
|
||
"""重置配置"""
|
||
reply = QMessageBox.question(
|
||
self, "确认",
|
||
"确定要重置所有配置吗?",
|
||
QMessageBox.Yes | QMessageBox.No
|
||
)
|
||
|
||
if reply == QMessageBox.Yes:
|
||
self.settings_manager = SettingsManager()
|
||
self.load_config()
|
||
|
||
def save_to_history(self):
|
||
"""保存到历史记录"""
|
||
try:
|
||
bandwidth = self.stats_labels['bandwidth'].text()
|
||
if bandwidth != "--":
|
||
row = self.history_table.rowCount()
|
||
self.history_table.insertRow(row)
|
||
|
||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||
mode = self.mode_combo.currentText()
|
||
protocol = self.protocol_combo.currentText()
|
||
server = self.server_ip_edit.text()
|
||
jitter = self.stats_labels['jitter'].text()
|
||
loss = self.stats_labels['loss'].text()
|
||
duration = str(self.time_spin.value())
|
||
parallel = str(self.parallel_spin.value())
|
||
|
||
note = ""
|
||
if self.reverse_check.isChecked():
|
||
note += "反向 "
|
||
if self.bidir_check.isChecked():
|
||
note += "双向 "
|
||
|
||
self.history_table.setItem(row, 0, QTableWidgetItem(current_time))
|
||
self.history_table.setItem(row, 1, QTableWidgetItem(mode))
|
||
self.history_table.setItem(row, 2, QTableWidgetItem(protocol))
|
||
self.history_table.setItem(row, 3, QTableWidgetItem(server))
|
||
self.history_table.setItem(row, 4, QTableWidgetItem(bandwidth))
|
||
self.history_table.setItem(row, 5, QTableWidgetItem(jitter))
|
||
self.history_table.setItem(row, 6, QTableWidgetItem(loss))
|
||
self.history_table.setItem(row, 7, QTableWidgetItem(duration))
|
||
self.history_table.setItem(row, 8, QTableWidgetItem(parallel))
|
||
self.history_table.setItem(row, 9, QTableWidgetItem(note.strip()))
|
||
except Exception as e:
|
||
print(f"保存历史记录错误: {e}")
|
||
|
||
def clear_history(self):
|
||
"""清空历史记录"""
|
||
reply = QMessageBox.question(
|
||
self, "确认",
|
||
"确定要清空历史记录吗?",
|
||
QMessageBox.Yes | QMessageBox.No
|
||
)
|
||
|
||
if reply == QMessageBox.Yes:
|
||
self.history_table.setRowCount(0)
|
||
|
||
def export_history(self):
|
||
"""导出历史记录"""
|
||
if self.history_table.rowCount() == 0:
|
||
QMessageBox.warning(self, "警告", "没有历史记录可导出")
|
||
return
|
||
|
||
file_path, _ = QFileDialog.getSaveFileName(
|
||
self, "导出历史记录", "", "CSV文件 (*.csv);;所有文件 (*)"
|
||
)
|
||
|
||
if file_path:
|
||
try:
|
||
with open(file_path, 'w', encoding='utf-8') as f:
|
||
# 写入表头
|
||
headers = []
|
||
for col in range(self.history_table.columnCount()):
|
||
headers.append(self.history_table.horizontalHeaderItem(col).text())
|
||
f.write(','.join(headers) + '\n')
|
||
|
||
# 写入数据
|
||
for row in range(self.history_table.rowCount()):
|
||
row_data = []
|
||
for col in range(self.history_table.columnCount()):
|
||
item = self.history_table.item(row, col)
|
||
row_data.append(item.text() if item else "")
|
||
f.write(','.join(row_data) + '\n')
|
||
|
||
QMessageBox.information(self, "成功", f"历史记录已导出到: {file_path}")
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "错误", f"导出失败: {str(e)}")
|
||
|
||
def load_history(self):
|
||
"""加载历史记录"""
|
||
file_path, _ = QFileDialog.getOpenFileName(
|
||
self, "加载历史记录", "", "CSV文件 (*.csv);;所有文件 (*)"
|
||
)
|
||
|
||
if file_path:
|
||
try:
|
||
with open(file_path, 'r', encoding='utf-8') as f:
|
||
lines = f.readlines()
|
||
|
||
if len(lines) > 0:
|
||
self.history_table.setRowCount(0)
|
||
|
||
for i, line in enumerate(lines[1:]): # 跳过表头
|
||
data = line.strip().split(',')
|
||
if len(data) >= 10:
|
||
row = self.history_table.rowCount()
|
||
self.history_table.insertRow(row)
|
||
|
||
for col in range(min(10, len(data))):
|
||
self.history_table.setItem(row, col, QTableWidgetItem(data[col]))
|
||
|
||
QMessageBox.information(self, "成功", "历史记录已加载")
|
||
except Exception as e:
|
||
QMessageBox.critical(self, "错误", f"加载失败: {str(e)}")
|
||
|
||
def apply_styles(self):
|
||
"""应用样式"""
|
||
style = """
|
||
QMainWindow {
|
||
background-color: #f5f6fa;
|
||
}
|
||
QGroupBox {
|
||
font-weight: bold;
|
||
border: 2px solid #dcdde1;
|
||
border-radius: 5px;
|
||
margin-top: 10px;
|
||
padding-top: 10px;
|
||
}
|
||
QGroupBox::title {
|
||
subcontrol-origin: margin;
|
||
left: 10px;
|
||
padding: 0 5px 0 5px;
|
||
}
|
||
QLabel {
|
||
color: #2c3e50;
|
||
}
|
||
QLineEdit, QComboBox, QSpinBox, QDoubleSpinBox {
|
||
padding: 5px;
|
||
border: 1px solid #bdc3c7;
|
||
border-radius: 3px;
|
||
min-height: 25px;
|
||
}
|
||
QTextEdit {
|
||
border: 1px solid #bdc3c7;
|
||
border-radius: 3px;
|
||
background-color: white;
|
||
}
|
||
QTableWidget {
|
||
border: 1px solid #bdc3c7;
|
||
border-radius: 3px;
|
||
background-color: white;
|
||
gridline-color: #ecf0f1;
|
||
}
|
||
QHeaderView::section {
|
||
background-color: #3498db;
|
||
color: white;
|
||
padding: 5px;
|
||
border: none;
|
||
}
|
||
QStatusBar {
|
||
background-color: #34495e;
|
||
color: white;
|
||
}
|
||
QTabWidget::pane {
|
||
border: 1px solid #bdc3c7;
|
||
border-radius: 3px;
|
||
background-color: white;
|
||
}
|
||
QTabBar::tab {
|
||
background-color: #ecf0f1;
|
||
padding: 8px 15px;
|
||
margin-right: 2px;
|
||
border-top-left-radius: 3px;
|
||
border-top-right-radius: 3px;
|
||
}
|
||
QTabBar::tab:selected {
|
||
background-color: #3498db;
|
||
color: white;
|
||
}
|
||
"""
|
||
self.setStyleSheet(style)
|
||
|
||
def load_settings(self):
|
||
"""加载窗口设置"""
|
||
geometry = self.settings_manager.get("geometry")
|
||
if geometry and isinstance(geometry, list) and len(geometry) == 4:
|
||
self.setGeometry(*geometry)
|
||
|
||
window_state = self.settings_manager.get("window_state")
|
||
if window_state and isinstance(window_state, str):
|
||
try:
|
||
# 将十六进制字符串转换回QByteArray
|
||
byte_array = QByteArray.fromHex(window_state.encode())
|
||
self.restoreState(byte_array)
|
||
except:
|
||
pass
|
||
|
||
# 加载配置
|
||
self.load_config()
|
||
|
||
# 更新模式相关控件状态
|
||
self.on_mode_changed(self.mode_combo.currentText())
|
||
self.on_protocol_changed(self.protocol_combo.currentText())
|
||
|
||
def closeEvent(self, event):
|
||
"""关闭事件处理"""
|
||
# 保存窗口状态
|
||
self.settings_manager.set("geometry", [
|
||
self.geometry().x(),
|
||
self.geometry().y(),
|
||
self.geometry().width(),
|
||
self.geometry().height()
|
||
])
|
||
|
||
# 保存窗口状态为QByteArray
|
||
window_state = self.saveState()
|
||
self.settings_manager.set("window_state", window_state)
|
||
|
||
# 保存当前配置
|
||
self.save_config()
|
||
|
||
# 停止正在运行的测试
|
||
if self.worker and self.worker.isRunning():
|
||
self.worker.stop()
|
||
self.worker.wait()
|
||
|
||
# 保存所有设置
|
||
self.settings_manager.save_all()
|
||
|
||
event.accept()
|
||
|
||
def main():
|
||
"""主函数"""
|
||
app = QApplication(sys.argv)
|
||
app.setStyle('Fusion')
|
||
|
||
# 设置应用名称
|
||
app.setApplicationName("iperf3 GUI - 完整版")
|
||
app.setOrganizationName("Network Tools")
|
||
|
||
# 创建并显示主窗口
|
||
window = Iperf3GUI()
|
||
window.show()
|
||
|
||
sys.exit(app.exec_())
|
||
|
||
if __name__ == "__main__":
|
||
main() |