首先介紹一下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的程序開發