实现资源断点下载

客户端断点下载资源

客户端请求下载

在客户端加载本地资源发现不存在的时候,需要请求服务器,获取资源。

如果资源比较大,需要分批下载,也就是支持断点下载。

我们先拿UserInfoPage举例

  1. UserInfoPage::UserInfoPage(QWidget *parent) :
  2. QWidget(parent),
  3. ui(new Ui::UserInfoPage)
  4. {
  5. ui->setupUi(this);
  6. auto icon = UserMgr::GetInstance()->GetIcon();
  7. qDebug() << "icon is " << icon ;
  8. //使用正则表达式检查是否使用默认头像
  9. QRegularExpression regex("^:/res/head_(\\d+)\\.jpg$");
  10. QRegularExpressionMatch match = regex.match(icon);
  11. if (match.hasMatch()) {
  12. QPixmap pixmap(icon);
  13. QPixmap scaledPixmap = pixmap.scaled(ui->head_lb->size(),
  14. Qt::KeepAspectRatio, Qt::SmoothTransformation); // 将图片缩放到label的大小
  15. ui->head_lb->setPixmap(scaledPixmap); // 将缩放后的图片设置到QLabel上
  16. ui->head_lb->setScaledContents(true); // 设置QLabel自动缩放图片内容以适应大小
  17. }
  18. else {
  19. // 如果是用户上传的头像,获取存储目录
  20. QString storageDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
  21. QDir avatarsDir(storageDir + "/avatars");
  22. // 确保目录存在
  23. if (avatarsDir.exists()) {
  24. auto file_name = QFileInfo(icon).fileName();
  25. QString avatarPath = avatarsDir.filePath(QFileInfo(icon).fileName()); // 获取上传头像的完整路径
  26. QPixmap pixmap(avatarPath); // 加载上传的头像图片
  27. if (!pixmap.isNull()) {
  28. //判断是否正在下载
  29. bool is_loading = UserMgr::GetInstance()->IsDownLoading(file_name);
  30. if (is_loading) {
  31. qWarning() << "正在下载: " << file_name;
  32. //先加载默认的
  33. QPixmap pixmap(":/res/head_1.jpg");
  34. QPixmap scaledPixmap = pixmap.scaled(ui->head_lb->size(),
  35. Qt::KeepAspectRatio, Qt::SmoothTransformation); // 将图片缩放到label的大小
  36. ui->head_lb->setPixmap(scaledPixmap); // 将缩放后的图片设置到QLabel上
  37. ui->head_lb->setScaledContents(true); // 设置QLabel自动缩放图片内容以适应大小
  38. return;
  39. }
  40. QPixmap scaledPixmap = pixmap.scaled(ui->head_lb->size(),
  41. Qt::KeepAspectRatio, Qt::SmoothTransformation); // 将图片缩放到label的大小
  42. ui->head_lb->setPixmap(scaledPixmap); // 将缩放后的图片设置到QLabel上
  43. ui->head_lb->setScaledContents(true); // 设置QLabel自动缩放图片内容以适应大小
  44. }
  45. else {
  46. qWarning() << "无法加载上传的头像:" << avatarPath;
  47. UserMgr::GetInstance()->AddLabelToReset(avatarPath, ui->head_lb);
  48. //先加载默认的
  49. QPixmap pixmap(":/res/head_1.jpg");
  50. QPixmap scaledPixmap = pixmap.scaled(ui->head_lb->size(),
  51. Qt::KeepAspectRatio, Qt::SmoothTransformation); // 将图片缩放到label的大小
  52. ui->head_lb->setPixmap(scaledPixmap); // 将缩放后的图片设置到QLabel上
  53. ui->head_lb->setScaledContents(true); // 设置QLabel自动缩放图片内容以适应大小
  54. //判断是否正在下载
  55. bool is_loading = UserMgr::GetInstance()->IsDownLoading(file_name);
  56. if (is_loading) {
  57. qWarning() << "正在下载: " << file_name;
  58. return;
  59. }
  60. //发送请求获取资源
  61. auto download_info = std::make_shared<DownloadInfo>();
  62. download_info->_name = file_name;
  63. download_info->_current_size = 0;
  64. download_info->_seq = 1;
  65. download_info->_total_size = 0;
  66. download_info->_client_path = avatarPath;
  67. //添加文件到管理者
  68. UserMgr::GetInstance()->AddDownloadFile(file_name, download_info);
  69. //发送消息
  70. FileTcpMgr::GetInstance()->SendDownloadInfo(download_info);
  71. }
  72. }
  73. else {
  74. qWarning() << "头像存储目录不存在:" << avatarsDir.path();
  75. }
  76. }
  77. //获取nick
  78. auto nick = UserMgr::GetInstance()->GetNick();
  79. //获取name
  80. auto name = UserMgr::GetInstance()->GetName();
  81. //描述
  82. auto desc = UserMgr::GetInstance()->GetDesc();
  83. ui->nick_ed->setText(nick);
  84. ui->name_ed->setText(name);
  85. ui->desc_ed->setText(desc);
  86. //连接上
  87. connect(ui->up_btn, &QPushButton::clicked, this, &UserInfoPage::slot_up_load);
  88. }

如果本地资源不存在,则需要向服务器请求。

封装请求资源接口

判断资源是否正在下载

  1. bool UserMgr::IsDownLoading(QString name) {
  2. std::lock_guard<std::mutex> lock(_down_load_mtx);
  3. auto iter = _name_to_download_info.find(name);
  4. if (iter == _name_to_download_info.end()) {
  5. return false;
  6. }
  7. return true;
  8. }

如果资源加载成功,很可能处于正在下载,所以也要判断一下。

如果资源未加载成功,则需要向服务器下载资源。先讲要下载的资源和要加载资源的空间缓存起来。

  1. void UserMgr::AddLabelToReset(QString path, QLabel* label)
  2. {
  3. auto iter = _name_to_reset_labels.find(path);
  4. if (iter == _name_to_reset_labels.end()) {
  5. QList<QLabel*> list;
  6. list.append(label);
  7. _name_to_reset_labels.insert(path, list);
  8. return;
  9. }
  10. iter->append(label);
  11. }

结构如下图

image-20251004125019778

更新正在下载资源

  1. void UserMgr::AddDownloadFile(QString name,
  2. std::shared_ptr<DownloadInfo> file_info) {
  3. std::lock_guard<std::mutex> lock(_down_load_mtx);
  4. _name_to_download_info[name] = file_info;
  5. }

发送下载请求

  1. void FileTcpMgr::SendDownloadInfo(std::shared_ptr<DownloadInfo> download) {
  2. QJsonObject jsonObj;
  3. jsonObj["name"] = download->_name;
  4. jsonObj["seq"] = download->_seq;
  5. jsonObj["trans_size"] = 0;
  6. jsonObj["total_size"] = 0;
  7. jsonObj["token"] = UserMgr::GetInstance()->GetToken();
  8. jsonObj["uid"] = UserMgr::GetInstance()->GetUid();
  9. jsonObj["client_path"] = download->_client_path;
  10. QJsonDocument doc(jsonObj);
  11. auto send_data = doc.toJson();
  12. SendData(ID_DOWN_LOAD_FILE_REQ, send_data);
  13. }

接收服务器回传

  1. _handlers.insert(ID_DOWN_LOAD_FILE_RSP, [this](ReqId id, int len, QByteArray data) {
  2. Q_UNUSED(len);
  3. qDebug() << "handle id is " << id << " data is " << data;
  4. // 将QByteArray转换为QJsonDocument
  5. QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
  6. // 检查转换是否成功
  7. if (jsonDoc.isNull()) {
  8. qDebug() << "Failed to create QJsonDocument.";
  9. return;
  10. }
  11. QJsonObject jsonObj = jsonDoc.object();
  12. if (!jsonObj.contains("error")) {
  13. int err = ErrorCodes::ERR_JSON;
  14. qDebug() << "parse create private chat json parse failed " << err;
  15. return;
  16. }
  17. int err = jsonObj["error"].toInt();
  18. if (err != ErrorCodes::SUCCESS) {
  19. qDebug() << "get create private chat failed, error is " << err;
  20. return;
  21. }
  22. qDebug() << "Receive download file info rsp success";
  23. QString base64Data = jsonObj["data"].toString();
  24. QString clientPath = jsonObj["client_path"].toString();
  25. int seq = jsonObj["seq"].toInt();
  26. bool is_last = jsonObj["is_last"].toBool();
  27. QString total_size_str = jsonObj["total_size"].toString();
  28. qint64 total_size = total_size_str.toLongLong(nullptr);
  29. QString current_size_str = jsonObj["current_size"].toString();
  30. qint64 current_size = current_size_str.toLongLong(nullptr);
  31. QString name = jsonObj["name"].toString();
  32. auto file_info = UserMgr::GetInstance()->GetDownloadInfo(name);
  33. if (file_info == nullptr) {
  34. qDebug() << "file: " << name << " not found";
  35. return;
  36. }
  37. file_info->_current_size = current_size;
  38. file_info->_total_size = total_size;
  39. //Base64解码
  40. QByteArray decodedData = QByteArray::fromBase64(base64Data.toUtf8());
  41. QFile file(clientPath);
  42. // 根据 seq 决定打开模式
  43. QIODevice::OpenMode mode;
  44. if (seq == 1) {
  45. // 第一个包,覆盖写入
  46. mode = QIODevice::WriteOnly;
  47. }
  48. else {
  49. // 后续包,追加写入
  50. mode = QIODevice::WriteOnly | QIODevice::Append;
  51. }
  52. if (!file.open(mode)) {
  53. qDebug() << "Failed to open file for writing:" << clientPath;
  54. qDebug() << "Error:" << file.errorString();
  55. return;
  56. }
  57. qint64 bytesWritten = file.write(decodedData);
  58. if (bytesWritten != decodedData.size()) {
  59. qDebug() << "Failed to write all data. Written:" << bytesWritten
  60. << "Expected:" << decodedData.size();
  61. }
  62. file.close();
  63. qDebug() << "Successfully wrote" << bytesWritten << "bytes to file";
  64. qDebug() << "Progress:" << current_size << "/" << total_size
  65. << "(" << (current_size * 100 / total_size) << "%)";
  66. if (is_last) {
  67. qDebug() << "File download completed:" << clientPath;
  68. UserMgr::GetInstance()->RmvDownloadFile(name);
  69. //发送信号通知主界面重新加载label
  70. emit sig_reset_label_icon(clientPath);
  71. }
  72. else {
  73. //继续请求
  74. file_info->_seq = seq+1;
  75. FileTcpMgr::GetInstance()->SendDownloadInfo(file_info);
  76. }
  77. });
  1. 判断seq是否为1,如果为1则说明新的文件,需要创建并保存
  2. 如果seq不为1,则说明是续传文件,更新追加就可以了
  3. 如果is_last字段为true,说明是最后一个包,那么移除缓存的下载信息,同时将信息发送到主界面更新图标

更新页面逻辑

ChatDialog界面中响应这个信号

  1. //重置label icon
  2. connect(FileTcpMgr::GetInstance().get(), &FileTcpMgr::sig_reset_label_icon, this, &ChatDialog::slot_reset_icon);

槽函数处理

  1. void ChatDialog::slot_reset_icon(QString path) {
  2. UserMgr::GetInstance()->ResetLabelIcon(path);
  3. }

UserMgr中封装重置icon逻辑

  1. void UserMgr::ResetLabelIcon(QString path)
  2. {
  3. auto iter = _name_to_reset_labels.find(path);
  4. if (iter == _name_to_reset_labels.end()) {
  5. return;
  6. }
  7. for (auto ele_iter = iter.value().begin(); ele_iter != iter.value().end(); ele_iter++) {
  8. QPixmap pixmap(path); // 加载上传的头像图片
  9. if (!pixmap.isNull()) {
  10. QPixmap scaledPixmap = pixmap.scaled((*ele_iter)->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
  11. (*ele_iter)->setPixmap(scaledPixmap);
  12. (*ele_iter)->setScaledContents(true);
  13. }
  14. else {
  15. qWarning() << "无法加载上传的头像:" << path;
  16. }
  17. }
  18. _name_to_reset_labels.erase(iter);
  19. }

测试效果

服务器断点传输逻辑

增加下载worker

  1. class DownloadWorker {
  2. public:
  3. DownloadWorker();
  4. ~DownloadWorker();
  5. void PostTask(std::shared_ptr<DownloadTask> task);
  6. private:
  7. void task_callback(std::shared_ptr<DownloadTask>);
  8. std::thread _work_thread;
  9. std::queue<std::shared_ptr<DownloadTask>> _task_que;
  10. std::atomic<bool> _b_stop;
  11. std::mutex _mtx;
  12. std::condition_variable _cv;
  13. };

DownloadWorker处理逻辑和之前的FileWorker类似

  1. DownloadWorker::DownloadWorker() :_b_stop(false)
  2. {
  3. _work_thread = std::thread([this]() {
  4. while (!_b_stop) {
  5. std::unique_lock<std::mutex> lock(_mtx);
  6. _cv.wait(lock, [this]() {
  7. if (_b_stop) {
  8. return true;
  9. }
  10. if (_task_que.empty()) {
  11. return false;
  12. }
  13. return true;
  14. });
  15. if (_b_stop) {
  16. break;
  17. }
  18. auto task = _task_que.front();
  19. _task_que.pop();
  20. task_callback(task);
  21. }
  22. });
  23. }
  24. DownloadWorker::~DownloadWorker()
  25. {
  26. _b_stop = true;
  27. _cv.notify_one();
  28. _work_thread.join();
  29. }
  30. void DownloadWorker::PostTask(std::shared_ptr<DownloadTask> task)
  31. {
  32. {
  33. std::lock_guard<std::mutex> lock(_mtx);
  34. _task_que.push(task);
  35. }
  36. _cv.notify_one();
  37. }
  38. void DownloadWorker::task_callback(std::shared_ptr<DownloadTask> task)
  39. {
  40. // 解码
  41. auto file_path_str = task->_file_path;
  42. //std::cout << "file_path_str is " << file_path_str << std::endl;
  43. boost::filesystem::path file_path(file_path_str);
  44. // 获取完整文件名(包含扩展名)
  45. std::string filename = file_path.filename().string();
  46. Json::Value result;
  47. result["error"] = ErrorCodes::Success;
  48. if (!boost::filesystem::exists(file_path)) {
  49. std::cerr << "文件不存在: " << file_path_str << std::endl;
  50. result["error"] = ErrorCodes::FileNotExists;
  51. task->_callback(result);
  52. return;
  53. }
  54. std::ifstream infile(file_path_str, std::ios::binary);
  55. if (!infile) {
  56. std::cerr << "无法打开文件进行读取。" << std::endl;
  57. result["error"] = ErrorCodes::FileReadPermissionFailed;
  58. task->_callback(result);
  59. return;
  60. }
  61. std::shared_ptr<FileInfo> file_info = nullptr;
  62. if (task->_seq == 1) {
  63. // 获取文件大小
  64. infile.seekg(0, std::ios::end);
  65. std::streamsize file_size = infile.tellg();
  66. infile.seekg(0, std::ios::beg);
  67. //如果为空,则创建FileInfo 构造数据存储
  68. file_info = std::make_shared<FileInfo>();
  69. file_info->_file_path_str = file_path_str;
  70. file_info->_name = filename;
  71. file_info->_seq = 1;
  72. file_info->_total_size = file_size;
  73. file_info->_trans_size = 0;
  74. // 立即保存到 Redis,覆盖旧数据,设置过期时间
  75. RedisMgr::GetInstance()->SetDownLoadInfo(filename, file_info);
  76. std::cout << "[新下载] 文件: " << filename
  77. << ", 大小: " << file_size << " 字节" << std::endl;
  78. }
  79. else {
  80. //断点续传,从 Redis 获取历史信息
  81. file_info = RedisMgr::GetInstance()->GetDownloadInfo(filename);
  82. if (file_info == nullptr) {
  83. // Redis 中没有信息(可能过期了)
  84. std::cerr << "断点续传失败,Redis 中无下载信息: " << filename << std::endl;
  85. result["error"] = ErrorCodes::RedisReadErr;
  86. task->_callback(result);
  87. infile.close();
  88. return;
  89. }
  90. // 验证序列号是否匹配
  91. if (task->_seq != file_info->_seq) {
  92. std::cerr << "序列号不匹配,期望: " << file_info->_seq
  93. << ", 实际: " << task->_seq << std::endl;
  94. result["error"] = ErrorCodes::FileSeqInvalid;
  95. task->_callback(result);
  96. infile.close();
  97. return;
  98. }
  99. std::cout << "[续传] 文件: " << filename
  100. << ", seq: " << task->_seq
  101. << ", 进度: " << file_info->_trans_size
  102. << "/" << file_info->_total_size << std::endl;
  103. }
  104. // 计算当前偏移量
  105. std::streamsize offset = ((std::streamsize)task->_seq - 1) * MAX_FILE_LEN;
  106. if (offset >= file_info->_total_size) {
  107. std::cerr << "偏移量超出文件大小。" << std::endl;
  108. result["error"] = ErrorCodes::FileOffsetInvalid;
  109. task->_callback(result);
  110. infile.close();
  111. return;
  112. }
  113. // 定位到指定偏移量
  114. infile.seekg(offset);
  115. // 读取最多2048字节
  116. char buffer[MAX_FILE_LEN];
  117. infile.read(buffer, MAX_FILE_LEN);
  118. //获取read实际读取多少字节
  119. std::streamsize bytes_read = infile.gcount();
  120. if (bytes_read <= 0) {
  121. std::cerr << "读取文件失败。" << std::endl;
  122. result["error"] = ErrorCodes::FileReadFailed;
  123. task->_callback(result);
  124. infile.close();
  125. return;
  126. }
  127. // 将读取的数据进行base64编码
  128. std::string data_to_encode(buffer, bytes_read);
  129. std::string encoded_data = base64_encode(data_to_encode);
  130. // 检查是否是最后一个包
  131. std::streamsize current_pos = offset + bytes_read;
  132. bool is_last = (current_pos >= file_info->_total_size);
  133. // 设置返回结果
  134. result["data"] = encoded_data;
  135. result["seq"] = task->_seq;
  136. result["total_size"] = std::to_string(file_info->_total_size);
  137. result["current_size"] = std::to_string(current_pos);
  138. result["is_last"] = is_last;
  139. infile.close();
  140. if (is_last) {
  141. std::cout << "文件读取完成: " << file_path_str << std::endl;
  142. RedisMgr::GetInstance()->DelDownLoadInfo(filename);
  143. }
  144. else {
  145. //更新信息
  146. file_info->_seq++;
  147. file_info->_trans_size = offset + bytes_read;
  148. //更新redis
  149. RedisMgr::GetInstance()->SetDownLoadInfo(filename, file_info);
  150. }
  151. if (task->_callback) {
  152. task->_callback(result);
  153. }
  154. }

FileSystem中创建worker

  1. FileSystem::FileSystem()
  2. {
  3. for (int i = 0; i < FILE_WORKER_COUNT; i++) {
  4. _file_workers.push_back(std::make_shared<FileWorker>());
  5. }
  6. for (int i = 0; i < DOWN_LOAD_WORKER_COUNT; i++) {
  7. _down_load_worker.push_back(std::make_shared<DownloadWorker>());
  8. }
  9. }

测试效果

将头像资源从本地删除后,重新登录时或者切换页面会引发资源重新加载,向服务器请求资源后再设置到界面显示。

image-20251004153501602

热门评论

热门文章

  1. Linux环境搭建和编码

    喜欢(594) 浏览(15350)
  2. 使用hexo搭建个人博客

    喜欢(533) 浏览(13711)
  3. MarkDown在线编辑器

    喜欢(514) 浏览(15557)
  4. vscode搭建windows C++开发环境

    喜欢(596) 浏览(96518)
  5. 聊天项目(28) 分布式服务通知好友申请

    喜欢(507) 浏览(7021)

最新评论

  1. 解决博客回复区被脚本注入的问题 secondtonone1:走到现在我忽然明白一个道理,无论工作也好生活也罢,最重要的是开心,即使一份安稳的工作不能给我带来事业上的积累也要合理的舍弃,所以我还是想去做喜欢的方向。
  2. 处理网络粘包问题 zyouth: //消息的长度小于头部规定的长度,说明数据未收全,则先将部分消息放到接收节点里 if (bytes_transferred < data_len) { memcpy(_recv_msg_node->_data + _recv_msg_node->_cur_len, _data + copy_len, bytes_transferred); _recv_msg_node->_cur_len += bytes_transferred; ::memset(_data, 0, MAX_LENGTH); _socket.async_read_some(boost::asio::buffer(_data, MAX_LENGTH), std::bind(&CSession::HandleRead, this, std::placeholders::_1, std::placeholders::_2, shared_self)); //头部处理完成 _b_head_parse = true; return; } 把_b_head_parse = true;放在_socket.async_read_some前面是不是更好
  3. C++ 线程池原理和实现 mzx2023:两种方法解决,一种是改排序算法,就是当线程耗尽的时候,使用普通递归,另一种是当在线程池commit的时候,判断线程是否耗尽,耗尽的话就直接当前线程执行task
  4. 利用指针和容器实现文本查询 越今朝:应该添加一个过滤功能以解决部分单词无法被查询的问题: eg: "I am a teacher."中的teacher无法被查询,因为在示例代码中teacher.被解释为一个单词从而忽略了teacher本身。
  5. 无锁并发队列 TenThousandOne:_head  和 _tail  替换为原子变量。那里pop的逻辑,val = _data[h] 可以移到循环外面吗

个人公众号

个人微信