IscanderChe
31 июля 2019 г. 13:57

Проект Simple Tracker. Часть 7: сервер и клиент

Содержание

Рассмотрим теперь сервер и клиента.

С вопросами, касающимися общей организации взаимодействия клиента и сервера, можно ознакомиться в этой статье: " Пример использования QLocalServer и QLocalSocket ". Здесь я коснусь лишь моментов, непосредственно связанных с передачей информации по задаче и закрытием задачи.

Сервер

  1. // trackerserver.cpp
  2.  
  3. TrackerServer::TrackerServer(QWidget* parent)
  4. : QWidget(parent)
  5. {
  6. // Задаём имя сервера
  7. nameServer = "TrackerServer";
  8. // Устанавливаем размер следующего блока равным нулю
  9. nextBlockSize = 0;
  10. ...
  11. }
  12.  
  13. // Метод создания и запуска сервера
  14. void TrackerServer::createServer()
  15. {
  16. localServer = new QLocalServer(this);
  17. // Если сервер не запустить, выдать сообщение и завершить программу
  18. if(!localServer->listen(nameServer))
  19. {
  20. QMessageBox::critical(0,
  21. tr("Ошибка сервера"),
  22. tr("Невозможно запустить сервер ") + nameServer + ": "
  23. + localServer->errorString());
  24. localServer->close();
  25. return;
  26. }
  27. }
  28. ...
  29.  
  30. // Метод создания подключений
  31. void TrackerServer::createConnections()
  32. {
  33. // Подключение сигнала сервера о новом подключении к обработчику нового подключения
  34. connect(localServer, &QLocalServer::newConnection, this, &TrackerServer::slotNewConnection);
  35. ...
  36. }

Теперь рассмотрим слоты сервера.

  1. // trackerserver.cpp
  2.  
  3. // Слот обработки нового соединения
  4. void TrackerServer::slotNewConnection()
  5. {
  6. // Получаем сокет, подключённый к серверу
  7. QLocalSocket* socket = localServer->nextPendingConnection();
  8.  
  9. // Соединяем сигнал отключения сокета с обработчиком удаления сокета
  10. connect(socket, &QLocalSocket::disconnected, socket, &QLocalSocket::deleteLater);
  11.  
  12. // Соединяем сигнал сокета о готовности передачи данных со слотом закрытия задачи
  13. connect(socket, &QLocalSocket::readyRead, this, &TrackerServer::slotCloseTask);
  14. }
  15.  
  16. // Слот получения информации от клиента и закрытия задачи
  17. void TrackerServer::slotCloseTask()
  18. {
  19. QLocalSocket* socket = (QLocalSocket*)sender();
  20. QDataStream in(socket);
  21. in.setVersion(QDataStream::Qt_5_3);
  22. for(;;)
  23. {
  24. if(!nextBlockSize)
  25. {
  26. if(socket->bytesAvailable() < (int)sizeof(quint16))
  27. break;
  28. in >> nextBlockSize;
  29. }
  30.  
  31. in >> clientTaskNumber >> clientRevision;
  32.  
  33. nextBlockSize = 0;
  34. }
  35.  
  36. if(!database->isClosed(clientTaskNumber) && clientTaskNumber && clientRevision)
  37. // Если задача не закрыта и если номер задачи и ревизия не пустые, закрываем задачу
  38. {
  39. database->closeTask(clientTaskNumber, QString("%1").arg(clientRevision));
  40.  
  41. // Обновляем SQL-таблицу задач
  42. taskSqlTableModel->select();
  43.  
  44. // Если все задачи для текущего проекта закрыты
  45. if(database->isClosedAllTasks(projectCurrentId))
  46. // сделать доступной кнопку "Архивировать"
  47. archiveProjectButton->setEnabled(true);
  48. else
  49. // иначе блокировать кнопку "Архивировать"
  50. archiveProjectButton->setDisabled(true);
  51. }
  52. }

Клиент

  1. // main.cpp
  2.  
  3. #include "trackerclient.h"
  4. #include <QCoreApplication>
  5. #include <QFile>
  6. #include <QTextStream>
  7. #include <QDebug>
  8. #include <QRegExp>
  9.  
  10. int main(int argc, char* argv[])
  11. {
  12. QCoreApplication app(argc, argv);
  13.  
  14. // Считываем из аргумента 1 командной строки путь к файлу log.txt
  15. QString pathToFile = argv[1];
  16.  
  17. QString result;
  18.  
  19. // Если файл открывается, считываем данные из файла в строку
  20. QFile file(pathToFile);
  21. if(file.open(QIODevice::ReadOnly | QIODevice::Text))
  22. {
  23. QTextStream stream(&file);
  24. result = stream.readAll();
  25. file.close();
  26. }
  27. else
  28. qDebug() << "Unable open file " + pathToFile;
  29.  
  30. int taskNumber = 0;
  31. int revisionNumber = 0;
  32.  
  33. // С помощью регулярного выражения извлекаем из строки номер задачи и номер ревизии
  34. QString expr = "(.*)(\\#\\{)(\\d+)(\\})(.*)(r\\{)(\\d+)(\\})";
  35. QRegExp taskRevision(expr);
  36. int position = taskRevision.indexIn(result);
  37. if(position > -1)
  38. {
  39. taskNumber = taskRevision.cap(3).toInt(); // это номер задачи
  40. revisionNumber = taskRevision.cap(7).toInt(); // это номер ревизии
  41. }
  42.  
  43. // Инициализируем клиента с соответствующими параметрами
  44. TrackerClient client("TrackerServer", taskNumber, revisionNumber);
  45.  
  46. app.exit(0);
  47. }

Теперь разберём вопрос, откуда берётся log.txt.

Как я уже писал в предыдущей части, для упрощения трекера по кнопке создания проектов можно создать только проект без поддержки СКВ. То есть такой виртуальный проект, существующий только в базе данных трекера и больше нигде.

Проекты с поддержкой СКВ формируются с помощью bat-файла. Его я написал несколько раньше из необходимости создавать и использовать репозитории SVN так, чтобы они при наличии коммитов автоматизированно архивировались. Рассмотрим его в той части, которая касается создания проектов с поддержкой СКВ, то есть уже реально существующих в файловой системе проектов.

  1. rem Запрашиваем наименование проекта
  2. set /p repo_name=[Input name project]:
  3. echo Name repository: %repo_name%
  4.  
  5. rem Устанавливаем путь к репозиторию
  6. set repo_path=%repo_root%\%repo_name%
  7. echo Full path to repository: %repo_path%
  8.  
  9. rem Создаём репозиторий
  10. svnadmin create %repo_path%
  11.  
  12. rem Очищаем папку репозитория hooks
  13. del /f /q %repo_path%\hooks\
  14.  
  15. rem Создаём / копируем в папке hooks файлы ini.bat, pre-commit.bat, post-commit.bat
  16. echo set commit=n>%repo_path%\hooks\ini.bat
  17. copy pre-commit.bat %repo_path%\hooks\
  18. copy post-commit2.bat %repo_path%\hooks\post-commit.bat
  19.  
  20. rem Импортируем файловую структуру по умолчанию и формируем первый коммит
  21. svn import %def_tree% file:///%repo_path% -m "initial import"

Файл ini.bat содержит в себе флаг commit, устанавливаемый в "n", если коммитов нет, или устанавливаемый в "y", если есть хотя бы один коммит. По его состоянию скрипт архивации определяет, надо ли архивировать данный репозиторий.

  1. rem pre-commit.bat
  2. @echo off
  3.  
  4. rem Считываем переменные из командной строки
  5. set repo_path=%1
  6. set transaction=%2
  7.  
  8. rem Записываем сообщение коммита в файл log.txt
  9. "G:\soft\svn\bin\svnlook.exe" log -t "%transaction%" "%repo_path%">%repo_path%\hooks\log.txt

В сообщении коммита содержится номер закрываемой задачи в формате #{ <номер задачи> } .

  1. rem post-commit.bat
  2.  
  3. @echo off
  4.  
  5. rem Считываем переменные из командной строки
  6. set repo_path=%1
  7. set revision=%2
  8.  
  9. rem Устанавливаем в файле ini.bat флаг commit в "y"
  10. echo set commit=y>%repo_path%\hooks\ini.bat
  11.  
  12. rem Записываем номер ревизии в файл log.txt
  13. echo r{%revision%}>>%repo_path%\hooks\log.txt
  14.  
  15. rem
  16. %path_to_tracker%\ICTrackerClient.exe %repo_path%\hooks\log.txt

После записи ревизии в post-commit.bat файл log.txt выглядит примерно следующим образом.

  1. Closing task #{2} end some editing files
  2. r{29}

И последним вызывается клиент, который считывает эти данные из файла.

Осталось только передать эти данные серверу.

  1. // trackerclient.h
  2.  
  3. #ifndef TRACKERCLIENT_H
  4. #define TRACKERCLIENT_H
  5.  
  6. #include <QObject>
  7. #include <QLocalSocket>
  8.  
  9. class TrackerClient : public QObject
  10. {
  11. Q_OBJECT
  12.  
  13. public:
  14. explicit TrackerClient(const QString& serverName, int taskId, int revision,
  15. QObject* parent = 0);
  16.  
  17. ~TrackerClient();
  18.  
  19. private:
  20. QLocalSocket* localSocket;
  21. int taskId;
  22. int revision;
  23.  
  24. private slots:
  25. void sendToServer();
  26. };
  27.  
  28. #endif // TRACKERCLIENT_H
  1. // trackerclient.cpp
  2.  
  3. #include "trackerclient.h"
  4. #include <QDataStream>
  5.  
  6. TrackerClient::TrackerClient(const QString &serverName, int taskId, int revision,
  7. QObject* parent) :
  8. QObject(parent), taskId(taskId), revision(revision)
  9. {
  10. localSocket = new QLocalSocket(this);
  11. localSocket->setServerName(serverName);
  12.  
  13. connect(localSocket, &QLocalSocket::connected, this, &TrackerClient::sendToServer);
  14.  
  15. localSocket->connectToServer();
  16. }
  17.  
  18. TrackerClient::~TrackerClient()
  19. {
  20. }
  21.  
  22. void TrackerClient::sendToServer()
  23. {
  24. QByteArray arrayBlock;
  25. QDataStream out(&arrayBlock, QIODevice::WriteOnly);
  26. out.setVersion(QDataStream::Qt_5_3);
  27. out << quint16(0) << taskId << revision;
  28.  
  29. out.device()->seek(0);
  30. out << quint16(arrayBlock.size() - sizeof(quint16));
  31.  
  32. localSocket->write(arrayBlock);
  33.  
  34. localSocket->disconnectFromServer();
  35. }

По статье задано0вопрос(ов)

3

Вам это нравится? Поделитесь в социальных сетях!

Evgenii Legotckoi
  • 1 августа 2019 г. 14:18

Добрый день, и снова немного ревью.

Вот это каст в стиле Си

  1. QLocalSocket* socket = (QLocalSocket*)sender();

Лучше кастовать в стиле С++, мы же на С++ пишем.

  1. QLocalSocket* socket = static_cast<QLocalSocket*>(sender());

Получается несколько многословнее, но честно, в большом проекте хоть есть за что глазу зацепиться. Плюс более контролируемый каст.

Далее, думаю, что вот это

  1. // Если все задачи для текущего проекта закрыты
  2. if(database->isClosedAllTasks(projectCurrentId))
  3. // сделать доступной кнопку "Архивировать"
  4. archiveProjectButton->setEnabled(true);
  5. else
  6. // иначе блокировать кнопку "Архивировать"
  7. archiveProjectButton->setDisabled(true);

Можно переписать так

  1. archiveProjectButton->setEnabled(database->isClosedAllTasks(projectCurrentId));

setEnabled и setDisabled делают тоже самое.

Вот это

  1. explicit TrackerClient(const QString& serverName, int taskId, int revision,
  2. QObject* parent = 0);

Нужно переписать так

  1. explicit TrackerClient(const QString& serverName, int taskId, int revision, QObject* parent = nullptr);

nullptr - это специальный псевдотип данных для нулевого указателя. В случае с 0, там по сути может оказаться какой угодно мусор в зависимости от платформы. В случае с nullptr поведение будет предсказуемым.

Далее вот это объявление

  1. QLocalSocket* localSocket;

Наверное стоит вам сразу писать как

  1. QLocalSocket* m_localSocket;

Это принятый код-стайл в Qt, а также именно на такую запись заточены макросы Q_PROPERTY. Ну и стоит в деструкторе удалять m_localSocket, чтобы не было утчечек памяти. Вообще этот как раз тот случай, где имеет смысл использовать умные указатели, как вы делали в прошлой статье.

IscanderChe
  • 1 августа 2019 г. 16:31
  • (ред.)

  1. QLocalSocket* socket = static_cast<QLocalSocket*>(sender());

Получается несколько многословнее, но честно, в большом проекте хоть есть за что глазу зацепиться. Плюс более контролируемый каст.

Да, я уже со static_cast освоился, такая запись понятнее.

setEnabled и setDisabled делают тоже самое

Ага.

nullptr - это специальный псевдотип данных для нулевого указателя

Понял.

Наверное стоит вам сразу писать как QLocalSocket* m_localSocket;

А что означает буква m перед наименованием? Эти буквы меня всегда путают и с толку сбивают.

Evgenii Legotckoi
  • 1 августа 2019 г. 16:37

А что означает буква m перед наименованием? Эти буквы меня всегда путают и с толку сбивают.

member - член класса, некоторое поле. В Qt это обычное обозначение тех объектов, которые объявлены в private или protected секциях.

Просто в Q_PROPERTY это прописывается без m_ но макрос разворачивается на член класса, который содержит этот префикс.

IscanderChe
  • 1 августа 2019 г. 16:56
  • (ред.)

Вообще этот как раз тот случай, где имеет смысл использовать умные указатели

Как правильно объявить умный указатель и инициализировать по отдельности? Я пробовал так:

  1. QSharedPointer<HelloWorldClass> hwc2;
  2. hwc2 = new HelloWorldClass();

выдаёт ошибку при компиляции.

Evgenii Legotckoi
  • 1 августа 2019 г. 16:59
  • (ред.)
  1. QSharedPointer<HelloWorldClass> hwc2;
  2. hwc2.reset(new HelloWorldClass()); // Удаляет автоматически предыдущий объект, если он там был, и если нет указателей на тот объект в других QSharedPointer
IscanderChe
  • 1 августа 2019 г. 17:20
  • (ред.)

Теперь на коннекте ошибку выдаёт:

  1. connect(localSocket, &QLocalSocket::connected, this, &TrackerClient::sendToServer);
Evgenii Legotckoi
  • 1 августа 2019 г. 17:24
  • (ред.)

connect(m_localSocket.get(), &QLocalSocket::connected, this, &TrackerClient::sendToServer);

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь
  • Последние комментарии
  • Evgenii Legotckoi
    16 апреля 2025 г. 17:08
    Благодарю за отзыв. И вам желаю всяческих успехов!
  • IscanderChe
    12 апреля 2025 г. 17:12
    Добрый день. Спасибо Вам за этот проект и отдельно за ответы на форуме, которые мне очень помогли в некоммерческих пет-проектах. Профессиональным программистом я так и не стал, но узнал мно…
  • AK
    1 апреля 2025 г. 11:41
    Добрый день. В данный момент работаю над проектом, где необходимо выводить звук из программы в определенное аудиоустройство (колонки, наушники, виртуальный кабель и т.д). Пишу на Qt5.12.12 поско…
  • Evgenii Legotckoi
    9 марта 2025 г. 21:02
    К сожалению, я этого подсказать не могу, поскольку у меня нет необходимости в обходе блокировок и т.д. Поэтому я и не задавался решением этой проблемы. Ну выглядит так, что вам действитель…
  • VP
    9 марта 2025 г. 16:14
    Здравствуйте! Я устанавливал Qt6 из исходников а также Qt Creator по отдельности. Все компоненты, связанные с разработкой для Android, установлены. Кроме одного... Когда пытаюсь скомпилиров…