From c637706c2248ae2604d54b0b0eecba6c4a64b3c6 Mon Sep 17 00:00:00 2001 From: nayk Date: Sat, 19 Jul 2025 10:44:33 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D0=B0=D0=BB=D0=B8=D0=B7=D0=B0?= =?UTF-8?q?=D1=86=D0=B8=D1=8F=20=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=D0=B0?= =?UTF-8?q?=20=D0=B8=20=D1=81=D0=B5=D1=80=D0=B2=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 21 ++- _include/application_config.cpp | 64 ++++++++ _include/application_config.h | 11 +- client/main_window.cpp | 240 +++++++++++++++++++++++++++++- client/main_window.h | 15 +- server/main_window.cpp | 252 +++++++++++++++++++++++++++++++- server/main_window.h | 21 ++- 7 files changed, 617 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 18eced7..be070fb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,22 @@ # TcpSocket_Client_Server_Example -Пример реализации клиента и сервера для обмена данными по TCP/IP \ No newline at end of file +Пример реализации клиента и сервера для обмена данными по TCP/IP. + +## Тестовое задание + +Реализовать на Qt сервер, который будет принимать входящие соединения по TCP на определенном порту. +Реализовать следующие функции: +- открытие и закрытие входящего порта сервера; +- чтение входящего пакета от клиента в виде массива байт; +- обработка полученного массива по алгоритму (см. ниже); +- отправка нового массива обратно клиенту. + +Алгоритм обработки входящего массива: +- если значение очередного байта 0x00 или 0xFF, заменить на 0xAA; +- иначе на четных индексах увеличить значение на 1; +- на нечетных позициях уменьшить значение на 1. + +Приложение может быть реализовано в виде ПО с графическим интерфейсом, либо в виде консольного приложения. + + + diff --git a/_include/application_config.cpp b/_include/application_config.cpp index 573141b..a6a54b4 100644 --- a/_include/application_config.cpp +++ b/_include/application_config.cpp @@ -29,11 +29,21 @@ #include #include #include +#include #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) # include #endif +//============================================================================== + +const char * clDefault[2] { "#111416", "#fdfdfd" }; +const char * clLogDate[2] { "#929292", "#333333" }; +const char * clLogInf[2] { "#ffffff", "#000000" }; +const char * clLogWrn[2] { "#ff9c54", "#8d3c00" }; +const char * clLogErr[2] { "#ff4040", "#a50000" }; +const char * clLogIn[2] { "#55d864", "#003706" }; + //============================================================================== void Application::initialize() { @@ -104,3 +114,57 @@ void Application::installTranslations(const QString &lng) } } //============================================================================== +void Application::initializeLog(QTextEdit *textEdit) +{ + if (!textEdit) + return; + + textEdit->setStyleSheet( + QString( + "QTextEdit { " + "color: %1; " + "background-color: %2; " + "font-family: Courier New, Cascadia Mono, Lucida Console, Monospace; " + "font-size: 10pt; " + "}" + ) + .arg( clDefault[ 1 ], clDefault[ 0 ] ) + ); + textEdit->document()->setMaximumBlockCount(10000); +} +//============================================================================== +void Application::addToLog(QTextEdit *textEdit, const QString &text, LogType logType) +{ + if (!textEdit) + return; + + const QString line {"[%2] %4"}; + + QString cl = QString(clLogInf[ 0 ]); + + switch (logType) { + case LogTypeError: + cl = QString(clLogErr[ 0 ]); + break; + case LogTypeWarning: + cl = QString(clLogWrn[ 0 ]); + break; + case LogTypeData: + cl = QString(clLogIn[ 0 ]); + break; + default: + break; + } + + textEdit->append( + line.arg( + QString(clLogDate[ 0 ]), + QDateTime::currentDateTime().toString("HH:mm:ss"), + cl, + text.toHtmlEscaped() + ) + ); + + QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); +} +//============================================================================== diff --git a/_include/application_config.h b/_include/application_config.h index 9604217..d126673 100644 --- a/_include/application_config.h +++ b/_include/application_config.h @@ -27,17 +27,26 @@ #define APP_CONFIG_H #include +#include //============================================================================== - class Application { Q_DISABLE_COPY(Application) public: + enum LogType { + LogTypeInfo, + LogTypeData, + LogTypeError, + LogTypeWarning + }; + static void initialize(); static QString applicationRootPath(); static void installTranslations(const QString &lng = "ru"); + static void initializeLog(QTextEdit *textEdit); + static void addToLog(QTextEdit *textEdit, const QString &text, LogType logType = LogTypeInfo); private: Application() = delete; diff --git a/client/main_window.cpp b/client/main_window.cpp index b6c48ce..01c82dc 100644 --- a/client/main_window.cpp +++ b/client/main_window.cpp @@ -24,7 +24,14 @@ ****************************************************************************/ #include "main_window.h" #include "./ui_main_window.h" + #include +#include +#include +#include +#include + +#include "application_config.h" namespace client { //=========================================================== @@ -34,13 +41,244 @@ MainWindow::MainWindow(QWidget *parent) , ui(new Ui::MainWindow) { ui->setupUi(this); - setWindowTitle( QApplication::applicationName() ); + + initialize(); + updateControls(); } //============================================================================== MainWindow::~MainWindow() { + socket.disconnectFromHost(); delete ui; } //============================================================================== +void MainWindow::initialize() +{ + setWindowTitle( QApplication::applicationName() ); + Application::initializeLog(ui->textEditLog); + + connect(ui->pushButtonConnect, &QPushButton::clicked, this, &MainWindow::btnConnect); + connect(ui->pushButtonSend, &QPushButton::clicked, this, &MainWindow::btnSend); + connect(&socket, &QTcpSocket::connected, this, &MainWindow::socketConnected); + connect(&socket, &QTcpSocket::disconnected, this, &MainWindow::socketDisconnected); + + QString ipRange {"(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])"}; + QRegularExpression ipRegex ("^" + ipRange + + "\\." + ipRange + + "\\." + ipRange + + "\\." + ipRange + "$"); + + ui->lineEditIp->setValidator( new QRegularExpressionValidator(ipRegex, this) ); + ui->lineEditIp->setText("127.0.0.1"); + + QRegularExpression hexRegex("([a-fA-F0-9]{2})([\\s][a-fA-F0-9]{2})*[\\s]?"); + ui->lineEditData->setValidator( new QRegularExpressionValidator(hexRegex, this) ); +} +//============================================================================== +void MainWindow::updateControls() +{ + bool connect = isConnected(); + + ui->pushButtonSend->setEnabled(connect); + ui->lineEditIp->setEnabled(!connect); + ui->spinBoxPort->setEnabled(!connect); + + ui->pushButtonConnect->setText(connect ? tr("Закрыть") : tr("Открыть")); +} +//============================================================================== +bool MainWindow::isConnected() const +{ + return socket.state() == QTcpSocket::SocketState::ConnectedState; +} +//============================================================================== +void MainWindow::btnConnect() +{ + ui->pushButtonConnect->setEnabled(false); + ui->pushButtonSend->setEnabled(false); + ui->lineEditData->setEnabled(false); + ui->lineEditIp->setEnabled(false); + ui->spinBoxPort->setEnabled(false); + + QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); + + if (isConnected()) { + + Application::addToLog( + ui->textEditLog, + tr("Закрытие соединения с сервером...")); + socket.disconnectFromHost(); + + if (socket.state() != QAbstractSocket::UnconnectedState) { + + if (!socket.waitForDisconnected(timeout)) { + + Application::addToLog( + ui->textEditLog, + tr("Не удалось закрыть соединение: %1") + .arg(socket.errorString()), + Application::LogTypeError); + } + } + } + else { + + Application::addToLog( + ui->textEditLog, tr("Открытие соединения с сервером...")); + socket.connectToHost( + QHostAddress(ui->lineEditIp->text()), + static_cast(ui->spinBoxPort->value()) + ); + + if (socket.state() != QTcpSocket::SocketState::ConnectedState) { + + if (!socket.waitForConnected(timeout)) { + + Application::addToLog( + ui->textEditLog, tr("Не удалось открыть соединение: %1") + .arg(socket.errorString()), + Application::LogTypeError); + } + } + } + + ui->pushButtonConnect->setEnabled(true); + ui->lineEditData->setEnabled(true); + updateControls(); +} +//============================================================================== +void MainWindow::btnSend() +{ + if (!isConnected()) + return; + + QStringList list = ui->lineEditData->text().split(' ', Qt::SkipEmptyParts); + QByteArray sendData; + + for (const QString &str: std::as_const(list) ) { + + sendData.append( static_cast( str.toInt(nullptr, 16) ) ); + } + + if (sendData.isEmpty()) return; + + ui->pushButtonSend->setEnabled(false); + Application::addToLog( + ui->textEditLog, + tr("Отправка массива %1 байт").arg(sendData.size())); + + qsizetype sendCount {0}; + QElapsedTimer timer; + + timer.start(); + + while ((sendCount < sendData.size()) && (timer.elapsed() < timeout)) { + + qsizetype count = socket.write(sendData.mid( + static_cast(sendCount) + )); + + if (count < 0) { + + Application::addToLog( + ui->textEditLog, + tr("Ошибка отправки данных"), Application::LogTypeError ); + } + else if (count > 0) { + + Application::addToLog( + ui->textEditLog, + tr("Отправка данных: %1 (%2 байт)") + .arg( + QString( + sendData.mid( + static_cast(sendCount), + static_cast(count) ) + .toHex(' ')) + ).arg(count), + Application::LogTypeData + ); + sendCount += count; + } + } + + if (sendCount < sendData.size()) { + + Application::addToLog( + ui->textEditLog, + tr("Таймаут отправки данных. Всего отправлено %1 байт из %2") + .arg(sendCount).arg(sendData.size()), + Application::LogTypeError ); + } + + if (sendCount == 0) { + + updateControls(); + return; + } + + Application::addToLog( + ui->textEditLog, + tr("Приём массива %1 байт").arg(sendData.size())); + + if (!socket.waitForReadyRead(timeout)) { + + Application::addToLog( + ui->textEditLog, + tr("Таймаут ожидания приёма данных"), Application::LogTypeError ); + updateControls(); + return; + } + + QByteArray readData; + timer.restart(); + + while ((readData.size() < sendData.size()) && (timer.elapsed() < timeout)) { + + QByteArray buf = socket.read( sendData.size() - readData.size() ); + + if (!buf.isEmpty()) { + + Application::addToLog( + ui->textEditLog, + tr("Приём данных: %1 (%2 байт)") + .arg( + QString(buf.toHex(' ')) + ).arg(buf.size()), + Application::LogTypeData + ); + readData.append(buf); + } + } + + if (readData.size() < sendData.size()) { + + Application::addToLog( + ui->textEditLog, + tr("Таймаут приёма данных. Всего принято %1 байт из %2") + .arg(readData.size()).arg(sendData.size()), + Application::LogTypeError ); + } + + updateControls(); +} +//============================================================================== +void MainWindow::socketConnected() +{ + Application::addToLog( + ui->textEditLog, + tr("Соединение открыто. Ip: %1, Порт: %2") + .arg(ui->lineEditIp->text()).arg(ui->spinBoxPort->value()), + Application::LogTypeWarning); + updateControls(); +} +//============================================================================== +void MainWindow::socketDisconnected() +{ + Application::addToLog( + ui->textEditLog, tr("Соединение закрыто"), Application::LogTypeWarning); + ui->textEditLog->append(""); + updateControls(); +} +//============================================================================== } // namespace //=============================================================== diff --git a/client/main_window.h b/client/main_window.h index 0da98b9..09dbccf 100644 --- a/client/main_window.h +++ b/client/main_window.h @@ -27,6 +27,7 @@ #define CLIENT_MAINWINDOW_H //============================================================================== #include +#include //============================================================================== QT_BEGIN_NAMESPACE @@ -47,8 +48,20 @@ public: ~MainWindow(); private: - Ui::MainWindow *ui; + const int timeout {5000}; + Ui::MainWindow *ui; + QTcpSocket socket; + + void initialize(); + void updateControls(); + bool isConnected() const; + +private slots: + void btnConnect(); + void btnSend(); + void socketConnected(); + void socketDisconnected(); }; } // namespace //=============================================================== diff --git a/server/main_window.cpp b/server/main_window.cpp index 32a301b..140f500 100644 --- a/server/main_window.cpp +++ b/server/main_window.cpp @@ -24,7 +24,14 @@ ****************************************************************************/ #include "main_window.h" #include "./ui_main_window.h" + #include +#include +#include +#include +#include + +#include "application_config.h" namespace server { //=========================================================== @@ -34,14 +41,257 @@ MainWindow::MainWindow(QWidget *parent) , ui(new Ui::MainWindow) { ui->setupUi(this); - setWindowTitle( QApplication::applicationName() ); + + initialize(); + updateControls(); } //============================================================================== MainWindow::~MainWindow() { + disconnectClients(); + server.close(); + delete ui; } //============================================================================== +void MainWindow::initialize() +{ + setWindowTitle( QApplication::applicationName() ); + Application::initializeLog(ui->textEditLog); + + connect(ui->pushButtonConnect, &QPushButton::clicked, this, &MainWindow::btnConnect); + connect(&server, &QTcpServer::newConnection, this, &MainWindow::newConnection); +} +//============================================================================== +void MainWindow::updateControls() +{ + bool connect {server.isListening()}; + + ui->spinBoxPort->setEnabled(!connect); + ui->pushButtonConnect->setText(connect ? tr("Закрыть") : tr("Открыть")); +} +//============================================================================== +void MainWindow::disconnectClients() +{ + while (!clients.isEmpty()) { + + QTcpSocket *socket = clients.takeLast(); + + if (socket) { + + socket->close(); + + if (socket->state() != QAbstractSocket::UnconnectedState) + socket->waitForDisconnected(2000); + } + } +} +//============================================================================== +void MainWindow::btnConnect() +{ + ui->pushButtonConnect->setEnabled(false); + ui->spinBoxPort->setEnabled(false); + + QApplication::processEvents(QEventLoop::ExcludeUserInputEvents); + + if (server.isListening()) { + + disconnectClients(); + server.close(); + + Application::addToLog( + ui->textEditLog, + tr("Сервер закрыт"), + Application::LogTypeWarning); + + ui->textEditLog->append(""); + } + else { + + if (server.listen( + QHostAddress::Any, + static_cast(ui->spinBoxPort->value()) + ) + ) { + + Application::addToLog( + ui->textEditLog, + tr("Сервер открыт для входящих подключений на порту %1") + .arg(ui->spinBoxPort->value()), + Application::LogTypeWarning); + } + else { + + Application::addToLog( + ui->textEditLog, + tr("Не удалось открыть сервер (%1)").arg(server.errorString()), + Application::LogTypeError); + } + } + + ui->pushButtonConnect->setEnabled(true); + updateControls(); +} +//============================================================================== +void MainWindow::newConnection() +{ + QTcpSocket* socket = server.nextPendingConnection(); + + if (!socket) + return; + + clients.append(socket); + + connect(socket, &QTcpSocket::readyRead, this, &MainWindow::clientRead); + connect(socket, &QTcpSocket::disconnected, this, &MainWindow::clientDisconnect); + + QHostAddress clientAddress = socket->peerAddress(); + quint16 clientPort = socket->peerPort(); + + Application::addToLog( + ui->textEditLog, + tr("Входящее подключение. Адрес клиента: %1; Порт клиента: %2") + .arg(clientAddress.toString()).arg(clientPort), + Application::LogTypeWarning); +} +//============================================================================== +void MainWindow::clientRead() +{ + QTcpSocket* socket = qobject_cast(sender()); + + if (!socket) + return; + + QHostAddress clientAddress = socket->peerAddress(); + + Application::addToLog( + ui->textEditLog, tr("Чтение входящих данных с %1").arg(clientAddress.toString())); + + QByteArray readData; + QElapsedTimer timer; + + timer.start(); + + while (readData.isEmpty() && (timer.elapsed() < timeout)) { + + readData = socket->readAll(); + } + + if (readData.isEmpty()) { + + Application::addToLog( + ui->textEditLog, + tr("Таймаут ожидания данных с %1").arg(clientAddress.toString()), + Application::LogTypeError); + return; + } + + Application::addToLog( + ui->textEditLog, + tr("Принятые данные c %1: %2 (%3 байт)") + .arg(clientAddress.toString(), QString(readData.toHex(' '))) + .arg(readData.size()), + Application::LogTypeData); + + Application::addToLog( + ui->textEditLog, + tr("Обработка массива %1 байт").arg(readData.size())); + + QByteArray sendData; + + // Преобразование данных в выходной массив: + // если значение 0x00 или 0xFF, заменить на 0xAA + // иначе на четных индексах увеличить значение на 1 + // на нечетных уменьшить значение на 1 + + for (auto i = 0; i < readData.size(); ++i) { + + quint8 byte = static_cast( readData.at(i) ); + + if ((byte == 0) || (byte == 0xFF)) + byte = 0xAA; + else if ((i % 2) == 0) + ++byte; + else + --byte; + + sendData.append( static_cast(byte) ); + } + + Application::addToLog( + ui->textEditLog, tr("Данные после обработки: %1") + .arg( + QString(sendData.toHex(' ')) + ), + Application::LogTypeData + ); + + Application::addToLog( + ui->textEditLog, + tr("Отправка данных клиенту %1").arg(clientAddress.toString())); + + qsizetype sendCount {0}; + timer.restart(); + + while ((sendCount < sendData.size()) && (timer.elapsed() < timeout)) { + + qsizetype count = socket->write(sendData.mid( + static_cast(sendCount) + )); + + if (count < 0) { + + Application::addToLog( + ui->textEditLog, tr("Ошибка отправки данных на %1") + .arg(clientAddress.toString()), + Application::LogTypeError ); + } + else if (count > 0) { + + Application::addToLog( + ui->textEditLog, tr("Отправка данных на %1: %2 (%3 байт)") + .arg(clientAddress.toString(), + QString(sendData.mid( + static_cast(sendCount), + static_cast(count) + ).toHex(' '))) + .arg(count), + Application::LogTypeData + ); + sendCount += count; + } + } + + if (sendCount < sendData.size()) { + + Application::addToLog( + ui->textEditLog, tr("Таймаут отправки данных на %1. Всего отправлено %2 байт из %3") + .arg(clientAddress.toString()).arg(sendCount).arg(sendData.size()), + Application::LogTypeError ); + } +} +//============================================================================== +void MainWindow::clientDisconnect() +{ + QString clientAddressStr; + + if (sender()) { + + QTcpSocket* socket = qobject_cast(sender()); + + if (socket) { + + QHostAddress clientAddress = socket->peerAddress(); + clientAddressStr = clientAddress.toString(); + } + } + + Application::addToLog( + ui->textEditLog, + tr("Закрытие входящего подключения %1").arg(clientAddressStr), + Application::LogTypeWarning); +} +//============================================================================== } // mamespace //=============================================================== diff --git a/server/main_window.h b/server/main_window.h index a802e56..5cab4d9 100644 --- a/server/main_window.h +++ b/server/main_window.h @@ -25,8 +25,12 @@ #pragma once #ifndef SERVER_MAINWINDOW_H #define SERVER_MAINWINDOW_H -//============================================================================== + #include +#include +#include +#include +#include //============================================================================== QT_BEGIN_NAMESPACE @@ -47,8 +51,21 @@ public: ~MainWindow(); private: - Ui::MainWindow *ui; + const qint64 timeout {5000}; + Ui::MainWindow *ui; + QTcpServer server; + QList clients; + + void initialize(); + void updateControls(); + void disconnectClients(); + +private slots: + void btnConnect(); + void newConnection(); + void clientRead(); + void clientDisconnect(); }; } // mamespace //===============================================================