客户端断点下载资源
客户端请求下载
在客户端加载本地资源发现不存在的时候,需要请求服务器,获取资源。
如果资源比较大,需要分批下载,也就是支持断点下载。
我们先拿UserInfoPage举例
UserInfoPage::UserInfoPage(QWidget *parent) :QWidget(parent),ui(new Ui::UserInfoPage){ui->setupUi(this);auto icon = UserMgr::GetInstance()->GetIcon();qDebug() << "icon is " << icon ;//使用正则表达式检查是否使用默认头像QRegularExpression regex("^:/res/head_(\\d+)\\.jpg$");QRegularExpressionMatch match = regex.match(icon);if (match.hasMatch()) {QPixmap pixmap(icon);QPixmap scaledPixmap = pixmap.scaled(ui->head_lb->size(),Qt::KeepAspectRatio, Qt::SmoothTransformation); // 将图片缩放到label的大小ui->head_lb->setPixmap(scaledPixmap); // 将缩放后的图片设置到QLabel上ui->head_lb->setScaledContents(true); // 设置QLabel自动缩放图片内容以适应大小}else {// 如果是用户上传的头像,获取存储目录QString storageDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);QDir avatarsDir(storageDir + "/avatars");// 确保目录存在if (avatarsDir.exists()) {auto file_name = QFileInfo(icon).fileName();QString avatarPath = avatarsDir.filePath(QFileInfo(icon).fileName()); // 获取上传头像的完整路径QPixmap pixmap(avatarPath); // 加载上传的头像图片if (!pixmap.isNull()) {//判断是否正在下载bool is_loading = UserMgr::GetInstance()->IsDownLoading(file_name);if (is_loading) {qWarning() << "正在下载: " << file_name;//先加载默认的QPixmap pixmap(":/res/head_1.jpg");QPixmap scaledPixmap = pixmap.scaled(ui->head_lb->size(),Qt::KeepAspectRatio, Qt::SmoothTransformation); // 将图片缩放到label的大小ui->head_lb->setPixmap(scaledPixmap); // 将缩放后的图片设置到QLabel上ui->head_lb->setScaledContents(true); // 设置QLabel自动缩放图片内容以适应大小return;}QPixmap scaledPixmap = pixmap.scaled(ui->head_lb->size(),Qt::KeepAspectRatio, Qt::SmoothTransformation); // 将图片缩放到label的大小ui->head_lb->setPixmap(scaledPixmap); // 将缩放后的图片设置到QLabel上ui->head_lb->setScaledContents(true); // 设置QLabel自动缩放图片内容以适应大小}else {qWarning() << "无法加载上传的头像:" << avatarPath;UserMgr::GetInstance()->AddLabelToReset(avatarPath, ui->head_lb);//先加载默认的QPixmap pixmap(":/res/head_1.jpg");QPixmap scaledPixmap = pixmap.scaled(ui->head_lb->size(),Qt::KeepAspectRatio, Qt::SmoothTransformation); // 将图片缩放到label的大小ui->head_lb->setPixmap(scaledPixmap); // 将缩放后的图片设置到QLabel上ui->head_lb->setScaledContents(true); // 设置QLabel自动缩放图片内容以适应大小//判断是否正在下载bool is_loading = UserMgr::GetInstance()->IsDownLoading(file_name);if (is_loading) {qWarning() << "正在下载: " << file_name;return;}//发送请求获取资源auto download_info = std::make_shared<DownloadInfo>();download_info->_name = file_name;download_info->_current_size = 0;download_info->_seq = 1;download_info->_total_size = 0;download_info->_client_path = avatarPath;//添加文件到管理者UserMgr::GetInstance()->AddDownloadFile(file_name, download_info);//发送消息FileTcpMgr::GetInstance()->SendDownloadInfo(download_info);}}else {qWarning() << "头像存储目录不存在:" << avatarsDir.path();}}//获取nickauto nick = UserMgr::GetInstance()->GetNick();//获取nameauto name = UserMgr::GetInstance()->GetName();//描述auto desc = UserMgr::GetInstance()->GetDesc();ui->nick_ed->setText(nick);ui->name_ed->setText(name);ui->desc_ed->setText(desc);//连接上connect(ui->up_btn, &QPushButton::clicked, this, &UserInfoPage::slot_up_load);}
如果本地资源不存在,则需要向服务器请求。
封装请求资源接口
判断资源是否正在下载
bool UserMgr::IsDownLoading(QString name) {std::lock_guard<std::mutex> lock(_down_load_mtx);auto iter = _name_to_download_info.find(name);if (iter == _name_to_download_info.end()) {return false;}return true;}
如果资源加载成功,很可能处于正在下载,所以也要判断一下。
如果资源未加载成功,则需要向服务器下载资源。先讲要下载的资源和要加载资源的空间缓存起来。
void UserMgr::AddLabelToReset(QString path, QLabel* label){auto iter = _name_to_reset_labels.find(path);if (iter == _name_to_reset_labels.end()) {QList<QLabel*> list;list.append(label);_name_to_reset_labels.insert(path, list);return;}iter->append(label);}
结构如下图

更新正在下载资源
void UserMgr::AddDownloadFile(QString name,std::shared_ptr<DownloadInfo> file_info) {std::lock_guard<std::mutex> lock(_down_load_mtx);_name_to_download_info[name] = file_info;}
发送下载请求
void FileTcpMgr::SendDownloadInfo(std::shared_ptr<DownloadInfo> download) {QJsonObject jsonObj;jsonObj["name"] = download->_name;jsonObj["seq"] = download->_seq;jsonObj["trans_size"] = 0;jsonObj["total_size"] = 0;jsonObj["token"] = UserMgr::GetInstance()->GetToken();jsonObj["uid"] = UserMgr::GetInstance()->GetUid();jsonObj["client_path"] = download->_client_path;QJsonDocument doc(jsonObj);auto send_data = doc.toJson();SendData(ID_DOWN_LOAD_FILE_REQ, send_data);}
接收服务器回传
_handlers.insert(ID_DOWN_LOAD_FILE_RSP, [this](ReqId id, int len, QByteArray data) {Q_UNUSED(len);qDebug() << "handle id is " << id << " data is " << data;// 将QByteArray转换为QJsonDocumentQJsonDocument jsonDoc = QJsonDocument::fromJson(data);// 检查转换是否成功if (jsonDoc.isNull()) {qDebug() << "Failed to create QJsonDocument.";return;}QJsonObject jsonObj = jsonDoc.object();if (!jsonObj.contains("error")) {int err = ErrorCodes::ERR_JSON;qDebug() << "parse create private chat json parse failed " << err;return;}int err = jsonObj["error"].toInt();if (err != ErrorCodes::SUCCESS) {qDebug() << "get create private chat failed, error is " << err;return;}qDebug() << "Receive download file info rsp success";QString base64Data = jsonObj["data"].toString();QString clientPath = jsonObj["client_path"].toString();int seq = jsonObj["seq"].toInt();bool is_last = jsonObj["is_last"].toBool();QString total_size_str = jsonObj["total_size"].toString();qint64 total_size = total_size_str.toLongLong(nullptr);QString current_size_str = jsonObj["current_size"].toString();qint64 current_size = current_size_str.toLongLong(nullptr);QString name = jsonObj["name"].toString();auto file_info = UserMgr::GetInstance()->GetDownloadInfo(name);if (file_info == nullptr) {qDebug() << "file: " << name << " not found";return;}file_info->_current_size = current_size;file_info->_total_size = total_size;//Base64解码QByteArray decodedData = QByteArray::fromBase64(base64Data.toUtf8());QFile file(clientPath);// 根据 seq 决定打开模式QIODevice::OpenMode mode;if (seq == 1) {// 第一个包,覆盖写入mode = QIODevice::WriteOnly;}else {// 后续包,追加写入mode = QIODevice::WriteOnly | QIODevice::Append;}if (!file.open(mode)) {qDebug() << "Failed to open file for writing:" << clientPath;qDebug() << "Error:" << file.errorString();return;}qint64 bytesWritten = file.write(decodedData);if (bytesWritten != decodedData.size()) {qDebug() << "Failed to write all data. Written:" << bytesWritten<< "Expected:" << decodedData.size();}file.close();qDebug() << "Successfully wrote" << bytesWritten << "bytes to file";qDebug() << "Progress:" << current_size << "/" << total_size<< "(" << (current_size * 100 / total_size) << "%)";if (is_last) {qDebug() << "File download completed:" << clientPath;UserMgr::GetInstance()->RmvDownloadFile(name);//发送信号通知主界面重新加载labelemit sig_reset_label_icon(clientPath);}else {//继续请求file_info->_seq = seq+1;FileTcpMgr::GetInstance()->SendDownloadInfo(file_info);}});
- 判断seq是否为1,如果为1则说明新的文件,需要创建并保存
- 如果seq不为1,则说明是续传文件,更新追加就可以了
- 如果
is_last字段为true,说明是最后一个包,那么移除缓存的下载信息,同时将信息发送到主界面更新图标
更新页面逻辑
在ChatDialog界面中响应这个信号
//重置label iconconnect(FileTcpMgr::GetInstance().get(), &FileTcpMgr::sig_reset_label_icon, this, &ChatDialog::slot_reset_icon);
槽函数处理
void ChatDialog::slot_reset_icon(QString path) {UserMgr::GetInstance()->ResetLabelIcon(path);}
UserMgr中封装重置icon逻辑
void UserMgr::ResetLabelIcon(QString path){auto iter = _name_to_reset_labels.find(path);if (iter == _name_to_reset_labels.end()) {return;}for (auto ele_iter = iter.value().begin(); ele_iter != iter.value().end(); ele_iter++) {QPixmap pixmap(path); // 加载上传的头像图片if (!pixmap.isNull()) {QPixmap scaledPixmap = pixmap.scaled((*ele_iter)->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);(*ele_iter)->setPixmap(scaledPixmap);(*ele_iter)->setScaledContents(true);}else {qWarning() << "无法加载上传的头像:" << path;}}_name_to_reset_labels.erase(iter);}
测试效果
服务器断点传输逻辑
增加下载worker
class DownloadWorker {public:DownloadWorker();~DownloadWorker();void PostTask(std::shared_ptr<DownloadTask> task);private:void task_callback(std::shared_ptr<DownloadTask>);std::thread _work_thread;std::queue<std::shared_ptr<DownloadTask>> _task_que;std::atomic<bool> _b_stop;std::mutex _mtx;std::condition_variable _cv;};
DownloadWorker处理逻辑和之前的FileWorker类似
DownloadWorker::DownloadWorker() :_b_stop(false){_work_thread = std::thread([this]() {while (!_b_stop) {std::unique_lock<std::mutex> lock(_mtx);_cv.wait(lock, [this]() {if (_b_stop) {return true;}if (_task_que.empty()) {return false;}return true;});if (_b_stop) {break;}auto task = _task_que.front();_task_que.pop();task_callback(task);}});}DownloadWorker::~DownloadWorker(){_b_stop = true;_cv.notify_one();_work_thread.join();}void DownloadWorker::PostTask(std::shared_ptr<DownloadTask> task){{std::lock_guard<std::mutex> lock(_mtx);_task_que.push(task);}_cv.notify_one();}void DownloadWorker::task_callback(std::shared_ptr<DownloadTask> task){// 解码auto file_path_str = task->_file_path;//std::cout << "file_path_str is " << file_path_str << std::endl;boost::filesystem::path file_path(file_path_str);// 获取完整文件名(包含扩展名)std::string filename = file_path.filename().string();Json::Value result;result["error"] = ErrorCodes::Success;if (!boost::filesystem::exists(file_path)) {std::cerr << "文件不存在: " << file_path_str << std::endl;result["error"] = ErrorCodes::FileNotExists;task->_callback(result);return;}std::ifstream infile(file_path_str, std::ios::binary);if (!infile) {std::cerr << "无法打开文件进行读取。" << std::endl;result["error"] = ErrorCodes::FileReadPermissionFailed;task->_callback(result);return;}std::shared_ptr<FileInfo> file_info = nullptr;if (task->_seq == 1) {// 获取文件大小infile.seekg(0, std::ios::end);std::streamsize file_size = infile.tellg();infile.seekg(0, std::ios::beg);//如果为空,则创建FileInfo 构造数据存储file_info = std::make_shared<FileInfo>();file_info->_file_path_str = file_path_str;file_info->_name = filename;file_info->_seq = 1;file_info->_total_size = file_size;file_info->_trans_size = 0;// 立即保存到 Redis,覆盖旧数据,设置过期时间RedisMgr::GetInstance()->SetDownLoadInfo(filename, file_info);std::cout << "[新下载] 文件: " << filename<< ", 大小: " << file_size << " 字节" << std::endl;}else {//断点续传,从 Redis 获取历史信息file_info = RedisMgr::GetInstance()->GetDownloadInfo(filename);if (file_info == nullptr) {// Redis 中没有信息(可能过期了)std::cerr << "断点续传失败,Redis 中无下载信息: " << filename << std::endl;result["error"] = ErrorCodes::RedisReadErr;task->_callback(result);infile.close();return;}// 验证序列号是否匹配if (task->_seq != file_info->_seq) {std::cerr << "序列号不匹配,期望: " << file_info->_seq<< ", 实际: " << task->_seq << std::endl;result["error"] = ErrorCodes::FileSeqInvalid;task->_callback(result);infile.close();return;}std::cout << "[续传] 文件: " << filename<< ", seq: " << task->_seq<< ", 进度: " << file_info->_trans_size<< "/" << file_info->_total_size << std::endl;}// 计算当前偏移量std::streamsize offset = ((std::streamsize)task->_seq - 1) * MAX_FILE_LEN;if (offset >= file_info->_total_size) {std::cerr << "偏移量超出文件大小。" << std::endl;result["error"] = ErrorCodes::FileOffsetInvalid;task->_callback(result);infile.close();return;}// 定位到指定偏移量infile.seekg(offset);// 读取最多2048字节char buffer[MAX_FILE_LEN];infile.read(buffer, MAX_FILE_LEN);//获取read实际读取多少字节std::streamsize bytes_read = infile.gcount();if (bytes_read <= 0) {std::cerr << "读取文件失败。" << std::endl;result["error"] = ErrorCodes::FileReadFailed;task->_callback(result);infile.close();return;}// 将读取的数据进行base64编码std::string data_to_encode(buffer, bytes_read);std::string encoded_data = base64_encode(data_to_encode);// 检查是否是最后一个包std::streamsize current_pos = offset + bytes_read;bool is_last = (current_pos >= file_info->_total_size);// 设置返回结果result["data"] = encoded_data;result["seq"] = task->_seq;result["total_size"] = std::to_string(file_info->_total_size);result["current_size"] = std::to_string(current_pos);result["is_last"] = is_last;infile.close();if (is_last) {std::cout << "文件读取完成: " << file_path_str << std::endl;RedisMgr::GetInstance()->DelDownLoadInfo(filename);}else {//更新信息file_info->_seq++;file_info->_trans_size = offset + bytes_read;//更新redisRedisMgr::GetInstance()->SetDownLoadInfo(filename, file_info);}if (task->_callback) {task->_callback(result);}}
在FileSystem中创建worker
FileSystem::FileSystem(){for (int i = 0; i < FILE_WORKER_COUNT; i++) {_file_workers.push_back(std::make_shared<FileWorker>());}for (int i = 0; i < DOWN_LOAD_WORKER_COUNT; i++) {_down_load_worker.push_back(std::make_shared<DownloadWorker>());}}
测试效果
将头像资源从本地删除后,重新登录时或者切换页面会引发资源重新加载,向服务器请求资源后再设置到界面显示。
