1. 程式人生 > >學習筆記:QT網路程式設計:C2S基於TCP的檔案傳輸

學習筆記:QT網路程式設計:C2S基於TCP的檔案傳輸



預處理: 1在.pro加入一條語句 QT += network 記得儲存檔案 2.標頭檔案中可包含標頭檔案/儘量用前向宣告(因為只宣告不用) TCP檔案傳輸
傳送時是先寫入固定的緩衝區,再用資料包作為載體傳送; 而讀取時接收的是資料包,要先對接收到的資料進行判斷,檔案頭提取出來建立本地檔案,檔案內容才存入緩衝區內寫入建立的檔案。
每次傳輸的資料大小是由傳送端決定的
1.訊號SIGNAL也是函式!
:封裝了對特定訊號的監聽方法和特定返回值。 2.檔名、檔案大小也要作為資訊傳遞給SERVER,但要預處理去掉路徑有關的符號 3.%1 是Qt特有的佔位符後面用.arg(字串變數)可以在該位置顯示自定義變數內容

客戶端: (1)類的處理 私有成員:     QTcpSocket * tcpClient ; //不像伺服器類,並沒有特定的CLIENT類    
QFile * localFile ;      // 要傳送的本地檔案     qint64 totalBytes ;     // 傳送資料的總大小     qint64 bytesWritten ;   // 已經發送資料大小    
qint64 bytesToWrite ;   // 剩餘資料大小     qint64 payloadSize ;    // 每次傳送資料的大小     QString fileName ;      // 儲存檔案路徑     QByteArray outBlock ;   // 資料緩衝區 ,即存放每次要傳送的資料塊   (2)建構函式: 初始化,繫結所涉及訊號和槽 (3)事件迴圈:      1.按下開啟按鈕 得到檔案對話方塊返回的檔名 2.按下發送按鈕 發起一個連線請求 3.連線成功:先發送檔案頭,再發送檔案內容 1.根據檔名開啟QFILE, 2.宣告一個臨時資料包變數,將其和緩衝區繫結,繫結以後對 資料包的資料操作就是對緩衝區操作 3.將QFILE內的 檔案頭打包成資料包, 加上描述本次資料傳輸的關鍵資料 如檔案總大小,檔名以後再發送 要注意的: * 資料包要嚴格按 檔案總大小-檔名大小-檔名 順序在對應位置填入資料,因此在不知道某些資訊(如總大小),要在對應位置根據標準大小為其佔位 *檔名要從讀取的檔案路徑提取出來:如下 QString currentFileName = fileName. right( fileName. size()                                              - fileName. lastIndexOf( '/')- 1);               4 寫入資料

伺服器端:單執行緒接收來自客戶端的檔案資料   (1)類的處理 私有成員:    QTcpServer tcpServer ;//專用的伺服器類     QTcpSocket * tcpServerConnection ;// 一次只支援一個連線 (多個連線涉及多執行緒)     qint64 totalBytes ;      // 存放總大小資訊     qint64 bytesReceived ;   // 已收到資料的大小     qint64 fileNameSize ;    // 檔名的大小資訊     QString fileName ;       // 存放檔名     QFile * localFile ;       // 本地檔案     QByteArray inBlock ;     // 資料緩衝區 ui介面: “監聽”按鈕 進度條 (2)  建構函式: 繫結“新連線請求訊號”,“連線處理槽函式”   (3)事件迴圈 1.按下監聽按鈕 開始監聽“連線請求訊號”、初始化各種資料以及reset進度條
    totalBytes = 0;//總的待接收檔案位元組數   bytesReceived = 0; //已經接收的位元組數    fileNameSize = 0;  //檔名大小 用於判斷當前接收資料包是檔案頭還是檔案內容     2. 新連線請求訊號: 一次檔案傳輸 一個訊號 1.繫結“準備好開始讀取”和“讀取過程處理”、“連接出錯”的訊號和槽
    connect( tcpServerConnection, SIGNAL(readyRead()),//             this, SLOT(updateServerProgress()));          對比客戶端的:        connect( tcpClient, SIGNAL( bytesWritten( qint64)),  //             this, SLOT(updateClientProgress( qint64)));
2.關閉伺服器 不再進行監聽        3.準備好開始讀取: 一次檔案傳輸 多個訊號 1.宣告一個臨時資料包來接收收到的資料包,繫結其和私有成員中的套接字,並設定版本號 若不設定,則無法正常讀取 2.拆包處理:按資料包傳送格式一個個拆取 若收到的資料是檔案頭,則需要用 臨時的資料包接收本次資料, 提取檔名併為其建立檔案,記錄待接收檔案總的大小 若已經開始接受檔案資料,則將 資料存入緩衝區追加到已建立的檔案末尾,已接收的資料與待接收總大小比較,判斷是否已經完成傳輸 3.重新整理記錄檔案傳輸過程的變數 4.重新整理進度條進度 若還在傳輸:    ui-> serverProgressBar-> setMaximum( totalBytes);    ui-> serverProgressBar-> setValue( bytesReceived); 若傳輸完成:        ui-> serverStatusLabel-> setText( tr( "接收檔案 %1 成功!")                                        . arg( fileName));      // %1 用來為檔名佔位 4. 連接出錯:


程式碼實現: 伺服器端
#ifndef SERVER_H
#define SERVER_H

#include <QDialog>
#include <QAbstractSocket>
#include <QTcpServer>
class QTcpSocket;
class QFile;

namespace Ui {
class Server;
}

class Server : public QDialog
{
    Q_OBJECT

public:
    explicit Server(QWidget *parent = 0);
    ~Server();

private:
    Ui::Server *ui;

    QTcpServer tcpServer;
    QTcpSocket *tcpServerConnection;
    qint64 totalBytes;     // 存放總大小資訊
    qint64 bytesReceived;  // 已收到資料的大小
    qint64 fileNameSize;   // 檔名的大小資訊
    QString fileName;      // 存放檔名
    QFile *localFile;      // 本地檔案
    QByteArray inBlock;    // 資料緩衝區

private slots:
    void start();
    void acceptConnection();
    void updateServerProgress();
    void displayError(QAbstractSocket::SocketError socketError);
    //
    void on_startButton_clicked();
};

#endif // SERVER_H
#include "server.h"#include "ui_server.h"#include <QtNetwork>Server::Server(QWidget *parent) : QDialog(parent), ui(new Ui::Server){ ui->setupUi(this); connect(&tcpServer, SIGNAL(newConnection()), this, SLOT(acceptConnection()));}Server::~Server(){ delete ui;}void Server::start(){ if (!tcpServer.listen(QHostAddress::LocalHost, 6666)) { qDebug() << tcpServer.errorString(); close(); return; } //判斷是否正常開啟監聽 ui->startButton->setEnabled(false); totalBytes = 0; bytesReceived = 0; fileNameSize = 0; ui->serverStatusLabel->setText(tr("監聽")); ui->serverProgressBar->reset();}void Server::acceptConnection(){ tcpServerConnection = tcpServer.nextPendingConnection(); connect(tcpServerConnection, SIGNAL(readyRead()), this, SLOT(updateServerProgress())); connect(tcpServerConnection, SIGNAL(error(QAbstractSocket::SocketError)), this, SLOT(displayError(QAbstractSocket::SocketError))); ui->serverStatusLabel->setText(tr("接受連線")); // 關閉伺服器,不再進行監聽 tcpServer.close();}void Server::updateServerProgress(){ QDataStream in(tcpServerConnection); in.setVersion(QDataStream::Qt_4_0); // 如果接收到的資料小於16個位元組,儲存到來的檔案頭結構 if (bytesReceived <= sizeof(qint64)*2) { if((tcpServerConnection->bytesAvailable() >= sizeof(qint64)*2) && (fileNameSize == 0)) { // 接收資料總大小資訊和檔名大小資訊 in >> totalBytes >> fileNameSize; bytesReceived += sizeof(qint64) * 2; } if((tcpServerConnection->bytesAvailable() >= fileNameSize)// && (fileNameSize != 0)) { // 接收檔名,並建立檔案 in >> fileName; ui->serverStatusLabel->setText(tr("接收檔案 %1 …") .arg(fileName)); bytesReceived += fileNameSize; localFile = new QFile(fileName); if (!localFile->open(QFile::WriteOnly)) { qDebug() << "server: open file error!"; return; } } else { return; } } // 如果接收的資料小於總資料,那麼寫入檔案 if (bytesReceived < totalBytes) { bytesReceived += tcpServerConnection->bytesAvailable(); inBlock = tcpServerConnection->readAll(); localFile->write(inBlock); inBlock.resize(0); } ui->serverProgressBar->setMaximum(totalBytes); ui->serverProgressBar->setValue(bytesReceived); // 接收資料完成時 if (bytesReceived == totalBytes) { tcpServerConnection->close(); localFile->close(); ui->startButton->setEnabled(true); ui->serverStatusLabel->setText(tr("接收檔案 %1 成功!") .arg(fileName)); }}void Server::displayError(QAbstractSocket::SocketError socketError){ qDebug() << tcpServerConnection->errorString(); tcpServerConnection->close(); ui->serverProgressBar->reset(); ui->serverStatusLabel->setText(tr("服務端就緒")); ui->startButton->setEnabled(true);}// 開始監聽按鈕void Server::on_startButton_clicked(){ start();}

客戶端接收一次性資料的大概流程: 類的處理: 1.私有成員加入一個用於接收的套接字(生命週期最好和主介面相同) 2 ui介面建立兩個input label用於接收使用者輸入想連線host的ip和埠號 建構函式: 1.繫結“傳送按鈕點選訊號”和“傳送的資料槽函式” 事件迴圈: 點擊發送按鈕: 1.abort之前的連線並請求一個新連線 2.繫結 “連線成功訊號”以及槽函式 連線成功: 1.輸出資訊到控制檯,表明連線成功觸發了這個函式

#ifndef CLIENT_H
#define CLIENT_H

#include <QDialog>
#include <QAbstractSocket>
class QTcpSocket;
class QFile;

namespace Ui {
class Client;
}

class Client : public QDialog
{
    Q_OBJECT

public:
    explicit Client(QWidget *parent = 0);
    ~Client();

private:
    Ui::Client *ui;

    QTcpSocket *tcpClient;
    QFile *localFile;     // 要傳送的檔案
    qint64 totalBytes;    // 傳送資料的總大小
    qint64 bytesWritten;  // 已經發送資料大小
    qint64 bytesToWrite;  // 剩餘資料大小
    qint64 payloadSize;   // 每次傳送資料的大小
    QString fileName;     // 儲存檔案路徑
    QByteArray outBlock;  // 資料緩衝區,即存放每次要傳送的資料塊

private slots:
    void openFile();
    void send();
    void startTransfer();
    void updateClientProgress(qint64);
    void displayError(QAbstractSocket::SocketError);


    void on_openButton_clicked();
    void on_sendButton_clicked();
};

#endif // CLIENT_H

#include "client.h"
#include "ui_client.h"
#include <QtNetwork>
#include <QFileDialog>

Client::Client(QWidget *parent) :
    QDialog(parent),
    ui(new Ui::Client)
{
    ui->setupUi(this);

    payloadSize = 64*1024; // 64KB
    totalBytes = 0;
    bytesWritten = 0;
    bytesToWrite = 0;
    tcpClient = new QTcpSocket(this);

    // 當連線伺服器成功時,發出connected()訊號,開始傳送檔案
    connect(tcpClient, SIGNAL(connected()), this, SLOT(startTransfer()));//
    connect(tcpClient, SIGNAL(bytesWritten(qint64)),          //
            this, SLOT(updateClientProgress(qint64)));
    connect(tcpClient, SIGNAL(error(QAbstractSocket::SocketError)),
            this, SLOT(displayError(QAbstractSocket::SocketError)));
    ui->sendButton->setEnabled(false);
}

Client::~Client()
{
    delete ui;
}

void Client::openFile()
{
    fileName = QFileDialog::getOpenFileName(this);
    if (!fileName.isEmpty()) {
        ui->sendButton->setEnabled(true);
        ui->clientStatusLabel->setText(tr("開啟檔案 %1 成功!").arg(fileName));
    }
}

void Client::send()
{
    ui->sendButton->setEnabled(false);

    // 初始化已傳送位元組為0
    bytesWritten = 0;
    ui->clientStatusLabel->setText(tr("連線中…"));
    tcpClient->connectToHost(ui->hostLineEdit->text(),
                             ui->portLineEdit->text().toInt());
}


void Client::startTransfer()
{
    localFile = new QFile(fileName);
    if (!localFile->open(QFile::ReadOnly)) {
        qDebug() << "client: open file error!";
        return;
    }
    // 獲取檔案大小
    totalBytes = localFile->size();

    QDataStream sendOut(&outBlock, QIODevice::WriteOnly);
    sendOut.setVersion(QDataStream::Qt_4_0);
    QString currentFileName = fileName.right(fileName.size()
                                             - fileName.lastIndexOf('/')-1);//
    // 保留總大小資訊空間、檔名大小資訊空間,然後輸入檔名
    sendOut << qint64(0) << qint64(0) << currentFileName;

    // 這裡的總大小是總大小資訊、檔名大小資訊、檔名和實際檔案大小的總和
    totalBytes += outBlock.size(); //
    sendOut.device()->seek(0);

    // 返回outBolock的開始,用實際的大小資訊代替兩個qint64(0)空間
    sendOut << totalBytes << qint64((outBlock.size() - sizeof(qint64)*2));

    // 傳送完檔案頭結構後剩餘資料的大小
    bytesToWrite = totalBytes - tcpClient->write(outBlock);

    ui->clientStatusLabel->setText(tr("已連線"));
    outBlock.resize(0);
}

void Client::updateClientProgress(qint64 numBytes)
{
    // 已經發送資料的大小
    bytesWritten += (int)numBytes;

    // 如果已經發送了資料
    if (bytesToWrite > 0) {
        // 每次傳送payloadSize大小的資料,這裡設定為64KB,如果剩餘的資料不足64KB,
        // 就傳送剩餘資料的大小
        outBlock = localFile->read(qMin(bytesToWrite, payloadSize));

        // 傳送完一次資料後還剩餘資料的大小
        bytesToWrite -= (int)tcpClient->write(outBlock);

        // 清空傳送緩衝區
        outBlock.resize(0);
    } else { // 如果沒有傳送任何資料,則關閉檔案
        localFile->close();
    }
    // 更新進度條
    ui->clientProgressBar->setMaximum(totalBytes);
    ui->clientProgressBar->setValue(bytesWritten);
    // 如果傳送完畢
    if(bytesWritten == totalBytes) {
        ui->clientStatusLabel->setText(tr("傳送檔案 %1 成功").arg(fileName));
        localFile->close();
        tcpClient->close();
    }
}

void Client::displayError(QAbstractSocket::SocketError)
{
    qDebug() << tcpClient->errorString();
    tcpClient->close();
    ui->clientProgressBar->reset();
    ui->clientStatusLabel->setText(tr("客戶端就緒"));
    ui->sendButton->setEnabled(true);
}


// 開啟按鈕
void Client::on_openButton_clicked()
{
    ui->clientProgressBar->reset();
    ui->clientStatusLabel->setText(tr("狀態:等待開啟檔案!"));
    openFile();

}

// 傳送按鈕
void Client::on_sendButton_clicked()
{
    send();
}