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

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

标签: Qt服务器客户端文件传输
4484人阅读 评论(2) 收藏 举报
本文章已收录于:
分类:

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

  1. testClient *socket = new testClient(this);  
  2.   
  3. if (!socket->setSocketDescriptor(socketDescriptor))  
  4. {  
  5.     emit error(socket->error());  
  6.     return;  
  7. }  
testClient *socket = new testClient(this);

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

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

  1. testClient *a;  
  2. a->writeData("message",8);  
testClient *a;
a->writeData("message",8);

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

  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);  
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()信号

  1. connect(this, SIGNAL(bytesWritten(qint64)), this, SLOT(updateClientProgress(qint64)));  
connect(this, SIGNAL(bytesWritten(qint64)), this, SLOT(updateClientProgress(qint64)));

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


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

  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());  
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()信号为新连接中有可读数据时发出

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

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

  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. }  
QDataStream in(a); //本地数据流
in.setVersion(QDataStream::Qt_5_0); //设置流版本,以防数据格式错误
quint8 receiveClass;
if(this->bytesAvailable() >= (sizeof(quint8)))
{    
    in >> receiveClass;
}

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

  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);  
QByteArray inBlock;
inBlock = a->readAll();	//读入所有数据
QFile receivedFile = new QFile(receiveFilePath);	//打开接收文件
if (!receivedFile->open(QFile::WriteOnly ))
{    return;}
QFile receivedFile->write(inBlock);	//写入文件
inBlock.resize(0);


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

  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")};  
//存储文件路径和文件名的结构体
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)类声明:

  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. };  
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)类定义:
构造函数:

  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. }  
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) 已写入发送消息流信号
 


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

  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. }  
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
未接收完记录接收进度
接收完文件进行善后,关闭文件流删除指针等
每进行完一步,向上层发送信号,包括客户端信息和接收进度
 


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

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


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

  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. }  
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发送流,记录已发送字节数


发送文件内容:
  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. }  
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继续发送,若没有则发出信号,更新客户端信息
 


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

  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. }  
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);    //开始传输文件
}
拷贝文件列表
生成发送文件列表文件
更新主窗口信息
开始传输列表文件
 


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

  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. }  
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函数如下:

  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. }  
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)类声明:

  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. };  
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)类定义:
构造函数:

  1. testServer::testServer(QObject *parent) :  
  2.     QTcpServer(parent)  
  3. {  
  4.     totalClient = 0;  
  5.     clientList.clear();  
  6.     ipList.clear();  
  7. }  
testServer::testServer(QObject *parent) :
    QTcpServer(parent)
{
    totalClient = 0;
    clientList.clear();
    ipList.clear();
}



接收tcp请求函数:

  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. }  
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信号和接收槽
更新客户端总数


 
检测用户是否存在的函数

  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. }  
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:删除客户端按钮


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


开启/关闭服务器按钮

  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. }  
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类


发送数据按钮
  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. }  
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函数开始传输


删除客户端按钮
  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. }  
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/


0
0
 
 
我的同类文章
http://blog.csdn.net
  • 5
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 23
    评论
### 回答1: Qt 是一个常用的跨平台应用框架,支持多种操作系统,可以用来开发客户端服务器应用程序。要实现客户端服务器的消息发送文件传输,需要使用 Qt 的网络模块和文件IO模块。 首先,要实现客户端服务器之间的消息发送,可以使用 Qt 的套接字接口来进行网络通信。在客户端程序中,可以使用 QTcpSocket 类来创建一个 TCP 套接字,与服务器建立连接,并通过这个套接字发送消息。而在服务器程序中,可以使用 QTcpServer 类来创建一个监听套接字,接受客户端的连接,并通过这个套接字接收和处理客户端发送的消息。 对于文件传输,可以借助于 Qt文件IO模块来实现。在客户端程序中,可以使用 QFile 类来打开要传输文件,并使用 QTcpSocket 的 write() 函数将文件内容发送服务器。而在服务器程序中,可以使用 QTcpSocket 的 read() 函数接收客户端发送文件内容,并使用 QFile 类的 write() 函数将内容保存到服务器文件系统中。 为了确保消息发送文件传输的可靠性,可以使用 Qt 的信号和槽机制来实现。客户端可以通过监视套接字的 connected() 和 disconnected() 信号来判断与服务器的连接状态,并通过 error() 信号来处理连接错误。而服务器可以通过监听套接字的 newConnection() 信号来接受新的客户端连接,并通过 readyRead() 信号来接收客户端发送的消息和文件内容。 此外,为了提高消息发送文件传输的效率,可以使用 Qt 的多线程机制来实现并发处理。可以将套接字通信和文件IO操作放在独立的线程中进行,以避免阻塞主线程的运行。 总之,通过使用 Qt 的网络模块和文件IO模块,我们可以很方便地实现客户端服务器之间的消息发送文件传输。这种实现方式简单易懂,同时也具备可靠性和高效性。 ### 回答2: Qt 是一个跨平台的 C++ 应用程序开发框架,提供了丰富的类库和工具来简化开发过程。要实现客户端服务器之间的消息发送文件传输,我们可以使用 Qt 中的网络模块和文件操作模块。 对于消息发送,可以使用 Qt 的套接字类来实现。客户端可以创建一个套接字并连接到指定的服务器。通过套接字的 write 方法可以将消息发送服务器,而服务器可以通过套接字的 read 方法来接收客户端发送的消息。这样就实现了客户端服务器之间的消息通信。 对于文件传输,也可以使用套接字类来实现。客户端可以将要发送文件读取到内存中,并使用套接字的 write 方法将文件数据发送服务器服务器可以使用套接字的 read 方法接收客户端发送文件数据,并将接收到的文件数据保存到指定位置。这样就实现了客户端服务器传输文件的功能。 在实现消息发送文件传输的过程中,需要注意以下几点: 1. 需要使用正确的 IP 地址或域名和端口号来建立客户端服务器之间的连接。 2. 如果发送的消息或文件较大,可以考虑对数据进行分块传输,以便提高传输效率。 3. 在接收文件时,可以使用文件打开和写入操作的错误处理来确保传输的完整性。 总之,使用 Qt 的网络模块和文件操作模块,我们可以方便地实现客户端服务器之间的消息发送文件传输功能。通过合理的设计和编码,可以实现高效可靠的通信。 ### 回答3: Qt是一个流行的跨平台应用程序开发框架,它提供了各种库和工具来简化软件开发。要实现客户端服务器之间的消息发送文件传输,我们可以使用Qt中的网络编程和文件操作模块。 在客户端服务器之间建立网络通信,我们可以使用Qt中的Qt Network模块。该模块提供了一种简单且高效的方式来创建TCP/IP网络连接并进行数据传输。我们可以使用QTcpSocket类在客户端服务器之间发送消息。通过在客户端上创建一个QTcpSocket对象并使用其connectToHost()方法连接服务器,我们可以发送接收消息。使用QTcpSocket的write()方法可以发送消息,而readyRead()信号可以用于接收并处理服务器发送的消息。 对于文件传输,我们可以使用Qt文件操作模块进行处理。我们可以使用QFile类打开和读写文件,以及QFileInfo类获取有关文件的信息。为了在客户端服务器之间传输文件,我们可以将文件分割为较小的数据块,并将每个数据块发送服务器。在服务器端,我们可以接收这些数据块并将它们重新组合成原始的文件。我们还可以使用QAbstractSocket类的readyRead()信号来处理文件传输过程中的读取操作。 此外,Qt还提供了不同类型的网络协议(如HTTP、FTP等)的支持。这使得我们在客户端服务器之间进行文件传输的方式更加灵活和多样化。例如,我们可以使用QHttp类来实现基于HTTP协议的文件传输,或者使用QFtp类来实现基于FTP协议的文件传输。 综上所述,使用Qt的网络编程和文件操作模块,我们可以轻松地实现客户端服务器之间的消息发送文件传输。这使得我们能够构建出功能强大且高效的网络应用程序。
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值