公司软件的一个重要特征是与员工沟通的能力。因此,内部即时消息系统是软件的关键部分。通过在 Qt 中加入网络模块,我们可以很容易地用它创建一个聊天系统。
在本章中,我们将涵盖以下主题:
- Qt 网络模块
- 创建即时消息服务器
- 创建即时消息客户端
使用 Qt 创建一个即时通讯系统比你想象的要容易得多。我们开始吧!
在下一节中,我们将了解 Qt 网络模块,以及它如何帮助我们通过 TCP 或 UDP 连接协议实现服务器-客户端通信。
Qt 中的网络模块是既提供低级网络功能(如 TCP 和 UDP 套接字)又提供高级网络类(用于 web 集成和网络通信)的模块。
在本章中,我们将使用 TCP ( 传输控制协议)互联网协议来代替 UDP ( 用户数据报协议)协议。主要区别在于,TCP 是一种面向连接的协议,要求所有客户端在能够相互通信之前建立与服务器的连接。
另一方面,UDP 是一种不需要连接的无连接协议。客户端将只发送它需要发送到目的地的任何数据,而不检查数据是否已经被另一端接收到。两种协议各有利弊,但 TCP 更适合我们的示例项目。我们想确保收件人收到每条聊天消息,不是吗?
两种协议的区别如下:
- TCP:
- 面向连接的协议
- 适用于要求高可靠性的应用,并且对数据传输时间要求不高
- TCP 的速度比 UDP 慢
- 在发送下一个数据之前,要求接收客户端确认收到
- 绝对保证传输的数据保持完整,并按照发送的顺序到达
- UDP:
- 无连接协议
- 适合需要快速、高效传输的应用,如游戏和 VOIP
- 因为不尝试错误恢复,所以 UDP 比 TCP 轻量级且更快
- 也适用于回答来自大量客户端的小查询的服务器
- 不能保证发送的数据到达目的地,因为没有跟踪连接,也不需要接收客户端的任何确认
由于我们不采用对等连接方式,我们的聊天系统将需要两个不同的软件——服务器程序和客户端程序。服务器程序将充当中间人(就像邮递员一样),接收来自所有用户的所有消息,并将它们相应地发送给目标接收者。服务器程序将在服务器机房的一台计算机上与普通用户隔离。
另一方面,客户端程序是所有用户都使用的即时通讯软件。这个程序是安装在用户计算机上的程序。用户可以使用这个客户端程序发送他们的消息,也可以看到其他人发送的消息。我们的消息传递系统的整体架构如下所示:
让我们开始设置我们的项目并启用 Qt 的网络模块!对于这个项目,我们将先从服务器程序开始,然后再处理客户端程序。
首先,创建一个新的 Qt 控制台应用项目。然后,打开项目文件(.pro
)并添加以下模块:
QT += core network
Qt -= gui
你应该注意到这个项目没有任何gui
模块(我们确保它被明确删除),因为我们不需要任何服务器程序的用户界面。这也是我们选择 Qt 控制台应用而不是通常的 Qt 小部件应用的原因。
实际上,就是这样——您已经成功地将网络模块添加到您的项目中。在下一节中,我们将学习如何为我们的聊天系统创建服务器程序。
在下一节中,我们将学习如何创建一个即时消息服务器来接收用户发送的消息,并将它们重新分发到各自的收件人。
在本节中,我们将学习如何创建一个不断监听特定端口的传入消息的 TCP 服务器。为了简单起见,我们将只创建一个全局聊天室,其中每个用户都可以看到聊天室中每个用户发送的消息,而不是带有朋友列表的一对一消息传递系统。一旦你理解了聊天系统是如何工作的,你就可以很容易地把这个系统即兴发挥给后者。
首先,转到文件|新建文件或项目,并在 C++ 类别下选择 C++ 类。然后,将类命名为server
,选择 QObject 作为基类。在继续创建自定义类之前,请确保选中了包含对象选项。你应该也注意到了mainwindow.ui
、mainwindow.h
和mainwindow.cpp
的缺席。这是因为控制台应用项目中没有用户界面。
一旦创建了服务器类,让我们打开server.h
并添加以下头、变量和函数:
#ifndef SERVER_H
#define SERVER_H
#include <QObject>
#include <QTcpServer>
#include <QTcpSocket>
#include <QDebug>
#include <QVector>
private:
QTcpServer* chatServer;
QVector<QTcpSocket*>* allClients;
public:
explicit server(QObject *parent = nullptr);
void startServer();
void sendMessageToClients(QString message); public slots: void newClientConnection();
void socketDisconnected();
void socketReadyRead();
void socketStateChanged(QAbstractSocket::SocketState state);
接下来,创建一个名为startServer()
的函数,并将以下代码添加到server.cpp
中的函数定义中:
void server::startServer()
{
allClients = new QVector<QTcpSocket*>;
chatServer = new QTcpServer();
chatServer->setMaxPendingConnections(10);
connect(chatServer, SIGNAL(newConnection()), this,
SLOT(newClientConnection()));
if (chatServer->listen(QHostAddress::Any, 8001))
{
qDebug() << "Server has started. Listening to port 8001.";
}
else
{
qDebug() << "Server failed to start. Error: " + chatServer-
>errorString();
}
}
我们创建了一个名为chatServer
的QTcpServer
对象,并让它不断地监听端口8001
。您可以选择任何未使用的端口号,范围从1024
到49151
。这个范围之外的其他号码通常是为普通系统保留的,比如 HTTP 或 FTP 服务,所以我们最好不要使用它们来避免冲突。我们还创建了一个名为allClients
的QVector
数组来存储所有连接的客户端,以便我们以后可以使用它来将传入的消息重定向到所有用户。
我们还使用setMaxPendingConnections()
功能将最大挂起连接数限制为 10 个客户端。您可以使用此方法将活动客户端的数量保持在特定数量,以便服务器的带宽始终在其限制范围内。这样可以保证良好的服务质量,保持积极的用户体验。
每当客户端连接到服务器时,chatServer
将触发newConnection()
信号,因此我们将该信号连接到名为newClientConnection()
的自定义插槽功能。插槽功能如下所示:
void server::newClientConnection()
{
QTcpSocket* client = chatServer->nextPendingConnection();
QString ipAddress = client->peerAddress().toString();
int port = client->peerPort();
connect(client, &QTcpSocket::disconnected, this, &server::socketDisconnected);
connect(client, &QTcpSocket::readyRead, this, &server::socketReadyRead);
connect(client, &QTcpSocket::stateChanged, this, &server::socketStateChanged);
allClients->push_back(client);
qDebug() << "Socket connected from " + ipAddress + ":" + QString::number(port);
}
每一个连接到服务器的新客户端都是一个QTcpSocket
对象,可以通过调用nextPendingConnection()
从QTcpServer
对象中获取。您可以通过分别拨打peerAddress()
和peerPort()
获取客户端的信息,如其 IP 地址和端口号。然后,我们将每个新客户端存储到allClients
阵列中以备将来使用。我们还将客户端的disconnected()
、readyRead()
和stateChanged()
信号连接到其各自的插槽功能。
当客户端与服务器断开连接时,会触发disconnected()
信号,随后会调用socketDisconnected()
、slot
功能。我们在这个函数中所做的只是在服务器控制台上显示消息,仅此而已。您可以在这里做任何您喜欢的事情,比如将用户的离线状态保存到数据库等等。为了简单起见,我们将在控制台窗口上打印出消息:
void server::socketDisconnected()
{
QTcpSocket* client = qobject_cast<QTcpSocket*>(QObject::sender());
QString socketIpAddress = client->peerAddress().toString();
int port = client->peerPort();
qDebug() << "Socket disconnected from " + socketIpAddress + ":" +
QString::number(port);
}
接下来,每当客户端向服务器发送消息时,都会触发readyRead()
信号。我们已经将信号连接到一个名为socketReadyRead()
的槽函数,它看起来像这样:
void server::socketReadyRead()
{
QTcpSocket* client = qobject_cast<QTcpSocket*>(QObject::sender());
QString socketIpAddress = client->peerAddress().toString();
int port = client->peerPort();
QString data = QString(client->readAll());
qDebug() << "Message: " + data + " (" + socketIpAddress + ":" +
QString::number(port) + ")";
sendMessageToClients(data);
}
在前面的代码中,我们只是将消息重定向到一个名为sendMessageToClients()
的自定义函数,该函数负责将消息传递给所有连接的客户端。我们一会儿就来看看这个函数是如何工作的。我们使用QObject::sender()
获取发出readyRead
信号的对象的指针,并将其转换为QTcpSocket
类,这样我们就可以访问其readAll()
功能。
之后,我们还将另一个名为stateChanged()
的信号连接到socketStateChanged()
槽功能。慢速功能如下所示:
void server::socketStateChanged(QAbstractSocket::SocketState state)
{
QTcpSocket* client = qobject_cast<QTcpSocket*>(QObject::sender());
QString socketIpAddress = client->peerAddress().toString();
int port = client->peerPort();
QString desc;
if (state == QAbstractSocket::UnconnectedState)
desc = "The socket is not connected.";
else if (state == QAbstractSocket::HostLookupState)
desc = "The socket is performing a host name lookup.";
else if (state == QAbstractSocket::ConnectingState)
desc = "The socket has started establishing a connection.";
else if (state == QAbstractSocket::ConnectedState)
desc = "A connection is established.";
else if (state == QAbstractSocket::BoundState)
desc = "The socket is bound to an address and port.";
else if (state == QAbstractSocket::ClosingState)
desc = "The socket is about to close (data may still be
waiting to be written).";
else if (state == QAbstractSocket::ListeningState)
desc = "For internal use only.";
qDebug() << "Socket state changed (" + socketIpAddress + ":" +
QString::number(port) + "): " + desc;
}
每当客户端的网络状态发生变化时,例如连接、断开、侦听等,就会触发此功能。我们将简单地根据它的新状态打印出相关的消息,这样我们就可以更容易地调试我们的程序。
现在,让我们看看sendMessageToClients()
函数是什么样子的:
void server::sendMessageToClients(QString message)
{
if (allClients->size() > 0)
{
for (int i = 0; i < allClients->size(); i++)
{
if (allClients->at(i)->isOpen() && allClients->at(i)-
>isWritable())
{
allClients->at(i)->write(message.toUtf8());
}
}
}
}
在前面的代码中,我们简单地循环通过allClients
数组,并将消息数据传递给所有连接的客户端。
最后,打开main.cpp
并添加以下代码来启动我们的服务器:
#include <QCoreApplication>
#include "server.h"
int main(int argc, char *argv[])
{
QCoreApplication a(argc, argv);
server* myServer = new server();
myServer->startServer();
return a.exec();
}
现在构建并运行程序,您应该会看到如下内容:
除了显示服务器正在监听端口8001
之外,看起来没有任何事情发生。别担心,因为我们还没有创建客户端程序。我们继续!
在下一节中,我们将继续创建我们的即时消息客户端,用户将使用它来发送和接收消息。
在本节中,我们将学习如何为即时消息客户端设计用户界面并为其创建功能:
- 首先,通过转到文件|新文件或项目来创建另一个 Qt 项目。然后在应用类别下选择 Qt 小部件应用。
- 项目创建完成后,打开
mainwindow.ui
并将线条编辑和文本浏览器拖到窗口画布上。然后,选择中心小部件,并单击位于上面小部件栏上的垂直布局按钮,将垂直布局效果应用于小部件:
- 之后,在底部放置一个水平布局,并将线编辑放入布局中。然后,从小部件框中拉出一个按钮,将其命名为
sendButton
;我们还将其标签设置为Send
,如下图:
- 完成后,拖放另一个水平布局,并将其放在文本浏览器的顶部。之后,在水平布局中放置标签、线编辑和按钮,如下所示:
我们调用行编辑小部件nameInput
,并为其设置一个默认文本为John Doe
,这样用户就有了一个默认名称。然后,我们调用按钮connectButton
并将其标签更改为Connect
。
我们已经完成了一个非常简单的即时消息程序的用户界面设计,它将完成以下任务:
- 连接到服务器
- 让用户设置他们的名字
- 可以看到所有用户发送的消息
- 用户可以输入并发送他们的消息,让所有人都能看到
现在编译并运行项目,您应该会看到您的程序如下所示:
请注意,我还将窗口标题更改为Chat Client
,使其看起来稍微专业一些。您可以通过在层次窗口中选择MainWindow
对象并更改其windowTitle
属性来实现。
在下一节中,我们将开始编程部分的工作,并实现上面列表中提到的特性。
在我们开始编写任何代码之前,我们必须首先通过打开我们的项目文件(.pro
)来启用网络模块,并在那里添加network
关键字:
QT += core gui network
接下来,打开mainwindow.h
并添加以下标题和变量:
#ifndef MAINWINDOW_H
#define MAINWINDOW_H
#include <QMainWindow>
#include <QDebug>
#include <QTcpSocket>
private:
Ui::MainWindow *ui;
bool connectedToHost;
QTcpSocket* socket;
我们在mainwindow.cpp
中默认将connectedToHost
变量设置为false
:
MainWindow::MainWindow(QWidget *parent) :
QMainWindow(parent),
ui(new Ui::MainWindow)
{
ui->setupUi(this);
connectedToHost = false;
}
完成后,我们需要实现的第一个特性是服务器连接。打开mainwindow.ui
,右键单击连接按钮,然后选择转到插槽...,并选择clicked()
。之后,会自动为您创建一个槽函数。在SLOT
功能中添加以下代码:
void MainWindow::on_connectButton_clicked()
{
if (!connectedToHost)
{
socket = new QTcpSocket();
connect(socket, SIGNAL(connected()), this,
SLOT(socketConnected()));
connect(socket, SIGNAL(disconnected()), this,
SLOT(socketDisconnected()));
connect(socket, SIGNAL(readyRead()), this,
SLOT(socketReadyRead()));
socket->connectToHost("127.0.0.1", 8001);
}
else
{
QString name = ui->nameInput->text();
socket->write("<font color="Orange">" + name.toUtf8() + " has
left the chat room.</font>");
socket->disconnectFromHost();
}
}
我们在前面的代码中所做的基本上是检查connectedToHost
变量。如果变量是false
(意味着客户端没有连接到服务器),创建一个名为socket
的QTcpSocket
对象,并使其连接到端口8801
上的127.0.0.1
主机。IP 地址127.0.0.1
代表本地主机。由于这仅用于测试目的,我们将客户端连接到位于同一台计算机上的测试服务器。如果您在另一台计算机上运行服务器,您可以根据需要将 IP 地址更改为局域网或广域网地址。
当connected()
、disconnected()
和readReady()
信号被触发时,我们还将socket
对象连接到其各自的插槽功能。这与我们之前所做的服务器代码完全相同。如果客户端已经连接到服务器,并且单击了连接(现在标记为Disconnect
)按钮,则向服务器发送断开消息并终止连接。
接下来,我们将看看槽函数,我们在上一步中将其连接到了socket
对象。第一个是socketConnected()
函数,当客户端成功连接到服务器时将调用该函数:
void MainWindow::socketConnected()
{
qDebug() << "Connected to server.";
printMessage("<font color="Green">Connected to server.</font>");
QString name = ui->nameInput->text();
socket->write("<font color="Purple">" + name.toUtf8() + " has joined
the chat room.</font>");
ui->connectButton->setText("Disconnect");
connectedToHost = true;
}
首先,客户端将在应用输出和文本浏览器小部件上显示Connected to server.
消息。我们将在一分钟后看到printMessage()
功能是什么样子的。然后,我们从输入字段中获取用户名,并将其合并到文本消息中,然后将其发送到服务器,以便通知所有用户。最后,将连接按钮的标签设置为Disconnect
,将connectedToHost
变量设置为true
。
在这之后,让我们看看socketDisconnected()
,顾名思义,每当客户端与服务器断开连接时都会调用它:
void MainWindow::socketDisconnected()
{
qDebug() << "Disconnected from server.";
printMessage("<font color="Red">Disconnected from server.</font>");
ui->connectButton->setText("Connect");
connectedToHost = false;
}
前面的代码非常简单。它所做的只是在应用输出和文本浏览器小部件上显示断开的消息,然后将断开按钮的标签设置为Connect
,将connectedToHost
变量设置为false
。请注意,由于只有在客户端与服务器断开连接后才会调用该函数,因此我们无法再向服务器发送任何消息来通知它断开连接。您应该在服务器端检查连接是否断开,并相应地通知所有用户。
然后是socketReadyRead()
功能,每当服务器向客户端发送数据时都会触发。这个函数甚至比之前的函数更简单,因为它所做的只是将输入数据传递给printMessage()
函数,而没有其他功能:
void MainWindow::socketReadyRead()
{
ui->chatDisplay->append(socket->readAll());
}
最后,我们来看看printMessage()
函数是什么样子的。其实也一样简单。它所做的只是将消息附加到文本浏览器中,然后就完成了:
void MainWindow::printMessage(QString message)
{
ui->chatDisplay->append(message);
}
最后,让我们看看如何实现向服务器发送消息的功能。打开mainwindow.ui
,右键点击发送按钮,选择转到插槽...,并选择clicked()
选项。为您创建插槽函数后,向函数中添加以下代码:
void MainWindow::on_sendButton_clicked()
{
QString name = ui->nameInput->text();
QString message = ui->messageInput->text();
socket->write("<font color="Blue">" + name.toUtf8() + "</font>: " +
message.toUtf8());
ui->messageInput->clear();
}
首先,我们取用户的名字,并将其与消息结合起来。然后,在通过调用write()
将整个东西发送到服务器之前,我们将名称设置为蓝色。之后,清除消息输入字段,我们就完成了。由于文本浏览器默认接受富文本,我们可以通过将文本放在<font>
标签中来使用富文本来给文本着色。
立即编译并运行项目;你们应该能够在不同的客户上互相聊天!在连接客户端之前,不要忘记打开服务器。如果一切顺利,你应该看到这样的情况:
同时,您还应该看到服务器端的所有活动:
就这样!我们已经成功地使用 Qt 创建了一个简单的聊天系统。欢迎你即兴发挥,创造一个完善的信息系统!
在本章中,我们学习了如何使用 Qt 的网络模块创建即时消息系统。在下一章中,我们将深入探讨使用 Qt 进行图形渲染的奇妙之处。