QTcpserver listen

NO IMAGE

網路程式設計,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。