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 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 IMessage
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 */
};
};
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<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
};
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<IMessage> 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 type
typedef std::map<std::string, std::function<std::unique_ptr<IMessage>(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<IMessage>. 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<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;
}
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 return a nullptr. The caller of the factory creator would then have to check this before being able to safely use the returned pointer.
Final thoughts
The above explained how to decode messages from a stream of tokens that you get from a socket. Writing is of course much easier because then you already know the message type. I think that the data transmission and connection management part, and the semantic part of how to respond to each message type is beyond the scope of this Q&A. The Boost.Asio example does not need too much modification to allow reading your message structures from a socket.
For the above message parsing machinery, I have implemented a heavily templated version of similar protocol called DamExchange (used for letting checkers/draughts engines over a network). You can follow the header trail on a simple use case in my BitBucket repo.