1. 程式人生 > 其它 >十二.QT5多執行緒

十二.QT5多執行緒

一、多執行緒的基礎知識

1、程序的定義:程序是程序實體的執行過程,是系統進行資源分配和排程的一個獨立單位。

2、執行緒的定義:引入程序的目的是為了更好地使多道程式併發執行,提高資源利用率和系統吞吐量;而引入執行緒的目的則是為了減小程式在併發執行時所付出的時空開銷,提高作業系統的併發效能。執行緒最直接的理解就是“輕量級程序”,它是一個基本的CPU執行單元,也是程式執行流的最小單元,由執行緒ID、程式計數器、暫存器集合和堆疊組成。執行緒是程序中的一個實體,是被系統獨立排程和分派的基本單位,執行緒自己不擁有系統資源,只擁有一點兒在執行中必不可少的.資源,但它可與同屬一個程序的其他執行緒共享程序所擁有的全部資源。一個執行緒可以建立和撤銷另一個執行緒,同一程序中的多個執行緒之間可以併發執行。由於執行緒之間的相互制約,致使執行緒在執行中呈現出間斷性。執行緒也有就緒、阻塞和執行三種基本狀態。

3、多執行緒的優勢

(1)提高應用程式的響應速度。這對於開發圖形介面的程式尤為重要,當一個操作耗時很長時,整個系統都會等待這個操作,程式就不能響應鍵盤、滑鼠、選單等的操作,而使用多執行緒技術可將耗時長的操作置於一個新的執行緒,從而避免出現以上問題。

(2)使多CPU系統更加有效。當執行緒數不大於CPU數目時,作業系統可以排程不同的執行緒運行於不同的CPU上。

(3)改善程式結構。一個既長又複雜的程序可以考慮分為多個執行緒,成為獨立或半獨立的執行部分,這樣有利於程式碼的理解和維護。

4、多執行緒程式具有以下特點

(1)多執行緒程式的行為無法預期,當多次執行上述程式時,每次的執行結果都可能不同。

(2)多執行緒的執行順序無法保證,它與作業系統的排程策略和執行緒優先順序等因素有關。

(3)多執行緒的切換可能發生在任何時刻、任何地點。

(4)由於多執行緒對程式碼的敏感度高,因此對程式碼的細微修改都可能產生意想不到的結果。

二、多執行緒及簡單例項

如圖所示,單擊“開始”按鈕將啟動數個工作執行緒(工作執行緒數目由MAXSIZE巨集決定),各個執行緒迴圈列印數字0~9,直到單擊“停止”按鈕終止所有執行緒為止。

具體步驟如下。

建立qt應用程式專案,類:QDialog

(1)在標頭檔案“threaddlg.h”中宣告用於介面顯示所需的控制元件,其具體程式碼如下:

#include <QDialog>
#include 
<QPushButton> class ThreadDlg : public QDialog { Q_OBJECT public: ThreadDlg(QWidget *parent = 0); ~ThreadDlg(); private: QPushButton *startBtn;//開始按鈕部件 QPushButton *stopBtn;//停止按鈕部件 QPushButton *quitBtn;//退出按鈕部件 };

(2)在原始檔“threaddlg.cpp”的建構函式中,完成各個控制元件的初始化工作,其具體程式碼如下:

#include "threaddlg.h"
#include <QHBoxLayout>
ThreadDlg::ThreadDlg(QWidget *parent)
    : QDialog(parent)
{
    setWindowTitle(tr("執行緒"));//設定主視窗標題
    startBtn = new QPushButton(tr("開始"));//設定按鈕文字
    stopBtn = new QPushButton(tr("停止"));
    quitBtn = new QPushButton(tr("退出"));
    QHBoxLayout *mainLayout = new QHBoxLayout(this);//設定水平佈局
    mainLayout->addWidget(startBtn);//將按鈕從左到右放入mainLayout
    mainLayout->addWidget(stopBtn);
    mainLayout->addWidget(quitBtn);
}

(3)此時執行程式,介面顯示如圖所示

以上完成了介面的設計,下面的內容是具體的功能實現。

(1)在標頭檔案“workthread.h”中,工作執行緒WorkThread類繼承自QThread類。重新實現run()函式。其具體程式碼如下:

#include <QThread>
class WorkThread : public QThread
{
    Q_OBJECT
public:
    WorkThread();
protected:
    void run();
};

(2)在原始檔“workthread.cpp”中新增具體實現程式碼如下:

#include "workthread.h"
#include <QtDebug>
WorkThread::WorkThread()
{
}
//run()函式實際上是一個死迴圈,它不停地列印數字0~9。為了顯示效果明顯,
//程式將每一個數字重複列印8次。
void WorkThread::run() { while(true) { for(int n=0;n<10;n++) qDebug()<<n<<n<<n<<n<<n<<n<<n<<n; } }

(3)在標頭檔案“threaddlg.h”中新增以下內容:

#include "workthread.h"
#define MAXSIZE 1  //MAXSIZE巨集定義了執行緒的數目
public slots:
    void slotStart();  //槽函式用於啟動執行緒
    void slotStop();  //槽函式用於終止執行緒
private:
WorkThread *workThread[MAXSIZE]; //WorkThread *workThread[MAXSIZE]:指向工作執行緒(WorkThread)的私有指標陣列workThread,記錄了所啟動的全部執行緒。

(4)在原始檔“threaddlg.cpp”中新增以下內容。 其中,在建構函式中新增如下程式碼:

connect(startBtn,SIGNAL(clicked()),this,SLOT(slotStart()));
connect(stopBtn,SIGNAL(clicked()),this,SLOT(slotStop()));
connect(quitBtn,SIGNAL(clicked()),this,SLOT(close()));

當用戶單擊“開始”按鈕時,將呼叫槽函式slotStart()。這裡使用兩個迴圈,目的是使新建的執行緒儘可能同時開始執行,其具體實現程式碼如下:

void ThreadDlg::slotStart()
{
    for(int i=0;i<MAXSIZE;i++)
    {
        workThread[i]=new WorkThread();//建立指定數目的WorkThread執行緒,並將WorkThread例項的指標儲存在指標陣列workThread中。

    }
    for(int i=0;i<MAXSIZE;i++)
    {
        workThread[i]->start();//呼叫QThread基類的start()函式,此函式將啟動run()函式,從而使執行緒開始真正執行。

    }
    startBtn->setEnabled(false);
    stopBtn->setEnabled(true);
}

當用戶單擊“停止”按鈕時,將呼叫槽函式slotStop()。其具體實現程式碼如下:

void ThreadDlg::slotStop()
{
    for(int i=0;i<MAXSIZE;i++)
    {
        workThread[i]->terminate();
        workThread[i]->wait();
    }
    startBtn->setEnabled(true);
    stopBtn->setEnabled(false);
}

(5)多執行緒簡單實現結果如圖12.2所示。 第1列是啟動5個執行緒的執行結果,第2列是啟動單一執行緒的執行結果。可以看出,單一執行緒的輸出是順序列印的,而多執行緒的輸出結果則是亂序列印的,這正是多執行緒的一大特點。

三、多執行緒控制

執行緒中存在著相互制約的關係,互斥和同步。

實現執行緒的互斥與同步常使用的類有QMutex、QMutexLocker、QReadWriteLocker、QReadLocker、QWriteLocker、QSemaphore和QWaitCondition。 下面舉一個例子加以說明:

class Key
{
public:
    Key() {key=0;}
    int creatKey() {++key; return key;}
    int value()const {return key;}
private:
    int key;    
};

在多執行緒環境下,這個類是不安全的,因為存在多個執行緒同時修改私有成員key,其結果是不可預知的。

雖然Key類產生主鍵的函式creatKey()只有一條語句執行修改成員變數key的值,但是C++的“++”操作符並不是原子操作,通常編譯後,它將被展開成為以下三條機器命令:  

  將變數值載入暫存器。

  將暫存器中的值加1。

  將暫存器中的值寫回主存。

1、互斥量

QMutex類

QMutex類是對互斥量的處理。它被用來保護一段臨界區程式碼,即每次只允許一個執行緒訪問這段程式碼。

QMutex類的lock()函式用於鎖住互斥量。如果互斥量處於解鎖狀態,則當前執行緒就會立即抓住並鎖定它,否則當前執行緒就會被阻塞,直到持有這個互斥量的執行緒對它解鎖。執行緒呼叫lock()函式後就會持有這個互斥量,直到呼叫unlock()操作為止。 QMutex類還提供了一個tryLock()函式。如果互斥量已被鎖定,則立即返回。 例如:

class Key
{
public:
    Key() {key=0;}
    int creatKey() { mutex.lock();  ++key;  return key;  mutex. unlock();}
    int value()const { mutex.lock();  return key;  mutex.unlock();}
private:
    int key;
    QMutex mutex;
};

QMutexLocker類

Qt提供的QMutexLocker類可以簡化互斥量的處理,它在建構函式中接收一個QMutex物件作為引數並將其鎖定,在解構函式中解鎖這個互斥量,這樣就解決了以上問題。 例如:

class Key
{
public:
    Key() {key=0;}
    int creatKey() { QmutexLocker locker(&mutex);  ++key;  return key; }
    int value()const { QmutexLocker locker(&mutex);  return key; }
private:
    int key;
    QMutex mutex;
};

locker()函式作為區域性變數會在函式退出時結束其作用域,從而自動對互斥量mutex解鎖。

2、訊號量

訊號量可以理解為對互斥量功能的擴充套件,互斥量只能鎖定一次而訊號量可以獲取多次,它可以用來保護一定數量的同種資源。訊號量的典型用例是控制生產者/消費者之間共享的環形緩衝區。

生產者/消費者例項中對同步的需求有兩處:

(1)如果生產者過快地生產資料,將會覆蓋消費者還沒有讀取的資料。

(2)如果消費者過快地讀取資料,將越過生產者並且讀取到一些過期資料。

針對以上問題,有兩種解決方法:

(1)首先使生產者填滿整個緩衝區,然後等待消費者讀取整個緩衝區,這是一種比較笨拙的方法。

(2)使生產者和消費者執行緒同時分別操作緩衝區的不同部分,這是一種比較高效的方法。

2.1基於控制檯程式實現。

(1)在原始檔“main.cpp”中新增的具體實現程式碼如下:

#include <QCoreApplication>
#include <QSemaphore>
#include <QThread>
#include <stdio.h>

const int DataSize=1000;
const int BufferSize=80;
int buffer[BufferSize];          //首先,生產者向buffer中寫入資料,直到它到達終點,然後從起點重新開始覆蓋已經存在的資料
QSemaphore freeBytes(BufferSize);  //freeBytes訊號量控制可被生產者填充的緩衝區部分,被初始化為ButterSize(80),表示程式一開始時有BufferSize個緩衝區單元可被填充
QSemaphore usedBytes(0);//usedByters訊號量控制可被消費者讀取的緩衝區部分

//Producer類繼承自QThread類,作為生產者類,其宣告如下:

class Producer : public QThread
{
public:
    Producer();
    void run();
};

Producer::Producer()
{
}

void Producer::run()
{
    for(int i=0;i<DataSize;i++)
    {
       freeBytes.acquire();  //生產者執行緒首先獲取一個空閒單元
       buffer[i%BufferSize]=(i%BufferSize);//一旦生產者獲取了某個空閒單元,就使用當前的緩衝區單元序號填寫這個緩衝區單元
       usedBytes.release();//釋放資源
    }
}
//Consumer類繼承自QThread類,作為消費者類,其宣告如下:
class Consumer : public QThread { public: Consumer(); void run(); }; Consumer::Consumer() { } void Consumer::run() { for(int i=0;i<DataSize;i++) { usedBytes.acquire();//消費者執行緒首先獲取一個可被讀取的單元,如果緩衝區中沒有包含任何可以讀取的資料,對此函式的呼叫就會阻塞,直到生產者生產了一些資料為止。 fprintf(stderr,"%d",buffer[i%BufferSize]);//一旦消費者獲取了這個單元,會將這個單元的內容打印出來。
if(i%16==0&&i!=0)
            fprintf(stderr,"\n");

        freeBytes.release();//使單元變為空閒
    }
    fprintf(stderr,"\n");
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    Producer producer;
    Consumer consumer;
    /* 啟動生產者和消費者執行緒 */
    producer.start();
    consumer.start();

/* 等待生產者和消費者各自執行完畢後自動退出 */

    producer.wait();
    consumer.wait();

    return a.exec();
}

最終執行結果如圖所示。

3、執行緒等待與喚醒

使用QWaitCondition類解決生產者和消費者問題。

原始檔“main.cpp”的具體內容如下:

#include <QCoreApplication>
#include <QWaitCondition>
#include <QMutex>
#include <QThread>
#include <stdio.h>
const int DataSize=1000;
const int BufferSize=80;
int buffer[BufferSize];
QWaitCondition bufferEmpty;
QWaitCondition bufferFull;
QMutex mutex;  //使用互斥量保證對執行緒操作的原子性
int numUsedBytes=0;    //變數numUsedBytes表示存在多少“可用位元組”
int rIndex=0;    //本例中啟動了兩個消費者執行緒,並且這兩個執行緒讀取同一個緩衝區,為了不重複讀取,設定全域性變數rIndex用於指示當前所讀取緩衝區位置。

class Producer : public QThread
{
public:
    Producer();
    void run();
};
Producer::Producer()
{
}
void Producer::run()
{
    for(int i=0;i<DataSize;i++)                //for迴圈中的所有語句都需要使用互斥量加以保護,以保證其操作的原子性。

    {
       mutex.lock();
       if(numUsedBytes==BufferSize)            //首先檢查緩衝區是否已被填滿。
          bufferEmpty.wait(&mutex);            //如果緩衝區已被填滿,則等待“緩衝區有空位”(bufferEmpty變數)條件成立。wait()函式將互斥量解鎖並在此等待,其原型如下:
       buffer[i%BufferSize]=numUsedBytes;    //如果緩衝區未被填滿,則向緩衝區中寫入一個整數值。
       ++numUsedBytes;                        //增加numUsedBytes變數
       bufferFull.wakeAll();                //最後喚醒等待“緩衝區有可用資料”(bufferEmpty變數)條件為“真”的執行緒。

       mutex.unlock();
    }
}
class Consumer : public QThread
{
public:
    Consumer();
    void run();
};
Consumer::Consumer()
{
}
void Consumer::run()
{
    forever
    {
        mutex.lock();
        if(numUsedBytes==0)
            bufferFull.wait(&mutex);            //當緩衝區中無資料時,等待“緩衝區有可用資料”(bufferFull變數)條件成立。
        printf("%ul::[%d]=%d\n",currentThreadId(),rIndex,buffer[rIndex]);
                                                //當緩衝區中有可用資料即條件成立時,列印當前執行緒號和rIndex變數,以及其指示的當前可讀取資料。這裡為了區分究竟是哪一個消費者執行緒消耗了緩衝區裡的資料,使用了QThread類的currentThreadId()靜態函式輸出當前執行緒的ID。這個ID在X11環境下是一個unsigned long 型別的值。
        rIndex=(++rIndex)%BufferSize;    //將rIndex變數迴圈加1
        --numUsedBytes;                            //numUsedBytes變數減1,即可用的資料減1。

        bufferEmpty.wakeAll();//喚醒等待“緩衝區有空位”(bufferEmpty變數)條件的生產者執行緒。
        mutex.unlock();
    }
    printf("\n");
}

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    Producer producer;
    Consumer consumerA;
    Consumer consumerB;
    producer.start();
    consumerA.start();
    consumerB.start();
    producer.wait();
    consumerA.wait();
    consumerB.wait();
    return a.exec();
}

四、綜合應用-----多執行緒應用

1、“例項”伺服器端程式設計

首先,建立伺服器端工程“TimeServer.pro”。檔案程式碼如下。

(1)在標頭檔案“dialog.h”中,定義伺服器端介面類Dialog繼承自QDialog類,其具體程式碼如下:

#ifndef DIALOG_H
#define DIALOG_H

#include <QDialog>
#include <QLabel>
#include <QPushButton>
class TimeServer;
class Dialog : public QDialog
{
    Q_OBJECT

public:
    Dialog(QWidget *parent = 0);
    ~Dialog();
public slots:
    void slotShow();                //此槽函式用於介面上顯示的請求次數
private:
    QLabel *Label1;                    //此標籤用於顯示監聽埠
    QLabel *Label2;                    //此標籤用於顯示請求次數
    QPushButton *quitBtn;            //退出按鈕
    TimeServer *timeServer;            //TCP伺服器端timeServer
    int count;                        //請求次數計數器count
};

#endif // DIALOG_H

(2)在原始檔“dialog.cpp”中,Dialog類的建構函式完成了初始化介面,其具體程式碼如下:

#include "dialog.h"
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QMessageBox>
#include "timeserver.h"
Dialog::Dialog(QWidget *parent)
    : QDialog(parent)
{
    setWindowTitle(tr("多執行緒時間伺服器"));
    Label1 =new QLabel(tr("伺服器埠:"));
    Label2 = new QLabel;
    quitBtn = new QPushButton(tr("退出"));
    QHBoxLayout *BtnLayout = new QHBoxLayout;
    BtnLayout->addStretch(1);
    BtnLayout->addWidget(quitBtn);
    BtnLayout->addStretch(1);
    QVBoxLayout *mainLayout = new QVBoxLayout(this);
    mainLayout->addWidget(Label1);
    mainLayout->addWidget(Label2);
    mainLayout->addLayout(BtnLayout);
    connect(quitBtn,SIGNAL(clicked()),this,SLOT(close()));
    count=0;
    timeServer = new TimeServer(this);
    if(!timeServer->listen())
    {
        QMessageBox::critical(this,tr("多執行緒時間伺服器"),
        tr("無法啟動伺服器:%1.").arg(timeServer->errorString()));
        close();
        return;
    }
    Label1->setText(tr("伺服器埠:%1.").arg(timeServer->serverPort()));
}
void Dialog::slotShow()
{
    Label2->setText(tr("第%1次請求完畢。").arg(++count));
}
Dialog::~Dialog()
{
}

(3)在伺服器端工程“TimeServer.pro”中,新增C++ Class檔案“timethread.h”及“timethread.cpp”。在標頭檔案“timethread.h”中,工作執行緒TimeThread類繼承自QThread類,實現TCP套接字,其具體程式碼如下:

#ifndef TIMETHREAD_H
#define TIMETHREAD_H

#include <QThread>
#include <QtNetwork>
#include <QTcpSocket>
class TimeThread : public QThread
{
    Q_OBJECT
public:
    TimeThread(int socketDescriptor,QObject *parent=0);
    void run();                                            //重寫此虛擬函式
signals:
    void error(QTcpSocket::SocketError socketError);    //出錯訊號
private:
    int socketDescriptor;                                //套接字描述符
};

#endif // TIMETHREAD_H
#include "timethread.h"
#include <QDateTime>
#include <QByteArray>
#include <QDataStream>
TimeThread::TimeThread(int socketDescriptor,QObject *parent):QThread(parent),socketDescriptor(socketDescriptor)
{

}
void TimeThread::run()
{
    QTcpSocket tcpSocket;                //建立一個QTcpSocket類
    if(!tcpSocket.setSocketDescriptor(socketDescriptor))    //將以上建立的QTcpSocket類置以從建構函式中傳入的套接字描述符,用於向客戶端傳回伺服器端的當前時間。
    {
        emit error(tcpSocket.error());                        //如果出錯,則發出error(tcpSocket.error())訊號報告錯誤。
        return;
    }
    QByteArray block;
    QDataStream out(&block,QIODevice::WriteOnly);
    out.setVersion(QDataStream::Qt_5_8);
    uint time2u = QDateTime::currentDateTime().toTime_t();    //如果不出錯,則開始獲取當前時間。
    out<<time2u;
    tcpSocket.write(block);                //將獲得的當前時間傳回客戶端
    tcpSocket.disconnectFromHost();        //斷開連線
    tcpSocket.waitForDisconnected();    //等待返回
}

(4)在伺服器端工程“TimeServer.pro”中新增C++ Class檔案“timeserver.h”及“timeserver.cpp”。在標頭檔案“timeserver.h”中,實現了一個TCP伺服器端,TimeServer類繼承自QTcpServer類,其具體程式碼如下:

#ifndef TIMESERVER_H
#define TIMESERVER_H

#include <QTcpServer>
class Dialog;                                             //伺服器端的宣告
class TimeServer : public QTcpServer
{
    Q_OBJECT
public:
    TimeServer(QObject *parent=0);
protected:
    void incomingConnection(int socketDescriptor);      //重寫此虛擬函式。這個函式在TCP伺服器端有新的連線時被呼叫,其引數為所接收新連線的套接字描述符。

private:
    Dialog *dlg;    //用於記錄建立這個TCP伺服器端物件的父類,這裡是介面指標,通過這個指標將執行緒發出的訊息關聯到介面的槽函式中。
};

#endif // TIMESERVER_H
#include "timeserver.h"
#include "timethread.h"
#include "dialog.h"
TimeServer::TimeServer(QObject *parent):QTcpServer(parent)
{
    dlg =(Dialog *)parent;
}
void TimeServer::incomingConnection(int socketDescriptor)
{
    TimeThread *thread = new TimeThread(socketDescriptor,0);    //以返回的套接字描述符socketDescriptor建立一個工作執行緒TimeThread。

    connect(thread,SIGNAL(finished()),dlg,SLOT(slotShow()));    //將上述建立的執行緒結束訊息函式finished()關聯到槽函式slotShow()用於顯示請求計數。此操作中,因為訊號是跨執行緒的,所以使用了排隊連線方式。

    connect(thread,SIGNAL(finished()),thread,SLOT(deleteLater()),
            Qt::DirectConnection);                                //將上述建立的執行緒結束訊息函式finished()關聯到執行緒自身的槽函式deleteLater()用於結束執行緒。

    thread->start();                                            //啟動上述建立的執行緒。執行此語句後,工作執行緒(TimeThread)的虛擬函式run()開始執行。

}

(5)在伺服器端工程檔案“TimeServer.pro”中新增如下程式碼: QT += network

2、“例項”客戶端程式設計

客戶端介面如圖所示。

操作步驟如下。

(1)建立客戶端工程“TimeClient.pro”。在標頭檔案“timeclient.h”中,定義了客戶端介面類TimeClient繼承自QDialog類

#ifndef TIMECLIENT_H
#define TIMECLIENT_H

#include <QDialog>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QDateTimeEdit>
#include <QTcpSocket>
#include <QAbstractSocket>
class TimeClient : public QDialog
{
    Q_OBJECT

public:
    TimeClient(QWidget *parent = 0);
    ~TimeClient();
public slots:
    void enableGetBtn();
    void getTime();
    void readTime();
    void showError(QAbstractSocket::SocketError socketError);
private:
    QLabel *serverNameLabel;
    QLineEdit *serverNameLineEdit;
    QLabel *portLabel;
    QLineEdit *portLineEdit;
    QDateTimeEdit *dateTimeEdit;
    QLabel *stateLabel;
    QPushButton *getBtn;
    QPushButton *quitBtn;
    uint time2u;
    QTcpSocket *tcpSocket;
};

#endif // TIMECLIENT_H

(2)在原始檔“timeclient.cpp”中,TimeClient類的建構函式完成了初始化介面,其具體程式碼。 在原始檔“timeclient.cpp”中,enableGetBtn()函式的具體程式碼如下:

#include "timeclient.h"
#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QGridLayout>
#include <QDataStream>
#include <QMessageBox>
TimeClient::TimeClient(QWidget *parent)
    : QDialog(parent)
{
    setWindowTitle(tr("多執行緒時間服務客戶端"));
    serverNameLabel =new QLabel(tr("伺服器名:"));
    serverNameLineEdit = new QLineEdit("Localhost");
    portLabel =new QLabel(tr("埠:"));
    portLineEdit = new QLineEdit;
    QGridLayout *layout = new QGridLayout;
    layout->addWidget(serverNameLabel,0,0);
    layout->addWidget(serverNameLineEdit,0,1);
    layout->addWidget(portLabel,1,0);
    layout->addWidget(portLineEdit,1,1);
    dateTimeEdit = new QDateTimeEdit(this);
    QHBoxLayout *layout1 = new QHBoxLayout;
    layout1->addWidget(dateTimeEdit);
    stateLabel =new QLabel(tr("請首先執行時間伺服器!"));
    QHBoxLayout *layout2 = new QHBoxLayout;
    layout2->addWidget(stateLabel);
    getBtn = new QPushButton(tr("獲取時間"));
    getBtn->setDefault(true);
    getBtn->setEnabled(false);
    quitBtn = new QPushButton(tr("退出"));
    QHBoxLayout *layout3 = new QHBoxLayout;
    layout3->addStretch();
    layout3->addWidget(getBtn);
    layout3->addWidget(quitBtn);
    QVBoxLayout *mainLayout = new QVBoxLayout(this);
    mainLayout->addLayout(layout);
    mainLayout->addLayout(layout1);
    mainLayout->addLayout(layout2);
    mainLayout->addLayout(layout3);
    connect(serverNameLineEdit,SIGNAL(textChanged(QString)),
        this,SLOT(enableGetBtn()));
    connect(portLineEdit,SIGNAL(textChanged(QString)),
        this,SLOT(enableGetBtn()));
    connect(getBtn,SIGNAL(clicked()),this,SLOT(getTime()));
    connect(quitBtn,SIGNAL(clicked()),this,SLOT(close()));
    tcpSocket = new QTcpSocket(this);
    connect(tcpSocket,SIGNAL(readyRead()),this,SLOT(readTime()));
    connect(tcpSocket,SIGNAL(error(QAbstractSocket::SocketError)),this,
        SLOT(showError(QAbstractSocket::SocketError)));
    portLineEdit->setFocus();
}

void TimeClient::enableGetBtn()
{
     getBtn->setEnabled(!serverNameLineEdit->text().isEmpty()&&
          !portLineEdit->text().isEmpty());
}

void TimeClient::getTime()
{
    getBtn->setEnabled(false);
    time2u =0;
    tcpSocket->abort();
    tcpSocket->connectToHost(serverNameLineEdit->text(),
        portLineEdit->text().toInt());
}

void TimeClient::readTime()
{
    QDataStream in(tcpSocket);
    in.setVersion(QDataStream::Qt_5_9);
    if(time2u==0)
    {
        if(tcpSocket->bytesAvailable()<(int)sizeof(uint))
            return;
        in>>time2u;
    }
    dateTimeEdit->setDateTime(QDateTime::fromTime_t(time2u));
    getBtn->setEnabled(true);
}

void TimeClient::showError(QAbstractSocket::SocketError socketError)
{
    switch (socketError)
    {
    case QAbstractSocket::RemoteHostClosedError:
        break;
    case QAbstractSocket::HostNotFoundError:
        QMessageBox::information(this, tr("時間服務客戶端"),
             tr("主機不可達!"));
        break;
    case QAbstractSocket::ConnectionRefusedError:
        QMessageBox::information(this, tr("時間服務客戶端"),
             tr("連線被拒絕!"));
        break;
    default:
        QMessageBox::information(this, tr("時間服務客戶端"),
              tr("產生如下錯誤: %1.").arg(tcpSocket->errorString()));
    }
    getBtn->setEnabled(true);
}

TimeClient::~TimeClient()
{

}