QSocketNotifier 完全指南
一句话:QSocketNotifier 是 Qt 给"原生 socket / 文件描述符"配的翻译器------把操作系统的"可读/可写"事件,翻译成 Qt 的信号
activated(),从而接进 Qt 的事件循环。适用:Qt 5.x / Qt 6.x。示例代码均为自写、可独立编译,不依赖任何项目。
文章目录
- [QSocketNotifier 完全指南](#QSocketNotifier 完全指南)
-
- [1. 先看结论:速查卡](#1. 先看结论:速查卡)
- [2. 它到底是什么](#2. 它到底是什么)
-
- [2.1 文件描述符(fd)是什么](#2.1 文件描述符(fd)是什么)
- [2.2 QSocketNotifier 干的事](#2.2 QSocketNotifier 干的事)
- [3. 为什么需要它(和 QUdpSocket 的关系)](#3. 为什么需要它(和 QUdpSocket 的关系))
-
- [3.1 正常的 Qt 网络编程](#3.1 正常的 Qt 网络编程)
- [3.2 什么时候必须用 QSocketNotifier](#3.2 什么时候必须用 QSocketNotifier)
- [3.3 不要做的事](#3.3 不要做的事)
- [4. 工作原理](#4. 工作原理)
-
- [4.1 Qt 事件循环](#4.1 Qt 事件循环)
- [4.2 三种类型](#4.2 三种类型)
- [4.3 水平触发(Level-triggered)](#4.3 水平触发(Level-triggered))
- [4.4 触发时序](#4.4 触发时序)
- [5. 完整 API](#5. 完整 API)
-
- [5.1 构造](#5.1 构造)
- [5.2 信号](#5.2 信号)
- [5.3 其他成员](#5.3 其他成员)
- [5.4 Qt5 / Qt6 差异速查](#5.4 Qt5 / Qt6 差异速查)
- [6. 最小可运行示例](#6. 最小可运行示例)
-
- [6.1 监听一个原生 UDP socket(跨平台)](#6.1 监听一个原生 UDP socket(跨平台))
- [6.2 监听标准输入(Unix)](#6.2 监听标准输入(Unix))
- [6.3 多个 fd:一个 notifier 一个](#6.3 多个 fd:一个 notifier 一个)
- [7. 封装成类的标准模板](#7. 封装成类的标准模板)
-
- [7.1 生命周期图(必须记住)](#7.1 生命周期图(必须记住))
- [8. 正确的读写模式(重点)](#8. 正确的读写模式(重点))
-
- [8.1 读:一次激活,读到空](#8.1 读:一次激活,读到空)
- [8.2 写:发完就关,缺数据再开](#8.2 写:发完就关,缺数据再开)
- [8.3 临时关闭监听](#8.3 临时关闭监听)
- [9. 常见坑清单(重点)](#9. 常见坑清单(重点))
-
- [坑 1:先关 fd,再删 notifier](#坑 1:先关 fd,再删 notifier)
- [坑 2:一次性读,不排空](#坑 2:一次性读,不排空)
- [坑 3:没有事件循环,notifier 永远不触发](#坑 3:没有事件循环,notifier 永远不触发)
- [坑 4:在其他线程里用](#坑 4:在其他线程里用)
- [坑 5:Windows 上 socket 被改成非阻塞](#坑 5:Windows 上 socket 被改成非阻塞)
- [坑 6:Windows 上的"虚假激活"](#坑 6:Windows 上的"虚假激活")
- [坑 7:Windows 上 notifier 的启用/禁用行为不同](#坑 7:Windows 上 notifier 的启用/禁用行为不同)
- [坑 8:`setSocket()` 的副作用(Qt 6.1+)](#坑 8:
setSocket()的副作用(Qt 6.1+)) - [坑 9:fd 的类型宽度](#坑 9:fd 的类型宽度)
- [坑 10:同一个 fd 装两个同类型 notifier](#坑 10:同一个 fd 装两个同类型 notifier)
- [坑 11:把普通文件 fd 交给它](#坑 11:把普通文件 fd 交给它)
- [坑 12:忘了排空 UDP 缓冲区](#坑 12:忘了排空 UDP 缓冲区)
- [坑 13:在槽里长时间阻塞](#坑 13:在槽里长时间阻塞)
- [坑 14:析构顺序](#坑 14:析构顺序)
- [10. 调试手段](#10. 调试手段)
-
- [10.1 确认 fd 是不是真的可读](#10.1 确认 fd 是不是真的可读)
- [10.2 确认事件到底有没有到](#10.2 确认事件到底有没有到)
- [10.3 常见现象对照表](#10.3 常见现象对照表)
- [10.4 最简单的自测](#10.4 最简单的自测)
- [11. 它和替代方案的对比](#11. 它和替代方案的对比)
- [12. 真实项目中的典型用法](#12. 真实项目中的典型用法)
-
- [12.1 槽函数里做的事](#12.1 槽函数里做的事)
- [12.2 停止时的顺序(和本文 7.1 一致)](#12.2 停止时的顺序(和本文 7.1 一致))
- [12.3 一个值得留意的细节](#12.3 一个值得留意的细节)
- [13. 一页总结](#13. 一页总结)
- 参考
1. 先看结论:速查卡
cpp
// 1) 拿到一个原生 fd(qintptr)
int fd = ...; // 例如 ::socket(...) 或 ::open(...)
// 2) 造 notifier,指定事件类型
auto *n = new QSocketNotifier(fd, QSocketNotifier::Read, parent);
// 3) 连信号
connect(n, &QSocketNotifier::activated, this, &MyClass::onReadable);
// 4) 槽里非阻塞地读,并且**读空为止**
// 5) 销毁顺序:先 delete notifier,再 close(fd)
| 事项 | 结论 |
|---|---|
| 构造参数 | qintptr socket,类型 Read/Write/Exception,父对象 |
| 触发信号 | activated(QSocketDescriptor, QSocketNotifier::Type)(Qt6) |
| 是否拥有 fd | 不拥有 。它不会帮你 close() |
| 是否线程安全 | 否 。必须在创建它的线程里用,且该线程要有事件循环 |
| 一次激活 = 一条数据? | 不是。必须循环读到"读空" |
| 能不能监听普通文件? | 不能(会一直"可读",导致 100% CPU) |
| Windows 上能用吗? | 能,但 socket 会被改成非阻塞,且可能"虚假激活" |
| 和 QUdpSocket 冲突吗? | 给 Qt 自己的 socket再装 notifier 会冲突,别混用 |
| 忘了删会怎样? | 父对象析构时会删;但先关 fd 再删 notifier会出问题 |
2. 它到底是什么
2.1 文件描述符(fd)是什么
在 Linux/macOS 上,几乎一切"能读能写的东西"都是一个整数编号------文件描述符(File Descriptor, fd):
- 文件 → fd
- socket(UDP/TCP)→ fd
- 串口
/dev/ttyUSB0→ fd - 管道、终端、标准输入 → fd 0/1/2
- 甚至 eventfd、timerfd、GPIO → fd
Windows 上对应的是 SOCKET / HANDLE,本质也是"一个不透明的编号"。
这些 fd 都是操作系统级别的 ,Qt 的信号槽系统天生不认识它们。
2.2 QSocketNotifier 干的事
操作系统: "fd 5 现在有数据了,可读"
│ (select/poll/epoll/kqueue)
▼
Qt 事件循环: 发现 fd 5 注册过 QSocketNotifier
│
▼
QSocketNotifier: emit activated()
│
▼
你的槽函数: 去 fd 5 上 recv() 把数据读走
它就是一个适配器(adapter):把"fd 就绪"这种底层事件,包装成 Qt 世界里的信号。
大白话:你有一个只会说"喂"的哑巴门铃(原生 fd),QSocketNotifier 是翻译官,把"喂"翻译成 Qt 听得懂的"叮咚(activated)"。
3. 为什么需要它(和 QUdpSocket 的关系)
3.1 正常的 Qt 网络编程
如果你用 Qt 自带的网络类:
cpp
QUdpSocket sock;
sock.bind(QHostAddress::Any, 9000);
connect(&sock, &QUdpSocket::readyRead, this, [&]{
while (sock.hasPendingDatagrams()) {
QByteArray d; d.resize(sock.pendingDatagramSize());
sock.readDatagram(d.data(), d.size());
// 处理 d
}
});
QUdpSocket 自带 readyRead() 信号,内部已经把 fd 接进了事件循环。这条路最简单,新代码优先用它。
3.2 什么时候必须用 QSocketNotifier
以下情况你拿不到 QUdpSocket,只有一个裸 fd:
| 场景 | 例子 |
|---|---|
| 第三方 C/C++ SDK 自己管 socket | SDK 只给你 int fd / native_handle() |
| 已有代码/团队统一用原生 socket | POSIX socket、Winsock |
| 串口 | open("/dev/ttyUSB0") 得到的 fd |
| 管道 / 进程间通信 | pipe()、socketpair() |
| 标准输入 | fd 0 |
| Linux 特殊 fd | eventfd、signalfd、timerfd、inotify |
| 需要极致的控制 | 自建 reactor、非标准协议 |
这时 QSocketNotifier 是唯一能把裸 fd 优雅接进 Qt 事件循环的官方手段(除此之外只能开线程阻塞读,或定时轮询)。
3.3 不要做的事
cpp
// ❌ 错误:给 Qt 自己的 socket 再套 notifier
QUdpSocket sock;
sock.bind(...);
auto *n = new QSocketNotifier(sock.socketDescriptor(), QSocketNotifier::Read);
Qt 内部已经在监听同一个 fd 了,再加一个 notifier 会导致重复读 、事件被抢走、行为诡异。要么用 QUdpSocket,要么用裸 fd + notifier,二选一。
4. 工作原理
4.1 Qt 事件循环
QCoreApplication::exec() 启动了一个事件循环,它大致是:
loop:
events = 等待(一堆事情: 定时器、窗口消息、socket、跨线程投递)
for e in events:
分发 e 到对应的接收者
"等待一堆事情"在 Linux 上靠 epoll/poll,在 macOS 上靠 kqueue,在 Windows 上靠 select/窗口消息机制。
QSocketNotifier 做的事就是:把自己的 fd 塞进这个"等待集合"。
4.2 三种类型
cpp
enum Type {
Read, // fd 有数据可读(收到包、管道有字节)
Write, // fd 可以写而不阻塞(发送缓冲区有空位)
Exception // 有"异常条件"(带外数据 OOB 等),注意:不是"出错了"
};
各自用途:
| 类型 | 典型用途 | 注意 |
|---|---|---|
Read |
收 UDP/TCP/串口/管道/ststdin | 必须排空 |
Write |
大块数据要分多次发、等待可写 | 发完立刻 setEnabled(false),否则 CPU 100% |
Exception |
TCP 带外数据 | 极少用;错误通常通过 SO_ERROR 或读写返回值获取 |
Exception≠ "socket 出错了"。它对应select()的exceptfds,是带外数据(OOB)。这是新手最常见的误解。
4.3 水平触发(Level-triggered)
Qt 在 Linux 上用事件循环的 poll/epoll,默认是水平触发:
只要 fd 里还有数据没读,下次事件循环就还会再通知你。
这意味着两件事:
- 不会丢事件:你这次少读一点,下次还会被叫醒。
- 一次激活不代表只有一条消息:可能积压了 N 个数据报,必须循环读空,否则每轮事件循环都触发一次,白白增加延迟。
而 Windows 上略有不同(见第 9 节),可能"虚假激活"------通知了但读不到东西,这是平台限制,不是 bug。
4.4 触发时序
[socket 收到 3 个 UDP 包]
↓
操作系统标记 fd 可读
↓
Qt 事件循环醒来
↓
emit activated()
↓
你的槽: recv() → recv() → recv() ← 读空
→ recv() 返回 -1/EAGAIN → break
↓
回到事件循环,继续等
5. 完整 API
5.1 构造
cpp
// 最常用
QSocketNotifier(qintptr socket, QSocketNotifier::Type type, QObject *parent = nullptr);
// Qt 6.1+: 先不指定 fd,稍后 setSocket()
QSocketNotifier(QSocketNotifier::Type type, QObject *parent = nullptr);
qintptr = "能装下指针的整数",在 64 位平台是 64 位。不要用 int 传 fd ,尤其 Windows 的 SOCKET 在 64 位下是 64 位句柄。
5.2 信号
cpp
// Qt 6 当前签名
void activated(QSocketDescriptor socket, QSocketNotifier::Type type);
// Qt 5 / 已废弃(5.15 起标记 obsolete)
void activated(int socket);
QSocketDescriptor 就是 qintptr 的别名。
连接方式(Qt 6 推荐):
cpp
// 只关心"有事件了",不关心参数 ------ 最省事,Qt5/Qt6 通用
connect(n, &QSocketNotifier::activated, this, &MyClass::onReadable);
// 需要参数 ------ 用 lambda
connect(n, &QSocketNotifier::activated, this,
[this](QSocketDescriptor fd, QSocketNotifier::Type t) {
// fd = 哪个socket, t = 哪种事件
});
项目里常见的写法就是连到无参槽函数,这样 Qt5/Qt6 都能编译,最省心。
5.3 其他成员
| 函数 | 说明 |
|---|---|
void setEnabled(bool) |
开关监听。false 等于"没装这个 notifier" |
bool isEnabled() const |
当前是否启用(默认 true) |
qintptr socket() const |
当前监听的 fd(Qt 6.1+) |
void setSocket(qintptr) |
换 fd(Qt 6.1+)。副作用:会把 notifier 置为 disabled,需要手动 setEnabled(true) |
Type type() const |
监听类型 |
bool isValid() const |
fd 是否有效(Qt 6.1+) |
5.4 Qt5 / Qt6 差异速查
| Qt 5 | Qt 6 | |
|---|---|---|
| 构造参数 | int / qintptr 都有 |
qintptr |
activated 参数 |
int |
QSocketDescriptor, Type |
socket() |
有 | 有(6.1+) |
setSocket() |
无 | 6.1+ |
| 连接无参槽 | ✅ | ✅ |
6. 最小可运行示例
6.1 监听一个原生 UDP socket(跨平台)
这是最贴近实际的例子:自己用系统 API 创建 UDP socket,交给 QSocketNotifier 驱动。
cpp
// udp_notifier_demo.cpp
// Linux/macOS (Qt6):
// g++ -std=c++20 -fPIC udp_notifier_demo.cpp -o demo \
// $(pkg-config --cflags --libs Qt6Core)
// Windows: 用 CMake,链接 Qt6::Core 和 ws2_32
// 运行: ./demo 9000
// 另开终端发数据: echo hello | nc -u -w1 127.0.0.1 9000
#include <QCoreApplication>
#include <QSocketNotifier>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#ifdef _WIN32
#include <winsock2.h>
#include <ws2tcpip.h>
using SocketHandle = SOCKET;
using SockLen = int;
static bool isBad(SocketHandle s) { return s == INVALID_SOCKET; }
static bool closeSock(SocketHandle s) { return ::closesocket(s) == 0; }
static bool netStartup() { WSADATA d{}; return WSAStartup(MAKEWORD(2, 2), &d) == 0; }
static void netCleanup() { WSACleanup(); }
#else
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <unistd.h>
using SocketHandle = int;
using SockLen = socklen_t;
static bool isBad(SocketHandle s) { return s < 0; }
static bool closeSock(SocketHandle s) { return ::close(s) == 0; }
static bool netStartup() { return true; }
static void netCleanup() {}
#endif
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
if (!netStartup()) {
std::fprintf(stderr, "network init failed\n");
return 1;
}
const int port = argc > 1 ? std::atoi(argv[1]) : 9000;
// ---------- 1. 创建原生 UDP socket 并绑定 ----------
const SocketHandle fd = ::socket(AF_INET, SOCK_DGRAM, 0);
if (isBad(fd)) {
std::fprintf(stderr, "socket() failed\n");
return 1;
}
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(static_cast<uint16_t>(port));
if (::bind(fd, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) != 0) {
std::fprintf(stderr, "bind(%d) failed\n", port);
closeSock(fd);
return 1;
}
std::printf("listening udp/%d (fd=%lld)\n", port,
static_cast<long long>(fd));
// ---------- 2. 交给 QSocketNotifier ----------
// 注意: 类型是 qintptr, 不是 int
auto *notifier = new QSocketNotifier(static_cast<qintptr>(fd),
QSocketNotifier::Read, &app);
// ---------- 3. 收到事件: 循环读空 ----------
QObject::connect(notifier, &QSocketNotifier::activated,
[fd](QSocketDescriptor /*which*/, QSocketNotifier::Type /*type*/) {
for (;;) {
char buf[65536];
sockaddr_in from{};
SockLen from_len = sizeof(from);
const int n = static_cast<int>(::recvfrom(
fd, buf, sizeof(buf) - 1, 0,
reinterpret_cast<sockaddr *>(&from), &from_len));
if (n <= 0) {
// 没有更多数据了。
// Windows 上这里可能是 WSAEWOULDBLOCK, 属正常, 不是错误。
break;
}
buf[n] = '\0';
char ip[INET_ADDRSTRLEN] = {};
inet_ntop(AF_INET, &from.sin_addr, ip, sizeof(ip));
std::printf("[%s:%u] %s\n", ip, ntohs(from.sin_port), buf);
std::fflush(stdout);
}
});
// ---------- 4. 退出: 先删 notifier, 再关 fd ----------
QObject::connect(&app, &QCoreApplication::aboutToQuit, [notifier, fd] {
delete notifier; // ① 先取消监听
closeSock(fd); // ② 再关闭 fd
netCleanup();
});
return app.exec();
}
验证方法:开两个终端
bash
./demo 9000
# 另一个终端
echo hello | nc -u -w1 127.0.0.1 9000
你会看到 [127.0.0.1:xxxx] hello。
6.2 监听标准输入(Unix)
这是 QSocketNotifier 的经典用法:不用 std::cin 阻塞,而是把 stdin 接进 Qt 事件循环。
cpp
// stdin_notifier_demo.cpp (Linux/macOS)
// g++ -std=c++20 -fPIC stdin_notifier_demo.cpp -o demo \
// $(pkg-config --cflags --libs Qt6Core)
#include <QCoreApplication>
#include <QSocketNotifier>
#include <cstdio>
#include <unistd.h>
int main(int argc, char **argv)
{
QCoreApplication app(argc, argv);
QSocketNotifier notifier(STDIN_FILENO, QSocketNotifier::Read);
QObject::connect(¬ifier, &QSocketNotifier::activated,
[](QSocketDescriptor, QSocketNotifier::Type) {
char buf[4096];
const ssize_t n = ::read(STDIN_FILENO, buf, sizeof(buf) - 1);
if (n > 0) {
buf[n] = '\0';
std::fputs(buf, stdout);
std::fflush(stdout);
} else if (n == 0) {
QCoreApplication::quit(); // EOF (Ctrl-D)
}
// n < 0: EAGAIN, 暂时没数据, 忽略
});
// 同时还能干别的事, 例如定时器, 完全不阻塞
return app.exec();
}
Windows 注意:控制台输入不是 socket,QSocketNotifier 在 Windows 上对 stdin 支持很差,不要用。Windows 下请用控制台 API 或干脆做成 GUI/命令行分线程。
6.3 多个 fd:一个 notifier 一个
QSocketNotifier 只能监听一个 fd。要监听 N 路,就创建 N 个:
cpp
struct Endpoint {
SocketHandle fd;
QSocketNotifier *notifier;
};
for (const int port : {8090, 8091, 8092}) {
SocketHandle fd = makeUdpSocket(port); // 你自己封装的创建+bind
auto *n = new QSocketNotifier(static_cast<qintptr>(fd),
QSocketNotifier::Read, this);
connect(n, &QSocketNotifier::activated, this,
[this, port, fd] { drainAndDispatch(port, fd); });
endpoints_.push_back({fd, n});
}
这和第 12 节项目里"三路通道装三个 notifier"是完全一样的模式。
7. 封装成类的标准模板
推荐把"创建 socket + 装 notifier + 正确销毁"封成一个类,避免到处复制粘贴。
cpp
// udp_endpoint.hpp
#pragma once
#include <QObject>
#include <QByteArray>
#include <QHostAddress>
#include <QSocketNotifier>
#include <cstdint>
class UdpEndpoint : public QObject
{
Q_OBJECT
public:
explicit UdpEndpoint(quint16 port, QObject *parent = nullptr)
: QObject(parent), port_(port) {}
~UdpEndpoint() override { stop(); }
bool start()
{
if (fd_ != kInvalid) return true;
fd_ = ::socket(AF_INET, SOCK_DGRAM, 0);
if (fd_ == kInvalid) return false;
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_addr.s_addr = htonl(INADDR_ANY);
addr.sin_port = htons(port_);
if (::bind(fd_, reinterpret_cast<sockaddr *>(&addr), sizeof(addr)) != 0) {
closeSocket(); // 绑定失败: 回收 socket, 不留半成品
return false;
}
notifier_ = new QSocketNotifier(static_cast<qintptr>(fd_),
QSocketNotifier::Read, this);
connect(notifier_, &QSocketNotifier::activated,
this, &UdpEndpoint::onReadable);
return true;
}
void stop()
{
// ★ 顺序不能反: 先删 notifier(取消监听), 再关 fd
if (notifier_) {
delete notifier_;
notifier_ = nullptr;
}
closeSocket();
}
signals:
void datagramReceived(const QByteArray &data, quint16 sourcePort);
private:
void onReadable()
{
for (;;) {
char buf[65536];
sockaddr_in from{};
socklen_t len = sizeof(from);
const int n = static_cast<int>(::recvfrom(
fd_, buf, sizeof(buf), 0,
reinterpret_cast<sockaddr *>(&from), &len));
if (n <= 0) break; // 读空 / EAGAIN
emit datagramReceived(QByteArray(buf, n), ntohs(from.sin_port));
}
}
void closeSocket()
{
if (fd_ != kInvalid) {
::close(fd_);
fd_ = kInvalid;
}
}
quint16 port_;
int fd_ = -1;
QSocketNotifier *notifier_ = nullptr;
};
CMake 里记得开
set(CMAKE_AUTOMOC ON),否则Q_OBJECT会链接失败。
7.1 生命周期图(必须记住)
start() stop() / 析构
│ │
├─ socket() → fd ├─ delete notifier ← 先
├─ bind(fd) ├─ close(fd) ← 后
└─ new QSocketNotifier(fd) └─ fd = -1
为什么顺序不能反?
- notifier 内部存着 fd 的副本并注册到了 Qt 的事件分发器;
- 如果先
close(fd),这个 fd 编号会立刻被系统回收,可能被别的 socket/文件复用; - 此时 notifier 还挂着这个编号,事件循环可能对着"新住户"触发回调,或者访问已失效的内部状态;
- 先删 notifier,事件分发器会主动注销这个 fd,之后关 fd 就安全了。
8. 正确的读写模式(重点)
8.1 读:一次激活,读到空
cpp
void onReadable()
{
for (;;) {
ssize_t n = ::recv(fd, buf, sizeof(buf), 0);
if (n > 0) {
handle(buf, n);
} else if (n == 0) {
// TCP: 对端关闭; UDP 一般不会返回 0
onPeerClosed();
break;
} else {
// EAGAIN / EWOULDBLOCK → 没数据了, 正常收工
// 其他 errno → 真错误, 记录并关闭
break;
}
}
}
为什么必须循环? 因为事件是"水平触发":一次通知可能对应 N 个数据报。只读一个就走,剩下的会在下一轮事件循环继续触发,延迟叠加、CPU 浪费。
8.2 写:发完就关,缺数据再开
写事件 notifier 是最容易被用错的地方:
cpp
// ❌ 错误: 一直开着 Write notifier
// 只要发送缓冲区有余量, fd 就一直"可写", 事件循环会疯狂空转 → CPU 100%
// ✅ 正确
connect(notifier_, &QSocketNotifier::activated, this, [this] {
notifier_->setEnabled(false); // 先关, 防止重入/空转
while (!outbuf_.isEmpty()) {
qint64 n = ::send(fd_, outbuf_.constData(), outbuf_.size(), 0);
if (n > 0) {
outbuf_.remove(0, n);
} else {
break; // EAGAIN: 缓冲区满了
}
}
if (!outbuf_.isEmpty()) {
notifier_->setEnabled(true); // 还有数据没发完, 继续等可写
}
});
规则:
| 场景 | 操作 |
|---|---|
| 要发数据时 | 先把数据塞进队列 → setEnabled(true) 让 notifier 开始等可写 |
activated 里 |
立刻 setEnabled(false),然后尽量发 |
| 发完了 | 保持 disabled |
| 没发完(EAGAIN) | setEnabled(true) 继续等 |
官方文档原话:写 notifier 应该在
activated()发出后立即禁用。
8.3 临时关闭监听
有时你希望"暂停接收",但又不想销毁 socket:
cpp
notifier->setEnabled(false); // 暂停: 事件不再进来, fd 还在, 数据在缓冲区里堆着
// ... 干别的事 ...
notifier->setEnabled(true); // 恢复
注意:setEnabled(false) 期间数据不会被读走 ,UDP 缓冲区满了之后新包会被内核丢弃。所以别关太久。
9. 常见坑清单(重点)
坑 1:先关 fd,再删 notifier
cpp
// ❌
close(fd);
delete notifier;
// ✅
delete notifier;
close(fd);
原因见 7.1。另外注意 delete 的时机:不要在 notifier 自己的槽函数里 delete this/notifier,用 deleteLater()。
坑 2:一次性读,不排空
cpp
// ❌ 只读一个包, 剩下的下一轮再触发
auto d = recv(...);
// ✅
while (auto d = recv(...)) { ... }
坑 3:没有事件循环,notifier 永远不触发
cpp
// ❌ 单元测试里常见
QSocketNotifier n(fd, QSocketNotifier::Read);
// 没有 app.exec() / processEvents(), 什么都不会发生
// ✅
QCoreApplication app(argc, argv);
// ... 或者手动
QCoreApplication::processEvents();
QEventLoop loop;
QTimer::singleShot(1000, &loop, &QEventLoop::quit);
loop.exec();
坑 4:在其他线程里用
QSocketNotifier 不是线程安全的,也不能跨线程工作:
cpp
// ❌ 在子线程创建 notifier, 却没有该线程的事件循环
std::thread([&]{
QSocketNotifier n(fd, QSocketNotifier::Read); // 不会触发
}).detach();
// ✅ 方案 A: 在子线程里跑 QEventLoop
// ✅ 方案 B: 在 GUI 线程创建 notifier, 子线程只负责写数据 + 信号唤醒
// ✅ 方案 C: 子线程里阻塞 recv, 然后用 Qt::QueuedConnection 把数据投递回 GUI 线程
判断标准:notifier 属于创建它的线程,那个线程必须转事件循环。
坑 5:Windows 上 socket 被改成非阻塞
文档明确说明:传给 QSocketNotifier 的 socket 会变成非阻塞,即使你创建时是阻塞的。带来的影响:
recv()没数据时返回SOCKET_ERROR+WSAEWOULDBLOCK,不是错误;- 必须用返回值判断,而不是假设"有激活就一定有数据"。
坑 6:Windows 上的"虚假激活"
官方原话大意:即使没有数据可读,activated() 有时也会因为系统整体活动而被触发。这是 Windows 平台限制(不是 Qt bug)。所以:
cpp
// 不要因为"激活了"就假设能读到数据
if (recv(...) < 0) {
// WSAEWOULDBLOCK 是正常情况, 静默返回即可
}
别在这里打错误日志,否则日志会被刷爆。
坑 7:Windows 上 notifier 的启用/禁用行为不同
文档:在 Windows 上,Qt 在通知之后总会把 notifier 禁用,只有在"还期望更多数据"时才重新启用。
实践建议:
- 读 notifier :正常循环读空即可,Qt 会处理重新启用;若遇到"只触发一次就再也不触发",在槽末尾显式
setEnabled(true)即可(对已启用状态调用是无害的)。 - 写 notifier:严格按 8.2 的模式手动控制。
坑 8:setSocket() 的副作用(Qt 6.1+)
cpp
n->setSocket(newFd); // ❗ 副作用: notifier 变成 disabled
n->setEnabled(true); // 必须手动重新启用
坑 9:fd 的类型宽度
cpp
// ❌ 危险: 用 int 存 fd
int fd = channel.native_handle(); // Windows 的 SOCKET 是 64 位句柄
// ✅
qintptr fd = static_cast<qintptr>(channel.native_handle());
如果某个 SDK 的 native_handle() 返回 int,而它在 Windows 上实际是 SOCKET 截断下来的,就存在句柄截断风险。跨平台项目里尤其要留意这个签名。
坑 10:同一个 fd 装两个同类型 notifier
cpp
// ❌ 未定义行为/事件被抢
new QSocketNotifier(fd, QSocketNotifier::Read);
new QSocketNotifier(fd, QSocketNotifier::Read);
// ✅ 一个 fd 只能有一个 Read notifier; 需要 Read+Write 就建两个不同类型
坑 11:把普通文件 fd 交给它
普通文件永远是"可读"的(不会阻塞),notifier 会不停地触发 → 事件循环空转 100% CPU。它只适合 socket、管道、串口、终端这类"会阻塞"的 fd。
坑 12:忘了排空 UDP 缓冲区
UDP 的接收缓冲区是有限的。如果处理太慢,或者 notifier 被 disabled 太久,新数据报会被内核直接丢弃(UDP 不重传)。监控类场景要注意处理速度。
坑 13:在槽里长时间阻塞
cpp
// ❌ 槽里做耗时操作, 整个 UI 卡住, 其他 notifier 也得不到处理
onReadable() {
cv::imwrite(...); // 几百毫秒
QThread::sleep(1);
}
事件循环是单线程的,槽函数里的耗时操作会阻塞所有事件。重活丢给工作线程,结果再用信号投递回来。
坑 14:析构顺序
如果 notifier 的父对象是 this,而 this 析构时先关了 fd,同样会踩坑 1。推荐:在 stop() / 析构函数里显式 delete notifier_,不要依赖父对象自动清理的时序。
10. 调试手段
10.1 确认 fd 是不是真的可读
cpp
std::fprintf(stderr, "notifier fired, fd=%lld\n",
static_cast<long long>(notifier->socket()));
10.2 确认事件到底有没有到
- Linux :
strace -e trace=network,poll,epoll_wait -p <pid> - 查看端口 :
ss -ulpn | grep 9000或netstat -ano | findstr 9000(Windows) - Windows:Process Explorer / Wireshark 看包有没有到
10.3 常见现象对照表
| 现象 | 可能原因 |
|---|---|
| 完全不触发 | 没进事件循环 / 在错误的线程 / fd 无效 / socket 没 bind 成功 |
| 只触发一次 | Windows 上 notifier 被禁用;或槽里异常退出/提前 return |
| 触发很频繁但读不到数据 | 普通文件 fd;或 Windows 虚假激活 |
| CPU 100% | Write notifier 一直开着;或监听普通文件 |
| 触发后程序崩溃 | fd 已被别处关闭/复用(坑 1) |
| 偶发丢包 | 没排空缓冲区 / UDP 缓冲太小 / 处理太慢 |
10.4 最简单的自测
用 nc 往端口发数据,看回调有没有打印------不要一上来就接真设备。
11. 它和替代方案的对比
| 方案 | 优点 | 缺点 | 适用 |
|---|---|---|---|
| QSocketNotifier | 零依赖、体积小、完全控制 fd、非阻塞 | 需要自己管 fd 生命周期和 framing | SDK 给的裸 fd、串口、管道 |
| QUdpSocket / QTcpSocket | 自带信号、跨平台封装好、有 readyRead |
只能用于 socket;与裸 fd 混用会冲突 | 新写的 Qt 网络代码 |
| 阻塞 recv + 工作线程 | 逻辑直观、容易写 | 需要线程同步;销毁线程麻烦;不能天然并入事件循环 | 数据量大、处理重 |
| 定时轮询(非阻塞 recv) | 实现最简单 | 延迟高、CPU 浪费、忙等 | 临时小工具 |
| Qt 的事件循环 + 原生 poll | 灵活 | 要自己实现事件源 | 极端定制 |
选择原则:
能用 Qt 网络类就用 Qt 网络类;被迫拿裸 fd 时,QSocketNotifier 是标准答案;重 CPU 处理交给工作线程。
12. 真实项目中的典型用法
下面只保留结构,参数做了简化,方便理解模式。
一个上位机程序通常要同时接收多路 UDP 数据,例如:
- 主控遥测(一个 socket)
- 声呐(另一个固定端口)
- DVL(又一个固定端口)
每一路都是独立的 fd,所以:
cpp
// 每一路一个 notifier
tel_notifier_ = new QSocketNotifier(tel.channel()->native_handle(),
QSocketNotifier::Read, this);
connect(tel_notifier_, &QSocketNotifier::activated,
this, &TelemetrySource::onTelReadable);
sonar_notifier_ = new QSocketNotifier(sonar_->native_handle(),
QSocketNotifier::Read, this);
connect(sonar_notifier_, &QSocketNotifier::activated,
this, &TelemetrySource::onSonarReadable);
dvl_notifier_ = new QSocketNotifier(dvl_->native_handle(),
QSocketNotifier::Read, this);
connect(dvl_notifier_, &QSocketNotifier::activated,
this, &TelemetrySource::onDvlReadable);
三路结构完全一样,只是 fd 和槽函数不同。
12.1 槽函数里做的事
cpp
void TelemetrySource::onTelReadable()
{
while (true) {
auto frame = tel.recvPlaintext(); // 非阻塞; 读空返回 nullopt
if (!frame.has_value()) break; // ← 排空
parser_.feed(*frame); // 喂给流式帧解析器
while (true) {
auto parsed = parser_.next_frame();
if (!parsed.has_value()) break;
dispatchFrame(*parsed); // 按类型分发
}
}
}
注意几个模式:
- 外层 while:排空 socket 缓冲区;
- 内层 while:UDP 一个包可能包含半帧/多帧,解析器要循环取完整帧;
- 解析与分发解耦 :notifier 只负责"有数据了",业务逻辑在
dispatchFrame。
12.2 停止时的顺序(和本文 7.1 一致)
cpp
void stop()
{
// 1. 先删除 notifier, 取消对 fd 的监听
delete tel_notifier_; tel_notifier_ = nullptr;
delete sonar_notifier_; sonar_notifier_ = nullptr;
delete dvl_notifier_; dvl_notifier_ = nullptr;
// 2. 再释放通道(内部会 close socket)
sonar_.reset();
dvl_.reset();
tel.stop();
}
这正是"先摘门铃,再拆门"的顺序。
12.3 一个值得留意的细节
如果底层库的 native_handle() 返回类型是 int,而 Qt6 的构造函数要 qintptr,编译期会自动转换,但 Windows 上会有句柄截断 隐患。自己写新代码时,尽量让 API 返回 qintptr/intptr_t。
13. 一页总结
核心心智模型
原生 fd ──注册──▶ QSocketNotifier ──activated──▶ 你的槽 ──非阻塞读写──▶ 业务
▲
│
Qt 事件循环(epoll/poll/select)
必背 6 条
- 一个 fd 一个 notifier;要 Read+Write 就建两个。
- 构造给
qintptr,类型选Read。 - 槽里循环读到空,一次激活可能有多条数据。
- 先删 notifier,再关 fd,顺序不能反。
- Write notifier 发完立刻
setEnabled(false),否则 CPU 100%。 - 必须在有事件循环的线程里用,它不拥有 fd、不负责拆包。
最小模板(手抄版)
cpp
auto *n = new QSocketNotifier(fd, QSocketNotifier::Read, parent);
connect(n, &QSocketNotifier::activated, this, [this] {
for (;;) {
auto data = readNonBlocking(); // 返回空表示读空
if (!data) break;
handle(data);
}
});
// 销毁: delete n; close(fd);
参考
- Qt 官方文档:QSocketNotifier Class(Qt 6)
- Qt 官方文档:QSocketNotifier 类(Qt 6 中文)
- Qt 文档镜像(Windows 注意事项的原文):QSocketNotifier Class Reference
- man page:QSocketNotifier(3)
文中涉及的关键事实来源
- Windows 上 socket 会变为非阻塞、可能出现虚假激活、通知后自动禁用------Qt 文档 "Notes for Windows Users"。
- Qt6
activated(QSocketDescriptor, QSocketNotifier::Type);Qt5 为activated(int),5.15 起废弃。 setSocket()(Qt 6.1+)会把 notifier 置为 disabled,需手动setEnabled(true)。- 写 notifier 应在
activated()后立即禁用。