Подтвердить что ты не робот

Лучшая структура для реализации протокола на основе запроса

Я использую протокол, который в основном является протоколом запроса и ответа по TCP, как и другие линейные протоколы (SMTP, HTTP и т.д.).

Протокол имеет около 130 различных методов запросов (например, логин, пользовательский ввод, обновление пользователя, получение журнала, информация о файле, информация о файлах,...). Все эти методы не так хорошо сопоставляются с широкими методами, которые используются в HTTP (GET, POST, PUT,...). Такие широкие методы приведут к некоторым непоследовательным поворотам фактического значения.

Но методы протокола могут быть сгруппированы по типу (например, управление пользователями, управление файлами, управление сеансом,...).

Текущая реализация на стороне сервера использует class Worker с методами ReadRequest() (читает запрос, состоящий из списка параметров метода плюс), HandleRequest() (см. ниже) и WriteResponse() (записывает код ответа и фактические данные ответа).

HandleRequest() вызовет функцию для фактического метода запроса - используя хэш-карту имени метода для указателя функции-члена к фактическому обработчику.

Фактический обработчик - это простая функция-член, по одному для каждого метода протокола: каждый проверяет свои входные параметры, делает все, что ему нужно, и устанавливает код ответа (успех да/нет) и данные ответа.

Пример кода:

class Worker {
    typedef bool (Worker::*CommandHandler)();
    typedef std::map<UTF8String,CommandHandler> CommandHandlerMap;

    // handlers will be initialized once
    //   e.g. m_CommandHandlers["login"] = &Worker::Handle_LOGIN;
    static CommandHandlerMap m_CommandHandlers;

    bool HandleRequest() {
        CommandHandlerMap::const_iterator ihandler;
        if( (ihandler=m_CommandHandlers.find(m_CurRequest.instruction)) != m_CommandHandler.end() ) {
            // call actual handler
            return (this->*(ihandler->second))();
        }
        // error case:
        m_CurResponse.success = false;
        m_CurResponse.info = "unknown or invalid instruction";
        return true;
    }

    //...


    bool Handle_LOGIN() {
        const UTF8String username = m_CurRequest.parameters["username"];
        const UTF8String password = m_CurRequest.parameters["password"];

        // ....

        if( success ) {

            // initialize some state...
            m_Session.Init(...);
            m_LogHandle.Init(...);
            m_AuthHandle.Init(...);

            // set response data
            m_CurResponse.success = true;
            m_CurResponse.Write( "last_login", ... );
            m_CurResponse.Write( "whatever", ... );
        } else {
            m_CurResponse.Write( "error", "failed, because ..." );
        }
        return true;
    }


};

Итак. Проблема в том, что мой рабочий класс теперь имеет около 130 "методов обработчика команд". И каждый из них нуждается в доступе к:

  • параметры запроса
  • объект ответа (для записи данных ответа)
  • различные другие локальные объекты сеанса (например, дескриптор базы данных, дескриптор запросов авторизации/разрешения, ведение журнала, обращение к различным подсистемам сервера и т.д.).

Что такое хорошая стратегия для лучшего структурирования этих методов обработчика команд?

Одна из идей заключалась в том, чтобы иметь один класс для каждого обработчика команд и инициализировать его ссылками на запросы, объекты ответа и т.д., но накладные расходы не приемлемы (на самом деле это добавит косвенность для любого отдельного доступа к все, что требуется обработчику: запрос, ответ, объекты сеанса,...). Это может быть приемлемым, если это даст реальное преимущество. Однако это не кажется разумным:

class HandlerBase {
protected:
    Request &request;
    Response &response;
    Session &session;
    DBHandle &db;
    FooHandle &foo;
    // ...
public:
    HandlerBase( Request &req, Response &rsp, Session &s, ... )
    : request(req), response(rsp), session(s), ...
    {}
    //...
    virtual bool Handle() = 0;
};

class LoginHandler : public HandlerBase {
public:
    LoginHandler( Request &req, Response &rsp, Session &s, ... )
    : HandlerBase(req,rsp,s,..)
    {}
    //...
    virtual bool Handle() {
        // actual code for handling "login" request ...
    }
};

Хорошо, HandlerBase может просто взять ссылку (или указатель) самому рабочему объекту (вместо ссылок на запрос, ответ и т.д.). Но это также добавит еще одну косвенную (this- > worker- > session вместо this- > session). Это направление было бы в порядке, если бы оно приобрело какое-то преимущество в конце концов.

Некоторая информация об общей архитектуре

Рабочий объект представляет собой один рабочий поток для фактического TCP-соединения с некоторым клиентом. Каждый поток (так, каждый рабочий) нуждается в собственном дескрипторе базы данных, дескрипторе авторизации и т.д. Эти "дескрипторы" - это потоковые объекты, которые разрешают доступ к некоторой подсистеме сервера.

Вся эта архитектура основана на какой-то инъекции зависимостей: например, для создания объекта сеанса необходимо предоставить "дескриптор базы данных" для конструктора сеанса. Затем объект сеанса использует этот дескриптор базы данных для доступа к базе данных. Он никогда не будет вызывать глобальный код или использовать одиночные игры. Таким образом, каждый поток может работать без изменений сам по себе.

Но стоимость заключается в том, что вместо того, чтобы просто вызывать одиночные объекты - рабочий и его обработчики команд должны обращаться к любым данным или другому коду системы через такие специфичные для потока дескрипторы. Эти дескрипторы определяют контекст выполнения.

Резюме и разъяснение: мой фактический вопрос

Я ищу элегантную альтернативу текущему ( "рабочий объект с огромным списком методов обработчика" ): он должен быть поддержанным, иметь низкую нагрузку и не должен требовать слишком много написания клея. Кроме того, он ДОЛЖЕН все еще разрешать каждому отдельному методу управлять очень разными аспектами его выполнения (это означает: если метод "super flurry foo" хочет сбой, когда включена полная луна, тогда для этой реализации это должно быть возможно), Это также означает, что я не хочу, чтобы какой-либо объект абстракции (создание/чтение/обновление/удаление XFoo-типа) на этом архитектурном уровне моего кода (он существует на разных уровнях моего кода). Этот архитектурный уровень - чистый протокол, и ничего больше.

В конце концов, это, безусловно, будет компромиссом, но меня интересуют любые идеи!

Бонус AAA: решение со взаимозаменяемыми реализациями протокола (а не только текущее class Worker, которое отвечает за разбор запросов и написание ответов). Возможно, может быть взаимозаменяемый class ProtocolSyntax, который обрабатывает данные синтаксиса протокола, но все еще использует наши новые блестящие структурированные обработчики команд.

4b9b3361

Ответ 1

У вас уже есть самые правильные идеи, вот как я буду продолжать.

Давайте начнем со второго вопроса: сменные протоколы. Если у вас есть общие объекты запроса и ответа, вы можете иметь интерфейс, который читает запросы и пишет ответы:

class Protocol {
  virtual Request *readRequest() = 0;
  virtual void writeResponse(Response *response) = 0;
}

и вы могли бы реализовать реализацию под названием HttpProtocol.

Что касается ваших обработчиков команд, "один класс для каждого обработчика команд" - правильный подход:

class Command {
  virtual void execute(Request *request, Response *response, Session *session) = 0;
}

Обратите внимание, что я свернул все обычные дескрипторы сеанса (DB, Foo и т.д.) в один объект вместо того, чтобы проходить вокруг целого ряда параметров. Кроме того, использование этих параметров метода вместо аргументов конструктора означает, что вам нужен только один экземпляр каждой команды.

Затем у вас будет CommandFactory, который содержит карту имен команд для командных объектов:

class CommandFactory {
  std::map<UTF8String, Command *> handlers;

  Command *getCommand(const UTF8String &name) {
    return handlers[name];
  }
}

Если вы все это сделали, Worker становится чрезвычайно тонким и просто координирует все:

class Worker {
  Protocol *protocol;
  CommandFactory *commandFactory;
  Session *session;

  void handleRequest() {
    Request *request = protocol->readRequest();
    Response response;

    Command *command = commandFactory->getCommand(request->getCommandName());
    command->execute(request, &response, session);

    protocol->writeResponse(&response);
  }
}

Ответ 2

Если бы это был я, я бы, вероятно, использовал гибридное решение этих двух в вашем вопросе.
У вас есть базовый класс рабочего класса, который может обрабатывать несколько взаимосвязанных команд и может позволить вашему основному классу "отправка" проверять поддерживаемые команды. Для клей вам просто нужно сообщить классу диспетчера о каждом рабочем классе.

class HandlerBase
{
public:
    HandlerBase(HandlerDispatch & dispatch) : m_dispatch(dispatch) {
        PopulateCommands();
    }
    virtual ~HandlerBase();

    bool CommandSupported(UTF8String & cmdName);

    virtual bool HandleCommand(UTF8String & cmdName, Request & req, Response & res);
    virtual void PopulateCommands();

protected:
    CommandHandlerMap m_CommandHandlers; 
    HandlerDispatch & m_dispatch;
};

class AuthenticationHandler : public HandlerBase
{
public:
    AuthenticationHandler(HandlerDispatch & dispatch) : HandlerBase(dispatch) {}

    bool HandleCommand(UTF8String & cmdName, Request & req, Response & res) {
        CommandHandlerMap::const_iterator ihandler;                     
        if( (ihandler=m_CommandHandlers.find(req.instruction)) != m_CommandHandler.end() ) {                     
            // call actual handler                     
            return (this->*(ihandler->second))(req,res);                     
        }                     
        // error case:                     
        res.success = false;                     
        res.info = "unknown or invalid instruction";                     
        return true; 
    }

    void PopulateCommands() {
        m_CommandHandlers["login"]=Handle_LOGIN;
        m_CommandHandlers["logout"]=Handle_LOGOUT;
    }

    void Handle_LOGIN(Request & req, Response & res) {
        Session & session = m_dispatch.GetSessionForRequest(req);
        // ...
    }
};

class HandlerDispatch
{
public:
    HandlerDispatch();
    virtual ~HandlerDispatch() {  
        // delete all handlers 
    }

    void AddHandler(HandlerBase * pHandler);
    bool HandleRequest() {
        vector<HandlerBase *>::iterator i;
        for ( i=m_handlers.begin() ; i < m_handlers.end(); i++ ) {
            if ((*i)->CommandSupported(m_CurRequest.instruction)) {
                return (*i)->HandleCommand(m_CurRequest.instruction,m_CurRequest,m_CurResponse);
            }
        }
        // error case:                                                            
        m_CurResponse.success = false;
        m_CurResponse.info = "unknown or invalid instruction";

        return true; 
    }
protected:
    std::vector<HandlerBase*> m_handlers;
}

И затем, чтобы склеить все это, вы сделаете что-то вроде этого:

// Init
m_handlerDispatch.AddHandler(new AuthenticationHandler(m_handlerDispatch));

Ответ 3

Что касается конкретной части транспорта (TCP), вы просмотрели библиотеку ZMQ, которая поддерживает различные распределенные вычислительные шаблоны через сокеты обмена сообщениями/очереди? IMHO вы должны найти соответствующий шаблон, который удовлетворяет ваши потребности в документе Guide.

Для выбора реализации протокольных сообщений я бы лично любил буферы протокола google, который очень хорошо работает с С++, мы используем его для пары проектов.

По крайней мере, вы сходитесь к реализациям диспетчера и обработчика для конкретных запросов и их параметров + необходимые возвращаемые параметры. Расширения сообщений Google protobuf позволяют это в общем виде.

EDIT:

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

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

Я не знаю, как лучше объяснить это, или показать конкретный пример того, как улучшить существующий код при таком подходе. Я боюсь, что вы уже потратили некоторые усилия на код де-/сериализации ваших форматов сообщений, чего можно было избежать, используя сообщения google protobuf (или какие классы Request и Response?).

Библиотека ZMQ может помочь реализовать ваш контекст Session для отправки запросов через инфраструктуру.

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

Ответ 4

Я думаю, что это идеальный случай для REST-подобной реализации. Другим способом может быть также группировка методов обработчика, основанная на категории/любых других критериях для нескольких рабочих классов.

Ответ 5

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

Но очень вероятно, что методы той же группы имеют некоторые из следующих общих черт:

  • В классе Worker могут быть некоторые поля данных, которые используются только одной группой методов или несколькими (но не каждой) группой. Например, если m_AuthHandle может использоваться только с помощью методов управления пользователями и управления сеансами.
  • Могут быть некоторые группы входных параметров, используемые каждым методом некоторой группы.
  • Могут быть некоторые общие данные, записанные в ответ любым методом какой-либо группы.
  • Могут быть некоторые распространенные методы, называемые несколькими методами некоторой группы.

Если некоторые из этих фактов верны, есть веская причина группировать эти функции в разные классы. Не один класс для каждого обработчика команд, а один класс для каждой группы событий. Или, если есть особенности, общие для нескольких групп, иерархия классов.

Может быть удобно группировать экземпляры всех этих групповых классов в одном месте:

classe UserManagement: public IManagement {...};
classe FileManagement: public IManagement {...};
classe SessionManagement: public IManagement {...};
struct Handlers {
  smartptr<IManagement> userManagement;
  smartptr<IManagement> fileManagement;
  smartptr<IManagement> sessionManagement;
  ...
  Handlers():
    userManagement(new UserManagement),
    fileManagement(new FileManagement),
    sessionManagement(new SessionManagement),
    ...
  {}
};

Вместо new SomeClass может использоваться некоторый шаблон, например make_unique. Или, если нужны "взаимозаменяемые реализации протокола", одна из возможностей заключается в использовании заводов вместо некоторых (или всех) new SomeClass операторов.

m_CommandHandlers.find() следует разделить на два поиска по карте: один - найти соответствующий обработчик в этой структуре, другой (в соответствующей реализации IManagement) - найти указатель функции-члена для фактического обработчика.

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

Также реализация IManagement может содержать метод WriteCommonResponce, чтобы упростить написание полей ответа, общие для всех обработчиков событий.

Ответ 6

Шаблон Command - это ваше решение для обоих аспектов этой проблемы.

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

Затем реализуйте свои команды таким же образом с помощью интерфейса ICommand и каждого метода команд, реализованного в отдельном классе. Вы почти там с этим. Разделите существующие методы на новые специализированные классы.

Оберните свои запросы и ответы как объекты Mememento