Skip to content

ДЗ Блокирующий ввод вывод

xphoenix edited this page Sep 20, 2018 · 5 revisions

Домашнее задание 4 - блокирующий ввод-вывод

Изучить построение сетевого приложения на основе блокирующего API ввода-вывода, рассмотреть достоинства и недостатки этого метода.

Основной скелет для домашнего задания уже написан в виду:

  • src/protocol/Parser.{h,cpp} - парсер протокола memcached (тесты и пойманные блохи категорически приветствуются)
  • src/network/mt_blocking/ServerImpl.cpp - прототип сервера на блокирующем API

Задача

Если сейчас запустить afina --network mt_block --storage mt_lru, то сразу после подключения сервер выведет клиенту сообщение-заглушку и закроет соединение. Задача в том, чтобы реализовать общение по memcached протоколу.

Парсер

Парсер задекларирован в src/protocol/Parser.h и предоставляет следующее API:

  • bool Parse(const char *input, const size_t size, size_t &parsed) - принимает строку символов и продолжает разбор данных так, как будто бы это продолжение строки из предыдущего вызова. В параметр parsed возвращается количество байт из строки, которое было разобрано, больше их в парсер передавать не надо. Метод возвращает true как только команда была считана из входного потока.
  • std::unique_ptr<Execute::Command> Build(uint32_t &body_size) const - Если метод Parse вернул true, данный метод создает экземпляр команды, которую можно выполнить над Storage с помощью метода Execute. Возвращаемый параметр body_size показывает сколько еще надо прочитать байт из сокета, чтобы получить аргумент команды
  • Перед чтением следующей команды нужно вызвать метод Reset

Команды

Каждая команда реализует интерфейс из include/afina/execute/Command.h:

class Command {
public:
    Command() {}
    virtual ~Command() {}
    virtual void Execute(Storage &storage, const std::string &args, std::string &out) = 0;
};

Метод Execute выполняет какое-то действие над заданным хранилищем данных. args это аргумент команды переданный по сети, к примеру это значение ключа для add или set, может быть пустым, как у get.

В выходной параметр out команда должна вернуть ответ, который будет записан клиенту. После отправки ответа, сервер также должен записать в сокет \r\n

Сервер

src/network/mt_block/ServerImpl.cpp это прототип сервера на блокирующем API, нужно реализовать следующую логику:

  • После создания нового соединения, нужно:
    • Если предел параллельных соединений еще не достигнут, создать новый Worker поток и обработать соединение в нем
    • Если предел достигнут, тогда закрыть соединение

Внутри каждого рабочего потока:

  • Прочитать команду
  • Прочитать аргумент команды, если body_size > 0
  • Выполнить команду, если при выполнение возникло исключение, вернуть клиенту строку SERVER_ERROR <описание>\r\n
  • Записать результат команды в соединение с пользователем, затем записать \r\n
  • Повторять, пока либо сервер не остановится, либо соединение не закроется.

При остановке сервера:

  • Дождаться окончания выполнения текущей команды
  • Записать результат
  • Закрыть соединение
  • Как только будет закрыт последний рабочий поток, остановить основной поток сервера
  • Отпустить все потоки, который ждали остановки сервера в Join

внимание: пример того, как может быть устроен цикл обработки одного соединения можно посмотреть в src/st_blocking/ServerImpl.cpp

Данные

В первом домашнем задание было написано LRU хранилище. В готовом виде его использовать вместе с mt_blockнельзя, т.к к нему будут обращаться насколько потоков, что создаст race condition.

Исходя из этого нужно реализовать класс src/storage/ThreadSafeSimpleLRU.h:

  • Содержит в себе экземпляр класса SimpleLRU
  • Глобальный std::mutex
  • Каждый вызываемый метод, должен захватить mutex, делегировать работу в SimpleLRU, затем отпустить mutex и вернуть результат.

внимание: т.к все вызываемые методы интерфейса storage виртуальный, то "делегирование" может быть сделано через наследование, как написано в коде afina, однако вы можете и переписать этот код, сохранив ссылку на SimpleLRU явно

Тесты

Тестов нет, проверять руками. Ожидаемое поведение:

[user@domain build] ./src/afina --network mt_block --storage mt_lru

Запустит приложение, к которому можно подсоединиться по порту 8080. Теперь туда можно отправлять команды на исполнение:

[user@domain build] echo -n -e "set foo 0 0 6\r\nfooval\r\n" | nc localhost 8080

Для того, чтобы проверить, что сервер дожидается выполнения команд при остановке, можно в какую-то из команд добавить sleep