From 089a11207ae62884c1bcb33cf8499b77e5c9dc08 Mon Sep 17 00:00:00 2001 From: Jokul Date: Sun, 9 Aug 2026 08:09:03 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20TCP=20=E8=BF=9E?= =?UTF-8?q?=E6=8E=A5=E6=B8=85=E5=8D=95=E7=AA=97=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package/components/TcpConnectionsWindow.qml | 303 ++++++++++++++++++++ package/networkview.qml | 20 ++ src/networkmonitorapplet.cpp | 196 +++++++++++++ src/networkmonitorapplet.h | 48 +++- 4 files changed, 566 insertions(+), 1 deletion(-) create mode 100644 package/components/TcpConnectionsWindow.qml diff --git a/package/components/TcpConnectionsWindow.qml b/package/components/TcpConnectionsWindow.qml new file mode 100644 index 0000000..be22052 --- /dev/null +++ b/package/components/TcpConnectionsWindow.qml @@ -0,0 +1,303 @@ +// SPDX-FileCopyrightText: 2026 Jokul +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +// TCP 连接清单窗口:屏幕居中的独立顶层窗口,展示当前所有 ESTABLISHED 状态的 +// TCP 连接详情(本地地址:端口、远程地址:端口、状态、进程名),数据来自 C++ 后端 +// 解析 /proc/net/tcp 与 tcp6(含进程名反查)。 +// 设计要点: +// - 视觉风格复用 TrafficStatsWindow.qml(圆角卡片 12px、1px 边框、44px 标题栏、 +// 28x28 圆形关闭按钮 hover 淡红底);深/浅主题由 isDarkMode 切换 +// (networkview.qml 依据 DockPalette 检测任务栏深浅后传入),默认浅色保证独立预览可用 +// - 数据通过属性注入:applet 即 networkview.qml 的 root.applet(C++ 后端对象), +// applet.tcpConnectionList 为 QVariantList of QVariantMap,每项含 +// {localAddress, localPort, remoteAddress, remotePort, state, processName}, +// 为 null 时组件可独立预览(显示空状态) +// - 定时刷新:窗口可见时每 5 秒重建清单(与 C++ 后端降频周期一致),关闭时停止 +// 触发方式:右键菜单"TCP 连接清单" -> show()/raise()/requestActivate() +// 公共能力复用:主题色取自 WindowTheme,标题栏(含关闭按钮)取自 TitleBar +import QtQuick 2.15 +import QtQuick.Layouts 1.15 +import QtQuick.Window 2.15 +import "." + +Window { + id: root + + // 公共主题色:集中定义于 WindowTheme.qml,随 isDarkMode 切换深浅 + WindowTheme { + id: theme + isDarkMode: root.isDarkMode + } + + // 对外依赖:关闭按钮 hover 态文字高亮色,由父组件传入 + // 默认值与 networkview.qml 中 root.accentRed 一致,确保独立可用 + property color accentColor: Qt.rgba(220 / 255, 38 / 255, 38 / 255, 1) + + // 对外依赖:C++ 后端对象(NetworkMonitorApplet),由 networkview.qml 传入 + // 提供 tcpConnectionList(QVariantList of QVariantMap);为 null 时可独立预览(显示空状态) + property var applet: null + + // 深色模式标记:由 networkview.qml 根据 DockPalette 检测后传入; + // 默认 false(浅色)保证组件独立预览时与原版视觉一致 + property bool isDarkMode: false + + // 表格模型:由 refreshModel() 生成的连接记录数组,每项 + // {localAddress, localPort, remoteAddress, remotePort, state, processName} + property var connModel: [] + + // 当前连接总数,底部汇总与顶部状态条共用 + property int connCount: 0 + + // 表格列宽:与表头/行/底部汇总共用同一组固定宽度,保证各列垂直对齐 + readonly property int colLocal: 180 + readonly property int colRemote: 180 + readonly property int colState: 90 + readonly property int colSpacing: 6 + + // 从 applet.tcpConnectionList 重建表格模型 + // 设计原因:连接清单数量随时变化,每次读取重建数组供 ListView 展示; + // applet 为 null 时置空模型,保证独立预览可用 + function refreshModel() { + var list = root.applet ? (root.applet.tcpConnectionList || []) : [] + root.connModel = list + root.connCount = list.length + } + + width: 680 + height: 420 + visible: false + flags: Qt.FramelessWindowHint | Qt.Window + modality: Qt.NonModal + color: "transparent" + + onVisibleChanged: { + if (visible) { + // 居中到当前屏幕(任务栏所在屏幕),加 virtualX/virtualY 偏移 + // 避免多显示器下窗口出现在非预期屏幕 + x = Screen.virtualX + (Screen.width - width) / 2 + y = Screen.virtualY + (Screen.height - height) / 2 + // 窗口显示时立即刷新一次,确保数据最新 + refreshModel() + } + } + + // 定时刷新:窗口可见时每 5 秒重建清单(与 C++ 后端降频周期一致), + // 不依赖 C++ 信号;关闭时停止,兼顾实时性与性能 + Timer { + interval: 5000 + repeat: true + running: root.visible + onTriggered: refreshModel() + } + + // 窗口主体:圆角卡片(背景与边框随 isDarkMode 切换深浅),1px 边框模拟 DTK 窗口描边 + Rectangle { + anchors.fill: parent + color: theme.winBg + radius: 12 + border.width: 1 + border.color: theme.lineColor + + ColumnLayout { + anchors.fill: parent + spacing: 0 + + // 公共标题栏:标题文字 + 关闭按钮,整栏可拖动 + TitleBar { + Layout.fillWidth: true + titleText: qsTr("TCP Connections") + textColor: theme.textPrimary + secondaryTextColor: theme.textSecondary + closeHoverColor: root.accentColor + } + + // 顶部状态条:当前连接数 + 刷新间隔说明 + RowLayout { + Layout.fillWidth: true + Layout.preferredHeight: 34 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + spacing: 8 + + Text { + // 动态刷新连接总数,与底部汇总一致 + text: qsTr("Active connections: ") + root.connCount + font.pixelSize: 12 + font.weight: Font.Medium + color: theme.textPrimary + } + + Item { Layout.fillWidth: true } + + Text { + text: qsTr("Refreshes every 5 seconds") + font.pixelSize: 11 + color: theme.textTertiary + } + } + + // 表头行:本地地址:端口 | 远程地址:端口 | 状态 | 进程名 + RowLayout { + Layout.fillWidth: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 2 + spacing: root.colSpacing + + Text { + Layout.preferredWidth: root.colLocal + text: qsTr("Local Address:Port") + font.pixelSize: 11 + font.weight: Font.Bold + color: theme.textSecondary + } + Text { + Layout.preferredWidth: root.colRemote + text: qsTr("Remote Address:Port") + font.pixelSize: 11 + font.weight: Font.Bold + color: theme.textSecondary + } + Text { + Layout.preferredWidth: root.colState + text: qsTr("State") + font.pixelSize: 11 + font.weight: Font.Bold + color: theme.textSecondary + } + Text { + Layout.fillWidth: true + text: qsTr("Process") + font.pixelSize: 11 + font.weight: Font.Bold + color: theme.textSecondary + } + } + + // 表头与内容间的分隔线 + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 4 + color: theme.lineColor + } + + // 表格区:ListView 展示每条连接记录,行 hover 高亮 + ListView { + Layout.fillWidth: true + Layout.fillHeight: true + Layout.leftMargin: 16 + Layout.rightMargin: 16 + Layout.topMargin: 2 + Layout.bottomMargin: 2 + clip: true + model: root.connModel + onCountChanged: emptyHint.visible = root.connModel.length === 0 + + // 空状态提示(当 connModel 为空时显示) + Item { + id: emptyHint + anchors.fill: parent + visible: root.connModel.length === 0 + Text { + anchors.centerIn: parent + text: qsTr("No active TCP connections") + font.pixelSize: 13 + color: theme.textTertiary + } + } + + delegate: Rectangle { + required property int index + required property var modelData + width: ListView.view.width + height: 28 + radius: 4 + // 行 hover 高亮:浅色用淡黑、深色用淡白,保证两种卡片背景上均可见 + color: rowMouse.containsMouse ? theme.hoverBg : "transparent" + + RowLayout { + anchors.fill: parent + anchors.leftMargin: 8 + anchors.rightMargin: 8 + spacing: root.colSpacing + + Text { + Layout.preferredWidth: root.colLocal + text: modelData.localAddress + ":" + modelData.localPort + font.pixelSize: 12 + color: theme.textPrimary + elide: Text.ElideRight + } + Text { + Layout.preferredWidth: root.colRemote + text: modelData.remoteAddress + ":" + modelData.remotePort + font.pixelSize: 12 + color: theme.textPrimary + elide: Text.ElideRight + } + Text { + Layout.preferredWidth: root.colState + text: modelData.state + font.pixelSize: 12 + color: theme.textSecondary + elide: Text.ElideRight + } + Text { + Layout.fillWidth: true + // 进程名可能为空(反查失败),显示占位符避免空白行 + text: modelData.processName.length > 0 ? modelData.processName : "-" + font.pixelSize: 12 + color: modelData.processName.length > 0 ? theme.textPrimary : theme.textTertiary + elide: Text.ElideRight + } + } + + MouseArea { + id: rowMouse + anchors.fill: parent + hoverEnabled: true + } + } + } + + // 表头与底部汇总间的分隔线 + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 1 + Layout.leftMargin: 16 + Layout.rightMargin: 16 + color: theme.lineColor + } + + // 底部汇总行:连接总数 + RowLayout { + Layout.fillWidth: true + Layout.preferredHeight: 34 + Layout.leftMargin: 24 + Layout.rightMargin: 16 + spacing: root.colSpacing + + Text { + Layout.fillWidth: true + text: qsTr("Total: ") + root.connCount + qsTr(" connections") + font.pixelSize: 12 + font.weight: Font.Bold + color: theme.textPrimary + } + } + } + } + + // 数据刷新:C++ 每 5 秒更新并发射 tcpConnectionListChanged,据此重建清单; + // 本窗口自身的 Timer 兜底,避免依赖信号时序 + Connections { + target: root.applet + function onTcpConnectionListChanged() { + refreshModel() + } + } +} diff --git a/package/networkview.qml b/package/networkview.qml index ea5b416..055eb5c 100644 --- a/package/networkview.qml +++ b/package/networkview.qml @@ -384,6 +384,16 @@ AppletItem { } } + // TCP 连接清单:屏幕居中独立窗口,展示当前所有 ESTABLISHED 状态的 TCP 连接 + Platform.MenuItem { + text: qsTr("TCP Connections") + onTriggered: { + tcpConnectionsWindow.show() + tcpConnectionsWindow.raise() + tcpConnectionsWindow.requestActivate() + } + } + Platform.MenuSeparator {} Platform.MenuItem { @@ -426,6 +436,16 @@ AppletItem { isDarkMode: root.isDarkMode } + // TCP 连接清单窗口:屏幕居中独立窗口,展示当前所有 ESTABLISHED 状态的 TCP 连接 + // 依赖通过属性传入:accentColor = root.accentRed,applet = root.applet, + // isDarkMode 取自上方 DockPalette 检测结果 + TcpConnectionsWindow { + id: tcpConnectionsWindow + accentColor: root.accentRed + applet: root.applet + isDarkMode: root.isDarkMode + } + // 设置窗口:独立顶层窗口,在桌面中间弹出 // 设计原因:Dialog 使用父窗口(dock layer-surface)的 overlay,仅覆盖任务栏区域, // 无法在桌面中间显示。改用 Window 创建独立顶层窗口,可在桌面任意位置弹出。 diff --git a/src/networkmonitorapplet.cpp b/src/networkmonitorapplet.cpp index c3b85a4..a39ad92 100644 --- a/src/networkmonitorapplet.cpp +++ b/src/networkmonitorapplet.cpp @@ -285,6 +285,12 @@ QJsonObject NetworkMonitorApplet::trafficLog() const return m_trafficLog; } +// 返回 TCP 连接详情列表(QVariantList of QVariantMap),供 QML 连接清单窗口展示 +QVariantList NetworkMonitorApplet::tcpConnectionList() const +{ + return m_tcpConnectionList; +} + // 返回当前活动接口的下行速度历史,转换为 QVariantList of QPointF 供 QML 使用 // 设计原因:QPointF 仅携带 x/y 两个值,上传与下载分别对应两个列表; // 结果缓存于成员变量,仅当 m_historyDirty 时重建,避免 QML 每次读取都重新构造 300 个点 @@ -500,6 +506,11 @@ void NetworkMonitorApplet::readNetworkStats() m_lowFreqCounter = 0; detectLowFreqStats(); } + // TCP 连接清单同样每 5 秒更新一次(进程名反查需遍历 /proc 开销大,不能每秒执行) + if (++m_tcpListCounter >= 5) { + m_tcpListCounter = 0; + buildTcpConnectionList(); + } } void NetworkMonitorApplet::calculateSpeed() @@ -810,6 +821,191 @@ int NetworkMonitorApplet::countTcpConnections() const return count; } +// 构建 TCP 连接详情列表(解析 /proc/net/tcp 与 tcp6,含进程名反查) +// 结果存入 m_tcpConnectionList(QVariantList of QVariantMap)并发射 tcpConnectionListChanged; +// 仅保留 ESTABLISHED(状态 01)连接。优雅降级:文件缺失/行格式异常时跳过该文件/行,不崩溃 +// 设计原因:进程名反查需遍历 /proc//fd 开销较大,故本方法由 readNetworkStats 降频 +// 至每 5 秒调用一次;先一次性构建 inode -> pid 映射,再为每个连接查映射,避免每连接重复遍历 +void NetworkMonitorApplet::buildTcpConnectionList() +{ + QVariantList list; + const QStringList paths = {QStringLiteral("/proc/net/tcp"), QStringLiteral("/proc/net/tcp6")}; + + // 一次性遍历 /proc 构建 socket inode -> pid 映射,供所有连接复用 + // 设计原因:连接多时遍历 /proc 是主要开销,先建映射再查询可将 O(连接数×进程数) + // 降为 O(进程数 + 连接数),且映射缓存至下次刷新 + const QHash inodePidMap = buildInodePidMap(); + + for (const QString &path : paths) { + const bool isV6 = path.endsWith(QStringLiteral("tcp6")); + QFile f(path); + if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) { + continue; + } + QTextStream stream(&f); + stream.readLine(); // 跳过标题行 + while (!stream.atEnd()) { + const QString line = stream.readLine().trimmed(); + if (line.isEmpty()) { + continue; + } + TcpConnectionInfo conn; + if (!parseTcpLine(line, isV6, conn)) { + continue; // 行格式异常,跳过 + } + // 进程名反查:parseTcpLine 已将 socket inode 暂存于 conn.state,取出后覆盖为状态文本 + const qint64 inode = conn.state.toLongLong(); + const auto it = inodePidMap.constFind(inode); + if (it != inodePidMap.constEnd()) { + conn.processName = processNameForInode(*it); + } + conn.state = QStringLiteral("ESTABLISHED"); + + QVariantMap item; + item[QStringLiteral("localAddress")] = conn.localAddress; + item[QStringLiteral("localPort")] = conn.localPort; + item[QStringLiteral("remoteAddress")] = conn.remoteAddress; + item[QStringLiteral("remotePort")] = conn.remotePort; + item[QStringLiteral("state")] = conn.state; + item[QStringLiteral("processName")] = conn.processName; + list.append(item); + } + f.close(); + } + + m_tcpConnectionList = list; + emit tcpConnectionListChanged(); +} + +// 解析单个 /proc/net/tcp 或 tcp6 行,成功返回 true 并填充 conn +// 列格式:sl local_address:local_port rem_address:rem_port st tx_queue:rx_queue ... +// local_address/rem_address 为十六进制 IP,端口为十六进制;st=01 为 ESTABLISHED +// 设计原因:仅保留 ESTABLISHED 状态(与 countTcpConnections 口径一致); +// conn.state 字段在建立映射时暂存 socket inode(第 10 列),随后由 buildTcpConnectionList +// 覆盖为状态文本,避免再增一个中间字段 +bool NetworkMonitorApplet::parseTcpLine(const QString &line, bool isV6, TcpConnectionInfo &conn) const +{ + const QStringList parts = line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts); + // 至少需要 10 列(含 inode),不足视为格式异常 + if (parts.size() < 10) { + return false; + } + // 状态码:第 4 列(索引 3),仅保留 ESTABLISHED(01) + if (parts[3] != QLatin1String("01")) { + return false; + } + + // 本地地址:端口(索引 1) + const QStringList local = parts[1].split(QLatin1Char(':')); + if (local.size() != 2) { + return false; + } + // 远程地址:端口(索引 2) + const QStringList remote = parts[2].split(QLatin1Char(':')); + if (remote.size() != 2) { + return false; + } + + conn.localAddress = hexToIp(local[0], isV6); + conn.localPort = local[1].toInt(nullptr, 16); // 端口为十六进制 + conn.remoteAddress = hexToIp(remote[0], isV6); + conn.remotePort = remote[1].toInt(nullptr, 16); + + // 暂存 socket inode(第 10 列,索引 9)供进程名反查;随后会被覆盖为状态文本 + conn.state = parts[9]; + conn.processName.clear(); + return true; +} + +// 将十六进制地址转换为可读 IP 字符串 +// IPv4(tcp):8 位十六进制,小端序按字节倒序 -> "A.B.C.D" +// IPv6(tcp6):32 位十六进制,大端序按 8 个 16 位组 -> "xxxx:xxxx:...:xxxx" +// 设计原因:/proc/net/tcp 的 IPv4 地址为小端字节序存储,需倒序; +// IPv6 则按大端序存储,直接分组即可。解析失败返回空串 +QString NetworkMonitorApplet::hexToIp(const QString &hex, bool isV6) const +{ + if (isV6) { + if (hex.size() != 32) { + return QString(); + } + QStringList groups; + for (int i = 0; i < 32; i += 4) { + groups << hex.mid(i, 4); + } + return groups.join(QLatin1Char(':')); + } + if (hex.size() != 8) { + return QString(); + } + // IPv4 小端序:每 2 位十六进制为一个字节,倒序拼接 + QStringList octets; + for (int i = 6; i >= 0; i -= 2) { + bool ok = false; + const int oct = hex.mid(i, 2).toInt(&ok, 16); + octets << (ok ? QString::number(oct) : QStringLiteral("0")); + } + return octets.join(QLatin1Char('.')); +} + +// 遍历 /proc//fd 构建 socket inode -> pid 映射 +// 设计原因:进程名反查需匹配连接的 socket inode 与进程的 fd 链接(socket:[inode]); +// 一次性遍历所有进程建立映射,供多个连接复用,避免对每个连接重复扫描 /proc +QHash NetworkMonitorApplet::buildInodePidMap() const +{ + QHash map; + QDir procDir(QStringLiteral("/proc")); + if (!procDir.exists()) { + return map; + } + const QStringList entries = procDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + for (const QString &pidStr : entries) { + // 仅处理纯数字目录(进程 PID) + bool ok = false; + const qint64 pid = pidStr.toLongLong(&ok); + if (!ok || pid <= 0) { + continue; + } + QDir fdDir(QStringLiteral("/proc/%1/fd").arg(pidStr)); + if (!fdDir.exists()) { + continue; // 进程可能已退出或无权限 + } + const QStringList fds = fdDir.entryList(QDir::NoDotAndDotDot); + for (const QString &fd : fds) { + // fd 为符号链接,symLinkTarget 返回其目标(如 "socket:[12345]") + const QString link = fdDir.absoluteFilePath(fd); + const QString resolved = QFile::symLinkTarget(link); + if (resolved.isEmpty() || !resolved.startsWith(QStringLiteral("socket:["))) { + continue; + } + // 提取 inode 数字 + const int start = resolved.indexOf(QLatin1Char('[')); + const int end = resolved.indexOf(QLatin1Char(']')); + if (start < 0 || end <= start) { + continue; + } + const qint64 inode = resolved.mid(start + 1, end - start - 1).toLongLong(); + if (inode > 0) { + map.insert(inode, pid); + } + } + } + return map; +} + +// 根据 socket inode 的 pid 反查进程名(读 /proc//comm),失败返回空串 +// 设计原因:comm 为进程可执行文件名(不含路径与参数),适合清单展示; +// 进程可能已退出导致读取失败,此时返回空串不影响其他列显示 +QString NetworkMonitorApplet::processNameForInode(qint64 pid) const +{ + QFile comm(QStringLiteral("/proc/%1/comm").arg(pid)); + if (!comm.open(QIODevice::ReadOnly | QIODevice::Text)) { + return QString(); + } + const QString name = QString::fromUtf8(comm.readAll()).trimmed(); + comm.close(); + return name; +} + // 读取指定接口的 WiFi 信号强度(dBm) // 仅无线接口(wlp/wlan)有效;/proc/net/wireless 桌面环境可能不存在(返回 0) // 该文件每行形如 "wlp3s0: 60. -65. -256 0 ...",level(dBm)为第 3 个数值字段 diff --git a/src/networkmonitorapplet.h b/src/networkmonitorapplet.h index 49d6e95..dfa13cd 100644 --- a/src/networkmonitorapplet.h +++ b/src/networkmonitorapplet.h @@ -17,6 +17,8 @@ #include #include #include +#include +#include DS_BEGIN_NAMESPACE @@ -41,6 +43,19 @@ struct SpeedSample { double uploadSpeed; }; +// 单个 TCP 连接信息,供 QML 展示连接清单 +// localAddress/remoteAddress 为可读的 IP 地址字符串(已从十六进制转换) +// localPort/remotePort 为十进制端口号 +// processName 为占用该连接的进程名(解析失败时为空串) +struct TcpConnectionInfo { + QString localAddress; + int localPort; + QString remoteAddress; + int remotePort; + QString state; // 状态文本,如 "ESTABLISHED" + QString processName; // 进程名,解析失败为空串 +}; + class NetworkMonitorApplet : public DApplet { Q_OBJECT @@ -92,6 +107,11 @@ class NetworkMonitorApplet : public DApplet // 保存频率:累加每秒执行(内存操作),写盘与通知 QML 降频(每 30 秒一次), // 避免每秒磁盘 IO 与 QML 重绘 Q_PROPERTY(QJsonObject trafficLog READ trafficLog NOTIFY trafficLogChanged) + // TCP 连接详情列表,供 QML 连接清单窗口展示 + // 每项为 TcpConnectionInfo(通过 QVariantList of QVariantMap 暴露给 QML) + // 仅包含 ESTABLISHED 状态连接,含进程名反查 + // 设计原因:进程名反查需遍历 /proc 开销较大,故降频至每 5 秒更新一次 + Q_PROPERTY(QVariantList tcpConnectionList READ tcpConnectionList NOTIFY tcpConnectionListChanged) public: explicit NetworkMonitorApplet(QObject *parent = nullptr); @@ -138,6 +158,8 @@ public: void setTextColor(const QString &color); // 返回流量统计日志(JSON 对象),结构见 trafficLog 属性注释,供 QML 流量统计窗口读取 QJsonObject trafficLog() const; + // 返回 TCP 连接详情列表(QVariantList of QVariantMap),供 QML 连接清单窗口展示 + QVariantList tcpConnectionList() const; // 定时器回调,由 m_refreshTimer 每秒调用。非 Q_INVOKABLE(QML 不应直接调用, // 否则会打破 1 秒定时间隔导致速度计算失真) @@ -167,6 +189,8 @@ signals: void textColorChanged(); // 流量统计日志变化时通知 QML 更新(降频,每 30 秒发射一次) void trafficLogChanged(); + // TCP 连接详情列表变化时通知 QML 更新(降频,每 5 秒发射一次) + void tcpConnectionListChanged(); private: void readNetworkStats(); @@ -182,6 +206,17 @@ private: int parseIwBitrate(const QString &output) const; // 统计 /proc/net/tcp 与 tcp6 中 ESTABLISHED(状态 01)的连接数 int countTcpConnections() const; + // 构建 TCP 连接详情列表(解析 /proc/net/tcp 与 tcp6,含进程名反查) + // 结果存入 m_tcpConnectionList 并发射 tcpConnectionListChanged;仅 ESTABLISHED + void buildTcpConnectionList(); + // 解析单个 /proc/net/tcp 或 tcp6 行的连接信息,格式异常时返回无效标志 + bool parseTcpLine(const QString &line, bool isV6, TcpConnectionInfo &conn) const; + // 将十六进制地址(IPv4 或 IPv6)转换为可读 IP 字符串 + QString hexToIp(const QString &hex, bool isV6) const; + // 遍历 /proc//fd 构建 socket inode -> pid 映射(一次性遍历,供进程名反查) + QHash buildInodePidMap() const; + // 根据 socket inode 反查进程名,未找到或读取失败时返回空串 + QString processNameForInode(qint64 inode) const; // 读取指定接口的 WiFi 信号强度(dBm),非无线接口或不可用时返回 0 int detectWifiSignal(const QString &iface); // 判断是否为物理网卡(无线 wlp/wlan,有线 enp/eth), @@ -291,6 +326,17 @@ private: // 按日记录最多保留 90 天、按月最多保留 24 个月,超出自动裁剪最旧记录 static constexpr int MAX_DAY_ENTRIES = 90; static constexpr int MAX_MONTH_ENTRIES = 24; + + // ---- TCP 连接清单成员 ---- + // TCP 连接详情列表(QVariantList of QVariantMap),供 QML 连接清单窗口读取 + // 设计原因:QVariantMap 便于 QML 以 JS 对象直接访问各字段, + // 不直接暴露 TcpConnectionInfo 结构体(需注册元类型,QML 访问繁琐) + QVariantList m_tcpConnectionList; + // TCP 连接清单降频计数器:进程名反查需遍历 /proc 开销大,每 5 次 refresh 更新一次 + // 初始化 4 使首次 refresh 立即更新 + int m_tcpListCounter = 4; }; -DS_END_NAMESPACE \ No newline at end of file +DS_END_NAMESPACE + +Q_DECLARE_METATYPE(ds::TcpConnectionInfo) \ No newline at end of file