1. 程式人生 > >Qt5 基於TCP傳輸的傳送/接收檔案伺服器(支援多客戶端)

Qt5 基於TCP傳輸的傳送/接收檔案伺服器(支援多客戶端)

一、實現功能
1、伺服器端選擇待發送的檔案,可以是多個
2、開啟伺服器,支援多客戶端接入,能夠實時顯示每個客戶端接入狀態
3、等待所有客戶端都處於已連線狀態時,依次傳送檔案集給每個客戶端,顯示每個客戶端傳送進度
4、傳送完成後等待接收客戶端發回的檔案,顯示接收進度
5、關閉伺服器


二、實現要點
先講一下實現上述功能的幾個關鍵點,明白的這幾個要點,功能的大框架就搭好了,細節在下一節再講


1、新建伺服器類testServer,繼承自QTcpServer
功能:用於接收客戶端TCP請求,儲存所有客戶端資訊,向主視窗傳送資訊
在這個類中例項化QTcpServer的虛擬函式:
void incomingConnection(int socketDescriptor); //虛擬函式,有tcp請求時會觸發
 
引數為描述socket ID的int變數
此函式在QTcpServer檢測到外來TCP請求時,會自動呼叫。

注:若要接收區域網內的TCP請求,連線newConnection訊號到自定義槽函式
connect(tcpServer,SIGNAL(newConnection()),this,SLOT(acceptConnection()));
但是這種方法無法處理多客戶端


2、新建客戶端類testClient,繼承自QTcpSocket
功能:儲存每個接入的客戶端資訊,控制傳送/接收檔案,向testServer傳送資訊

在testServer的incomingConnection函式中,通過設定socketDescriptor,初始化一個testClient例項

testClient *socket = new testClient(this);

if (!socket->setSocketDescriptor(socketDescriptor))
{
    emit error(socket->error());
    return;
}

3、傳送資料
例項化testClient類,通過void writeData(const char * data, qint64 size)函式將資料寫入TCP流
testClient *a;
a->writeData("message",8);

一般來說,使用QByteArray和QDataStream進行格式化的資料寫入,設定流化資料格式型別為QDataStream::Qt_5_0,與客戶端一致
testClient *a;
QByteArray outBlock;       //快取一次傳送的資料
QDataStream sendOut(&outBlock, QIODevice::WriteOnly);	//資料流
sendOut.setVersion(QDataStream::Qt_5_0);
sendOut << "message";

a->writeData(outBlock,outBlock.size());

outBlock.resize(0);

注意outBlock如果為全域性變數,最後需要resize,否則下一次寫入時會出錯


在testClient建構函式中連線資料成功傳送後產生的bytesWritten()訊號
connect(this, SIGNAL(bytesWritten(qint64)), this, SLOT(updateClientProgress(qint64)));

引數為成功寫入TCP流的位元組數
通過捕獲這個訊號,可以實現資料的連續傳送。因為傳送檔案時,資料是一塊一塊傳送的,比如設定一塊的大小為20位元組,那麼傳送完20位元組,怎麼繼續傳送呢?就靠的是這個函式,再用全域性變數記錄下總共傳送的位元組數,便可以控制傳送的結束。


4、傳送檔案
先把檔案讀到QFile變數中,再通過QBytesArray傳送
testClient *a;
QFile sendFile = new QFile(sendFilePath);//讀取傳送檔案路徑
if (!sendFile->open(QFile::ReadOnly ))  //讀取傳送檔案
{    return;}
QByteArray outBlock;
outBlock = sendFile->read(sendFile.size());
a->writeData(outBlock,outBlock.size());

5、接收資料
依舊是testClient類,連線有readyRead()訊號,readyRead()訊號為新連線中有可讀資料時發出

connect(this, SIGNAL(readyRead()), this, SLOT(recvData()));

首先將testClient連線例項(如testClient *a)封裝到QDataStream變數中,設定流化資料格式型別為QDataStream::Qt_5_0,與客戶端一致。
在讀入資料前,用a->bytesAvailable()檢測資料流中可讀入的位元組數,然後就可以通過"<<"操作符讀入資料了。
QDataStream in(a); //本地資料流
in.setVersion(QDataStream::Qt_5_0); //設定流版本,以防資料格式錯誤
quint8 receiveClass;
if(this->bytesAvailable() >= (sizeof(quint8)))
{    
    in >> receiveClass;
}

6、接收檔案
使用readAll讀入TCP流中所有資料,再寫入到QFile變數中
QByteArray inBlock;
inBlock = a->readAll();	//讀入所有資料
QFile receivedFile = new QFile(receiveFilePath);	//開啟接收檔案
if (!receivedFile->open(QFile::WriteOnly ))
{    return;}
QFile receivedFile->write(inBlock);	//寫入檔案
inBlock.resize(0);


三、具體實現
1、全域性定義
需要定義一些全域性變數和常量,定義如下

//儲存檔案路徑和檔名的結構體
struct openFileStruct
{
    QString filePath;
    QString fileName;
};

struct clientInfo   //客戶端資訊
{
    QString ip;     //ip
    int state;      //狀態
    QString id;     //id
};

const quint8 sendtype_file = 0;    //傳送型別是檔案
const quint8 sendtype_start_test = 10;    //傳送型別是開始測試
const quint8 sendtype_msg = 20;    //傳送型別是訊息

const quint16 filetype_list = 0;    //檔案型別為列表
const quint16 filetype_wavfile = 1;    //檔案型別為wav檔案

const QString clientStatus[7] =    //客戶端狀態
    {QObject::tr("Unconnected"),QObject::tr("HostLookup"),QObject::tr("Connecting"),
        QObject::tr("Connected"),QObject::tr("Bound"),QObject::tr("Listening"),QObject::tr("Closing")};
openFileStruct 用於主視窗,儲存傳送檔案的路徑和檔名
clientInfo 用於主視窗,儲存每個客戶端的資訊
規定傳輸協議為先發送一個quint8的標誌位,規定傳輸型別,即為sendtype定義的部分
sendtype為0時,傳送檔案。filetype為檔案型別
傳送檔案協議為:傳送型別 | 資料總大小 | 檔案型別 | 檔名大小 | 檔名 | 檔案內容
傳送檔案時,先發送一個檔案列表,每行一個檔名,再逐個傳送檔案
clientStatus為客戶端狀態,與Qt內部的socket status定義相同

2、testClient類
一個testClient類例項為一個socket客戶端描述
(1)類宣告:

class testClient : public QTcpSocket
{
    Q_OBJECT
public:
    testClient(QObject *parent = 0);
    int num;    //客戶端序號

    void prepareTransfer(std::vector<openFileStruct>& openFileList,int testtype_t);    //準備傳輸檔案

signals:
    void newClient(QString, int, int);  //客戶端資訊變更訊號
    void outputlog(QString);    //輸出日誌資訊
    void updateProgressSignal(qint64,qint64,int);   //更新發送進度訊號
    void newClientInfo(QString,int,int);  //更新客戶端資訊

private slots:
    void recvData();    //接收資料
    void clientConnected();     //已連線
    void clientDisconnected();  //斷開連線
    void newState(QAbstractSocket::SocketState);    //新狀態
    void startTransfer(const quint16);    //開始傳輸檔案
    void updateClientProgress(qint64 numBytes);  //傳送檔案內容
    void getSendFileList(QString path);     //在指定路徑生成傳送檔案列表
    quint64 getSendTotalSize();    //獲取sendFilePath對應檔案加上頭的大小

private:
    //傳送檔案所需變數
    qint64  loadSize;          //每次接收的資料塊大小

    qint64  TotalSendBytes;        //總共需傳送的位元組數
    qint64  bytesWritten;      //已傳送位元組數
    qint64  bytesToWrite;      //待發送位元組數
    QString sendFileName;          //待發送的檔案的檔名
    QString sendFilePath;          //待發送的檔案的檔案路徑
    QFile *sendFile;          //待發送的檔案
    QByteArray outBlock;       //快取一次傳送的資料
    int sendNum;        //記錄當前傳送到第幾個檔案
    int sendFileNum;    //記錄傳送檔案個數
    int totalSendSize;  //記錄傳送檔案總大小
    quint64 proBarMax;  //傳送進度條最大值
    quint64 proBarValue;    //進度條當前值

    std::vector<openFileStruct> openList;   //傳送檔案列表

    //接收檔案用變數
    quint8 receiveClass;        //0:檔案,1:開始測試,20:客戶端接收到檔案反饋
    quint16 fileClass;          //待接收檔案型別
    qint64 TotalRecvBytes;          //總共需接收的位元組數
    qint64 bytesReceived;       //已接收位元組數
    qint64 fileNameSize;        //待接收檔名位元組數
    QString receivedFileName;   //待接收檔案的檔名
    QFile *receivedFile;        //待接收檔案
    QByteArray inBlock;         //接收臨時儲存塊
    qint64 totalBytesReceived;  //接收的總大小
    qint32 recvNum;       //現在接收的是第幾個
    quint64 recvFileNum;         //接收檔案個數
    quint64 totalRecvSize;    //接收檔案總大小
    bool isReceived[3];     //是否收到客戶端的接收檔案反饋
    
};

public為需要上層訪問的公有變數和函式
signals為向上層傳送的訊號,保證主視窗實時顯示資訊
private slots包括底層實現傳送、接收訊息的函式,以及訊號處理的槽
private為私有變數,包括髮送檔案和接收檔案所需的變數

(2)類定義:
建構函式:

testClient::testClient(QObject *parent) :
    QTcpSocket(parent)
{
    //傳送變數初始化
    num = -1;
    loadSize = 100*1024;
    openList.clear();

    //接收變數初始化
    TotalRecvBytes = 0;
    bytesReceived = 0;
    fileNameSize = 0;
    recvFileNum = 0;
    totalRecvSize = 0;
    totalBytesReceived = 0;
    recvNum = 0;
    receiveClass = 255;
    fileClass = 0;
    for(int i=0; i<3; i++)
        isReceived[i] = false;

    //連線訊號和槽
    connect(this, SIGNAL(readyRead()), this, SLOT(recvData()));
    connect(this, SIGNAL(disconnected()), this, SLOT(clientDisconnected()));
    connect(this, SIGNAL(stateChanged(QAbstractSocket::SocketState)),
            this, SLOT(newState(QAbstractSocket::SocketState)));
    connect(this, SIGNAL(bytesWritten(qint64)), this, SLOT(updateClientProgress(qint64)));
}

初始化變數
連線qt的tcp訊號和自定義槽,包括:
readyRead() 接收訊息訊號
disconnected() 斷開連線訊號
stateChanged(QAbstractSocket::SocketState) 連線狀態變更訊號
bytesWritten(qint64) 已寫入傳送訊息流訊號
 


接收資料槽,設定伺服器只接收一個檔案:

void testClient::recvData()     //接收資料,伺服器只接收客戶端的一個結果檔案
{
    QDataStream in(this); //本地資料流
    in.setVersion(QDataStream::Qt_5_0); //設定流版本,以防資料格式錯誤

    QString unit;
    qint32 msg;

    if(bytesReceived <= (sizeof(quint8)))
    {
        if(this->bytesAvailable() >= (sizeof(quint8)))
        {
            in >> receiveClass;
        }
        switch(receiveClass)
        {
        case sendtype_file:     //接收檔案
            bytesReceived += sizeof(quint8);
            qDebug() << "bytesReceived: " << bytesReceived;
            break;

        case sendtype_msg:
            in >> msg;
		
            if(msg == 0)	//接收檔案列表
            {
                emit outputlog(tr("client %1 have received list file")
                               .arg(this->peerAddress().toString()));

            }
            else if(msg == 1)   //接收檔案
            {
                emit outputlog(tr("client %1 have received file(s)")
                               .arg(this->peerAddress().toString()));

            }
            return;

        default:
            return;
        }
    }

    if(bytesReceived >= (sizeof(quint8)) && bytesReceived <= (sizeof(quint8) + sizeof(qint64)*2 + sizeof(quint16)))   //開始接收檔案,先接受報頭
    {
        //收3個int型資料,分別儲存總長度、檔案型別和檔名長度
        if( ( this->bytesAvailable() >= (sizeof(qint64)*2 + sizeof(quint16)) ) && (fileNameSize == 0) )
        {
            in >> TotalRecvBytes >> fileClass >> fileNameSize;

            bytesReceived += sizeof(qint64)*2;  //收到多少位元組
            bytesReceived += sizeof(quint16);

            if(fileClass == filetype_result)
            {
                recvNum = 1;
                recvFileNum = 1;
                totalRecvSize = TotalRecvBytes;  //只有一個檔案,檔案總大小為該檔案傳送大小
                totalBytesReceived += sizeof(qint64)*2;
                totalBytesReceived += sizeof(quint16);
                totalBytesReceived += sizeof(quint8);

                emit newClientInfo(tr("receiving result"),num,4);
            }
            else
            {
                QMessageBox::warning(NULL,tr("WARNING"),tr("client %1 send wrong type of file to server")
                                     .arg(this->peerAddress().toString()));
                return;
            }

        }
        //接著收檔名並建立檔案
        if((this->bytesAvailable() >= fileNameSize)&&(fileNameSize != 0))
        {
            in >> receivedFileName;
            bytesReceived += fileNameSize;

            totalBytesReceived += fileNameSize;

            QString receiveFilePath = receive_path + "/" + receivedFileName;    //接收檔案路徑

            emit outputlog(tr("receive from client %1\nsave as:%2")
                           .arg(this->peerAddress().toString())
                           .arg(receiveFilePath));

            //建立檔案
            receivedFile = new QFile(receiveFilePath);

            if (!receivedFile->open(QFile::WriteOnly ))
            {
                QMessageBox::warning(NULL, tr("WARNING"),
                                     tr("cannot open file %1:\n%2.").arg(receivedFileName).arg(receivedFile->errorString()));
                return;
            }
        }
        else
        {
            return;
        }
    }

    //一個檔案沒有接受完
    if (bytesReceived < TotalRecvBytes)
    {
        //可用內容比整個檔案長度-已接收長度短,則全部接收並寫入檔案
        qint64 tmp_Abailable = this->bytesAvailable();
        if(tmp_Abailable <= (TotalRecvBytes - bytesReceived))
        {
            bytesReceived += tmp_Abailable;
            totalBytesReceived += tmp_Abailable;
            inBlock = this->readAll();
            receivedFile->write(inBlock);
            inBlock.resize(0);
            tmp_Abailable = 0;
        }
        //可用內容比整個檔案長度-已接收長度長,則接收所需內容,並寫入檔案
        else
        {
            inBlock = this->read(TotalRecvBytes - bytesReceived);

            if(fileClass == filetype_wavfile)
            {
                totalBytesReceived += (TotalRecvBytes - bytesReceived);
            }
            bytesReceived = TotalRecvBytes;
            receivedFile->write(inBlock);
            inBlock.resize(0);
            tmp_Abailable = 0;
        }
    }

    emit updateProgressSignal(totalBytesReceived,totalRecvSize,num);   //更新發送進度訊號


    //善後:一個檔案全部收完則重置變數關閉檔案流,刪除指標
    if (bytesReceived == TotalRecvBytes)
    {
        //變數重置
        TotalRecvBytes = 0;
        bytesReceived = 0;
        fileNameSize = 0;
        receiveClass = 255;
        receivedFile->close();
        delete receivedFile;

        //輸出資訊
        emit outputlog(tr("Have received file: %1 from client %2")
                       .arg(receivedFileName)
                       .arg(this->peerAddress().toString()));   //log information

        //全部檔案接收完成
        if(recvNum == recvFileNum)
        {
            //變數重置
            recvFileNum = 0;
            recvNum = 0;
            totalBytesReceived = 0;
            totalRecvSize = 0;

            emit outputlog(tr("Receive all done!"));
        }

        if(fileClass == filetype_result)
        {
            emit newClientInfo(tr("Evaluating"),num,4);
        }
    }
}
接收檔案時,需要一步一步判斷接收位元組是否大於協議的下一項,若大於則再判斷其值
接收的檔案型別必須是filetype_result
未接收完記錄接收進度
接收完檔案進行善後,關閉檔案流刪除指標等
每進行完一步,向上層傳送訊號,包括客戶端資訊和接收進度
 


更新客戶端狀態函式,向上層傳送訊號

void testClient::newState(QAbstractSocket::SocketState state)    //新狀態
{
    emit newClient(this->peerAddress().toString(), (int)state, num);
}
傳送的訊號引數為:該客戶端IP,狀態序號,客戶端編號
 


開始傳輸檔案函式(傳送包含檔案資訊的檔案頭)

void testClient::startTransfer(const quint16 type)    //開始傳輸檔案
{
    TotalSendBytes = 0;    //總共需傳送的位元組數
    bytesWritten = 0;       //已傳送位元組數
    bytesToWrite = 0;       //待發送位元組數

    //開始傳輸檔案訊號
    emit outputlog(tr("start sending file to client: %1\n filename: %2")
                   .arg(this->peerAddress().toString())
                   .arg(sendFileName));

    sendFile = new QFile(sendFilePath);
    if (!sendFile->open(QFile::ReadOnly ))  //讀取傳送檔案
    {
        QMessageBox::warning(NULL, tr("WARNING"),
                             tr("can not read file %1:\n%2.")
                             .arg(sendFilePath)
                             .arg(sendFile->errorString()));
        return;
    }
    TotalSendBytes = sendFile->size();
    QDataStream sendOut(&outBlock, QIODevice::WriteOnly);
    sendOut.setVersion(QDataStream::Qt_5_0);

    //寫入傳送型別,資料大小,檔案型別,檔名大小,檔名
    sendOut << quint8(0) << qint64(0) << quint16(0) << qint64(0) << sendFileName;
    TotalSendBytes +=  outBlock.size();
    sendOut.device()->seek(0);
    sendOut << quint8(sendtype_file)<< TotalSendBytes << quint16(type)
            << qint64((outBlock.size() - sizeof(qint64) * 2) - sizeof(quint16) - sizeof(quint8));

    this->writeData(outBlock,outBlock.size());
    //this->flush();
    bytesToWrite = TotalSendBytes - outBlock.size();

    outBlock.resize(0);
}
讀取傳送檔案
建立傳送檔案頭
用writeData將檔案頭寫入TCP傳送流,記錄已傳送位元組數


傳送檔案內容:
void testClient::updateClientProgress(qint64 numBytes)  //傳送檔案內容
{
    if(TotalSendBytes == 0)
        return;

    bytesWritten += (int)numBytes;
    proBarValue += (int)numBytes;

    emit updateProgressSignal(proBarValue,proBarMax,num);   //更新發送進度訊號

    if (bytesToWrite > 0)
    {
        outBlock = sendFile->read(qMin(bytesToWrite, loadSize));
        bytesToWrite -= (int)this->writeData(outBlock,outBlock.size());
        outBlock.resize(0);
    }
    else
    {
        sendFile->close();

        //結束傳輸檔案訊號
        if(TotalSendBytes < 1024)
        {
            emit outputlog(tr("finish sending file to client: %1\n filename: %2 %3B")
                           .arg(this->peerAddress().toString())
                           .arg(sendFileName)
                           .arg(TotalSendBytes));
        }
        else if(TotalSendBytes < 1024*1024)
        {
            emit outputlog(tr("finish sending file to client: %1\n filename: %2 %3KB")
                           .arg(this->peerAddress().toString())
                           .arg(sendFileName)
                           .arg(TotalSendBytes / 1024.0));
        }
        else
        {
            emit outputlog(tr("finish sending file to client: %1\n filename: %2 %3MB")
                           .arg(this->peerAddress().toString())
                           .arg(sendFileName)
                           .arg(TotalSendBytes / (1024.0*1024.0)));
        }

            if(sendNum < openList.size())   //還有檔案需要傳送
            {
                if(sendNum == 0)
                {
                    //QFile::remove(sendFilePath);    //刪除列表檔案
                    proBarMax = totalSendSize;
                    proBarValue = 0;
                }
                sendFilePath = openList[sendNum].filePath;
                sendFileName = openList[sendNum].fileName;
                sendNum++;
                startTransfer(filetype_wavfile);
            }
            else    //傳送結束
            {
                emit newClientInfo(tr("send complete"),num,4);

                TotalSendBytes = 0;    //總共需傳送的位元組數
                bytesWritten = 0;       //已傳送位元組數
                bytesToWrite = 0;       //待發送位元組數
            }
    }
}
檔案未傳送完:記錄傳送位元組數,writeData繼續傳送,writeData一旦寫入傳送流,自動又進入updateClientProgress函式
檔案已傳送完:發出訊號,檢測是否還有檔案需要傳送,若有則呼叫startTransfer繼續傳送,若沒有則發出訊號,更新客戶端資訊
 


準備傳輸檔案函式,被上層呼叫,引數為傳送檔案列表:

void testClient::prepareTransfer(std::vector<openFileStruct>& openFileList)    //準備傳輸檔案
{
    if(openFileList.size() == 0)    //沒有檔案
    {
        return;
    }

    testtype_now = testtype_t;
    isSendKeyword = false;
    for(int i=0; i<2; i++)
        isReceived[i] = false;

    openList.clear();
    openList.assign(openFileList.begin(),openFileList.end());   //拷貝檔案列表

    QString sendFileListName = "sendFileList.txt";
    QString sendFileListPath = temp_Path + "/" + sendFileListName;

    getSendFileList(sendFileListPath);     //在指定路徑生成傳送檔案列表

    emit newClientInfo(tr("sending test files"),num,4);   //更新主視窗測試階段
        sendFilePath = sendFileListPath;
        sendFileName = sendFileListName;
        sendNum = 0;    //傳送到第幾個檔案

        proBarMax = getSendTotalSize();
        proBarValue = 0;

        startTransfer(filetype_list);    //開始傳輸檔案
}
拷貝檔案列表
生成傳送檔案列表檔案
更新主視窗資訊
開始傳輸列表檔案
 


上面呼叫的生成列表檔案函式如下:

void testClient::getSendFileList(QString path)     //在指定路徑生成傳送檔案列表
{
    sendFileNum = openList.size();    //記錄傳送檔案個數
    totalSendSize = 0;  //記錄傳送檔案總大小

    for(int i = 0; i < sendFileNum; i++)
    {
        sendFileName = openList[i].fileName;
        sendFilePath = openList[i].filePath;

        totalSendSize += getSendTotalSize();
    }

    FILE *fp;
    fp = fopen(path.toLocal8Bit().data(),"w");

    fprintf(fp,"%d\n",sendFileNum);
    fprintf(fp,"%d\n",totalSendSize);

    for(int i = 0; i < sendFileNum; i++)
    {
        fprintf(fp,"%s\n",openList[i].fileName.toLocal8Bit().data());
    }

    fclose(fp);
}


被上面呼叫getSendTotalSize函式如下:
quint64 testClient::getSendTotalSize()    //獲取sendFilePath對應檔案加上頭的大小
{
    int totalsize;
    //計算列表檔案及檔案頭總大小
    QFile *file = new QFile(sendFilePath);
    if (!file->open(QFile::ReadOnly ))  //讀取傳送檔案
    {
        QMessageBox::warning(NULL, tr("WARNING"),
                             tr("can not read file %1:\n%2.")
                             .arg(sendFilePath)
                             .arg(file->errorString()));
        return 0;
    }

    totalsize = file->size();  //檔案內容大小
    QDataStream sendOut(&outBlock, QIODevice::WriteOnly);
    sendOut.setVersion(QDataStream::Qt_5_0);
    //寫入傳送型別,資料大小,檔案型別,檔名大小,檔名
    sendOut << quint8(0) << qint64(0) << quint16(0) << qint64(0) << sendFileName;
    totalsize +=  outBlock.size();  //檔案頭大小

    file->close();

    outBlock.resize(0);

    return totalsize;
}



3、testServer類
一個testClient類例項為一個socket伺服器端描述
(1)類宣告:

class testServer : public QTcpServer
{
    Q_OBJECT
public:
    testServer(QObject *parent = 0);
    std::vector<testClientp> clientList;     //客戶端tcp連線
    std::vector<QString> ipList;    //客戶端ip
    int totalClient;  //客戶端數

protected:
    void incomingConnection(int socketDescriptor);  //虛擬函式,有tcp請求時會觸發

signals:
    void error(QTcpSocket::SocketError socketError);    //錯誤訊號
    void newClientSignal(QString clientIP,int state,int threadNum);   //將新客戶端資訊發給主視窗
    void updateProgressSignal(qint64,qint64,int);   //更新發送進度訊號
    void outputlogSignal(QString);  //傳送日誌訊息訊號
    void newClientInfoSignal(QString,int,int);    //更新客戶端資訊

public slots:
    void newClientSlot(QString clientIP,int state,int threadNum);   //將新客戶端資訊發給主視窗
    void updateProgressSlot(qint64,qint64,int);   //更新發送進度槽
    void outputlogSlot(QString);        //傳送日誌訊息槽
    void newClientInfoSlot(QString,int,int);      //更新客戶端資訊

private:
    int getClientNum(testClientp socket); //檢測使用者,若存在,返回下標,若不存在,返回使用者數
};
public:需要主視窗訪問的變數
incomingConnection:接收tcp請求
signals:傳送客戶端資訊的訊號
public slots:接收下層testClient訊號的槽,並向上層主視窗傳送訊號
private:檢測使用者是否存在的輔助函式
 


(2)類定義:
建構函式:

testServer::testServer(QObject *parent) :
    QTcpServer(parent)
{
    totalClient = 0;
    clientList.clear();
    ipList.clear();
}



接收tcp請求函式:
void testServer::incomingConnection(int socketDescriptor)
{
    testClient *socket = new testClient(this);

    if (!socket->setSocketDescriptor(socketDescriptor))
    {
        QMessageBox::warning(NULL,"ERROR",socket->errorString());
        emit error(socket->error());
        return;
    }

    int num = getClientNum(socket); //檢測使用者,若存在,返回下標,若不存在,返回使用者數
    socket->num = num;  //記錄序號

    emit newClientSignal(socket->peerAddress().toString(),(int)socket->state(),num);   //將新客戶端資訊發給主視窗

    //連線訊號和槽
    connect(socket, SIGNAL(newClient(QString,int,int)), this, SLOT(newClientSlot(QString,int,int)));
    connect(socket, SIGNAL(outputlog(QString)), this, SLOT(outputlogSlot(QString)));
    connect(socket, SIGNAL(updateProgressSignal(qint64,qint64,int)),
            this, SLOT(updateProgressSlot(qint64,qint64,int)));
    connect(socket, SIGNAL(newClientInfo(QString,int,int)),
            this, SLOT(newClientInfoSlot(QString,int,int)));
    connect(socket, SIGNAL(readyToTest(int,int)), this, SLOT(readyToTestSlot(int,int)));
    connect(socket, SIGNAL(startEvaluate(int,QString)), this, SLOT(startEvaluateSlot(int,QString)));
    connect(socket, SIGNAL(evaluateComplete(int,QString,int)),
            this, SLOT(evaluateCompleteSlot(int,QString,int)));

    if(num == totalClient)
        totalClient++;
}
通過setSocketDescriptor獲取當前接入的socket描述符
檢測使用者是否存在,記錄序號
向主視窗傳送訊號,連線下層testClient訊號和接收槽
更新客戶端總數

 
檢測使用者是否存在的函式

int testServer::getClientNum(testClientp socket) //檢測使用者,若存在,返回下標,若不存在,加入列表
{
    for(int i = 0; i < ipList.size(); i++)
    {
        qDebug() << "No." << i << "ip: " << ipList[i];

        if(ipList[i] == socket->peerAddress().toString())
        {
            clientList[i] = socket;
            return i;
        }
    }

    clientList.push_back(socket);   //存入客戶列表
    ipList.push_back(socket->peerAddress().toString()); //存入ip列表
    return totalClient;
}
其他傳送訊號的函式均為連線下層和上層的中轉,由於比較簡單就不贅述了
 
 
4、主視窗
在主視窗(繼承自QMainWindow)繪製所需控制元件:
lineEdit_ipaddress:顯示伺服器IP
lineEdit_port:設定伺服器埠
listWidget_sendwav:傳送檔案列表
tableWidget_clientlist:客戶端資訊列表,顯示所有客戶端IP,接入狀態,傳送/接收進度條
textEdit_status:狀態列
pushButton_addwav:添加發送檔案按鈕
pushButton_deletewav:刪除傳送檔案按鈕
pushButton_startserver:開啟/關閉伺服器按鈕
pushButton_senddata:傳送資料按鈕
pushButton_deleteclient:刪除客戶端按鈕


主視窗需定義的變數
testServer *tcpServer;  //tcp伺服器指標
bool isServerOn;    //伺服器是否開啟
std::vector<openFileStruct> openFileList; //儲存目錄和檔名對


開啟/關閉伺服器按鈕

void testwhynot::on_pushButton_startserver_clicked()    //開啟伺服器
{
    if(isServerOn == false)
    {
        tcpServer = new testServer(this);

        ui->comboBox_testtype->setEnabled(false);   //開啟伺服器後不能更改測試型別
        ui->pushButton_addwav->setEnabled(false);   //不能更改wav列表
        ui->pushButton_deletewav->setEnabled(false);

        //監聽本地主機埠,如果出錯就輸出錯誤資訊,並關閉
        if(!tcpServer->listen(QHostAddress::Any,ui->lineEdit_port->text().toInt()))
        {
            QMessageBox::warning(NULL,"ERROR",tcpServer->errorString());
            return;
        }
        isServerOn = true;
        ui->pushButton_startserver->setText(tr("close server"));

        //顯示到狀態列
        ui->textEdit_status->append(tr("%1 start server: %2:%3")
                                    .arg(getcurrenttime())
                                    .arg(ipaddress)
                                    .arg(ui->lineEdit_port->text()));
        //連結錯誤處理訊號和槽
        //connect(this,SIGNAL(error(int,QString)),this,SLOT(displayError(int,QString)));

        //處理多客戶端,連線從客戶端執行緒發出的訊號和主視窗的槽
        //連線客戶端資訊更改訊號和槽
        connect(tcpServer,SIGNAL(newClientSignal(QString,int,int)),this,SLOT(acceptNewClient(QString,int,int)));
        //連線日誌訊息訊號和槽
        connect(tcpServer,SIGNAL(outputlogSignal(QString)),
                this,SLOT(acceptOutputlog(QString)));
        //連線更新發送進度訊號和槽
        connect(tcpServer, SIGNAL(updateProgressSignal(qint64,qint64,int)),
                this, SLOT(acceptUpdateProgress(qint64,qint64,int)));
        //連線更新客戶端資訊列表訊號和槽
        connect(tcpServer, SIGNAL(newClientInfoSignal(QString,int,int)),
                this, SLOT(acceptNewClientInfo(QString,int,int)));

        //顯示到狀態列
        ui->textEdit_status->append(tr("%1 wait for client...")
                                    .arg(getcurrenttime()));
    }
    else
    {
        isServerOn = false;
        ui->pushButton_startserver->setText(tr("start server"));

        //斷開所有客戶端連線,發出disconnected()訊號
         for(int i=0; i<tcpServer->clientList.size(); i++)
        {

            if(ui->tableWidget_clientlist->item(i,2)->text() == clientStatus[3])    //處於連線狀態才斷開,否則無法訪問testClientp指標
              {
                testClientp p = tcpServer->clientList[i];
                   p->close();
            }
        }

        //清空列表
         tcpServer->clientList.clear();
        tcpServer->ipList.clear();
        clientInfoList.clear();
        for(int i=0; i<ui->tableWidget_clientlist->rowCount(); )
            ui->tableWidget_clientlist->removeRow(i);

        tcpServer->close(); //關閉伺服器

        ui->textEdit_status->append(tr("%1 clost server.").arg(getcurrenttime()));

        ui->comboBox_testtype->setEnabled(true);    //可以重新選擇測試型別
        ui->pushButton_addwav->setEnabled(true);   //能更改wav列表
        ui->pushButton_deletewav->setEnabled(true);

        //按鈕無效化
        ui->pushButton_senddata->setEnabled(false);
        ui->pushButton_starttest->setEnabled(false);
        ui->pushButton_deleteclient->setEnabled(false);
    }
}
注意:
1、listen(“監聽型別”,“埠號”) 用於開啟伺服器,在指定埠監聽客戶端連線
2、connect連線了客戶端更新資訊的訊號,來自下層testServer類


傳送資料按鈕
void testwhynot::on_pushButton_senddata_clicked()   //傳送資料
{

    //多客戶端傳送資料
    if(ui->comboBox_testtype->currentIndex() == testtype_keyword)
    {
        if(!check_file(keyword_filepath))
            return;
    }

    vector<clientInfo>::iterator it;

    //遍歷所有客戶端資訊,確保都處於“已連線”狀態
    for(it=clientInfoList.begin(); it!=clientInfoList.end(); it++)
    {
        if((*it).state != 3)   //有客戶端未處於“已連線”狀態
        {
            QMessageBox::warning(this,tr("WARNING"),tr("client %1 is not connected.").arg((*it).ip));
            return;
        }
    }

    //沒有檔案
    if(openFileList.size() == 0)
    {
        QMessageBox::warning(NULL,tr("WARNING"),tr("no file! can not send!"));
        return;
    }

    for(int i = 0; i < tcpServer->clientList.size(); i++)
    {
        tcpServer->clientList[i]->prepareTransfer(openFileList,ui->comboBox_testtype->currentIndex());
    }

    //按鈕無效化
    ui->pushButton_senddata->setEnabled(false);
}
呼叫底層prepareTransfer函式開始傳輸


刪除客戶端按鈕
void testwhynot::on_pushButton_deleteclient_clicked()
{
    QList<QTableWidgetItem*> list = ui->tableWidget_clientlist->selectedItems();   //讀取所有被選中的item
    if(list.size() == 0)    //沒有被選中的就返回
    {
        QMessageBox::warning(this,QObject::tr("warning"),QObject::tr("please select a test client"));
        return;
    }

    std::set<int> del_row;   //記錄要刪除的行號,用set防止重複

    for(int i=0; i<list.size(); i++)    //刪除選中的項
    {
        QTableWidgetItem* sel = list[i]; //指向選中的item的指標
        if (sel)
        {
            int row = ui->tableWidget_clientlist->row(sel);   //獲取行號
            del_row.insert(row);
        }
    }

    std::vector<int> del_list;  //賦值給del_list,set本身為有序
    for(std::set<int>::iterator it=del_row.begin(); it!=del_row.end(); it++)
    {
        del_list.push_back(*it);
    }

    for(int i=del_list.size()-1; i>=0; i--)    //逆序遍歷
    {
        testClientp p = tcpServer->clientList[del_list[i]];
        p->close();

        ui->tableWidget_clientlist->removeRow(del_list[i]);   //從顯示列表中刪除行
        clientInfoList.erase(clientInfoList.begin() + del_list[i]); //從內部列表刪除
        tcpServer->ipList.erase(tcpServer->ipList.begin() + del_list[i]);
        tcpServer->clientList.erase(tcpServer->clientList.begin() + del_list[i]);
        tcpServer->totalClient--;
    }

    for(int i=0; i<tcpServer->clientList.size(); i++)
    {
        tcpServer->clientList[i]->num = i;
    }

    if(clientInfoList.empty())  //沒有客戶端,刪除按鈕無效化
    {
        ui->pushButton_deleteclient->setEnabled(false);
    }
}
刪除客戶端較麻煩,由於支援多選刪除,需要先將選中的行號排序、去重、從大到小遍歷刪除(防止刪除後剩餘行號變化)
不僅要關閉客戶端,還要將其從顯示列表和內部列表刪除,保持顯示和實際列表同步




上述便是基於tcp傳輸的傳送/接收伺服器端的搭建,客戶端只需遵從上述的傳送協議搭建即可,客戶端傳送與接收與伺服器基本相同,也不贅述了。
 
本文偏重於工程實現,Qt的TCP傳輸原理敘述不多,若要深入瞭解qt套接字程式設計,請參考:http://cool.worm.blog.163.com/blog/static/64339006200842922851118/

相關推薦

Qt5 基於TCP傳輸傳送/接收檔案伺服器支援客戶

一、實現功能 1、伺服器端選擇待發送的檔案,可以是多個 2、開啟伺服器,支援多客戶端接入,能夠實時顯示每個客戶端接入狀態 3、等待所有客戶端都處於已連線狀態時,依次傳送檔案集給每個客戶端,顯示每個客戶端傳送進度 4、傳送完成後等待接收客戶端發回的檔案,顯示接收進度 5、關閉

基於Tcp穿越的Windows遠端桌面遠端桌面管理工具

基於Tcp穿越的Windows遠端桌面(遠端桌面管理工具)        距離上一篇文章《基於.NET環境,C#語言 實現 TCP NAT》已經很久沒有發隨筆了,而且文章閱讀量不高,可能很多人對Tcp穿越不感興趣。今天這篇文章是基於上一篇文章做的應用,一個遠端桌面管理

java執行緒通訊伺服器客戶

基於TCP的多執行緒通訊 伺服器執行緒: package com.netproject1; import java.io.DataInputStream; import java.io.DataOutputStream; import java.io.IOExc

delphi獲取音視訊媒體檔案資訊支援D7,XE7

福利來了,delphi獲取音視訊媒體檔案資訊,親測支援D7和XE7。支援音訊檔案(*.ACC;*.AC3;*.APE;*.DTS;*.FLAC;*.M4A;*.MKA;*.MP2;*.MP3;*.MPA;*.PMC;*.OFR;*.OGG;*.RA;*.TTA;*.WAV;

Unity 與C#伺服器 實現Socket的UDP通訊客戶

前言 上一篇簡單的介紹了下Unity客戶端和伺服器的Socket通訊,但是還不能實現多個客戶端與伺服器的通訊,所以今天在這邊把前面的工程完善一下(使用的是上篇講到的UdpClient類來實現),實現多個客戶端與伺服器的udp通訊。效果圖如下,兩個客戶端可以向伺服器傳送訊息,

C#.網路程式設計 Socket基礎 基於WinForm系統Socket TCP協議 實現伺服器客戶.txt.word.png等不同型別檔案傳輸

一、簡介: 前面的兩篇介紹了字串傳輸、圖片傳輸: 其實,本文針對Socket基礎(二)進一步完成,以便可以進行多種檔案傳輸。 二、基於不同的流(檔案流、記憶體流、網路等)讀寫。 1、圖片傳輸 方法一:(在客戶端用檔案流傳送(即將圖片寫到檔案流去,以便傳送),

Windows下基於TCP協議的大檔案傳輸流形式

在TCP下進行大檔案傳輸,不像小檔案那樣直接打包個BUFFER傳送出去,因為檔案比較大可能是1G,2G或更大,第一效率問題,第二TCP粘包問題。針對服務端的設計來說就更需要嚴緊些。下面介紹簡單地實現大檔案在TCP的傳輸應用。 粘包出現原因:在流傳輸中出現,UDP不會出現粘包,因為它有訊息邊界(參考Wi

虛擬機器主機linuxunbuntu和開發板使用串列埠連線以及傳送接收檔案

一、串列埠使用背景 基本上檔案都是用tftp、nfs協議上傳和接收,不過這個需要使用到網路,相當於佔用網線口,不過相對而言,檔案上傳速度較快,對於檔案小的檔案(<1M大小),建議使用minicom工具;對於大檔案,推薦使用tftp或者nfs工具。 二、minicom工具 1、linux

Python網路程式設計--通過fork、tcp併發完成ftp檔案伺服器

ftp檔案伺服器  一、專案功能      1.服務端和客戶端兩部分,要求啟動一個服務端可以同時處理多個客戶端請求      2.功能:1).可以檢視服務端檔案庫中所有的普通檔案       &nbs

網路程式設計--使用TCP協議傳送接收資料

package com.zhangxueliang.tcp; import java.io.IOException; import java.io.OutputStream; import java.net.InetAddress; import java.net.Socket; /*** * 使用

TCP檔案伺服器 *****領卓教育******

server int process_list(int newsockfd,char *buf) //設定要共享的目錄 { DIR * dirp; int ret; struct dirent * direntp; dirp = opendir("./");/

基於TCP傳輸的粘包問題

1我們都知道TCP傳輸,是基於位元組流傳輸的,所以流與流直接傳輸就會產生邊界問題,我個人對粘包的理解就是,TCP傳輸無法獲悉不同包與包之間的“界限”。 如果對等接受方彼此直接沒有約定好傳輸資料大小的話,就會出現解析資料不準確問題,而且傳輸資料小於約定大小空間的話,也會出現浪費空間問題,為了解

Android TCP/IP 傳送接收16進位制資料

幫朋友推薦,贏眾投理財,CEO是我朋友,全是真實可靠的農業專案,投資收益可達年化9.8%,且有多重安全保障! // 設定伺服器IP和埠private static final String SERVERIP_2        ="192.168.5.178"; p

java網路程式設計基於TCP客戶連線伺服器

package com.test.net; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.ServerSocket; import java.net.S

Java網路程式設計TCP協議傳送接收資料

一、客戶端傳送,伺服器端接收 package net; import java.io.IOException; import java.io.InputStream; import java.net.ServerSocket; import java.net.Socket; /* * TC

服務接收客戶傳送的檔名,並把檔案的內容返回給客戶

public class ScoketService {public static void server() {System.out.println("-------------服務已啟動-------------");ServerSocket serverSocket = null;try {server

System V訊息佇列實現的檔案伺服器不跨網路

可能是定時的部分有問題吧,導致客戶端無法接收資料,不過我感覺思想是沒錯的。。。先pull上吧,以後發現錯誤再改 參考資料:UNP卷二 message.h #ifndef _MESSAGE_H #define _MESSAGE_H #include<stdio.h> #i

[原始碼和文件分享]基於java語言的FTP伺服器Ping測試工具軟體

一 需求分析 已知引數:目的節點IP地址或主機名 設計要求:通過原始套接字程式設計,模擬Ping命令,實現其基本功能,即輸入一個IP地址或一段IP地址的範圍,分別測試其中每個IP地址所對應主機的可達性,並返回耗時、生存時間等引數,並統計成功傳送和回送的Ping報文

基於TCP的C/S模式模板Winsock實現

伺服器程式server.cpp: #define _CRT_SECURE_NO_WARNINGS #include <stdio.h> #include <tchar.h> #include <locale.h> #include <WinSo

C#.網路程式設計 Socket基礎Socket TCP協議 實現伺服器客戶簡單字串通訊

簡介:        本章節主要討論了Socket的入門知識,還未針對Socket的難點問題(比如TCP的無訊息邊界問題)展開討論,往後在其他章節中進行研究。 注意點: 伺服器(比如臺式電腦)的IP為1.1.1.2,那麼客戶端(其他裝置,比如手機,Ipad)連線的一定是