patterncppMinor
Design for a protocol based software in C++
Viewed 0 times
protocoldesignsoftwareforbased
Problem
I want a good design to implement a master-slave protocol. The protocol is proprietary and similar to cctalk.
We have:
Master:
Slave:
My choice was to have concrete base classes for request and for response:
The response can be a good response or a NACK (an error). My Implentation is:
```
class PacketRes {
public:
PacketRes(const std::vector& buffer)
{
if (!checkChecksum( buffer ))
{
throw Error("invalid checksum");
}
PacketHeaderHelper header( buffer );
mCommand = header.command();
if (header.len() > PacketHeaderHelper::HEADER_SIZE)
{
const size_t bodySize = header.len() - PacketHeaderHelper::HEADER_SIZE;
mData.reserve( bodySize );
mData.insert(
mData.end(),
buffer.begin() + PacketHeaderHelper::HEADER_SIZE - 1,
buffer.end() - 1
);
}
}
virtual ~PacketReq() {}
PacketCommand command() const
{
return mCommand;
}
const std::vector& body() const
{
return mData;
}
privat
We have:
Master:
HEADER [ DATA ] CHECKSUMSlave:
HEADER [ DATA ] CHECKSUMMy choice was to have concrete base classes for request and for response:
class PacketReq {
public:
typedef std::vector container;
typedef container::const_iterator const_iterator;
PacketCommand command() const
{
return static_cast( mBuffer[ 2 ] );
};
const container& data() const
{
return mBuffer;
}
const_iterator begin() const
{
return mBuffer.begin();
}
const_iterator end() const
{
return mBuffer.end();
}
protected:
container mBuffer;
};
class PacketOut1 : public PacketReq {
public:
PacketOut1()
{
mBuffer.reserve( PacketHeaderHelper::HEADER_SIZE );
mBuffer.push_back( /* something. */ );
mBuffer.push_back( /* something. */ );
mBuffer.push_back( /* something. */ );
mBuffer.push_back( /* something. */ );
}
};The response can be a good response or a NACK (an error). My Implentation is:
```
class PacketRes {
public:
PacketRes(const std::vector& buffer)
{
if (!checkChecksum( buffer ))
{
throw Error("invalid checksum");
}
PacketHeaderHelper header( buffer );
mCommand = header.command();
if (header.len() > PacketHeaderHelper::HEADER_SIZE)
{
const size_t bodySize = header.len() - PacketHeaderHelper::HEADER_SIZE;
mData.reserve( bodySize );
mData.insert(
mData.end(),
buffer.begin() + PacketHeaderHelper::HEADER_SIZE - 1,
buffer.end() - 1
);
}
}
virtual ~PacketReq() {}
PacketCommand command() const
{
return mCommand;
}
const std::vector& body() const
{
return mData;
}
privat
Solution
Preliminaries
I would decouple the data transmission + message encoding/decoding from the data connection problem. The linked Uncle Bob article also shows how to distinguish connection management differences for master and slave. You probably want this to work with any reasonable networking implementation. Since you already use
My recommendation would be for you to study, and then modify, the simple chat_message/chat_client/chat_server example from the Boost.Asio documentation. That library -or a modified version of it- is likely to become the official networking component in a future version of the Standard Library. So it's a good investment to learn about it.
Message Interface
Compared to the simple
Message Implementations
You could then define a bunch of different concrete messages that inherit the general message interface
You might find it puzzling that I have written both a constructor and a
Note that
Reading messages
If you look at the Boost.Asio example, the
Especially since you tagged this question with
What this says is that every unique header (encoded in a
Given a buffer that has been read from the socket, and converted to a
What this does is to split the message into a header and body. These functions are static functions of the message interface, so that they can be called before we have a concrete message object. Then the header is looked up in the registry.
If it is found, we simply call the corresponding
If we didn't find a registered message type, we
I would decouple the data transmission + message encoding/decoding from the data connection problem. The linked Uncle Bob article also shows how to distinguish connection management differences for master and slave. You probably want this to work with any reasonable networking implementation. Since you already use
boost::noncopyable, I take that as a hint that you are willing to use Boost in general.My recommendation would be for you to study, and then modify, the simple chat_message/chat_client/chat_server example from the Boost.Asio documentation. That library -or a modified version of it- is likely to become the official networking component in a future version of the Standard Library. So it's a good investment to learn about it.
Message Interface
Compared to the simple
chat_message example, your question is a little more involved. I assume that the header information is used to identify which type of message is going to follow. I would recommend to factor all the common message stuff HEADER|BODY|CHECKSUM into an abstract base class IMessageclass IMessage
{
pubic:
virtual ~IMessage() {} // virtual destructor
// virtual functions that can be overriden for concrete message types
static std::string header(std::string const& input)
{
return input.substr(0, HeaderLength);
}
static std::string body(std::string const& input)
{
return input.substr(HeaderLength, HeaderLength + BodyLength);
}
enum {
HeaderLength = /* bla */,
BodyLength = /* bla */,
ChecksumLength = /* bla */
};
};Message Implementations
You could then define a bunch of different concrete messages that inherit the general message interface
IMessage, and add some stuff of their own:class Request: public IMessage
{
Request(std::string const& body) { /* decode the buffer */ }
static std::unique_ptr create(std::string body const&)
{
return std::make_unique(body);
}
// Request specific overrides of virtual functions
// Request unique functions
};
class Acknowledge: public IMessage
{
Acknowledge(std::string const& body) { /* decode the buffer */ }
static std::unique_ptr create(std::string const& body)
{
return std::make_unique(body);
}
// Acknowledge specific overrides of virtual functions
// Acknowledge unique functions
};You might find it puzzling that I have written both a constructor and a
create() function. That is explained in the next section. But for now, note that the declared return type is std::unqiue_ptr but the function body return a unique pointer to the concrete message types. This legal C++ feature is known as covariant return types, and makes it possible to have polymorphic object construction.Note that
std::make_unique is not yet officially availabe but you can get a working version that has been accepted for the upcoming C++14 Standard.Reading messages
If you look at the Boost.Asio example, the
chat_client and chat_server read from a character buffer that is being filled from a socket. So you need to decode the header in order to distinguish which message has to be created. Especially since you tagged this question with
design-patterns, I would recommend to write a small Factory class that keeps a registry of function pointers. At startup, you fill the factory's registry with function pointers to the static member functions create() of all the message types in your application. In this case the registry would be e.g. of typetypedef std::map(std::string const&)> Registry;What this says is that every unique header (encoded in a
std::string) stores a pointer to a function taking a std::string and returning a std::unique_ptr. Those type of function pointers are preciesly the create() functions of your messages.Given a buffer that has been read from the socket, and converted to a
std::string, your factory would do something like:std::unique_ptr Factory::create(std::string const& input) const
{
auto const fun = registry_.find(IMessage::header(input));
// here you can also do your CHECKSUM validation
return fun? (fun)(IMessage::body(input)) : nullptr;
}What this does is to split the message into a header and body. These functions are static functions of the message interface, so that they can be called before we have a concrete message object. Then the header is looked up in the registry.
If it is found, we simply call the corresponding
create() message that was stored, taking the body as argument. This will call the constructor (now you see why we had both a constructor and a creator, because constructors cannot be stored in a factory registry). The constructor will finally create a std::unique_ptr to the concrete message type that was encoded in the header. If we didn't find a registered message type, we
Code Snippets
class IMessage
{
pubic:
virtual ~IMessage() {} // virtual destructor
// virtual functions that can be overriden for concrete message types
static std::string header(std::string const& input)
{
return input.substr(0, HeaderLength);
}
static std::string body(std::string const& input)
{
return input.substr(HeaderLength, HeaderLength + BodyLength);
}
enum {
HeaderLength = /* bla */,
BodyLength = /* bla */,
ChecksumLength = /* bla */
};
};class Request: public IMessage
{
Request(std::string const& body) { /* decode the buffer */ }
static std::unique_ptr<IMessage> create(std::string body const&)
{
return std::make_unique<Request>(body);
}
// Request specific overrides of virtual functions
// Request unique functions
};
class Acknowledge: public IMessage
{
Acknowledge(std::string const& body) { /* decode the buffer */ }
static std::unique_ptr<IMessage> create(std::string const& body)
{
return std::make_unique<Acknowledge>(body);
}
// Acknowledge specific overrides of virtual functions
// Acknowledge unique functions
};typedef std::map<std::string, std::function<std::unique_ptr<IMessage>(std::string const&)> Registry;std::unique_ptr<IMessage> Factory::create(std::string const& input) const
{
auto const fun = registry_.find(IMessage::header(input));
// here you can also do your CHECKSUM validation
return fun? (fun)(IMessage::body(input)) : nullptr;
}Context
StackExchange Code Review Q#27297, answer score: 4
Revisions (0)
No revisions yet.