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()
{
    if (0 == m_fileSize && m_pSocket->bytesAvailable() > sizeof(qint64)) {
        m_inStream >> m_fileSize >> m_fileName;
        m_file.setFileName(m_fileName);
        if (!m_file.open(QIODevice::WriteOnly)) {
            qCritical() << m_file.errorString();
            return;
        }
        ui->plainTextEditLog->appendPlainText(QStringLiteral("正在接收【%1】 ...").arg(m_fileName));
    }

    qint64 size = qMin(m_pSocket->bytesAvailable(), m_fileSize - m_fileBytesReceived);

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

    m_fileBytesReceived += size;

    if (m_fileBytesReceived == m_fileSize) {
        m_file.close();
        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狀態改變發出信號,對應的槽函數內將自動發送文件,相關代碼如下:
void MainWindow::onSocketStateChanged(QAbstractSocket::SocketState state)
{
    switch (state) {
    case QAbstractSocket::UnconnectedState:
        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.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);
}

客戶端每次發送數據後,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()
{
    m_file.close();
    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 下載

完整代碼

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章