cc_tutorial/tutorials/tutorial15 at master · commschamp/cc_tutorial

Tutorial 15

Avoiding unnecessary decoding of messages.

In all the previous tutorials both client and server decoded all the input messages even if such messages were discarded as irrelevant. The decoding process involves reading of the message payload. Now imagine the situation where the application being developed is some kind of filter / proxy which is required to monitor / change / drop only limited set of messages and let all the rest (majority) of the messages through. It is not uncommon for such application to change the framing as well when the message is forwarded on different I/O link. In such case the decoding of all the irrelevant messages seems like a waste of CPU cycles and maybe even memory.

To help with such cases the COMMS Library introduced comms::GenericMessage. It extends comms::MessageBase like any regular message definition and has a single member field (payload) which is comms::field::ArrayList of raw data std::uint8_t (equivalent to basic <data> field without any prefixes). However, it doesn't specify any numeric message ID, i.e. its ID information is available at run-time, not compile-time like any other.

In order to show how to use such message the server of this tutorial is implemented to recognize and drop Msg1 while echo back all other messages.

The src/ServerSession.h has the following definition(s):

// Common interface class for all the messages
using Message =
    tutorial15::Message<
        ...
    >;

// Generic message
using GenericMessage =
    comms::GenericMessage<
        Message,
        comms::option::app::OrigDataView // Passed to raw data storage field
    >;

The second template parameter of the comms::GenericMessage is options passed to its comms::field::ArrayList member field. Passing of the comms::option::app::OrigDataView option allows avoiding data copying from the input buffer to the the inner storage of the comms::field::ArrayList. Just to remind the reader that the storage type will be comms::util::ArrayView or std::span depending on the C++ standard been used during the compilation. Note that the comms::option::app::OrigDataView option cannot be used if the input buffer is not sequential, i.e. some kind of circular buffer.

The input messages to recognize tuple contains only single Msg1:

// Definition of relevant messages
using Msg1 = tutorial15::message::Msg1<Message, ServerProtocolOptions>;

// Relevant input messages
using InputMessages =
    std::tuple<
        Msg1
    >;

If the configuration is left like this, then all other messages won't even be recognized and will be dropped by the framing management. In order to force creation of GenericMessage for all other unknown messages, the Id framing layer needs to receive comms::option::app::SupportGenericMessage option.


SIDE NOTE: As was already explained in one of the previous tutorials, it's actually comms::MsgFactory class that is responsible to create message objects. It is used by the comms::frame::MsgIdLayer. All the options passed to the later are forwarded to the former. As the result if the tuple of input messages doesn't contain any class with specified message ID, then the specified GenericMessage is created and returned instead.


Hence, the protocol options for the server application are defined to be:

using ServerProtocolOptionsBase =
    tutorial15::options::DataViewDefaultOptionsT<
        tutorial15::options::ServerDefaultOptions
    >;

// Protocol options for server
class ServerProtocolOptions : public ServerProtocolOptionsBase
{
    using Base = ServerProtocolOptionsBase;
public:
    struct frame : public Base::frame
    {
        struct FrameLayers : public Base::frame::FrameLayers
        {
            // Extra options for tutorial15::frame::FrameLayers::Id layer.
            using Id =
                std::tuple<
                    comms::option::app::SupportGenericMessage<GenericMessage>,
                    Base::frame::FrameLayers::Id
                >;
        }; // struct FrameLayers
    }; // struct Frame
};

The handling of the incoming messages the ServerSession implements in the following way:

void ServerSession::handle(Msg1& msg)
{
    std::cout << "Received message \"" << msg.doName() << "\" with ID=" << (unsigned)msg.doGetId() <<
        ", dropping it..." << std::endl;
}

void ServerSession::handle(GenericMessage& msg)
{
    std::cout << "Received message \"" << msg.doName() << "\" with ID=" << (unsigned)msg.doGetId() <<
        ", echoing it back..." << std::endl;

    sendMessage(msg);
}

void ServerSession::handle(Message& msg)
{
    // The statement below uses polymorphic message name and ID retrievals.
    std::cout << "Received unexpected message \"" << msg.name() << "\" with ID=" << (unsigned)msg.getId() <<
        ", ignoring..." << std::endl;

    assert(!"Should not happen");
}

When client sends Msg1, Msg2 and Msg3 to the server, the latter produces the following output:

Processing input: 0 7 1 0 0 aa bb 0 9 2 5 68 65 6c 6c 6f
Received message "Message 1" with ID=1, dropping it...
Received message "Generic Message" with ID=2, echoing it back...
Sending message "Generic Message" with ID=2: 0 9 2 5 68 65 6c 6c 6f
Processing input: 0 9 3 5 ab cd ef 1 2
Received message "Generic Message" with ID=3, echoing it back...
Sending message "Generic Message" with ID=3: 0 9 3 5 ab cd ef 1 2

Please take a look how the comms::GenericMessage is actually implemented. It contains definition of the doName() which results in printing "Generic Message" as recognized message name.

Summary

  • To avoid decoding of some messages the COMMS Library provides comms::GenericMessage.
  • To support creation of the GenericMessage object instead of unrecognized messages the comms::option::app::SupportGenericMessage option needs to be passed the Id framing layer (comms::frame::MsgIdLayer)
  • It is recommended to also pass comms::option::app::OrigDataView option to the inner field definition of the GenericMessage to avoid unnecessary copy of the data from the input buffer, unless the input buffer is not sequential and/or the message object outlives the input buffer.

Read Previous Tutorial <-----------------------> Read Next Tutorial