一:系列總連結:
QT5.14.2 官方例子 - 學習系列
https://blog.csdn.net/qq_22122811/article/details/108007519
二:項目位置:
Examples\Qt-5.14.2\network\network-chat
注:在Examples下的路徑
項目子產品:network\network-chat
2.1: 資源下載下傳:
管道1:
下載下傳qtcreator源碼,會附帶該例程;
管道2:
github下載下傳連結:
https://github.com/peterwei24/QT5.14.2_Examples/tree/master/allExamplesCode/network/network-chat
三:項目描述:
示範一個有狀态的對等聊天用戶端。
這個例子使用QUdpSocket和QNetworkInterface廣播來發現它的對等點。(換言之,區域網路的聊天室)
項目效果:

四:官網講解:
Network Chat Example | Qt Network 5.14.2
https://doc.qt.io/qt-5.14/qtnetwork-network-chat-example.html
五:解析:
5.1:原理分析:
利用了廣播的特性,将每一個裝置作為用戶端,分别向同一個區域網路的位址群發送資料包,同時也作為服務端,綁定指定端口和任一IP,監聽所有區域網路内的所有IP位址,并接收資料,這樣就完成了區域網路聊天室的功能;
5.2:界面布局:
5.3:結構設計:
如原理分析中,每個執行端都作為用戶端,同時也作為服務端,來完成區域網路聊天室的功能;
5.4:類解析:
ChatDialog: 界面顯示;
Client: 控制本機,并開始廣播;
PeerManager: 管理本機同時作為用戶端和服務端的工作,接收和發送;
Server: 繼承QTcpServer,如果目前有新的連接配接,則建立連接配接;
Connection: 繼承QTcpSocket,建立連接配接;
六:邏輯了解:
6.1:總體的邏輯:
通過Chatdialog建立一個對話框,在對話框中建立一個用戶端Client,這個用戶端會建立同級點之間的管理者PeerManager,來處理接收和發送的任務。
6.2:發消息給區域網路其他成員的過程:
lineEdit輸入文字,Enter鍵按下, ChatDialog調用returnPressed槽來響應:
/* 在lineEdit輸入文本,并敲Enter鍵後,執行該函數*/
void ChatDialog::returnPressed()
{
QString text = lineEdit->text();
if (text.isEmpty())
return;
if (text.startsWith(QChar('/'))) {
QColor color = textEdit->textColor();
textEdit->setTextColor(Qt::red);
textEdit->append(tr("! Unknown command: %1")
.arg(text.left(text.indexOf(' '))));
textEdit->setTextColor(color);
} else {
// 用戶端像區域網路内發送該文本
client.sendMessage(text);
// 追加該綽号和文本,并顯示在左邊對話框的上面
appendMessage(myNickName, text);
}
lineEdit->clear();
}
Client執行sendMessage():
// 發送資訊
void Client::sendMessage(const QString &message)
{
if (message.isEmpty())
return;
// 周遊所有的區域網路已連接配接的socket, 并逐一發送消息
for (Connection *connection : qAsConst(peers))
connection->sendMessage(message);
}
Connection(socket)執行sendMessage():
// 發送資訊
bool Connection::sendMessage(const QString &message)
{
if (message.isEmpty())
return false;
// 往流中寫入資料, 對QCborStreamWriter的使用還待了解
writer.startMap(1);
writer.append(PlainText);
writer.append(message);
writer.endMap();
return true;
}
6.3:接收區域網路其他成員發送的消息過程:
目前的使用者在peerManager類内建立一個socket,該socket綁定了指定的廣播端口以及任意位址,并允許位址複用和共享:
// 建立udpsocket,綁定任一位址,指定端口,設定位址可共用,可複用
broadcastSocket.bind(QHostAddress::Any, broadcastPort, QUdpSocket::ShareAddress
| QUdpSocket::ReuseAddressHint);
// socket讀到資料 => 處理廣播接收到的資料
connect(&broadcastSocket, SIGNAL(readyRead()),
this, SLOT(readBroadcastDatagram()));
如果PeerManager内的broadSocket讀取到資料,則執行readBroadcastDatagram():
// 讀取廣播包
void PeerManager::readBroadcastDatagram()
{
// 以45000端口接收到的資料
while (broadcastSocket.hasPendingDatagrams())
{
QHostAddress senderIp;
quint16 senderPort;
QByteArray datagram;
datagram.resize(broadcastSocket.pendingDatagramSize());
// 讀取資料包
if (broadcastSocket.readDatagram(datagram.data(), datagram.size(),
&senderIp, &senderPort) == -1)
continue;
int senderServerPort;
{
// decode the datagram
QCborStreamReader reader(datagram);
if (reader.lastError() != QCborError::NoError || !reader.isArray())
continue;
if (!reader.isLengthKnown() || reader.length() != 2)
continue;
reader.enterContainer();
if (reader.lastError() != QCborError::NoError || !reader.isString())
continue;
while (reader.readString().status == QCborStreamReader::Ok) {
// we don't actually need the username right now
}
if (reader.lastError() != QCborError::NoError || !reader.isUnsignedInteger())
continue;
senderServerPort = reader.toInteger();
}
// 如果發送的位址是目前本地的位址,并且發送的伺服器端口和伺服器端口一緻,
// 則認為該發送者無效
if (isLocalHostAddress(senderIp) && senderServerPort == serverPort)
continue;
// 判斷用戶端和該發送的IP位址是否有連接配接
if (!client->hasConnection(senderIp))
{
// 如果沒連接配接,則将該連接配接添加到發送伺服器的端口和IP上
Connection *connection = new Connection(this);
emit newConnection(connection);
connection->connectToHost(senderIp, senderServerPort);
}
}
}
這個socket在自己的類中connection,接收到資訊的并輸出顯示到左邊對話框QTextEdit内:
// readyRead():讀到輸入的資料 => processReadyRead(): 處理該資料
QObject::connect(this, SIGNAL(readyRead()), this, SLOT(processReadyRead()));
處理資料:
// 處理讀取資料
void Connection::processReadyRead()
{
// we've got more data, let's parse
reader.reparse();
while (reader.lastError() == QCborError::NoError)
{
if (state == WaitingForGreeting)
{
if (!reader.isArray())
break; // protocol error
reader.enterContainer(); // we'll be in this array forever
state = ReadingGreeting;
}
else if (reader.containerDepth() == 1)
{
// Current state: no command read
// Next state: read command ID
if (!reader.hasNext())
{
reader.leaveContainer();
disconnectFromHost();
return;
}
if (!reader.isMap() || !reader.isLengthKnown() || reader.length() != 1)
break; // protocol error
reader.enterContainer();
}
else if (currentDataType == Undefined)
{
// Current state: read command ID
// Next state: read command payload
if (!reader.isInteger())
break; // protocol error
currentDataType = DataType(reader.toInteger());
reader.next();
}
else
{
// Current state: read command payload
if (reader.isString())
{
auto r = reader.readString();
buffer += r.data;
if (r.status != QCborStreamReader::EndOfString)
continue;
}
else if (reader.isNull())
{
reader.next();
}
else
{
break; // protocol error
}
// Next state: no command read
reader.leaveContainer();
if (transferTimerId != -1) {
killTimer(transferTimerId);
transferTimerId = -1;
}
if (state == ReadingGreeting)
{
if (currentDataType != Greeting)
break; // protocol error
processGreeting();
}
else
{
// 處理其他使用者發送的資料
processData();
}
}
}
if (reader.lastError() != QCborError::EndOfFile)
abort(); // parse error
if (transferTimerId != -1 && reader.containerDepth() > 1)
transferTimerId = startTimer(TransferTimeout);
}
// 處理資料
void Connection::processData()
{
switch (currentDataType) {
case PlainText:
// 告知目前用戶端,有新資料
emit newMessage(username, buffer);
break;
case Ping:
writer.startMap(1);
writer.append(Pong);
writer.append(nullptr); // no payload
writer.endMap();
break;
case Pong:
pongTime.restart();
break;
default:
break;
}
currentDataType = Undefined;
buffer.clear();
}
// 新使用者加入聊天室,執行該槽函數
void Client::readyForUse()
{
Connection *connection = qobject_cast<Connection *>(sender());
if (!connection || hasConnection(connection->peerAddress(),
connection->peerPort()))
return;
// socket觸發信号,目前用戶端轉發該信号,并傳遞使用者資訊和資料
connect(connection, SIGNAL(newMessage(QString,QString)),
this, SIGNAL(newMessage(QString,QString)));
peers.insert(connection->peerAddress(), connection);
QString nick = connection->name();
if (!nick.isEmpty())
emit newParticipant(nick);
}
=》
ChatDialog::ChatDialog(QWidget *parent)
: QDialog(parent)
{
setupUi(this);
// 設定焦點
lineEdit->setFocusPolicy(Qt::StrongFocus);
textEdit->setFocusPolicy(Qt::NoFocus);
textEdit->setReadOnly(true);
listWidget->setFocusPolicy(Qt::NoFocus);
// 敲lineEditEnter鍵,執行returnPressed()
connect(lineEdit, SIGNAL(returnPressed()), this, SLOT(returnPressed()));
connect(lineEdit, SIGNAL(returnPressed()), this, SLOT(returnPressed()));
// 用戶端有新資訊輸入newMessage(),執行appendMessage()
connect(&client, SIGNAL(newMessage(QString,QString)),
this, SLOT(appendMessage(QString,QString)));
// 用戶端有新加入newParticipant(),執行newParticipant()
connect(&client, SIGNAL(newParticipant(QString)),
this, SLOT(newParticipant(QString)));
//
connect(&client, SIGNAL(participantLeft(QString)),
this, SLOT(participantLeft(QString)));
myNickName = client.nickName();
newParticipant(myNickName);
tableFormat.setBorder(0);
QTimer::singleShot(10 * 1000, this, SLOT(showInformation()));
}
=》
/* 左邊的list清單新增條目時,即區域網路其他使用者加入,向其中追加資訊 */
void ChatDialog::appendMessage(const QString &from, const QString &message)
{
if (from.isEmpty() || message.isEmpty())
return;
// 擷取目前文本光标的位置
QTextCursor cursor(textEdit->textCursor());
// 移動光标到末尾
cursor.movePosition(QTextCursor::End);
// 插入一行兩列的文本表格
QTextTable *table = cursor.insertTable(1, 2, tableFormat);
// 第一列,插入<使用者名等資訊>
table->cellAt(0, 0).firstCursorPosition().insertText('<' + from + "> ");
// 第二列,插入聊天資訊
table->cellAt(0, 1).firstCursorPosition().insertText(message);
// 擷取垂直滾動條
QScrollBar *bar = textEdit->verticalScrollBar();
// 設定滾動條最大值
bar->setValue(bar->maximum());
}
6.4: 有新使用者加入時,接收新使用者的使用者名和端口資訊:
假設我時A使用者,B使用者登入時,會執行startBroadcasting(),告知所有區域網路的連接配接上的使用者,自己的登入資訊:
peerManager->startBroadcasting();
PeerManager執行開始廣播:
// 開始廣播
void PeerManager::startBroadcasting()
{
broadcastTimer.start();
}
定時器隔2S告知廣播内的成員,自身的登入資訊:
// 發送廣播包
void PeerManager::sendBroadcastDatagram()
{
QByteArray datagram;
{
/* QCborStreamWriter:
* 這個類可以用來快速将CBOR内容流直接編碼到QByteArray或QIODevice。CBOR是簡潔的二進制對象表示,
* 它是一種非常緊湊的二進制資料編碼形式,與JSON相容。它是由IETF限制的RESTful環境(CoRE)工作組
* 建立的,該工作組在許多新的rfc中使用了它。它将與CoAP協定一起使用。*/
QCborStreamWriter writer(&datagram);
/*在CBOR流中啟動長度不确定的CBOR數組。每個startArray()調用必須與一個endArray()調用配對,并且
* 目前的CBOR元素擴充到數組的末尾。
*/
writer.startArray(2);
writer.append(username);
writer.append(serverPort);
writer.endArray();
}
bool validBroadcastAddresses = true;
for (const QHostAddress &address : qAsConst(broadcastAddresses)) {
// 對擷取的廣播位址分别寫入資料
if (broadcastSocket.writeDatagram(datagram, address,
broadcastPort) == -1)
validBroadcastAddresses = false;
}
if (!validBroadcastAddresses)
updateAddresses();
}
七:類或函數積累:
1.QNetworkConfigurationManager說明:
QNetworkConfigurationManager提供對系統已知的網絡配置的通路,并允許應用程式在運作時檢測系統功能(關于網絡會話)。
QNetworkConfiguration抽象了一組配置選項,描述必須如何配置網絡接口以連接配接到特定的目标網絡。QNetworkConfigurationManager維護并更新QNetworkConfigurations全局清單。應用程式可以通過allConfigurations()通路和過濾這個清單。如果添加了新的配置,或者删除或更改了現有配置,則分别發出configurationAdded()、configurationRemoved()和configurationChanged()信号。
當打算立即建立一個新的網絡會話而不關心特定的配置時,可以使用defaultConfiguration()。它傳回QNetworkConfiguration::Discovered配置。如果沒有發現任何配置,則傳回無效配置。
一些配置更新可能需要一些時間來執行更新。WLAN掃描就是這樣一個例子。除非平台執行内部更新,否則可能需要通過QNetworkConfigurationManager::updateConfigurations()手動觸發配置更新。更新過程的完成通過發出updateCompleted()信号來表示。更新過程確定更新每個現有的QNetworkConfiguration執行個體。不需要通過allConfigurations()請求更新配置清單。
八:其他積累:
無;
九:舉一反三:
無;
十:借鑒思路:
如何利用廣播實作區域網路的多人聊天,多人通信;
十一:輔助
和Network相關的類拓撲圖: