Qt中的TCP/UDP编程

作者在 2015-08-30 14:58:21 发布以下内容

原文来自:http://blog.csdn.net/yuxikuo_1/article/details/32980193

网络编程,OSI(开放式系统互联参考模型)七层参考模型:应用层、表示层、会话层、传输层、网络层、数据链路层、物理层。


    套接字(Socket)是网络通信的基本构建模块,又分为流式套接字(Stream Socket)和数据报套接字(Datagram Socket)两种类型的套接字。
    TCP:传送控制协议(Transmission Control Protocol),这是一种提供给用户的可靠的全双工字节流面向连接的协议。
    UDP:用户数据报协议(User Datagram Protocol),这是提供给用户进程的无连接协议,用于传送数据而不执行正确性检查。
    当然TCP、UDP都归属于传输层协议。

    对所用的网络知识简短的介绍,下面步入正题,开始Qt套接字编程~

    在TCP/IP网络中两个进程间的相互作用的主要模式是客户机/服务器模式(Client/Server model),是构造分布式应用程序最常用的模式。
    Qt中几乎所有的QtNetwork类都是异步的,一般情况下没有必要Socket使用在多线程中。

■、UDP
    UDP是不可信赖的,它是基于包的协议。一些应用程序层的协议使用UDP是因为它比TCP更加小巧,数据是从一个主机到另一个主机以包的形式发送的。这里没有连接到的概念,并且如果一个UDP包没有被正确交付,它不会向系统报告任何错误。
    下面写一个简单的广播示例,由客户端和服务器两部分组成。

//客户端发送数据
void Client::sendDatagram()
{
    QByteArray datagram;
    QDataStream out(&datagram, QIODevice::WriteOnly);
    out.setVersion(QDataStream::Qt_4_3);
    out << QDateTime::currentDateTime() << "vic.MINg!" << 3.14;

    QUdpSocket udpSocket(this);
    udpSocket.writeDatagram(datagram, QHostAddress::Broadcast, 1981);
}

    在QByteArray型局部变量datagram中构建待发送的数据包,然后通过QUdpSocket类的 writeDatagram ( const QByteArray & datagram, const QHostAddress & host, quint16 port );函数将数据包发出。值得注意的是,这里的地址使用了QHostAddress::Broadcast值,它对应IPv4下的广播地址,如果将该值更换 成单机地址(如本机地址QHostAddress::LocalHost),将变成一个普通的点对点的UDP程序。

//服务器接收数据
void Server::initSocket()
{
    udpSocket = new QUdpSocket(this);
    udpSocket->bind(1981);

    connect(udpSocket, SIGNAL(readyRead()),
            this, SLOT(readPendingDatagrams()));
}

    初始化生成QUdpSocket实例,并绑定与客户端约定的端口(1981)。这里多说几句,在编写网络程序时应该使用1024以上的端口号,1024以下的端口号通常被系统保留,紧密的绑定了一些服务(如80端口是http服务、21端口是ftp服务)。

void Server::readPendingDatagrams()
{
    while (udpSocket->hasPendingDatagrams()) {
        QByteArray datagram;
        datagram.resize(udpSocket->pendingDatagramSize());
        QHostAddress sender;
        quint16 senderPort;

        udpSocket->readDatagram(datagram.data(), datagram.size(),
                                 &sender, &senderPort);
        QDateTime dateTime;
        QString name;
        double data;
        QDataStream in(&datagram, QIODevice::ReadOnly);
        in.setVersion(QDataStream::Qt_4_3);
        in >> dateTime >> name >> data;
    }
}

    接受数据函数首先调用QUdpSocket类的成员函数hasPendingDatagrams()以判断是否有可供读取的数据。如果有则通过 pendingDatagramSize()获取当前可供读取的UDP报文大小,并据此大小分配接收缓冲区,最后读取相应数据。


■、TCP
    TCP是一个基于流的协议。对于应用程序,数据表现为一个长长的流,而不是一个大大的平面文件。基于TCP的高层协议通常是基于行的或者基于块的。
      ●、基于行的协议把数据作为一行文本进行传输,每行都以一个换行符结尾。
      ●、基于块的协议把数据作为二进制块进行传输,每块是由一个size大小字段和紧跟它的一个size字节的数据组成。
    QTcpSocket通过器父类QAbstractSocket继承了QIODevice,因此他可以通过使用QTextStream和QDataStream来进行读取和写入。
    QTcpServer类在服务器端处理来自TCP客户端连接数据,需要注意的是,该类直接继承于QObject基类,而不是QAbstractSocket抽象套接字类。

    下面介绍一个TCP应用示例,示例来自《精通Qt4编程》,感觉十分不错,它也是由客户端和服务器两部分组成,客户端选择本地文件,并通过TCP连接将它上传到服务器端。
    由于使用了TCP协议,所以可以轻松的传递大文件,而无需担心传输过程造成文件损坏。
    其中客户端程序SendFile从本地文件系统中选中一个已有文件并在成功连接服务器后开始发送,服务器端程序ReceiveFile则将该文件保存在当前目录下,两端均以进度条和数据两种形式分别显示文件传输进度和详细的数据传输字节数。
    客户端程序SendFile的用户界面是一个简单的对话框,上面布置一个QProgressBar进度条,一个用于显示状态的QLabel,三个QPushButton按钮,分别用来选择文件、发送文件和退出程序。
    Qt的QFileDialog类提供了一个文件选择对话框,用户使用它可以很容易的进行目录或文件的选择。
    下面将Dialog类部分代码陈列出来,它是QDialog的子类,实现客户端的全部功能。

class Dialog : public QDialog
{
    Q_OBJECT

public:
    Dialog(QWidget *parent = 0);

public slots:
    void start();
    void startTransfer();
    void updateClientProgress(qint64 numBytes);
    void displayError(QAbstractSocket::SocketError socketError);
    void openFile();

private:
    QProgressBar *clientProgressBar;
    QLabel *clientStatusLabel;
    QPushButton *startButton;
    QPushButton *quitButton;
    QPushButton *openButton;
    QDialogButtonBox *buttonBox;

    QTcpSocket tcpClient;      //客户端套接字
    qint64  TotalBytes;        //总共需发送的字节数
    qint64  bytesWritten;      //已发送字节数
    qint64  bytesToWrite;      //待发送字节数
    qint64  loadSize;          //被初始化为一个4Kb的常量
    QString fileName;          //待发送的文件的文件名
    QFile *localFile;          //待发送的文件
    QByteArray outBlock;       //缓存一次发送的数据
};

    为了发送较大的文件,变量使用了qint64类型,Qt保证该类型数据在所有其所支持的平台下均为64位大小,这几乎可以表示一个无限大的文件了。
    loadSize用来尽可能的将一个较大的文件分割,每次发送4Kb大小,余下不足4Kb的按实际大小发送。

Dialog::Dialog(QWidget *parent)
    : QDialog(parent)
{
    loadSize = 4*1024;     // 4Kb
    TotalBytes = 0;
    bytesWritten = 0;
    bytesToWrite = 0;
    clientProgressBar = new QProgressBar;
    clientStatusLabel = new QLabel(tr("客户端就绪"));
    startButton = new QPushButton(tr("开始"));
    quitButton = new QPushButton(tr("退出"));
    openButton = new QPushButton (tr("打开"));
    startButton->setEnabled(false);
    buttonBox = new QDialogButtonBox;
    buttonBox->addButton(startButton, QDialogButtonBox::ActionRole);
    buttonBox->addButton(openButton, QDialogButtonBox::ActionRole);
    buttonBox->addButton(quitButton, QDialogButtonBox::RejectRole);

    connect(startButton, SIGNAL(clicked()), this, SLOT(start()));
    connect(quitButton, SIGNAL(clicked()), this, SLOT(close()));
    connect(openButton, SIGNAL(clicked()), this, SLOT(openFile()));
    connect(&tcpClient, SIGNAL(connected()), this, SLOT(startTransfer()));
    connect(&tcpClient, SIGNAL(bytesWritten(qint64)),
            this, SLOT(updateClientProgress(qint64)));
    connect(&tcpClient, SIGNAL(error(QAbstractSocket::SocketError)),
            this, SLOT(displayError(QAbstractSocket::SocketError)));

    QVBoxLayout *mainLayout = new QVBoxLayout;
    mainLayout->addWidget(clientProgressBar);
    mainLayout->addWidget(clientStatusLabel);
    mainLayout->addStretch(1);
    mainLayout->addSpacing(10);
    mainLayout->addWidget(buttonBox);
    setLayout(mainLayout);
    setWindowTitle(tr("发送文件"));
}   

    这里关联了QTcpSocket的三个重要信号,它们分别是成功与服务器建立连接后产生的connected()信号,数据成功发送后产生的bytesWritten()信号和产生错误的error()信号。

void Dialog::openFile()
{
    fileName = QFileDialog::getOpenFileName(this);
        if (!fileName.isEmpty())
            startButton->setEnabled(true);     
}

    用户在客户端界面按下"打开"按钮后,openFile()槽函数将被调用。该函数通过Qt文件选择对画框QFileDialog所提供的静态函数 getOpenFileName(),能够很容易地返回用户所选取的文件名,这里将其保存在私有成员变量fileName中。如果选中返回的文件名非空, 将激活"开始"按钮。

void Dialog::start()
{
    startButton->setEnabled(false);
    QApplication::setOverrideCursor(Qt::WaitCursor);
    bytesWritten = 0;
    clientStatusLabel->setText(tr("连接中..."));
    tcpClient.connectToHost(QHostAddress::LocalHost, 16689);
}

    用户在客户端界面按下"开始"按钮后,start()槽函数将被调用。该函数的主要功能是连接服务器,它使用了QTcpSocket类的connectToHost()函数,其中的两个参数分别是服务器主机地址及其监听端口,读者可以根据实际应用需求进行修改。

void Dialog::startTransfer()
{
    localFile = new QFile(fileName);
    if (!localFile->open(QFile::ReadOnly )) {
        QMessageBox::warning(this, tr("应用程序"),
                             tr("无法读取文件 %1:\n%2.")
                             .arg(fileName)
                             .arg(localFile->errorString()));
        return;
    }
    TotalBytes = localFile->size();
    QDataStream sendOut(&outBlock, QIODevice::WriteOnly);
    sendOut.setVersion(QDataStream::Qt_4_3);
    
    QString currentFile = fileName.right(fileName.size() - fileName.lastIndexOf('/') - 1);
    sendOut << qint64(0) << qint64(0) << currentFile;
    TotalBytes +=  outBlock.size();
    sendOut.device()->seek(0);
    sendOut << TotalBytes << qint64((outBlock.size() - sizeof(qint64) * 2));
    bytesToWrite = TotalBytes - tcpClient.write(outBlock);
    clientStatusLabel->setText(tr("已连接"));
    qDebug() << currentFile << TotalBytes;
    outBlock.resize(0);
}

    一旦连接建立成功,QTcpSocket类将发出connected()消息,继而调用startTransfer()槽函数。该函数首先向服务器端发送一个文件头结构。
    文件头结构由三个字段组成,分别是64位的总长度(包括文件数据长度和文件头自身长度),64位的文件名长度和文件名。
    函数startTransfer()首先以只读方式打开选中的文件,然后通过QFile类的size()函数获取待发送文件的大小,并将该值暂存于TotalBytes变量中。
    接下来将发送缓冲区outBlock封装在一个QDataStream类型的变量中,这样做可以很方便的通过重载的"<<"操作符填写文件头结构。
    设置文件头结构的操作有些小技巧,这里首先通过QString类的right()函数去掉文件的路径部分,仅将文件部分保存在currentFile变量 中,然后通过sendOut << qint64(0) << qint64(0) << currentFile操作构造一个临时的文件头,将该值追加到TotalBytes字段,从而完成实际需发送字节数的记录。
    接着通过sendOut.device()->seek(0)函数将读写操作指向从头开始,并且调用类似操作sendOut << TotalBytes << qint64((outBlock.size() - sizeof(qint64) * 2)),填写实际的总长度和文件长度。
    需要注意的是,不能错误地通过QString::size()函数获取文件名的大小,该函数返回的是QString类型文件名所包含的字节数,而不是实际所占存储空间的大小,由于字节编码和QString类存储管理的原因,两者往往并不相等。

    完成了文件头结构的填写后,调用tcpClient.write(outBlock)函数将该文件头发出,同时修改待发送字节数bytesToWrite。最后,调用outBlock.resize(0)函数清空发送缓冲区以备下次使用。

void Dialog::updateClientProgress(qint64 numBytes)
{
    bytesWritten += (int)numBytes;
    if (bytesToWrite > 0) {
     outBlock = localFile->read(qMin(bytesToWrite, loadSize));
  bytesToWrite -= (int)tcpClient.write(outBlock);
 outBlock.resize(0);    
    }
    else{  
     localFile->close();
    }
    clientProgressBar->setMaximum(TotalBytes);
    clientProgressBar->setValue(bytesWritten);
    clientStatusLabel->setText(tr("已发送 %1MB").arg(bytesWritten / (1024 * 1024)));
}

    一旦数据发出,QTcpSocket类将会产生bytesWritten()信号,继而调用updateClientProgress(qint64)槽 函数,参数表示实际已发出的字节数。如果待发送数据计数bytesToWritten大于0,将尽可能地从发送文件中读取4Kb数据,并将其发送,否则发 送完毕关闭文件。还需要在此更新亦发和待发数据计数,并以此更新发送进度条和状态显示。

void Dialog::displayError(QAbstractSocket::SocketError socketError)
{
    if (socketError == QTcpSocket::RemoteHostClosedError)
        return;

    QMessageBox::information(this, tr("网络"),
        tr("产生如下错误: %1.").arg(tcpClient.errorString()));

    tcpClient.close();
    clientProgressBar->reset();
    clientStatusLabel->setText(tr("客户端就绪"));
    startButton->setEnabled(true);
    QApplication::restoreOverrideCursor();
}

    如果连接或数据传输过程中的某次操作发生错误,QTcpSocket类发出error()信号,并触发错误处理槽函数displayError()。该函数的错误处理方式比较简单,仅是显示出错误对话框并关闭连接。
    main()函数实现与以前的例子类似,这里不再叙述了。


    服务器端程序ReceiveFile完成的功能与客户端程序恰恰相反,它负责从TCP连接上接收数据,并将其写入当前目录下的指定文件中。
    其界面也是一个简单的对话框,上面布置一个QProgressBar进度条,一个用来显示状态的QLabel,两个QPushButton按钮分别用来开启监听和退出程序。
    该程序的主要功能也是在一个从QDialog类继承而来的Dialog类中完成的。

class Dialog : public QDialog
{
    Q_OBJECT

public:
    Dialog(QWidget *parent = 0);

public slots:
    void start();
    void acceptConnection();
    void updateServerProgress();
    void displayError(QAbstractSocket::SocketError socketError);

private:
    QProgressBar *clientProgressBar;
    QProgressBar *serverProgressBar;
    QLabel *serverStatusLabel;
    QPushButton *startButton;
    QPushButton *quitButton;
    QPushButton *openButton;
    QDialogButtonBox *buttonBox;

    QTcpServer tcpServer;                 //服务器套接字
    QTcpSocket *tcpServerConnection;      //连接后服务器返回的套接字
    qint64  TotalBytes;                   //总共需接收的字节数
    qint64  bytesReceived;                //已接收字节数
    qint64  fileNameSize;                 //待接收文件名字节数
    QString fileName;                     //待接收文件的文件名
    QFile *localFile;                     //待接收文件
    QByteArray inBlock;
};

 

Dialog::Dialog(QWidget *parent)
    : QDialog(parent)
{
    TotalBytes = 0;
    bytesReceived = 0;
    fileNameSize = 0;
    serverProgressBar = new QProgressBar;
    serverStatusLabel = new QLabel(tr("服务端就绪"));

    startButton = new QPushButton(tr("接收"));
    quitButton = new QPushButton(tr("退出"));

    buttonBox = new QDialogButtonBox;
    buttonBox->addButton(startButton, QDialogButtonBox::ActionRole);
    buttonBox->addButton(quitButton, QDialogButtonBox::RejectRole);

    connect(startButton, SIGNAL(clicked()), this, SLOT(start()));
    connect(quitButton, SIGNAL(clicked()), this, SLOT(close()));
    connect(&tcpServer, SIGNAL(newConnection()), this, SLOT(acceptConnection()));

    QVBoxLayout *mainLayout = new QVBoxLayout;
    mainLayout->addWidget(serverProgressBar);
    mainLayout->addWidget(serverStatusLabel);
    mainLayout->addStretch(1);
    mainLayout->addSpacing(10);
    mainLayout->addWidget(buttonBox);
    setLayout(mainLayout);

    setWindowTitle(tr("接收文件"));
}

    构造函数负责初始化界面,并将开始和退出按钮与各自的槽函数关联。这里还关联了QTcpServer的newConnection()信号,该信号在有可用的TCP连接是发出。

void Dialog::start()
{
    startButton->setEnabled(false);

    QApplication::setOverrideCursor(Qt::WaitCursor);
    bytesReceived = 0;

    while (!tcpServer.isListening() && !tcpServer.listen(QHostAddress::LocalHost,16689)) {
        QMessageBox::StandardButton ret = QMessageBox::critical(this,
                                        tr("回环"),
                                        tr("无法开始测试: %1.").arg(tcpServer.errorString()),
                                        QMessageBox::Retry | QMessageBox::Cancel);
        if (ret == QMessageBox::Cancel)
            return;
    }
    serverStatusLabel->setText(tr("监听"));
}

    当用户按下"接收"按钮后,start()函数开始执行,它调用QTcpServer的isListening()函数和listen()函数判断当前服务器是否已处在监听状态以及在本地16689端口建立监听是否成功。
    如果一切正常,服务器端就已经成功监听,随时等待处理客户端的TCP连接请求,否则弹出错误信息,报告错误后返回。

void Dialog::acceptConnection()
{
    tcpServerConnection = tcpServer.nextPendingConnection();
    connect(tcpServerConnection, SIGNAL(readyRead()),
            this, SLOT(updateServerProgress()));
    connect(tcpServerConnection, SIGNAL(error(QAbstractSocket::SocketError)),
            this, SLOT(displayError(QAbstractSocket::SocketError)));

    serverStatusLabel->setText(tr("接受连接"));
    tcpServer.close();
}

    有客户端请求到来时,QTcpSocket类将会发出newConnection()信号,从而触发acceptConnection()函数。
    QTcpServer类在接受了外来TCP连接请求后,可以通过nextPendingConnection()函数获取一个新的已建立连接的子套接字, (该套接字封装在QTcpSocket类中)并返回QTcpSocket类指针,将返回值保存在tcpServerConnection私有变量中。
    接下来关联QTcpSocket类的readyRead()信号和error()信号,其中readyRead()信号在新连接中有可读数据时发出,而当新连接中产生错误是会发出error()信号。
    由于本例只处理一个客户端请求,因此在返回一个连接后,就调用QTcpSocket类的close()函数关闭服务器端的监听,后面的工作均在新建的tcpServerConnection连接上完成。

void Dialog::updateServerProgress()
{
    QDataStream in(tcpServerConnection);
    in.setVersion(QDataStream::Qt_4_3);
    
    if(bytesReceived <= sizeof(qint64)*2){
      if((tcpServerConnection->bytesAvailable() >= sizeof(qint64)*2)&&(fileNameSize ==0)){
         in >> TotalBytes >> fileNameSize;
   bytesReceived += sizeof(qint64)*2;
      }       
      if((tcpServerConnection->bytesAvailable() >= fileNameSize)&&(fileNameSize !=0)){
         in >> fileName;
         bytesReceived += fileNameSize;
         localFile = new QFile(fileName);
         if (!localFile->open(QFile::WriteOnly )) {
         QMessageBox::warning(this, tr("应用程序"),
                    tr("无法读取文件 %1:\n%2.").arg(fileName).arg(localFile->errorString()));
         return;
     }   
      }else{
        return;
      }
    }
    
    if (bytesReceived < TotalBytes){
         bytesReceived += tcpServerConnection->bytesAvailable();
         inBlock = tcpServerConnection->readAll();
         localFile->write(inBlock);
         inBlock.resize(0);
    }
    serverProgressBar->setMaximum(TotalBytes);
    serverProgressBar->setValue(bytesReceived);
    qDebug()<<bytesReceived;
    serverStatusLabel->setText(tr("已接收 %1MB").arg(bytesReceived / (1024 * 1024)));

    if (bytesReceived == TotalBytes) {
        tcpServerConnection->close();
        startButton->setEnabled(true);
        QApplication::restoreOverrideCursor();
    }
}

    当建立的连接有新的可供读取的数据时,QTcpSocket类会发出readyRead()信号,从而触发updateServerProgress()函数。该函数完成数据的接收、存储,并更新进度显示。
    首先将上面返回的TCP连接tcpServerConnection封装的QDataStream类型变量in中,同时设置流化数据格式类型为 QDataStream::Qt_4_3,与客户端保持一致。现在可以很方便的通过重载后的"<<"操作符读取TCP连接上的数据了。
    由于流数据是没有结构的,为了知道接收的文件名以及文件何时接收完毕,必须首先获取文件头结构,这里还有个小问题,由于开始时所传输文件名的长度是未知 的,导致文件头结构的长度也是未知的,因此无法知道TCP数据流中前多少字节属于文件头结构部分。实际上文件头结构的接收可分两布完成:
      1、从TCP数据流中接收前16个字节(两个qint64结构长),用来确定总共需接收的字节数和文件名长度,并将这两个值保存在私有成员 TotalBytes和fileNameSize中,然后根据fileNameSize值接收文件名。值得注意的是,无法保证在上述接收文件头结构过程 中,TCP连接上总是有足够的数据,因此在第一步中,需要通过tcpServerConnection->bytesAvailable() >= sizeof(qint64)*2) && (fileNameSize ==0)操作确保至少有16字节的可用数据且文件名长度为0(表示未从TCP连接接收文件名长度字段,仍处于第一步操作),然后调用in >> TotalBytes >> fileNameSize操作读取总共需接收的数据和文件名长度。
      2、类似的通过(tcpServerConnection->bytesAvailable() >= fileNameSize) && (fileNameSize !=0)操作确保连接上的数据已包含完整的文件名且文件名长度不为0(表示已从TCP连接接收文件名长度字段,处于第二步操作中),然后调用in >> fileName操作读取文件名,并根据该文件名在本地以只写方式打开一个同名文件localFile,用来保存接收到的数据。
    接下来的工作是读取实际的文件数据并保存,以及更新进度显示,直到接收到完全的数据。由于所发送的文件内容自身也是无格式的流,因此在接收文件内容时,只 要TCP连接上有数据,就调用tcpServerConnection->readAll()操作将当前全部可读数据读入接收缓冲inBlock 中,随后再将该缓冲中的数据写入文件localFile中。当已收到的数据bytesReceived等于TotalBytes时,接收完毕,这时通过 tcpServerConnection->close()操作关闭连接。
    最后,错误处理函数displayError()和主函数main()与客户端程序类似,这里不再多说了~


    通常QTcpSocket类和QTcpServer类以异步方式工作,但可以通过调用其waitFor...()类型的函数实现同步操作,这类操作将阻塞调用线程直到某个信号发出。
    例如:在调用了非阻塞的QTcpSocket::connectToHost()函数后紧接着调用QTcpSocket::waitForConnected()函数以阻塞调用线程,知道connected()信号发出。
    一般而言,同步操作往往可以简化代码的控制流程,但也存在较大的缺点,调用waitFor...()函数将阻塞事件的处理,对于GUI线程会引起用户界面的冻结。
    因此,Qt建议在GUI线程中不使用同步套接字,此时QTcpSocket也不在需要事件循环。


    已经写了不少,累呀:( ,可是还有例子要举...
    
    下一个例子,其实是想讲解一个Socket编程最为典型的例子程序了,自己写的聊天程序,这个例子主要讲解的是单服务器、多客户端进行的处理过程。
    但是,由于一个字"懒"的原因,这里就只对服务端如何实现进行多客户端进行简短的讲解,其实在聊天程序的比较主要的知识点,在下面这个多线程网络程序中也涉及到了~~

    现在让我们看看服务器包含的两个类:QCharServer和QCharClient。
    QCharServer类继承了QServerSocker,QTcpServer类允许接受外来TCP连接,每当检测到外来TCP连接请求时,会自动调 用QTcpServer::incomingConnection()函数,参数为标识socket ID的int型变量。

QCharServer* serverSocket = new QCharServer(this);
if (!serverSocket->listen(QHostAddress::Any, m_port)) 
{
    QMessageBox::critical(this, tr("CharServer"),
                          tr("Unable To Start The Server: %1.")
                          .arg(serverSocket->errorString()));
        serverSocket->close();
}

    在主界面下创建和监听,等待客户端连接。

class QCharServer : public QTcpServer
{
    Q_OBJECT
public:
    QCharServer(QObject *parent = 0);
private:
    void incomingConnection( int socketDescriptor );
signals:
    void error(QTcpSocket::SocketError socketError);
};

QCharServer::QCharServer(QObject *parent)
 : QTcpServer(parent)
{
}   

void QCharServer::incomingConnection(int socketDescriptor)
{
 QCharClient *socket = new QCharClient(this);
 if (!socket->setSocketDescriptor(socketDescriptor)) 
 {
        emit error(socket->error());
        return;
    }
}

    设置socketDescriptor并且将QCharClient保存到一个内部列表中,从而在任何时候,在内存中QCharClient对象的数量和正在服务的客户端数量都是一样的。

    QCharClient继承了QTcpSocket并且封装了一个单独的客户端的状态。

class QCharClient : public QTcpSocket
{
 Q_OBJECT
public:
    QCharClient(QObject *parent = 0);

private slots:
    void recvData();
    void tryTest();
    void clientDisconnected();

private:
    void sendData();
};

QCharClient::QCharClient(QObject *parent)
    : QTcpSocket(parent)
{
    connect(this, SIGNAL(connected()), this, SLOT(clientConnected()));
    connect(this, SIGNAL(readyRead()), this, SLOT(recvData()));
    connect(this, SIGNAL(disconnected()), this, SLOT(clientDisconnected()));
}

void QCharClient::clientConnected()
{
    ...
}

void QCharClient::recvData()
{
    QDataStream in(this);
    char buffer[MAX_RECV_BUFFER_SIZE];
    memset(buffer, 0, MAX_RECV_BUFFER_SIZE);
    unsigned int len = in.readRawData(buffer, MAX_RECV_BUFFER_SIZE);
}

void QCharClient::clientDisconnected()
{
    ...
    deleteLater();
}

void QCharClient::sendData()
{
    QDataStream out(this);
    char *buffer;
    buffer = "vic.MINg";
    int len = strlen( buffer );
    out.writeRawData(buffer, len);
}

    这里没有什么新内容,不做多废话了~


    一个多线程的网络时间服务器,这个程序也是来自《精通Qt4编程》一书,每当由客户请求到达时,这个服务器将启动一个新线程为它返回当前的时间,服务器完毕后这个线程将自动退出,同时用户界面会显示当前以接受请求的次数。

class TimeServer : public QTcpServer
{
    Q_OBJECT
public:
    TimeServer(QObject *parent = 0);

protected:
    void incomingConnection(int socketDescriptor);
private:
    Dialog *dlg;
};

    首先需要实现一个TCP服务端类TimeServer,这里直接从QTcpServer类继承,并重写了其虚函数void incomingConnection( int socketDescriptor )。这个函数在TCP服务端有新的连接时被调用,参数这是界面指针,借用这个指针,将线程发出的消息关联到界面的槽函数中。

TimeServer::TimeServer(QObject *parent)
    : QTcpServer(parent)
{
    dlg = (Dialog*)parent;
}

    构造函数十分简单,这里用传入的父类指针parent初始化私有变量dlg就可以了。

void TimeServer::incomingConnection(int socketDescriptor)
{
    TimeThread *thread = new TimeThread(socketDescriptor,this);
    connect(thread, SIGNAL(finished()), dlg, SLOT(showResult()),Qt::QueuedConnection);
    connect(thread, SIGNAL(finished()), thread, SLOT(deleteLater()));
    thread->start();
}

    在重写的虚函数incomingConnection()中,首先以返回的套接字描述符socketDescriptor创建一个工作线程 TimeThread,然后将这个线程的结束消息finished()分别关联到界面显示类的槽函数showResult()用于显示请求计数,以及线程 自身的槽函数deleteLater()用于结束线程。
    一切准备工作完成后启动这个线程。需要注意的是,在第一个connect操作中,使用了排队连接方式,第二个connect操作中使用了直接连接方式,原 因在于前一个信号是跨线程的,后一个信号是在同一个线程中,当然也可以省略connect()函数的最后一个参数,而采用Qt的自动连接选择方式。另一个 需要注意的是,由于工作线程中存在网络事件,因此不能被外界线程销毁,这里使用了延迟销毁函数deleterLater()保证由工作线程自身销毁。

class TimeThread : public QThread
{
    Q_OBJECT
public:
    TimeThread(int socketDescriptor, QObject *parent);
    void run();

signals:
    void error(QTcpSocket::SocketError socketError);
private:
    int socketDescriptor;
};

    工作线程TimeThread由QThread类继承而来,这里将重写重要的虚函数run()。此外,还定义了一个出错信号void error(QTcpSocket::SocketError socketError)和一个私有的套接字描述符socketDescriptor。

TimeThread::TimeThread(int socketDescriptor,QObject *parent)
    : QThread(parent), socketDescriptor(socketDescriptor)
{
}

    构造函数十分简单,这里仅是初始化了私有套接字描述符。

void TimeThread::run()
{
    QTcpSocket tcpSocket;
    if (!tcpSocket.setSocketDescriptor(socketDescriptor)) {
        emit error(tcpSocket.error());
        return;
    }

    QDateTime time;
    QByteArray block;
    QDataStream out(&block, QIODevice::WriteOnly);
    out.setVersion(QDataStream::Qt_4_3);
    uint time2u = QDateTime::currentDateTime().toTime_t();
    out << time2u;
    tcpSocket.write(block);
    tcpSocket.disconnectFromHost();
    tcpSocket.waitForDisconnected();
}

    虚函数run()是工作线程的实质所在,当在TimeServer::incomingConnection()函数中调用了start()函数后,这个 虚函数开始执行。它首先创建一个QTcpSocket类并置以从构造函数中传入的套接字描述符,用来向客户端传回服务器端的当前时间。如果出错,发出 error(tcpSocket.error())信号报告错误;否则,开始获取当前时间并将它传回客户端,然后断开连接等待返回。
    这里介绍以下时间数据的传输格式,Qt虽然可以很方便的通过QDateTime类的静态函数currentDateTime()获取一个时间对象,但类结 构是无法直接在网络间传输的,此时需要将它转换成一个标准的数据类型后再传输。幸好的是QDateTime类提供了uint toTime_t() const函数,这个函数返回当前自1970-01-01 00:00:00经过了多少秒,为一个uint类型,可以将这个值传输给客户端。在客户端方面,使用QDateTime类void setTime_t(uint seconds)将这个时间还原。

class Dialog : public QDialog
{
    Q_OBJECT
public:
    Dialog(QWidget *parent = 0);
public slots:
    void showResult();
private:
    QLabel *statusLabel;
    QLabel *reqStatusLable;
    QPushButton *quitButton;
    TimeServer *server;
    int count;
};

    界面类Dialog比较简单,它实际上就是一个对话框。在此定义了一个用于显示请求次数的槽函数void showResult(),以及用于显示监听端口的标签statusLabel,用于显示请求次数的标签reqStatusLabel,退出按钮 quitButton,TCP服务器server和请求次数计数器count。

Dialog::Dialog(QWidget *parent)
    : QDialog(parent),count(0)
{
    server = new TimeServer(this);
    statusLabel = new QLabel;
    reqStatusLable = new QLabel;
    quitButton = new QPushButton(tr("退出"));
    quitButton->setAutoDefault(false);
    if (!server->listen()) {
        QMessageBox::critical(this, tr("多线程时间服务器"),
            tr("无法启动服务器: %1.").arg(server->errorString()));
        close();
        return;
    }

    statusLabel->setText(tr("时间服务器运行在端口: %1.\n")
                         .arg(server->serverPort()));
    connect(quitButton, SIGNAL(clicked()), this, SLOT(close()));

    QHBoxLayout *buttonLayout = new QHBoxLayout;
    buttonLayout->addStretch(1);
    buttonLayout->addWidget(quitButton);
    buttonLayout->addStretch(1);

    QVBoxLayout *mainLayout = new QVBoxLayout;
    mainLayout->addWidget(statusLabel);
    mainLayout->addWidget(reqStatusLable);
    mainLayout->addLayout(buttonLayout);
    setLayout(mainLayout);
    setWindowTitle(tr("多线程时间服务器"));
}

    构造函数Dialog完成了两件事,一件是初始化界面,另一件是启动服务器端的网络监听。

void Dialog::showResult()
{
    reqStatusLable->setText(tr("第%1次请求完毕.\n").arg(++count));
}

    槽函数showResult()功能十分简单,它在标签reqStatusLable上显示当前的请求次数,并将请求计数count加1。

默认分类 | 阅读 10148 次
文章评论,共0条
游客请输入验证码
浏览106031次
文章分类
最新评论