第七次实训工作报告

设计题目

华南理工大学:植此青绿

本周工作小结:

本周我主要进行了以下工作:

  1. 熟悉Qt网络编程,特别是TCP通信的基础知识。
  2. 开发和测试一个基本的Qt TCP服务器和客户端应用程序,主要实现了服务器和客户端之间的基本消息通信功能。
  3. 添加了文件传输功能,使得服务器可以向客户端发送文件。
  4. 进行了用户界面的设计和美化,包括设置背景图片、窗口图标、按钮样式等。

环境与工具

  • 开发环境:Qt Creator 13.0.0
  • 操作系统:Windows 11

前置工作

对QT内提供的用于套接字通信的类(TCP、UDP)进行学习。

QTcpServer:服务器类,用于监听客户端连接以及和客户端建立连接。 QTcpSocket:通信的套接字类,客户端、服务器端都需要使用。

基于TCP的Qt网络通信 | 爱编程的大丙 (subingwen.cn)

后期预备开启功能:
  1. 增强文件传输功能。
  2. 增加客户端之间的通信功能,实现多人聊天。

程序功能说明

chatroomwindow类(聊天室)

1. 界面设计

  • 使用了Qt的QWidget类作为基类,通过垂直布局管理器(QVBoxLayout)和水平布局管理器(QHBoxLayout)来布局界面。
  • 界面包括返回按钮和两个功能按钮(连接服务器按钮和启动服务器按钮)。

2. 按钮设计

  • 返回按钮位于界面左上角,用于返回到上一个页面。
  • 连接服务器按钮用于启动客户端功能,点击后会弹出一个客户端窗口。
  • 启动服务器按钮用于启动服务端功能,点击后会弹出一个服务端窗口。

3. 按钮样式

  • 使用样式表设置了按钮的样式,包括背景颜色、边框、字体等,使按钮看起来更加美观。
1
2
3
4
5
QPushButton *startServerButton = new QPushButton("服务端",this);
startServerButton->setStyleSheet(buttonStyle);
startServerButton->setIcon(QIcon(":/new/images/fwd.png"));
startServerButton->setIconSize(QSize(35, 35));
startServerButton->setFixedSize(250, 100);

4. 功能实现

  • 点击连接服务器按钮会创建并显示客户端窗口(ClientWidget类的实例)。
  • 点击启动服务器按钮会创建并显示服务端窗口(ServerWidget类的实例)。
  • 每个按钮都有对应的点击事件处理函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
void ChatRoomWindow::on_connectServerButton_clicked()
{
if (!clientWidget)
{
clientWidget = new ClientWidget;
//保持在其他窗口顶部,并设置窗口标志
clientWidget->setWindowFlags(clientWidget->windowFlags() | Qt::WindowStaysOnTopHint);
clientWidget->show();
}
else
{
clientWidget->show();
}
}
void ChatRoomWindow::on_startServerButton_clicked()
{
if (!serverWidget)
{
serverWidget = new ServerWidget;
//保持在其他窗口顶部,并设置窗口标志
serverWidget->setWindowFlags(serverWidget->windowFlags() | Qt::WindowStaysOnTopHint);
serverWidget->show();
}
else
{
serverWidget->show();
}
}

5. 窗口管理

  • 程序会根据按钮的点击情况创建相应的窗口,并保持在其他窗口的顶部。

6. 内存管理

  • 在程序退出时,会正确释放创建的客户端和服务端窗口对象,以避免内存泄漏。
1
2
3
4
5
6
7
ChatRoomWindow::~ChatRoomWindow()
{
if (clientWidget)
delete clientWidget;
if (serverWidget)
delete serverWidget;
}

7. 信号与槽连接

  • 连接了按钮的点击信号与相应的槽函数,以实现按钮点击事件的响应。
1
2
3
connect(connectServerButton, &QPushButton::clicked, this, &ChatRoomWindow::on_connectServerButton_clicked);
connect(startServerButton, &QPushButton::clicked, this, &ChatRoomWindow::on_startServerButton_clicked);
connect(returnButton, &QPushButton::clicked, this, &ChatRoomWindow::returnToCompetitionPage);

8. 返回按钮

  • 返回按钮点击后会触发一个信号,用于返回到竞赛页面。
1
2
3
4
void ChatRoomWindow::on_ReturnButton_clicked()
{
emit returnToCompetitionPage();
}

serverwidget类(服务端)

1. 界面设计

  • 通过Qt的QWidget类实现窗口部件。

1
2
3
class ServerWidget : public QWidget {
// 窗口部件的定义和相关控件声明
};

2. 启动和停止服务器

  • 点击启动服务器按钮(startButton)可以启动服务器,并监听指定的主机地址和端口号。

1
connect(startButton, &QPushButton::clicked, this, &ServerWidget::startServer);

  • 点击终止服务器按钮(stopButton)可以停止服务器,关闭服务器监听。

1
connect(stopButton, &QPushButton::clicked, this, &ServerWidget::stopServer);

3. 文件传输

  • 点击选择文件按钮(fileTransferButton)可以打开文件选择对话框,选择要发送的文件。

1
connect(fileTransferButton, &QPushButton::clicked, this, &ServerWidget::selectFile);

  • 选择文件后,可以将文件发送给连接到服务器的客户端,通过进度条显示文件传输进度。

1
sendFile(fileName);

4. 消息发送

  • 在消息输入框(messageLineEdit)中输入消息,并点击发送按钮(sendButton),即可向连接到服务器的客户端发送消息。

1
connect(sendButton, &QPushButton::clicked, this, &ServerWidget::sendMessage);

5. 实时显示连接状态和消息

  • 通过通信日志文本框(communicationLog)实时显示服务器连接状态、收到的消息和文件传输情况。

6. 事件处理

  • 使用事件过滤器捕获消息输入框的回车键事件,以便按下回车键发送消息。

1
2
3
4
5
bool ServerWidget::eventFilter(QObject *obj, QEvent *event) {
if (obj == messageLineEdit && event->type() == QEvent::KeyPress) {
// 处理回车键事件
}
}

7. 局域网IP地址获取

  • 通过getLocalIPAddress函数获取本地局域网IP地址,并显示在主机地址输入框中,方便用户查看服务器所在局域网IP地址。

1
QString hostAddress = getLocalIPAddress();

8. 界面样式

  • 使用样式表设置了界面各部分的样式,包括按钮、通信日志文本框等,使界面具有更好的可视性和用户体验。

一些核心函数的解释:

1. startServer()函数

  • 当用户点击启动服务器按钮时触发,获取主机地址和端口号,并尝试通过server->listen()函数启动服务器。
  • 如果服务器成功启动,则禁用启动按钮,启用终止按钮、发送按钮和选择文件按钮,并更新服务器状态标签。
  • 如果启动失败,则在通信日志中记录错误信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void ServerWidget::startServer()
{
// 获取主机地址和端口号
QString hostAddress = hostLineEdit->text();
qint16 port = portLineEdit->text().toInt();

// 如果服务器成功启动,则禁用启动按钮,启用终止按钮、发送按钮和选择文件按钮,并更新服务器状态标签
if (server->listen(QHostAddress(hostAddress), port))
{
startButton->setEnabled(false);
stopButton->setEnabled(true);
sendButton->setEnabled(true);
fileTransferButton->setEnabled(true);
serverStatusLabel->setText("服务端正在运行: " + hostAddress + ":" + QString::number(port));
}
else
{
// 若无法启动服务器,则在通信日志中记录错误信息
communicationLog->append("无法启动服务端: " + server->errorString());
}
}

2. stopServer()函数

  • 当用户点击终止服务器按钮时触发,通过server->close()函数关闭服务器监听,并进行相关界面控件的状态更新。

1
2
3
4
5
6
7
8
9
10
void ServerWidget::stopServer()
{
server->close();
startButton->setEnabled(true);
stopButton->setEnabled(false);
sendButton->setEnabled(false);
fileTransferButton->setEnabled(false);
serverStatusLabel->setText("服务端还未启动");
communicationLog->clear();
}

3. selectFile()函数

  • 当用户点击选择文件按钮时触发,打开文件选择对话框,用户选择要发送的文件。
  • 如果用户选择了文件,则调用sendFile()函数发送文件。

1
2
3
4
5
6
7
8
9
10
void ServerWidget::selectFile()
{
QString filePath = QFileDialog::getOpenFileName(this, "选择您的文件进行发送");
if (!filePath.isEmpty())
{
selectedFilePath = filePath;
QString fileName = QFileInfo(filePath).fileName();
sendFile(fileName);
}
}

4. sendFile(const QString &fileName)函数

  • 通过选定的文件路径打开文件,并读取文件内容。
  • 将文件名和文件大小发送给所有连接到服务器的客户端。
  • 分块发送文件内容给客户端,并在发送过程中更新通信日志。
  • 发送txt文件可以正常显示但其他文件如word会乱码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void ServerWidget::sendFile(const QString &fileName)
{
QFile file(selectedFilePath);
if (!file.open(QIODevice::ReadOnly))
{
communicationLog->append("无法打开文件: " + file.errorString());
return;
}

QByteArray fileData = file.readAll();
file.close();

QByteArray fileNameBytes = fileName.toUtf8();
qint64 fileSize = fileData.size();

// 发送文件名和文件大小
for (QTcpSocket *clientSocket : clients) {
QDataStream outStream(clientSocket);
outStream << fileNameBytes << fileSize;
}

// 分块发送文件内容
for (QTcpSocket *clientSocket : clients) {
qint64 bytesWritten = 0;
while (bytesWritten < fileSize) {
qint64 bytesToWrite = qMin(fileSize - bytesWritten, qint64(64 * 1024)); // 分块大小为 64KB
qint64 bytesWrittenNow = clientSocket->write(fileData.constData() + bytesWritten, bytesToWrite);
if (bytesWrittenNow == -1) {
communicationLog->append("发送文件失败: " + clientSocket->errorString());
return;
}
bytesWritten += bytesWrittenNow;
}
displayMessage("服务端: 文件 '" + fileName + "' 已经发送给客户端");
}
}

5. sendMessage()函数

  • 当用户在消息输入框中输入消息并点击发送按钮时触发,将消息转换为字节数组并发送给所有客户端。
  • 在发送过程中更新通信日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
void ServerWidget::sendMessage()
{
QString message = messageLineEdit->text();
if (!message.isEmpty()) {
QByteArray data = message.toUtf8();
for (QTcpSocket *clientSocket : clients)
{
clientSocket->write(data);
displayMessage("服务端: " + message);
}
messageLineEdit->clear();
}
}

6. incomingConnection()函数

  • 当新的客户端连接到服务器时触发,通过server->nextPendingConnection()获取新客户端的QTcpSocket对象,并添加到clients列表中。
  • 为新客户端的readyReaddisconnected信号连接相应的槽函数,并在通信日志中显示客户端连接信息。

1
2
3
4
5
6
7
8
9
10
11
void ServerWidget::incomingConnection()
{
while (server->hasPendingConnections()) {
QTcpSocket *clientSocket = server->nextPendingConnection();
clients.append(clientSocket);
connect(clientSocket, &QTcpSocket::readyRead, this, &ServerWidget::readyReadHandler);
connect(clientSocket, &QTcpSocket::disconnected, this, &ServerWidget::disconnectedHandler);

displayMessage("新客户端连接: " + clientSocket->peerAddress().toString());
}
}

7. readyReadHandler()函数

  • 当客户端发送数据到服务器时触发,读取客户端发送的数据并显示在通信日志中。

1
2
3
4
5
6
7
8
9
10
11
12
13
void ServerWidget::readyReadHandler()
{
QTcpSocket *clientSocket = qobject_cast<QTcpSocket*>(sender());
if (!clientSocket)
return;

while (clientSocket->bytesAvailable() > 0)
{
QByteArray data = clientSocket->readAll();
QString message = QString::fromUtf8(data);
displayMessage("Client: " + message);
}
}

8. disconnectedHandler()函数

  • 当客户端与服务器断开连接时触发,从clients列表中移除断开连接的客户端,并在通信日志中显示相关信息。

1
2
3
4
5
6
7
8
void ServerWidget::disconnectedHandler()
{
QTcpSocket *clientSocket = qobject_cast<QTcpSocket*>(sender());
if (!clientSocket)
return;
clients.removeOne(clientSocket);
clientSocket->deleteLater();
}

9. displayMessage(const QString &message)函数

  • 用于在通信日志中显示消息,包括服务器状态、收到的消息和文件传输情况。

1
2
3
4
void ServerWidget::displayMessage(const QString &message)
{
communicationLog->append(message);
}

10. getLocalIPAddress()函数

- 用于获取本地局域网IP地址,如果找不到则返回默认值"127.0.0.1"。(也就是本地回环地址)

1
2
3
4
5
6
7
8
9
10
QString ServerWidget::getLocalIPAddress()
{
const QList<QHostAddress> &addresses = QNetworkInterface::allAddresses();
for (const QHostAddress &address : addresses) {
if (address.protocol() == QAbstractSocket::IPv4Protocol && address != QHostAddress::LocalHost) {
return address.toString();
}
}
return "127.0.0.1"; // 如果没有找到局域网IP地址,则返回localhost地址
}

关于本地回环地址和网络接口Ip地址的区分,起初我是采用本地回环地址进行联网,显然这是不行的。

1
2
3
4
5
6
/*
当套接字连接到本地回环地址时,数据将在计算机内部传递,而不会经过网络接口发送到网络上。
*/
/*
* 当套接字连接到网络接口的IP地址时,数据将通过网络接口发送到网络上的其他设备。
*/

clientwidget类(客户端类)

1. 界面设计

  • 界面使用了Qt的GUI库,包括各种部件如按钮(QPushButton)、文本框(QTextEdit、QLineEdit)和标签(QLabel)等。
  • 使用布局管理器(QVBoxLayout、QHBoxLayout)来管理部件的布局。

2. 连接功能

  • connectToServer() 函数用于连接到服务器,它获取用户输入的服务器地址和端口号,并调用 socket->connectToHost(hostAddress, port) 尝试连接到服务器。
  • disconnectFromServer() 函数用于断开与服务器的连接,通过调用 socket->disconnectFromHost() 实现。

3. 通信功能

  • 用户可以在消息输入框中输入消息,然后点击发送按钮发送消息给服务器。
  • 接收到服务器发送的消息后,会显示在通信日志框中,格式为 "服务端: " + 消息内容。

4. 事件处理

  • 通过事件过滤器 eventFilter() 捕获用户在消息输入框中按下 Enter 键的事件,从而实现按下 Enter 键发送消息的功能。

5. 样式设计

  • 使用样式表设置了按钮和通信日志框的样式,包括背景颜色、边框、字体大小等,以提高界面的美观度和可用性。

6. IP地址获取

  • getLocalIPAddress() 函数用于获取客户端所在局域网中的 IP 地址。
  • 它遍历系统中所有的网络接口,找到第一个 IPv4 地址并返回,如果没有找到合适的 IP 地址,则返回默认地址 "127.0.0.1",即本地回环地址。

7. 调试信息

  • 使用 qDebug() 输出调试信息,例如连接服务器的地址和端口号。

一些核心代码的书写:

1. ~ClientWidget()函数

  • 在客户端部件被销毁时自动调用,断开与服务器的连接。

1
2
3
4
5
ClientWidget::~ClientWidget()
{
socket->disconnectFromHost();
socket->waitForDisconnected();
}

2. connectToServer()函数

  • 获取用户在界面上输入的服务器地址和端口号,然后尝试连接到指定的服务器。

1
2
3
4
5
6
7
8
9
void ClientWidget::connectToServer()
{
// 获取服务器地址和端口号
QString hostAddress = serverAddressLineEdit->text();
quint16 port = serverPortLineEdit->text().toUShort();
qDebug() << "Attempting to connect to server at address:" << hostAddress << "port:" << port; // 输出调试信息
// 尝试连接到服务器
socket->connectToHost(hostAddress, port);
}

3. disconnectFromServer()函数

  • 断开与服务器的连接。

1
2
3
4
5
void ClientWidget::disconnectFromServer()
{
socket->disconnectFromHost();
}

4. sendMessage()函数

  • 获取用户在消息输入框中输入的消息,并将消息转换为字节数组后发送到服务器。

1
2
3
4
5
6
7
8
9
10
11
12
void ClientWidget::sendMessage()
{
QString message = messageLineEdit->text();
if (!message.isEmpty())
{
QByteArray data = message.toUtf8();
//将消息数据写入到与服务器连接的socket
socket->write(data);
messageLineEdit->clear();
displayMessage("客户端: " + message); // Display client messages with an identifier
}
}

5. connectedToServer()函数

  • 当客户端连接成功时触发,禁用连接按钮,启用断开连接按钮和发送按钮,并更新连接状态标签为已连接。

1
2
3
4
5
6
7
void ClientWidget::connectedToServer()
{
connectButton->setEnabled(false);
disconnectButton->setEnabled(true);
sendButton->setEnabled(true);
connectionStatusLabel->setText("已连接");
}

6. disconnectedFromServer()函数

  • 当与服务器断开连接时触发,禁用断开连接按钮和发送按钮,启用连接按钮,并更新连接状态标签为未连接。

1
2
3
4
5
6
7
void ClientWidget::disconnectedFromServer()
{
connectButton->setEnabled(true);
disconnectButton->setEnabled(false);
sendButton->setEnabled(false);
connectionStatusLabel->setText("未连接");
}

7. displayMessage(const QString &message)函数

  • 用于在通信日志中显示消息,包括客户端发送和接收的消息。

1
2
3
4
void ClientWidget::displayMessage(const QString &message)
{
communicationLog->append(message);
}

8. displayError(const QString &error)函数

  • 用于在通信日志中显示错误消息。

1
2
3
4
void ClientWidget::displayError(const QString &error)
{
communicationLog->append("错误: " + error);
}

9. getLocalIPAddress()函数

  • 用于获取客户端所在局域网中的 IP 地址,遍历系统中所有的网络接口,找到第一个 IPv4 地址并返回。
1
2
3
4
5
6
7
8
9
10
QString ClientWidget::getLocalIPAddress()
{
const QList<QHostAddress> &addresses = QNetworkInterface::allAddresses();
for (const QHostAddress &address : addresses) {
if (address.protocol() == QAbstractSocket::IPv4Protocol && address != QHostAddress::LocalHost) {
return address.toString();
}
}
return "127.0.0.1"; // 如果没有找到局域网IP地址,则返回localhost地址
}

具体页面展示:

image-20240521204453832

客户端页面

image-20240521204507847

服务端页面

image-20240521204518781

客户端与服务端建立连接并通信页面:

image-20240521204532623

寄语

最后一周,实现音乐播放器的设计,再小小塞个数独私货,并美化一下页面,希望可以完成。

我见青山多妩媚,料青山见我应如是。