Qt實(shí)現(xiàn)簡單TCP服務(wù)器
本文實(shí)例為大家分享了Qt學(xué)習(xí)記錄之簡單的TCP服務(wù)器,供大家參考,具體內(nèi)容如下
簡單的多連接TCP服務(wù)器?
本節(jié)我們使用Qt來編寫一個(gè)簡單的多連接TCP服務(wù)器程序,涉及到的功能有監(jiān)聽本地IP、打印上線客戶端的IP端口號,接收客戶端發(fā)來的文字信息并打印其IP端口號、單獨(dú)或全部地向客戶端發(fā)送文字信息、顯示下線客戶端的IP端口號,并具有踢人的功能。
? 該程序使用正點(diǎn)原子的網(wǎng)絡(luò)助手來驗(yàn)證功能。Qt基于5.9.9版本。
1、創(chuàng)建工程以及配置工作
創(chuàng)建工程的過程就不再介紹了,這里我選擇的是 QWidget ,因?yàn)楸容^簡單。
然后我們在 .pro 文件中添加網(wǎng)絡(luò)的模塊,否則待會添加頭文件時(shí)會提示沒有這個(gè)頭文件。
其他東西不需要看,只要在這后面加上 network 即可。
再然后,在 widget.h 頭文件中包含兩個(gè)頭文件:
#include <QTcpServer> #include <QTcpSocket>
使用Qt搭建TCP服務(wù)器需要兩個(gè)套接字:
- 一個(gè)是QTcpServer套接字,這是是用來監(jiān)聽本地的某個(gè)IP及端口的。監(jiān)聽成功后,其他的TCP客戶端就可以連接這個(gè)服務(wù)器了(當(dāng)然前提是這個(gè)服務(wù)器監(jiān)聽的IP是公網(wǎng)IP,或者客戶端與服務(wù)器在同一局域網(wǎng)下)
- 一個(gè)是QTcpSocket套接字,這個(gè)是服務(wù)器與客戶端通信用的套接字。每當(dāng)有一臺客戶端連接上了這臺服務(wù)器,都會產(chǎn)生這樣的一個(gè)套接字??蛻舳丝梢酝ㄟ^某個(gè)套接字來查看某個(gè)客戶端的IP地址與端口號等信息,也可以拿著這個(gè)套接字單獨(dú)地與這一個(gè)客戶端收發(fā)數(shù)據(jù)。理論上來講,一個(gè)TCP服務(wù)器是可以被無限個(gè)客戶端連上的。
這里也插一句,如果使用Qt搭建一個(gè)TCP客戶端,只需要QTcpSocket套接字。
2、ui界面的設(shè)計(jì)
1、comboBox。下拉列表框,用來選擇監(jiān)聽的IP地址,因?yàn)槟軌虮槐O(jiān)聽的地址肯定是本機(jī)擁有的地址,可以是有線、無線網(wǎng)卡的IP地址(局域網(wǎng)),也可以是寬帶分配到的IP地址(廣域網(wǎng))。下一節(jié)我們再來學(xué)習(xí)如何查看本機(jī)支持的IP。
2、portEdit。旁邊的單行文本框是用來輸入要監(jiān)聽的端口號。有時(shí)候IP的某個(gè)端口號已經(jīng)被某個(gè)程序占用,此時(shí)再去監(jiān)聽就會監(jiān)聽失敗。
3、openbtn。點(diǎn)擊開啟按鈕,監(jiān)聽選定的IP地址及端口號。監(jiān)聽失敗會在Qt Creator的控制臺打印失敗的信息。監(jiān)聽成功,同樣也會打印信息,并且IP下拉框、端口號文本框還有本按鈕控件都會變成不可選中狀態(tài)。
4、comboBox_2。選擇某個(gè)已連接的客戶端(如果有的話),或者選擇全部。這個(gè)可以查看連接上服務(wù)器的客戶端IP及端口號,單獨(dú)或全部地向客戶端發(fā)送信息,或者強(qiáng)制踢下線。
5、kickbtn。點(diǎn)擊按鈕,強(qiáng)制使選中的客戶端下線。
6、closebtn。關(guān)閉服務(wù)器。停止監(jiān)聽之前選中的IP及端口號,并斷開全部的TCP連接。
7、recvEdit。接收文本框。當(dāng)客戶端發(fā)來信息,會打印在上面。
8、clearbtn。清除接收文本框內(nèi)的全部內(nèi)容。
9、pushButton。在控制臺上打印全部連接的客戶端IP及端口號。
10、sendEdit。發(fā)送文本框。
11、sendbtn。點(diǎn)擊發(fā)送按鈕,會向某個(gè)、全部客戶端發(fā)送文本框內(nèi)的信息
大體的設(shè)計(jì)就是這樣,目前只滿足了基本的功能,后續(xù)可以增添更多的功能,并對界面進(jìn)行美化
3、本地IP的獲取
從這一節(jié)開始,我們將正式進(jìn)行代碼的編寫
首先,打開命令行,輸入 ipconfig
這四個(gè)IP地址就我我電腦上可以監(jiān)聽的IP地址,我的程序就是以此來搭建TCP服務(wù)器。其他的IP地址,目前是監(jiān)聽不了的。
Qt獲取本機(jī)IP
首先在 widge.cpp 文件中包含一個(gè)頭文件
#include <QtNetwork>
然后在Widget類的構(gòu)造函數(shù)中添加如下代碼:
/*讀取本機(jī)網(wǎng)卡信息...*/ QString localHostName = QHostInfo::localHostName(); QHostInfo info = QHostInfo::fromName(localHostName); /*將本機(jī)所有的IPV4地址添加到comboBox下.*/ foreach(QHostAddress ipAddress, info.addresses()) ? ? { ? ? ? ? if(ipAddress.protocol() == QAbstractSocket::IPv4Protocol) ? ? ? ? { ? ? ? ? ? ? qDebug() << ipAddress.toString(); ? ? ? ? ? ? ui->comboBox->addItem(ipAddress.toString()); ? ? ? ? } ? ? }
這里解釋幾點(diǎn):
- 調(diào)用
info.addresses()
成員函數(shù),返回的是一個(gè) QList 容器,里面包含著本機(jī)全部的IP地址 - 所有的IP地址都在一個(gè)容器中,我們需要遍歷這個(gè)容器,將里面的IP地址一一取出,這里用到了 foreach 去遍歷。和for循環(huán)類似,首先定義一個(gè)用于接收的對象:ipAddress,然后會一一讀取容器中的內(nèi)容,賦值給這個(gè)對象。如果暫時(shí)弄不明白也沒有關(guān)系,看多了自然就懂了?;蛘邔W(xué)過python,這句語句和
for ipAddr in ipAddrList:
一樣。 if(ipAddress.protocol() == QAbstractSocket::IPv4Protocol)
這句用來判斷遍歷到的IP是不是IPV4的地址。因?yàn)槟壳癐PV6還沒有完全普及,而且不如IPV4易讀。- 下拉列表控件使用
addItem()
成員函數(shù)添加元素。
點(diǎn)擊運(yùn)行,控制臺就會打印信息,并且下拉列表也會顯示信息:
4、開啟服務(wù)器
首先要有一個(gè) QTcpServer 套接字對象,可以直接在 Widget 類中聲明這么一個(gè)對象;也可以在類中聲明一個(gè)對象指針,然后在構(gòu)造函數(shù)中 new 一個(gè)對象,讓這個(gè)指針指向它。這里我選擇第二種方法。
/*在widget.h中類定義里添加*/ QTcpServer *server; /*在widget.cpp中構(gòu)造函數(shù)中添加*/ server = new QTcpServer(this);
然后為開啟按鈕添加槽函數(shù),這里我圖方便直接右擊控件并點(diǎn)擊轉(zhuǎn)到槽。
void Widget::on_openbtn_clicked() { ? ? /*監(jiān)聽本地IP加端口號.*/ ? ? if(server->listen(QHostAddress(ui->comboBox->currentText()), ui->portEdit->text().toInt()) == false) ? ? { ? ? ? ? /*監(jiān)聽失敗,打印信息.*/ ? ? ? ? qDebug()<<"listen false"; ? ? } ? ? else ? ? { ? ? ? ? /*監(jiān)聽成功,則將一些控件鎖死.*/ ? ? ? ? ui->openbtn->setDisabled(true); ? ? ? ? ui->portEdit->setDisabled(true); ? ? ? ? ui->comboBox->setDisabled(true); ? ? ? ? qDebug()<<"監(jiān)聽成功"; ? ? ? ? /*激活關(guān)閉按鈕.*/ ? ? ? ? ui->closebtn->setEnabled(true); ? ? } }
解釋:
對于下拉列表控件(comboBox),使用currentText()
成員函數(shù)獲取選中元素的信息,返回QString型。
對于單行文本控件(portEdit),使用text()
成員函數(shù)返回文本,并用toInt()
轉(zhuǎn)化為int型
對于TCP服務(wù)器套接字(server),使用listen(const QHostAddress &address = QHostAddress::Any, quint16 port = 0)
成員函數(shù)綁定IP及端口。
對于大部分控件,使用setDisabled(true)
成員函數(shù)可使控件變?yōu)椴豢牲c(diǎn)擊狀態(tài)
最后,我們就可以開啟服務(wù)器了。如果開啟失敗,往往是因?yàn)檫x擇的端口號被占用,這里推薦使用8081這個(gè)端口號(8080經(jīng)常會被占用)。開啟成功后,我們就可以使用正點(diǎn)原子的網(wǎng)絡(luò)助手去嘗試連接它。
可以看到,右邊的網(wǎng)絡(luò)助手已經(jīng)處于連接成功的狀態(tài),因此可以證明TCP服務(wù)器已經(jīng)被成功開啟。其他的東西暫時(shí)不用看,后面會一一實(shí)現(xiàn)。
5、服務(wù)器等待客戶端連接
在構(gòu)造函數(shù)中添加如下代碼:
connect(server, &QTcpServer::newConnection, this, [=](){ ? /*獲取新連接客戶端的socket*/ ? ? ? ? QTcpSocket *socket = server->nextPendingConnection(); ? ? ? ?? ? ? ? ? /*將這個(gè)socket添加到List容器中...*/ ? ? ? ? sockList.append(socket); ? ? ? ?? ? ? ? ? /*獲取客戶端的IP地址和端口號信息,并轉(zhuǎn)換為字符串.*/ ? ? ? ? QString info = socket->peerAddress().toString() \ ? ? ? ? ? ? ? ? + ':' + QString::number(socket->peerPort()); ? ? ? ?? ? ? ? ? /*將信息打印到文本框.*/ ? ? ? ? ui->recvEdit->append("已連接:"+info); ? ? ? ?? ? ? ? ? /*將客戶端的信息添加到comboBox_2下.*/ ? ? ? ? ui->comboBox_2->addItem(info); ? ? ? ? /*將新連接的socket對象的可以讀取信號連接到接收槽函數(shù).*/ ? ? ? ? connect(socket, &QTcpSocket::readyRead, this, &Widget::on_recv); ? ? ? ? /*將新連接的socket對象的斷開連接信號連接到斷開槽函數(shù).*/ ? ? ? ? connect(socket, &QTcpSocket::disconnected, this, &Widget::on_disconnect); ? ? });
解釋:
- 當(dāng)有客戶端連接到服務(wù)器,服務(wù)器套接字會觸發(fā)一個(gè) newConnection 信號。
- 服務(wù)器套接字通過
nextPendingConnection()
成員函數(shù)獲取到用于和客戶端通信的socket(QTcpSocket)。 - QTcpSocket 套接字通過
peerAddress()
和peerPort()
成員函數(shù),得到該客戶端的IP及端口號。toString()
是為了把IP信息轉(zhuǎn)化為字符串,可以試試打印轉(zhuǎn)化前的樣子。QString::number(long n, int base = 10)
用于將數(shù)字轉(zhuǎn)化為字符串。 - 接收文本框(recvEdit)使用
append()
成員函數(shù)打印文字。 - 最后的兩個(gè)信號和槽的連接暫時(shí)不用看
- 這里我還使用了一個(gè) QList 容器來保存已連接客戶端的socket
/*在widget.h中類定義里添加*/ QList<QTcpSocket *>sockList;
最后,我們就可以檢驗(yàn)一下多連接時(shí)的狀態(tài)了
6、實(shí)現(xiàn)接收數(shù)據(jù)
如果用于通信的socket(QTcpSocket)接收到數(shù)據(jù),就會觸發(fā) QTcpSocket::readyRead 信號,我這里寫了一個(gè)槽函數(shù),專門用來接收數(shù)據(jù):
void Widget::on_recv() { ? ? /*找到觸發(fā)信號的那個(gè)socket對象.*/ ? ? QTcpSocket *sock = qobject_cast<QTcpSocket *>(sender()); ? ? /*讀取信息并轉(zhuǎn)化為字符串.*/ ? ? QString info = "來自" + sock->peerAddress().toString() \ ? ? ? ? ? ? + ':' + QString::number(sock->peerPort()); ? ? /*將客戶端的信息打印到文本框.*/ ? ? ui->recvEdit->append(info); ? ? /*將接收到的數(shù)據(jù)也打印到文本框.*/ ? ? ui->recvEdit->append(sock->readAll()); }
解釋:
- 第一句是用來找到觸發(fā)信號的那個(gè)對象(QTcpSocket)。因?yàn)檫@個(gè)程序允許多個(gè)客戶端連接,每個(gè)客戶端在連接后,server都會獲取到與之相對應(yīng)的QTcpSocket套接字對象,意思就是說套接字是與客戶端一一對應(yīng)的。因此找到觸發(fā)信號的QTcpSocket對象,就等于找到了發(fā)送消息的那個(gè)客戶端。
- 同樣的,將客戶端的信息打包成字符串,并且顯示到接收文本框
- QTcpSocket對象,調(diào)用
readAll()
成員函數(shù),來獲取接收到的信息。
現(xiàn)在也可以解釋上一節(jié)中的connect(socket, &QTcpSocket::readyRead, this, &Widget::on_recv);
了??赡苡腥藭苫螅@里的socket是局部變量,調(diào)用完會被釋放掉,那么這個(gè)連接是不是傳了個(gè)野指針進(jìn)去?其實(shí)并不是的,socket是個(gè)指針,指向server->nextPendingConnection()
返回的QTcpSocket對象,真正的對象應(yīng)該是存在內(nèi)部堆內(nèi)存中的,這里只是用了一個(gè)局部指針變量去接收它。而connect函數(shù),傳進(jìn)去的是地址,地址自然就是內(nèi)部QTcpSocket對象的地址了。
最后,演示一下:
注意:Qt使用utf-8編碼格式,而原子的軟件默認(rèn)是GBk格式,所以發(fā)送中文之前,先把原子的軟件全部改成utf-8格式。
實(shí)現(xiàn)清除數(shù)據(jù)
這個(gè)非常簡單,就沒有必要多說了:
void Widget::on_clearbtn_clicked() { ? ? /*點(diǎn)擊清除按鈕則清除接收文本框.*/ ? ? ui->recvEdit->clear(); }
7、實(shí)現(xiàn)客戶端的選擇
對客戶端的選擇無非就兩種情況:一種情況是選擇全部連接上的客戶端,一種是選擇單獨(dú)的某個(gè)客戶端。
我為Widget類添加了一個(gè)屬性來解決這兩類問題:
QTcpSocket *currSock;
如果第二個(gè)下拉列表框選擇了 All ,那么這個(gè)指針就會指向 NULL 。如果選擇的是某個(gè)特定的IP及端口,那么這個(gè)指針就會指向?qū)?yīng)的那個(gè)QTcpSocket對象了。
那么對象從哪里找?從前幾節(jié)說過的容器中去找。這個(gè)容器用于保存當(dāng)前連接上的客戶端的socket。每當(dāng)有客戶端連上,這個(gè)容器就會將它的socket保存進(jìn)來。每當(dāng)有客戶端下線,這個(gè)容器同樣會把下線客戶端的socket刪掉。
QList<QTcpSocket *>sockList;
- QList 表示這是一個(gè) QList 類型的容器,還有其他類型的容器
- 尖括號內(nèi)的東西,表示這個(gè)一個(gè)裝QTcpSocket對象指針的容器。因?yàn)檫@是一個(gè)模板類,還可以是int型的容器,取決于自己的定義。
- 最后的 sockList 則代表這個(gè)容器的名字
下拉列表框的信號和槽
這里我同樣是以右擊控件再點(diǎn)轉(zhuǎn)到槽的方式來自動生成槽函數(shù):
這里的信號不要選錯(cuò)了,當(dāng)這個(gè)控件選擇的選項(xiàng)發(fā)送變化時(shí),會觸發(fā)這個(gè)信號。下面是槽函數(shù):
void Widget::on_comboBox_2_currentIndexChanged(const QString &arg1) { ? ? /*如果當(dāng)前選擇的是All,當(dāng)前sock指針就指向空.*/ ? ? if(arg1 == "All") ? ? { ? ? ? ? currSock = NULL; ? ? ? ? return; ? ? } ? ? /*不然就讀取選中的信息,將其拆分為IP地址和端口號.*/ ? ? QStringList info = arg1.split(':'); ? ? QString ip = info[0]; ? ? int port = info[1].toInt(); ? ? /*遍歷容器,找到對應(yīng)的那個(gè)socket.*/ ? ? foreach(QTcpSocket *sock, sockList) ? ? { ? ? ? ? if(sock->peerAddress().toString() == ip && sock->peerPort() == port) ? ? ? ? { ? ? ? ? ? ? /*當(dāng)前sock指針指向找到的那個(gè)socket.*/ ? ? ? ? ? ? currSock = sock; ? ? ? ? ? ? break; ? ? ? ? } ? ? } }
解釋:
- 這個(gè)槽函數(shù)是帶有參數(shù)的,參數(shù)就是選中選項(xiàng)的字符串。
- 后面的代碼通過冒號用來拆分字符串。比如某個(gè)選項(xiàng)上面的內(nèi)容是“192.168.1.1:8080”,調(diào)用這些代碼后,就會被拆分成IP地址和端口號。
- 最后遍歷容器內(nèi)的所有socket,找到IP與端口與選項(xiàng)中一樣的那個(gè)socket。
這個(gè)功能暫時(shí)不太好演示,大家可以自己弄個(gè)按鈕控件,點(diǎn)擊按鈕則打印 currSock 指向 socket 的信息,來檢驗(yàn)一下。
8、實(shí)現(xiàn)發(fā)送功能
到了這一步,程序就開始越寫越簡單了
為發(fā)送按鈕添加一個(gè)槽函數(shù):
void Widget::on_sendbtn_clicked() { ? ? /*如果是為開啟服務(wù)器、未連接或者選擇All的時(shí)候,currSock才會指向空 ? ? ? ? 前兩種情況容器是空的,所以也不會出錯(cuò)。如果是選中All時(shí)候, ? ? ? ? 且多個(gè)客戶端連接,則會遍歷容器中所有的socket,并且一一發(fā)送出去.*/ ? ? if(currSock == NULL) ? ? { ? ? ? ? foreach(QTcpSocket *sock, sockList) ? ? ? ? { ? ? ? ? ? ? sock->write(ui->sendEdit->toPlainText().toUtf8()); ? ? ? ? } ? ? } ? ? else ? ? { ? ? ? ? /*如果選擇的是一個(gè)特定的客戶端,則只向它發(fā)送.*/ ? ? ? ? currSock->write(ui->sendEdit->toPlainText().toUtf8()); ? ? } }
解釋:
當(dāng) currSock 指針指向空的時(shí)候,可能有以下幾種狀態(tài):
程序剛剛初始化完成,此時(shí)還沒有監(jiān)聽
程序已經(jīng)開始監(jiān)聽,且有一個(gè)或多個(gè)客戶端上線
程序開始監(jiān)聽,還沒有客戶端連接,或者之前連接的客戶端全部下線
對于1、3兩種情況,sockList 容器中是空的,因此即使遍歷也遍歷不到任何東西,自然也就不會給不存在的socket發(fā)送信息。對于第二種情況,也就不用多解釋了。當(dāng)然,這樣的程序可能是存在問題的??梢远嘧鲆恍┡袛啵⑶覐棾鼍嫣崾究颍嵝延脩糇隽隋e(cuò)誤的操作。
最后,演示一下。服務(wù)器先給1號客戶端發(fā)送你好1,接著分別給2、3號客戶端發(fā)送你好2、3,最后給所有的客戶端發(fā)送你好123:
9、實(shí)現(xiàn)客戶端下線
之前我們在構(gòu)造函數(shù)中添加了這一句代碼:
connect(socket, &QTcpSocket::disconnected, this, &Widget::on_disconnect);
把套接字的斷開連接信號連接到了斷開連接槽函數(shù),槽函數(shù)內(nèi)容如下:
void Widget::on_disconnect() { ? ? /*找到觸發(fā)信號的那個(gè)socket對象.*/ ? ? QTcpSocket *sock = qobject_cast<QTcpSocket *>(sender()); ? ? /*將斷開連接的那個(gè)socket從List容器中移除.*/ ? ? sockList.removeOne(sock); ? ?? ? ? /*將客戶端信息轉(zhuǎn)化為字符串.*/ ? ? QString info = sock->peerAddress().toString() \ ? ? ? ? ? ? + ':' + QString::number(sock->peerPort()); ? ? /*將斷開連接的消息打印到文本框.*/ ? ? ui->recvEdit->append(info+"已斷開"); ? ?? ? ? /*根據(jù)字符串找到comboBox_2中的對應(yīng)元素的索引號*/ ? ? int index = ui->comboBox_2->findText(info); ? ? /*刪除那個(gè)元素.*/ ? ? ui->comboBox_2->removeItem(index); ? ? /*將新連接的socket對象的可以讀取信號與接收槽函數(shù)斷開.*/ ? ? disconnect(sock, &QTcpSocket::readyRead, this, &Widget::on_recv); ? ? /*將新連接的socket對象的斷開連接信號與斷開槽函數(shù)斷開.*/ ? ? disconnect(sock, &QTcpSocket::disconnected, this, &Widget::on_disconnect); }
解釋:
- 同樣先找到觸發(fā)信號的那一個(gè)套接字。
- 使用
removeOne
成員函數(shù)將它從容器中刪除 - 將對應(yīng)客戶端的信息打包成字符串,并且將離線的消息打印到接收文本框
- 將下拉列表框中的對應(yīng)項(xiàng)也給刪除
- 斷開信號和槽的連接
最后,演示一下客戶端主動下線會有什么現(xiàn)象:
10、實(shí)現(xiàn)踢人功能
剛才是客戶端自己下線,現(xiàn)在是服務(wù)器主動踢人下線。但無論是哪一種,在斷開連接的時(shí)候都會觸發(fā)QTcpSocket::disconnected 信號
為踢人按鈕添加槽函數(shù):
void Widget::on_kickbtn_clicked() { ? ? /*將全部的客戶端踢下線.*/ ? ? if(currSock == NULL) ? ? { ? ? ? ? foreach(QTcpSocket *sock, sockList) ? ? ? ? { ? ? ? ? ? ? sock->close(); ? ? ? ? } ? ? } ? ? else ? ? { ? ? ? ? /*將選中的那一個(gè)客戶端踢下線.*/ ? ? ? ? currSock->close(); ? ? } }
到了這里,我感覺已經(jīng)沒有什么好說的了,只需知道調(diào)用close()
函數(shù)可以斷開連接。
這里也不做演示了。
11、實(shí)現(xiàn)關(guān)閉服務(wù)器
關(guān)閉服務(wù)器主要需要做兩件事:
1、停止監(jiān)聽IP及端口。
2、關(guān)閉所有與客戶端之間的連接。因?yàn)橥V贡O(jiān)聽是不夠的,之前的連接還是存在的,甚至還能繼續(xù)收發(fā)數(shù)據(jù)。
void Widget::on_closebtn_clicked() { ? ? currSock = NULL; ? ? /*關(guān)閉監(jiān)聽.*/ ? ? server->close(); ? ? /*將一些控件恢復(fù).*/ ? ? ui->closebtn->setDisabled(true); ?? ? ? ui->openbtn->setEnabled(true); ? ? ui->portEdit->setEnabled(true); ? ? ui->comboBox->setEnabled(true); ? ? /*遍歷之前全部連接的socket,并一一斷開.*/ ? ? foreach(QTcpSocket *sock, sockList) ? ? { ? ? ? ? sock->close(); ? ? } }
演示:
在斷開連接后,下拉框會刪除掉之前全部的連接信息,被迫選擇到“All”,然后 currSock 指針也會自然而然地指向NULL。當(dāng)然不放心的也可以手動弄一下。
好了,至此全部的功能已經(jīng)講解完畢了,多的兩個(gè)按鈕是我自己調(diào)試用的。如果綁定的是一個(gè)公網(wǎng)IP,時(shí)間長了也許會有一些奇怪的信息顯示在接收文本框,不用害怕,可能是哪個(gè)迷路的孩子找錯(cuò)家了。
總結(jié)
1、這個(gè)程序總體而言是比較簡單的,實(shí)現(xiàn)的功能簡單、設(shè)計(jì)的想法也很簡單,連注釋加空行也不過兩百行代碼,但我卻花了這么長的篇幅去介紹它,可能有些大佬看著會嫌啰嗦,但其實(shí)我寫這樣的一篇文檔也是很花費(fèi)時(shí)間的,我自己也覺得很累。我主要是想鍛煉一下自己的文檔能力,也希望能幫助其他人度過入門的難關(guān),往后的文章可能不會寫這么詳細(xì)了。
2、Qt Creator 好像有什么奇怪的問題。如果用 /**/ 的方式寫注釋,編譯是不通過的。解決辦法是最后一個(gè)字用英文字符。還有控件自動生成的槽函數(shù)一直有黃色警告,雖然不影響使用,但感覺很變扭,希望有知道原因的大佬能不嗇賜教。
3、這個(gè)程序只能用于非常簡單且頻率很低的數(shù)據(jù)收發(fā)。信號和槽的機(jī)制雖然很容易使用,但并不好用在并發(fā)的場合下。且 foreach 遍歷是比較耗費(fèi)時(shí)間的,假如有上萬個(gè)客戶端都連接到這臺服務(wù)器,并且服務(wù)器要給所有客戶端都發(fā)送一條較長的信息,這個(gè)時(shí)候就不太好了。所以,后續(xù)要升級,肯定要使用線程的方式去解決這些問題。
完整代碼
widget.h
#ifndef WIDGET_H #define WIDGET_H #include <QWidget> #include <QTcpServer> #include <QTcpSocket> QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACE class Widget : public QWidget { ? ? Q_OBJECT public: ? ? Widget(QWidget *parent = nullptr); ? ? ~Widget(); private slots: ? ? void on_openbtn_clicked(); ? ? void on_clearbtn_clicked(); ? ? void on_recv(); ? ? void on_disconnect(); ? ? void on_sendbtn_clicked(); ? ? void on_pushButton_clicked(); ? ? void on_closebtn_clicked(); ? ? void on_comboBox_2_currentIndexChanged(const QString &arg1); ? ? void on_kickbtn_clicked(); private: ? ? Ui::Widget *ui; ? ? QTcpServer *server; ? ? QList<QTcpSocket *>sockList; ? ? QTcpSocket *currSock; }; #endif // WIDGET_H
widget.cpp
#include "widget.h" #include "ui_widget.h" #include <QtNetwork> #include <QDebug> Widget::Widget(QWidget *parent) ? ? : QWidget(parent) ? ? , ui(new Ui::Widget) { ? ? ui->setupUi(this); ? ? /*整個(gè)對象的初始化工作.*/ ? ? currSock = NULL; ? ? ? ? ? ? ? ? ? ?//當(dāng)前的socket對象指針指向空 ? ? ui->recvEdit->setReadOnly(true); ? ?//將接受文本框設(shè)為只讀模式 ? ? ui->closebtn->setEnabled(true); ? ? //將關(guān)閉按鈕設(shè)為不可點(diǎn)擊,因?yàn)榇藭r(shí)服務(wù)器還未被開啟 ? ? ui->portEdit->setText("8081"); ? ? ?//默認(rèn)端口號為8081 ? ? /*server指針指向new創(chuàng)建出來的QTCPServer對象.*/ ? ? server = new QTcpServer(this); ? ? /*讀取本機(jī)網(wǎng)卡信息...*/ ? ? QString localHostName = QHostInfo::localHostName(); ? ? QHostInfo info = QHostInfo::fromName(localHostName); ? ? /*將本機(jī)所有的IPV4地址添加到comboBox下.*/ ? ? foreach(QHostAddress ipAddress, info.addresses()) ? ? { ? ? ? ? if(ipAddress.protocol() == QAbstractSocket::IPv4Protocol) ? ? ? ? { ? ? ? ? ? ? ui->comboBox->addItem(ipAddress.toString()); ? ? ? ? } ? ? } ? ? /*將server對象的新連接信號連接到槽.*/ ? ? connect(server, &QTcpServer::newConnection, this, [=](){ ? ? ? ? /*獲取新連接客戶端的socket*/ ? ? ? ? QTcpSocket *socket = server->nextPendingConnection(); ? ? ? ? /*將這個(gè)socket添加到List容器中...*/ ? ? ? ? sockList.append(socket); ? ? ? ? /*獲取客戶端的IP地址和端口號信息,并轉(zhuǎn)換為字符串.*/ ? ? ? ? QString info = socket->peerAddress().toString() \ ? ? ? ? ? ? ? ? + ':' + QString::number(socket->peerPort()); ? ? ? ? /*將信息打印到文本框.*/ ? ? ? ? ui->recvEdit->append("已連接:"+info); ? ? ? ? /*將客戶端的信息添加到comboBox_2下.*/ ? ? ? ? ui->comboBox_2->addItem(info); ? ? ? ? /*將新連接的socket對象的可以讀取信號連接到接收槽函數(shù).*/ ? ? ? ? connect(socket, &QTcpSocket::readyRead, this, &Widget::on_recv); ? ? ? ? /*將新連接的socket對象的斷開連接信號連接到斷開槽函數(shù).*/ ? ? ? ? connect(socket, &QTcpSocket::disconnected, this, &Widget::on_disconnect); ? ? }); } Widget::~Widget() { ? ? delete ui; } void Widget::on_recv() { ? ? /*找到觸發(fā)信號的那個(gè)socket對象.*/ ? ? QTcpSocket *sock = qobject_cast<QTcpSocket *>(sender()); ? ? /*讀取信息并轉(zhuǎn)化為字符串.*/ ? ? QString info = "來自" + sock->peerAddress().toString() \ ? ? ? ? ? ? + ':' + QString::number(sock->peerPort()); ? ? /*將客戶端的信息打印到文本框.*/ ? ? ui->recvEdit->append(info); ? ? /*將接收到的數(shù)據(jù)也打印到文本框.*/ ? ? ui->recvEdit->append(sock->readAll()); } void Widget::on_disconnect() { ? ? /*找到觸發(fā)信號的那個(gè)socket對象.*/ ? ? QTcpSocket *sock = qobject_cast<QTcpSocket *>(sender()); ? ? /*將斷開連接的那個(gè)socket從List容器中移除.*/ ? ? sockList.removeOne(sock); ? ? /*將客戶端信息轉(zhuǎn)化為字符串.*/ ? ? QString info = sock->peerAddress().toString() \ ? ? ? ? ? ? + ':' + QString::number(sock->peerPort()); ? ? /*將斷開連接的消息打印到文本框.*/ ? ? ui->recvEdit->append(info+"已斷開"); ? ? /*根據(jù)字符串找到comboBox_2中的對應(yīng)元素的索引號*/ ? ? int index = ui->comboBox_2->findText(info); ? ? /*刪除那個(gè)元素.*/ ? ? ui->comboBox_2->removeItem(index); ? ? /*將新連接的socket對象的可以讀取信號與接收槽函數(shù)斷開.*/ ? ? disconnect(sock, &QTcpSocket::readyRead, this, &Widget::on_recv); ? ? /*將新連接的socket對象的斷開連接信號與斷開槽函數(shù)斷開.*/ ? ? disconnect(sock, &QTcpSocket::disconnected, this, &Widget::on_disconnect); } void Widget::on_openbtn_clicked() { ? ? /*監(jiān)聽本地IP加端口號.*/ ? ? if(server->listen(QHostAddress(ui->comboBox->currentText()), ui->portEdit->text().toInt()) == false) ? ? { ? ? ? ? /*監(jiān)聽失敗打印信息.*/ ? ? ? ? qDebug()<<"listen false"; ? ? } ? ? else ? ? { ? ? ? ? /*監(jiān)聽成功則將一些控件鎖死.*/ ? ? ? ? ui->openbtn->setDisabled(true); ? ? ? ? ui->portEdit->setDisabled(true); ? ? ? ? ui->comboBox->setDisabled(true); ? ? ? ? qDebug()<<"監(jiān)聽成功"; ? ? ? ? /*激活關(guān)閉按鈕.*/ ? ? ? ? ui->closebtn->setEnabled(true); ? ? } } void Widget::on_clearbtn_clicked() { ? ? /*點(diǎn)擊清除按鈕則清除接收文本框.*/ ? ? ui->recvEdit->clear(); } void Widget::on_sendbtn_clicked() { ? ? /*如果是為開啟服務(wù)器、未連接或者選擇All的時(shí)候,currSock才會指向空 ? ? ? ? 前兩種情況容器是空的,所以也不會出錯(cuò)。如果是選中All時(shí)候, ? ? ? ? 且多個(gè)客戶端連接,則會遍歷容器中所有的socket,并且一一發(fā)送出去.*/ ? ? if(currSock == NULL) ? ? { ? ? ? ? foreach(QTcpSocket *sock, sockList) ? ? ? ? { ? ? ? ? ? ? sock->write(ui->sendEdit->toPlainText().toUtf8()); ? ? ? ? } ? ? } ? ? else ? ? { ? ? ? ? /*如果選擇的是一個(gè)特定的客戶端,則只向它發(fā)送.*/ ? ? ? ? currSock->write(ui->sendEdit->toPlainText().toUtf8()); ? ? } } void Widget::on_pushButton_clicked() { ? ? foreach(QTcpSocket *sock, sockList) ? ? { ? ? ? ? qDebug()<<sock->peerAddress()<<':'<<sock->peerPort(); ? ? } } void Widget::on_closebtn_clicked() { ? ? currSock = NULL; ? ? /*關(guān)閉監(jiān)聽.*/ ? ? server->close(); ? ? /*將一些控件恢復(fù).*/ ? ? ui->closebtn->setDisabled(true); ?? ? ? ui->openbtn->setEnabled(true); ? ? ui->portEdit->setEnabled(true); ? ? ui->comboBox->setEnabled(true); ? ? /*遍歷之前全部連接的socket,并一一斷開.*/ ? ? foreach(QTcpSocket *sock, sockList) ? ? { ? ? ? ? sock->close(); ? ? } } void Widget::on_comboBox_2_currentIndexChanged(const QString &arg1) { ? ? /*如果當(dāng)前選擇的是All,當(dāng)前sock指針就指向空.*/ ? ? if(arg1 == "All") ? ? { ? ? ? ? currSock = NULL; ? ? ? ? return; ? ? } ? ? /*不然就讀取選中的信息,將其拆分為IP地址和端口號.*/ ? ? QStringList info = arg1.split(':'); ? ? QString ip = info[0]; ? ? int port = info[1].toInt(); ? ? /*遍歷容器,找到對應(yīng)的那個(gè)socket.*/ ? ? foreach(QTcpSocket *sock, sockList) ? ? { ? ? ? ? if(sock->peerAddress().toString() == ip && sock->peerPort() == port) ? ? ? ? { ? ? ? ? ? ? /*當(dāng)前sock指針指向找到的那個(gè)socket.*/ ? ? ? ? ? ? currSock = sock; ? ? ? ? ? ? break; ? ? ? ? } ? ? } } void Widget::on_kickbtn_clicked() { ? ? /*將全部的客戶端踢下線.*/ ? ? if(currSock == NULL) ? ? { ? ? ? ? foreach(QTcpSocket *sock, sockList) ? ? ? ? { ? ? ? ? ? ? sock->close(); ? ? ? ? } ? ? } ? ? else ? ? { ? ? ? ? /*將選中的那一個(gè)客戶端踢下線.*/ ? ? ? ? currSock->close(); ? ? } }
main.cpp
維持原樣不需改動
以上就是本文的全部內(nèi)容,希望對大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
C語言實(shí)現(xiàn)個(gè)人財(cái)務(wù)管理
這篇文章主要為大家詳細(xì)介紹了C語言實(shí)現(xiàn)個(gè)人財(cái)務(wù)管理,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-11-11C語言基于EasyX庫實(shí)現(xiàn)有圖形界面時(shí)鐘
這篇文章主要為大家詳細(xì)介紹了C語言基于EasyX庫實(shí)現(xiàn)有圖形界面時(shí)鐘,獲得本地時(shí)間,輸出文字,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-03-03基于c++ ege圖形庫實(shí)現(xiàn)五子棋游戲
這篇文章主要為大家詳細(xì)介紹了基于c++ ege圖形庫實(shí)現(xiàn)五子棋游戲,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-12-12用C語言模仿Python函數(shù)的一種簡單實(shí)現(xiàn)方法
這篇文章主要介紹了用C語言模仿Python函數(shù)的一種簡單實(shí)現(xiàn)方法,需要的朋友可以參考下2017-05-05C語言函數(shù)的遞歸和調(diào)用實(shí)例分析
一個(gè)函數(shù)在它的函數(shù)體內(nèi)調(diào)用它自身稱為遞歸調(diào)用。這種函數(shù)稱為遞歸函數(shù)。C語言允許函數(shù)的遞歸調(diào)用。在遞歸調(diào)用中,主調(diào)函數(shù)又是被調(diào)函數(shù)。執(zhí)行遞歸函數(shù)將反復(fù)調(diào)用其自身,每調(diào)用一次就進(jìn)入新的一層2013-07-07簡單掌握桶排序算法及C++版的代碼實(shí)現(xiàn)
桶排序是將要排序的算法按桶分組排序之后再遍歷匯總的一種線性排序算法,下面就讓我們來通過小例子簡單掌握桶排序算法及C++版的代碼實(shí)現(xiàn)^^2016-07-07一文帶你了解C語言中static關(guān)鍵字的3個(gè)作用
static這個(gè)關(guān)鍵字是“靜態(tài)”的意思,在C語言里主要有3個(gè)作用。這篇文章主要通過一些簡單示例為大家詳細(xì)講講這3個(gè)左右,感興趣的小伙伴可以了解一下2023-04-04