- udp 文件传输上报 ACK 重传
cpp
// ==================== ResponseAndResult.h ====================
#pragma once
#include <QObject>
#include <QByteArray>
#include <QVector>
#include <QTimer>
#include <QHash>
#include <QSet>
#define MAX_FILES_PER_FRAME 10 // 每帧最多文件数
#define FRAME_RETRY_MAX 3 // 最大重传次数
#define FRAME_RETRY_INTERVAL 1000 // 重传间隔(ms)
#define FRAME_ACK_TIMEOUT 3000 // 帧确认超时(ms)
// 帧信息结构
struct PendingFrame {
int frameIndex; // 帧序号
QByteArray data; // 帧数据
int retryCount = 0; // 已重传次数
QTimer* retryTimer = nullptr; // 重传定时器
QTimer* ackTimeoutTimer = nullptr; // 确认超时定时器
};
class ResponseAndResult : public QObject
{
Q_OBJECT
public:
explicit ResponseAndResult(QObject* parent = nullptr);
~ResponseAndResult();
int ReportFileListInfoResult(QByteArray cmdInfo);
signals:
void sig_send_monitor_data(QByteArray data);
void sig_fileListReportComplete(bool success, int totalFrames, int ackedFrames);
void sig_frameAckReceived(int frameIndex);
void sig_frameRetry(int frameIndex, int retryCount);
public slots:
// 接收ACK确认
void onFileListAckReceived(int frameIndex);
// 全部帧重传
void onRetryAllFrames();
private:
// 构建单帧数据
QByteArray buildFrame(const FileListCtrlReport& response,
const QVector<BaseFileInfoParams>& vecFileInfo,
int& nVecIndex, int frameIndex, int nFrameCnt, int nRemain);
// 发送单帧
void sendFrame(int frameIndex);
// 开始重传定时器
void startRetryTimer(int frameIndex);
// 停止所有定时器
void stopAllTimers();
// 检查是否全部完成
void checkAllFramesComplete();
private:
QHash<int, PendingFrame*> m_pendingFrames; // 待确认的帧
int m_totalFrames = 0; // 总帧数
int m_ackedFrames = 0; // 已确认帧数
int m_nFileCnt = 0; // 文件总数
QSet<int> m_ackedFrameIds; // 已确认的帧ID集合
bool m_reportComplete = false; // 上报是否完成
};
// ==================== ResponseAndResult.cpp ====================
#include "ResponseAndResult.h"
ResponseAndResult::ResponseAndResult(QObject* parent)
: QObject(parent)
{
}
ResponseAndResult::~ResponseAndResult()
{
stopAllTimers();
}
// ========== 主函数:启动文件列表上报 ==========
int ResponseAndResult::ReportFileListInfoResult(QByteArray cmdInfo)
{
// ===== 1. 解析命令 =====
QVector<BaseFileInfoParams> vecFileInfo;
FileListSelectCMD cmd;
memcpy(&cmd, cmdInfo.data(), sizeof(cmd));
// ===== 2. 构建响应模板 =====
FileListCtrlReport response;
memcpy(&response.net_header, &cmd.net_head, sizeof(response.net_header));
response.net_header.BID = PROCESS_CTRLRESULT_REP;
response.net_header.DID = cmd.net_head.SID;
response.net_header.SID = cmd.net_head.DID;
response.process_result_head.process_cmd_id =
(queryfilelist_rep & 0xff00) | ((~(queryfilelist_rep & 0x00ff)) & 0x00ff);
// ===== 3. 获取文件信息 =====
bool bRet = DbManager::getInstance()->getAllFileInfo(vecFileInfo);
m_nFileCnt = vecFileInfo.size();
response.result_info.file_total_number = m_nFileCnt;
// ===== 4. 计算分帧 =====
int nFrameCnt = m_nFileCnt / MAX_FILES_PER_FRAME;
int nRemain = m_nFileCnt % MAX_FILES_PER_FRAME;
if (nFrameCnt > 0 && nRemain != 0) {
nFrameCnt += 1;
} else if (nFrameCnt <= 0 && nRemain != 0) {
nFrameCnt = 1;
}
// 处理空文件列表
if (m_nFileCnt == 0) {
nFrameCnt = 1;
}
m_totalFrames = nFrameCnt;
m_ackedFrames = 0;
m_ackedFrameIds.clear();
m_reportComplete = false;
response.result_info.file_list_total_frame = nFrameCnt;
// ===== 5. 构建并发送所有帧 =====
int nVecIndex = 0;
for (int frameIndex = 0; frameIndex < nFrameCnt; frameIndex++) {
// 构建帧数据
QByteArray frameData = buildFrame(response, vecFileInfo, nVecIndex,
frameIndex, nFrameCnt, nRemain);
// 创建待确认帧信息
PendingFrame* pendingFrame = new PendingFrame();
pendingFrame->frameIndex = frameIndex;
pendingFrame->data = frameData;
pendingFrame->retryCount = 0;
m_pendingFrames[frameIndex] = pendingFrame;
// 发送帧
sendFrame(frameIndex);
// 启动重传定时器
startRetryTimer(frameIndex);
}
Log::getInstance()->insert(Log::Usr, Log::Info,
QStringLiteral("文件列表上报启动:总文件=%1,总帧数=%2")
.arg(m_nFileCnt).arg(m_totalFrames));
Globalutils::fileAllNum = 0;
return normal;
}
// ========== 构建单帧数据 ==========
QByteArray ResponseAndResult::buildFrame(
const FileListCtrlReport& responseTemplate,
const QVector<BaseFileInfoParams>& vecFileInfo,
int& nVecIndex, int frameIndex, int nFrameCnt, int nRemain)
{
FileListCtrlReport response = responseTemplate;
QByteArray BA;
response.result_info.file_list_current_frame = frameIndex + 1;
if (m_nFileCnt == 0) {
// 空文件列表
response.result_info.file_current_frame_number = 0;
response.net_header.DataLen = sizeof(response) - sizeof(NetTransmitHead);
BA.append((char*)&response, sizeof(response));
}
else if (frameIndex < nFrameCnt - 1) {
// 非最后一帧:装满
response.result_info.file_current_frame_number = MAX_FILES_PER_FRAME;
response.net_header.DataLen = sizeof(response) - sizeof(NetTransmitHead)
+ sizeof(BaseFileInfoParams) * MAX_FILES_PER_FRAME;
BA.append((char*)&response, sizeof(response));
for (int i = 0; i < MAX_FILES_PER_FRAME; i++) {
BaseFileInfoParams fileInfo = vecFileInfo.at(nVecIndex++);
BA.append((char*)&fileInfo, sizeof(fileInfo));
}
}
else {
// 最后一帧
int lastFrameCount = (nRemain == 0) ? MAX_FILES_PER_FRAME : nRemain;
response.result_info.file_current_frame_number = lastFrameCount;
response.net_header.DataLen = sizeof(response) - sizeof(NetTransmitHead)
+ sizeof(BaseFileInfoParams) * lastFrameCount;
BA.append((char*)&response, sizeof(response));
for (int i = 0; i < lastFrameCount; i++) {
BaseFileInfoParams fileInfo = vecFileInfo.at(nVecIndex++);
BA.append((char*)&fileInfo, sizeof(fileInfo));
}
}
return BA;
}
// ========== 发送单帧 ==========
void ResponseAndResult::sendFrame(int frameIndex)
{
if (!m_pendingFrames.contains(frameIndex)) {
return;
}
PendingFrame* frame = m_pendingFrames[frameIndex];
Log::getInstance()->insert(Log::Usr, Log::Info,
QStringLiteral("发送帧 [%1/%2],重传次数:%3")
.arg(frameIndex + 1).arg(m_totalFrames).arg(frame->retryCount));
Log::getInstance()->insert(Log::Usr, Log::Error,
QStringLiteral("文件列表上报数据") + toHexString(frame->data));
emit sig_send_monitor_data(frame->data);
}
// ========== 启动重传定时器 ==========
void ResponseAndResult::startRetryTimer(int frameIndex)
{
if (!m_pendingFrames.contains(frameIndex)) {
return;
}
PendingFrame* frame = m_pendingFrames[frameIndex];
// 停止旧定时器
if (frame->retryTimer) {
frame->retryTimer->stop();
delete frame->retryTimer;
}
if (frame->ackTimeoutTimer) {
frame->ackTimeoutTimer->stop();
delete frame->ackTimeoutTimer;
}
// 创建重传定时器
frame->retryTimer = new QTimer(this);
frame->retryTimer->setSingleShot(true);
connect(frame->retryTimer, &QTimer::timeout, this, [this, frameIndex]() {
if (!m_pendingFrames.contains(frameIndex)) {
return;
}
PendingFrame* f = m_pendingFrames[frameIndex];
if (f->retryCount < FRAME_RETRY_MAX) {
// 重传
f->retryCount++;
emit sig_frameRetry(frameIndex, f->retryCount);
Log::getInstance()->insert(Log::Usr, Log::Warning,
QStringLiteral("帧 [%1/%2] 超时,第%3次重传")
.arg(frameIndex + 1).arg(m_totalFrames).arg(f->retryCount));
sendFrame(frameIndex);
startRetryTimer(frameIndex); // 重新启动定时器
}
else {
// 超过最大重传次数,放弃
Log::getInstance()->insert(Log::Usr, Log::Error,
QStringLiteral("帧 [%1/%2] 重传失败,已达最大重传次数%3")
.arg(frameIndex + 1).arg(m_totalFrames).arg(FRAME_RETRY_MAX));
emit sig_fileListReportComplete(false, m_totalFrames, m_ackedFrames);
}
});
frame->retryTimer->start(FRAME_RETRY_INTERVAL);
// 创建总超时定时器(帧级别的超时,不是重传间隔)
frame->ackTimeoutTimer = new QTimer(this);
frame->ackTimeoutTimer->setSingleShot(true);
connect(frame->ackTimeoutTimer, &QTimer::timeout, this, [this, frameIndex]() {
if (m_pendingFrames.contains(frameIndex)) {
Log::getInstance()->insert(Log::Usr, Log::Error,
QStringLiteral("帧 [%1/%2] 总超时,未收到ACK")
.arg(frameIndex + 1).arg(m_totalFrames));
}
});
frame->ackTimeoutTimer->start(FRAME_ACK_TIMEOUT);
}
// ========== 接收ACK确认 ==========
void ResponseAndResult::onFileListAckReceived(int frameIndex)
{
// frameIndex 是0-based的帧序号
if (m_ackedFrameIds.contains(frameIndex)) {
// 重复ACK,忽略
Log::getInstance()->insert(Log::Usr, Log::Warning,
QStringLiteral("收到重复ACK:帧[%1/%2]")
.arg(frameIndex + 1).arg(m_totalFrames));
return;
}
if (!m_pendingFrames.contains(frameIndex)) {
// 未知帧号
Log::getInstance()->insert(Log::Usr, Log::Warning,
QStringLiteral("收到未知帧ACK:%1").arg(frameIndex));
return;
}
// 停止该帧的定时器
PendingFrame* frame = m_pendingFrames[frameIndex];
if (frame->retryTimer) {
frame->retryTimer->stop();
delete frame->retryTimer;
frame->retryTimer = nullptr;
}
if (frame->ackTimeoutTimer) {
frame->ackTimeoutTimer->stop();
delete frame->ackTimeoutTimer;
frame->ackTimeoutTimer = nullptr;
}
// 标记已确认
m_ackedFrameIds.insert(frameIndex);
m_ackedFrames++;
Log::getInstance()->insert(Log::Usr, Log::Info,
QStringLiteral("帧 [%1/%2] ACK确认成功,已确认:%3/%4")
.arg(frameIndex + 1).arg(m_totalFrames)
.arg(m_ackedFrames).arg(m_totalFrames));
emit sig_frameAckReceived(frameIndex);
// 检查是否全部完成
checkAllFramesComplete();
}
// ========== 检查是否全部帧已确认 ==========
void ResponseAndResult::checkAllFramesComplete()
{
if (m_reportComplete) {
return;
}
if (m_ackedFrames >= m_totalFrames) {
m_reportComplete = true;
// 清理所有待确认帧
for (auto it = m_pendingFrames.begin(); it != m_pendingFrames.end(); ++it) {
if (it.value()->retryTimer) {
it.value()->retryTimer->stop();
delete it.value()->retryTimer;
}
if (it.value()->ackTimeoutTimer) {
it.value()->ackTimeoutTimer->stop();
delete it.value()->ackTimeoutTimer;
}
delete it.value();
}
m_pendingFrames.clear();
Log::getInstance()->insert(Log::Usr, Log::Info,
QStringLiteral("文件列表上报完成!所有%1帧已确认")
.arg(m_totalFrames));
emit sig_fileListReportComplete(true, m_totalFrames, m_ackedFrames);
}
}
// ========== 全部帧重传(外部触发) ==========
void ResponseAndResult::onRetryAllFrames()
{
Log::getInstance()->insert(Log::Usr, Log::Warning,
QStringLiteral("收到全部重传请求,重新发送所有帧"));
// 重置所有帧的重传计数
for (auto it = m_pendingFrames.begin(); it != m_pendingFrames.end(); ++it) {
it.value()->retryCount = 0;
sendFrame(it.key());
startRetryTimer(it.key());
}
}
// ========== 停止所有定时器 ==========
void ResponseAndResult::stopAllTimers()
{
for (auto it = m_pendingFrames.begin(); it != m_pendingFrames.end(); ++it) {
if (it.value()->retryTimer) {
it.value()->retryTimer->stop();
delete it.value()->retryTimer;
}
if (it.value()->ackTimeoutTimer) {
it.value()->ackTimeoutTimer->stop();
delete it.value()->ackTimeoutTimer;
}
delete it.value();
}
m_pendingFrames.clear();
}
控制端:
cpp
// ==================== FileListReceiver.h ====================
class FileListReceiver : public QObject
{
Q_OBJECT
public:
explicit FileListReceiver(QObject* parent = nullptr);
void onDataReceived(const QByteArray& data); // 收到UDP数据
QVector<BaseFileInfoParams> getCompleteFileList() const { return m_allFiles; }
signals:
void sig_fileListComplete(); // 文件列表接收完成
void sig_frameReceived(int frameIndex, int totalFrames);
void sig_needRetransmit(int frameIndex); // 请求重传某帧
private:
void sendAck(int frameIndex); // 发送ACK确认
void checkComplete(); // 检查是否收齐
void requestMissingFrames(); // 请求缺失的帧
private:
int m_totalFrames = 0;
int m_totalFiles = 0;
QSet<int> m_receivedFrames;
QVector<BaseFileInfoParams> m_allFiles;
QTimer* m_completeCheckTimer = nullptr;
bool m_complete = false;
};
// ==================== FileListReceiver.cpp ====================
void FileListReceiver::onDataReceived(const QByteArray& data)
{
if (data.size() < sizeof(FileListCtrlReport)) {
return;
}
const FileListCtrlReport* header = (const FileListCtrlReport*)data.data();
// 验证BID
if (header->net_header.BID != PROCESS_CTRLRESULT_REP) {
return;
}
int totalFrames = header->result_info.file_list_total_frame;
int currentFrame = header->result_info.file_list_current_frame - 1; // 转0-based
int fileCount = header->result_info.file_current_frame_number;
int totalFiles = header->result_info.file_total_number;
// 初始化
if (!m_complete) {
m_totalFrames = totalFrames;
m_totalFiles = totalFiles;
}
// 去重
if (m_receivedFrames.contains(currentFrame)) {
sendAck(currentFrame); // 重复也回ACK(防止ACK丢失)
return;
}
// 提取文件信息
const BaseFileInfoParams* files =
(const BaseFileInfoParams*)(data.data() + sizeof(FileListCtrlReport));
for (int i = 0; i < fileCount; i++) {
m_allFiles.append(files[i]);
}
m_receivedFrames.insert(currentFrame);
Log::getInstance()->insert(Log::Usr, Log::Info,
QStringLiteral("收到帧 [%1/%2],本帧%3个文件,累计%4个文件")
.arg(currentFrame + 1).arg(totalFrames).arg(fileCount).arg(m_allFiles.size()));
emit sig_frameReceived(currentFrame, totalFrames);
// 发送ACK
sendAck(currentFrame);
// 检查是否收齐
checkComplete();
// 启动缺失帧检查定时器
if (!m_complete && !m_completeCheckTimer) {
m_completeCheckTimer = new QTimer(this);
m_completeCheckTimer->setSingleShot(true);
connect(m_completeCheckTimer, &QTimer::timeout, this, [this]() {
if (!m_complete) {
requestMissingFrames();
}
});
m_completeCheckTimer->start(5000); // 5秒后检查
}
}
void FileListReceiver::sendAck(int frameIndex)
{
// 构建ACK包(简化版,实际需要按你的协议格式)
QByteArray ackData;
// ... 填充ACK包结构
// ackData包含:frameIndex, totalFrames等
// 发送ACK(通过UDP发回工控机)
// emit sig_send_ack(ackData);
Log::getInstance()->insert(Log::Usr, Log::Info,
QStringLiteral("发送ACK:帧[%1/%2]").arg(frameIndex + 1).arg(m_totalFrames));
}
void FileListReceiver::checkComplete()
{
if (m_receivedFrames.size() >= m_totalFrames && !m_complete) {
m_complete = true;
if (m_completeCheckTimer) {
m_completeCheckTimer->stop();
}
Log::getInstance()->insert(Log::Usr, Log::Info,
QStringLiteral("文件列表接收完成!共%1个文件").arg(m_allFiles.size()));
emit sig_fileListComplete();
}
}
void FileListReceiver::requestMissingFrames()
{
for (int i = 0; i < m_totalFrames; i++) {
if (!m_receivedFrames.contains(i)) {
Log::getInstance()->insert(Log::Usr, Log::Warning,
QStringLiteral("请求重传缺失帧:[%1/%2]").arg(i + 1).arg(m_totalFrames));
emit sig_needRetransmit(i);
}
}
// 重新启动检查定时器
if (m_completeCheckTimer) {
m_completeCheckTimer->start(5000);
}
}
交互时序图
工控机(发送端) 控制中心(接收端) │ │ │──── 帧1/3 (10个文件) ────────────────→│ │ │── 保存文件1-10 │←── ACK: 帧1 ────────────────────────│ │ │ │──── 帧2/3 (10个文件) ─────→✗ 丢失 │ │ │ │──── 帧3/3 (5个文件) ─────────────────→│ │ │── 保存文件21-25 │←── ACK: 帧3 ────────────────────────│ │ │ │ 1秒后超时,帧2未收到ACK │ │ │ │──── 帧2/3 (重传#1) ──────────────────→│ │ │── 保存文件11-20 │←── ACK: 帧2 ────────────────────────│ │ │ │ ✓ 所有帧已确认 │ ✓ 文件列表完整 │ │ │ emit sig_fileListReportComplete(true) │ emit sig_fileListComplete()
- udp 分包
分包传输在工控机中的定位分析
分包传输确实是工控机大数据传输的核心方法,但它不是孤立使用的,而是一整套策略中的一环。下面从多个维度分析:
一、为什么需要分包传输
根本原因:UDP的物理限制
┌─────────────────────────────────────────────────────────────┐ │ 以太网 MTU:1500 字节 │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ IP头(20) │ UDP头(8) │ 数据(最大1472) │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ │ 如果UDP数据 > 1472字节: │ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ │ │ 分片1 │ │ 分片2 │ │ 分片3 │ ← IP层分片 │ │ └──────────┘ └──────────┘ └──────────┘ │ │ │ │ 风险:任意一个分片丢失 → 整个UDP包丢弃! │ │ 分片越多,丢包概率越大(N个分片,丢包率 ≈ N × 单包丢包率) │ └─────────────────────────────────────────────────────────────┘工控机场景的特殊性
典型工控机数据量: ┌──────────────────────────────────────┐ │ 文件列表:100个文件 × 580字节 = 58KB │ ← 需要分包 │ 频谱数据:1秒采集 = 2MB │ ← 需要分包 │ 日志文件:几MB到几十MB │ ← 需要分包 │ 状态信息:几百字节 │ ← 不需要分包 │ 控制命令:几十字节 │ ← 不需要分包 └──────────────────────────────────────┘
二、工控机常用的大数据传输方法对比
方法1:UDP分包传输(你代码中使用的方法)
┌─────────────────────────────────────────────────────────────┐ │ 适用场景:实时性要求高、可容忍少量丢包 │ │ │ │ 优点: │ │ ✓ 无连接建立开销,延迟低 │ │ ✓ 支持广播/组播 │ │ ✓ 实现简单,不依赖OS协议栈复杂功能 │ │ ✓ 丢包只影响单帧,可重传 │ │ │ │ 缺点: │ │ ✗ 需要自己实现帧序号、重组、去重 │ │ ✗ 需要自己处理丢包重传 │ │ ✗ 无拥塞控制 │ │ │ │ 典型实现:你的代码 │ │ - 帧头:total_frame / current_frame / file_count │ │ - 每帧独立,接收端按帧号重组 │ │ - 丢失某帧 → 请求重传该帧 │ └─────────────────────────────────────────────────────────────┘方法2:TCP流式传输
┌─────────────────────────────────────────────────────────────┐ │ 适用场景:可靠性要求高、大文件传输 │ │ │ │ 优点: │ │ ✓ 自动分包、重组、重传、拥塞控制 │ │ ✓ 保证顺序、不丢包、不重复 │ │ ✓ 开发简单,socket编程即可 │ │ │ │ 缺点: │ │ ✗ 需要建立连接(三次握手) │ │ ✗ 延迟较高(尤其弱网环境) │ │ ✗ 不支持广播/组播 │ │ ✗ 连接断开需要重新建立 │ │ │ │ 典型实现: │ │ // 发送端 │ │ QTcpSocket socket; │ │ socket.connectToHost("192.168.1.100", 8888); │ │ socket.write(fileData); // TCP自动分包 │ │ │ │ // 接收端 │ │ connect(&socket, &QTcpSocket::readyRead, [](){ │ │ QByteArray data = socket.readAll(); │ │ }); │ └─────────────────────────────────────────────────────────────┘方法3:UDP + 应用层重传(ARQ)
┌─────────────────────────────────────────────────────────────┐ │ 适用场景:需要UDP的低延迟,又需要可靠传输 │ │ │ │ 发送端 接收端 │ │ ┌──────┐ ┌──────┐ │ │ │帧1发送│──────────────→│帧1收到│ │ │ │ │←────ACK1──────│ │ │ │ │帧2发送│──────────────→│ │ ← 帧2丢失 │ │ │ │ (超时) │ │ │ │ │帧2重发│──────────────→│帧2收到│ │ │ │ │←────ACK2──────│ │ │ │ └──────┘ └──────┘ │ │ │ │ 典型实现:KCP、UDT、QUIC │ │ 你的代码改进方向:加ACK确认机制 │ └─────────────────────────────────────────────────────────────┘方法4:文件传输协议(FTP/TFTP)
┌─────────────────────────────────────────────────────────────┐ │ 适用场景:大文件批量传输 │ │ │ │ TFTP(简单文件传输协议): │ │ - 基于UDP │ │ - 每包512字节 │ │ - 简单的ACK确认 │ │ - 适用于嵌入式/工控设备固件升级 │ │ │ │ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │请求读│────────→│数据块1│────────→│ACK 1 │ │ │ │ │←────────│ │←────────│ │ │ │ │ │────────→│数据块2│────────→│ACK 2 │ │ │ │ │←────────│ │←────────│ │ │ │ └──────┘ └──────┘ └──────┘ │ └─────────────────────────────────────────────────────────────┘
三、各种方法的适用场景对比
表格
场景 推荐方法 原因 文件列表查询(几百KB) UDP分包 数据量小,分几帧即可,延迟低 实时频谱数据(持续流) UDP分包 允许少量丢包,延迟优先 固件升级文件(几十MB) TCP 或 TFTP 完整性要求高,一个字节都不能错 录制文件下载(GB级) TCP 或 FTP 大文件,需要可靠传输 设备状态上报(几百字节) 单包UDP 一个包搞定,无需分包 控制命令 单包UDP 包小,实时性要求高 日志同步 TCP 可靠,数据量大
四、你的代码中的分包方案分析
当前方案特点
你的代码采用的是 "UDP分包 + 无确认" 模式: 发送端 接收端 │ │ │── 帧1(1/3, 10个文件) ────────→│ │── 帧2(2/3, 10个文件) ────────→│ ← 如果这帧丢了? │── 帧3(3/3, 5个文件) ────────→│ │ │ │ 没有ACK,没有重传机制 │潜在问题
// 问题1:丢帧无法感知 // 如果3帧中丢了第2帧,接收端只能看到20个文件,丢失了10个 // 接收端不知道丢了,发送端也不知道丢了 // 问题2:接收端无法判断是否收齐 // 只能通过 total_frame 和 current_frame 判断,但如果最后一帧丢了? // 接收端会一直等待,不知道已经结束了 // 问题3:没有超时重传 // 如果网络抖动,某帧延迟很大,接收端无法请求重传改进方案:添加重传机制
// 改进后的文件列表上报 class FileListReporter { private: struct FrameInfo { int frameIndex; QByteArray data; QTimer retryTimer; int retryCount = 0; static constexpr int MAX_RETRY = 3; }; QHash<int, FrameInfo> m_sentFrames; // 已发送的帧,等待ACK void sendFrame(int frameIndex) { FrameInfo info; info.frameIndex = frameIndex; info.data = buildFrame(frameIndex); // 发送 emit sig_send_monitor_data(info.data); // 设置重传定时器 info.retryTimer.setSingleShot(true); connect(&info.retryTimer, &QTimer::timeout, [this, frameIndex]() { if (m_sentFrames[frameIndex].retryCount < MAX_RETRY) { m_sentFrames[frameIndex].retryCount++; emit sig_send_monitor_data(m_sentFrames[frameIndex].data); m_sentFrames[frameIndex].retryTimer.start(1000); } else { Log::error("Frame " + QString::number(frameIndex) + " retry exhausted"); } }); info.retryTimer.start(1000); // 1秒超时 m_sentFrames[frameIndex] = info; } public slots: void onAckReceived(int frameIndex) { if (m_sentFrames.contains(frameIndex)) { m_sentFrames[frameIndex].retryTimer.stop(); m_sentFrames.remove(frameIndex); // 检查是否所有帧都已确认 if (m_sentFrames.isEmpty()) { Log::info("All frames acknowledged"); } } } };
五、工控机数据传输的完整方案
┌─────────────────────────────────┐ │ 工控机数据传输策略 │ └─────────────────────────────────┘ │ ┌─────────────┬───────────┼───────────┬─────────────┐ │ │ │ │ │ ┌─────▼─────┐ ┌────▼───┐ ┌─────▼─────┐ ┌───▼────┐ ┌─────▼─────┐ │ 小数据 │ │ 中等数据│ │ 实时流数据 │ │大文件 │ │ 可靠性 │ │ (<1KB) │ │ (1-64KB)│ │ (持续流) │ │ (>1MB) │ │ 要求极高 │ └─────┬─────┘ └────┬───┘ └─────┬─────┘ └───┬────┘ └─────┬─────┘ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ 单包UDP │ │ UDP分包 │ │ UDP分包 │ │ TCP传输 │ │ TCP + │ │ 无确认 │ │ + 重传 │ │ 允许丢包 │ │ 断点续传 │ │ 校验 │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ 控制命令 文件列表 频谱数据 固件升级 数据库同步 状态查询 文件信息 视频流 录制文件 配置备份
总结
分包传输是工控机大数据传输的基础方法,但不是唯一方法:
- UDP分包:最常用,适合实时性要求高的场景
- TCP:适合可靠性要求高的大文件传输
- UDP分包+ARQ:折中方案,兼顾实时性和可靠性(你的代码可以朝这个方向改进)
- TFTP/FTP:标准协议,适合固件升级等场景
你的代码采用的是UDP分包无确认模式,适合文件列表这种"丢了影响不大,下次查询还能拿到"的场景。对于关键数据,建议加上ACK确认和重传机制。
TCP也需要分包?FTP是什么?我的代码加上ACK重新生成,这三个问题,分别回答
问题一:TCP也需要分包吗?
结论:TCP不需要应用层手动分包,但底层自动分片
应用层视角(你写的代码): ┌─────────────────────────────────────────────────────────────────┐ │ socket.write(100MB数据); // 一次性写入,TCP自动处理一切 │ │ // 不需要循环、不需要帧号、不需要计算每帧大小 │ └─────────────────────────────────────────────────────────────────┘ 底层TCP自动做的事情: ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 100MB 数据 │ │ ┌──────────────────────────────────────────────────────────┐ │ │ │ TCP自动切分成 N 个 MSS 大小的段(通常1460字节) │ │ │ └──────────────────────────────────────────────────────────┘ │ │ │ │ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ │ │段1 │ │段2 │ │段3 │ │段4 │ ... │段N │ │ │ │seq=1 │ │seq=2 │ │seq=3 │ │seq=4 │ │seq=N │ │ │ └──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ └──┬───┘ │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ │ │ ┌──────────────────────────────────────────────────────┐ │ │ │ TCP自动: │ │ │ │ ✓ 自动分段(MSS大小) │ │ │ │ ✓ 自动编号(序列号seq) │ │ │ │ ✓ 自动确认(ACK) │ │ │ │ ✓ 自动重传(超时重传) │ │ │ │ ✓ 自动排序(乱序重排) │ │ │ │ ✓ 自动去重(重复包丢弃) │ │ │ │ ✓ 流量控制(滑动窗口) │ │ │ │ ✓ 拥塞控制(慢启动、拥塞避免) │ │ │ └──────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────┘UDP vs TCP 对比
UDP TCP 应用层:┌──────────┐ 应用层:┌──────────┐ │ 手动分包 │ │ 一次写入 │ │ 手动编号 │ │ 全部搞定 │ │ 手动重传 │ └────┬─────┘ │ 手动去重 │ │ └────┬─────┘ 传输层:┌────▼─────┐ │ │ 自动分段 │ 传输层:┌──▼───┐ │ 自动确认 │ │ UDP │ │ 自动重传 │ │ 无连接 │ │ 自动排序 │ │ 不可靠 │ │ 流量控制 │ └──────┘ └──────────┘形象比喻
UDP = 寄明信片 - 每张大小有限 - 不保证到达 - 不保证顺序 - 需要自己编号("第1/3张"、"第2/3张") - 丢了要自己重新寄 TCP = 打电话 - 建立连接后,你只管说 - 底层自动把语音切成数据包 - 保证对方听到的和你说的顺序一致 - 听不清会自动重传
问题二:FTP是什么?
FTP = File Transfer Protocol(文件传输协议)
┌─────────────────────────────────────────────────────────────────┐ │ FTP 基本架构 │ │ │ │ ┌──────────────┐ ┌──────────────┐ │ │ │ 客户端 │ │ 服务器 │ │ │ │ (控制中心) │ │ (工控机) │ │ │ └──────┬───────┘ └──────┬───────┘ │ │ │ │ │ │ │══════ 控制连接 (端口21) ═══════════════│ │ │ │ 发送命令:USER, PASS, LIST, │ │ │ │ RETR, STOR, DELE... │ │ │ │ │ │ │ │────── 数据连接 (端口20) ──────────────→│ │ │ │ 实际的文件数据传输 │ │ │ │ │ │ └─────────────────────────────────────────────────────────────────┘FTP的工作原理
控制连接(一直保持): ┌──────────┐ ┌──────────┐ │ 客户端 │──── USER admin ──→│ 服务器 │ │ │←── 331 Password ──│ │ │ │──── PASS 123 ────→│ │ │ │←── 230 Login OK ──│ │ │ │──── LIST ────────→│ │ │ │←── 150 Opening ───│ │ └──────────┘ └──────────┘ 数据连接(传输文件时建立): ┌──────────┐ ┌──────────┐ │ 客户端 │←── 文件数据 ──────│ 服务器 │ │ │ 20260726_001.dat │ │ │ │ 512MB │ │ │ │←── 226 Transfer OK │ │ └──────────┘ └──────────┘FTP常用命令
表格
命令 含义 示例 USER 用户名 USER admin PASS 密码 PASS 123456 LIST 列出文件 LIST RETR 下载文件 RETR data.dat STOR 上传文件 STOR config.ini DELE 删除文件 DELE old.dat QUIT 断开连接 QUIT 三种FTP模式对比
1. 主动模式(PORT): 客户端开放端口,服务器主动连接客户端 问题:客户端防火墙可能拦截 2. 被动模式(PASV): 服务器开放端口,客户端连接服务器 常用:适合客户端在防火墙后 3. TFTP(简单文件传输协议): 基于UDP,每包512字节 极简实现,适合嵌入式设备固件升级 ┌──────┐ ┌──────┐ │读请求│───────→│数据块1│ │ │←──ACK──│ │ │ │←───────│数据块2│ │ │──ACK──→│ │ └──────┘ └──────┘为什么你的代码不用FTP
FTP场景: 你的场景: ┌──────────────┐ ┌──────────────┐ │ 传输完整文件 │ │ 传输文件列表 │ │ 文件很大(G) │ │ 列表很小(K) │ │ 需要断点续传 │ │ 不需要续传 │ │ 需要认证登录 │ │ 已有协议认证 │ │ 标准化协议 │ │ 私有协议 │ └──────────────┘ └──────────────┘ 所以文件列表用UDP分包就够了, 真正下载录制文件时,可以考虑用FTP或TCP。
问题三:加上ACK确认的完整代码
// ==================== ResponseAndResult.h ==================== #pragma once #include <QObject> #include <QByteArray> #include <QVector> #include <QTimer> #include <QHash> #include <QSet> #define MAX_FILES_PER_FRAME 10 // 每帧最多文件数 #define FRAME_RETRY_MAX 3 // 最大重传次数 #define FRAME_RETRY_INTERVAL 1000 // 重传间隔(ms) #define FRAME_ACK_TIMEOUT 3000 // 帧确认超时(ms) // 帧信息结构 struct PendingFrame { int frameIndex; // 帧序号 QByteArray data; // 帧数据 int retryCount = 0; // 已重传次数 QTimer* retryTimer = nullptr; // 重传定时器 QTimer* ackTimeoutTimer = nullptr; // 确认超时定时器 }; class ResponseAndResult : public QObject { Q_OBJECT public: explicit ResponseAndResult(QObject* parent = nullptr); ~ResponseAndResult(); int ReportFileListInfoResult(QByteArray cmdInfo); signals: void sig_send_monitor_data(QByteArray data); void sig_fileListReportComplete(bool success, int totalFrames, int ackedFrames); void sig_frameAckReceived(int frameIndex); void sig_frameRetry(int frameIndex, int retryCount); public slots: // 接收ACK确认 void onFileListAckReceived(int frameIndex); // 全部帧重传 void onRetryAllFrames(); private: // 构建单帧数据 QByteArray buildFrame(const FileListCtrlReport& response, const QVector<BaseFileInfoParams>& vecFileInfo, int& nVecIndex, int frameIndex, int nFrameCnt, int nRemain); // 发送单帧 void sendFrame(int frameIndex); // 开始重传定时器 void startRetryTimer(int frameIndex); // 停止所有定时器 void stopAllTimers(); // 检查是否全部完成 void checkAllFramesComplete(); private: QHash<int, PendingFrame*> m_pendingFrames; // 待确认的帧 int m_totalFrames = 0; // 总帧数 int m_ackedFrames = 0; // 已确认帧数 int m_nFileCnt = 0; // 文件总数 QSet<int> m_ackedFrameIds; // 已确认的帧ID集合 bool m_reportComplete = false; // 上报是否完成 }; // ==================== ResponseAndResult.cpp ==================== #include "ResponseAndResult.h" ResponseAndResult::ResponseAndResult(QObject* parent) : QObject(parent) { } ResponseAndResult::~ResponseAndResult() { stopAllTimers(); } // ========== 主函数:启动文件列表上报 ========== int ResponseAndResult::ReportFileListInfoResult(QByteArray cmdInfo) { // ===== 1. 解析命令 ===== QVector<BaseFileInfoParams> vecFileInfo; FileListSelectCMD cmd; memcpy(&cmd, cmdInfo.data(), sizeof(cmd)); // ===== 2. 构建响应模板 ===== FileListCtrlReport response; memcpy(&response.net_header, &cmd.net_head, sizeof(response.net_header)); response.net_header.BID = PROCESS_CTRLRESULT_REP; response.net_header.DID = cmd.net_head.SID; response.net_header.SID = cmd.net_head.DID; response.process_result_head.process_cmd_id = (queryfilelist_rep & 0xff00) | ((~(queryfilelist_rep & 0x00ff)) & 0x00ff); // ===== 3. 获取文件信息 ===== bool bRet = DbManager::getInstance()->getAllFileInfo(vecFileInfo); m_nFileCnt = vecFileInfo.size(); response.result_info.file_total_number = m_nFileCnt; // ===== 4. 计算分帧 ===== int nFrameCnt = m_nFileCnt / MAX_FILES_PER_FRAME; int nRemain = m_nFileCnt % MAX_FILES_PER_FRAME; if (nFrameCnt > 0 && nRemain != 0) { nFrameCnt += 1; } else if (nFrameCnt <= 0 && nRemain != 0) { nFrameCnt = 1; } // 处理空文件列表 if (m_nFileCnt == 0) { nFrameCnt = 1; } m_totalFrames = nFrameCnt; m_ackedFrames = 0; m_ackedFrameIds.clear(); m_reportComplete = false; response.result_info.file_list_total_frame = nFrameCnt; // ===== 5. 构建并发送所有帧 ===== int nVecIndex = 0; for (int frameIndex = 0; frameIndex < nFrameCnt; frameIndex++) { // 构建帧数据 QByteArray frameData = buildFrame(response, vecFileInfo, nVecIndex, frameIndex, nFrameCnt, nRemain); // 创建待确认帧信息 PendingFrame* pendingFrame = new PendingFrame(); pendingFrame->frameIndex = frameIndex; pendingFrame->data = frameData; pendingFrame->retryCount = 0; m_pendingFrames[frameIndex] = pendingFrame; // 发送帧 sendFrame(frameIndex); // 启动重传定时器 startRetryTimer(frameIndex); } Log::getInstance()->insert(Log::Usr, Log::Info, QStringLiteral("文件列表上报启动:总文件=%1,总帧数=%2") .arg(m_nFileCnt).arg(m_totalFrames)); Globalutils::fileAllNum = 0; return normal; } // ========== 构建单帧数据 ========== QByteArray ResponseAndResult::buildFrame( const FileListCtrlReport& responseTemplate, const QVector<BaseFileInfoParams>& vecFileInfo, int& nVecIndex, int frameIndex, int nFrameCnt, int nRemain) { FileListCtrlReport response = responseTemplate; QByteArray BA; response.result_info.file_list_current_frame = frameIndex + 1; if (m_nFileCnt == 0) { // 空文件列表 response.result_info.file_current_frame_number = 0; response.net_header.DataLen = sizeof(response) - sizeof(NetTransmitHead); BA.append((char*)&response, sizeof(response)); } else if (frameIndex < nFrameCnt - 1) { // 非最后一帧:装满 response.result_info.file_current_frame_number = MAX_FILES_PER_FRAME; response.net_header.DataLen = sizeof(response) - sizeof(NetTransmitHead) + sizeof(BaseFileInfoParams) * MAX_FILES_PER_FRAME; BA.append((char*)&response, sizeof(response)); for (int i = 0; i < MAX_FILES_PER_FRAME; i++) { BaseFileInfoParams fileInfo = vecFileInfo.at(nVecIndex++); BA.append((char*)&fileInfo, sizeof(fileInfo)); } } else { // 最后一帧 int lastFrameCount = (nRemain == 0) ? MAX_FILES_PER_FRAME : nRemain; response.result_info.file_current_frame_number = lastFrameCount; response.net_header.DataLen = sizeof(response) - sizeof(NetTransmitHead) + sizeof(BaseFileInfoParams) * lastFrameCount; BA.append((char*)&response, sizeof(response)); for (int i = 0; i < lastFrameCount; i++) { BaseFileInfoParams fileInfo = vecFileInfo.at(nVecIndex++); BA.append((char*)&fileInfo, sizeof(fileInfo)); } } return BA; } // ========== 发送单帧 ========== void ResponseAndResult::sendFrame(int frameIndex) { if (!m_pendingFrames.contains(frameIndex)) { return; } PendingFrame* frame = m_pendingFrames[frameIndex]; Log::getInstance()->insert(Log::Usr, Log::Info, QStringLiteral("发送帧 [%1/%2],重传次数:%3") .arg(frameIndex + 1).arg(m_totalFrames).arg(frame->retryCount)); Log::getInstance()->insert(Log::Usr, Log::Error, QStringLiteral("文件列表上报数据") + toHexString(frame->data)); emit sig_send_monitor_data(frame->data); } // ========== 启动重传定时器 ========== void ResponseAndResult::startRetryTimer(int frameIndex) { if (!m_pendingFrames.contains(frameIndex)) { return; } PendingFrame* frame = m_pendingFrames[frameIndex]; // 停止旧定时器 if (frame->retryTimer) { frame->retryTimer->stop(); delete frame->retryTimer; } if (frame->ackTimeoutTimer) { frame->ackTimeoutTimer->stop(); delete frame->ackTimeoutTimer; } // 创建重传定时器 frame->retryTimer = new QTimer(this); frame->retryTimer->setSingleShot(true); connect(frame->retryTimer, &QTimer::timeout, this, [this, frameIndex]() { if (!m_pendingFrames.contains(frameIndex)) { return; } PendingFrame* f = m_pendingFrames[frameIndex]; if (f->retryCount < FRAME_RETRY_MAX) { // 重传 f->retryCount++; emit sig_frameRetry(frameIndex, f->retryCount); Log::getInstance()->insert(Log::Usr, Log::Warning, QStringLiteral("帧 [%1/%2] 超时,第%3次重传") .arg(frameIndex + 1).arg(m_totalFrames).arg(f->retryCount)); sendFrame(frameIndex); startRetryTimer(frameIndex); // 重新启动定时器 } else { // 超过最大重传次数,放弃 Log::getInstance()->insert(Log::Usr, Log::Error, QStringLiteral("帧 [%1/%2] 重传失败,已达最大重传次数%3") .arg(frameIndex + 1).arg(m_totalFrames).arg(FRAME_RETRY_MAX)); emit sig_fileListReportComplete(false, m_totalFrames, m_ackedFrames); } }); frame->retryTimer->start(FRAME_RETRY_INTERVAL); // 创建总超时定时器(帧级别的超时,不是重传间隔) frame->ackTimeoutTimer = new QTimer(this); frame->ackTimeoutTimer->setSingleShot(true); connect(frame->ackTimeoutTimer, &QTimer::timeout, this, [this, frameIndex]() { if (m_pendingFrames.contains(frameIndex)) { Log::getInstance()->insert(Log::Usr, Log::Error, QStringLiteral("帧 [%1/%2] 总超时,未收到ACK") .arg(frameIndex + 1).arg(m_totalFrames)); } }); frame->ackTimeoutTimer->start(FRAME_ACK_TIMEOUT); } // ========== 接收ACK确认 ========== void ResponseAndResult::onFileListAckReceived(int frameIndex) { // frameIndex 是0-based的帧序号 if (m_ackedFrameIds.contains(frameIndex)) { // 重复ACK,忽略 Log::getInstance()->insert(Log::Usr, Log::Warning, QStringLiteral("收到重复ACK:帧[%1/%2]") .arg(frameIndex + 1).arg(m_totalFrames)); return; } if (!m_pendingFrames.contains(frameIndex)) { // 未知帧号 Log::getInstance()->insert(Log::Usr, Log::Warning, QStringLiteral("收到未知帧ACK:%1").arg(frameIndex)); return; } // 停止该帧的定时器 PendingFrame* frame = m_pendingFrames[frameIndex]; if (frame->retryTimer) { frame->retryTimer->stop(); delete frame->retryTimer; frame->retryTimer = nullptr; } if (frame->ackTimeoutTimer) { frame->ackTimeoutTimer->stop(); delete frame->ackTimeoutTimer; frame->ackTimeoutTimer = nullptr; } // 标记已确认 m_ackedFrameIds.insert(frameIndex); m_ackedFrames++; Log::getInstance()->insert(Log::Usr, Log::Info, QStringLiteral("帧 [%1/%2] ACK确认成功,已确认:%3/%4") .arg(frameIndex + 1).arg(m_totalFrames) .arg(m_ackedFrames).arg(m_totalFrames)); emit sig_frameAckReceived(frameIndex); // 检查是否全部完成 checkAllFramesComplete(); } // ========== 检查是否全部帧已确认 ========== void ResponseAndResult::checkAllFramesComplete() { if (m_reportComplete) { return; } if (m_ackedFrames >= m_totalFrames) { m_reportComplete = true; // 清理所有待确认帧 for (auto it = m_pendingFrames.begin(); it != m_pendingFrames.end(); ++it) { if (it.value()->retryTimer) { it.value()->retryTimer->stop(); delete it.value()->retryTimer; } if (it.value()->ackTimeoutTimer) { it.value()->ackTimeoutTimer->stop(); delete it.value()->ackTimeoutTimer; } delete it.value(); } m_pendingFrames.clear(); Log::getInstance()->insert(Log::Usr, Log::Info, QStringLiteral("文件列表上报完成!所有%1帧已确认") .arg(m_totalFrames)); emit sig_fileListReportComplete(true, m_totalFrames, m_ackedFrames); } } // ========== 全部帧重传(外部触发) ========== void ResponseAndResult::onRetryAllFrames() { Log::getInstance()->insert(Log::Usr, Log::Warning, QStringLiteral("收到全部重传请求,重新发送所有帧")); // 重置所有帧的重传计数 for (auto it = m_pendingFrames.begin(); it != m_pendingFrames.end(); ++it) { it.value()->retryCount = 0; sendFrame(it.key()); startRetryTimer(it.key()); } } // ========== 停止所有定时器 ========== void ResponseAndResult::stopAllTimers() { for (auto it = m_pendingFrames.begin(); it != m_pendingFrames.end(); ++it) { if (it.value()->retryTimer) { it.value()->retryTimer->stop(); delete it.value()->retryTimer; } if (it.value()->ackTimeoutTimer) { it.value()->ackTimeoutTimer->stop(); delete it.value()->ackTimeoutTimer; } delete it.value(); } m_pendingFrames.clear(); }
接收端(控制中心)配套代码
// ==================== FileListReceiver.h ==================== class FileListReceiver : public QObject { Q_OBJECT public: explicit FileListReceiver(QObject* parent = nullptr); void onDataReceived(const QByteArray& data); // 收到UDP数据 QVector<BaseFileInfoParams> getCompleteFileList() const { return m_allFiles; } signals: void sig_fileListComplete(); // 文件列表接收完成 void sig_frameReceived(int frameIndex, int totalFrames); void sig_needRetransmit(int frameIndex); // 请求重传某帧 private: void sendAck(int frameIndex); // 发送ACK确认 void checkComplete(); // 检查是否收齐 void requestMissingFrames(); // 请求缺失的帧 private: int m_totalFrames = 0; int m_totalFiles = 0; QSet<int> m_receivedFrames; QVector<BaseFileInfoParams> m_allFiles; QTimer* m_completeCheckTimer = nullptr; bool m_complete = false; }; // ==================== FileListReceiver.cpp ==================== void FileListReceiver::onDataReceived(const QByteArray& data) { if (data.size() < sizeof(FileListCtrlReport)) { return; } const FileListCtrlReport* header = (const FileListCtrlReport*)data.data(); // 验证BID if (header->net_header.BID != PROCESS_CTRLRESULT_REP) { return; } int totalFrames = header->result_info.file_list_total_frame; int currentFrame = header->result_info.file_list_current_frame - 1; // 转0-based int fileCount = header->result_info.file_current_frame_number; int totalFiles = header->result_info.file_total_number; // 初始化 if (!m_complete) { m_totalFrames = totalFrames; m_totalFiles = totalFiles; } // 去重 if (m_receivedFrames.contains(currentFrame)) { sendAck(currentFrame); // 重复也回ACK(防止ACK丢失) return; } // 提取文件信息 const BaseFileInfoParams* files = (const BaseFileInfoParams*)(data.data() + sizeof(FileListCtrlReport)); for (int i = 0; i < fileCount; i++) { m_allFiles.append(files[i]); } m_receivedFrames.insert(currentFrame); Log::getInstance()->insert(Log::Usr, Log::Info, QStringLiteral("收到帧 [%1/%2],本帧%3个文件,累计%4个文件") .arg(currentFrame + 1).arg(totalFrames).arg(fileCount).arg(m_allFiles.size())); emit sig_frameReceived(currentFrame, totalFrames); // 发送ACK sendAck(currentFrame); // 检查是否收齐 checkComplete(); // 启动缺失帧检查定时器 if (!m_complete && !m_completeCheckTimer) { m_completeCheckTimer = new QTimer(this); m_completeCheckTimer->setSingleShot(true); connect(m_completeCheckTimer, &QTimer::timeout, this, [this]() { if (!m_complete) { requestMissingFrames(); } }); m_completeCheckTimer->start(5000); // 5秒后检查 } } void FileListReceiver::sendAck(int frameIndex) { // 构建ACK包(简化版,实际需要按你的协议格式) QByteArray ackData; // ... 填充ACK包结构 // ackData包含:frameIndex, totalFrames等 // 发送ACK(通过UDP发回工控机) // emit sig_send_ack(ackData); Log::getInstance()->insert(Log::Usr, Log::Info, QStringLiteral("发送ACK:帧[%1/%2]").arg(frameIndex + 1).arg(m_totalFrames)); } void FileListReceiver::checkComplete() { if (m_receivedFrames.size() >= m_totalFrames && !m_complete) { m_complete = true; if (m_completeCheckTimer) { m_completeCheckTimer->stop(); } Log::getInstance()->insert(Log::Usr, Log::Info, QStringLiteral("文件列表接收完成!共%1个文件").arg(m_allFiles.size())); emit sig_fileListComplete(); } } void FileListReceiver::requestMissingFrames() { for (int i = 0; i < m_totalFrames; i++) { if (!m_receivedFrames.contains(i)) { Log::getInstance()->insert(Log::Usr, Log::Warning, QStringLiteral("请求重传缺失帧:[%1/%2]").arg(i + 1).arg(m_totalFrames)); emit sig_needRetransmit(i); } } // 重新启动检查定时器 if (m_completeCheckTimer) { m_completeCheckTimer->start(5000); } }
完整交互时序图
工控机(发送端) 控制中心(接收端) │ │ │──── 帧1/3 (10个文件) ────────────────→│ │ │── 保存文件1-10 │←── ACK: 帧1 ────────────────────────│ │ │ │──── 帧2/3 (10个文件) ─────→✗ 丢失 │ │ │ │──── 帧3/3 (5个文件) ─────────────────→│ │ │── 保存文件21-25 │←── ACK: 帧3 ────────────────────────│ │ │ │ [1秒后超时,帧2未收到ACK] │ │ │ │──── 帧2/3 (重传#1) ──────────────────→│ │ │── 保存文件11-20 │←── ACK: 帧2 ────────────────────────│ │ │ │ ✓ 所有帧已确认 │ ✓ 文件列表完整 │ │ │ emit sig_fileListReportComplete(true) │ emit sig_fileListComplete()