简介:面向Linux平台C++/Qt开发者的一份Wi-Fi连接程序实践源码,聚焦在Ubuntu 16.04环境下借助Qt网络模块与NetworkManager、wpa_supplicant协作完成无线网络的扫描、选择与连接。资源共12个文件,以4个cpp源文件、3个h头文件、3个ui界面文件为主,另含1个pro工程文件与1个user配置,压缩包约9KB,体量轻巧便于直接导入Qt Creator编译调试。项目将界面与业务逻辑分离,主窗口负责Wi-Fi列表展示与密码输入,独立网络管理类封装扫描与激活流程,并借助信号槽机制同步连接状态与错误提示,适合想了解Qt GUI与Linux底层网络管理如何衔接的读者参考。已有4524人学习下载,可据此梳理扫描、认证、连接各环节的代码组织方式,作为课程设计或嵌入式联网界面的起步模板。
1. 一个连 WiFi 的小程序,为什么值得单独拆一遍
在 Linux 桌面上点一下图标就能连上无线网,这件事在图形环境里稀松平常,可一旦你要把它塞进自己的 Qt 应用里,麻烦就来了:NetworkManager 的 D-Bus 接口文档写得像天书,wpa_supplicant的配置文件格式又和命令行工具对不上,扫描结果里还混着一堆隐藏 SSID 和重复 AP。我见过不少做嵌入式面板、自助终端、工控 HMI 的同行,界面都画好了,卡在“怎么让程序自己把 WiFi 连上”这一步,最后退回调用nmcli解析字符串,稳定性全靠运气。
这份资源就是一个用 Qt 在 Linux 下连接 WiFi 的完整程序,走的是 NetworkManager 的 D-Bus 路线,不依赖外部命令行工具的输出解析。它适合三类人:做 Linux 桌面或嵌入式 Qt 应用的开发者、需要给设备配网界面的产品团队、以及想搞清楚 D-Bus 到底怎么在 Qt 里落地的人。下面我按“先跑起来、再改参数、最后避坑”的顺序,把它拆成能照着复现的步骤。
2. 环境准备与 D-Bus 接口选型:为什么不用 nmcli
2.1 两条技术路线的取舍
Linux 下让程序控制 WiFi,常见做法无非三种:直接调nmcli或iwconfig并解析标准输出、直接和wpa_supplicant的 ctrl_interface 通信、走 NetworkManager 的 D-Bus 接口。第一种最快出活,但输出格式随版本变,遇到中文 locale 或特殊字符 SSID 就翻车;第二种控制粒度最细,可你得自己管 supplicant 生命周期,桌面环境里还容易和 NetworkManager 抢网卡;第三种是这份资源选的路线,接口稳定、权限模型清晰,缺点是 D-Bus 的类型系统(a{sv}、ao、ay)对新手不友好。
我一般会这样判断:如果目标机器上装了 NetworkManager(绝大多数桌面发行版和不少嵌入式镜像都默认装),就走 D-Bus;如果是极简 rootfs 且只跑一个应用,才考虑直接管 supplicant。这份程序的前提就是 NetworkManager 在跑,所以第一步先确认它活着。
# 确认 NetworkManager 服务状态与版本 systemctl status NetworkManager --no-pager nmcli --version # 确认 D-Bus 上能看到的 NetworkManager 服务名 busctl list | grep -i networkmanagersystemctl status看的是服务是否 active,busctl list看的是 D-Bus 总线上有没有org.freedesktop.NetworkManager这个 well-known name。两条都通过,说明后面 Qt 里的QDBusInterface才有对象可连。如果busctl里看不到,先别怀疑代码,八成是服务没起或者你连错了 bus(system bus 而不是 session bus)。
2.2 Qt 工程需要哪些模块
这份程序用到的 Qt 模块不多,但少一个就编译不过。QT += core dbus是硬性的,界面部分按你项目实际来,纯后台配网服务可以不要 widgets。D-Bus 模块在部分发行版的 Qt 安装包里是单独的包,比如 Debian 系叫libqt5dbus5加开发头文件,缺了会在链接阶段报undefined reference to QDBusInterface。
# .pro 文件关键片段 QT += core dbus CONFIG += c++11 console TARGET = wifitool SOURCES += main.cpp \ wifimanager.cpp HEADERS += wifimanager.h这里console是为了先跑通逻辑,等 D-Bus 调用稳定了再挂界面。wifimanager.cpp里封装的就是对 NetworkManager 的调用,把 D-Bus 的脏活关在一个类里,界面层只发信号槽,这是我认为这份代码结构上最值得借鉴的一点——它没有把QDBusInterface散落在各个窗口里。
提示:如果你用的是 Qt6,
QT += dbus依然有效,但部分枚举和QDBusArgument的流操作符行为有细微差别,编译报错时优先检查头文件包含顺序。
2.3 权限:别让 polkit 在背后拦你
D-Bus 调通了不代表能改网络。NetworkManager 的AddAndActivateConnection、RequestScan这些方法受 polkit 策略约束,普通用户直接调会收到org.freedesktop.NetworkManager.NotAuthorized。桌面环境下通常有认证代理弹窗,但在无头的嵌入式设备或自启动服务里,没有代理就静默失败。
# 查看当前用户对网络管理的权限 nmcli general permissions # 临时验证:以 root 跑一次扫描,排除权限因素 sudo nmcli device wifi rescannmcli general permissions会列出org.freedesktop.NetworkManager.wifi.scan、...enable-disable-wifi等条目的 yes/no。如果全是 no,你的 Qt 程序再怎么调都是白搭。常见解法是给运行用户配一条 polkit 规则放行特定操作,而不是简单粗暴地整个程序 setuid——后者在带界面的程序里是血泪教训,环境变量和 X 授权都会出问题。
3. 扫描与连接的核心实现:把 D-Bus 调用翻译成 Qt 代码
3.1 拿到无线设备对象路径
NetworkManager 的 D-Bus 结构是分层的:根对象/org/freedesktop/NetworkManager上有GetDevices()返回所有设备路径,每个设备再暴露Device.Wireless接口做扫描。所以第一步不是直接扫,而是先筛出类型为无线的设备。
// 获取第一个无线网卡的 D-Bus 对象路径 QString WifiManager::findWirelessDevice() { QDBusInterface nm("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager", "org.freedesktop.NetworkManager", QDBusConnection::systemBus()); // GetDevices 返回 ao,即对象路径数组 QDBusReply<QList<QDBusObjectPath>> reply = nm.call("GetDevices"); if (!reply.isValid()) { qWarning() << "GetDevices failed:" << reply.error().message(); return QString(); } for (const QDBusObjectPath &path : reply.value()) { QDBusInterface dev("org.freedesktop.NetworkManager", path.path(), "org.freedesktop.NetworkManager.Device", QDBusConnection::systemBus()); // DeviceType == 2 表示 WiFi(1 是有线) uint type = dev.property("DeviceType").toUInt(); if (type == 2) { return path.path(); } } return QString(); }这段的关键在DeviceType这个属性,2是 NM_DEVICE_TYPE_WIFI 的固定值,别去猜。QDBusReply的isValid()一定要判,D-Bus 调用失败不会抛异常,只会让 reply 无效,不判就会拿着空路径继续往下走,最后报一个和真实原因八竿子打不着的错。返回的路径形如/org/freedesktop/NetworkManager/Devices/3,后面的扫描和连接都基于它。
3.2 触发扫描并解析 AP 列表
扫描是异步的:调RequestScan只是发起请求,结果要通过AccessPointAdded信号或轮询GetAccessPoints拿。这份程序用的是“发起后等一小段再取列表”的朴素做法,够用但要注意时序。
// 发起扫描并读取当前 AP 列表 void WifiManager::scan() { QString devPath = findWirelessDevice(); if (devPath.isEmpty()) return; QDBusInterface wireless("org.freedesktop.NetworkManager", devPath, "org.freedesktop.NetworkManager.Device.Wireless", QDBusConnection::systemBus()); // 参数是 a{sv},这里传空字典即可 QVariantMap options; wireless.call("RequestScan", options); // 给驱动一点时间,嵌入式网卡尤其慢 QThread::msleep(1500); QDBusReply<QList<QDBusObjectPath>> aps = wireless.call("GetAccessPoints"); for (const QDBusObjectPath &ap : aps.value()) { QDBusInterface apIf("org.freedesktop.NetworkManager", ap.path(), "org.freedesktop.NetworkManager.AccessPoint", QDBusConnection::systemBus()); QString ssid = QString::fromUtf8( apIf.property("Ssid").toByteArray()); int strength = apIf.property("Strength").toInt(); // 过滤空 SSID(隐藏网络)和重复项 if (!ssid.isEmpty()) { emit apFound(ssid, strength); } } }Ssid属性是ay(字节数组),必须用toByteArray()再fromUtf8,直接toString()会得到乱码,这是新手最容易踩的一个坑。Strength是 0–100 的信号强度,用来排序。msleep(1500)是经验值,桌面网卡 500ms 就够,但一些 USB 无线模块扫描一轮要两三秒,固定等待在慢设备上会漏 AP,更稳的做法是监听AccessPointAdded信号做去重收集,等LastScan属性变化后再结束。
3.3 组装连接参数并激活
连接的核心是AddAndActivateConnection,它接收一个a{sa{sv}}的连接配置和一个a{sv}的额外参数,返回连接路径和活动连接路径。配置里最关键是802-11-wireless段的ssid和security,以及802-11-wireless-security段的密钥管理方式。
// 连接指定 SSID(WPA2-PSK) void WifiManager::connectTo(const QString &ssid, const QString &password) { QString devPath = findWirelessDevice(); QVariantMap wireless; wireless["ssid"] = ssid.toUtf8(); QVariantMap security; if (!password.isEmpty()) { wireless["security"] = "802-11-wireless-security"; security["key-mgmt"] = "wpa-psk"; security["psk"] = password; } QVariantMap conn; conn["802-11-wireless"] = wireless; if (!password.isEmpty()) conn["802-11-wireless-security"] = security; conn["connection"] = QVariantMap{ {"id", ssid}, {"type", "802-11-wireless"} }; QDBusInterface nm("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager", "org.freedesktop.NetworkManager", QDBusConnection::systemBus()); QDBusMessage msg = nm.call("AddAndActivateConnection", QVariant::fromValue(conn), QVariant::fromValue(QDBusObjectPath(devPath)), QVariant::fromValue(QDBusObjectPath("/"))); if (msg.type() == QDBusMessage::ErrorMessage) { qWarning() << "connect failed:" << msg.errorMessage(); } }三个参数分别是连接配置、设备路径、特定对象路径(传/表示自动选 AP)。ssid用toUtf8()转字节数组,和读取时对称。key-mgmt除了wpa-psk,开放网络不填 security 段,企业级 WPA-EAP 则要补eap、identity、password等字段,这份程序只覆盖了家用场景,企业认证得自己扩。调用返回的是QDBusMessage而不是QDBusReply,因为返回类型是复合结构,用msg.type()判错更直接。
4. 避坑与排查:五个真实翻车现场
4.1 现象:扫描永远返回空列表
原因通常是权限或设备路径拿错。先跑nmcli general permissions确认wifi.scan是 yes;再确认findWirelessDevice()返回的不是空——有些机器同时有多个无线设备,取第一个可能是个被 rfkill 关掉的。解决:在扫描前检查WirelessEnabled属性,为 false 时先调Enable(true),并打印设备路径核对。
4.2 现象:连接成功但拿不到 IP
AddAndActivateConnection返回成功只代表配置被接受,DHCP 是 NetworkManager 异步做的。如果程序紧接着就去请求网络,会拿到Network unreachable。解决:监听org.freedesktop.NetworkManager的StateChanged信号,等到NM_STATE_CONNECTED_GLOBAL(值 70)再继续业务逻辑,别用固定 sleep 赌。
4.3 现象:中文或特殊字符 SSID 变乱码
读取时用了toString()而不是fromUtf8(toByteArray()),或者写入时用了QString直接塞进QVariantMap。SSID 在 D-Bus 里是ay,两端都必须按字节数组处理。解决:统一封装两个转换函数,读fromUtf8、写toUtf8,全项目只走这一对。
4.4 现象:程序在桌面能跑,开机自启就失败
自启动时 D-Bus system bus 可能还没就绪,或者运行用户没有 polkit 授权代理。解决:启动脚本里先busctl list轮询等待 NetworkManager 出现,再拉起程序;权限问题用 polkit 规则针对具体 action 放行,别整个程序提权。
4.5 现象:重复连接后旧配置越积越多
每次AddAndActivateConnection都会新建一个连接配置,反复调试后nmcli connection show里一堆同名条目。解决:连接前先用Settings.ListConnections按 SSID 查已有配置,存在就复用其路径调ActivateConnection,不存在再新建;调试阶段也可以定期nmcli connection delete清理。
5. 进阶:把连接状态做成可观测的
跑通连接只是及格线,真正上线时你需要知道“现在到底连没连上、信号多少、走的哪个 AP”。这份程序里我加了一段状态订阅,把 NetworkManager 的StateChanged和活动连接的AccessPoint属性串起来,界面上的状态灯就不再是玄学。
// 订阅全局状态变化,映射成可读状态 QDBusConnection::systemBus().connect( "org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager", "org.freedesktop.NetworkManager", "StateChanged", this, SLOT(onNmStateChanged(uint))); void WifiManager::onNmStateChanged(uint state) { // 70 = CONNECTED_GLOBAL, 40 = CONNECTED_SITE, 30 = CONNECTED_LOCAL if (state == 70) { QDBusInterface active("org.freedesktop.NetworkManager", "/org/freedesktop/NetworkManager", "org.freedesktop.NetworkManager", QDBusConnection::systemBus()); // ActiveConnections 是活动连接路径数组 auto conns = active.property("ActiveConnections") .value<QList<QDBusObjectPath>>(); // 取第一个活动连接的具体 AP 做展示 emit networkReady(conns.size()); } }StateChanged的数值含义建议直接对照 NetworkManager 的枚举,别记混:20 是 disconnected,30/40/70 是三级“已连接”,只有 70 才代表真正能出网。ActiveConnections属性返回ao,用value<QList<QDBusObjectPath>>()取,取不到多半是QVariant类型没注册,需要在main里qDBusRegisterMetaType。
验证方法我一般分三步走:先用nmcli device status看设备状态是否connected,再用ip addr show确认网卡拿到了地址,最后ping一个外网地址确认路由通。三步都过,才敢说这个连接逻辑是稳的。参数上,RequestScan的等待时间、StateChanged的超时阈值这两个值最需要按你的硬件调,桌面网卡和工业模块能差出好几倍。
从那以后我每次把这类配网逻辑交给别人之前,都会强制自己在一台“干净”的机器上从零跑一遍扫描、连接、断线重连,因为 D-Bus 的坑从来不在代码里,而在环境里。希望这份拆解能帮到你,少走我当年解析nmcli输出解析到怀疑人生的弯路。
本文还有配套的精品资源,点击获取