Архитектура сетевого приложения
От: Mazay Россия  
Дата: 02.05.12 17:49
Оценка:
Пишу приложение, работающее по простенькому пакетному протоколу.
Общий цикл работы приложения такой:

  1. Принять пакет
  2. Распарсить пакет
  3. Если пакет удовлетворяет условию A_i, то вызвать функцию F_i где i = 1...N.

Проблема в том, что функции F_i принимают разные наборы параметров, разные как по количеству так и по типам данных. В качестве параметров могут выступать как поля полученного пакета (передаются как rvalue, то есть не должны меняться в F_i), так и переменные, живущие на протяжении всего цикла работы (свойства соединения, счётчик пакетов, некое состояние протокола и т. п., эти вещи могут передаваться как lvalue и меняться внутри F_i).

Код сейчас выглядит примерно так:

struct BasePacket
{
   virtual void F(Connection &, Database &) const = 0; // большой список параметров
};

struct PingPacket: public BasePacket
{   
   virtual void F(Connection &conn, Database & /*unused parameter*/) const // лишние параметры
   {
     ...
     conn.send_data(int_field+1);
     ++conn._packet_counter;   // нужно допускать PingPacket к внутренностям Connection
     ...
   }

   int int_field;
};

struct PongPacket: public BasePacket
{
   virtual void F(Connection & /*unused parameter*/, Database &db) const // лишние параметры
   {
     ...
     db.save(float_field);
     conn._timer.stop();   // нужно допускать PingPacket к внутренностям Connection
     ...
   }

   float float_field;
};

Connection::main_cycle(Database &db)
{
   while (1)
   {
        BasePacket *packet = receive_packet(); /// Здесь фабрика: получаем пакет, парсим, в зависимости от того, 
                                               /// что получили, создаём того или иного потомка BasePacket.

        packet->F(*this, db);                  /// Передаём всё окружение, необходимое исполнения логики протокола.
   }
}


Ситуация дурацкая. По идее наследники BasePacket это не более чем хранилища распарсенных данных из сетевых пакетов. Но получается, что по их виртуальным методы распихивается логика протокола (мне же не хочется писать огромный switch по типам пакетов). Протокол stateful, так что всем этим методам нужен доступ к общим данным (Connection, Database, Counter, Cache, Query, etc). В принципе все эти данные можно уложить в Connection, но тогда получится, что методы пакетов работают с данными соединения, что нарушает инкапсуляцию. По хорошему бы эти методы должны быть членами Connection, а к данным из соответствующего пакета доступаться через геттеры (которые всё равно есть). Но как тогда реализовать полиморфный вызов этих методов? Таки очень не хочется писать огромный switch по типам пакетов.

Подозреваю, что то, что мне нужно, называется мультиметодами. Чтобы работало вот так:

Connection::handle_packet(PingPacket *packet)
{
     send_data(packet.get_int_field()+1);
     ++packet_counter;    // работаем со своими приватными переменными
}

Connection::handle_packet(PongPacket *packet)
{
     db.save(packet.get_float_field());
     _timer.stop();   // работаем со своими приватными переменными
}

Connection::main_cycle(Database &db)
{
   ...
      BasePacket *packet = receive_packet(); /// Здесь фабрика: ...
      handle_packet(packet);    //здесь полиморфный вызов по формальному параметру, а не по this.
   ...
}


Можно ли здесь обойтись без опасных хаков и жуткого синтаксиса? Может ещё как-нибудь архитектуру перевернуть?
Главное гармония ...
мультиметоды
Re: Архитектура сетевого приложения
От: ParfenMyshkin  
Дата: 02.05.12 18:46
Оценка: 1 (1)
Здравствуйте, Mazay, Вы писали:

M>Пишу приложение, работающее по простенькому пакетному протоколу.


M>Можно ли здесь обойтись без опасных хаков и жуткого синтаксиса? Может ещё как-нибудь архитектуру перевернуть?


На предыдущем проекте убедился, что для сетевого приложения, работающего по некоторому протоколу с состояниями, хорошо подходит конечный автомат, или иначе State Machine (если работа по протоколу главная задача приложения). Если не нравится решение через switch, можно применить паттерн State (Gof). Я вначале не автомат применил, столкнулся со сложностями, которые сами собой решились, когда переделал все на автомат.
Re: Архитектура сетевого приложения
От: Тот кто сидит в пруду Россия  
Дата: 03.05.12 10:18
Оценка:
Здравствуйте, Mazay, Вы писали:

M>Можно ли здесь обойтись без опасных хаков и жуткого синтаксиса? Может ещё как-нибудь архитектуру перевернуть?


Я делал просто — у Connection есть контейнер, в который кто угодно может добавить что угодно (унаследованное от общего предка, само собой). И потом найти соответствующий ConnectionPart по типу или имени. Примерно как с фацетами локалей. И есть некоторый протокол удаления/использования этих ConnectionPart при разрыве/восстановлении связи, аварийном отключении клиента и т.п. Типа всем ConnectionPart вызвать вовремя OnDropConnection, OnCloseConnection, удалять в порядке, обратном порядку добавления и т.п. Пока хватает.
Одним из 33 полных кавалеров ордена "За заслуги перед Отечеством" является Геннадий Хазанов.
Re: Архитектура сетевого приложения
От: uzhas Ниоткуда  
Дата: 03.05.12 11:16
Оценка:
Здравствуйте, Mazay, Вы писали:


M>Проблема в том, что функции F_i принимают разные наборы параметров, разные как по количеству так и по типам данных. В качестве параметров могут выступать как поля полученного пакета (передаются как rvalue, то есть не должны меняться в F_i), так и переменные, живущие на протяжении всего цикла работы (свойства соединения, счётчик пакетов, некое состояние протокола и т. п., эти вещи могут передаваться как lvalue и меняться внутри F_i).


во-первых, я вашу задачу воспринимаю как реализацию простого rpc
во-вторых, не рекомендую употреблять rvalue\lvalue не к месту, в данном случае уместнее было бы говорить о константных параметрах (передаваемых в пакете), и инфраструктурных параметрах (неконстантных), которые являются частью приложения и не содержатся в пакете
думаю, что у вас не пакеты должны быть полиморфными, а их обработчики, причем желательно отделять обработчика от реальной бизнес-логики, чтобы отделить работу о слаботипизированными данными (пакетом) от реальной логики. которая работает с конкретными типам (int, string, etc)
введите доп сущность и она справится со всем
по типу пакета можно switch делать, а можно и поиск в map<packetID, packetHandler>

успехов
Re[2]: Архитектура сетевого приложения
От: Mazay Россия  
Дата: 04.05.12 13:10
Оценка:
Здравствуйте, ParfenMyshkin, Вы писали:

PM>На предыдущем проекте убедился, что для сетевого приложения, работающего по некоторому протоколу с состояниями, хорошо подходит конечный автомат, или иначе State Machine (если работа по протоколу главная задача приложения). Если не нравится решение через switch, можно применить паттерн State (Gof). Я вначале не автомат применил, столкнулся со сложностями, которые сами собой решились, когда переделал все на автомат.


У меня не на столько сложная логика протокола, чтобы автомат пользовать. Там на каждый тип пакета практически один и тот же код выполняется, просто с разными параметрами. Соответственно State здесь тоже никак не поможет.
Главное гармония ...
Re[2]: Архитектура сетевого приложения
От: Mazay Россия  
Дата: 04.05.12 13:14
Оценка:
Здравствуйте, Тот кто сидит в пруду, Вы писали:

ТКС>Здравствуйте, Mazay, Вы писали:


M>>Можно ли здесь обойтись без опасных хаков и жуткого синтаксиса? Может ещё как-нибудь архитектуру перевернуть?


ТКС>Я делал просто — у Connection есть контейнер, в который кто угодно может добавить что угодно (унаследованное от общего предка, само собой). И потом найти соответствующий ConnectionPart по типу или имени. Примерно как с фацетами локалей. И есть некоторый протокол удаления/использования этих ConnectionPart при разрыве/восстановлении связи, аварийном отключении клиента и т.п. Типа всем ConnectionPart вызвать вовремя OnDropConnection, OnCloseConnection, удалять в порядке, обратном порядку добавления и т.п. Пока хватает.



То есть код так и останется в методах пакетов, которые будут получать ссылку на хранилище как формальный параметр. Сейчас так и есть, но мне не нравится, что пакеты совмещают две обязанности — хранение данных пакета и логику работы протокола. Особенно печально, что логика работы размазана по методам разных объектов и даже классов.
Главное гармония ...
Re[2]: Архитектура сетевого приложения
От: Mazay Россия  
Дата: 04.05.12 13:40
Оценка:
Здравствуйте, uzhas, Вы писали:

M>>Проблема в том, что функции F_i принимают разные наборы параметров, разные как по количеству так и по типам данных. В качестве параметров могут выступать как поля полученного пакета (передаются как rvalue, то есть не должны меняться в F_i), так и переменные, живущие на протяжении всего цикла работы (свойства соединения, счётчик пакетов, некое состояние протокола и т. п., эти вещи могут передаваться как lvalue и меняться внутри F_i).


U>думаю, что у вас не пакеты должны быть полиморфными, а их обработчики, причем желательно отделять обработчика от реальной бизнес-логики, чтобы отделить работу о слаботипизированными данными (пакетом) от реальной логики. которая работает с конкретными типам (int, string, etc)

С тем, что полиморфизм пакетов не нужен я полностью согласен. Дальше ничего не понял. Реальная бизнес-логика и обработчик чем по вашему отличаются? У меня вроде бы ничем. Аналогично с типами — у меня в пакеты собственно и содержат int, string, etc, что тогда есть "конкретные типы"?

U>введите доп сущность и она справится со всем

Эммм.... Какую?

U>по типу пакета можно switch делать, а можно и поиск в map<packetID, packetHandler>

Это одно и тоже по сути.

Мне кажется проблема в том, что логика задачи требует дважды делать ветвление по типу пакета. Первый раз при его парсинге, второй — при выборе обработчика. Сейчас первый выбор делается в фабрике пакетов, а второй выбор инкапсулирован в вызов полиморфной функции уже созданного пакета.
Это решение мне не нравится, потому что логика протокола размазана по пакетам. Но если я помещу обработчики в какой-то один класс, то мне придётся второй выбор делать явным образом (опять через switch или через map<packetID, packetHandler>).

Вот была бы перегрузка функций по указателю на полиморфный объект. Например в виде таблицы виртуальных функций других классов.
Главное гармония ...
Re[3]: Архитектура сетевого приложения
От: Тот кто сидит в пруду Россия  
Дата: 04.05.12 13:50
Оценка:
Здравствуйте, Mazay, Вы писали:

M>>>Можно ли здесь обойтись без опасных хаков и жуткого синтаксиса? Может ещё как-нибудь архитектуру перевернуть?


ТКС>>Я делал просто — у Connection есть контейнер, в который кто угодно может добавить что угодно (унаследованное от общего предка, само собой). И потом найти соответствующий ConnectionPart по типу или имени. Примерно как с фацетами локалей. И есть некоторый протокол удаления/использования этих ConnectionPart при разрыве/восстановлении связи, аварийном отключении клиента и т.п. Типа всем ConnectionPart вызвать вовремя OnDropConnection, OnCloseConnection, удалять в порядке, обратном порядку добавления и т.п. Пока хватает.



M>То есть код так и останется в методах пакетов, которые будут получать ссылку на хранилище как формальный параметр. Сейчас так и есть, но мне не нравится, что пакеты совмещают две обязанности — хранение данных пакета и логику работы протокола. Особенно печально, что логика работы размазана по методам разных объектов и даже классов.


Возможно, у нас сильно разные задачи. Не исключено также, что мы и говорим о разном. У меня довольно жирный сервер с полноценным самодельным rpc. Пакеты представляют собой сериализуемые "команды" — хэндл объекта, по которому ищется экземпляр класса, указатель на метод, и параметры. Логика работы естественным образом размазана по сотням классов. В ConnectionPart живет та привязанная к текущему соединению информация, которую не имеет смысла пропихивать в каждый rpc-метод, но которая может понадобится в любой момент. Ну, например, имя текущего пользователя. Оно может потребоваться на 33 уровне вложенности бизнес-логики для вывода в лог, причем, например, не в нити, обрабатывающей в настоящий момент rpc-вызов, а в нити, которая запущена в результате rpc-вызова. А сама rpc нить в этот момент уже с другим пользователем работает или вообще завершилась. Причем с логикой работы протокола это тоже неким образом связано — например, пока не завершена имперсонация, исполнять можно только команды-пакеты, для которых это специально указано.
Другие варианты работы с состоянием соединения, по-моему, в моем случае еще хуже.
Одним из 33 полных кавалеров ордена "За заслуги перед Отечеством" является Геннадий Хазанов.
Re: Архитектура сетевого приложения
От: jazzer Россия Skype: enerjazzer
Дата: 04.05.12 13:57
Оценка: 4 (1) +2
Здравствуйте, Mazay, Вы писали:

M>Таки очень не хочется писать огромный switch по типам пакетов.

Автоматизируй
switch — самое прямое решение тут. В объектах-сообщениях только данные (заодно и уйдет ненужная тут иерархия с виртуальностями), вся обработка в обработчиках.
Если не хочется писать switch вручную, то можно использовать разные автоматизированные суррогаты, например, создавать сообщение как boost::variant и потом его обрабатывать static_visitor'ом, либо использовать бустовский switch_ от Steven Watanabe.
jazzer (Skype: enerjazzer) Ночная тема для RSDN
Автор: jazzer
Дата: 26.11.09

You will always get what you always got
  If you always do  what you always did
Re[3]: Архитектура сетевого приложения
От: uzhas Ниоткуда  
Дата: 04.05.12 14:04
Оценка: +1
Здравствуйте, Mazay, Вы писали:

M>Реальная бизнес-логика и обработчик чем по вашему отличаются? У меня вроде бы ничем. Аналогично с типами — у меня в пакеты собственно и содержат int, string, etc, что тогда есть "конкретные типы"?

в пакете должна лежать некая аморфная субстанция (это xml или void* + size в случае своего бинарного протокола) и пакет должен предоставлять методы для вытаскивания типизированных данных типа std::string, int, etc
обработчик пакетов должен вытаскивать нужные данные из пакета и вызывать функцию бизнес-логики, обработчик пакетов не является бизнес-логикой, он является частью rpc
обработчики полиморфные: они знают что и как надо вытащить из пакета и как вытащенные данные пробросить в необходимую функцию (типа f1)

U>>введите доп сущность и она справится со всем

M>Эммм.... Какую?
обработчик пакетов

M>Мне кажется проблема в том, что логика задачи требует дважды делать ветвление по типу пакета. Первый раз при его парсинге

не вижу смысла делать ветвление при парсинге: пакет лишь содержит некие данные и его структура одинаковая для разных видов данных
Re: Архитектура сетевого приложения
От: Хвост  
Дата: 04.05.12 19:20
Оценка: 4 (1)
Здравствуйте, Mazay, Вы писали:
можно и с двойной диспетчеризацией (а-ля визитор), т.е. как-то так:


interface logic {
  handle_packet(ping_packet) = 0;
  handle_packet(pong_packet) = 0;
};

interface packet {
  virtual process(logic) = 0; // dispatch to logic, see below
};


class ping_packet : public packet {
  process(logic) {
    logic->handle_packet(this);
  }

  // ping-packet specific fields/accessors
  // ..
};

class pong_packet : public packet {
  process(logic) {
    logic->handle_packet(this);
  }

  // pong-packet specific fields/accessors
  // ..
};

// create concrete packets from raw data (xml, bytearray, etc)
class packet_factory {
  shared_ptr<packet> parse(rawpacket);
};

class protocol_logic : public logic {
  rawdata_source rawdata_src;
  packet_factory pkt_factory;
  database db;
  connection_properties cp;

  logic_loop() {
    while ( rawpacket = rawdata_src->produce() ) {
      shared_ptr<packet> packet = pkt_factory->parse(rawpacket);
      packet->process(this);
    }
  }

  handle_packet(ping_packet) {
    // ...
  }

  handle_packet(pong_packet) {
    // ...
  }
};
People write code, programming languages don't.
Re[2]: Архитектура сетевого приложения
От: Mazay Россия  
Дата: 05.05.12 09:30
Оценка:
Здравствуйте, Хвост, Вы писали:

Х>Здравствуйте, Mazay, Вы писали:

Х>можно и с двойной диспетчеризацией (а-ля визитор), т.е. как-то так:
...
Да. Похоже либо так, либо второй switch, как jazzer говорит.
Спасибо.
Главное гармония ...
Re: Архитектура сетевого приложения
От: vic.scherba  
Дата: 08.04.14 21:27
Оценка:
Здравствуйте!

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

#include <iostream>
#include <functional>

#include <mml/generation/make_multimethod.hpp>


struct BasePacket
{
   virtual void F() const = 0; // большой список параметров
};

struct PingPacket: public BasePacket
{   
   virtual void F() const
   {
   }

   int int_field;
};

struct PongPacket: public BasePacket
{
   virtual void F() const // лишние параметры
   {
   }

   float float_field;
};

struct Connection
{
    void handle_packet_base(BasePacket *packet)
    {
        std::cout << typeid(BasePacket).name();
    }

    void handle_packet_ping(PingPacket *packet)
    {
        std::cout << typeid(PingPacket).name();
    }

    void handle_packet_pong(PongPacket *packet)
    {
        std::cout << typeid(PongPacket).name();
    }

    void main_cycle()
    {
        // p может указывать на любой объект подкласса BasePacket или самого BasePacket
        BasePacket *p = new PingPacket; // receive_packet()

        mml::make_multimethod(
              std::mem_fun(&Connection::handle_packet_base)
            , std::mem_fun(&Connection::handle_packet_ping)
            , std::mem_fun(&Connection::handle_packet_pong)
            ) // создает функциональный объект
        (this, p); // вызывает его

        // если доступно ключевое слово auto, то можно сохранить мультиметод в переменную auto mm = make_multimethod(...),
        // иначе, можно сохранить, обернув в boost::function<void(Connection*, BasePacket*)> mm = make_multimethod(...),
        // или передать в шаблонную функцию, которая сама выведет тип мультиметода
        // как своего шаблонного параметра, например: process_packet(make_multimethod(...), this, p)
    }
};


Единственное, что здесь важно — это создать мультиметод.
Для этого нужно заинклудить: <mml/generation/make_multimethod.hpp> и вызвать mml::make_multimethod.

Хотя в этом примере мультиметод вырожденный (он диспетчеризует по одному полиморфному аргументу, а не по множеству), тем не менее его можно использовать как внешнюю полиморфную функцию над одним аргументом (connection здесь не полиморфен и в диспетчеризации не участвует).
В этой библиотеке можно смешивать полиморфные и неполиморфные аргументы. Диспетчеризация в рантайме будет работать только на полиморфных аргументах (будет вычисляться их реальный динамический подтип).

Библиотека находится тут.
Описание с примерами и теорией можно почитать тут.
Надеюсь, библиотека будет полезна.
Re: Архитектура сетевого приложения
От: IROV..  
Дата: 08.04.14 22:07
Оценка:
Здравствуйте, Mazay, Вы писали:

M>Пишу приложение, работающее по простенькому пакетному протоколу.

M>Общий цикл работы приложения такой:

M>

    M>
  1. Принять пакет
    M>
  2. Распарсить пакет
    M>
  3. Если пакет удовлетворяет условию A_i, то вызвать функцию F_i где i = 1...N.
    M>

M>Проблема в том, что функции F_i принимают разные наборы параметров, разные как по количеству так и по типам данных. В качестве параметров могут выступать как поля полученного пакета (передаются как rvalue, то есть не должны меняться в F_i), так и переменные, живущие на протяжении всего цикла работы (свойства соединения, счётчик пакетов, некое состояние протокола и т. п., эти вещи могут передаваться как lvalue и меняться внутри F_i).


M>Код сейчас выглядит примерно так:

По своему опыту скажу что самое простое и быстрое и лучшее всего всего всего, это rpc + codegen
за примером — попробуйте пару простых примеров из http://www.zeroc.com/ я на нем делал сервер для ММО

дальше была попытка написать свое
http://sourceforge.net/projects/axe-engine/

использовал asio + spirit

огромный плюс данного подхода, это очень удобный дебаг
я не волшебник, я только учусь!
Re: Архитектура сетевого приложения
От: Кодт Россия  
Дата: 09.04.14 09:09
Оценка:
Здравствуйте, Mazay, Вы писали:

M>Можно ли здесь обойтись без опасных хаков и жуткого синтаксиса? Может ещё как-нибудь архитектуру перевернуть?


Во-первых, контекст для функций (по крайней мере, общую его часть) можно передавать не россыпью аргументов, а одной структурой.

Во-вторых, раз у тебя есть однозначное соответствие тип пакета — способ обработки (вынесенный вовне), то
(не знаю, как по-русски сказать, — проще показать подходы к мультиметодам)
struct Handler {
  void handle(A*);
  void handle(B*);
  void handle(C*);
};

struct Factory {
  A* createA(SRC*);
  B* createB(SRC*);
  C* createC(SRC*);
};

// исходно было - тупой мультиметод на свитчах
Packet* create(SRC* s, Factory* f)
{
  if(this_is_a) return f->createA(s);
  if(this_is_b) return f->createB(s);
  if(this_is_c) return f->createC(s);
  throw wtf;
};

SRC* s;
Factory* f;
Handler* h;

Packet* p = create(s, f);
// вот этот мультиметод
if(A* q = dynamic_cast<A*>(p)) h->handle(q);
if(B* q = dynamic_cast<B*>(p)) h->handle(q);
if(C* q = dynamic_cast<C*>(p)) h->handle(q);

// патч номер ноль: двойная диспетчеризация прямо в пакете (удобно делать через CRTP, чтобы не плодить код)
struct Packet
{
  .....
  virtual void be_handled(Handler* h) = 0;
};

template<FinalPacketType>
struct PacketBase : Packet
{
  .......
  void be_handled(Handler* h) { h->handle((FinalPacketType*)this); }
};

struct A: PacketBase<A> { ..... };
struct B: PacketBase<B> { ..... };
struct C: PacketBase<C> { ..... };


// патч номер раз: сплавляем фабрику и обработчик
Packet* create_and_handle(SRC* s, Factory* f, Handler* h)
{
  if(this_is_a) { A* p = f->createA(s); h->handle(p); return p; }
  if(this_is_b) { B* p = f->createB(s); h->handle(p); return p; }
  if(this_is_c) { C* p = f->createC(s); h->handle(p); return p; }
  throw wtf;
};

Packet* p = create_and_handle(s,f,h);


// патч номер два: возвращаем продолжение
typedef function<void(Handler*,Packet*)> Continuation;
pair<Packet*,Continuation> create_and_howto(SRC* s, Factory* f)
{
  if(this_is_a) { return make_pair( f->createA(s), (void(Handler::*)(A*))&Handler::handle ); }
  if(this_is_b) { return make_pair( f->createB(s), (void(Handler::*)(B*))&Handler::handle ); }
  if(this_is_c) { return make_pair( f->createC(s), (void(Handler::*)(C*))&Handler::handle ); }
  throw wtf;
};

pair<Packet*,Continuation> pc = create_and_howto(s, f);
pc.second(h, pc.first); // вызываем продолжение тогда, когда нам это нужно

Я не призываю возвращать именно такое продолжение, — там может быть и какой-нибудь объект, — но специфичный именно для фактического класса.
Т.е. мы не тупо стираем тип в недрах фабрики, а возмещаем это возвращением внешнего словаря, — чего-то более подходящего нам, чем type_info, из которого только dynamic_cast и можно добиться.


Выбор подхода определяется степенью связности твоих компонентов.
Если можно всё в одну кучу свалить, то CRTP будет самым простым и накатанным решением.
Перекуём баги на фичи!
Re: Архитектура сетевого приложения
От: niXman Ниоткуда https://github.com/niXman
Дата: 09.04.14 10:40
Оценка:
Здравствуйте, Mazay, Вы писали:

вроде бы YARMI подходит. писался для геймдева, и используется в нескольких наших проектах.
пачка бумаги А4 стОит 2000 р, в ней 500 листов. получается, лист обычной бумаги стОит дороже имперского рубля =)
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.