Архитектура - gogafefe/StAlGoLM GitHub Wiki

Архитектура

Как обрабатывается один запрос

Клиент → mytcpserver.cpp (принял байты, нашёл границу сообщения)
       → server_functions.cpp: processCommand() (разобрал команду, вызвал нужный cmd_*)
       → databasemanager.cpp (если нужно — прочитал/записал в SQLite)
       → taskN.cpp (если это команда taskN — сгенерировал условие)
       → ответ пишется обратно в тот же QTcpSocket

Многоклиентность

MyTcpServer — синглтон (getInstance()), но у каждого подключённого клиента свой QTcpSocket. Состояние конкретного клиента (залогинен ли, под какой ролью, какое задание сейчас активно) хранится в QMap<long, UserSession> mSessions, где ключ — socketDescriptor() этого клиента. Один и тот же обработчик (slotServerRead) обслуживает всех клиентов сразу — внутри он через sender() узнаёт, какой именно сокет прислал данные, и достаёт из mSessions сессию именно этого клиента.

Граница сообщения (важно)

TCP — это непрерывный поток байт, у него нет встроенного понятия "одно сообщение". Протокол сервера сам придумывает разделитель — байт 0x01 в конце каждой команды.

Раньше код ждал, что этот байт придёт отдельным, самостоятельным чтением из сокета. На практике текст команды и разделитель почти всегда приходят слитно за одно чтение (особенно на localhost) — это привело к реальному багу с авторизацией, подробности в Известные проблемы. Сейчас разделитель ищется внутри пришедших байт, а недописанная часть сообщения копится в буфере mBuffers до тех пор, пока разделитель не найдётся — в том числе если он придёт позже, отдельным чтением.

Задания (task1 … task4)

Каждое задание — отдельная пара .h/.cpp с одинаковой формой:

struct TaskNProblem {
    QString text;       // текст условия, который увидит клиент
    int correctAnswer;   // то, с чем сверяется ответ пользователя
};

TaskNProblem taskN_generate(int num);

num — это номер шаблона внутри самого файла (какая функция/числа подставляются), а не номер варианта из общего списка курса. Например, task2 (вариант №6, метод Ньютона) и task3 (вариант №3, половинное деление) используют num от 1 до 10, а task1 и task4 — от 1 до 40. Диспетчер в cmd_task по числу taskType (1–4) решает, какой из taskN_generate вызвать — добавление нового задания означает: новый taskN.h/.cpp + одна ветка case N в cmd_task + поддержка таблицы статистики (см. databasemanager.cpp) + строчка в help.

Статистика

На каждое задание — своя таблица StatisticTaskN (N = 1..4) в SQLite, с колонками p1..p40. Для task1 и task4 используются все 40, для task2 и task3 — первые 10 (в каждом по 10 шаблонов функций). Таблицы создаются один раз при первом запуске сервера.

Все операции со статистикой (создание строк при регистрации, обновление ячейки, чтение, удаление при удалении аккаунта) проходят по всем четырём таблицам — при добавлении нового задания достаточно, чтобы каждый из этих циклов покрывал новый номер таблицы.