Реализация клиента и сервера

main
Евгений Тетерин 2025-07-19 10:44:33 +03:00
parent c58fa4e814
commit c637706c22
7 changed files with 617 additions and 7 deletions

View File

@ -1,3 +1,22 @@
# TcpSocket_Client_Server_Example
Пример реализации клиента и сервера для обмена данными по TCP/IP
Пример реализации клиента и сервера для обмена данными по TCP/IP.
## Тестовое задание
Реализовать на Qt сервер, который будет принимать входящие соединения по TCP на определенном порту.
Реализовать следующие функции:
- открытие и закрытие входящего порта сервера;
- чтение входящего пакета от клиента в виде массива байт;
- обработка полученного массива по алгоритму (см. ниже);
- отправка нового массива обратно клиенту.
Алгоритм обработки входящего массива:
- если значение очередного байта 0x00 или 0xFF, заменить на 0xAA;
- иначе на четных индексах увеличить значение на 1;
- на нечетных позициях уменьшить значение на 1.
Приложение может быть реализовано в виде ПО с графическим интерфейсом, либо в виде консольного приложения.

View File

@ -29,11 +29,21 @@
#include <QtCore/QDebug>
#include <QtCore/QDir>
#include <QtCore/QStringList>
#include <QtCore/QDateTime>
#if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
# include <QtCore/QTextCodec>
#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 {"<font color=\"%1\">[%2]</font> <font color=\"%3\">%4</font>"};
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);
}
//==============================================================================

View File

@ -27,17 +27,26 @@
#define APP_CONFIG_H
#include <QtCore/QString>
#include <QtWidgets/QTextEdit>
//==============================================================================
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;

View File

@ -24,7 +24,14 @@
****************************************************************************/
#include "main_window.h"
#include "./ui_main_window.h"
#include <QtWidgets/QApplication>
#include <QtGui/QRegularExpressionValidator>
#include <QtNetwork/QHostAddress>
#include <QtCore/QDateTime>
#include <QtCore/QElapsedTimer>
#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<quint16>(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<char>( 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<int>(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<int>(sendCount),
static_cast<int>(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 //===============================================================

View File

@ -27,6 +27,7 @@
#define CLIENT_MAINWINDOW_H
//==============================================================================
#include <QtWidgets/QMainWindow>
#include <QtNetwork/QTcpSocket>
//==============================================================================
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 //===============================================================

View File

@ -24,7 +24,14 @@
****************************************************************************/
#include "main_window.h"
#include "./ui_main_window.h"
#include <QtWidgets/QApplication>
#include <QtGui/QRegularExpressionValidator>
#include <QtNetwork/QHostAddress>
#include <QtCore/QDateTime>
#include <QtCore/QElapsedTimer>
#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<quint16>(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<QTcpSocket*>(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<quint8>( readData.at(i) );
if ((byte == 0) || (byte == 0xFF))
byte = 0xAA;
else if ((i % 2) == 0)
++byte;
else
--byte;
sendData.append( static_cast<char>(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<int>(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<int>(sendCount),
static_cast<int>(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<QTcpSocket*>(sender());
if (socket) {
QHostAddress clientAddress = socket->peerAddress();
clientAddressStr = clientAddress.toString();
}
}
Application::addToLog(
ui->textEditLog,
tr("Закрытие входящего подключения %1").arg(clientAddressStr),
Application::LogTypeWarning);
}
//==============================================================================
} // mamespace //===============================================================

View File

@ -25,8 +25,12 @@
#pragma once
#ifndef SERVER_MAINWINDOW_H
#define SERVER_MAINWINDOW_H
//==============================================================================
#include <QtWidgets/QMainWindow>
#include <QtNetwork/QTcpServer>
#include <QtNetwork/QTcpSocket>
#include <QtCore/QMap>
#include <QtCore/QList>
//==============================================================================
QT_BEGIN_NAMESPACE
@ -47,8 +51,21 @@ public:
~MainWindow();
private:
Ui::MainWindow *ui;
const qint64 timeout {5000};
Ui::MainWindow *ui;
QTcpServer server;
QList<QTcpSocket*> clients;
void initialize();
void updateControls();
void disconnectClients();
private slots:
void btnConnect();
void newConnection();
void clientRead();
void clientDisconnect();
};
} // mamespace //===============================================================