【写在前面】
本篇为传输篇。
接上篇。
【正文开始】
在上一篇中,我们已经扫描到了整个局域网中运行了本工具的用户,以 <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 也没想象那么好用啊〒▽〒 )。
然后还有一些 ConnectionManager、FileManager 之类的很简单而且没多少关系就没有讲了,建议自己看代码了。
最后还有一些功能比如暂停/继续等等没有实现,不过都很容易,有兴趣可以自己玩玩~
最后,资源地址:QtQuick制作的文件传输器-C++文档类资源-CSDN下载
也可以访问项目地址:https://github.com/mengps/FileTransfer