【无标题】

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 里还有数据没读,下次事件循环就还会再通知你。

这意味着两件事:

  1. 不会丢事件:你这次少读一点,下次还会被叫醒。
  2. 一次激活不代表只有一条消息:可能积压了 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(&notifier, &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);         // 按类型分发
        }
    }
}

注意几个模式:

  1. 外层 while:排空 socket 缓冲区;
  2. 内层 while:UDP 一个包可能包含半帧/多帧,解析器要循环取完整帧;
  3. 解析与分发解耦 :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 条

  1. 一个 fd 一个 notifier;要 Read+Write 就建两个。
  2. 构造给 qintptr ,类型选 Read。
  3. 槽里循环读到空,一次激活可能有多条数据。
  4. 先删 notifier,再关 fd,顺序不能反。
  5. Write notifier 发完立刻 setEnabled(false),否则 CPU 100%。
  6. 必须在有事件循环的线程里用,它不拥有 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);

参考

文中涉及的关键事实来源

  1. Windows 上 socket 会变为非阻塞、可能出现虚假激活、通知后自动禁用------Qt 文档 "Notes for Windows Users"。
  2. Qt6 activated(QSocketDescriptor, QSocketNotifier::Type);Qt5 为 activated(int),5.15 起废弃。
  3. setSocket()(Qt 6.1+)会把 notifier 置为 disabled,需手动 setEnabled(true)。
  4. 写 notifier 应在 activated() 后立即禁用。
相关推荐
金色熊族1 小时前
在Qt中使用asan检测内存泄漏(二)--检查依赖库的内存使用
linux·qt·asan
大陈子编程1 小时前
C++ 背包九讲:从入门到精通的动态规划专题
开发语言·c++·动态规划
qq_401700411 小时前
Qt 组播(Multicast)实战详解
开发语言·qt
谢客诚科技1 小时前
镀膜脆性薄片,不能接触镀膜面,脆性薄片搬运选伯努利还是边缘承靠式末端模组?
人工智能·机器人
鸽芷咕2 小时前
从环境搭建到敬礼动作:启元 Q1 SDK 全流程实战与 8 个实测坑排查手册
数据库·ai·机器人·sdk·启元q1
真的想上岸啊11 小时前
笔记2:思小伴 搭载 EEG 脑电专注力监测的多功能儿童陪伴桌宠
机器人
是Yu欸13 小时前
机器人如何把二维画面变成三维动作?ECCV 2026 3DWay 论文解读
3d·机器人·论文解读·具身智能·eccv 2026
努力实干14 小时前
面向对象设计原则——C++
开发语言·c++·数码相机
深圳多奥智能一卡(码、脸)通系统15 小时前
如何实现VIP乘梯、高峰自动归底与机器人自动乘梯?
机器人·门禁·梯控·智能梯控·二维码电梯·梯控一卡通·vip乘梯