聊天图片资源续传和进度显示

增加上传可视化进度

之前我们传输图片的时候,只能通过服务器查看上传进度。客户端无法感知上传进度,所以考虑在图片传输基础上,显示上传进度。

封装可点击标签

  1. #pragma once
  2. #include <QLabel>
  3. #include <QWidget>
  4. #include <QIcon>
  5. class ClickableLabel :
  6. public QLabel
  7. {
  8. Q_OBJECT
  9. public:
  10. explicit ClickableLabel(QWidget* parent = nullptr);
  11. void setIconOverlay(const QIcon& icon); //设置遮罩图标
  12. void showIconOverlay(bool show); //显示/隐藏遮罩图标
  13. protected:
  14. void mousePressEvent(QMouseEvent* event) override;
  15. void enterEvent(QEvent* event) override;
  16. void leaveEvent(QEvent* event) override;
  17. void paintEvent(QPaintEvent* event) override;
  18. signals:
  19. void clicked();
  20. private:
  21. QIcon m_overlayIcon;
  22. bool m_showOverlay;
  23. bool m_hovered;
  24. };

具体实现

  1. #include "ClickableLabel.h"
  2. #include <QMouseEvent>
  3. #include <QPainter>
  4. // ClickableLabel.cpp
  5. ClickableLabel::ClickableLabel(QWidget* parent)
  6. : QLabel(parent)
  7. , m_showOverlay(false)
  8. , m_hovered(false)
  9. {
  10. setCursor(Qt::PointingHandCursor);
  11. setMouseTracking(true);
  12. }
  13. void ClickableLabel::mousePressEvent(QMouseEvent* event)
  14. {
  15. if (event->button() == Qt::LeftButton) {
  16. emit clicked();
  17. }
  18. QLabel::mousePressEvent(event);
  19. }
  20. void ClickableLabel::enterEvent(QEvent* event)
  21. {
  22. m_hovered = true;
  23. update();
  24. QLabel::enterEvent(event);
  25. }
  26. void ClickableLabel::leaveEvent(QEvent* event)
  27. {
  28. m_hovered = false;
  29. update();
  30. QLabel::leaveEvent(event);
  31. }
  32. void ClickableLabel::paintEvent(QPaintEvent* event)
  33. {
  34. QLabel::paintEvent(event);
  35. if (m_showOverlay && !m_overlayIcon.isNull()) {
  36. QPainter painter(this);
  37. // 绘制半透明遮罩
  38. if (m_hovered) {
  39. painter.fillRect(rect(), QColor(0, 0, 0, 100));
  40. }
  41. else {
  42. painter.fillRect(rect(), QColor(0, 0, 0, 60));
  43. }
  44. // 绘制图标
  45. int iconSize = qMin(width(), height()) / 3; // 图标大小为图片的1/3
  46. QRect iconRect(
  47. (width() - iconSize) / 2,
  48. (height() - iconSize) / 2,
  49. iconSize,
  50. iconSize
  51. );
  52. m_overlayIcon.paint(&painter, iconRect);
  53. }
  54. }
  55. void ClickableLabel::setIconOverlay(const QIcon& icon)
  56. {
  57. m_overlayIcon = icon;
  58. update();
  59. }
  60. void ClickableLabel::showIconOverlay(bool show)
  61. {
  62. m_showOverlay = show;
  63. update();
  64. }

图片设置可点击和进度条

image-20260102111204491

在图片中添加可点击的标签,并且添加进度条

  1. #ifndef PICTUREBUBBLE_H
  2. #define PICTUREBUBBLE_H
  3. #include "BubbleFrame.h"
  4. #include <QHBoxLayout>
  5. #include <QPixmap>
  6. #include "ClickableLabel.h"
  7. #include <QProgressBar>
  8. #include "global.h"
  9. class PictureBubble : public BubbleFrame
  10. {
  11. Q_OBJECT
  12. public:
  13. PictureBubble(const QPixmap& picture, ChatRole role,int total, QWidget* parent = nullptr);
  14. void setProgress(int value);
  15. void showProgress(bool show);
  16. void setState(TransferState state);
  17. void resumeState();
  18. void setMsgInfo(std::shared_ptr<MsgInfo> msg);
  19. TransferState state() const { return m_state; }
  20. signals:
  21. void pauseRequested(QString unique_name, TransferType transfer_type); // 请求暂停
  22. void resumeRequested(QString unique_name, TransferType transfer_type); // 请求继续
  23. void cancelRequested(QString unique_name, TransferType transfer_type); // 请求取消
  24. private slots:
  25. void onPictureClicked();
  26. private:
  27. void updateIconOverlay();
  28. void adjustSize();
  29. private:
  30. ClickableLabel* m_picLabel;
  31. QProgressBar* m_progressBar;
  32. TransferState m_state;
  33. QIcon m_pauseIcon;
  34. QIcon m_playIcon;
  35. QIcon m_downloadIcon;
  36. QSize m_pixmapSize;
  37. QVBoxLayout* m_vLayout;
  38. int m_total_size;
  39. std::shared_ptr<MsgInfo> _msg_info;
  40. };
  41. #endif // PICTUREBUBBLE_H

构造函数里添加进度条和样式。

  1. PictureBubble::PictureBubble(const QPixmap &picture, ChatRole role, int total, QWidget *parent)
  2. :BubbleFrame(role, parent), m_state(TransferState::None), m_total_size(total)
  3. {
  4. // 加载图标(使用Qt内置图标或自定义图标)
  5. m_pauseIcon = style()->standardIcon(QStyle::SP_MediaPause);
  6. m_playIcon = style()->standardIcon(QStyle::SP_MediaPlay);
  7. m_downloadIcon = style()->standardIcon(QStyle::SP_ArrowDown);
  8. // 创建容器
  9. QWidget* container = new QWidget();
  10. m_vLayout = new QVBoxLayout(container);
  11. m_vLayout->setContentsMargins(0, 0, 0, 0);
  12. m_vLayout->setSpacing(5);
  13. // 创建可点击的图片标签
  14. m_picLabel = new ClickableLabel();
  15. m_picLabel->setScaledContents(true);
  16. QPixmap pix = picture.scaled(QSize(PIC_MAX_WIDTH, PIC_MAX_HEIGHT),
  17. Qt::KeepAspectRatio, Qt::SmoothTransformation);
  18. m_pixmapSize = pix.size();
  19. m_picLabel->setPixmap(pix);
  20. m_picLabel->setFixedSize(pix.size());
  21. connect(m_picLabel, &ClickableLabel::clicked,
  22. this, &PictureBubble::onPictureClicked);
  23. // 创建进度条
  24. m_progressBar = new QProgressBar();
  25. m_progressBar->setFixedWidth(pix.width());
  26. m_progressBar->setFixedHeight(10);
  27. m_progressBar->setRange(0, 100);
  28. m_progressBar->setValue(0);
  29. m_progressBar->setTextVisible(true);
  30. setState(TransferState::None);
  31. // 样式美化
  32. m_progressBar->setStyleSheet(
  33. "QProgressBar {"
  34. " border: 1px solid #ccc;"
  35. " border-radius: 3px;"
  36. " text-align: center;"
  37. " background-color: #f0f0f0;"
  38. " font-size: 10px;"
  39. "}"
  40. "QProgressBar::chunk {"
  41. " background-color: qlineargradient(x1:0, y1:0, x2:1, y2:0, "
  42. " stop:0 #4CAF50, stop:1 #45a049);"
  43. " border-radius: 2px;"
  44. "}"
  45. );
  46. m_vLayout->addWidget(m_picLabel);
  47. m_vLayout->addWidget(m_progressBar);
  48. this->setWidget(container);
  49. adjustSize();
  50. }

设置传输状态为None,进度条初始值为0

调整界面设置,将图片拉伸包括进度条

  1. void PictureBubble::adjustSize()
  2. {
  3. int left_margin = this->layout()->contentsMargins().left();
  4. int right_margin = this->layout()->contentsMargins().right();
  5. int v_margin = this->layout()->contentsMargins().bottom();
  6. int width = m_pixmapSize.width() + left_margin + right_margin;
  7. int height = m_pixmapSize.height() + v_margin * 2;
  8. if (m_progressBar->isHidden() == false) {
  9. height += m_progressBar->height() + m_vLayout->spacing();
  10. }
  11. setFixedSize(width, height);
  12. }

封装设置进度函数

  1. void PictureBubble::setProgress(int value)
  2. {
  3. float percent = (value / (m_total_size*1.0))*100;
  4. m_progressBar->setValue(percent);
  5. if (percent >= 100) {
  6. setState(TransferState::Completed);
  7. }
  8. }

展示进度条

  1. void PictureBubble::showProgress(bool show)
  2. {
  3. m_progressBar->show();
  4. adjustSize();
  5. }

当传输停止后,点击后会恢复,恢复传输或者下载状态,逻辑如下

  1. void PictureBubble::resumeState() {
  2. if (_msg_info->_transfer_type == TransferType::Download) {
  3. _msg_info->_transfer_state = TransferState::Downloading;
  4. m_state = TransferState::Downloading;
  5. updateIconOverlay();
  6. return;
  7. }
  8. if (_msg_info->_transfer_type == TransferType::Upload) {
  9. _msg_info->_transfer_state = TransferState::Uploading;
  10. m_state = TransferState::Uploading;
  11. updateIconOverlay();
  12. return;
  13. }
  14. }

设置状态逻辑如下, 状态分为None,下载,上传,暂停,传输完成,传输失败等。

  1. void PictureBubble::setState(TransferState state)
  2. {
  3. m_state = state;
  4. if (_msg_info) {
  5. _msg_info->_transfer_state = state;
  6. }
  7. updateIconOverlay();
  8. // 根据状态显示/隐藏进度条
  9. switch (state) {
  10. case TransferState::Downloading:
  11. case TransferState::Uploading:
  12. case TransferState::Paused:
  13. showProgress(true);
  14. break;
  15. case TransferState::Completed:
  16. // 完成后延迟隐藏进度条
  17. QTimer::singleShot(1000, this, [this]() {
  18. showProgress(false);
  19. });
  20. break;
  21. case TransferState::None:
  22. case TransferState::Failed:
  23. showProgress(false);
  24. break;
  25. }
  26. }

为了更好的控制传输逻辑,将std::shared_ptr<MsgInfo>作为成员变量设置给PictureBubble

这样在点击图片后,可以发送图片关联的消息idunique_name

点击图片触发槽函数

  1. void PictureBubble::onPictureClicked()
  2. {
  3. switch (m_state) {
  4. case TransferState::Downloading:
  5. case TransferState::Uploading:
  6. // 暂停
  7. setState(TransferState::Paused);
  8. emit pauseRequested(_msg_info->_unique_name, _msg_info->_transfer_type);
  9. break;
  10. case TransferState::Paused:
  11. // 继续
  12. resumeState(); //
  13. emit resumeRequested(_msg_info->_unique_name, _msg_info->_transfer_type);
  14. break;
  15. case TransferState::Failed:
  16. // 重试
  17. emit resumeRequested(_msg_info->_unique_name, _msg_info->_transfer_type);
  18. break;
  19. default:
  20. // 其他状态可以实现查看大图等功能
  21. break;
  22. }
  23. }

根据状态发送不同的信号,如果是下载或者暂停状态,则发送暂停信号。

如果是暂停状态,则发送继续下载或者继续上传状态。

  1. void PictureBubble::updateIconOverlay()
  2. {
  3. switch (m_state) {
  4. case TransferState::Downloading:
  5. case TransferState::Uploading:
  6. m_picLabel->setIconOverlay(m_pauseIcon);
  7. m_picLabel->showIconOverlay(true);
  8. break;
  9. case TransferState::Paused:
  10. m_picLabel->setIconOverlay(m_playIcon);
  11. m_picLabel->showIconOverlay(true);
  12. break;
  13. case TransferState::Failed:
  14. m_picLabel->setIconOverlay(m_downloadIcon); // 或重试图标
  15. m_picLabel->showIconOverlay(true);
  16. break;
  17. default:
  18. m_picLabel->showIconOverlay(false);
  19. break;
  20. }
  21. }

链接状态信号

ChatPage的构造函数中添加信号和槽函数链接

  1. //链接暂停信号
  2. connect(dynamic_cast<PictureBubble*>(pBubble), &PictureBubble::pauseRequested,
  3. this, &ChatPage::on_clicked_paused);
  4. //链接恢复信号
  5. connect(dynamic_cast<PictureBubble*>(pBubble), &PictureBubble::resumeRequested,
  6. this, &ChatPage::on_clicked_resume);

槽函数中设置文件为暂停状态

  1. void ChatPage::on_clicked_paused(QString unique_name, TransferType transfer_type)
  2. {
  3. UserMgr::GetInstance()->PauseTransFileByName(unique_name);
  4. }

槽函数中设置为续传或者下载状态,并且调用FileMgr续传之前未传递完成的内容

  1. void ChatPage::on_clicked_resume(QString unique_name, TransferType transfer_type)
  2. {
  3. UserMgr::GetInstance()->ResumeTransFileByName(unique_name);
  4. //继续发送或者下载
  5. if (transfer_type == TransferType::Upload) {
  6. FileTcpMgr::GetInstance()->ContinueUploadFile(unique_name);
  7. return;
  8. }
  9. if (transfer_type == TransferType::Download) {
  10. return;
  11. }
  12. }

续传逻辑

FileTcpMgr发送信号sig_continue_upload_file, 这么做的好处是,发送信号的线程和接收信号的线程不一致,也可以跨线程调用。

  1. void FileTcpMgr::ContinueUploadFile(QString unique_name) {
  2. emit sig_continue_upload_file(unique_name);
  3. }

FileTcpMgr中链接了这个信号

  1. //链接续传信号
  2. QObject::connect(this, &FileTcpMgr::sig_continue_upload_file, this, &FileTcpMgr::slot_continue_upload_file);

槽函数slot_continue_upload_file

  1. void FileTcpMgr::slot_continue_upload_file(QString unique_name) {
  2. auto msg_info = UserMgr::GetInstance()->GetTransFileByName(unique_name);
  3. if (msg_info == nullptr) {
  4. return;
  5. }
  6. //将待发送序列号更新为已经确认接收的序列号,然后基于此序列号再递增。
  7. msg_info->_seq = msg_info->_last_confirmed_seq;
  8. if ((msg_info->_seq) * MAX_FILE_LEN >= msg_info->_total_size) {
  9. qDebug() << "file has sent finished";
  10. return;
  11. }
  12. if (MAX_CWND_SIZE - _cwnd_size == 0) {
  13. return;
  14. }
  15. //打开
  16. QFile file(msg_info->_text_or_url);
  17. if (!file.open(QIODevice::ReadOnly)) {
  18. qWarning() << "Could not open file: " << file.errorString();
  19. return;
  20. }
  21. //文件偏移到已经发送的位置,继续读取发送
  22. file.seek(msg_info->_seq * MAX_FILE_LEN);
  23. bool b_last = false;
  24. //再次组织数据发送
  25. for (; MAX_CWND_SIZE - _cwnd_size > 0; ) {
  26. QByteArray buffer;
  27. msg_info->_seq++;
  28. //放入发送未回包集合
  29. msg_info->_flighting_seqs.insert(msg_info->_seq);
  30. //每次读取MAX_FILE_LEN字节发送
  31. buffer = file.read(MAX_FILE_LEN);
  32. QJsonObject sendObj;
  33. //将文件内容转换为base64编码
  34. QString base64Data = buffer.toBase64();
  35. sendObj["md5"] = msg_info->_md5;
  36. sendObj["name"] = msg_info->_unique_name;
  37. sendObj["seq"] = msg_info->_seq;
  38. msg_info->_current_size = buffer.size() + (msg_info->_seq - 1) * MAX_FILE_LEN;
  39. sendObj["trans_size"] = msg_info->_current_size;
  40. sendObj["total_size"] = msg_info->_total_size;
  41. b_last = false;
  42. if (buffer.size() + (msg_info->_seq - 1) * MAX_FILE_LEN >= msg_info->_total_size) {
  43. sendObj["last"] = 1;
  44. b_last = true;
  45. }
  46. else {
  47. sendObj["last"] = 0;
  48. }
  49. sendObj["data"] = base64Data;
  50. sendObj["last_seq"] = msg_info->_max_seq;
  51. sendObj["uid"] = UserMgr::GetInstance()->GetUid();
  52. QJsonDocument doc(sendObj);
  53. auto send_data = doc.toJson();
  54. //直接发送,其实是放入tcpmgr发送队列
  55. SendData(ID_IMG_CHAT_CONTINUE_UPLOAD_REQ, send_data);
  56. _cwnd_size++;
  57. //如果
  58. if (b_last) {
  59. break;
  60. }
  61. }
  62. file.close();
  63. }

ID_IMG_CHAT_CONTINUE_UPLOAD_REQ为断点续传请求

  1. ID_IMG_CHAT_CONTINUE_UPLOAD_REQ = 1043, //续传聊天图片资源请求
  2. ID_IMG_CHAT_CONTINUE_UPLOAD_RSP = 1044, //续传聊天图片资源回复

响应续传回复

  1. _handlers.insert(ID_IMG_CHAT_CONTINUE_UPLOAD_RSP, [this](ReqId id, int len, QByteArray data) {
  2. Q_UNUSED(len);
  3. qDebug() << "handle id is " << id;
  4. // 将QByteArray转换为QJsonDocument
  5. QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
  6. _cwnd_size--;
  7. // 检查转换是否成功
  8. if (jsonDoc.isNull()) {
  9. qDebug() << "Failed to create QJsonDocument.";
  10. return;
  11. }
  12. QJsonObject recvObj = jsonDoc.object();
  13. qDebug() << "data jsonobj is " << recvObj;
  14. if (!recvObj.contains("error")) {
  15. int err = ErrorCodes::ERR_JSON;
  16. qDebug() << "icon upload_failed, err is Json Parse Err" << err;
  17. //todo ... 提示上传失败
  18. //emit upload_failed();
  19. return;
  20. }
  21. int err = recvObj["error"].toInt();
  22. if (err != ErrorCodes::SUCCESS) {
  23. qDebug() << "Login Failed, err is " << err;
  24. //emit upload_failed();
  25. return;
  26. }
  27. auto name = recvObj["name"].toString();
  28. auto file_info = UserMgr::GetInstance()->GetTransFileByName(name);
  29. if (!file_info) {
  30. return;
  31. }
  32. auto md5 = file_info->_md5;
  33. auto seq = recvObj["seq"].toInt();
  34. //根据seq从未接收集合移动到已接收集合中
  35. file_info->_flighting_seqs.erase(seq);
  36. //将seq放入已收到集合中
  37. file_info->_rsp_seqs.insert(seq);
  38. //计算当前最后确认的序列号
  39. while (file_info->_rsp_seqs.count(file_info->_last_confirmed_seq + 1)) {
  40. ++file_info->_last_confirmed_seq;
  41. }
  42. qDebug() << "recv : " << name << "file seq is " << seq;
  43. //判断最大序列和最后确认序列号相等,说明收全了
  44. if (file_info->_last_confirmed_seq == file_info->_max_seq) {
  45. //更新已经传输的文件大小
  46. file_info->_rsp_size = file_info->_total_size;
  47. //通知界面显示
  48. emit sig_update_upload_progress(file_info);
  49. UserMgr::GetInstance()->RmvTransFileByName(name);
  50. //todo 此处添加发送其他待发送的文件
  51. auto free_file = UserMgr::GetInstance()->GetFreeUploadFile();
  52. if (free_file == nullptr) {
  53. return;
  54. }
  55. BatchSend(free_file);
  56. return;
  57. }
  58. //更新已经传输的文件大小
  59. file_info->_rsp_size = (file_info->_last_confirmed_seq) * MAX_FILE_LEN;
  60. //发送信号,更新图片上传进度
  61. emit sig_update_upload_progress(file_info);
  62. //如果传输状态不为上传,则直接返回。
  63. if (!UserMgr::GetInstance()->TransFileIsUploading(name)) {
  64. return;
  65. }
  66. BatchSend(file_info); });

sig_update_upload_progress信号为更新进度,因为在接收到服务器回包前,文件状态可能被设置为暂停,所以要判断一下文件状态是否为暂停,如果为暂停,则不发送后续内容。否则直接发送后续内容。

sig_update_upload_progress信号,也会在正常上传服务器,服务器回复给客户端时,客户端发送

比如上传图片信息回复逻辑里

  1. _handlers.insert(ID_IMG_CHAT_UPLOAD_RSP, [this](ReqId id, int len, QByteArray data) {
  2. Q_UNUSED(len);
  3. qDebug() << "handle id is " << id;
  4. // 将QByteArray转换为QJsonDocument
  5. QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
  6. _cwnd_size--;
  7. // 检查转换是否成功
  8. if (jsonDoc.isNull()) {
  9. qDebug() << "Failed to create QJsonDocument.";
  10. return;
  11. }
  12. QJsonObject recvObj = jsonDoc.object();
  13. qDebug() << "data jsonobj is " << recvObj;
  14. if (!recvObj.contains("error")) {
  15. int err = ErrorCodes::ERR_JSON;
  16. qDebug() << "icon upload_failed, err is Json Parse Err" << err;
  17. //todo ... 提示上传失败
  18. //emit upload_failed();
  19. return;
  20. }
  21. int err = recvObj["error"].toInt();
  22. if (err != ErrorCodes::SUCCESS) {
  23. qDebug() << "Login Failed, err is " << err;
  24. //emit upload_failed();
  25. return;
  26. }
  27. auto name = recvObj["name"].toString();
  28. auto file_info = UserMgr::GetInstance()->GetTransFileByName(name);
  29. if (!file_info) {
  30. return;
  31. }
  32. auto md5 = file_info->_md5;
  33. auto seq = recvObj["seq"].toInt();
  34. //根据seq从未接收集合移动到已接收集合中
  35. file_info->_flighting_seqs.erase(seq);
  36. //将seq放入已收到集合中
  37. file_info->_rsp_seqs.insert(seq);
  38. //计算当前最后确认的序列号
  39. while (file_info->_rsp_seqs.count(file_info->_last_confirmed_seq + 1)) {
  40. ++file_info->_last_confirmed_seq;
  41. }
  42. qDebug() << "recv : " << name << "file seq is " << seq;
  43. //判断最大序列和最后确认序列号相等,说明收全了
  44. if (file_info->_last_confirmed_seq == file_info->_max_seq) {
  45. //更新已经传输的文件大小
  46. file_info->_rsp_size = file_info->_total_size;
  47. //通知界面显示
  48. emit sig_update_upload_progress(file_info);
  49. UserMgr::GetInstance()->RmvTransFileByName(name);
  50. //todo 此处添加发送其他待发送的文件
  51. auto free_file = UserMgr::GetInstance()->GetFreeUploadFile();
  52. if (free_file == nullptr) {
  53. return;
  54. }
  55. BatchSend(free_file);
  56. return;
  57. }
  58. //更新已经传输的文件大小
  59. file_info->_rsp_size = (file_info->_last_confirmed_seq) * MAX_FILE_LEN;
  60. //发送信号,更新图片上传进度
  61. emit sig_update_upload_progress(file_info);
  62. //如果传输状态不为上传,则直接返回。
  63. if (!UserMgr::GetInstance()->TransFileIsUploading(name)) {
  64. return;
  65. }
  66. BatchSend(file_info); });
  67. _handlers.insert(ID_IMG_CHAT_CONTINUE_UPLOAD_RSP, [this](ReqId id, int len, QByteArray data) {
  68. Q_UNUSED(len);
  69. qDebug() << "handle id is " << id;
  70. // 将QByteArray转换为QJsonDocument
  71. QJsonDocument jsonDoc = QJsonDocument::fromJson(data);
  72. _cwnd_size--;
  73. // 检查转换是否成功
  74. if (jsonDoc.isNull()) {
  75. qDebug() << "Failed to create QJsonDocument.";
  76. return;
  77. }
  78. QJsonObject recvObj = jsonDoc.object();
  79. qDebug() << "data jsonobj is " << recvObj;
  80. if (!recvObj.contains("error")) {
  81. int err = ErrorCodes::ERR_JSON;
  82. qDebug() << "icon upload_failed, err is Json Parse Err" << err;
  83. //todo ... 提示上传失败
  84. //emit upload_failed();
  85. return;
  86. }
  87. int err = recvObj["error"].toInt();
  88. if (err != ErrorCodes::SUCCESS) {
  89. qDebug() << "Login Failed, err is " << err;
  90. //emit upload_failed();
  91. return;
  92. }
  93. auto name = recvObj["name"].toString();
  94. auto file_info = UserMgr::GetInstance()->GetTransFileByName(name);
  95. if (!file_info) {
  96. return;
  97. }
  98. auto md5 = file_info->_md5;
  99. auto seq = recvObj["seq"].toInt();
  100. //根据seq从未接收集合移动到已接收集合中
  101. file_info->_flighting_seqs.erase(seq);
  102. //将seq放入已收到集合中
  103. file_info->_rsp_seqs.insert(seq);
  104. //计算当前最后确认的序列号
  105. while (file_info->_rsp_seqs.count(file_info->_last_confirmed_seq + 1)) {
  106. ++file_info->_last_confirmed_seq;
  107. }
  108. qDebug() << "recv : " << name << "file seq is " << seq;
  109. //判断最大序列和最后确认序列号相等,说明收全了
  110. if (file_info->_last_confirmed_seq == file_info->_max_seq) {
  111. //更新已经传输的文件大小
  112. file_info->_rsp_size = file_info->_total_size;
  113. //通知界面显示
  114. emit sig_update_upload_progress(file_info);
  115. UserMgr::GetInstance()->RmvTransFileByName(name);
  116. //todo 此处添加发送其他待发送的文件
  117. auto free_file = UserMgr::GetInstance()->GetFreeUploadFile();
  118. if (free_file == nullptr) {
  119. return;
  120. }
  121. BatchSend(free_file);
  122. return;
  123. }
  124. //更新已经传输的文件大小
  125. file_info->_rsp_size = (file_info->_last_confirmed_seq) * MAX_FILE_LEN;
  126. //发送信号,更新图片上传进度
  127. emit sig_update_upload_progress(file_info);
  128. //如果传输状态不为上传,则直接返回。
  129. if (!UserMgr::GetInstance()->TransFileIsUploading(name)) {
  130. return;
  131. }
  132. BatchSend(file_info); });
  133. }

同步信息回复也会发送进度上传信号

  1. _handlers.insert(ID_FILE_INFO_SYNC_RSP, [this](ReqId id, int len, QByteArray data) {
  2. Q_UNUSED(len);
  3. qDebug() << "handle id is " << id;
  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 recvObj = jsonDoc.object();
  12. qDebug() << "data jsonobj is " << recvObj;
  13. if (!recvObj.contains("error")) {
  14. int err = ErrorCodes::ERR_JSON;
  15. qDebug() << "icon upload_failed, err is Json Parse Err" << err;
  16. //todo ... 提示上传失败,将来可能断点重传等
  17. //emit upload_failed();
  18. return;
  19. }
  20. int err = recvObj["error"].toInt();
  21. if (err != ErrorCodes::SUCCESS) {
  22. qDebug() << "Login Failed, err is " << err;
  23. //emit upload_failed();
  24. return;
  25. }
  26. //为了简单起见,先处理网络正常情况
  27. auto seq = recvObj["seq"].toInt();
  28. auto name = recvObj["name"].toString();
  29. auto file_info = UserMgr::GetInstance()->GetTransFileByName(name);
  30. if (!file_info) {
  31. return;
  32. }
  33. //根据seq从未接收集合移动到已接收集合中
  34. file_info->_flighting_seqs.erase(seq);
  35. //将seq放入已收到集合中
  36. file_info->_rsp_seqs.insert(seq);
  37. //计算当前最后确认的序列号
  38. while (file_info->_rsp_seqs.count(file_info->_last_confirmed_seq + 1)) {
  39. ++file_info->_last_confirmed_seq;
  40. }
  41. qDebug() << "recv : " << name << "file seq is " << seq;
  42. //判断最大序列和最后确认序列号相等,说明收全了
  43. if (file_info->_last_confirmed_seq == file_info->_max_seq) {
  44. //更新已经传输的文件大小
  45. file_info->_rsp_size = file_info->_total_size;
  46. //通知界面显示
  47. emit sig_update_upload_progress(file_info);
  48. UserMgr::GetInstance()->RmvTransFileByName(name);
  49. //todo 此处添加发送其他待发送的文件
  50. auto free_file = UserMgr::GetInstance()->GetFreeUploadFile();
  51. if (free_file == nullptr) {
  52. return;
  53. }
  54. BatchSend(free_file);
  55. return;
  56. }
  57. //更新已经传输的文件大小
  58. file_info->_rsp_size = (file_info->_last_confirmed_seq) * MAX_FILE_LEN;
  59. //通知界面显示
  60. emit sig_update_upload_progress(file_info);
  61. //如果传输状态不为上传,则直接返回。
  62. if (!UserMgr::GetInstance()->TransFileIsUploading(name)) {
  63. return;
  64. }
  65. BatchSend(file_info);
  66. });

ChatPage中链接进度上传信号和槽函数

  1. //接收tcp返回的上传进度信息
  2. connect(FileTcpMgr::GetInstance().get(), &FileTcpMgr::sig_update_upload_progress,
  3. this, &ChatDialog::slot_update_upload_progress);

接收进度上传信号

  1. void ChatDialog::slot_update_upload_progress(std::shared_ptr<MsgInfo> msg_info) {
  2. auto chat_data = UserMgr::GetInstance()->GetChatThreadByThreadId(msg_info->_thread_id);
  3. if (chat_data == nullptr) {
  4. return;
  5. }
  6. //更新消息,其实不用更新,都是共享msg_info的一块内存,这里为了安全还是再次更新下
  7. chat_data->UpdateProgress(msg_info);
  8. if (_cur_chat_thread_id != msg_info->_thread_id) {
  9. return;
  10. }
  11. //更新聊天界面信息
  12. ui->chat_page->UpdateFileProgress(msg_info);
  13. }

调用ChatPageUpdateFileProgress函数

  1. void ChatPage::UpdateFileProgress(std::shared_ptr<MsgInfo> msg_info) {
  2. auto iter = _base_item_map.find(msg_info->_msg_id);
  3. if (iter == _base_item_map.end()) {
  4. return;
  5. }
  6. if (msg_info->_msg_type == MsgType::IMG_MSG) {
  7. auto bubble = iter.value()->getBubble();
  8. PictureBubble* pic_bubble = dynamic_cast<PictureBubble*>(bubble);
  9. pic_bubble->setProgress(msg_info->_rsp_size);
  10. }
  11. }

根据消息类型为图片,则调用PictureBubble更新图片进度

服务器

ResoureServer服务器要响应断点续传请求

LogicWorker注册消息处理

  1. _fun_callbacks[ID_IMG_CHAT_CONTINUE_UPLOAD_REQ] = [this](shared_ptr<CSession> session, const short& msg_id,
  2. const string& msg_data) {
  3. Json::Reader reader;
  4. Json::Value root;
  5. reader.parse(msg_data, root);
  6. auto md5 = root["md5"].asString();
  7. auto seq = root["seq"].asInt();
  8. auto name = root["name"].asString();
  9. auto total_size = root["total_size"].asInt();
  10. auto trans_size = root["trans_size"].asInt();
  11. auto last = root["last"].asInt();
  12. auto file_data = root["data"].asString();
  13. auto file_path = ConfigMgr::Inst().GetFileOutPath();
  14. auto uid = root["uid"].asInt();
  15. //转化为字符串
  16. auto uid_str = std::to_string(uid);
  17. auto file_path_str = (file_path / uid_str / name).string();
  18. Json::Value rtvalue;
  19. auto callback = [=](const Json::Value& result) {
  20. // 在异步任务完成后调用
  21. Json::Value rtvalue = result;
  22. rtvalue["error"] = ErrorCodes::Success;
  23. rtvalue["total_size"] = total_size;
  24. rtvalue["seq"] = seq;
  25. rtvalue["name"] = name;
  26. rtvalue["trans_size"] = trans_size;
  27. rtvalue["last"] = last;
  28. rtvalue["md5"] = md5;
  29. rtvalue["uid"] = uid;
  30. std::string return_str = rtvalue.toStyledString();
  31. session->Send(return_str, ID_IMG_CHAT_CONTINUE_UPLOAD_RSP);
  32. };
  33. // 使用 std::hash 对字符串进行哈希
  34. std::hash<std::string> hash_fn;
  35. size_t hash_value = hash_fn(name); // 生成哈希值
  36. int index = hash_value % FILE_WORKER_COUNT;
  37. std::cout << "Hash value: " << hash_value << std::endl;
  38. //第一个包
  39. if (seq == 1) {
  40. //构造数据存储
  41. auto file_info = std::make_shared<FileInfo>();
  42. file_info->_file_path_str = file_path_str;
  43. file_info->_name = name;
  44. file_info->_seq = seq;
  45. file_info->_total_size = total_size;
  46. file_info->_trans_size = trans_size;
  47. bool success = RedisMgr::GetInstance()->SetFileInfo(name, file_info);
  48. if (!success) {
  49. rtvalue["error"] = ErrorCodes::FileSaveRedisFailed;
  50. std::string return_str = rtvalue.toStyledString();
  51. session->Send(return_str, ID_IMG_CHAT_CONTINUE_UPLOAD_RSP);
  52. return;
  53. }
  54. }
  55. else {
  56. auto file_info = RedisMgr::GetInstance()->GetFileInfo(name);
  57. if (file_info == nullptr) {
  58. rtvalue["error"] = ErrorCodes::FileNotExists;
  59. std::string return_str = rtvalue.toStyledString();
  60. session->Send(return_str, ID_IMG_CHAT_CONTINUE_UPLOAD_RSP);
  61. return;
  62. }
  63. file_info->_seq = seq;
  64. file_info->_trans_size = trans_size;
  65. bool success = RedisMgr::GetInstance()->SetFileInfo(name, file_info);
  66. if (!success) {
  67. rtvalue["error"] = ErrorCodes::FileSaveRedisFailed;
  68. std::string return_str = rtvalue.toStyledString();
  69. session->Send(return_str, ID_IMG_CHAT_CONTINUE_UPLOAD_RSP);
  70. return;
  71. }
  72. }
  73. FileSystem::GetInstance()->PostMsgToQue(
  74. std::make_shared<FileTask>(session, ID_IMG_CHAT_CONTINUE_UPLOAD_REQ, uid, file_path_str, name, seq, total_size,
  75. trans_size, last, file_data, callback),
  76. index
  77. );
  78. };

FileWorker响应图片上传逻辑

  1. //处理续传图片请求
  2. _handlers[ID_IMG_CHAT_CONTINUE_UPLOAD_REQ] = [this](std::shared_ptr<FileTask> task) {
  3. // 解码
  4. std::string decoded = base64_decode(task->_file_data);
  5. auto file_path_str = task->_path;
  6. auto last = task->_last;
  7. //std::cout << "file_path_str is " << file_path_str << std::endl;
  8. boost::filesystem::path file_path(file_path_str);
  9. boost::filesystem::path dir_path = file_path.parent_path();
  10. // 获取完整文件名(包含扩展名)
  11. std::string filename = file_path.filename().string();
  12. Json::Value result;
  13. result["error"] = ErrorCodes::Success;
  14. // Check if directory exists, if not, create it
  15. if (!boost::filesystem::exists(dir_path)) {
  16. if (!boost::filesystem::create_directories(dir_path)) {
  17. std::cerr << "Failed to create directory: " << dir_path.string() << std::endl;
  18. result["error"] = ErrorCodes::FileNotExists;
  19. task->_callback(result);
  20. return;
  21. }
  22. }
  23. std::ofstream outfile;
  24. //第一个包
  25. if (task->_seq == 1) {
  26. // 打开文件,如果存在则清空,不存在则创建
  27. outfile.open(file_path_str, std::ios::binary | std::ios::trunc);
  28. }
  29. else {
  30. // 保存为文件
  31. outfile.open(file_path_str, std::ios::binary | std::ios::app);
  32. }
  33. if (!outfile) {
  34. std::cerr << "无法打开文件进行写入。" << std::endl;
  35. result["error"] = ErrorCodes::FileWritePermissionFailed;
  36. task->_callback(result);
  37. return;
  38. }
  39. outfile.write(decoded.data(), decoded.size());
  40. if (!outfile) {
  41. std::cerr << "写入文件失败。" << std::endl;
  42. result["error"] = ErrorCodes::FileWritePermissionFailed;
  43. task->_callback(result);
  44. return;
  45. }
  46. outfile.close();
  47. if (last) {
  48. std::cout << "文件已成功保存为: " << task->_name << std::endl;
  49. //todo...更新数据库聊天图像上传状态
  50. //todo...通过grpc通知ChatServer
  51. }
  52. if (task->_callback) {
  53. task->_callback(result);
  54. }
  55. };

效果展示

image-20260102101925154

image-20260102102645373

热门评论

热门文章

  1. vscode搭建windows C++开发环境

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

    喜欢(533) 浏览(14372)
  3. Linux环境搭建和编码

    喜欢(594) 浏览(15945)
  4. MarkDown在线编辑器

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

    喜欢(507) 浏览(7367)

最新评论

  1. C++ 线程池原理和实现 mzx2023:两种方法解决,一种是改排序算法,就是当线程耗尽的时候,使用普通递归,另一种是当在线程池commit的时候,判断线程是否耗尽,耗尽的话就直接当前线程执行task
  2. 利用指针和容器实现文本查询 越今朝:应该添加一个过滤功能以解决部分单词无法被查询的问题: eg: "I am a teacher."中的teacher无法被查询,因为在示例代码中teacher.被解释为一个单词从而忽略了teacher本身。
  3. 无锁并发队列 TenThousandOne:_head  和 _tail  替换为原子变量。那里pop的逻辑,val = _data[h] 可以移到循环外面吗
  4. 解决博客回复区被脚本注入的问题 secondtonone1:走到现在我忽然明白一个道理,无论工作也好生活也罢,最重要的是开心,即使一份安稳的工作不能给我带来事业上的积累也要合理的舍弃,所以我还是想去做喜欢的方向。
  5. 处理网络粘包问题 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前面是不是更好

个人公众号

个人微信