简介:面向QT开发者的通信编程参考代码,整合TCP、UDP与串口三类通信,覆盖TCP服务器与客户端搭建、UDP收发、串口参数配置和实时读写等关键环节,适合嵌入式、物联网及设备调试场景。资源包共67个文件,包含20个cpp、15个h源文件与5个ui界面设计文件,另有pro工程、makefile编译配置及release/debug目录,压缩后约2.11MB,结构清晰便于直接导入工程修改调试。源码既提供简单的演示界面,也封装了底层通信逻辑,可帮助初学者理解网络与串口编程的基本流程,同时为有经验的开发者提供可复用的模块。已有434人学习,说明该示例具备较好的参考价值。细致研读可掌握QTcpSocket、QUdpSocket与QSerialPort的典型用法,理解事件绑定、错误处理与跨平台编译技巧,为开发网络调试助手或设备管理软件打下扎实基础。此外,代码中加入了qextserialport串口扩展库的接入方式,方便在不同平台下使用自定义串口参数,并通过ui文件快速调整主控界面,整体工程可直接编译运行,适合作为通信类项目的基础模板。
1. 拿到一套基于QT的TCP UDP 串口通讯源代码:先看清它到底替你干了多少活
做上位机开发的人,几乎都会遇到同一个需求:既要走 TCP 跟服务器对接,又要开 UDP 转发数据,还得留个串口通道备用。去网上搜源码,铺天盖地的 demo,但大多只有单个协议,或者界面和收发逻辑全搅在一起,想换协议就要动 UI,调参摸不着头脑。这套基于 Qt 的 TCP/UDP/串口通讯源代码,好就好在把三种通讯方式收到同一个框架里,数据收发走独立线程、界面只管刷新,拿到手能直接当通讯层去套用。它适合刚接触 Qt 网络编程的开发者做入门参考,更适合被通讯模块反复折腾、想找个完整封装模板的从业者。要判断这套代码好不好用,先看它有没有把QTcpSocket、QUdpSocket、QSerialPort三类设备用同一套调度逻辑管理起来,再看它的收发缓冲区是怎么处理粘包和半包的。
2. 三种通讯方式在 Qt 里的选型边界:为什么不是一套代码打通所有场景
2.1 通讯类各自的本职:从套接字类型到数据边界
Qt 把网络通讯拆成几个职责明确的类:TCP 对应QTcpSocket和QTcpServer,UDP 用QUdpSocket,串口则是QSerialPort。三个类都继承自QIODevice,这意味着它们都有readyRead信号、都支持read/write/bytesAvailable,这是能统一封装的前提。但它们的底层行为差异很大,这也是选型时最容易忽略的部分。
TCP 是面向字节流的可靠传输,QTcpSocket发数据只管往内核缓冲区塞,接收侧拿到的是一个没有边界的字节流。常见的坑是:你按bytesAvailable判断收完一包数据再解析,结果对方分两次发、本地一次读完,或者对方一次发多包、本地只读走一部分。这套源码里把 TCP 接收单独放在一个队列里,每次readyRead先把字节追加到QByteArray,再根据包头长度去截帧,而不是读多少算多少,这个思路是值得借鉴的。
UDP 是面向数据报的不可靠传输,QUdpSocket每次 receive 都对应一个完整的数据报,边界天然存在。代价是丢包、乱序、重复全都得自己处理,而且一个 socket 接收多个客户端的数据时,你只能在pendingDatagramSize里去区分来源。源码里 UDP 的收发节点独立挂了一套信号转发机制,收到数据后直接把sender的 IP 和端口附带在信号里抛出去,不用在槽函数里再想办法查来源。串口则更特殊,QSerialPort虽然也叫 socket,但它面对的是波特率、数据位、校验位、流控这些参数,数据流模型和 TCP 类似,却没有内核帮你做流量控制,误码和丢字节更常见。
三个类的统一性在于信号槽模型一致,差异在于协议语义完全不同。这份源代码用一组抽象接口把三类收发器包了起来,对外只提供open/close/send/receive四个动作,内部再各自转发。这种封装方式的好处是:业务层不用关心当前用的是 TCP 还是串口,切换通道只改配置项。
// 抽象接口示意,源码里对应的核心结构 class CommChannel : public QObject { Q_OBJECT public: virtual bool open(const QVariantMap ¶ms) = 0; // 打开通道,TCP/UDP/串口各自解析参数 virtual void close() = 0; virtual qint64 send(const QByteArray &data) = 0; // 发送数据,携带完整包体 signals: void dataReceived(const QByteArray &data); // 统一数据上报信号 void stateChanged(int state); // 连接状态变更 };这段代码展现的是通道抽象层,open返回 bool 是为了让调用方明确知道参数校验是否通过,send返回 qint64 是保留一次发送实际写入的长度。TCP 里 send 返回的可能是缓冲到内核的数据长度而非对方实际收到的,串口里 send 返回 -1 往往意味着参数没配对或者端口被占用。这个接口设计得好不好,直接决定后续业务代码要不要到处写 if-else 判断通道类型。
2.2 线程分工:UI 不被收发拖死,才能给后续项目留余地
多数通讯源码的问题在于把readyRead的处理直接写在主窗口类里,导致大包数据到达时 UI 刷新卡顿。这套源码把通讯对象移到了独立线程,界面只通过信号槽和通道线程交互。常见做法是用QThread配合moveToThread,把QTcpSocket、QUdpSocket、QSerialPort的实例移动过去,然后在线程里跑事件循环。这样做的实质是让 socket 的readyRead信号在线程的事件循环里触发,而不是阻塞主线程。
| 通道类型 | 数据边界 | 可靠性 | 线程模型建议 | 常见瓶颈 |
|---|---|---|---|---|
| TCP | 字节流,无边界 | 可靠传输、流量控制 | 独立收发线程 + 分包队列 | 粘包、半包、断线重连 |
| UDP | 数据报,有边界 | 不可靠,可能丢包乱序 | 独立 socket 线程 | 丢包、组播延迟 |
| 串口 | 字节流,分包困难 | 依赖波特率和硬件流控 | 收发线程 + 串口数据拼包 | 字节丢失、误码率 |
线程模型选择上有个细节值得强调:UDP 的数据报边界是天然存在的,但如果你在接收槽里做emit dataReceived(newData),信号槽跨线程时默认是队列连接,newData会拷贝一份到主线程。数据量大时频繁拷贝才是性能瓶颈。代码里没有对每个数据报都触发射信号,而是攒够一次事件循环再批量抛给 UI,这样有效降低了重复内存拷贝。另一个容易踩的点是moveToThread之后,socket 的父对象不能再是主窗口,否则事件会跑回主线程。源码里每个通道对象都是单独new出来,不设父对象,在线程启动后再moveToThread,这个细节从根上避免了对象归属错乱引发的时好时坏问题。
2.3 协议解析:一帧数据该何时被判定为完整
| 帧头(2B) | 长度(2B) | 数据区(N) | 校验(1B) |上表是一个典型的自定义通信协议帧结构,在 TCP 和串口场景都适用。判断一包数据是否完整有两个硬条件:帧头匹配、数据区长度字段说明的字节数全部到齐。源码里把帧校验逻辑放在数据接收队列之后、业务解析之前,这个位置有讲究——校验放在线程内做,避免了主界面还没刷新完又收到下一包导致 UI 事件堆积。查询到了一段资料对此方案的描述与之匹配:解析前先攒缓冲,检查长度字段,不足则等待多个readyRead事件,这不会丢数据,也不会把属于下一包的字节消耗掉。
// 数据切包流程,伪代码揭示核心处理策略 void TcpChannel::onReadyRead() { buffer_.append(tcpSocket_->readAll()); while (buffer_.size() >= FRAME_HEADER_SIZE) { if (buffer_.at(0) != FRAME_MAGIC) { buffer_.remove(0, 1); // 帧头不匹配,逐字节跳过 continue; } quint16 frameLen = (quint16(buffer_.at(1)) << 8) | quint16(buffer_.at(2)); if (buffer_.size() < frameLen + FRAME_HEADER_SIZE) { break; // 半包,等待下一次 readyRead } QByteArray frame = buffer_.left(FRAME_HEADER_SIZE + frameLen); buffer_.remove(0, FRAME_HEADER_SIZE + frameLen); emit dataReceived(frame); } }这段代码里的FRAME_MAGIC是帧头标记,frameLen取自固定偏移的 2 字节字段。跳字节的逻辑是针对数据流中被误判为帧头的脏数据,break半包处理则保证了字节流的顺序一致。实测中,如果跳过字节处没有对buffer_大小做二次判断,TCP 大流量下很容易出现数组越界或死循环。写完切包逻辑之后,一定要打上不同长度、跨readyRead边界的测试包。
3. TCP 收发落地:状态机、分包缓冲与断线自愈
3.1 从连接建立到数据进队列:状态机才是稳定的前提
TCP 通道里常见的不稳定表现是:界面有时候能收到数据,有时候卡住,过一会又突然收到一大批。多数原因是代码没有维护连接状态,在disconnected信号发出之后还继续发送数据。这套源码里用了一个简单的状态机:Disconnected、Connecting、Connected,分别在connectToHost、connected、disconnected信号里切换状态。
void TcpClient::open(const QVariantMap ¶ms) { QString host = params.value("host").toString(); quint16 port = params.value("port").toUInt(); socket_ = new QTcpSocket(this); connect(socket_, &QTcpSocket::connected, this, &TcpClient::onConnected); connect(socket_, &QTcpSocket::disconnected, this, &TcpClient::onDisconnected); connect(socket_, &QTcpSocket::readyRead, this, &TcpClient::onReadyRead); connect(socket_, QOverload<QAbstractSocket::SocketError>::of(&QAbstractSocket::error), this, &TcpClient::onError); socket_->connectToHost(host, port); if (!socket_->waitForConnected(3000)) { qWarning() << "connect failed:" << socket_->errorString(); setState(Disconnected); } }waitForConnected(3000)在选型这里是一个关键的参数:它阻塞调用线程 3 秒,适合调试阶段快速暴露连接问题,但不建议在正式产品的 UI 线程里这么干。参数里QOverload是 Qt5 里正确连接 error 信号的写法,Qt4 时代的SIGNAL(error(QAbstractSocket::SocketError))宏写法在 Qt5/Qt6 下偶尔能编译但信号连不上。连接状态的切换必须放在信号槽里,而不是靠socket_->state()去轮询,轮询在 latency 高的网络环境下会产生一种"明明状态还是 UnconnectedState,但数据已经进来了"的坏味道。
连接到服务器之后,发送动作需要额外关注。write只把数据复制进内核发送缓冲区,不代表对方立刻收到。需要业务层自己做应用层确认,或者在断线重连后补发未确认的数据帧。源码里在send后记录一个lastSentTime_,配合 3 秒一次的定时器做应用层心跳保活,保证中间的网络代理不因空闲而断开连接。心跳包里建议单独设一个特殊帧头,不要和业务数据混用同一套序列,否则协议解析处还需要额外判断。
3.2 断线重连的节奏控制与三类翻车现场
断线重连是最容易写飘的地方。最常见的三种翻车:一是disconnected信号里立刻调用connectToHost重连,服务端崩溃恢复时客户端疯狂快速重连,把服务端打挂;二是重连次数不做上限,底层网络一直不通,线程里堆积大量失败信号,最后界面卡死;三是没有清理旧的 socket 对象,每个连接周期new一个,内存里积了一堆僵尸对象。
void TcpClient::onDisconnected() { if (state_ == _ManualClose) return; // 主动关闭不触发重连 retryCount_++; if (retryCount_ > maxRetryCount_) { emit stateChanged(Error); return; } int delay = qMin(30000, retryCount_ * 2000); // 指数退避,最大 30 秒 QTimer::singleShot(delay, this, [this]() { if (state_ != Connected) { socket_->connectToHost(host_, port_); } }); }retryCount_ * 2000是一个指数退避序列:1 次失败等 2 秒,2 次失败等 4 秒,5 次失败后等 10 秒,封顶 30 秒。用qMin封顶是为了避免长时间断线时重连周期拉得太长,恢复后不能及时感知。代码里在重连定时器触发前还会检查一次状态,防止用户在定时器的等待区间里手动操作,这里加了一个二次判断,实际上消灭了重连回调里的重复连。真正要考虑的是服务端不可达的情况下,connectToHost可能会立即触发 error 而不会先进入Connecting状态,所以错误分支里也要处理重连计数,否则会出现只重连一次就不动弹的问题。
TCP 接收缓冲区的容量也是个值得养成的习惯。QTcpSocket默认缓冲区可能只有几千字节,如果你从readyRead里用readAll()把整池数据拿出来,再自己维护buffer_,其实只是把压力转移到自己的内存里。更好的方案是限制buffer_的最大容量,超过 512KB 就把最老的帧丢弃,避免内存涨到一定程度触发奇怪的bad_alloc。模拟项目里打死我也用不到这么大的缓冲,但实际上位机对接 PLC 时,对方可能一瞬间喷出一整包配方数据,没有上限控制,跑几天内存就爆。这套代码里同样做了buffer_.capacity()检查,超限即清空并切断连接,属于保守但安全的处理方式。
4. UDP 收发落地:数据报边界、组播组管理与大流量丢包
4.1 单播与广播:参数绑定和接收来源辨别
QUdpSocket在收发上比 TCP 轻量,但敞开写容易出洞。常见问题是一个 UDP socket 既要收业务主站的指令,又要监听自身广播回来的响应,结果两个数据源撞在同一个readyRead上。实际上 UDP 在同一端口上收任何来源的数据,这些数据本来就会全进入同一个接收队列,你只能通过senderAddress和senderPort去区分来源。这套源代码在接收信号里带上的就是这两个参数,省去了在槽函数里再调readDatagram查一遍的麻烦。
void UdpChannel::onReadyRead() { while (udpSocket_->hasPendingDatagrams()) { QByteArray datagram; datagram.resize(int(udpSocket_->pendingDatagramSize())); QHostAddress senderAddr; quint16 senderPort; udpSocket_->readDatagram(datagram.data(), datagram.size(), &senderAddr, &senderPort); if (senderAddr == QHostAddress::LocalHost) { // 本地回环数据,经常是调试时自己发的,优先级降级 emit dataReceived(datagram, senderAddr, senderPort, true); } else { emit dataReceived(datagram, senderAddr, senderPort, false); } } }pendingDatagramSize必须先取后读,且是 int 类型,QByteArray 的长度也必须是这个值,否则读出来一包数据长度不对,残留的字节会影响下一包内容。这段代码里专门识别了LocalHost,实际场景中是本机测试用,如果本地有两个进程用 UDP 互发,误把回环数据当真会把状态机搞乱。UDP 的广播地址QHostAddress::Broadcast在 Qt6 里的行为与 Qt5 一致,发送时对象无需绑定端口,但接收广播前必须bind到具体端口且通常要setSocketOption(ShareAddress, 1),否则同一台机器上几个进程同时监听会导致后绑定的收不到数据。
这里有个听上去很玄学的坑:Windows 上绑定端口时如果没加ShareAddress,偶尔会报Address already in use,而 Linux 反而不报,只在数据接收表现上打折扣。代码里在bind时对 socket 选项做了统一设置,不同平台表现一致,避免同样的启动顺序跑出两种结果。
4.2 组播与丢包:路由器之外还要处理网卡绑定
组播收发在工业场景里用得不少,尤其是摄像头流推送和实时数据分发。Qt 里组播的基本动作是joinMulticastGroup和leaveMulticastGroup,但坑在于一个主机有多张网卡时,joinMulticastGroup默认加入的是系统选中的那张网卡上的组,实际项目里常常需要指定网卡索引。最稳妥的方式是用QNetworkInterface::allInterfaces()枚举出所有有效网卡,再根据 IP 段过滤出与业务网段匹配的那个 interface,最后调用带上interface参数的重载函数。
// 组播加入示例:绑定指定网卡 QList<QNetworkInterface> ifaces = QNetworkInterface::allInterfaces(); foreach (const QNetworkInterface &iface, ifaces) { if (!iface.isValid()) continue; if (iface.flags() & QNetworkInterface::LoopBack) continue; if (udpSocket_->joinMulticastGroup(multicastAddr, iface)) { qInfo() << "joined multicast on:" << iface.name(); break; } }joinMulticastGroup的返回只是告知内核加入操作是否成功,真正的数据是否到来取决于路由和交换机的IGMP snooping配置。代码里遍历所有物理网卡逐个 join,直到成功就 break,省去了手动选网卡的操作。组播场景下,丢包表现比单播更隐蔽的是:路由器允许组播进入但交换机没开 snooping,数据在局域网里广播式传播,偶尔能收到、延迟偏大。排查方式是抓包看目的 MAC 是不是组播 MAC,如果抓到了但应用收不到,那问题一定出在joinMulticastGroup的网卡绑定。
UDP 的接收缓冲在 Linux 上可以设置setSocketOption(QAbstractSocket::ReceiveBufferSizeSocketOption, 1 MB)。大数据量场景里,如果内核缓冲区不足,UDP 会静默丢包,不像 TCP 有重传机制。这段源码没有对发送队列做缓存,因为 UDP 本身不保证送达,在发送端堆积只会加大延迟。实测大流量readyRead槽里如果做了复杂的解析操作,下一个数据报积压在内核中的概率明显上升,忙不过来的信号最终也会被丢弃,所以保留每包数据的完整性,数据到达后尽快把 buffer 数据挪走,再异步解析,是这套代码在收包路径上的核心策略。
5. 串口通讯落地:QSerialPort 参数配置与五条高频踩坑
5.1 串口参数的正确打开顺序与热插拔处理
QSerialPort用起来比 socket 更啰嗦,因为参数组合多,而且一个参数不对整条链路数据全废。打开串口的正确顺序是:setPortName→setBaudRate→setDataBits→setParity→setStopBits→setFlowControl→open(ReadWrite)。先 open 再 setBaudRate 也能临时工作,但有些串口驱动在 open 时读取波特率并锁存,之后设置不生效。源码里封装了串口参数校验结构体,配置完成后统一一次性应用到串口对象,避免多个参数在不同线程里分开设置导致的竞态。
bool SerialChannel::open(const QVariantMap ¶ms) { QSerialPort serial_; serial_.setPortName(params.value("portName").toString()); serial_.setBaudRate(params.value("baudRate").toInt()); serial_.setDataBits(QSerialPort::Data8); serial_.setParity(QSerialPort::NoParity); serial_.setStopBits(QSerialPort::OneStop); serial_.setFlowControl(QSerialPort::NoFlowControl); if (!serial_.open(QIODevice::ReadWrite)) { qWarning() << "serial open failed:" << serial_.errorString(); return false; } connect(&serial_, &QSerialPort::readyRead, this, &SerialChannel::onReadyRead); return true; }代码里的关键点是setFlowControl默认设置为NoFlowControl。很多串口设备的硬件没有接 RTS/CTS 线,一旦启用硬件流控,数据会等在对端或本地缓存里迟迟不流动。对这种坑,调设备时先 NoFlowControl,协议通之后再考虑加流控,是稳妥路线。
热插拔是另一个高频问题。设备拔掉再插回来,QSerialPort对象没有自动感知机制,你必须在readyRead超时后重新调用QSerialPortInfo::availablePorts()枚举,比对之前记录的端口名与描述。有些 USB 转串口芯片每次插拔后的 COM 号会变,Windows 上尤其明显。用串口枚举时,固定设备不能纯靠端口名,还要检查manufacturer和serialNumber,否则插拔一次就换了一个 COM 号,连接对象完全错位。
5.2 串口常见问题结构与排查路径:现象、原因、解决
串口的坑比 TCP 多得多,下面按"现象 → 原因 → 解决"列出高频问题,省得你一次一次试错。
问题一:串口能发不能收,打开设备时又报"Permission denied"。现象是程序运行后能正常打开串口,写数据也不报错,但readyRead永远不触发;或者打开时就弹权限错误。原因通常是同一进程内重复open被拒绝,或者上次程序异常退出没有释放资源。解决是打开前先if (serial_->isOpen()) serial_->close();清理现场再 open;如果确认串口已被占用,检查系统里是否有旧的进程实例,Linux 下还要确认用户属于dialout组。
问题二:数据收发都是乱码,ASCII 字符和十六进制混着跳。现象是设备发送A5 5A 01 02,接收端显示成¥Z等乱码;发送时中文变成问号。原因是协议层用了文本模式而设备是二进制帧。解决是把QByteArray转换成toHex(' ')在日志里打印,并以十六进制字符串视角写收发。源码里加了一个"hex 显示"开关,把原始数据转为大写十六进制再做显示,局部分包异常时能立刻看出到底错在哪个字节。
问题三:波特率配置填 115200,设备却以 9600 回应。现象是接收到的字节间隔稳定但内容完全对不上,逻辑上又看不出问题。原因是设备出厂默认波特率就是 9600,或者芯片自动波特率检测没有被触发。解决是先用示波器或串口助手确认设备当前波特率,再检查驱动里是否开启 FIFO,Windows 下老的 CH340 驱动在 115200 档位偶尔有数据错位,换成官方新版驱动就好。折腾一套代码不如先确认硬件现状,这属于真正的血泪经验。
问题四:串口waitForReadyRead(100)阻塞,UI 卡住 1 秒以上。现象是槽函数里用while(waitForReadyRead(100))循环读数据,界面一卡就是好几秒;数据量稍微一大,整个窗体的按钮都没反应。原因是纯轮询阻塞在主线程,通信延迟直接体现在 UI。解决是把serial_对象 move 到独立线程,把readyRead驱动的数据传输用信号槽带上主线程刷新,不要在串口事件的槽函数里做耗时解析。代码里串口通道的线程模型就是上述方案,去掉一切waitForReadyRead。
问题五:硬件流控没接,偏偏配置成开启HardwareControl。现象是设备发数据一切正常,但上位机发给设备总是失败,偶发一次成功。原因是RTS/CTS对应信号线没有连接,驱动在等待握手信号,缓冲里的数据一直发不出。解决是setFlowControl(QSerialPort::NoFlowControl);必须硬件流控的场景,先通过clearError()清除错误标志,再检查串口线序。很多免焊调试线把这几个脚没引出,这个坑能浪费半天。
串口通讯的另一个重要点在于分帧。QSerialPort同样是字节流,没有边界概念,如果协议包长度不固定,串口端的拼包比 TCP 更麻烦。源码里串口接收按时间窗拼帧:每次readyRead后开一个 50ms 的QTimer,如果该定时器触发前没有新数据,就认定之前的一段数据组成一帧。这是最简单可靠的串口拆包方案,尤其适合不定长但间隔稳定的协议。
6. 通讯联调三板斧:用回显服务验证收发与组包健壮性
调试 TCP、UDP、串口通道,最简单有效的方法是先搭一个本机回显服务,验证数据的收发链路是否完整,再把问题从"通路不通"收敛到"协议解析逻辑不对"。下面用三种方式分别验证三类通道。
TCP 端,用一段 Python 起一个监听在 127.0.0.1:8899 的回显服务,收到什么就原样返回什么。这样客户端连接的收发是否通顺、断线重连是否符合预期,一眼能看出。Python 的socketserver只要十几行就能跑起来,不需要外部依赖。
import socketserver class EchoHandler(socketserver.BaseRequestHandler): def handle(self): data = self.request.recv(1024) print("recv:", data.hex()) self.request.sendall(data) with socketserver.TCPServer(("127.0.0.1", 8899), EchoHandler) as server: server.serve_forever()这段代码启动 TCP 回显服务,收到客户端data后原样发回去。通路上如果客户端发01 02 03收到的还是01 02 03,说明QTcpSocket收发和信号槽连接正常;如果回显与发送不一致,排查readyRead里是否提前消费了readAll()的数据。recv(1024)只在单连接调试里够用,多客户端压力拉起来时要改成循环 recv 或者用socketserver.ThreadingTCPServer。
UDP 的回显用nc -lu或者 Python 的socketserver.DatagramRequestHandler也能轻易做。关键是验证数据报边界是否和发送端一致,而不是关注内容顺序。串口端则需要一个虚拟串口对,比如 Windows 的com0com,Linux 的socat -d -d pty,raw,echo=0 pty,raw,echo=0,创建一对虚拟串口让两个程序各挂一端,直接从程序向串口写数据,再从对端读取验证。socat的三个参数pty生成虚拟终端,raw关闭终端行处理,echo=0关闭回显,不关就会看到自己发的数据原样弹回,干扰结果判断。
一次实际的调试过程里,我用这套源码搭了个温控模拟项目X,上位机通过 TCP 连某跨平台系统,中间用 UDP 转给一块显示屏刷新,同时留了一路串口读设备诊断信息。结果三路通道里,TCP 因为组包逻辑少判断了一种帧类型,偶发数据迟到;UDP 因为缓冲区默认 4K,高刷时丢包约 0.3%;串口倒是稳,但热插拔之后 COM 号漂移导致设备连不上。三路都跑完一遍回显,问题很快定位到个别模块的边界条件,而不是全局电路故障。遇到类似通讯问题,先把三路通道分别接回环验证,等到每条通路单独看都干净了,再做多通道并发测试,这样做排错效率最高。
那次深夜敲完代码,测试发现 TCP 偶发解析错包,排查了很久,最后发现是断线重连时旧 socket 的readyRead还在触发,新连接的数据混进了旧连接缓冲。从那以后我每次搭建通讯 Demo 都强制走一遍固定流程:先给通道加唯一标识,收到数据时校验标识是否匹配当前连接,再做分包解析;串口热插拔也固定用serialNumber索引设备,而不是端口号。这个习惯帮我挡掉了后续好几个莫名其妙的数据串扰问题,希望帮到你。
本文还有配套的精品资源,点击获取