Qt Quick实现的文件传输工具(TCP传输篇)

【写在前面】

        本篇为传输篇。

        接上篇。


 【正文开始】

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

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

    .
    .
    .
ListView
{
    id: listView
    clip: true
    anchors.top: apLabel.bottom
    anchors.topMargin: 10
    anchors.bottom: parent.bottom
    anchors.left: parent.left
    anchors.leftMargin: 5
    anchors.right: parent.right
    spacing: 4
    ScrollBar.vertical: ScrollBar
    {
        policy: ScrollBar.AsNeeded
    }
    displaced: Transition
    {
        NumberAnimation { properties: "x,y"; easing.type: Easing.OutQuad }
    }
    model: ListModel { id: accessPoints }
    delegate: Component
    {
        Rectangle
        {
            width: listView.width - 20
            height: 32
            radius: 2
            border.color: "#777"
            color: hovered ? "#559EF2FA" : "#55556677"
            property bool hovered: false

            MouseArea
            {
                anchors.fill: parent
                hoverEnabled: true
                onEntered: parent.hovered = true;
                onExited: parent.hovered = false;
                onClicked:
                {
                    scanner.stop();
                    discoverCon.connectToName(name);
                    fileTransfer.setAccessPoint(name);
                    root.connected = true;
                    root.connectName = name;
                    accessPoints.clear();
                }
            }

            Text
            {
                anchors.centerIn: parent
                text: qsTr(name)
            }
        }
    }
}
    .
    .
    .

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

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

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

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

void FileTransfer::setAccessPoint(const QString &name)
{
    DiscoverConnection *dc = DiscoverConnection::instance();
    QHostAddress address = dc->getAddress(name);

    QThread *thread = new QThread;
    connect(thread, &QThread::finished, thread, &QThread::deleteLater);
    TransferSocket *socket = new TransferSocket;
    socket->moveToThread(thread);
    thread->start();
    m_socket = socket;
    QMetaObject::invokeMethod(m_socket, "setDestAddress", Q_ARG(QHostAddress, address));
}

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

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

        接下来就是传输用的 TransferSocket。

        transfersocket.h:

#ifndef TRANSFERSOCKET_H
#define TRANSFERSOCKET_H

#include <QTcpSocket>
#include <QHostAddress>

class QFile;
class TransferSocket : public QTcpSocket
{
    Q_OBJECT

public:
    TransferSocket();
    ~TransferSocket();

    void requestNewConnection();

    Q_INVOKABLE void setDestAddress(const QHostAddress &address);
    Q_INVOKABLE void sendFile(const QUrl &url);
    Q_INVOKABLE void writeToSocket(const QByteArray &data) { QTcpSocket::write(data); }

signals:
    void hasError(const QString &error);

public slots:
    void processRecvBlock();

private:
    int m_maxRecvNum = 8;
    QString m_cachePath;
    QByteArray m_recvData;
    //可以用一个struct File { QFile *file; qint32 size; }
    QMap<QString, QFile *> m_recvFiles;
    QMap<QString, qint32> m_recvFileSize;
    QHostAddress m_destAddress;
};

#endif // TRANSFERSOCKET_H

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

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

        然后是 transfersocket.cpp:

#include "fileblock.h"
#include "filemanager.h"
#include "transfersocket.h"

#include <QtConcurrent>
#include <QFile>
#include <QFileInfo>
#include <QQmlFile>

const int maxBlockSize = 1024;

TransferSocket::TransferSocket()
{
    m_cachePath = qApp->applicationDirPath() + "/FileRecv/";
    QDir dir;
    if (!dir.exists(m_cachePath))
    {
        dir.mkpath(m_cachePath);
    }

    connect(this, &QTcpSocket::readyRead, this, [this]()
    {
        m_recvData += readAll();
        processRecvBlock();
    });
}

TransferSocket::~TransferSocket()
{

}

void TransferSocket::requestNewConnection()
{
    abort();
    connectToHost(m_destAddress, 43800);
    waitForConnected(5000);
}

void TransferSocket::setDestAddress(const QHostAddress &address)
{
    if (m_destAddress != address)
        m_destAddress = address;
    requestNewConnection();
}

void TransferSocket::sendFile(const QUrl &url)
{
    if (state() != SocketState::ConnectedState)
        requestNewConnection();

    QtConcurrent::run([this, url]()
    {
        QTime time;
        time.start();
        QFile file(QQmlFile::urlToLocalFileOrQrc(url));
        file.open(QIODevice::ReadOnly);

        qint32 offset = 0;
        qint32 totalSize = qint32(file.size());
        QString fileName = QFileInfo(QQmlFile::urlToLocalFileOrQrc(url)).fileName();
        while (offset < totalSize)
        {
            file.seek(offset);
            QByteArray dataBlock = file.read(maxBlockSize);
            FileBlock block = { qint16(dataBlock.size()), offset, totalSize,
                                fileName.toLocal8Bit(), dataBlock};
            QByteArray data;
            QDataStream out(&data, QIODevice::WriteOnly);
            out.setVersion(QDataStream::Qt_5_12);
            out << block;
            QMetaObject::invokeMethod(this, "writeToSocket", Q_ARG(QByteArray, data));

            offset += dataBlock.size();
            if (time.elapsed() >= 1000 || offset >= totalSize)
            {
                time.restart();
                QMetaObject::invokeMethod(FileManager::instance(), "updateWriteFile",
                                          Q_ARG(QString, fileName), Q_ARG(int, offset));
            }
        }

        file.close();
    });
}

void TransferSocket::processRecvBlock()
{
    static QTime time = QTime::currentTime();
    if (m_recvData.size() > 0)
    {
        FileBlock block;
        QDataStream in(&m_recvData, QIODevice::ReadOnly);
        in.setVersion(QDataStream::Qt_5_12);
        in >> block;

        if (block.isEmpty())
            return;

        QString fileName = QString::fromLocal8Bit(block.fileName);

        if (!m_recvFiles[fileName])
        {
            QFile *file = new QFile(m_cachePath + fileName);
            file->open(QIODevice::WriteOnly);
            m_recvFiles[fileName] = file;
            m_recvFileSize[fileName] = 0;
            QMetaObject::invokeMethod(FileManager::instance(), "addReadFile",
                                      Q_ARG(QString, fileName), Q_ARG(int, block.fileSize));
            QThread::msleep(100);
        }

        if (m_recvFileSize[fileName] < block.fileSize)
        {
            m_recvFileSize[fileName] += block.blockSize;
            m_recvFiles[fileName]->write(block.dataBlock);
            qDebug() << block;
        }

        if (m_recvFileSize[fileName] == block.fileSize)
        {
            m_recvFiles[fileName]->close();
            m_recvFiles[fileName]->deleteLater();
            m_recvFiles.remove(fileName);
            m_recvFileSize.remove(fileName);
            QMetaObject::invokeMethod(FileManager::instance(), "updateReadFile",
                                      Q_ARG(QString, fileName), Q_ARG(int, block.fileSize));
        }

        if (time.elapsed() >= 1000)
        {
            time.restart();
            QMetaObject::invokeMethod(FileManager::instance(), "updateReadFile",
                                      Q_ARG(QString, fileName), Q_ARG(int, m_recvFileSize[fileName]));
        }

        m_recvData.remove(0, block.size());
        if (m_recvData.size() > 0)  //如果还有则继续处理
            processRecvBlock();
    }
}

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

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

#ifndef FILEBLOCK_H
#define FILEBLOCK_H

#include <QDebug>
#include <QtGlobal>
#include <QDataStream>

struct FileBlock
{
    qint16 blockSize;
    qint32 offset;
    qint32 fileSize;
    QByteArray fileName;
    QByteArray dataBlock;

    bool isEmpty() const
    {
        return fileName.isEmpty() || dataBlock.isEmpty();
    }

    int size() const
    {
        return int(sizeof(blockSize)) +
               int(sizeof(offset)) +
               int(sizeof(fileSize)) +
               fileName.size() +
               dataBlock.size() +
               2 * 4;       //有两个QByteArray,每个会在前面加4字节大小
    }
};

QDataStream& operator>>(QDataStream &in, FileBlock &block);
QDataStream& operator<<(QDataStream &out, FileBlock &block);
QDebug operator<<(QDebug debug, const FileBlock &block);

#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

评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

梦起丶

您的鼓励和支持是我创作最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值