Пакет дизайн-завод для сетевого приложения


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

Я обнаружил, что лучший способ достичь этого с помощью шаблона "Фабрика". (Или, для более чем одного протокола, абстрактная Фабрика).

Вот моя реализация:

IPacket.ч:

#pragma once
#include "PacketData.h"

class IPacket
{
public:
    // Deleted fuctions
    IPacket(const IPacket&) = delete;

    IPacket& operator=(const IPacket&) = delete;
public: 
    IPacket();
    virtual ~IPacket();

    // Encode data into a byte buffer
    virtual void Serialize(PacketData &) = 0;

    // Decode data from a byte buffer
    virtual void Deserialize(const PacketData &) = 0;
};

Это обеспечивает интерфейс пакета, который будет использоваться для дальнейших реализаций протоколов.

MBXGeneric.ч:

#pragma once
#include "IPacket.h"
#include "MBXHeader.h"
#include "MBXPacketFactory.h"

// Base class for custom protocol 
class MBXGenericPacket : public IPacket
{
public:
    MBXGenericPacket(MBXPacketFactory::MBXPacket _type) : H_MBX(static_cast<uint16_t>(m_Type))
    {   }

    // Serialize MBXHeader
    virtual void Serialize(PacketData& data) override
    {
        data.Data << H_MBX.h_time << H_MBX.P_Type;
    }

    // Extract MBXHeader
    virtual void Deserialize(const PacketData& data) override
    {
        data.Data >> H_MBX >> P_Type;
    }

    MBXPacketFactory::MBXPacket GetType() const { return static_cast<MBXPacketFactory::MBXPacket>( H_MBX.P_Type ); }

    static std::unique_ptr<IPacket> CreateGenericPacket(const MBXPacketFactory::MBXPacket Type)
    {
        return std::make_unique<MBXGenericPacket>(Type);
    }
protected:
    MBXHeader H_MBX;
};

Это базовый класс пример протокола.
Пакеты имеют следующую структуру:

|П / заголовок (метаданные + PacketType) | орган |

Заголовок будет инициализирован с каждого пакета, которые можно вывести из MBXGenericPacket. Затем он может быть расшифрован или закодированных в байтовый буфер.

MBXPacketFactory.ч:

#pragma once
#include <map>
#include <memory>
#include <mutex>
#include "MBXGeneric.h"
#include "Packets.h"

using CreateMBXPacketCb = std::unique_ptr<MBXGenericPacket>(*)();

class MBXPacketFactory
{
public:
    enum class MBXPacket : uint16_t
    {
        MBXGeneric,
        Msg_MessageBox,
        ErrorResponseMsg,

        MAX_PACKET
    };

private:
    using PacketsMap = std::map<MBXPacketFactory::MBXPacket, CreateMBXPacketCb>;

    PacketsMap map;
private:

    MBXPacketFactory()
    {
        map[MBXPacket::Msg_MessageBox] = ChatMessage::CreatePacket;
        map[MBXPacket::ErrorResponseMsg] = ErrorResponse::CreatePacket;
    }
public:
    MBXPacketFactory(const MBXPacketFactory&) = delete;
    MBXPacketFactory & operator=(const MBXPacketFactory&) = delete;
public:

    bool UnRegisterPacket(MBXPacketFactory::MBXPacket Type)
    {
        return map.erase(Type);
    }

    bool RegisterPacket(const MBXPacketFactory::MBXPacket& type, CreateMBXPacketCb callback)
    {
        return map.insert(std::make_pair(type, callback)).second;
    }

    std::unique_ptr<MBXGenericPacket> CreatePacket(MBXPacketFactory::MBXPacket Type)
    {
        auto it = map.find(Type);

        if (it == map.end())
            throw std::runtime_error("Unknown packet type!");

        return it->second();
    }

    static MBXPacketFactory& Get()
    {
        static MBXPacketFactory instance;
        return instance;
    }
};

Вот пример пакетов:

// Packets.h
#include "MBXGeneric.h"
#include "MBXPacketFactory.h"

class ChatMessage : public MBXGenericPacket
{
public:
    ChatMessage() : MBXGenericPacket(MBXPacketFactory::MBXPacket::Msg_MessageBox)
    { }

    void Serialize(PacketData& data) override
    {
        data << H_MBX;

        data << sz_Title << s_Title;
        data << sz_Message << s_Message;
    }

    void Deserialize(const PacketData& data) override
    {
        // Extract the header
        data >> H_MBX;

        data >> sz_Title;
        data.read(s_Title, sz_Title);

        data >> sz_Message;
        data.read(s_Message, sz_Message);
    }

    static std::unique_ptr<MBXGenericPacket> CreatePacket()
    {
        return std::make_unique<ChatMessage>();
    }
public:

    // Getters and setters

    std::string GetTitle()
    {
        return s_Title;
    }

    std::string GetMessage()
    {
        return s_Message;
    }

    void SetTitle(const std::string& title)
    {
        s_Title = title;
    }

    void SetMessage(const std::string& message)
    {
        s_Message = message;
    }


private:
    int32_t sz_Title, sz_Message;

    std::string s_Title, s_Message;
};



class ErrorResponse : public MBXGenericPacket
{
public:
    ErrorResponse() : MBXGenericPacket(MBXPacketFactory::MBXPacket::ErrorResponseMsg)
    { }

    void Serialize(PacketData& data) override
    {
        data << H_MBX;
        data << sz_Error << s_ErrorMessage;
    }

    void Deserialize(const PacketData& data) override
    {
        data >> H_MBX;

        data >> sz_Error;
        data.read(s_ErrorMessage, sz_Error);
    }

    static std::unique_ptr<MBXGenericPacket> CreatePacket()
    {
        return std::make_unique<ErrorResponse>();
    }
public:

    std::string GetErrorMessage()
    {
        return s_ErrorMessage;
    }

    void SetErrorMessage(const std::string& msg_error)
    {
        s_ErrorMessage = msg_error;
    }

private:
    int32_t sz_Error;
    std::string s_ErrorMessage;
};

Пример использования фабрики:

При получении пакета:

...

PacketData NewPacket = connection.RecvPacket();

uint16_t PacketID = GetUint16(NewPacket.Data);

std::unique_ptr<MBXGenericPacket> Packet = MBXPacketFactory::Get().CreatePacket(static_cast<MBXPacketFactory::MBXPacket>(PacketID));

// Up-cast the packet pointer to the right packet type

// Let's asume we've received an error response

std::unique_ptr<ErrorResponse> ErrorPacket(reinterpret_cast<ErrorResponse*>( Packet.release() ));

ErrorPacket->Deserialize(NewPacket);

std::cout << ErrorPacket->GetErrorMessage() << '\n';

...

При отправке пакета:

...

std::string Title = GetUserInput();
std::string Message = GetUserInput();

std::unique_ptr<MBXGenericPacket> Interface = MBXPacketFactory::Get().CreatePacket(MBXPacketFactory::MBXPacket::Msg_MessageBox);

std::unique_ptr<ChatMessage> MessagePk(reinterpret_cast<ChatMessage*>( Interface.release() ));

MessagePk->SetTitle(Title);
MessagePk->SetMessage(Message);

PacketData MsgPacketData;
MessagePk->Serialize(MsgPacketData);

PacketQueue.push(MsgPacketData);

...

Улучшения:

Что хотелось бы улучшить в текущий дизайн:

  • На вниз-отливки из базового класса в производный классв чтобы получить доступ к функции-члены.
  • С каждым новым пакетов, полученных от MBXGenericPacketЯ вынужден кодировать и декодировать MBX_Header в сериализации/десериализации, вызывая код повторения.
  • Для получения требуемого класса пакет с завода, мне нужно сначала извлечь идентификатор из заголовка и затем передать его CreatePacket() метод. Я думаю, что это будет здорово, если вы могли бы просто пройти PacketData на завод, а потом расшифровывать информация по себя.
  • Цель фабрики-это абстрактный процесс создания классов. На мой взгляд, идентификаторы пакетов должен быть закрыт заводе, но это вызывает много шума в коде, как у вас типа: MBXPacketFactory::MBXPacket:: ...

Позвольте мне знать ваши мысли о коде. Я здесь, чтобы учиться, так что любое мнение приветствуется !



Комментарии
4 ответа

Это довольно долго, так что я пишу новый ответ, а не делать это P. S. На первой записке о посетителях.


У вас есть отдельная IPacket (интерфейс-только) и MBXGenericPacket (любой пакет). Я использую только один класс, как я хочу, чтобы мой typeid для присутствовать в полиморфный указатель. Мне не нравится, как у вас есть два класса — который я должен использовать, и стоит ли использовать один или другой в разных обстоятельствах? Я думаю, что это артефакт мышления интерфейсов в стиле Java.

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

enum MBXHeader {
eChat = 0,
eError,
// etc.
};

class Message {
protected:
MBXHeader H_MBX; // packet type ID
Message (MBXHeader id) : H_MBX(id) {}
public:
MBXHeader get_header() const { return H_MBX; }
virtual ~Message() = default;
virtual void serialize(PacketData&) = 0;

// this is your factory (needed for receiving)
static unique_ptr<Message> Create (PacketData& data);
};

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

class ChatMessage : public Message {
public:
int32_t sz_Title;
int32_t sz_Message;
std::string s_Title;
std::string s_Message;

ChatMessage() : Message(eChat) {}
ChatMessage (PacketData& data);
void serialize(PacketData&) override;
};

class ErrorMessage : public Message {
public:
int32_t sz_Error;
int32_t sz_Message;

ErrorMessage() : Message(eError) {}
ErrorMessage (PacketData& data);
void serialize(PacketData&) override;
};

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

Просто создать объект, заполнить его, и отправить его.

   ChatMessage msg;  // no pointers!!
msg.sMessage= "Hello World!";
// ... etc.
PacketData MsgPacketData;
msg->serialize(MsgPacketData);
PacketQueue.push(MsgPacketData);

Для получения сообщения, Вы не знаете тип. Вот где нужен завод.

   PacketData NewPacket = connection.RecvPacket();
auto incomingMessage = Message::Create (NewPacket);

Что теперь вам делать с этим? Самый простой способ:

   incomingMessage->process();  // virtual function call

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

Но это всего лишь игрушки, например, и я по себе знаю, что мы действительно сделать разные вещи мы хотим сделать и изолировать разных систем друг от друга. Так, как указал ваш исходный вопрос, вы хотите использовать шаблон Visitor.

Давайте начнем с того, как вы хотите использовать его, а затем вернуться и заставить его работать.

Как объяснялось ранее, вы определяете свои "перегруженная функция" для каждого типа вы хотите обрабатывать. Подробности того, что вы хотите сделать перейти в тело функции, не представлено. Каждое тело получает параметр правильного типа.

struct process_message {
void operator()(const ChatMessage&) const;
void operator()(const ErrorMessage&) const;
};

Тогда есть некоторые общие диспетчерскую инфраструктуру, которая получает объект справа функции на основе фактического типа.

   visit (process_message(), *incomingMessage);

Просто как вы пишете visit функция? Мы уже смотрели на двойной отправки и не сделаешь этого.

Обратите внимание, что мы ставим наш собственный идентификатор типа кодов, который является fundimental часть провода кодирования различных пакетов. Вот что нам нужно для протокола, и наконец, как приемник действительно знать тип. Так что просто использовать это! В отличии от c++ ИД типа, это компактный диапазон целых чисел так switch/case заявление можно и очень эффективно.

   template<typename V>
void visit (V visitor, Message& m)
{
switch (m.get_header()) {
default: throw std::invalid_argument("oops!");
case eChat: {
auto& msg = static_cast<ChatMessage&>(m);
visitor(msg);
break;
}
case eError: {
auto& msg = static_cast<ErrorMessage&>(m);
visitor(msg);
break;
}
// repeat the cases for each kind of message.
// Note: if checking for non-final types, order matters!
}
}

Кусок торта, но вы должны повторить маленькую оговорку случае вручную.

А как насчет чтения? Вы заметите, что занятия отражают мои предыдущие комментарии, что десериализация выполняется конструктором. Он может быть реализован таким же образом, как посетитель.

   unique_ptr<Message> Message::Create (const PacketData& data)
{
uint16_t PacketID = GetUint16(data.Data);
switch (PacketID) {
default: throw std::invalid_argument("oops!");
case eChat:
return make_unique<ChatMessage>(data);
case eError:
return make_unique<ErrorMessage>(data);
}
// add a case for each message type
}
}

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

2
ответ дан 8 апреля 2018 в 12:04 Источник Поделиться

Для первой точки — отливки из базового класса для каждого использования.

на самом деле нужно?

В последнем блоке кода, Вы создаете пакет типа вы в действительности знать заранее — вы просите ChatMessage так что вы можете заполнить его. Поэтому, не используйте здесь завод! Просто создать объект.

У этого класса предоставляют статический член для этого, поэтому они всегда создаются правильно (в куче).

auto MessagePk = ChatMessage::Create();
MessagePk->SetTitle(Title);

// prep and send it.

Шаблон Visitor

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

Что вы хотите здесь посетителя шаблон.

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

auto Packet= MBXPacketFactory::Get_Next_Packet();  // whatever
Packet.Deserialize(NewPacket); // this one should be a simple virtual function.
visit (my_processor{}, *Packet);

struct my_processor {
void operator() (ChatMessage& packet);
void operator() (ErrorMessage& packet)
{
std::cout << packet->GetErrorMessage() << '\n';
}
// etc.
};


Теперь, как вы пишете visit? Вы можете включить виртуальные функции в иерархии классов, что делает это. Это классический подход, но он имеет тот недостаток, что processor должен наследовать от интерфейса класса и все функции должны быть виртуальными.

class Base {
virtual void visit (const VisitInterface&) =0;
}

class Derived_1 : public Base {
void visit (const VisitInterface& visitor)
{
visitor (*this);
}

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

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


Посмотреть std::variant и первоначальный импульс.Вариант , который имеет хороший обзор и руководство об этом.

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

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

Тогда мой пример выше будет работать, как я написал это по телефону std::visit.

1
ответ дан 4 апреля 2018 в 07:04 Источник Поделиться

Второй момент — повторение кодекс MBX_Header

Это только одна линия, и она должна быть. Все, что общего должны пойти в базовый класс, так что вам нужен только один звонок, чтобы вызвать все это.

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

void Serialize(PacketData& data) override
{
MBXGenericPacket::Serialize(data);
data << sz_Error << s_ErrorMessage;
}

Используя тот же синтаксис для сериализации или десериализации общий заголовок, как у вас в коде, интуитивно понятен и прост для понимания.

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

1
ответ дан 4 апреля 2018 в 08:04 Источник Поделиться

За третий пункт — CreatePacket принимает PacketData

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

Так что да, есть статическая фабрика, включить логику, чтобы выяснить, какой пакет он, скрывая, что деталь от пользователей класса.

Затем, с завода есть данные, почему бы это не сделать тоже десериализовать?
Вы уже диспетчеризации в CreateMBXPacketCb функции, которые написаны отдельно для каждого класса. Так и передайте данные через это!

auto msg = Factory::Create(packet_data);

выясняет тип данных, а затем вызывает соответствующую

unique_ptr<MBXGenericPacket> WhateverClass::CreatePacket(PacketData);

единственное отличие от того, что у вас есть, что CreatePacket принимает параметр сейчас.

Внутри функции:

//static   
unique_ptr<MBXGenericPacket> WhateverClass::CreatePacket(PacketData data)
{
auto self= make_unique<WhateverClass>();
self->Deserialize(data);
return self;
}

Четвертый пункт тоже!

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


Дополнительно

Из моей родовой CreatePacket выше вы можете увидеть, что каждый из них точно так же, за исключением имени собственного класса.

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

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

1
ответ дан 4 апреля 2018 в 09:04 Источник Поделиться