一:系列总链接:
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输入文字,回车键按下, ChatDialog调用returnPressed槽来响应:
/* 在lineEdit输入文本,并敲回车键后,执行该函数*/
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);
// 敲lineEdit回车键,执行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相关的类拓扑图: