• 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
    save 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:
    %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
     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:
    %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
     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
     filename: %2 %3KB")
                               .arg(this->peerAddress().toString())
                               .arg(sendFileName)
                               .arg(TotalSendBytes / 1024.0));
            }
            else
            {
                emit outputlog(tr("finish sending file to client: %1
     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
    ",sendFileNum);
        fprintf(fp,"%d
    ",totalSendSize);
    
        for(int i = 0; i < sendFileNum; i++)
        {
            fprintf(fp,"%s
    ",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:
    %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/


  • 相关阅读:
    新装CentOS7用yum安装软件提示 cannot find a valid baseurl for repobase7x86_64
    新手快速上手webpack4打包工具的使用详解
    2种方法教你,如何将exe注册为windows服务,直接从后台运行
    如何部署前端DIST目录
    安装了vs2019 编译node-sass node-gyp 找不到编译器的解决方法
    node-sass安装失败之npm ERR! Failed at the node-sass@4.9.4 postinstall script.
    Node.js安装及环境配置之Windows篇
    npm源切换,从淘宝源切换至npm源
    找不到Python问题解决 (转)
    redisson
  • 原文地址:https://www.cnblogs.com/james1207/p/3283536.html
Copyright © 2020-2023  润新知