Qt 再談TCP/IP(多客戶端連線伺服器) 多個客戶端同時登陸的小聊天室示例
一、TCP和UDP的區別
這裡我會用一個表格來顯示這兩者的區別
比較項 | TCP | UDP |
是否連線 | 面向連線 | 無連線 |
傳輸是否可靠 | 可靠 | 不可靠 |
流量控制 | 提供 | 不提供 |
工作方式 | 全雙工 | 可以是全雙工 |
應用場合 | 大量資料 | 少量資料 |
速度 | 慢 | 快 |
二、incomingConnection函式
這個函式和之前講過的newConnection訊號功能差不多,只要有新的連接出現,就會自動呼叫這個函式。
然後我們只需在這個函式中新建一個QTcpSocket物件,並且將這個套接字指定為這個函式的引數socketDescriptor,然後將這個套接字存放到套接字列表中就可以實現多個客戶端同時登陸了。
這裡我們簡單看一下這個函式裡的內容
1 void Server::incomingConnection(int socketDescriptor)
2 {
3 TcpClientSocket *tcpclientsocket = new TcpClientSocket(this);//只要有新的連線就生成一個新的通訊套接字
4 //將新建立的通訊套接字描述符指定為引數socketdescriptor
5 tcpclientsocket->setSocketDescriptor(socketDescriptor);
6
7 //將這個套接字加入客戶端套接字列表中
8 tcpclientsocketlist.append(tcpclientsocket);
9 }
Server這個類是繼承於QTcpServer類的,所以我們需要在Server類中重寫incomingConnection函式。
三、多個客戶端同時登陸的小聊天室示例。
首先說明一下這個小示例的功能,當有一個客戶端進入聊天室的時候,會向伺服器傳送一個資訊,告訴伺服器這個賬號進入了聊天室, 然後伺服器會在介面中顯示哪個賬號進入了聊天室,並且伺服器會向所有的客戶端傳送訊息,告訴所有客戶端有哪個賬號進入了聊天室。某個賬號傳送資訊的時候也是如此,伺服器會將收到的資訊傳送給所有的客戶端。
如果需要實現網路通訊,就必須在pro檔案中加入 QT += network
伺服器端
在伺服器端需要新增三個類,一個類用來建立介面,一個類用來監聽,這個類的基類是QTcpServer,最後一個類用來通訊,通訊的類的基類是QTcpSocket類。
1、用來建立介面的Tcpserver類
1 #ifndef TCPSERVER_H
2 #define TCPSERVER_H
3
4 #include <QWidget>
5 #include "server.h"
6
7 namespace Ui {
8 class TcpServer;
9 }
10
11 class TcpServer : public QWidget
12 {
13 Q_OBJECT
14
15 public:
16 explicit TcpServer(QWidget *parent = 0);
17 ~TcpServer();
18
19 private:
20 Ui::TcpServer *ui;
21 int port;
22 Server *server;
23
24 protected slots:
25 void slotupdateserver(QString, int);//接收到server發過來的訊號就更新介面的資訊
26
27
28 private slots:
29 void on_pushButtonCreateChattingRoom_clicked();
30 };
31
32 #endif // TCPSERVER_H
1 #include "tcpserver.h"
2 #include "ui_tcpserver.h"
3
4
5 TcpServer::TcpServer(QWidget *parent) :
6 QWidget(parent),
7 ui(new Ui::TcpServer)
8 {
9 ui->setupUi(this);
10 port = 8888;
11
12 }
13
14 TcpServer::~TcpServer()
15 {
16 delete ui;
17 }
18
19 void TcpServer::on_pushButtonCreateChattingRoom_clicked()
20 {
21 server = new Server(this, port);
22 connect(server, &Server::updateserver, this, &TcpServer::slotupdateserver);
23 ui->pushButtonCreateChattingRoom->setEnabled(false);
24 }
25
26 void TcpServer::slotupdateserver(QString msg, int length)
27 {
28 ui->textEdit->append(msg);
29 }
2、用來監聽的類Server
1 #ifndef SERVER_H
2 #define SERVER_H
3
4 #include <QTcpServer>
5 #include <QObject>
6 #include <QList>
7 #include "tcpclientsocket.h"
8
9 class Server : public QTcpServer
10 {
11 Q_OBJECT //為了實現訊號和槽的通訊
12 public:
13 Server(QObject *parent = 0, int port = 0);
14 QList<TcpClientSocket*> tcpclientsocketlist;
15 protected:
16 void incomingConnection(int socketDescriptor);//只要出現一個新的連線,就會自動呼叫這個函式
17 protected slots:
18 void sliotupdateserver(QString, int);//用來處理tcpclient發過來的訊號
19 void slotclientdisconnect(int);
20
21 signals:
22 void updateserver(QString, int);//傳送訊號給介面,讓介面更新資訊
23
24 };
25
26 #endif // SERVER_H
1 #include "server.h"
2 #include <QHostAddress>
3
4 Server::Server(QObject *parent, int port):QTcpServer(parent)
5 {
6 listen(QHostAddress::Any, port); //監聽
7 }
8
9 void Server::incomingConnection(int socketDescriptor)
10 {
11 TcpClientSocket *tcpclientsocket = new TcpClientSocket(this);//只要有新的連線就生成一個新的通訊套接字
12 //將新建立的通訊套接字描述符指定為引數socketdescriptor
13 tcpclientsocket->setSocketDescriptor(socketDescriptor);
14
15 //將這個套接字加入客戶端套接字列表中
16 tcpclientsocketlist.append(tcpclientsocket);
17
18
19 //接收到tcpclientsocket傳送過來的更新介面的訊號
20 connect(tcpclientsocket, &TcpClientSocket::updateserver, this, &Server::sliotupdateserver);
21 connect(tcpclientsocket, &TcpClientSocket::clientdisconnected, this, &Server::slotclientdisconnect);
22
23 }
24
25 void Server::sliotupdateserver(QString msg, int length)
26 {
27 //將這個訊號傳送給介面
28 emit updateserver(msg, length);
29
30 //將收到的資訊傳送給每個客戶端,從套接字列表中找到需要接收的套接字
31 for(int i = 0; i < tcpclientsocketlist.count(); i++)
32 {
33 QTcpSocket *item = tcpclientsocketlist.at(i);
34 // if(item->write((char*)msg.toUtf8().data(), length) != length)
35 // {
36 // continue;
37 // }
38 item->write(msg.toUtf8().data());
39 }
40
41 }
42
43 void Server::slotclientdisconnect(int descriptor)
44 {
45 for(int i = 0; i < tcpclientsocketlist.count(); i++)
46 {
47 QTcpSocket *item = tcpclientsocketlist.at(i);
48 if(item->socketDescriptor() == descriptor)
49 {
50 tcpclientsocketlist.removeAt(i);//如果有客戶端斷開連線, 就將列表中的套接字刪除
51 return;
52 }
53 }
54 return;
55 }
3、用來通訊的類TcpClientSocket
1 #ifndef TCPCLIENTSOCKET_H
2 #define TCPCLIENTSOCKET_H
3
4 #include <QTcpSocket>
5
6 class TcpClientSocket : public QTcpSocket
7 {
8 Q_OBJECT //新增這個巨集是為了實現訊號和槽的通訊
9 public:
10 TcpClientSocket(QObject *parent = 0);
11 protected slots:
12 void receivedata();//處理readyRead訊號讀取資料
13 void slotclientdisconnected();//客戶端斷開連線觸發disconnected訊號,這個槽函式用來處理這個訊號
14
15 signals:
16 void updateserver(QString, int);//用來告訴tcpserver需要跟新介面的顯示
17 void clientdisconnected(int); //告訴server有客戶端斷開連線
18 };
19
20 #endif // TCPCLIENTSOCKET_H
1 #include "tcpclientsocket.h"
2
3 TcpClientSocket::TcpClientSocket(QObject *parent)
4 {
5 //客戶端傳送資料過來就會觸發readyRead訊號
6 connect(this, &TcpClientSocket::readyRead, this, &TcpClientSocket::receivedata);
7 connect(this, &TcpClientSocket::disconnected, this, &TcpClientSocket::slotclientdisconnected);
8 }
9
10 void TcpClientSocket::receivedata()
11 {
12 // while(bytesAvailable() > 0)
13 // {
14 // int length = bytesAvailable();
15 // char buf[1024]; //用來存放獲取的資料
16 // read(buf, length);
17 // QString msg = buf;
18 // //發訊號給介面,讓介面顯示登入者的資訊
19 // emit updateserver(msg, length);
20 // }
21 int length = 10;
22 QByteArray array = readAll();
23 QString msg = array;
24 emit updateserver(msg, length);
25 }
26
27 void TcpClientSocket::slotclientdisconnected()
28 {
29 emit clientdisconnected(this->socketDescriptor());
30 }
客戶端
客戶端只需要一個類就行了,這個類我們只需要建立一個通訊套接字來和伺服器進行通訊就可以了。
1 #ifndef TCPCLIENT_H
2 #define TCPCLIENT_H
3
4 #include <QWidget>
5 #include <QTcpSocket>
6
7 namespace Ui {
8 class TcpClient;
9 }
10
11 class TcpClient : public QWidget
12 {
13 Q_OBJECT
14
15 public:
16 explicit TcpClient(QWidget *parent = 0);
17 ~TcpClient();
18
19 private slots:
20 void on_pushButtonEnter_clicked();
21 void slotconnectedsuccess();//用來處理連線成功的訊號
22 void slotreceive();//接收伺服器傳過來的資訊
23 void on_pushButtonSend_clicked();
24 void slotdisconnected();//用來處理離開聊天室的訊號
25
26
27 private:
28 Ui::TcpClient *ui;
29 bool status;//用來判斷是否進入了聊天室
30 int port;
31 QHostAddress *serverIP;
32 QString userName;
33 QTcpSocket *tcpsocket;
34 };
35
36 #endif // TCPCLIENT_H
1 #include "tcpclient.h"
2 #include "ui_tcpclient.h"
3 #include <QHostAddress>
4 #include <QMessageBox>
5
6 TcpClient::TcpClient(QWidget *parent) :
7 QWidget(parent),
8 ui(new Ui::TcpClient)
9 {
10 ui->setupUi(this);
11 //將進入聊天室的標誌位置為false
12 status = false;
13 //埠為8888
14 port = 8888;
15 ui->lineEditServerPort->setText(QString::number(port));//介面中埠預設顯示8888
16
17 serverIP = new QHostAddress();
18
19 //未進入聊天室內不能傳送資訊
20 ui->pushButtonSend->setEnabled(false);
21 }
22
23 TcpClient::~TcpClient()
24 {
25 delete ui;
26 }
27
28 //進入聊天室
29 void TcpClient::on_pushButtonEnter_clicked()
30 {
31 //首先判斷這個使用者是不是在聊天室中
32 if(status == false)
33 {
34 //不在聊天室中就和伺服器進行連線
35 QString ip = ui->lineEditServerIp->text();//從介面獲取ip地址
36 if(!serverIP->setAddress(ip))//用這個函式判斷IP地址是否可以被正確解析
37 {
38 //不能被正確解析就彈出一個警告視窗
39 QMessageBox::warning(this, "錯誤", "IP地址不正確");
40 return;
41 }
42 if(ui->lineEditUserName->text() == "")
43 {
44 //使用者名稱不能為空
45 QMessageBox::warning(this, "錯誤", "使用者名稱不能為空");
46 return;
47 }
48
49 //從介面獲取使用者名稱
50 userName = ui->lineEditUserName->text();
51 //建立一個通訊套接字,用來和伺服器進行通訊
52 tcpsocket = new QTcpSocket(this);
53
54 //和伺服器進行連線
55 tcpsocket->connectToHost(*serverIP, port);
56
57 //和伺服器連線成功能會觸發connected訊號
58 connect(tcpsocket, &QTcpSocket::connected, this, &TcpClient::slotconnectedsuccess);
59 //接收到伺服器的資訊就會觸發readyRead訊號
60 connect(tcpsocket, &QTcpSocket::readyRead, this, &TcpClient::slotreceive);
61
62
63
64 //將進入聊天室的標誌位置為true;
65 status = true;
66 }
67 else//已經進入了聊天室
68 {
69 int length = 0;
70 QString msg = userName + ":Leave Chat Room";
71 // if((length = tcpsocket->write((char*)msg.toUtf8().data(), msg.length())) != msg.length())
72 // {
73 // return;
74 // }
75 tcpsocket->write(msg.toUtf8().data());
76 tcpsocket->disconnectFromHost();
77 status = false;
78 //離開聊天室就會觸發disconnected訊號
79 connect(tcpsocket, &QTcpSocket::disconnected, this, &TcpClient::slotdisconnected);
80 }
81 }
82
83 //用來處理連線成功的訊號
84 void TcpClient::slotconnectedsuccess()
85 {
86 //進入聊天室可以傳送資訊了
87 ui->pushButtonSend->setEnabled(true);
88 //將進入聊天的按鈕改為離開聊天室
89 ui->pushButtonEnter->setText("離開聊天室");
90
91 int length = 0;
92 //將使用者名稱傳送給伺服器
93 QString msg= userName + " :Enter Chat Room";
94
95 // if((length = tcpsocket->write((char*)msg.toUtf8().data(), msg.length())) != msg.length())
96 // {
97 // return;
98 // }
99 tcpsocket->write(msg.toUtf8().data());
100 }
101
102
103 void TcpClient::slotreceive()
104 {
105 // while(tcpsocket->bytesAvailable() > 0 )
106 // {
107 // QByteArray datagram;
108 // datagram.resize(tcpsocket->bytesAvailable());
109 // tcpsocket->read(datagram.data(), datagram.size());
110 // QString msg = datagram.data();
111 // ui->textEdit->append(msg.left(datagram.size()));
112 // }
113 QByteArray array = tcpsocket->readAll();
114 ui->textEdit->append(array);
115 }
116
117 void TcpClient::on_pushButtonSend_clicked()
118 {
119 if(ui->lineEditSend->text() == "")
120 {
121 return;
122 }
123 QString msg = userName + ":" + ui->lineEditSend->text();
124 // tcpsocket->write((char*)msg.toUtf8().data(), msg.length());
125 tcpsocket->write(msg.toUtf8().data());
126 ui->lineEditSend->clear();
127 }
128
129 void TcpClient::slotdisconnected()
130 {
131 ui->pushButtonSend->setEnabled(false);
132 ui->pushButtonEnter->setText("進入聊天室");
133 }
編譯完之後執行的介面就是這樣的
整個通訊的步驟
首先我們點選伺服器的建立聊天室,TcpServer類的中的相應的槽函式會建立一個Server類的物件,然後呼叫建構函式會進行監聽。一旦有客戶端點選進入聊天室的按鈕,客戶端的tcpsocket就會和伺服器進行連線,只要伺服器監聽到新的連線,Server類物件就會自動呼叫incomingConnection(int socketDescripter)這個函式。接著我們只需要在這個函式中建立一個通訊套接字也就是TcpClientSocket物件,並且將這個套接字描述符設定為引數socketDescripter,然後將這個套接字加入套接字列表中。這個時候客戶端和伺服器連線成功了,客戶端就會觸發一個connected訊號,需要我們寫一個槽函式來處理這個訊號,這裡的處理就是將這個使用者名稱和“Enter Chat Room”傳送給伺服器。伺服器一旦收到資訊,就會觸發readyRead訊號,這個時候我們需要在寫一個相應的槽函式來讀取套接字中的資訊。我們這裡是用readall來讀取套接字中的所有資訊。當收到資料的時候,我們需要發一個訊號給Server,將讀取的資訊再發送給所有在聊天室裡的使用者,所以在Server類中要新增一個相應的槽函式來將資料傳送給所有的使用者。因為同時需要在伺服器介面中顯示所有使用者發過來的訊息,所以需要在剛剛那個槽函式中將自己定義的一個訊號傳送給TcpServer類,讓其將收到的訊息顯示在介面中。客戶端收到訊息之後也會觸發readyRead訊號,客戶端只需要將從套接字中讀取的訊息顯示在見面中就行了。客戶端傳送訊息的時候需要從QLineEdit物件中讀取內容,然後通過tcpsocket->write()函式將訊息傳送給伺服器就行了。伺服器同樣會將收到的資訊傳送給所有的使用者,同時顯示在自己的介面中。最後客戶端退出聊天室,客戶端點選離開聊天室按鈕,進入相應的槽函式中呼叫disconnectFromHost()函式,就會和伺服器斷開連線,一旦有呼叫了這個函式,就會觸發disconnected訊號,然後我們需要寫一個相應的槽函式來處理這個訊號,我們所做的處理就是將這個使用者名稱和“Leave Chat Roo”傳送給伺服器。伺服器那一端檢測到有客戶端斷開連線也會觸發disconnected訊號,這個時候我們需要將套接字列表中將對應的套接字刪除就可以了。