Skip to content
kkoenig edited this page Oct 28, 2015 · 6 revisions

MsgPack Tutorial

The new Convenience Factory Methods

To prevent memory leaks and be C++11 compatible, all MsgPack::Elements are delivered using std::unique_ptrs. But sometimes the expressions can get really long and redundant and explicit types are not necessary all the time. Therefor I introduce these convenience methods to netLink / MsgPack:

// Implicit types / decided by compiler
MsgPack::Factory() // Nil MsgPack::Primitive
MsgPack::Factory(true / false) // Boolean MsgPack::Primitive
MsgPack::Factory("String") // MsgPack::String
MsgPack::Factory(std::string("String")) // MsgPack::String
MsgPack::Factory(0123456789) // MsgPack::Number

// Explicit types and containers
MsgPack__Factory(Binary(16, seedData)) // MsgPack::Binary
MsgPack__Factory(Extended(0, 16, seedData)) // MsgPack::Extended
MsgPack__Factory(ArrayHeader(0)) // Empty MsgPack::Array
MsgPack__Factory(MapHeader(0)) // Empty MsgPack::Map
MsgPack__Factory(Array(std::move(seedArray))) // MsgPack::Array
MsgPack__Factory(Map(std::move(seedMap))) // MsgPack::Map

and the following serializer example:

MsgPack::Serializer serializer(socket);

std::vector<std::unique_ptr<MsgPack::Element>> arrayWithoutElements, arrayWith3Elements;
std::unique_ptr<MsgPack::Element> primitive(new MsgPack::Primitive(true));
std::unique_ptr<MsgPack::Element> string(new MsgPack::String("Hello World!"));
std::unique_ptr<MsgPack::Element> emptyArray(new MsgPack::Array(std::move(arrayWithoutElements)));

arrayWith3Elements.push_back(std::move(primitive));
arrayWith3Elements.push_back(std::move(emptyArray));
arrayWith3Elements.push_back(std::move(string));

std::unique_ptr<MsgPack::Element> array(new MsgPack::Array(std::move(arrayWith3Elements)));
serializer << array;

could be reduced to ... (see next section)

Serialization

There are two ways you can put elements into the serializer. The first one is hierarchical using MsgPack::Array or MsgPack::Map. You just have to put all the elements into their container first and then you can push the entire container into the serializer.

MsgPack::Serializer serializer(socket);

std::vector<std::unique_ptr<MsgPack::Element>> arrayWithoutElements, arrayWith3Elements;
arrayWith3Elements.push_back(MsgPack::Factory(true));
arrayWith3Elements.push_back(MsgPack__Factory(Array(std::move(arrayWithoutElements))));
arrayWith3Elements.push_back(MsgPack::Factory("Hello World!"));

serializer << MsgPack__Factory(Array(std::move(arrayWith3Elements)));

The second one is to push only hints of how many of the next elements belong into containers. Use MsgPack::ArrayHeader or MsgPack::MapHeader for this approach.

MsgPack::Serializer serializer(socket);

serializer << MsgPack__Factory(ArrayHeader(3)); //Next 3 elements belong in this array
serializer << MsgPack::Factory(true);
serializer << MsgPack__Factory(ArrayHeader(0));
serializer << MsgPack::Factory("Hello World!");

The above two examples generate the same output. In some situations one or the other might be a better match for your needs. You can even mix the two ways of serialization.

Deserialization

The same goes for deserialization. Except that you can't mix them, but have to define which one to use in the deserialize() call.

MsgPack::Deserializer deserializer(socket);

deserializer.deserialize([](std::unique_ptr<MsgPack::Element> parsed) {
    std::cout << "Parsed: " << *parsed << "\n";
    return false;
}, true);

If the second parameter is true, the deserializer will generate a hierarchy of MsgPack::Array and MsgPack::Map else it will just generate a stream of elements sometimes containing a MsgPack::ArrayHeader or MsgPack::MapHeader. If you don't pass a second parameter then it is true by default.

Push vs. Pull

Until here we only have had examples of a push serializer and deserializer, where the program has to push elements into the serializer and the deserializer pushes back deserialized elements. But the pull approach is also supported. This way you have to define a callback and deliver the next element to be serialized. The serializer will then pull the next element when it is needed.

MsgPack::Serializer serializer(socket);

serializer.serialize([]() {
    return MsgPack::Factory("Serialize me !");
});

The same goes for deserialization again.

MsgPack::Deserializer deserializer(socket);

std::unique_ptr<MsgPack::Element> element;
deserializer >> element;
if(element)
    [...]

or you can use a callback but return true to cancel the deserializion of further elements, which is nearly the same as a pull parser:

MsgPack::Deserializer deserializer(socket);

std::unique_ptr<MsgPack::Element> element;
deserializer.deserialize([&element](std::unique_ptr<MsgPack::Element> parsed) {
    element = std::move(parsed);
    return true;
});
if(element)
    [...]

But be careful, all of the MsgPack streams are non blocking which means that they don't wait until a element hast been serialized or deserialized. When you use push serialization elements might remain in the std::unique_ptr, a call to serialize() will try to serialize as much as possible and when you use pull serialization then the callback won't be called until the last element is serialized. It is simular at the deserialization. A push deserializer won't call the callback until a element is completly deserialized and a pull deserializer might return a empty std::unique_ptr if there is nothing to pull.

Because of this you might have to call serialize() or deserialize() multiple times to do the rest of the last element which might be only partially done. But both methods will try to process as many bytes as possible. Therefore you should call serialize() again, when there is new space in your stream buffer to be written in and deserialize() when there is new data available in your stream buffer to be read. And you will have to call them with the same MsgPack::Serializer or MsgPack::Deserializer again, else the intermediate state of the last time will be lost.

Data flow control

Another nice feature is, that you can control the flow of data exactly and decide how many bytes will be processed. Just add a additional parameter to the serialize() or deserialize() call.

MsgPack::Serializer serializer(socket);

auto primitive = MsgPack::Factory(true);
std::cout << "Processed " << serializer.serialize(primitive, 16) << " bytes\n";

to seralize only 16 bytes or

MsgPack::Deserializer deserializer(socket);

std::cout << "Processed " << deserializer.deserialize([](std::unique_ptr<MsgPack::Element> parsedElement) {
    std::cout << "Parsed: " << *parsedElement << "\n";
    return false;
}, true, 16) << " bytes\n";

to deseralize only 16 bytes. Both methods return how many bytes they actually processed.

Further reading

More example code explaining how to transfer MsgPack over network using TCP or UDP