Qt5 基于TCP传输的发送/接收文件服务器(支持多客户端)

来源:互联网 发布:淘宝口碑店铺 编辑:程序博客网 时间:2024/05/22 12:14

Qt5 基于TCP传输的发送/接收文件服务器(支持多客户端)

标签: Qt服务器客户端文件传输
4484人阅读 评论(2)收藏举报
本文章已收录于:
计算机网络知识库
分类:
作者同类文章X

    一、实现功能
    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实例

    [cpp] view plain copy
    print?
    1. testClient *socket = new testClient(this);  
    2.   
    3. if (!socket->setSocketDescriptor(socketDescriptor))  
    4. {  
    5.     emit error(socket->error());  
    6.     return;  
    7. }  

    3、发送数据
    实例化testClient类,通过void writeData(const char * data, qint64 size)函数将数据写入TCP流

    [cpp] view plain copy
    print?
    1. testClient *a;  
    2. a->writeData("message",8);  

    一般来说,使用QByteArray和QDataStream进行格式化的数据写入,设置流化数据格式类型为QDataStream::Qt_5_0,与客户端一致

    [cpp] view plain copy
    print?
    1. testClient *a;  
    2. QByteArray outBlock;       //缓存一次发送的数据  
    3. QDataStream sendOut(&outBlock, QIODevice::WriteOnly);   //数据流  
    4. sendOut.setVersion(QDataStream::Qt_5_0);  
    5. sendOut << "message";  
    6.   
    7. a->writeData(outBlock,outBlock.size());  
    8.   
    9. outBlock.resize(0);  

    注意outBlock如果为全局变量,最后需要resize,否则下一次写入时会出错


    在testClient构造函数中连接数据成功发送后产生的bytesWritten()信号

    [cpp] view plain copy
    print?
    1. connect(this, SIGNAL(bytesWritten(qint64)), this, SLOT(updateClientProgress(qint64)));  

    参数为成功写入TCP流的字节数
    通过捕获这个信号,可以实现数据的连续发送。因为发送文件时,数据是一块一块发送的,比如设定一块的大小为20字节,那么发送完20字节,怎么继续发送呢?就靠的是这个函数,再用全局变量记录下总共发送的字节数,便可以控制发送的结束。


    4、发送文件
    先把文件读到QFile变量中,再通过QBytesArray发送

    [cpp] view plain copy
    print?
    1. testClient *a;  
    2. QFile sendFile = new QFile(sendFilePath);<span style="WHITE-SPACE: pre">    </span>//读取发送文件路径  
    3. if (!sendFile->open(QFile::ReadOnly ))  //读取发送文件  
    4. {    return;}  
    5. QByteArray outBlock;  
    6. outBlock = sendFile->read(sendFile.size());  
    7. a->writeData(outBlock,outBlock.size());  


    5、接收数据
    依旧是testClient类,连接有readyRead()信号,readyRead()信号为新连接中有可读数据时发出

    [cpp] view plain copy
    print?
    1. connect(this, SIGNAL(readyRead()), this, SLOT(recvData()));  

    首先将testClient连接实例(如testClient *a)封装到QDataStream变量中,设置流化数据格式类型为QDataStream::Qt_5_0,与客户端一致。
    在读入数据前,用a->bytesAvailable()检测数据流中可读入的字节数,然后就可以通过"<<"操作符读入数据了。

    [cpp] view plain copy
    print?
    1. QDataStream in(a); //本地数据流  
    2. in.setVersion(QDataStream::Qt_5_0); //设置流版本,以防数据格式错误  
    3. quint8 receiveClass;  
    4. if(this->bytesAvailable() >= (sizeof(quint8)))  
    5. {      
    6.     in >> receiveClass;  
    7. }  

    6、接收文件
    使用readAll读入TCP流中所有数据,再写入到QFile变量中

    [cpp] view plain copy
    print?
    1. QByteArray inBlock;  
    2. inBlock = a->readAll();  //读入所有数据  
    3. QFile receivedFile = new QFile(receiveFilePath);    //打开接收文件  
    4. if (!receivedFile->open(QFile::WriteOnly ))  
    5. {    return;}  
    6. QFile receivedFile->write(inBlock);  //写入文件  
    7. inBlock.resize(0);  


    三、具体实现
    1、全局定义
    需要定义一些全局变量和常量,定义如下

    [cpp] view plain copy
    print?
    1. //存储文件路径和文件名的结构体  
    2. struct openFileStruct  
    3. {  
    4.     QString filePath;  
    5.     QString fileName;  
    6. };  
    7.   
    8. struct clientInfo   //客户端信息  
    9. {  
    10.     QString ip;     //ip  
    11.     int state;      //状态  
    12.     QString id;     //id  
    13. };  
    14.   
    15. const quint8 sendtype_file = 0;    //发送类型是文件  
    16. const quint8 sendtype_start_test = 10;    //发送类型是开始测试  
    17. const quint8 sendtype_msg = 20;    //发送类型是消息  
    18.   
    19. const quint16 filetype_list = 0;    //文件类型为列表  
    20. const quint16 filetype_wavfile = 1;    //文件类型为wav文件  
    21.   
    22. const QString clientStatus[7] =    //客户端状态  
    23.     {QObject::tr("Unconnected"),QObject::tr("HostLookup"),QObject::tr("Connecting"),  
    24.         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)类声明:

    [cpp] view plain copy
    print?
    1. class testClient : public QTcpSocket  
    2. {  
    3.     Q_OBJECT  
    4. public:  
    5.     testClient(QObject *parent = 0);  
    6.     int num;    //客户端序号  
    7.   
    8.     void prepareTransfer(std::vector<openFileStruct>& openFileList,int testtype_t);    //准备传输文件  
    9.   
    10. signals:  
    11.     void newClient(QString, intint);  //客户端信息变更信号  
    12.     void outputlog(QString);    //输出日志信息  
    13.     void updateProgressSignal(qint64,qint64,int);   //更新发送进度信号  
    14.     void newClientInfo(QString,int,int);  //更新客户端信息  
    15.   
    16. private slots:  
    17.     void recvData();    //接收数据  
    18.     void clientConnected();     //已连接  
    19.     void clientDisconnected();  //断开连接  
    20.     void newState(QAbstractSocket::SocketState);    //新状态  
    21.     void startTransfer(const quint16);    //开始传输文件  
    22.     void updateClientProgress(qint64 numBytes);  //发送文件内容  
    23.     void getSendFileList(QString path);     //在指定路径生成发送文件列表  
    24.     quint64 getSendTotalSize();    //获取sendFilePath对应文件加上头的大小  
    25.   
    26. private:  
    27.     //发送文件所需变量  
    28.     qint64  loadSize;          //每次接收的数据块大小  
    29.   
    30.     qint64  TotalSendBytes;        //总共需发送的字节数  
    31.     qint64  bytesWritten;      //已发送字节数  
    32.     qint64  bytesToWrite;      //待发送字节数  
    33.     QString sendFileName;          //待发送的文件的文件名  
    34.     QString sendFilePath;          //待发送的文件的文件路径  
    35.     QFile *sendFile;          //待发送的文件  
    36.     QByteArray outBlock;       //缓存一次发送的数据  
    37.     int sendNum;        //记录当前发送到第几个文件  
    38.     int sendFileNum;    //记录发送文件个数  
    39.     int totalSendSize;  //记录发送文件总大小  
    40.     quint64 proBarMax;  //发送进度条最大值  
    41.     quint64 proBarValue;    //进度条当前值  
    42.   
    43.     std::vector<openFileStruct> openList;   //发送文件列表  
    44.   
    45.     //接收文件用变量  
    46.     quint8 receiveClass;        //0:文件,1:开始测试,20:客户端接收到文件反馈  
    47.     quint16 fileClass;          //待接收文件类型  
    48.     qint64 TotalRecvBytes;          //总共需接收的字节数  
    49.     qint64 bytesReceived;       //已接收字节数  
    50.     qint64 fileNameSize;        //待接收文件名字节数  
    51.     QString receivedFileName;   //待接收文件的文件名  
    52.     QFile *receivedFile;        //待接收文件  
    53.     QByteArray inBlock;         //接收临时存储块  
    54.     qint64 totalBytesReceived;  //接收的总大小  
    55.     qint32 recvNum;       //现在接收的是第几个  
    56.     quint64 recvFileNum;         //接收文件个数  
    57.     quint64 totalRecvSize;    //接收文件总大小  
    58.     bool isReceived[3];     //是否收到客户端的接收文件反馈  
    59.       
    60. };  

    public为需要上层访问的公有变量和函数
    signals为向上层发送的信号,保证主窗口实时显示信息
    private slots包括底层实现发送、接收消息的函数,以及信号处理的槽
    private为私有变量,包括发送文件和接收文件所需的变量


    (2)类定义:
    构造函数:

    [cpp] view plain copy
    print?
    1. testClient::testClient(QObject *parent) :  
    2.     QTcpSocket(parent)  
    3. {  
    4.     //发送变量初始化  
    5.     num = -1;  
    6.     loadSize = 100*1024;  
    7.     openList.clear();  
    8.   
    9.     //接收变量初始化  
    10.     TotalRecvBytes = 0;  
    11.     bytesReceived = 0;  
    12.     fileNameSize = 0;  
    13.     recvFileNum = 0;  
    14.     totalRecvSize = 0;  
    15.     totalBytesReceived = 0;  
    16.     recvNum = 0;  
    17.     receiveClass = 255;  
    18.     fileClass = 0;  
    19.     for(int i=0; i<3; i++)  
    20.         isReceived[i] = false;  
    21.   
    22.     //连接信号和槽  
    23.     connect(this, SIGNAL(readyRead()), this, SLOT(recvData()));  
    24.     connect(this, SIGNAL(disconnected()), this, SLOT(clientDisconnected()));  
    25.     connect(this, SIGNAL(stateChanged(QAbstractSocket::SocketState)),  
    26.             this, SLOT(newState(QAbstractSocket::SocketState)));  
    27.     connect(this, SIGNAL(bytesWritten(qint64)), this, SLOT(updateClientProgress(qint64)));  
    28. }  

    初始化变量
    连接qt的tcp信号和自定义槽,包括:
    readyRead() 接收消息信号
    disconnected() 断开连接信号
    stateChanged(QAbstractSocket::SocketState) 连接状态变更信号
    bytesWritten(qint64) 已写入发送消息流信号
     


    接收数据槽,设定服务器只接收一个文件:

    [cpp] view plain copy
    print?
    1. void testClient::recvData()     //接收数据,服务器只接收客户端的一个结果文件  
    2. {  
    3.     QDataStream in(this); //本地数据流  
    4.     in.setVersion(QDataStream::Qt_5_0); //设置流版本,以防数据格式错误  
    5.   
    6.     QString unit;  
    7.     qint32 msg;  
    8.   
    9.     if(bytesReceived <= (sizeof(quint8)))  
    10.     {  
    11.         if(this->bytesAvailable() >= (sizeof(quint8)))  
    12.         {  
    13.             in >> receiveClass;  
    14.         }  
    15.         switch(receiveClass)  
    16.         {  
    17.         case sendtype_file:     //接收文件  
    18.             bytesReceived += sizeof(quint8);  
    19.             qDebug() << "bytesReceived: " << bytesReceived;  
    20.             break;  
    21.   
    22.         case sendtype_msg:  
    23.             in >> msg;  
    24.           
    25.             if(msg == 0)    //接收文件列表  
    26.             {  
    27.                 emit outputlog(tr("client %1 have received list file")  
    28.                                .arg(this->peerAddress().toString()));  
    29.   
    30.             }  
    31.             else if(msg == 1)   //接收文件  
    32.             {  
    33.                 emit outputlog(tr("client %1 have received file(s)")  
    34.                                .arg(this->peerAddress().toString()));  
    35.   
    36.             }  
    37.             return;  
    38.   
    39.         default:  
    40.             return;  
    41.         }  
    42.     }  
    43.   
    44.     if(bytesReceived >= (sizeof(quint8)) && bytesReceived <= (sizeof(quint8) + sizeof(qint64)*2 + sizeof(quint16)))   //开始接收文件,先接受报头  
    45.     {  
    46.         //收3个int型数据,分别存储总长度、文件类型和文件名长度  
    47.         if( ( this->bytesAvailable() >= (sizeof(qint64)*2 + sizeof(quint16)) ) && (fileNameSize == 0) )  
    48.         {  
    49.             in >> TotalRecvBytes >> fileClass >> fileNameSize;  
    50.   
    51.             bytesReceived += sizeof(qint64)*2;  //收到多少字节  
    52.             bytesReceived += sizeof(quint16);  
    53.   
    54.             if(fileClass == filetype_result)  
    55.             {  
    56.                 recvNum = 1;  
    57.                 recvFileNum = 1;  
    58.                 totalRecvSize = TotalRecvBytes;  //只有一个文件,文件总大小为该文件发送大小  
    59.                 totalBytesReceived += sizeof(qint64)*2;  
    60.                 totalBytesReceived += sizeof(quint16);  
    61.                 totalBytesReceived += sizeof(quint8);  
    62.   
    63.                 emit newClientInfo(tr("receiving result"),num,4);  
    64.             }  
    65.             else  
    66.             {  
    67.                 QMessageBox::warning(NULL,tr("WARNING"),tr("client %1 send wrong type of file to server")  
    68.                                      .arg(this->peerAddress().toString()));  
    69.                 return;  
    70.             }  
    71.   
    72.         }  
    73.         //接着收文件名并建立文件  
    74.         if((this->bytesAvailable() >= fileNameSize)&&(fileNameSize != 0))  
    75.         {  
    76.             in >> receivedFileName;  
    77.             bytesReceived += fileNameSize;  
    78.   
    79.             totalBytesReceived += fileNameSize;  
    80.   
    81.             QString receiveFilePath = receive_path + "/" + receivedFileName;    //接收文件路径  
    82.   
    83.             emit outputlog(tr("receive from client %1\nsave as:%2")  
    84.                            .arg(this->peerAddress().toString())  
    85.                            .arg(receiveFilePath));  
    86.   
    87.             //建立文件  
    88.             receivedFile = new QFile(receiveFilePath);  
    89.   
    90.             if (!receivedFile->open(QFile::WriteOnly ))  
    91.             {  
    92.                 QMessageBox::warning(NULL, tr("WARNING"),  
    93.                                      tr("cannot open file %1:\n%2.").arg(receivedFileName).arg(receivedFile->errorString()));  
    94.                 return;  
    95.             }  
    96.         }  
    97.         else  
    98.         {  
    99.             return;  
    100.         }  
    101.     }  
    102.   
    103.     //一个文件没有接受完  
    104.     if (bytesReceived < TotalRecvBytes)  
    105.     {  
    106.         //可用内容比整个文件长度-已接收长度短,则全部接收并写入文件  
    107.         qint64 tmp_Abailable = this->bytesAvailable();  
    108.         if(tmp_Abailable <= (TotalRecvBytes - bytesReceived))  
    109.         {  
    110.             bytesReceived += tmp_Abailable;  
    111.             totalBytesReceived += tmp_Abailable;  
    112.             inBlock = this->readAll();  
    113.             receivedFile->write(inBlock);  
    114.             inBlock.resize(0);  
    115.             tmp_Abailable = 0;  
    116.         }  
    117.         //可用内容比整个文件长度-已接收长度长,则接收所需内容,并写入文件  
    118.         else  
    119.         {  
    120.             inBlock = this->read(TotalRecvBytes - bytesReceived);  
    121.   
    122.             if(fileClass == filetype_wavfile)  
    123.             {  
    124.                 totalBytesReceived += (TotalRecvBytes - bytesReceived);  
    125.             }  
    126.             bytesReceived = TotalRecvBytes;  
    127.             receivedFile->write(inBlock);  
    128.             inBlock.resize(0);  
    129.             tmp_Abailable = 0;  
    130.         }  
    131.     }  
    132.   
    133.     emit updateProgressSignal(totalBytesReceived,totalRecvSize,num);   //更新发送进度信号  
    134.   
    135.   
    136.     //善后:一个文件全部收完则重置变量关闭文件流,删除指针  
    137.     if (bytesReceived == TotalRecvBytes)  
    138.     {  
    139.         //变量重置  
    140.         TotalRecvBytes = 0;  
    141.         bytesReceived = 0;  
    142.         fileNameSize = 0;  
    143.         receiveClass = 255;  
    144.         receivedFile->close();  
    145.         delete receivedFile;  
    146.   
    147.         //输出信息  
    148.         emit outputlog(tr("Have received file: %1 from client %2")  
    149.                        .arg(receivedFileName)  
    150.                        .arg(this->peerAddress().toString()));   //log information  
    151.   
    152.         //全部文件接收完成  
    153.         if(recvNum == recvFileNum)  
    154.         {  
    155.             //变量重置  
    156.             recvFileNum = 0;  
    157.             recvNum = 0;  
    158.             totalBytesReceived = 0;  
    159.             totalRecvSize = 0;  
    160.   
    161.             emit outputlog(tr("Receive all done!"));  
    162.         }  
    163.   
    164.         if(fileClass == filetype_result)  
    165.         {  
    166.             emit newClientInfo(tr("Evaluating"),num,4);  
    167.         }  
    168.     }  
    169. }  
    接收文件时,需要一步一步判断接收字节是否大于协议的下一项,若大于则再判断其值
    接收的文件类型必须是filetype_result
    未接收完记录接收进度
    接收完文件进行善后,关闭文件流删除指针等
    每进行完一步,向上层发送信号,包括客户端信息和接收进度
     


    更新客户端状态函数,向上层发送信号

    [cpp] view plain copy
    print?
    1. void testClient::newState(QAbstractSocket::SocketState state)    //新状态  
    2. {  
    3.     emit newClient(this->peerAddress().toString(), (int)state, num);  
    4. }  
    发送的信号参数为:该客户端IP,状态序号,客户端编号
     


    开始传输文件函数(发送包含文件信息的文件头)

    [cpp] view plain copy
    print?
    1. void testClient::startTransfer(const quint16 type)    //开始传输文件  
    2. {  
    3.     TotalSendBytes = 0;    //总共需发送的字节数  
    4.     bytesWritten = 0;       //已发送字节数  
    5.     bytesToWrite = 0;       //待发送字节数  
    6.   
    7.     //开始传输文件信号  
    8.     emit outputlog(tr("start sending file to client: %1\n filename: %2")  
    9.                    .arg(this->peerAddress().toString())  
    10.                    .arg(sendFileName));  
    11.   
    12.     sendFile = new QFile(sendFilePath);  
    13.     if (!sendFile->open(QFile::ReadOnly ))  //读取发送文件  
    14.     {  
    15.         QMessageBox::warning(NULL, tr("WARNING"),  
    16.                              tr("can not read file %1:\n%2.")  
    17.                              .arg(sendFilePath)  
    18.                              .arg(sendFile->errorString()));  
    19.         return;  
    20.     }  
    21.     TotalSendBytes = sendFile->size();  
    22.     QDataStream sendOut(&outBlock, QIODevice::WriteOnly);  
    23.     sendOut.setVersion(QDataStream::Qt_5_0);  
    24.   
    25.     //写入发送类型,数据大小,文件类型,文件名大小,文件名  
    26.     sendOut << quint8(0) << qint64(0) << quint16(0) << qint64(0) << sendFileName;  
    27.     TotalSendBytes +=  outBlock.size();  
    28.     sendOut.device()->seek(0);  
    29.     sendOut << quint8(sendtype_file)<< TotalSendBytes << quint16(type)  
    30.             << qint64((outBlock.size() - sizeof(qint64) * 2) - sizeof(quint16) - sizeof(quint8));  
    31.   
    32.     this->writeData(outBlock,outBlock.size());  
    33.     //this->flush();  
    34.     bytesToWrite = TotalSendBytes - outBlock.size();  
    35.   
    36.     outBlock.resize(0);  
    37. }  
    读取发送文件
    建立发送文件头
    用writeData将文件头写入TCP发送流,记录已发送字节数


    发送文件内容:
    [cpp] view plain copy
    print?
    1. void testClient::updateClientProgress(qint64 numBytes)  //发送文件内容  
    2. {  
    3.     if(TotalSendBytes == 0)  
    4.         return;  
    5.   
    6.     bytesWritten += (int)numBytes;  
    7.     proBarValue += (int)numBytes;  
    8.   
    9.     emit updateProgressSignal(proBarValue,proBarMax,num);   //更新发送进度信号  
    10.   
    11.     if (bytesToWrite > 0)  
    12.     {  
    13.         outBlock = sendFile->read(qMin(bytesToWrite, loadSize));  
    14.         bytesToWrite -= (int)this->writeData(outBlock,outBlock.size());  
    15.         outBlock.resize(0);  
    16.     }  
    17.     else  
    18.     {  
    19.         sendFile->close();  
    20.   
    21.         //结束传输文件信号  
    22.         if(TotalSendBytes < 1024)  
    23.         {  
    24.             emit outputlog(tr("finish sending file to client: %1\n filename: %2 %3B")  
    25.                            .arg(this->peerAddress().toString())  
    26.                            .arg(sendFileName)  
    27.                            .arg(TotalSendBytes));  
    28.         }  
    29.         else if(TotalSendBytes < 1024*1024)  
    30.         {  
    31.             emit outputlog(tr("finish sending file to client: %1\n filename: %2 %3KB")  
    32.                            .arg(this->peerAddress().toString())  
    33.                            .arg(sendFileName)  
    34.                            .arg(TotalSendBytes / 1024.0));  
    35.         }  
    36.         else  
    37.         {  
    38.             emit outputlog(tr("finish sending file to client: %1\n filename: %2 %3MB")  
    39.                            .arg(this->peerAddress().toString())  
    40.                            .arg(sendFileName)  
    41.                            .arg(TotalSendBytes / (1024.0*1024.0)));  
    42.         }  
    43.   
    44.             if(sendNum < openList.size())   //还有文件需要发送  
    45.             {  
    46.                 if(sendNum == 0)  
    47.                 {  
    48.                     //QFile::remove(sendFilePath);    //删除列表文件  
    49.                     proBarMax = totalSendSize;  
    50.                     proBarValue = 0;  
    51.                 }  
    52.                 sendFilePath = openList[sendNum].filePath;  
    53.                 sendFileName = openList[sendNum].fileName;  
    54.                 sendNum++;  
    55.                 startTransfer(filetype_wavfile);  
    56.             }  
    57.             else    //发送结束  
    58.             {  
    59.                 emit newClientInfo(tr("send complete"),num,4);  
    60.   
    61.                 TotalSendBytes = 0;    //总共需发送的字节数  
    62.                 bytesWritten = 0;       //已发送字节数  
    63.                 bytesToWrite = 0;       //待发送字节数  
    64.             }  
    65.     }  
    66. }  
    文件未发送完:记录发送字节数,writeData继续发送,writeData一旦写入发送流,自动又进入updateClientProgress函数
    文件已发送完:发出信号,检测是否还有文件需要发送,若有则调用startTransfer继续发送,若没有则发出信号,更新客户端信息
     


    准备传输文件函数,被上层调用,参数为发送文件列表:

    [cpp] view plain copy
    print?
    1. void testClient::prepareTransfer(std::vector<openFileStruct>& openFileList)    //准备传输文件  
    2. {  
    3.     if(openFileList.size() == 0)    //没有文件  
    4.     {  
    5.         return;  
    6.     }  
    7.   
    8.     testtype_now = testtype_t;  
    9.     isSendKeyword = false;  
    10.     for(int i=0; i<2; i++)  
    11.         isReceived[i] = false;  
    12.   
    13.     openList.clear();  
    14.     openList.assign(openFileList.begin(),openFileList.end());   //拷贝文件列表  
    15.   
    16.     QString sendFileListName = "sendFileList.txt";  
    17.     QString sendFileListPath = temp_Path + "/" + sendFileListName;  
    18.   
    19.     getSendFileList(sendFileListPath);     //在指定路径生成发送文件列表  
    20.   
    21.     emit newClientInfo(tr("sending test files"),num,4);   //更新主窗口测试阶段  
    22.         sendFilePath = sendFileListPath;  
    23.         sendFileName = sendFileListName;  
    24.         sendNum = 0;    //发送到第几个文件  
    25.   
    26.         proBarMax = getSendTotalSize();  
    27.         proBarValue = 0;  
    28.   
    29.         startTransfer(filetype_list);    //开始传输文件  
    30. }  
    拷贝文件列表
    生成发送文件列表文件
    更新主窗口信息
    开始传输列表文件
     


    上面调用的生成列表文件函数如下:

    [cpp] view plain copy
    print?
    1. void testClient::getSendFileList(QString path)     //在指定路径生成发送文件列表  
    2. {  
    3.     sendFileNum = openList.size();    //记录发送文件个数  
    4.     totalSendSize = 0;  //记录发送文件总大小  
    5.   
    6.     for(int i = 0; i < sendFileNum; i++)  
    7.     {  
    8.         sendFileName = openList[i].fileName;  
    9.         sendFilePath = openList[i].filePath;  
    10.   
    11.         totalSendSize += getSendTotalSize();  
    12.     }  
    13.   
    14.     FILE *fp;  
    15.     fp = fopen(path.toLocal8Bit().data(),"w");  
    16.   
    17.     fprintf(fp,"%d\n",sendFileNum);  
    18.     fprintf(fp,"%d\n",totalSendSize);  
    19.   
    20.     for(int i = 0; i < sendFileNum; i++)  
    21.     {  
    22.         fprintf(fp,"%s\n",openList[i].fileName.toLocal8Bit().data());  
    23.     }  
    24.   
    25.     fclose(fp);  
    26. }  


    被上面调用getSendTotalSize函数如下:

    [cpp] view plain copy
    print?
    1. quint64 testClient::getSendTotalSize()    //获取sendFilePath对应文件加上头的大小  
    2. {  
    3.     int totalsize;  
    4.     //计算列表文件及文件头总大小  
    5.     QFile *file = new QFile(sendFilePath);  
    6.     if (!file->open(QFile::ReadOnly ))  //读取发送文件  
    7.     {  
    8.         QMessageBox::warning(NULL, tr("WARNING"),  
    9.                              tr("can not read file %1:\n%2.")  
    10.                              .arg(sendFilePath)  
    11.                              .arg(file->errorString()));  
    12.         return 0;  
    13.     }  
    14.   
    15.     totalsize = file->size();  //文件内容大小  
    16.     QDataStream sendOut(&outBlock, QIODevice::WriteOnly);  
    17.     sendOut.setVersion(QDataStream::Qt_5_0);  
    18.     //写入发送类型,数据大小,文件类型,文件名大小,文件名  
    19.     sendOut << quint8(0) << qint64(0) << quint16(0) << qint64(0) << sendFileName;  
    20.     totalsize +=  outBlock.size();  //文件头大小  
    21.   
    22.     file->close();  
    23.   
    24.     outBlock.resize(0);  
    25.   
    26.     return totalsize;  
    27. }  



    3、testServer类
    一个testClient类实例为一个socket服务器端描述
    (1)类声明:

    [cpp] view plain copy
    print?
    1. class testServer : public QTcpServer  
    2. {  
    3.     Q_OBJECT  
    4. public:  
    5.     testServer(QObject *parent = 0);  
    6.     std::vector<testClientp> clientList;     //客户端tcp连接  
    7.     std::vector<QString> ipList;    //客户端ip  
    8.     int totalClient;  //客户端数  
    9.   
    10. protected:  
    11.     void incomingConnection(int socketDescriptor);  //虚函数,有tcp请求时会触发  
    12.   
    13. signals:  
    14.     void error(QTcpSocket::SocketError socketError);    //错误信号  
    15.     void newClientSignal(QString clientIP,int state,int threadNum);   //将新客户端信息发给主窗口  
    16.     void updateProgressSignal(qint64,qint64,int);   //更新发送进度信号  
    17.     void outputlogSignal(QString);  //发送日志消息信号  
    18.     void newClientInfoSignal(QString,int,int);    //更新客户端信息  
    19.   
    20. public slots:  
    21.     void newClientSlot(QString clientIP,int state,int threadNum);   //将新客户端信息发给主窗口  
    22.     void updateProgressSlot(qint64,qint64,int);   //更新发送进度槽  
    23.     void outputlogSlot(QString);        //发送日志消息槽  
    24.     void newClientInfoSlot(QString,int,int);      //更新客户端信息  
    25.   
    26. private:  
    27.     int getClientNum(testClientp socket); //检测用户,若存在,返回下标,若不存在,返回用户数  
    28. };  
    public:需要主窗口访问的变量
    incomingConnection:接收tcp请求
    signals:发送客户端信息的信号
    public slots:接收下层testClient信号的槽,并向上层主窗口发送信号
    private:检测用户是否存在的辅助函数
     


    (2)类定义:
    构造函数:

    [cpp] view plain copy
    print?
    1. testServer::testServer(QObject *parent) :  
    2.     QTcpServer(parent)  
    3. {  
    4.     totalClient = 0;  
    5.     clientList.clear();  
    6.     ipList.clear();  
    7. }  



    接收tcp请求函数:

    [cpp] view plain copy
    print?
    1. void testServer::incomingConnection(int socketDescriptor)  
    2. {  
    3.     testClient *socket = new testClient(this);  
    4.   
    5.     if (!socket->setSocketDescriptor(socketDescriptor))  
    6.     {  
    7.         QMessageBox::warning(NULL,"ERROR",socket->errorString());  
    8.         emit error(socket->error());  
    9.         return;  
    10.     }  
    11.   
    12.     int num = getClientNum(socket); //检测用户,若存在,返回下标,若不存在,返回用户数  
    13.     socket->num = num;  //记录序号  
    14.   
    15.     emit newClientSignal(socket->peerAddress().toString(),(int)socket->state(),num);   //将新客户端信息发给主窗口  
    16.   
    17.     //连接信号和槽  
    18.     connect(socket, SIGNAL(newClient(QString,int,int)), this, SLOT(newClientSlot(QString,int,int)));  
    19.     connect(socket, SIGNAL(outputlog(QString)), this, SLOT(outputlogSlot(QString)));  
    20.     connect(socket, SIGNAL(updateProgressSignal(qint64,qint64,int)),  
    21.             this, SLOT(updateProgressSlot(qint64,qint64,int)));  
    22.     connect(socket, SIGNAL(newClientInfo(QString,int,int)),  
    23.             this, SLOT(newClientInfoSlot(QString,int,int)));  
    24.     connect(socket, SIGNAL(readyToTest(int,int)), this, SLOT(readyToTestSlot(int,int)));  
    25.     connect(socket, SIGNAL(startEvaluate(int,QString)), this, SLOT(startEvaluateSlot(int,QString)));  
    26.     connect(socket, SIGNAL(evaluateComplete(int,QString,int)),  
    27.             this, SLOT(evaluateCompleteSlot(int,QString,int)));  
    28.   
    29.     if(num == totalClient)  
    30.         totalClient++;  
    31. }  
    通过setSocketDescriptor获取当前接入的socket描述符
    检测用户是否存在,记录序号
    向主窗口发送信号,连接下层testClient信号和接收槽
    更新客户端总数


     
    检测用户是否存在的函数

    [cpp] view plain copy
    print?
    1. int testServer::getClientNum(testClientp socket) //检测用户,若存在,返回下标,若不存在,加入列表  
    2. {  
    3.     for(int i = 0; i < ipList.size(); i++)  
    4.     {  
    5.         qDebug() << "No." << i << "ip: " << ipList[i];  
    6.   
    7.         if(ipList[i] == socket->peerAddress().toString())  
    8.         {  
    9.             clientList[i] = socket;  
    10.             return i;  
    11.         }  
    12.     }  
    13.   
    14.     clientList.push_back(socket);   //存入客户列表  
    15.     ipList.push_back(socket->peerAddress().toString()); //存入ip列表  
    16.     return totalClient;  
    17. }  
    其他发送信号的函数均为连接下层和上层的中转,由于比较简单就不赘述了
     
     
    4、主窗口
    在主窗口(继承自QMainWindow)绘制所需控件:
    lineEdit_ipaddress:显示服务器IP
    lineEdit_port:设置服务器端口
    listWidget_sendwav:发送文件列表
    tableWidget_clientlist:客户端信息列表,显示所有客户端IP,接入状态,发送/接收进度条
    textEdit_status:状态栏
    pushButton_addwav:添加发送文件按钮
    pushButton_deletewav:删除发送文件按钮
    pushButton_startserver:开启/关闭服务器按钮
    pushButton_senddata:发送数据按钮
    pushButton_deleteclient:删除客户端按钮


    主窗口需定义的变量
    [cpp] view plain copy
    print?
    1. testServer *tcpServer;  //tcp服务器指针  
    2. bool isServerOn;    //服务器是否开启  
    3. std::vector<openFileStruct> openFileList; //存储目录和文件名对  


    开启/关闭服务器按钮

    [cpp] view plain copy
    print?
    1. void testwhynot::on_pushButton_startserver_clicked()    //开启服务器  
    2. {  
    3.     if(isServerOn == false)  
    4.     {  
    5.         tcpServer = new testServer(this);  
    6.   
    7.         ui->comboBox_testtype->setEnabled(false);   //开启服务器后不能更改测试类型  
    8.         ui->pushButton_addwav->setEnabled(false);   //不能更改wav列表  
    9.         ui->pushButton_deletewav->setEnabled(false);  
    10.   
    11.         //监听本地主机端口,如果出错就输出错误信息,并关闭  
    12.         if(!tcpServer->listen(QHostAddress::Any,ui->lineEdit_port->text().toInt()))  
    13.         {  
    14.             QMessageBox::warning(NULL,"ERROR",tcpServer->errorString());  
    15.             return;  
    16.         }  
    17.         isServerOn = true;  
    18.         ui->pushButton_startserver->setText(tr("close server"));  
    19.   
    20.         //显示到状态栏  
    21.         ui->textEdit_status->append(tr("%1 start server: %2:%3")  
    22.                                     .arg(getcurrenttime())  
    23.                                     .arg(ipaddress)  
    24.                                     .arg(ui->lineEdit_port->text()));  
    25.         //链接错误处理信号和槽  
    26.         //connect(this,SIGNAL(error(int,QString)),this,SLOT(displayError(int,QString)));  
    27.   
    28.         //处理多客户端,连接从客户端线程发出的信号和主窗口的槽  
    29.         //连接客户端信息更改信号和槽  
    30.         connect(tcpServer,SIGNAL(newClientSignal(QString,int,int)),this,SLOT(acceptNewClient(QString,int,int)));  
    31.         //连接日志消息信号和槽  
    32.         connect(tcpServer,SIGNAL(outputlogSignal(QString)),  
    33.                 this,SLOT(acceptOutputlog(QString)));  
    34.         //连接更新发送进度信号和槽  
    35.         connect(tcpServer, SIGNAL(updateProgressSignal(qint64,qint64,int)),  
    36.                 this, SLOT(acceptUpdateProgress(qint64,qint64,int)));  
    37.         //连接更新客户端信息列表信号和槽  
    38.         connect(tcpServer, SIGNAL(newClientInfoSignal(QString,int,int)),  
    39.                 this, SLOT(acceptNewClientInfo(QString,int,int)));  
    40.   
    41.         //显示到状态栏  
    42.         ui->textEdit_status->append(tr("%1 wait for client...")  
    43.                                     .arg(getcurrenttime()));  
    44.     }  
    45.     else  
    46.     {  
    47.         isServerOn = false;  
    48.         ui->pushButton_startserver->setText(tr("start server"));  
    49.   
    50.         //断开所有客户端连接,发出disconnected()信号  
    51.          for(int i=0; i<tcpServer->clientList.size(); i++)  
    52.         {  
    53.   
    54.             if(ui->tableWidget_clientlist->item(i,2)->text() == clientStatus[3])    //处于连接状态才断开,否则无法访问testClientp指针  
    55.               {  
    56.                 testClientp p = tcpServer->clientList[i];  
    57.                    p->close();  
    58.             }  
    59.         }  
    60.   
    61.         //清空列表  
    62.          tcpServer->clientList.clear();  
    63.         tcpServer->ipList.clear();  
    64.         clientInfoList.clear();  
    65.         for(int i=0; i<ui->tableWidget_clientlist->rowCount(); )  
    66.             ui->tableWidget_clientlist->removeRow(i);  
    67.   
    68.         tcpServer->close(); //关闭服务器  
    69.   
    70.         ui->textEdit_status->append(tr("%1 clost server.").arg(getcurrenttime()));  
    71.   
    72.         ui->comboBox_testtype->setEnabled(true);    //可以重新选择测试类型  
    73.         ui->pushButton_addwav->setEnabled(true);   //能更改wav列表  
    74.         ui->pushButton_deletewav->setEnabled(true);  
    75.   
    76.         //按钮无效化  
    77.         ui->pushButton_senddata->setEnabled(false);  
    78.         ui->pushButton_starttest->setEnabled(false);  
    79.         ui->pushButton_deleteclient->setEnabled(false);  
    80.     }  
    81. }  
    注意:
    1、listen(“监听类型”,“端口号”) 用于开启服务器,在指定端口监听客户端连接
    2、connect连接了客户端更新信息的信号,来自下层testServer类


    发送数据按钮
    [cpp] view plain copy
    print?
    1. void testwhynot::on_pushButton_senddata_clicked()   //发送数据  
    2. {  
    3.   
    4.     //多客户端发送数据  
    5.     if(ui->comboBox_testtype->currentIndex() == testtype_keyword)  
    6.     {  
    7.         if(!check_file(keyword_filepath))  
    8.             return;  
    9.     }  
    10.   
    11.     vector<clientInfo>::iterator it;  
    12.   
    13.     //遍历所有客户端信息,确保都处于“已连接”状态  
    14.     for(it=clientInfoList.begin(); it!=clientInfoList.end(); it++)  
    15.     {  
    16.         if((*it).state != 3)   //有客户端未处于“已连接”状态  
    17.         {  
    18.             QMessageBox::warning(this,tr("WARNING"),tr("client %1 is not connected.").arg((*it).ip));  
    19.             return;  
    20.         }  
    21.     }  
    22.   
    23.     //没有文件  
    24.     if(openFileList.size() == 0)  
    25.     {  
    26.         QMessageBox::warning(NULL,tr("WARNING"),tr("no file! can not send!"));  
    27.         return;  
    28.     }  
    29.   
    30.     for(int i = 0; i < tcpServer->clientList.size(); i++)  
    31.     {  
    32.         tcpServer->clientList[i]->prepareTransfer(openFileList,ui->comboBox_testtype->currentIndex());  
    33.     }  
    34.   
    35.     //按钮无效化  
    36.     ui->pushButton_senddata->setEnabled(false);  
    37. }  
    调用底层prepareTransfer函数开始传输


    删除客户端按钮
    [cpp] view plain copy
    print?
    1. void testwhynot::on_pushButton_deleteclient_clicked()  
    2. {  
    3.     QList<QTableWidgetItem*> list = ui->tableWidget_clientlist->selectedItems();   //读取所有被选中的item  
    4.     if(list.size() == 0)    //没有被选中的就返回  
    5.     {  
    6.         QMessageBox::warning(this,QObject::tr("warning"),QObject::tr("please select a test client"));  
    7.         return;  
    8.     }  
    9.   
    10.     std::set<int> del_row;   //记录要删除的行号,用set防止重复  
    11.   
    12.     for(int i=0; i<list.size(); i++)    //删除选中的项  
    13.     {  
    14.         QTableWidgetItem* sel = list[i]; //指向选中的item的指针  
    15.         if (sel)  
    16.         {  
    17.             int row = ui->tableWidget_clientlist->row(sel);   //获取行号  
    18.             del_row.insert(row);  
    19.         }  
    20.     }  
    21.   
    22.     std::vector<int> del_list;  //赋值给del_list,set本身为有序  
    23.     for(std::set<int>::iterator it=del_row.begin(); it!=del_row.end(); it++)  
    24.     {  
    25.         del_list.push_back(*it);  
    26.     }  
    27.   
    28.     for(int i=del_list.size()-1; i>=0; i--)    //逆序遍历  
    29.     {  
    30.         testClientp p = tcpServer->clientList[del_list[i]];  
    31.         p->close();  
    32.   
    33.         ui->tableWidget_clientlist->removeRow(del_list[i]);   //从显示列表中删除行  
    34.         clientInfoList.erase(clientInfoList.begin() + del_list[i]); //从内部列表删除  
    35.         tcpServer->ipList.erase(tcpServer->ipList.begin() + del_list[i]);  
    36.         tcpServer->clientList.erase(tcpServer->clientList.begin() + del_list[i]);  
    37.         tcpServer->totalClient--;  
    38.     }  
    39.   
    40.     for(int i=0; i<tcpServer->clientList.size(); i++)  
    41.     {  
    42.         tcpServer->clientList[i]->num = i;  
    43.     }  
    44.   
    45.     if(clientInfoList.empty())  //没有客户端,删除按钮无效化  
    46.     {  
    47.         ui->pushButton_deleteclient->setEnabled(false);  
    48.     }  
    49. }  
    删除客户端较麻烦,由于支持多选删除,需要先将选中的行号排序、去重、从大到小遍历删除(防止删除后剩余行号变化)
    不仅要关闭客户端,还要将其从显示列表和内部列表删除,保持显示和实际列表同步




    上述便是基于tcp传输的发送/接收服务器端的搭建,客户端只需遵从上述的发送协议搭建即可,客户端发送与接收与服务器基本相同,也不赘述了。
     
    本文偏重于工程实现,Qt的TCP传输原理叙述不多,若要深入了解qt套接字编程,请参考:http://cool.worm.blog.163.com/blog/static/64339006200842922851118/


    0
    0
     
     

    我的同类文章

    http://blog.csdn.net
    • Qt5 更新翻译2013-08-23
    • Qt5 在表格中加入控件2013-07-17
    • Qt5 在控件中绘图2013-05-31
    • Qt5 error LNK2019 无法解析的外部符号 解决办法2013-05-30
    • Qt5 写DOS格式文件2013-07-24
    • Qt5 在win7上发布 & 打包依赖dll生成exe方法2013-06-18
    • Qt5绘制wav波形图2013-05-30
    0 0
    原创粉丝点击