Qt实战2.老生常谈的文件传输

1 需求描述

  1. 实现点对点的文件传输功能;
  2. 可以批量传输文件。

2 设计思路

说到文件的传输当然使用QTcpSocket,思路还是蛮简单的,发送端维护一个文件队列,然后再将队列中的文件逐个传输到服务端,服务端使用QTcpServer进行监听,并逐个接收文件。
为了实现文件名的统一,客户端每次发送新文件时需要先发送文件名以及文件的大小,这样服务端才能做好后续处理。

3 代码实现

3.1 服务端(接收端)

服务端处理过程:打开监听->处理连接->接收数据->文件落盘

  1. 服务端首先打开端口监听,以便处理客户端的连接请求,相关代码如下:
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow),
    m_pSocket(nullptr),
    m_fileSize(0),
    m_fileBytesReceived(0)
{
    ui->setupUi(this);

    setWindowTitle(QApplication::applicationName() + QStringLiteral(" Qt小罗"));
    qApp->setStyle(QStyleFactory::create("fusion"));

    if (m_server.listen()) {
        ui->statusBar->showMessage(QStringLiteral("状态:正在监听!"));
    } else {
        ui->statusBar->showMessage(QStringLiteral("状态:监听失败!"));
    }
    ui->labelListenPort->setText(QString::number(m_server.serverPort()));

    connect(&m_server, &QTcpServer::newConnection, this, &MainWindow::onNewConnection);
    connect(ui->pushButtonCancel, &QPushButton::clicked, this, &MainWindow::close);
}
void MainWindow::onNewConnection()
{
    m_pSocket = m_server.nextPendingConnection();

    connect(m_pSocket, &QTcpSocket::disconnected, m_pSocket, &QObject::deleteLater);
    connect(m_pSocket, &QIODevice::readyRead, this, &MainWindow::onReadyRead);
    connect(m_pSocket, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(onSocketError(QAbstractSocket::SocketError)));

    m_inStream.setDevice(m_pSocket);
    m_inStream.setVersion(QDataStream::Qt_5_0);
}
  1. 然后接收客户端的数据,先接收文件名和文件大小信息,然后接收文件的二进制数据,接收代码如下:
void MainWindow::onReadyRead()
{
    while (m_pSocket->bytesAvailable()) {
        if (0 == m_fileSize && m_pSocket->bytesAvailable() > sizeof(qint64)) {
            m_inStream >> m_fileSize >> m_fileName;
            m_file.close();
            m_file.setFileName(m_fileName);
            if (!m_file.open(QIODevice::WriteOnly)) {
                qCritical() << m_file.errorString();
                return;
            }
            ui->plainTextEditLog->appendPlainText(QStringLiteral("正在接收【%1】 ...").arg(m_fileName));
        } else {
            qint64 size = qMin(m_pSocket->bytesAvailable(), m_fileSize - m_fileBytesReceived);
            if (size == 0) {
                reset();
                continue;
            }

            QByteArray arry(size, 0);
            m_inStream.readRawData(arry.data(), size);
            m_file.write(arry);

            m_fileBytesReceived += size;

            if (m_fileBytesReceived == m_fileSize) {
                QFileInfo info(m_fileName);
                ui->plainTextEditLog->appendPlainText(QStringLiteral("成功接收【%1】 -> %2").arg(m_fileName).arg(info.absoluteFilePath()));
                reset();
            }
        }
    }
}

到这里,服务端已准备就绪,随时准备接收客户端的连接请求。

3.2 客户端(发送端)

客户端处理过程:选择文件列表->连接服务端->连接建立后自动逐个打开队列中的文件并发送

  1. 文件选择后,点击发送按钮,连接服务端,相关代码如下:
void MainWindow::sendFile()
{
    QString address = ui->lineEditIpAddress->text();
    int port = ui->spinBoxPort->text().toInt();

    QHostAddress hostAddress;
    if (!hostAddress.setAddress(address)) {
        QMessageBox::critical(this, QStringLiteral("错误"), QStringLiteral("目标网络地址错误!"));
        return;
    }

    if (0 == ui->listWidget->count()) {
        QMessageBox::information(this, QStringLiteral("提示"), QStringLiteral("请选择需要发送的文件!"));
        addFile();
        return;
    }

    m_fileQueue.clear();
    int count = ui->listWidget->count();
    for (int i = 0; i < count; ++i) {
        QString file = ui->listWidget->item(i)->text();
        m_fileQueue.append(file);

        QFileInfo info(file);
        m_totalFileSize += info.size();
    }

    m_socket.connectToHost(address, port);
}
  1. 与服务端的连接建立后,客户端socket状态改变发出信号,对应的槽函数内调用send自动发送文件,相关代码如下:
void MainWindow::onSocketStateChanged(QAbstractSocket::SocketState state)
{
    switch (state) {
    case QAbstractSocket::UnconnectedState:
        m_file.close();
        qDebug() << m_totalFileSize << " " << m_totalFileBytesWritten;
        qDebug() << __FUNCTION__ << "QAbstractSocket::UnconnectedState";
        break;
    case QAbstractSocket::HostLookupState:
        qDebug() << __FUNCTION__ << "QAbstractSocket::HostLookupState";
        break;
    case QAbstractSocket::ConnectingState:
        qDebug() << __FUNCTION__ << "QAbstractSocket::ConnectingState";
        break;
    case QAbstractSocket::ConnectedState:
        qDebug() << __FUNCTION__ << "QAbstractSocket::ConnectedState";
        m_timer.restart();
        send();
        break;
    case QAbstractSocket::BoundState:
        break;
    case QAbstractSocket::ClosingState:
        qDebug() << __FUNCTION__ << "QAbstractSocket::ClosingState";
        break;
    case QAbstractSocket::ListeningState:
        break;
    default:
        break;
    }
}
void MainWindow::send()
{
    m_file.close();
    m_file.setFileName(m_fileQueue.dequeue());

    if (!m_file.open(QIODevice::ReadOnly)) {
        qCritical() << m_file.errorString();
        QMessageBox::critical(this, QStringLiteral("错误"), m_file.errorString());
        return;
    }

    m_currentFileSize = m_file.size();

    //设置当前文件进度显示格式
    ui->currentProgressBar->setFormat(QStringLiteral("%1 : %p%").arg(m_file.fileName()));

    m_outStream.setDevice(&m_socket);
    m_outStream.setVersion(QDataStream::Qt_5_0);

    QFileInfo info(m_file.fileName());
    QString fileName = info.fileName();

    //发送文件大小及文件名
    m_outStream << m_currentFileSize << fileName;

    //开始传输文件
    QByteArray arry = m_file.read(m_blockSize);
    int size = arry.size();
    m_outStream.writeRawData(arry.constData(), size);

    ui->pushButtonSend->setEnabled(false);
    updateProgress(size);
}
  1. 客户端每次发送数据后,socket会发出bytesWritten信号,通过该信号进行循环发送,直到文件发送完毕,对应的槽函数如下:
void MainWindow::onBytesWritten(const qint64 &bytes)
{
    Q_UNUSED(bytes)

    QByteArray arry = m_file.read(m_blockSize);
    if (arry.isEmpty()) {
        reset();
        return;
    }

    int size = arry.size();
    m_outStream.writeRawData(arry.constData(), size);

    updateProgress(size);
}
void MainWindow::reset()
{
    ui->pushButtonSend->setEnabled(true);

    m_currentFileBytesWritten = 0;

    if (m_fileQueue.isEmpty()) {
        m_socket.close();

        qint64 milliseconds = m_timer.elapsed();
        QMessageBox::information(this, QStringLiteral("提示"), QStringLiteral("共耗时:%1 毫秒  平均:%2 KB/s")
                                 .arg(QString::number(milliseconds))
                                 .arg(QString::number(((double(m_totalFileSize) / 1024) / (double(milliseconds) / 1000)), 'f', 3)));
        m_totalFileSize = 0;
        m_totalFileBytesWritten = 0;

    } else {
        send();
    }
}

到此,客户端已经具备批量发送文件的能力了。

4 总结

理清思路后,用Qt实现文件传输功能还是很简单的。当然如果需要的话,也可以让服务端单独启动线程接收文件,这样客户端就可以多个文件同时发送,服务端多个文件同时接收,这样效率貌似会更高,这算是一个拓展吧,不管怎样理清设计思路才是根本所在。

由于文件传输过程中会进行界面显示处理,性能可能会丢失一部分,如果将本例子程序改为纯后台的,效率应该会高一些。

5 下载

完整代码

posted @ 2020-08-07 16:27  Qt小罗  阅读(5078)  评论(3编辑  收藏  举报