当前位置:   article > 正文

Qt Quick实现的文件传输工具(TCP传输篇)_qt项目实战极速文件传输工具

qt项目实战极速文件传输工具

【写在前面】

        本篇为传输篇。

        接上篇。


 【正文开始】

        在上一篇中,我们已经扫描到了整个局域网中运行了本工具的用户,以 <name, ip> 的形式存储在 DiscoverConnection 中,并且,它是一个单例对象。

        在 qml 中,这些 name 还会存储在一个 ListModel 中:

  1. .
  2. .
  3. .
  4. ListView
  5. {
  6. id: listView
  7. clip: true
  8. anchors.top: apLabel.bottom
  9. anchors.topMargin: 10
  10. anchors.bottom: parent.bottom
  11. anchors.left: parent.left
  12. anchors.leftMargin: 5
  13. anchors.right: parent.right
  14. spacing: 4
  15. ScrollBar.vertical: ScrollBar
  16. {
  17. policy: ScrollBar.AsNeeded
  18. }
  19. displaced: Transition
  20. {
  21. NumberAnimation { properties: "x,y"; easing.type: Easing.OutQuad }
  22. }
  23. model: ListModel { id: accessPoints }
  24. delegate: Component
  25. {
  26. Rectangle
  27. {
  28. width: listView.width - 20
  29. height: 32
  30. radius: 2
  31. border.color: "#777"
  32. color: hovered ? "#559EF2FA" : "#55556677"
  33. property bool hovered: false
  34. MouseArea
  35. {
  36. anchors.fill: parent
  37. hoverEnabled: true
  38. onEntered: parent.hovered = true;
  39. onExited: parent.hovered = false;
  40. onClicked:
  41. {
  42. scanner.stop();
  43. discoverCon.connectToName(name);
  44. fileTransfer.setAccessPoint(name);
  45. root.connected = true;
  46. root.connectName = name;
  47. accessPoints.clear();
  48. }
  49. }
  50. Text
  51. {
  52. anchors.centerIn: parent
  53. text: qsTr(name)
  54. }
  55. }
  56. }
  57. }
  58. .
  59. .
  60. .

        在 MouseArea.onClicked 中( 即用户点击了某一个 name 时 ),将会进行:

        1、scanner.stop() 停止扫描动画。

        2、discoverCon.connectToName(name),这将会发送一个 UDP 数据报 [CONNECT]## + name

        3、fileTransfer.setAccessPoint(name),其实现如下:

  1. void FileTransfer::setAccessPoint(const QString &name)
  2. {
  3. DiscoverConnection *dc = DiscoverConnection::instance();
  4. QHostAddress address = dc->getAddress(name);
  5. QThread *thread = new QThread;
  6. connect(thread, &QThread::finished, thread, &QThread::deleteLater);
  7. TransferSocket *socket = new TransferSocket;
  8. socket->moveToThread(thread);
  9. thread->start();
  10. m_socket = socket;
  11. QMetaObject::invokeMethod(m_socket, "setDestAddress", Q_ARG(QHostAddress, address));
  12. }

        这将会创建一个传输用的 TCP 连接,并运行在另一个独立的线程中,因此,最后一行使用 invokeMethod() 调用它的 setDestAddress() 来设置目的地址。

        4、accessPoints.clear(),每次连接后将会清空上一次扫描到的用户。

        接下来就是传输用的 TransferSocket。

        transfersocket.h:

  1. #ifndef TRANSFERSOCKET_H
  2. #define TRANSFERSOCKET_H
  3. #include <QTcpSocket>
  4. #include <QHostAddress>
  5. class QFile;
  6. class TransferSocket : public QTcpSocket
  7. {
  8. Q_OBJECT
  9. public:
  10. TransferSocket();
  11. ~TransferSocket();
  12. void requestNewConnection();
  13. Q_INVOKABLE void setDestAddress(const QHostAddress &address);
  14. Q_INVOKABLE void sendFile(const QUrl &url);
  15. Q_INVOKABLE void writeToSocket(const QByteArray &data) { QTcpSocket::write(data); }
  16. signals:
  17. void hasError(const QString &error);
  18. public slots:
  19. void processRecvBlock();
  20. private:
  21. int m_maxRecvNum = 8;
  22. QString m_cachePath;
  23. QByteArray m_recvData;
  24. //可以用一个struct File { QFile *file; qint32 size; }
  25. QMap<QString, QFile *> m_recvFiles;
  26. QMap<QString, qint32> m_recvFileSize;
  27. QHostAddress m_destAddress;
  28. };
  29. #endif // TRANSFERSOCKET_H

        这里我用了两个 QMap 来存储接收文件的 QFile 指针和其大小,实际上用一个 QMap<QString, File*> 存储即可,File 在注释中提到,我就懒得改了( •́ὤ•̀)~

        m_maxRecvNum 是用来设置同时接收文件数上限的( 即最多同时接收8个文件 ),然鹅只是我设想中的,实际并没有实现,有兴趣的可以自己实现它。

        然后是 transfersocket.cpp:

  1. #include "fileblock.h"
  2. #include "filemanager.h"
  3. #include "transfersocket.h"
  4. #include <QtConcurrent>
  5. #include <QFile>
  6. #include <QFileInfo>
  7. #include <QQmlFile>
  8. const int maxBlockSize = 1024;
  9. TransferSocket::TransferSocket()
  10. {
  11. m_cachePath = qApp->applicationDirPath() + "/FileRecv/";
  12. QDir dir;
  13. if (!dir.exists(m_cachePath))
  14. {
  15. dir.mkpath(m_cachePath);
  16. }
  17. connect(this, &QTcpSocket::readyRead, this, [this]()
  18. {
  19. m_recvData += readAll();
  20. processRecvBlock();
  21. });
  22. }
  23. TransferSocket::~TransferSocket()
  24. {
  25. }
  26. void TransferSocket::requestNewConnection()
  27. {
  28. abort();
  29. connectToHost(m_destAddress, 43800);
  30. waitForConnected(5000);
  31. }
  32. void TransferSocket::setDestAddress(const QHostAddress &address)
  33. {
  34. if (m_destAddress != address)
  35. m_destAddress = address;
  36. requestNewConnection();
  37. }
  38. void TransferSocket::sendFile(const QUrl &url)
  39. {
  40. if (state() != SocketState::ConnectedState)
  41. requestNewConnection();
  42. QtConcurrent::run([this, url]()
  43. {
  44. QTime time;
  45. time.start();
  46. QFile file(QQmlFile::urlToLocalFileOrQrc(url));
  47. file.open(QIODevice::ReadOnly);
  48. qint32 offset = 0;
  49. qint32 totalSize = qint32(file.size());
  50. QString fileName = QFileInfo(QQmlFile::urlToLocalFileOrQrc(url)).fileName();
  51. while (offset < totalSize)
  52. {
  53. file.seek(offset);
  54. QByteArray dataBlock = file.read(maxBlockSize);
  55. FileBlock block = { qint16(dataBlock.size()), offset, totalSize,
  56. fileName.toLocal8Bit(), dataBlock};
  57. QByteArray data;
  58. QDataStream out(&data, QIODevice::WriteOnly);
  59. out.setVersion(QDataStream::Qt_5_12);
  60. out << block;
  61. QMetaObject::invokeMethod(this, "writeToSocket", Q_ARG(QByteArray, data));
  62. offset += dataBlock.size();
  63. if (time.elapsed() >= 1000 || offset >= totalSize)
  64. {
  65. time.restart();
  66. QMetaObject::invokeMethod(FileManager::instance(), "updateWriteFile",
  67. Q_ARG(QString, fileName), Q_ARG(int, offset));
  68. }
  69. }
  70. file.close();
  71. });
  72. }
  73. void TransferSocket::processRecvBlock()
  74. {
  75. static QTime time = QTime::currentTime();
  76. if (m_recvData.size() > 0)
  77. {
  78. FileBlock block;
  79. QDataStream in(&m_recvData, QIODevice::ReadOnly);
  80. in.setVersion(QDataStream::Qt_5_12);
  81. in >> block;
  82. if (block.isEmpty())
  83. return;
  84. QString fileName = QString::fromLocal8Bit(block.fileName);
  85. if (!m_recvFiles[fileName])
  86. {
  87. QFile *file = new QFile(m_cachePath + fileName);
  88. file->open(QIODevice::WriteOnly);
  89. m_recvFiles[fileName] = file;
  90. m_recvFileSize[fileName] = 0;
  91. QMetaObject::invokeMethod(FileManager::instance(), "addReadFile",
  92. Q_ARG(QString, fileName), Q_ARG(int, block.fileSize));
  93. QThread::msleep(100);
  94. }
  95. if (m_recvFileSize[fileName] < block.fileSize)
  96. {
  97. m_recvFileSize[fileName] += block.blockSize;
  98. m_recvFiles[fileName]->write(block.dataBlock);
  99. qDebug() << block;
  100. }
  101. if (m_recvFileSize[fileName] == block.fileSize)
  102. {
  103. m_recvFiles[fileName]->close();
  104. m_recvFiles[fileName]->deleteLater();
  105. m_recvFiles.remove(fileName);
  106. m_recvFileSize.remove(fileName);
  107. QMetaObject::invokeMethod(FileManager::instance(), "updateReadFile",
  108. Q_ARG(QString, fileName), Q_ARG(int, block.fileSize));
  109. }
  110. if (time.elapsed() >= 1000)
  111. {
  112. time.restart();
  113. QMetaObject::invokeMethod(FileManager::instance(), "updateReadFile",
  114. Q_ARG(QString, fileName), Q_ARG(int, m_recvFileSize[fileName]));
  115. }
  116. m_recvData.remove(0, block.size());
  117. if (m_recvData.size() > 0) //如果还有则继续处理
  118. processRecvBlock();
  119. }
  120. }

        ★ 构造函数中,初始化了接收文件的路径,并且,连接 QTcpSocket::readyRead 信号,将接收到的数据进行缓存并调用文件块处理程序(函数)。

        这里,我用的文件块来传输某个文件的某一部分,它定义在 fileblock.h 中:

  1. #ifndef FILEBLOCK_H
  2. #define FILEBLOCK_H
  3. #include <QDebug>
  4. #include <QtGlobal>
  5. #include <QDataStream>
  6. struct FileBlock
  7. {
  8. qint16 blockSize;
  9. qint32 offset;
  10. qint32 fileSize;
  11. QByteArray fileName;
  12. QByteArray dataBlock;
  13. bool isEmpty() const
  14. {
  15. return fileName.isEmpty() || dataBlock.isEmpty();
  16. }
  17. int size() const
  18. {
  19. return int(sizeof(blockSize)) +
  20. int(sizeof(offset)) +
  21. int(sizeof(fileSize)) +
  22. fileName.size() +
  23. dataBlock.size() +
  24. 2 * 4; //有两个QByteArray,每个会在前面加4字节大小
  25. }
  26. };
  27. QDataStream& operator>>(QDataStream &in, FileBlock &block);
  28. QDataStream& operator<<(QDataStream &out, FileBlock &block);
  29. QDebug operator<<(QDebug debug, const FileBlock &block);
  30. #endif // FILEBLOCK_H

        具体每个成员应该不用我说明了吧,见名知意,接着我们回到 TransferSocket:

        ★ requestNewConnection() 和 setDestAddress() 比较简单,就不说明了。

        ★ sendFile(),发送文件的函数,它接受一个本地文件的 URL,这个 URL 由 qml 传进来的,因此我比较喜欢使用 QQmlFile::urlToLocalFileOrQrc() 来进行解析。

        1、QtConcurrent::run() 是 Qt 提供的高级并发 API,使用起来非常方便,这样,每个文件的发送将会使用一个单独的线程进行。

        2、QTime 用来记录流逝的时间,如果过去了一秒,就更新文件进度,文件进度由文件管理器 FileManager 控制,它是一个单例。

        3、QFile 保存了发送的文件,我们使用 seek() 设置文件偏移,然后读取 maxBlockSize ( 任意,我这里是1024 )字节的数据。

        4、需要注意的是,lambda 函数运行在不同于 TransferSocket 的线程中,在 Qt Socket 中,不同的线程不能直接 Read / Write 的,因此,这里使用一个 Q_INVOKABLE 修饰的 writeToSocket() 函数来进行 socket write,记住使用 invokeMethod() 调用(或者加锁)。

        ★ processRecvBlock() 文件块处理函数。每当有数据到来时被调用。

        1、首先我们尝试读取一个文件块,如果不为空( 即为完整的块 )。

        2、然后我们将这个块中的数据写入对应文件中。

        3、清空缓存,并且,如果流逝的时间超过一秒,就更新文件进度。

        至此,文件传输部分结束。


 【结语】

        好了,其实整个工具最关键的部分就是传输部分,然鹅我并没有花多少时间在这上面,反而在界面上花了不少时间( 有些本末倒置了呢 ),不过界面有些地方确实麻烦( qml 也没想象那么好用啊〒▽〒 )。

        然后还有一些 ConnectionManagerFileManager 之类的很简单而且没多少关系就没有讲了,建议自己看代码了。

        最后还有一些功能比如暂停/继续等等没有实现,不过都很容易,有兴趣可以自己玩玩~

        最后,资源地址:QtQuick制作的文件传输器-C++文档类资源-CSDN下载

        也可以访问项目地址:https://github.com/mengps/FileTransfer

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/繁依Fanyi0/article/detail/305146
推荐阅读
相关标签
  

闽ICP备14008679号