Skip to content

dvetutnev/tomsksoft

Repository files navigation

  • Консольное приложение сервер, имеющее настроечный файл (IP, port), принимающий произвольное кол-во клиентов.
  • От клиента принимать произвольную строчку, выводить на экран и записывать её в файл.
  • Иметь сборочный скрипт CMakeList.txt.
  • Возможность запустить сервер в докер контейнере(должен прилагаться Docker.file).
  • Исходники выложить на Git(lab/hub).
  • Все запускается под Linux.
  • Исходные коды тестового задания можно будет выложить в открытый доступ, и можете использовать их по своему усмотрению.

  • Общение через TCP
  • Клиент может несколько строчек отправить.
  • Формат пакета : |uint |строка| (первые 4 байта это длина строки, за ними идет сама строка)
  • Если клиент в течении 20 секунд ничего не присылает, считать соединение мертвым и закрывать его.

Делать будем с использованием state-машин. Такую мелкую штуку можно и без них написать, но тогда переходы между состояниями и условия переходов будут тонким слоем размазаны по всему проекту. С машинами состояний переходы (transitions), условия (guards) и действия (actions) будут собраны в декларативные таблички. Заодно этот проект пригодится в качестве примера для статьи "Как я перестал боятся и полюбил конечные автоматы".

Сессия

Машина состояний:

Session FSM

Структура сессии:

Session classes

А теперь небольшое пояснение зачем такая странная конструкция сделана. Связка uvw, к которой обработчики отдельно прицепляются на handle-лы и state-машины позволила получить полностью декларативный код, все состояния и переходы между ними описаны простой табличкой. В нем ровно одно ветвление: цикл для передачи в state-машину отдельных байтов. Если бы uvw позволяла по одному байту получать из сокета, то вообще без ветвлений получилось.

Реализация сессии:

template <typename Server, typename Writer, typename Socket, typename Timer>
class Session : public SessionBase
{
public:
    Session(Server&, Writer&, std::shared_ptr<Socket>, std::shared_ptr<Timer>);

private:
    Server& server;
    Writer& writer;

    std::shared_ptr<Socket> client;
    std::shared_ptr<Timer> timer;


    std::string headerBuffer;
    std::uint32_t dataLength;
    std::string dataBuffer;

    void init();
    void pushToHeaderBuffer(char);
    bool isHeaderComplete() const;
    void processHeader();
    void pushToDataBuffer(char);
    bool isDataComplete() const;
    void processResult();
    void restart();
    void halt();

    struct DefFSM
    {
        explicit DefFSM(Session& s) : session{s} {}

        struct InitEvent {};
        struct ByteEvent { char byte; };
        struct HaltEvent {};

        auto operator()() const {

            auto init = [this] () { session.init(); };
            auto pushToHeaderBuffer = [this] (const ByteEvent& e) { session.pushToHeaderBuffer(e.byte); };
            auto isHeaderComplete = [this] () -> bool { return session.isHeaderComplete(); };
            auto processHeader = [this] () { session.processHeader(); };
            auto pushToDataBuffer = [this] (const ByteEvent& e) { session.pushToDataBuffer(e.byte); };
            auto isDataComplete = [this] () -> bool { return session.isDataComplete(); };
            auto processResult = [this] { session.processResult(); };
            auto restart = [this] { session.restart(); };
            auto halt = [this] { session.halt(); };

            using namespace boost::sml;

            return make_transition_table(
                *"Wait init"_s          + event<InitEvent> / init                     = "Receive header"_s,
                 "Receive header"_s     + event<ByteEvent> / pushToHeaderBuffer       = "Is header complete"_s,
                 "Is header complete"_s   [ !isHeaderComplete ]                       = "Receive header"_s,
                 "Is header complete"_s   [ isHeaderComplete  ]                       = "Process header"_s,
                 "Process header"_s                        / processHeader            = "Receive data"_s,
                 "Receive data"_s       + event<ByteEvent> / pushToDataBuffer         = "Is data complete"_s,
                 "Is data complete"_s     [ !isDataComplete ]                         = "Receive data"_s,
                 "Is data complete"_s     [ isDataComplete  ]                         = "Process result"_s,
                 "Process result"_s                        / (processResult, restart) = "Receive header"_s,

                *"In work"_s            + event<HaltEvent> / halt                     = X
            );
        }

        Session& session;
    };

    DefFSM defFsm;
    boost::sml::sm<DefFSM> fsm;
};


template <typename Server, typename Writer, typename Socket, typename Timer>
Session<Server, Writer, Socket, Timer>::Session(Server& s, Writer& w, std::shared_ptr<Socket> c, std::shared_ptr<Timer> t)
    :
      server{s},
      writer{w},

      client{std::move(c)},
      timer{std::move(t)},

      defFsm{*this},
      fsm{defFsm}
{
    auto dataHandler = [this] (const uvw::DataEvent& e, auto&) {
        for (std::size_t i = 0; i < e.length; i++) {
            typename DefFSM::ByteEvent byteEvent{e.data[i]};
            fsm.process_event(byteEvent);
        }
    };
    client->template on<uvw::DataEvent>(dataHandler);

    auto errorHandler = [this] (const uvw::ErrorEvent&, auto&) { fsm.process_event(typename DefFSM::HaltEvent{}); };
    client->template on<uvw::ErrorEvent>(errorHandler);

    auto timeoutHandler = [this] (const uvw::TimerEvent&, auto&) { fsm.process_event(typename DefFSM::HaltEvent{}); };
    timer->template on<uvw::TimerEvent>(timeoutHandler);

    fsm.process_event(typename DefFSM::InitEvent{});
}

Методы Session, которые дергает state-машина не показаны, почти все они однострочные и логики там нет, вся логика собрана transition table. Класс Session шаблонизирован для подстановки моков во время тестирования (статический полиморфизм).

Обработка данных

Машина состояний довольно простая (обработка ошибок не показанна):

Writer FSM

И тут опять очень пригодились возможности state-машины: если во время записи прилетит еще один эвент, то он поместиться в очередь. По завершению записи данных предыдущего эвента отложенный эвент извлекается из очереди (если их несколько прилетело, то сохраняется порядок) и обрабатывается. И все это происходит автоматически)) Открытие файла будет для упрощения синхронным (при инстанционировании класса Writer), обработка ошибок ФС - std::abort (пусть с этим разбирается то, что запустило этот сервер, на уровне возникновения проблема не решается). Разумеется можно прикрутить открытие файла по приходу первого эвента с данными, но для экономии времени на текущий момент реализовавать такую штуку не буду. Реализация:

template <typename File>
class Writer
{
public:
    Writer(std::shared_ptr<File>);

    void push(const std::string&);
    void shutdown();

private:
    std::shared_ptr<File> file;

    void writeToFile(const std::string&);
    void closeFile();

    std::int64_t offset = 0;

    struct DefFSM
    {
        explicit DefFSM(Writer& w) : writer{w} {}

        struct DataEvent { std::string data; };
        struct WrittenEvent {};
        struct ShutdownEvent {};

        auto operator()() const {

            auto write = [this] (const DataEvent& e) { writer.writeToFile(e.data); };
            auto close = [this] (const ShutdownEvent&) { writer.closeFile(); };

            using namespace boost::sml;

            return make_transition_table(
                *"Wait"_s   + event<DataEvent>      / write = "Write"_s,
                 "Wait"_s   + event<ShutdownEvent>  / close = X,
                 "Write"_s  + event<DataEvent>      / defer,
                 "Write"_s  + event<ShutdownEvent>  / defer,
                 "Write"_s  + event<WrittenEvent>           = "Wait"_s
            );
        }

        Writer& writer;
    };

    DefFSM defFsm;
    boost::sml::sm<DefFSM, boost::sml::defer_queue<std::deque>> fsm;
};

Благодаря state-машине довольно легко реализовалось чистое завершение, после вызова метода shutdown Writer перестает принимать входящие сообщения, дописывает в файл то, что ему накидали (лежит в очереди state-машины) и закрывает файл. Подтверждающий тест:

TEST(Writer, shutdown) {
    auto file = std::make_shared<NiceMock<MockFile>>();

    MockHandle::THandler<uvw::FsEvent<uvw::FileReq::Type::WRITE>> handlerWriteEvent;
    EXPECT_CALL(*file, saveWriteHandler).WillOnce(SaveArg<0>(&handlerWriteEvent));
    {
        InSequence _;
        EXPECT_CALL(*file, write).Times(1);
        EXPECT_CALL(*file, close).Times(1);
    }

    Writer<MockFile> writer{file};

    writer.push("aaaa");
    writer.shutdown();
    writer.push("bbbb");

    uvw::FsEvent<uvw::FileReq::Type::WRITE> event{"file.txt", 4};
    handlerWriteEvent(event, *file);
}

Сервер

При запуске инстанционирует ожидающией сокет и Writer. При подключение клиента инстанционирует клиентский сокет и Session передавая ей клиентский сокет. Сессия при отлючении клиента удаляет себя из сервера (метод remove). При остановке сервер закрывает ожидающий сокет и пришибает сессии (метод halt). Реализация (тут я уж не стал тестами обкладывать):

class Server
{
public:
    Server(uvw::Loop& loop, std::string ip, unsigned int port, const std::filesystem::path&);

    void remove(SessionBase*);
    void stop();

private:
    uvw::Loop& loop;

    std::shared_ptr<uvw::TCPHandle> listener;

    using W = Writer<uvw::FileReq>;
    std::shared_ptr<W> writer;

    using S = Session<Server, W, uvw::TCPHandle, uvw::TimerHandle>;
    std::set<std::shared_ptr<SessionBase>> connections;
};


inline Server::Server(uvw::Loop& loop, std::string ip, unsigned int port, const std::filesystem::path& path)
    :
      loop{loop}
{
    listener = loop.resource<uvw::TCPHandle>();

    auto onConnect = [this] (const uvw::ListenEvent&, auto&) {
        auto client = this->loop.resource<uvw::TCPHandle>();
        this->listener->accept(*client);

        auto timer = this->loop.resource<uvw::TimerHandle>();

        auto session = std::make_shared<S>(*this, *writer, std::move(client), std::move(timer));
        this->connections.insert(std::move(session));
    };
    listener->on<uvw::ListenEvent>(onConnect);

    auto onError = [] (const uvw::ErrorEvent& e, auto&) {
        std::cout << "Server error: " << e.what() << std::endl;
        std::abort();
    };
    listener->on<uvw::ErrorEvent>(onError);

    listener->bind("127.0.0.1", 4242);
    listener->listen();

    auto file = loop.resource<uvw::FileReq>();
    file->openSync(path, O_CREAT | O_RDWR, 0644);
    writer = std::make_shared<W>(std::move(file));
}

inline void Server::remove(SessionBase* s) {
    auto pred = [s] (const std::shared_ptr<SessionBase>& conn) -> bool {
        return conn.get() == s;
    };
    auto it = std::find_if(std::begin(connections), std::end(connections), pred);
    assert(it != std::end(connections));
    connections.erase(it);
}

inline void Server::stop() {
    listener->close();

    auto copy = connections;
    for (auto& session : copy) {
        session->halt();
    }

    std::cout << "Server in stop" << std::endl;
}

Функциональный тест:

TEST(Functional, _) {
    const std::filesystem::path path = "log.txt";
    if (std::filesystem::exists(path)) {
        std::filesystem::remove(path);
    }

    auto loop = uvw::Loop::getDefault();

    Server server{*loop, "127.0.0.1", 4242, path};

    auto socket = loop->resource<uvw::TCPHandle>();
    auto onConnect = [](const uvw::ConnectEvent&, uvw::TCPHandle& socket){
        uvw::DataEvent packet = createPacket("sTRINg");
        socket.write(std::move(std::move(packet.data)), packet.length);
        socket.close();
    };
    auto onError = [](const uvw::ErrorEvent& e, auto&) {
        FAIL() << "Connect failed: " << e.what();
    };

    socket->once<uvw::ConnectEvent>(onConnect);
    socket->on<uvw::ErrorEvent>(onError);
    socket->connect(std::string{"127.0.0.1"}, 4242);

    auto timer = loop->resource<uvw::TimerHandle>();
    auto onTimer = [&server, &timer](const uvw::TimerEvent&, auto&) {
        server.stop();
        timer->close();
    };
    timer->on<uvw::TimerEvent>(onTimer);

    timer->start(std::chrono::milliseconds{100}, std::chrono::milliseconds{0});


    loop->run();


    std::fstream file{path};
    std::string log;
    file >> log;

    ASSERT_EQ(log, "sTRINg");
}

Промежуточные выводы

При выполнении этой ТЗшки я решил поэкспериментировать (поумничать) и опробовать новую для себя библиотеку state-машин Boost.SML и статический полиморфизм для подмены объектов асинхронного IO.

Boost.SML удивительная вещь. Очень редко когда с новой библиотекой все получается сразу, вот с ней очень даже. А описание таблицы состояний прямо в нотации UML заслуживает отдельного лайка. Вот что дает перегрузка операторов, можно без всяких парсеров реализовать нужный синтаксис.

Статитеский полиморфизм это удобно. Классы Session и Writer писались и отлаживались на mock-объектах (таймер\сокет\файл), с реальными классами из uvw оно заработало сразу. Хотя казалось бы, mock-ки и рабочие классы даже общей базы не имеют. Но документацию на мокируемые объекты нужно читать внимательно и не стесняться для mock-а написать тест.

Контейнер

Сборка и запуск контейнера:

docker build -t tomsksoft .
docker run -i -t --rm -p 6789:6789 tomsksoft

Address: 0.0.0.0, port: 6789, log: "log.txt"
dddd
aaaa
^CServer in stop

cat log.txt
ddddaaaa

Отправка сообщений в сервер:

build/tomsksoft-client 127.0.0.1 6789 dddd aaaa

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages