1. 程式人生 > >Qt實現客戶端與伺服器訊息傳送與檔案傳輸

Qt實現客戶端與伺服器訊息傳送與檔案傳輸

客戶端與伺服器之間的資料傳送在很多案例場景裡都會有應用。這裡Jungle用Qt來簡單設計實現一個場景,即:
①兩端:伺服器QtServer和客戶端QtClient
②功能:服務端連線客戶端,兩者能夠互相傳送訊息,傳送檔案,並且顯示檔案傳送進度。
環境:VS2008+Qt4.8.6+Qt設計師

1.基本概念

客戶端與伺服器的基本概念不說了,關於TCP通訊的三次握手等等,在經典教材謝希仁的《計算機網路》裡都有詳細介紹。這裡說下兩者是如何建立起通訊連線的。
①IP地址:首先伺服器和每一個客戶端都有一個地址,即IP地址。(底層的MAC地址,不關心,因為TCP通訊以及IP,是七層架構裡面的網路層、傳輸層了,底層透明)。對於伺服器來說,客戶端的數量及地址是未知的,除非建立了連線。但是對於客戶端來說,必須知道伺服器的地址,因為兩者之間的連線是由客戶端主動發起的。
②埠號:

軟體層面的埠號,指的是“應用層的各種協議程序與運輸實體進行層間互動的一種地址”。簡而言之,每一個TCP連線都是一個程序,作業系統需要為每個程序分配一個協議埠(即每一個客戶端與服務端的連線,不是兩臺主機的連線,而是兩個埠的連線)。但一臺主機通常會有很多服務,很多程序,單靠一個IP地址不能標識某個具體的程序或者連線。所以用埠號來標識訪問的目標伺服器以及伺服器的目標服務型別。埠號也有分類,但這不是本文的重點,詳見教材。
③TCP連線:總的來說,TCP的連線管理分為單個階段:建立連線->資料傳送->連線釋放。在②裡說到,每個TCP連線的是具體IP地址的主機的兩個埠,即TCP連線的兩個端點由IP地址和埠號組成,這即是套接字(socket)
的概念:
套接字socket=IP:埠號
因此,我們要通過建立套接字來建立服務端與客戶端的通訊連線。

2.Qt相關類

QTcpSocket:提供套接字
QTcpServer:提供基於TCP的服務端,看官方文件的解釋如下:
This class makes it possible to accept incoming TCP connections. You can specify the port or have QTcpServer pick one automatically. You can listen on a specific address or on all the machine’s addresses.


這個解釋裡面提到兩點:
①指定埠:即開通哪一個埠用於建立TCP連線;
②監聽:監聽①中指定的埠是否有連線的請求。

3.UI設計

客戶端:
這裡寫圖片描述
服務端:
這裡寫圖片描述

4.客戶端實現

類設計如下:

class QtClient : public QWidget
{
    Q_OBJECT
public:
    QtClient(QWidget *parent = 0, Qt::WFlags flags = 0);
    ~QtClient();

    void initTCP();
    void newConnect();

    private slots:
        ////連線伺服器
        void connectServer();
        ////與伺服器斷開連線
        void disconnectServer();
        ////接收伺服器傳送的資料
        void receiveData(); 
        ////向伺服器傳送資料
        void sendData();

        ////瀏覽檔案
        void selectFile();
        ////傳送檔案
        void sendFile();
        ////更新檔案傳送進度
        void updateFileProgress(qint64);
        ////更新檔案接收進度
        void updateFileProgress();

private:
    Ui::QtClientClass ui;
    QTcpSocket *tcpSocket;
    QTcpSocket *fileSocket;

    ///檔案傳送
    QFile *localFile;
    ///檔案大小
    qint64 totalBytes;      //檔案總位元組數
    qint64 bytesWritten;    //已傳送的位元組數
    qint64 bytestoWrite;    //尚未傳送的位元組數
    qint64 filenameSize;    //檔名字的位元組數
    qint64 bytesReceived;   //接收的位元組數
    ///每次傳送資料大小
    qint64 perDataSize;
    QString filename;
    ///資料緩衝區
    QByteArray inBlock;
    QByteArray outBlock;

    ////系統時間
    QDateTime current_date_time;
    QString str_date_time;
};

類實現如下:

#include "qtclient.h"

QtClient::QtClient(QWidget *parent, Qt::WFlags flags)
    : QWidget(parent, flags)
{
    ui.setupUi(this);
    this->initTCP();

    /////檔案傳送相關變數初始化
    ///每次傳送資料大小為64kb
    perDataSize = 64*1024;
    totalBytes = 0;
    bytestoWrite = 0;
    bytesWritten = 0;
    bytesReceived = 0;
    filenameSize = 0;

    connect(this->ui.pushButton_openFile,SIGNAL(clicked()),this,SLOT(selectFile()));
    connect(this->ui.pushButton_sendFile,SIGNAL(clicked()),this,SLOT(sendFile()));
}

QtClient::~QtClient()
{
}

void QtClient::initTCP()
{
    this->tcpSocket = new QTcpSocket(this);
    connect(ui.pushButton_connect,SIGNAL(clicked()),this,SLOT(connectServer()));
    connect(ui.pushButton_disconnect,SIGNAL(clicked()),this,SLOT(disconnectServer()));
    connect(ui.pushButton_send,SIGNAL(clicked()),this,SLOT(sendData()));
}

void QtClient::connectServer()
{
    tcpSocket->abort();
    tcpSocket->connectToHost("127.0.0.1",6666);
    connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(receiveData()));
}

這裡說明一下兩個方法:
①abort():官方文件給出了說明:
Aborts the current connection and resets the socket. Unlike disconnectFromHost(), this function immediately closes the socket, discarding any pending data in the write buffer.
即終止之前的連線,重置套接字。
②connectToHost():給定IP地址和埠號,連線伺服器。這裡我們給127.0.0.1,即本機地址,埠號隨便給了個,一般來說介於49152~65535之間的都行。

void QtClient::disconnectServer()
{
    //這裡不做實現了,大家自己定義吧O(∩_∩)O哈哈~
}

void QtClient::receiveData()
{
    /////獲取當前時間
    current_date_time = QDateTime::currentDateTime();
    str_date_time = current_date_time.toString("yyyy-MM-dd hh:mm:ss")+"\n";
    ////接收資料
    QString str = tcpSocket->readAll();
    ////顯示
    str = "Server "+str_date_time+str;
    this->ui.textEdit->append(str);
}

void QtClient::sendData()
{
    ////傳送資料
    QString str = ui.lineEdit->text();
    this->tcpSocket->write(ui.lineEdit->text().toLatin1());
    ////顯示
    current_date_time = QDateTime::currentDateTime();
    str_date_time = current_date_time.toString("yyyy-MM-dd hh:mm:ss");
    str = "You "+str_date_time+"\n"+str;
    ui.textEdit->append(str);
}

這裡說明QTCPSocket的兩個方法:
①readAll():如果把一個socket比作一個通訊管道,那麼這個方法的作用是讀取該管道里的所有資料(格式為QByteArray);
②write():同上面的比喻,這個方法的作用是向管道里塞資料。

void QtClient::selectFile()
{
    this->fileSocket = new QTcpSocket(this);
    fileSocket->abort();
    fileSocket->connectToHost("127.0.0.1",8888);
    ////檔案傳送進度更新
    connect(fileSocket,SIGNAL(bytesWritten(qint64)),this,SLOT(updateFileProgress(qint64)));
    connect(fileSocket,SIGNAL(readyRead()),this,SLOT(updateFileProgress()));

    this->ui.progressBar->setValue(0);
    this->filename = QFileDialog::getOpenFileName(this,"Open a file","/","files (*)");
    ui.lineEdit_filename->setText(filename);
}

從上面那段程式碼可以看出,Jungle設計了兩個socket,一個用於傳送字元資料,另一個套接字用於傳送檔案。兩個socket分別使用兩個不同的埠。在服務端裡也是這樣,待會兒不再解釋了。

void QtClient::sendFile()
{
    this->localFile = new QFile(filename);
    if(!localFile->open(QFile::ReadOnly))
    {
        ui.textEdit->append(tr("Client:open file error!"));
        return;
    }
    ///獲取檔案大小
    this->totalBytes = localFile->size();
    QDataStream sendout(&outBlock,QIODevice::WriteOnly);
    sendout.setVersion(QDataStream::Qt_4_8);
    QString currentFileName = filename.right(filename.size()-filename.lastIndexOf('/')-1);

    qDebug()<<sizeof(currentFileName);
    ////保留總代大小資訊空間、檔名大小資訊空間、檔名
    sendout<<qint64(0)<<qint64(0)<<currentFileName;
    totalBytes += outBlock.size();
    sendout.device()->seek(0);
    sendout<<totalBytes<<qint64((outBlock.size()-sizeof(qint64)*2));

    bytestoWrite = totalBytes-fileSocket->write(outBlock);
    outBlock.resize(0);
}

這裡同樣說明兩點:
①setVision():設定資料序列的版本,官方文件裡說明這個不是必須的,但是推薦我們要去進行這一步的工作。我這裡是Qt4.8.6,所以設定為Qt4.8.見下圖(截自Qt官方文件)
這裡寫圖片描述
②qint64:這個型別在Jungle之前的部落格裡也提到過,是指qt的無符號的整型,64位。

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

    ////如果已經發送了資料
    if(bytestoWrite > 0)
    {
        outBlock = localFile->read(qMin(bytestoWrite,perDataSize));
        ///傳送完一次資料後還剩餘資料的大小
        bytestoWrite -= ((int)fileSocket->write(outBlock));
        ///清空傳送緩衝區
        outBlock.resize(0);
    }
    else
        localFile->close();

    ////更新進度條
    this->ui.progressBar->setMaximum(totalBytes);
    this->ui.progressBar->setValue(bytesWritten);

    ////如果傳送完畢
    if(bytesWritten == totalBytes)
    {
        localFile->close();
        //fileSocket->close();
    }
}

void QtClient::updateFileProgress()
{
    QDataStream inFile(this->fileSocket);
    inFile.setVersion(QDataStream::Qt_4_8);

    ///如果接收到的資料小於16個位元組,儲存到來的檔案頭結構
    if(bytesReceived <= sizeof(qint64)*2)
    {
        if((fileSocket->bytesAvailable()>=(sizeof(qint64))*2) && (filenameSize==0))
        {
            inFile>>totalBytes>>filenameSize;
            bytesReceived += sizeof(qint64)*2;
        }
        if((fileSocket->bytesAvailable()>=filenameSize) && (filenameSize != 0))
        {
            inFile>>filename;
            bytesReceived += filenameSize;
            filename = "ServerData/"+filename;
            localFile = new QFile(filename);
            if(!localFile->open(QFile::WriteOnly))
            {
                qDebug()<<"Server::open file error!";
                return;
            }
        }
        else
            return;
    }
    /////如果接收的資料小於總資料,則寫入檔案
    if(bytesReceived < totalBytes)
    {
        bytesReceived += fileSocket->bytesAvailable();
        inBlock = fileSocket->readAll();
        localFile->write(inBlock);
        inBlock.resize(0);
    }

    ////資料接收完成時
    if(bytesReceived == totalBytes)
    {
        this->ui.textEdit->append("Receive file successfully!");
        bytesReceived = 0;
        totalBytes = 0;
        filenameSize = 0;
        localFile->close();
        //fileSocket->close();
    }
}

5.服務端實現

類的設計:

class QtServer : public QWidget
{
    Q_OBJECT

public:
    QtServer(QWidget *parent = 0, Qt::WFlags flags = 0);
    ~QtServer();

    QTcpServer *server;
    QTcpSocket *socket;
    QTcpServer *fileserver;
    QTcpSocket *filesocket;

private slots:  
    void sendMessage(); 
    void acceptConnection();
    ////接收客戶端傳送的資料
    void receiveData();

    void acceptFileConnection();
    void updateFileProgress();
    void displayError(QAbstractSocket::SocketError socketError);

    ///選擇傳送的檔案
    void selectFile();
    void sendFile();
    ////更新檔案傳送進度
    void updateFileProgress(qint64);

private:
    Ui::QtServerClass ui;

    ////傳送檔案相關變數
    qint64 totalBytes;
    qint64 bytesReceived;
    qint64 bytestoWrite;
    qint64 bytesWritten;
    qint64 filenameSize;
    QString filename;
    ///每次傳送資料大小
    qint64 perDataSize;
    QFile *localFile;
    ////本地緩衝區
    QByteArray inBlock;
    QByteArray outBlock;

    ////系統時間
    QDateTime current_date_time;
    QString str_date_time;
};

實現:

#include "qtserver.h"
#include <QDataStream>
#include <QMessageBox>
#include <QString>
#include <QByteArray>

QtServer::QtServer(QWidget *parent, Qt::WFlags flags)
    : QWidget(parent, flags)
{
    ui.setupUi(this);

    this->socket = new QTcpSocket(this);
    this->server = new QTcpServer(this);
    ///開啟監聽
    this->server->listen(QHostAddress::Any,6666);
    connect(this->server,SIGNAL(newConnection()),this,SLOT(acceptConnection()));
    connect(ui.pushButton_send,SIGNAL(clicked()),this,SLOT(sendMessage()));

    ///檔案傳送套接字
    this->filesocket = new QTcpSocket(this);
    this->fileserver = new QTcpServer(this);
    this->fileserver->listen(QHostAddress::Any,8888);
    connect(this->fileserver,SIGNAL(newConnection()),this,SLOT(acceptFileConnection()));

    //// 檔案傳送相關變數初始化
    bytesReceived = 0;
    totalBytes = 0;
    filenameSize = 0;
    connect(ui.pushButton_selectFile,SIGNAL(clicked()),this,SLOT(selectFile()));
    connect(ui.pushButton_sendFile,SIGNAL(clicked()),this,SLOT(sendFile()));
}

QtServer::~QtServer()
{

}

void QtServer::acceptConnection()
{
    ////返回一個socket連線
    this->socket = this->server->nextPendingConnection();
    connect(socket,SIGNAL(readyRead()),this,SLOT(receiveData()));
}

void QtServer::acceptFileConnection()
{
    bytesWritten = 0;
    ///每次傳送資料大小為64kb
    perDataSize = 64*1024;
    this->filesocket = this->fileserver->nextPendingConnection();
    ///接受檔案
    connect(filesocket,SIGNAL(readyRead()),this,SLOT(updateFileProgress()));    
    connect(filesocket,SIGNAL(error(QAbstractSocket::SocketError)),this,SLOT(updateFileProgress(qint64)));
    connect(filesocket,SIGNAL(bytesWritten(qint64)),this,SLOT(displayError(QAbstractSocket::SocketError socketError)));
}

void QtServer::sendMessage()
{
    this->socket->write(ui.lineEdit->text().toLatin1());
    ////顯示
    current_date_time = QDateTime::currentDateTime();
    str_date_time = current_date_time.toString("yyyy-MM-dd hh:mm:ss");
    QString str = "You "+str_date_time+"\n"+ui.lineEdit->text();
    ui.browser->append(str);
}

void QtServer::receiveData()
{
    /////獲取當前時間
    current_date_time = QDateTime::currentDateTime();
    str_date_time = current_date_time.toString("yyyy-MM-dd hh:mm:ss")+"\n";

    ////接收資料
    QString str = this->socket->readAll();

    ////顯示
    str = "Client "+str_date_time+str;
    this->ui.browser->append(str);
}

void QtServer::updateFileProgress()
{
    QDataStream inFile(this->filesocket);
    inFile.setVersion(QDataStream::Qt_4_8);

    ///如果接收到的資料小於16個位元組,儲存到來的檔案頭結構
    if(bytesReceived <= sizeof(qint64)*2)
    {
        if((filesocket->bytesAvailable()>=(sizeof(qint64))*2) && (filenameSize==0))
        {
            inFile>>totalBytes>>filenameSize;
            bytesReceived += sizeof(qint64)*2;
        }
        if((filesocket->bytesAvailable()>=filenameSize) && (filenameSize != 0))
        {
            inFile>>filename;
            bytesReceived += filenameSize;
            ////接收的檔案放在指定目錄下
            filename = "ClientData/"+filename;
            localFile = new QFile(filename);
            if(!localFile->open(QFile::WriteOnly))
            {
                qDebug()<<"Server::open file error!";
                return;
            }
        }
        else
            return;
    }
    /////如果接收的資料小於總資料,則寫入檔案
    if(bytesReceived < totalBytes)
    {
        bytesReceived += filesocket->bytesAvailable();
        inBlock = filesocket->readAll();
        localFile->write(inBlock);
        inBlock.resize(0);
    }
    ////更新進度條顯示
    this->ui.progressBar_fileProgress->setMaximum(totalBytes);
    this->ui.progressBar_fileProgress->setValue(bytesReceived);
    ////資料接收完成時
    if(bytesReceived == totalBytes)
    {
        this->ui.browser->append("Receive file successfully!");
        bytesReceived = 0;
        totalBytes = 0;
        filenameSize = 0;
        localFile->close();
        //filesocket->close();
    }
}

void QtServer::displayError(QAbstractSocket::SocketError socketError)
{
    qDebug()<<socket->errorString();
    socket->close();
}

void QtServer::selectFile()
{
    filesocket->open(QIODevice::WriteOnly);
    ////檔案傳送進度更新
    connect(filesocket,SIGNAL(bytesWritten(qint64)),this,SLOT(updateFileProgress(qint64)));

    this->filename = QFileDialog::getOpenFileName(this,"Open a file","/","files (*)");
    ui.lineEdit_fileName->setText(filename);
}

void QtServer::sendFile()
{
    this->localFile = new QFile(filename);
    if(!localFile->open(QFile::ReadOnly))
    {
        return;
    }
    ///獲取檔案大小
    this->totalBytes = localFile->size();
    QDataStream sendout(&outBlock,QIODevice::WriteOnly);
    sendout.setVersion(QDataStream::Qt_4_8);
    QString currentFileName = filename.right(filename.size()-filename.lastIndexOf('/')-1);

    ////保留總代大小資訊空間、檔名大小資訊空間、檔名
    sendout<<qint64(0)<<qint64(0)<<currentFileName;
    totalBytes += outBlock.size();
    sendout.device()->seek(0);
    sendout<<totalBytes<<qint64((outBlock.size()-sizeof(qint64)*2));

    bytestoWrite = totalBytes-filesocket->write(outBlock);
    outBlock.resize(0);
}

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

    ////如果已經發送了資料
    if(bytestoWrite > 0)
    {
        outBlock = localFile->read(qMin(bytestoWrite,perDataSize));
        ///傳送完一次資料後還剩餘資料的大小
        bytestoWrite -= ((int)filesocket->write(outBlock));
        ///清空傳送緩衝區
        outBlock.resize(0);
    }
    else
        localFile->close();

    ////如果傳送完畢
    if(bytesWritten == totalBytes)
    {
        localFile->close();
        //filesocket->close();
    }
}

6.測試

這裡寫圖片描述
這裡寫圖片描述
這裡傳送了幾條訊息,並從客戶端將《Windows網路程式設計技術.pdf》傳到服務端,在服務端的ClientData資料夾裡,該檔案存在,證明程式可行!