絡(luò)調(diào)試助手開(kāi)發(fā):TCP/UDP多線程與QAbstractTableModel優(yōu)化)
簡(jiǎn)介這是一份基于Qt框架重寫(xiě)的網(wǎng)絡(luò)調(diào)試助手完整源碼面向嵌入式開(kāi)發(fā)、網(wǎng)絡(luò)通信調(diào)試及Qt學(xué)習(xí)人群可解決TCP/UDP多模式聯(lián)調(diào)、數(shù)據(jù)收發(fā)與設(shè)備模擬等實(shí)際需求。壓縮包共35個(gè)文件約263KB包含8個(gè)cpp與8個(gè)h源文件、4個(gè)ui界面文件、2個(gè)pri工程模塊、2個(gè)qm多語(yǔ)言文件以及qrc、rc、ico、pro等工程配置資源另附send.txt、device.txt等示例數(shù)據(jù)與readme說(shuō)明目錄結(jié)構(gòu)規(guī)整便于二次開(kāi)發(fā)與移植。功能上覆蓋TCP服務(wù)端、TCP客戶端、UDP服務(wù)端、UDP客戶端四種模式支持16進(jìn)制與ASCII收發(fā)、定時(shí)自動(dòng)發(fā)送、配置持久化、常用數(shù)據(jù)下拉加載、設(shè)備模擬自動(dòng)回復(fù)、單連接或全部發(fā)送、多客戶端并發(fā)并采用單線程實(shí)現(xiàn)。目前已有7837人學(xué)習(xí)下載適合作為網(wǎng)絡(luò)調(diào)試工具原型或Qt網(wǎng)絡(luò)編程參考案例幫助讀者快速理解多模式通信架構(gòu)與配置化設(shè)計(jì)思路。1. 網(wǎng)絡(luò)調(diào)試助手為什么我最終用 Qt 重寫(xiě)了三遍串口屏、PLC、自研板卡混在同一條調(diào)試鏈路上時(shí)手邊沒(méi)一個(gè)趁手的網(wǎng)絡(luò)調(diào)試助手排查效率會(huì)低到讓人懷疑人生。市面上的工具要么只做 TCP 客戶端要么 UDP 收發(fā)包時(shí)界面卡成幻燈片更別提同時(shí)開(kāi) TCP 服務(wù)端和 UDP 服務(wù)端做回環(huán)測(cè)試。我前后用 Qt 寫(xiě)了三版第一版單線程收發(fā)數(shù)據(jù)量一上來(lái)直接假死第二版把 socket 塞進(jìn)moveToThread結(jié)果信號(hào)槽跨線程傳QByteArray時(shí)忘了注冊(cè)元類型編譯報(bào)了一長(zhǎng)串dependent ..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwid...的路徑錯(cuò)誤折騰半天才發(fā)現(xiàn)是構(gòu)建套件沒(méi)配對(duì)。第三版才把 TCP 客戶端、TCP 服務(wù)端、UDP 服務(wù)端三套邏輯拆干凈用QAbstractTableModel管收發(fā)記錄十萬(wàn)行數(shù)據(jù)滾動(dòng)不卡。這篇筆記就把這套終極版的落地路徑拆開(kāi)講從選型理由到每個(gè) socket 的創(chuàng)建參數(shù)再到多線程收發(fā)的坑新手能照著跑通熟手能直接拿去改自己的調(diào)試工具。2. 三套通信鏈路怎么拆TCP 客戶端、TCP 服務(wù)端、UDP 服務(wù)端的選型與骨架2.1 為什么不用 QTcpSocket 一把梭而是三套獨(dú)立封裝很多人寫(xiě)網(wǎng)絡(luò)調(diào)試助手習(xí)慣把QTcpSocket、QTcpServer、QUdpSocket全塞進(jìn)主窗口類里信號(hào)槽連一堆初期看著省事后期加個(gè)「十六進(jìn)制顯示」或「定時(shí)發(fā)送」就得動(dòng)全身。我的做法是抽一個(gè)NetWorker基類只暴露open()、close()、send(QByteArray)、readyRead信號(hào)四個(gè)接口然后TcpClientWorker、TcpServerWorker、UdpWorker各自繼承。這樣主窗口只跟基類指針打交道切換模式時(shí)不用改 UI 邏輯。選型上TCP 客戶端用QTcpSocket是唯一解但要注意connectToHost之后不能立刻write必須等connected信號(hào)。TCP 服務(wù)端用QTcpServer監(jiān)聽(tīng)每個(gè)新連接通過(guò)nextPendingConnection()拿到獨(dú)立的QTcpSocket這里有個(gè)血淚經(jīng)驗(yàn)如果不在incomingConnection里把 socket 的父對(duì)象設(shè)成 server客戶端斷開(kāi)后 socket 對(duì)象不會(huì)自動(dòng)回收跑一晚上內(nèi)存能漲幾百兆。UDP 服務(wù)端用QUdpSocket綁定端口時(shí)記得加QAbstractSocket::ShareAddress和ReuseAddressHint否則同一臺(tái)機(jī)器上開(kāi)兩個(gè)調(diào)試助手會(huì)搶端口。提示Qt 5.15 之后QAbstractSocket::error信號(hào)被標(biāo)記廢棄改用errorOccurred如果你從舊版本遷移編譯會(huì)報(bào)錯(cuò)但不會(huì)中斷運(yùn)行時(shí)收不到錯(cuò)誤提示排查半天找不到原因。2.2 最小可運(yùn)行骨架三個(gè) Worker 的創(chuàng)建與信號(hào)連接下面這段代碼是主窗口里初始化三套鏈路的骨架我把它放在MainWindow::initWorkers()里每個(gè) Worker 都moveToThread到獨(dú)立線程避免 socket 阻塞 UI。// mainwindow.cpp void MainWindow::initWorkers() { // TCP 客戶端獨(dú)立線程避免 connectToHost 阻塞界面 m_tcpClient new TcpClientWorker; m_tcpClientThread new QThread(this); m_tcpClient-moveToThread(m_tcpClientThread); connect(m_tcpClientThread, QThread::finished, m_tcpClient, QObject::deleteLater); connect(m_tcpClient, NetWorker::readyRead, this, MainWindow::onTcpClientData); connect(m_tcpClient, NetWorker::errorOccurred, this, MainWindow::onNetError); m_tcpClientThread-start(); // TCP 服務(wù)端監(jiān)聽(tīng)端口每個(gè)客戶端連接單獨(dú)處理 m_tcpServer new TcpServerWorker; m_tcpServerThread new QThread(this); m_tcpServer-moveToThread(m_tcpServerThread); connect(m_tcpServer, NetWorker::readyRead, this, MainWindow::onTcpServerData); connect(m_tcpServer, NetWorker::errorOccurred, this, MainWindow::onNetError); m_tcpServerThread-start(); // UDP 服務(wù)端綁定本地端口收發(fā)廣播/單播 m_udpWorker new UdpWorker; m_udpWorkerThread new QThread(this); m_udpWorker-moveToThread(m_udpWorkerThread); connect(m_udpWorker, NetWorker::readyRead, this, MainWindow::onUdpData); connect(m_udpWorker, NetWorker::errorOccurred, this, MainWindow::onNetError); m_udpWorkerThread-start(); }邏輯說(shuō)明每個(gè) Worker 都moveToThread到獨(dú)立QThread這樣QTcpSocket的waitForReadyRead或connectToHost不會(huì)卡住主線程。參數(shù)上QThread::start()默認(rèn)優(yōu)先級(jí)QThread::InheritPriority調(diào)試工具不需要改。信號(hào)連接里readyRead統(tǒng)一發(fā)QByteArray主窗口收到后追加到模型不直接操作 UI 控件。注意moveToThread之后Worker 里的QTimer也必須在該線程內(nèi)創(chuàng)建否則定時(shí)發(fā)送會(huì)報(bào)QObject::startTimer: Timers cannot be started from another thread。我一般把定時(shí)器放在 Worker 的open()槽函數(shù)里 new父對(duì)象設(shè)成 Worker 自身。2.3 收發(fā)數(shù)據(jù)模型用 QAbstractTableModel 替代 QTableWidget熱詞里「qt 表格大數(shù)據(jù)卡頓優(yōu)化 tablewiget 到 qtableview 自定義 model」是真實(shí)痛點(diǎn)。我第一版用QTableWidget每收一包insertRow一次收包頻率到 1000 包/秒時(shí)界面直接無(wú)響應(yīng)。后來(lái)?yè)Q成QAbstractTableModelQTableView只維護(hù)一個(gè)QVectorPacketRecorddata()里按索引返回beginInsertRows/endInsertRows批量提交十萬(wàn)行滾動(dòng)絲滑。// packetmodel.h class PacketModel : public QAbstractTableModel { Q_OBJECT public: enum Column { TimeCol, DirectionCol, DataCol, LengthCol, ColumnCount }; int rowCount(const QModelIndex parent QModelIndex()) const override { return parent.isValid() ? 0 : m_records.size(); } int columnCount(const QModelIndex parent QModelIndex()) const override { return parent.isValid() ? 0 : ColumnCount; } QVariant data(const QModelIndex index, int role) const override { if (!index.isValid() || role ! Qt::DisplayRole) return {}; const auto rec m_records.at(index.row()); switch (index.column()) { case TimeCol: return rec.timestamp.toString(hh:mm:ss.zzz); case DirectionCol: return rec.isSend ? TX : RX; case DataCol: return rec.displayText; // 已按 HEX/ASCII 格式化 case LengthCol: return rec.raw.size(); } return {}; } void appendRecord(const PacketRecord rec) { beginInsertRows(QModelIndex(), m_records.size(), m_records.size()); m_records.append(rec); endInsertRows(); // 超過(guò) 50000 行時(shí)裁剪頭部防止內(nèi)存無(wú)限增長(zhǎng) if (m_records.size() 50000) { beginRemoveRows(QModelIndex(), 0, 999); m_records.remove(0, 1000); endRemoveRows(); } } private: QVectorPacketRecord m_records; };參數(shù)說(shuō)明appendRecord里每次只插入一行Qt 的 view 會(huì)自動(dòng)只刷新可見(jiàn)區(qū)域所以性能瓶頸不在插入而在data()的調(diào)用頻率。displayText在收包時(shí)就格式化好不要在data()里做toHex()轉(zhuǎn)換否則滾動(dòng)時(shí) CPU 飆高。裁剪閾值 50000 行是我實(shí)測(cè) 8GB 內(nèi)存機(jī)器上的安全值再大滾動(dòng)條會(huì)變短但內(nèi)存占用線性增長(zhǎng)。3. 把三套鏈路跑起來(lái)TCP 客戶端連接、服務(wù)端監(jiān)聽(tīng)、UDP 綁定的具體命令與參數(shù)3.1 TCP 客戶端connectToHost 的四個(gè)必調(diào)參數(shù)與超時(shí)處理TCP 客戶端的核心是connectToHost但很多人只傳地址和端口結(jié)果連接失敗時(shí)界面卡死。我一般會(huì)顯式設(shè)置QAbstractSocket::LowDelayOption和KeepAliveOption前者禁用 Nagle 算法調(diào)試小包時(shí)延遲從 40ms 降到 1ms 以內(nèi)后者讓系統(tǒng)定期發(fā)心跳防止中間設(shè)備斷鏈。// tcpclientworker.cpp void TcpClientWorker::open(const QString host, quint16 port) { if (m_socket) close(); m_socket new QTcpSocket(this); m_socket-setSocketOption(QAbstractSocket::LowDelayOption, 1); m_socket-setSocketOption(QAbstractSocket::KeepAliveOption, 1); connect(m_socket, QTcpSocket::connected, this, [this]() { emit connectedChanged(true); }); connect(m_socket, QTcpSocket::readyRead, this, TcpClientWorker::onReadyRead); connect(m_socket, QTcpSocket::errorOccurred, this, [this](QAbstractSocket::SocketError err) { emit errorOccurred(m_socket-errorString()); emit connectedChanged(false); }); m_socket-connectToHost(host, port); // 5 秒超時(shí)避免一直轉(zhuǎn)圈 QTimer::singleShot(5000, this, [this]() { if (m_socket m_socket-state() ! QAbstractSocket::ConnectedState) { m_socket-abort(); emit errorOccurred(連接超時(shí)); } }); }邏輯說(shuō)明LowDelayOption設(shè)為 1 后write的數(shù)據(jù)會(huì)立即發(fā)出不會(huì)攢到 MSS 再發(fā)。KeepAliveOption在 Windows 上默認(rèn) 2 小時(shí)才發(fā)第一個(gè)探測(cè)包調(diào)試場(chǎng)景下建議配合setSocketOption(QAbstractSocket::KeepAliveOption, 1)并在系統(tǒng)層調(diào)短但 Qt 沒(méi)暴露更細(xì)的參數(shù)所以實(shí)際心跳還是靠應(yīng)用層定時(shí)發(fā)空包。超時(shí)用QTimer::singleShot而不是waitForConnected因?yàn)楹笳邥?huì)阻塞事件循環(huán)在 Worker 線程里雖然不卡 UI但會(huì)卡住該線程的其他信號(hào)。提示如果連接的是本機(jī)127.0.0.1connectToHost幾乎瞬間返回超時(shí)定時(shí)器不會(huì)觸發(fā)連外網(wǎng)設(shè)備時(shí) 5 秒是經(jīng)驗(yàn)值超過(guò) 5 秒還沒(méi)連上多半是路由或防火墻問(wèn)題繼續(xù)等沒(méi)意義。3.2 TCP 服務(wù)端監(jiān)聽(tīng)端口、處理多連接與廣播發(fā)送TCP 服務(wù)端用QTcpServer::listen參數(shù)里QHostAddress::Any表示監(jiān)聽(tīng)所有網(wǎng)卡端口傳 0 表示系統(tǒng)隨機(jī)分配。實(shí)際調(diào)試時(shí)我一般固定端口比如 8080方便對(duì)端配置。// tcpserverworker.cpp void TcpServerWorker::open(quint16 port) { if (m_server) close(); m_server new QTcpServer(this); connect(m_server, QTcpServer::newConnection, this, TcpServerWorker::onNewConnection); if (!m_server-listen(QHostAddress::Any, port)) { emit errorOccurred(m_server-errorString()); return; } emit listeningChanged(true, m_server-serverPort()); } void TcpServerWorker::onNewConnection() { while (m_server-hasPendingConnections()) { QTcpSocket *client m_server-nextPendingConnection(); client-setSocketOption(QAbstractSocket::LowDelayOption, 1); m_clients.append(client); connect(client, QTcpSocket::readyRead, this, [this, client]() { emit readyRead(client-readAll(), client-peerAddress().toString(), client-peerPort()); }); connect(client, QTcpSocket::disconnected, this, [this, client]() { m_clients.removeAll(client); client-deleteLater(); }); emit clientCountChanged(m_clients.size()); } } void TcpServerWorker::sendToAll(const QByteArray data) { for (QTcpSocket *client : m_clients) { if (client-state() QAbstractSocket::ConnectedState) { client-write(data); } } }參數(shù)說(shuō)明nextPendingConnection返回的 socket 父對(duì)象默認(rèn)是QTcpServer所以deleteLater后會(huì)被 server 回收不用手動(dòng) delete。m_clients用QListQTcpSocket*存活躍連接disconnected信號(hào)里先removeAll再deleteLater順序反了會(huì)導(dǎo)致野指針。廣播發(fā)送時(shí)逐個(gè)write如果客戶端數(shù)量超過(guò) 50 個(gè)建議改成QTimer分批發(fā)送避免單次事件循環(huán)里寫(xiě)太多數(shù)據(jù)導(dǎo)致界面卡頓。3.3 UDP 服務(wù)端綁定參數(shù)、收發(fā)模式與廣播地址UDP 服務(wù)端用QUdpSocket::bind關(guān)鍵參數(shù)是ShareAddress和ReuseAddressHint前者允許多個(gè)進(jìn)程綁定同一端口后者讓系統(tǒng)在端口被占用時(shí)盡量復(fù)用。// udpworker.cpp void UdpWorker::open(quint16 localPort) { if (m_socket) close(); m_socket new QUdpSocket(this); bool ok m_socket-bind(QHostAddress::AnyIPv4, localPort, QAbstractSocket::ShareAddress | QAbstractSocket::ReuseAddressHint); if (!ok) { emit errorOccurred(m_socket-errorString()); return; } connect(m_socket, QUdpSocket::readyRead, this, UdpWorker::onReadyRead); emit listeningChanged(true, localPort); } void UdpWorker::onReadyRead() { while (m_socket-hasPendingDatagrams()) { QByteArray buf; buf.resize(m_socket-pendingDatagramSize()); QHostAddress sender; quint16 senderPort; m_socket-readDatagram(buf.data(), buf.size(), sender, senderPort); emit readyRead(buf, sender.toString(), senderPort); } } void UdpWorker::send(const QByteArray data, const QString host, quint16 port) { m_socket-writeDatagram(data, QHostAddress(host), port); }邏輯說(shuō)明pendingDatagramSize()返回下一個(gè)數(shù)據(jù)報(bào)的大小必須按這個(gè)大小分配緩沖區(qū)否則readDatagram會(huì)截?cái)唷hareAddress在 Windows 上需要管理員權(quán)限才能生效普通用戶綁定時(shí)如果端口被占用會(huì)直接失敗這時(shí)可以換端口或提示用戶。廣播發(fā)送時(shí)目標(biāo)地址填255.255.255.255但很多路由器默認(rèn)禁止廣播調(diào)試時(shí)優(yōu)先用單播。注意UDP 的writeDatagram返回值是實(shí)際發(fā)送的字節(jié)數(shù)如果返回 -1 表示發(fā)送失敗常見(jiàn)原因是目標(biāo)地址不可達(dá)或緩沖區(qū)滿。調(diào)試時(shí)不要忽略這個(gè)返回值我一般會(huì)在發(fā)送后檢查m_socket-error()。4. 避坑與排查多線程收發(fā)、中文編碼、大數(shù)據(jù)量下的五個(gè)翻車(chē)現(xiàn)場(chǎng)4.1 現(xiàn)象程序跑幾分鐘后界面卡死CPU 占用 100%原因readyRead信號(hào)里直接append到QTableWidget每收一包觸發(fā)一次 UI 重繪收包頻率高時(shí)事件循環(huán)被重繪占滿。解決換成QAbstractTableModel并且收包時(shí)不要逐條beginInsertRows而是攢 50 條或 100ms 批量提交一次。我現(xiàn)在的做法是 Worker 里收包后發(fā)信號(hào)到主窗口主窗口用一個(gè)QTimer每 100ms 從隊(duì)列取一批數(shù)據(jù)插入模型界面立刻流暢。4.2 現(xiàn)象TCP 服務(wù)端客戶端斷開(kāi)后再次連接收不到數(shù)據(jù)原因disconnected信號(hào)里只removeAll沒(méi)有deleteLater舊 socket 對(duì)象還在nextPendingConnection返回新對(duì)象時(shí)舊對(duì)象仍占用文件描述符系統(tǒng)達(dá)到上限后新連接無(wú)法建立。解決disconnected里先client-deleteLater()再?gòu)膍_clients移除。如果客戶端異常斷開(kāi)拔網(wǎng)線disconnected可能不觸發(fā)需要加QTimer定期檢查state()。4.3 現(xiàn)象發(fā)送中文時(shí)對(duì)端收到亂碼原因Qt 的QString::toUtf8()默認(rèn) UTF-8但很多調(diào)試工具對(duì)端按 GBK 解析。解決在發(fā)送前讓用戶選擇編碼QString::toLocal8Bit()在中文 Windows 上是 GBKtoUtf8()是 UTF-8。我一般默認(rèn) UTF-8并在界面上加一個(gè)編碼下拉框切換時(shí)重新編碼待發(fā)送文本。4.4 現(xiàn)象UDP 綁定端口失敗提示 Address already in use原因沒(méi)有加ShareAddress或者加了但系統(tǒng)不支持。解決先嘗試ShareAddress | ReuseAddressHint如果還失敗用netstat -ano | findstr 端口號(hào)找到占用進(jìn)程殺掉或換端口。Linux 上ReuseAddressHint對(duì)應(yīng)SO_REUSEADDR行為略有差異綁定前最好先close()舊 socket。4.5 現(xiàn)象編譯報(bào)錯(cuò)dependent ..\..\..\..\..\..\qt\5.15.2\msvc2019_64\include\qtwid...原因Qt Creator 的構(gòu)建套件路徑變了或者項(xiàng)目文件里INCLUDEPATH寫(xiě)死了絕對(duì)路徑。解決檢查Kits里 Qt 版本和編譯器是否匹配刪除.pro.user重新配置。如果用了Q_OBJECT宏但沒(méi)跑qmake也會(huì)報(bào)類似錯(cuò)誤執(zhí)行qmake再make即可。5. 進(jìn)階技巧用 QDataStream 做協(xié)議解析與收發(fā)日志的后悔藥5.1 自定義協(xié)議幀QDataStream 的字節(jié)序與版本控制調(diào)試助手經(jīng)常要對(duì)接自定義二進(jìn)制協(xié)議比如幀頭0xAA55 長(zhǎng)度 命令字 數(shù)據(jù) CRC。用QDataStream可以省去手動(dòng)拼字節(jié)但要注意setByteOrder和setVersion。// protocol.cpp QByteArray buildFrame(quint8 cmd, const QByteArray payload) { QByteArray frame; QDataStream ds(frame, QIODevice::WriteOnly); ds.setByteOrder(QDataStream::LittleEndian); // 與設(shè)備端一致 ds.setVersion(QDataStream::Qt_5_15); // 固定版本避免跨 Qt 版本解析差異 ds quint16(0xAA55); // 幀頭 ds quint16(payload.size() 3); // 長(zhǎng)度命令字 1 字節(jié) 數(shù)據(jù) CRC 2 字節(jié) ds cmd; ds.writeRawData(payload.constData(), payload.size()); quint16 crc crc16(frame.mid(2)); // 從長(zhǎng)度字段開(kāi)始算 ds crc; return frame; }參數(shù)說(shuō)明setByteOrder必須和嵌入式端一致常見(jiàn)的是小端。setVersion固定為Qt_5_15這樣即使以后升級(jí) Qt 6解析舊日志時(shí)行為不變。writeRawData不添加長(zhǎng)度前綴適合已經(jīng)手動(dòng)管理長(zhǎng)度的場(chǎng)景。CRC 計(jì)算范圍從長(zhǎng)度字段開(kāi)始避免幀頭參與校驗(yàn)。5.2 收發(fā)日志的后悔藥自動(dòng)保存與回放調(diào)試時(shí)經(jīng)常遇到「剛才那包數(shù)據(jù)沒(méi)存想重發(fā)一次」的情況。我的做法是每收一包就追加到QVectorPacketRecord同時(shí)用QFile以二進(jìn)制格式寫(xiě)日志格式就是QDataStream序列化的PacketRecord?;胤艜r(shí)讀日志文件按時(shí)間戳逐條send。// logmanager.cpp void LogManager::append(const PacketRecord rec) { QFile file(m_logPath); if (file.open(QIODevice::Append)) { QDataStream ds(file); ds.setVersion(QDataStream::Qt_5_15); ds rec.timestamp rec.isSend rec.raw rec.displayText; file.close(); } } QVectorPacketRecord LogManager::load(const QString path) { QVectorPacketRecord records; QFile file(path); if (!file.open(QIODevice::ReadOnly)) return records; QDataStream ds(file); ds.setVersion(QDataStream::Qt_5_15); while (!ds.atEnd()) { PacketRecord rec; ds rec.timestamp rec.isSend rec.raw rec.displayText; records.append(rec); } return records; }邏輯說(shuō)明日志文件用追加模式每條記錄獨(dú)立序列化即使程序崩潰已寫(xiě)入的記錄仍可讀。displayText存格式化后的文本回放時(shí)直接顯示不用重新按 HEX/ASCII 轉(zhuǎn)換。回放時(shí)注意時(shí)間戳間隔如果原始間隔是 10ms回放時(shí)用QTimer按相同間隔發(fā)送否則對(duì)端可能處理不過(guò)來(lái)。提示日志文件超過(guò) 100MB 后打開(kāi)會(huì)變慢我一般按天分文件文件名用yyyyMMdd.log啟動(dòng)時(shí)自動(dòng)加載當(dāng)天文件。5.3 一個(gè)具體技巧用 QSignalSpy 做自動(dòng)化回歸測(cè)試網(wǎng)絡(luò)調(diào)試助手改多了容易引入回歸 bug比如「TCP 服務(wù)端廣播發(fā)送」被誤改成只發(fā)第一個(gè)客戶端。我寫(xiě)了一個(gè)簡(jiǎn)單的測(cè)試用例用QSignalSpy監(jiān)聽(tīng)readyRead信號(hào)模擬兩個(gè)客戶端連接后廣播斷言兩個(gè)客戶端都收到數(shù)據(jù)。// test_tcpserver.cpp void TestTcpServer::testBroadcast() { TcpServerWorker server; server.open(0); // 隨機(jī)端口 quint16 port server.serverPort(); QTcpSocket client1, client2; client1.connectToHost(127.0.0.1, port); client2.connectToHost(127.0.0.1, port); QVERIFY(client1.waitForConnected(1000)); QVERIFY(client2.waitForConnected(1000)); QSignalSpy spy1(client1, QTcpSocket::readyRead); QSignalSpy spy2(client2, QTcpSocket::readyRead); server.sendToAll(hello); QVERIFY(spy1.wait(1000)); QVERIFY(spy2.wait(1000)); QCOMPARE(client1.readAll(), QByteArray(hello)); QCOMPARE(client2.readAll(), QByteArray(hello)); }這個(gè)測(cè)試跑在QTest框架里每次改完TcpServerWorker先跑一遍確認(rèn)廣播邏輯沒(méi)退化。參數(shù)上waitForConnected超時(shí) 1000ms 對(duì)本地回環(huán)足夠QSignalSpy::wait也是 1000ms超過(guò)說(shuō)明信號(hào)沒(méi)發(fā)出來(lái)。我一般把這類測(cè)試放在tests/目錄用qmake的SUBDIRS模板跟主工程一起構(gòu)建。寫(xiě)到這里這套終極版我前后迭代了三個(gè)月最大的教訓(xùn)是不要在主線程里碰任何 socket 的阻塞調(diào)用哪怕只是waitForReadyRead(10)在收包頻率高時(shí)也會(huì)讓界面掉幀。另一個(gè)習(xí)慣是每加一個(gè)功能就先寫(xiě)個(gè)QSignalSpy測(cè)試跑通了再合進(jìn)主分支省得后面改一處崩三處。希望幫到你。本文還有配套的精品資源點(diǎn)擊獲取