第七次C++实训报告
第七次实训工作报告
设计题目
华南理工大学:植此青绿
本周工作小结:
本周我主要进行了以下工作:
- 熟悉Qt网络编程,特别是TCP通信的基础知识。
- 开发和测试一个基本的Qt TCP服务器和客户端应用程序,主要实现了服务器和客户端之间的基本消息通信功能。
- 添加了文件传输功能,使得服务器可以向客户端发送文件。
- 进行了用户界面的设计和美化,包括设置背景图片、窗口图标、按钮样式等。
环境与工具:
- 开发环境:Qt Creator 13.0.0
- 操作系统:Windows 11
前置工作
对QT内提供的用于套接字通信的类(TCP、UDP)进行学习。
QTcpServer
:服务器类,用于监听客户端连接以及和客户端建立连接。
QTcpSocket
:通信的套接字类,客户端、服务器端都需要使用。
基于TCP的Qt网络通信 | 爱编程的大丙 (subingwen.cn)
后期预备开启功能:
- 增强文件传输功能。
- 增加客户端之间的通信功能,实现多人聊天。
程序功能说明
chatroomwindow类(聊天室)
1. 界面设计:
- 使用了Qt的QWidget类作为基类,通过垂直布局管理器(QVBoxLayout)和水平布局管理器(QHBoxLayout)来布局界面。
- 界面包括返回按钮和两个功能按钮(连接服务器按钮和启动服务器按钮)。
2. 按钮设计:
- 返回按钮位于界面左上角,用于返回到上一个页面。
- 连接服务器按钮用于启动客户端功能,点击后会弹出一个客户端窗口。
- 启动服务器按钮用于启动服务端功能,点击后会弹出一个服务端窗口。
3. 按钮样式:
- 使用样式表设置了按钮的样式,包括背景颜色、边框、字体等,使按钮看起来更加美观。
1 | QPushButton *startServerButton = new QPushButton("服务端",this); |
4. 功能实现:
- 点击连接服务器按钮会创建并显示客户端窗口(ClientWidget类的实例)。
- 点击启动服务器按钮会创建并显示服务端窗口(ServerWidget类的实例)。
- 每个按钮都有对应的点击事件处理函数。
1 | void ChatRoomWindow::on_connectServerButton_clicked() |
5. 窗口管理:
- 程序会根据按钮的点击情况创建相应的窗口,并保持在其他窗口的顶部。
6. 内存管理:
- 在程序退出时,会正确释放创建的客户端和服务端窗口对象,以避免内存泄漏。
1 | ChatRoomWindow::~ChatRoomWindow() |
7. 信号与槽连接:
- 连接了按钮的点击信号与相应的槽函数,以实现按钮点击事件的响应。
1 | connect(connectServerButton, &QPushButton::clicked, this, &ChatRoomWindow::on_connectServerButton_clicked); |
8. 返回按钮:
- 返回按钮点击后会触发一个信号,用于返回到竞赛页面。
1 | void ChatRoomWindow::on_ReturnButton_clicked() |
serverwidget类(服务端)
1. 界面设计:
- 通过Qt的QWidget类实现窗口部件。
1
2
3class 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
5bool 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
21void 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
10void 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
10void 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
36void 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
13void 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
列表中。 - 为新客户端的
readyRead
和disconnected
信号连接相应的槽函数,并在通信日志中显示客户端连接信息。
1
2
3
4
5
6
7
8
9
10
11void 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
13void 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
8void ServerWidget::disconnectedHandler()
{
QTcpSocket *clientSocket = qobject_cast<QTcpSocket*>(sender());
if (!clientSocket)
return;
clients.removeOne(clientSocket);
clientSocket->deleteLater();
}
9.
displayMessage(const QString &message)
函数:
- 用于在通信日志中显示消息,包括服务器状态、收到的消息和文件传输情况。
1
2
3
4void 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 | /* |
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
5ClientWidget::~ClientWidget()
{
socket->disconnectFromHost();
socket->waitForDisconnected();
}
2.
connectToServer()
函数:
- 获取用户在界面上输入的服务器地址和端口号,然后尝试连接到指定的服务器。
1
2
3
4
5
6
7
8
9void 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
5void ClientWidget::disconnectFromServer()
{
socket->disconnectFromHost();
}
4.
sendMessage()
函数:
- 获取用户在消息输入框中输入的消息,并将消息转换为字节数组后发送到服务器。
1
2
3
4
5
6
7
8
9
10
11
12void 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
7void ClientWidget::connectedToServer()
{
connectButton->setEnabled(false);
disconnectButton->setEnabled(true);
sendButton->setEnabled(true);
connectionStatusLabel->setText("已连接");
}
6.
disconnectedFromServer()
函数:
- 当与服务器断开连接时触发,禁用断开连接按钮和发送按钮,启用连接按钮,并更新连接状态标签为未连接。
1
2
3
4
5
6
7void ClientWidget::disconnectedFromServer()
{
connectButton->setEnabled(true);
disconnectButton->setEnabled(false);
sendButton->setEnabled(false);
connectionStatusLabel->setText("未连接");
}
7.
displayMessage(const QString &message)
函数:
- 用于在通信日志中显示消息,包括客户端发送和接收的消息。
1
2
3
4void ClientWidget::displayMessage(const QString &message)
{
communicationLog->append(message);
}
8.
displayError(const QString &error)
函数:
- 用于在通信日志中显示错误消息。
1
2
3
4void ClientWidget::displayError(const QString &error)
{
communicationLog->append("错误: " + error);
}
9.
getLocalIPAddress()
函数:
- 用于获取客户端所在局域网中的 IP 地址,遍历系统中所有的网络接口,找到第一个 IPv4 地址并返回。
1 | QString ClientWidget::getLocalIPAddress() |
具体页面展示:
客户端页面
服务端页面
客户端与服务端建立连接并通信页面:
寄语
最后一周,实现音乐播放器的设计,再小小塞个数独私货,并美化一下页面,希望可以完成。
我见青山多妩媚,料青山见我应如是。