1. 程式人生 > 實用技巧 >Qt TCP網路程式設計基本教程

Qt TCP網路程式設計基本教程

首先介紹一下TCP:(Transmission Control Protocol 傳輸控制協議)是一種面向連線的、可靠的、基於位元組流的傳輸層通訊協議。相比而言UDP,就是開放式、無連線、不可靠的傳輸層通訊協議。 下面,我一次進行客戶端和伺服器端的QT實現。我的開發環境是:QT Creator 5.7。

先看下效果圖:

一:客戶端程式設計

QT提供了QTcpSocket類,可以直接例項化一個客戶端,可在help中索引如下:

1 The QTcpSocket class provides a TCP socket. More...
2 Header      #include <QTcpSocket> 
3
qmake QT += network 4 Inherits: QAbstractSocket 5 Inherited By: QSslSocket

從這裡,我們可以看到,必須要在.pro檔案中新增QT += network才可以進行網路程式設計,否則是訪問不到<QTcpSocket>標頭檔案的。 客戶端讀寫相對簡單,我們看一下程式碼標頭檔案:

 1 #ifndef MYTCPCLIENT_H
 2 #define MYTCPCLIENT_H
 3 
 4 #include <QMainWindow>
 5 #include <QTcpSocket>
 6
#include <QHostAddress> 7 #include <QMessageBox> 8 namespace Ui { 9 class MyTcpClient; 10 } 11 12 class MyTcpClient : public QMainWindow 13 { 14 Q_OBJECT 15 16 public: 17 explicit MyTcpClient(QWidget *parent = 0); 18 ~MyTcpClient(); 19 20 private: 21 Ui::MyTcpClient *ui;
22 QTcpSocket *tcpClient; 23 24 private slots: 25 //客戶端槽函式 26 void ReadData(); 27 void ReadError(QAbstractSocket::SocketError); 28 29 void on_btnConnect_clicked(); 30 void on_btnSend_clicked(); 31 void on_pushButton_clicked(); 32 }; 33 34 #endif // MYTCPCLIENT_H

我們在視窗類中,定義了一個私有成員QTcpSoket *tcpClient。

1) 初始化QTcpSocket
在建構函式中,我們需要先對其進行例項化,並連線訊號與槽函式:

1     //初始化TCP客戶端
2     tcpClient = new QTcpSocket(this);   //例項化tcpClient
3     tcpClient->abort();                 //取消原有連線
4     connect(tcpClient, SIGNAL(readyRead()), this, SLOT(ReadData()));
5     connect(tcpClient, SIGNAL(error(QAbstractSocket::SocketError)), \
6             this, SLOT(ReadError(QAbstractSocket::SocketError)));

2)建立連線 和 斷開連線

1     tcpClient->connectToHost(ui->edtIP->text(), ui->edtPort->text().toInt());
2     if (tcpClient->waitForConnected(1000))  // 連線成功則進入if{}
3     {
4         ui->btnConnect->setText("斷開");
5         ui->btnSend->setEnabled(true);
6     }

a)建立TCP連線的函式:void connectToHost(const QHostAddress &address, quint16 port, OpenMode openMode = ReadWrite)是從QAbstractSocket繼承下來的public function,同時它又是一個virtual function。作用為:Attempts to make a connection to address on port port。

b)等待TCP連線成功的函式:bool waitForConnected(int msecs = 30000)同樣是從QAbstractSocket繼承下來的public function,同時它又是一個virtual function。作用為:Waits until the socket is connected, up to msecs milliseconds. If the connection has been established, this function returns true; otherwise it returns false. In the case where it returns false, you can call error() to determine the cause of the error.

上述程式碼中,edtIP, edtPort是ui上的兩個lineEditor,用來填寫伺服器IP和埠號。btnConnect是“連線/斷開”複用按鈕,btnSend是向伺服器傳送資料的按鈕,只有連線建立之後,才將其setEnabled。

1   tcpClient->disconnectFromHost();
2   if (tcpClient->state() == QAbstractSocket::UnconnectedState \
3        || tcpClient->waitForDisconnected(1000))  //已斷開連線則進入if{}
4   {
5             ui->btnConnect->setText("連線");
6             ui->btnSend->setEnabled(false);
7   }

a)斷開TCP連線的函式:void disconnectFromHost()是從QAbstractSocket繼承的public function,同時它又是一個virtual function。作用為:Attempts to close the socket. If there is pending data waiting to be written, QAbstractSocket will enter ClosingState and wait until all data has been written. Eventually, it will enter UnconnectedState and emit the disconnected() signal.

b)等待TCP斷開連線函式:bool waitForDisconnected(int msecs = 30000),同樣是從QAbstractSocket繼承下來的public function,同時它又是一個virtual function。作用為:Waits until the socket has disconnected, up to msecs milliseconds. If the connection has been disconnected, this function returns true; otherwise it returns false. In the case where it returns false, you can call error() to determine the cause of the error.

3)讀取伺服器傳送過來的資料
readyRead()是QTcpSocket從父類QIODevice中繼承下來的訊號:This signal is emitted once every time new data is available for reading from the device’s current read channel。
readyRead()對應的槽函式為:

1 void MyTcpClient::ReadData()
2 {
3     QByteArray buffer = tcpClient->readAll();
4     if(!buffer.isEmpty())
5     {
6         ui->edtRecv->append(buffer);
7     }
8 }

readAll()是QTcpSocket從QIODevice繼承的public function,直接呼叫就可以讀取從伺服器發過來的資料了。我這裡面把資料顯示在textEditor控制元件上(ui>edtRecv)。由此完成了讀操作。

error(QAbstractSocket::SocketError)是QTcpSocket從QAbstractSocket繼承的signal, This signal is emitted after an error occurred. The socketError parameter describes the type of error that occurred.連線到的槽函式定義為:

1 void MyTcpClient::ReadError(QAbstractSocket::SocketError)
2 {
3     tcpClient->disconnectFromHost();
4     ui->btnConnect->setText(tr("連線"));
5     QMessageBox msgBox;
6     msgBox.setText(tr("failed to connect server because %1").arg(tcpClient->errorString()));
7   8 }

這段函式的作用是:當錯誤發生時,首先斷開TCP連線,再用QMessageBox提示出errorString,即錯誤原因。

4)向伺服器傳送資料

1     QString data = ui->edtSend->toPlainText();
2     if(data != "")
3     {
4         tcpClient->write(data.toLatin1()); //qt5去除了.toAscii()
5     }

定義一個QString變數,從textEditor(edtSend)中獲取帶傳送資料,write()是QTcpSocket從QIODevice繼承的public function,直接呼叫就可以向伺服器傳送資料了。這裡需要注意的是:toAscii()到qt5就沒有了,這裡要寫成toLatin1()。

至此,通過4步,我們就完成了TCP Client的程式開發。

二:伺服器端程式設計
伺服器段程式設計相比於客戶端要繁瑣一些,因為對於客戶端來說,只能連線一個伺服器。而對於伺服器來說,它是面向多連線的,如何協調處理多客戶端連線就顯得尤為重要。
前言:程式設計過程中遇到的問題 和 解決的方法
遇到的問題:每個新加入的客戶端,伺服器給其分配一個SocketDescriptor後,就會emit newConnection()訊號,但分配好的SocketDecriptor並沒有通過newConnection()訊號傳遞,所以使用者得不到這個客戶端標識SocketDescriptor。同樣的,每當伺服器收到新的訊息時,客戶端會emit readReady()訊號,然而readReady()訊號也沒有傳遞SocketDescriptor, 這樣的話,伺服器端即使接收到訊息,也不知道這個訊息是從哪個客戶端發出的。

解決的方法:
1. 通過重寫[virtual protected] void QTcpServer::incomingConnection(qintptr socketDescriptor),獲取soketDescriptor。自定義TcpClient類繼承QTcpSocket,並將獲得的soketDescriptor作為類成員。 這個方法的優點是:可以獲取到soketDescriptor,靈活性高。缺點是:需要重寫函式、自定義類。
2. 在newConnection()訊號對應的槽函式中,通過QTcpSocket *QTcpServer::nextPendingConnection()函式獲取 新連線的客戶端:Returns the next pending connection as a connected QTcpSocket object. 雖然仍然得不到soketDescriptor,但可以通過QTcpSocket類的peerAddress()和peerPort()成員函式獲取客戶端的IP和埠號,同樣是唯一標識。 優點:無需重寫函式和自定義類,程式碼簡潔。缺點:無法獲得SocketDecriptor,靈活性差。

本文介紹第二種方法:

QT提供了QTcpServer類,可以直接例項化一個客戶端,可在help中索引如下:

1 The QTcpServer class provides a TCP-based server. More...
2 Header:     #include <QTcpServer> 
3 qmake:      QT += network
4 Inherits:       QObject

從這裡,我們可以看到,必須要在.pro檔案中新增QT += network才可以進行網路程式設計,否則是訪問不到<QTcpServer>標頭檔案的。

我們看一下程式碼標頭檔案:

 1 #ifndef MYTCPSERVER_H
 2 #define MYTCPSERVER_H
 3 
 4 #include <QMainWindow>
 5 #include <QTcpServer>
 6 #include <QTcpSocket>
 7 #include <QNetworkInterface>
 8 #include <QMessageBox>
 9 namespace Ui {
10 class MyTcpServer;
11 }
12 
13 class MyTcpServer : public QMainWindow
14 {
15     Q_OBJECT
16 
17 public:
18     explicit MyTcpServer(QWidget *parent = 0);
19     ~MyTcpServer();
20 
21 private:
22     Ui::MyTcpServer *ui;
23     QTcpServer *tcpServer;
24     QList<QTcpSocket*> tcpClient;
25     QTcpSocket *currentClient;
26 
27 private slots:
28     void NewConnectionSlot();
29     void disconnectedSlot();
30     void ReadData();
31 
32     void on_btnConnect_clicked();
33     void on_btnSend_clicked();
34     void on_btnClear_clicked();
35 };
36 
37 #endif // MYTCPSERVER_H

值得注意的是,在服務端編寫時,需要同時定義伺服器端變數QTcpServer *tcpServer和客戶端變數 QList<QTcpSocket*> tcpClient。tcpSocket QList儲存了連線到伺服器的所有客戶端。因為QTcpServer並不是QIODevice的子類,所以在QTcpServer中並沒有任何有關讀寫操作的成員函式,讀寫資料的操作全權交由QTcpSocket處理。

1)初始化QTcpServer

1     tcpServer = new QTcpServer(this);
2     ui->edtIP->setText(QNetworkInterface().allAddresses().at(1).toString());   //獲取本地IP
3     ui->btnConnect->setEnabled(true);
4     ui->btnSend->setEnabled(false);
5 
6     connect(tcpServer, SIGNAL(newConnection()), this, SLOT(NewConnectionSlot()));

通過QNetworkInterface().allAddresses().at(1)獲取到本機IP顯示在lineEditor上(edtIP)。

介紹如下: [static] QList<QHostAddress> QNetworkInterface::allAddresses() This convenience function returns all IP addresses found on the host machine. It is equivalent to calling addressEntries() on all the objects returned by allInterfaces() to obtain lists of QHostAddress objects then calling QHostAddress::ip() on each of these.: 每當新的客戶端連線到伺服器時,newConncetion()訊號觸發,NewConnectionSlot()是使用者的槽函式,定義如下:

1 void MyTcpServer::NewConnectionSlot()
2 {
3     currentClient = tcpServer->nextPendingConnection();
4     tcpClient.append(currentClient);
5     ui->cbxConnection->addItem(tr("%1:%2").arg(currentClient->peerAddress().toString().split("::ffff:")[1])\
6                                           .arg(currentClient->peerPort()));
7     connect(currentClient, SIGNAL(readyRead()), this, SLOT(ReadData()));
8     connect(currentClient, SIGNAL(disconnected()), this, SLOT(disconnectedSlot()));
9 }

通過nextPendingConnection()獲得連線過來的客戶端資訊,取到peerAddress和peerPort後顯示在comboBox(cbxConnection)上,並將客戶端的readyRead()訊號連線到伺服器端自定義的讀資料槽函式ReadData()上。將客戶端的disconnected()訊號連線到伺服器端自定義的槽函式disconnectedSlot()上。

2)監聽埠 與 取消監聽

1      bool ok = tcpServer->listen(QHostAddress::Any, ui->edtPort->text().toInt());
2      if(ok)
3      {
4          ui->btnConnect->setText("斷開");
5          ui->btnSend->setEnabled(true);
6      }

a)監聽埠的函式:bool QTcpServer::listen(const QHostAddress &*address* = QHostAddress::Any, quint16 *port* = 0),該函式的作用為:Tells the server to listen for incoming connections on address *address* and port *port*. If port is 0, a port is chosen automatically. If address is QHostAddress::Any, the server will listen on all network interfaces. Returns true on success; otherwise returns false.

 1      for(int i=0; i<tcpClient.length(); i++)//斷開所有連線
 2      {
 3          tcpClient[i]->disconnectFromHost();
 4          bool ok = tcpClient[i]->waitForDisconnected(1000);
 5          if(!ok)
 6          {
 7              // 處理異常
 8          }
 9          tcpClient.removeAt(i);  //從儲存的客戶端列表中取去除
10      }
11      tcpServer->close();     //不再監聽埠

b)斷開客戶端與伺服器連線的函式:disconnectFromHost()和waitForDisconnected()上文已述。斷開連線之後,要將其從QList tcpClient中移除。伺服器取消監聽的函式:tcpServer->close()。

 1     //由於disconnected訊號並未提供SocketDescriptor,所以需要遍歷尋找
 2     for(int i=0; i<tcpClient.length(); i++)
 3     {
 4         if(tcpClient[i]->state() == QAbstractSocket::UnconnectedState)
 5         {
 6             // 刪除儲存在combox中的客戶端資訊
 7             ui->cbxConnection->removeItem(ui->cbxConnection->findText(tr("%1:%2")\
 8                                   .arg(tcpClient[i]->peerAddress().toString().split("::ffff:")[1])\
 9                                   .arg(tcpClient[i]->peerPort())));
10             // 刪除儲存在tcpClient列表中的客戶端資訊
11              tcpClient[i]->destroyed();
12              tcpClient.removeAt(i);
13         }
14     }

c)若某個客戶端斷開了其與伺服器的連線,disconnected()訊號被觸發,但並未傳遞引數。所以使用者需要遍歷tcpClient list來查詢每個tcpClient的state(),若是未連線狀態(UnconnectedState),則刪除combox中的該客戶端,刪除tcpClient列表中的該客戶端,並destroy()。

3)讀取客戶端傳送過來的資料

 1     // 客戶端資料可讀訊號,對應的讀資料槽函式
 2     void MyTcpServer::ReadData()
 3     {
 4         // 由於readyRead訊號並未提供SocketDecriptor,所以需要遍歷所有客戶端
 5         for(int i=0; i<tcpClient.length(); i++)
 6         {
 7             QByteArray buffer = tcpClient[i]->readAll();
 8             if(buffer.isEmpty())    continue;
 9 
10             static QString IP_Port, IP_Port_Pre;
11             IP_Port = tr("[%1:%2]:").arg(tcpClient[i]->peerAddress().toString().split("::ffff:")[1])\
12                                          .arg(tcpClient[i]->peerPort());
13 
14             // 若此次訊息的地址與上次不同,則需顯示此次訊息的客戶端地址
15             if(IP_Port != IP_Port_Pre)
16                 ui->edtRecv->append(IP_Port);
17 
18             ui->edtRecv->append(buffer);
19 
20             //更新ip_port
21             IP_Port_Pre = IP_Port;
22         }
23     }

這裡需要注意的是,雖然tcpClient產生了readReady()訊號,但readReady()訊號並沒有傳遞任何引數,當面向多連線客戶端時,tcpServer並不知道是哪一個tcpClient是資料來源,所以這裡遍歷tcpClient列表來讀取資料(略耗時,上述的解決方法1則不必如此)。 讀操作由tcpClient變數處理:tcpClient[i]->readAll();

4)向客戶端傳送資料

1     //全部連線
2     if(ui->cbxConnection->currentIndex() == 0)
3     {
4         for(int i=0; i<tcpClient.length(); i++)
5             tcpClient[i]->write(data.toLatin1()); //qt5除去了.toAscii()
6     }

a)向當前連線的所有客戶端發資料,遍歷即可。

 1     //指定連線
 2     QString clientIP = ui->cbxConnection->currentText().split(":")[0];
 3     int clientPort = ui->cbxConnection->currentText().split(":")[1].toInt();
 4     for(int i=0; i<tcpClient.length(); i++)
 5     {
 6         if(tcpClient[i]->peerAddress().toString().split("::ffff:")[1]==clientIP\
 7                         && tcpClient[i]->peerPort()==clientPort)
 8         {
 9             tcpClient[i]->write(data.toLatin1());
10             return; //ip:port唯一,無需繼續檢索
11         }
12     }

b)在comboBox(cbxConnction)中選擇指定連線傳送資料:通過peerAddress和peerPort匹配客戶端,併發送。寫操作由tcpClient變數處理:tcpClient[i]->write()。

至此,通過4步,我們就完成了TCP Server的程式開發