[Prev] [Next] [TOC] [Chapters]

5 Messages and Packets

5.1 Overview

Messages are a central concept in OMNeT++. In the model, message objects represent events, packets, commands, jobs, customers or other kinds of entities, depending on the model domain.

Messages are represented with the cMessage class and its subclass cPacket. cPacket is used for network packets (frames, datagrams, transport packets, etc.) in a communication network, and cMessage is used for everything else. Users are free to subclass both cMessage and cPacket to create new types and to add data.

cMessage has the following fields; some are used by the simulation kernel, and others are provided for the convenience of the simulation programmer:

The cPacket class extends cMessage with fields that are useful for representing network packets:

5.2 The cMessage Class

5.2.1 Basic Usage

The cMessage constructor accepts an object name and a message kind, both optional:

cMessage(const char *name=nullptr, short kind=0);

Descriptive message names can be very useful when tracing, debugging or demonstrating the simulation, so it is recommended to use them. Message kind is usually initialized with a symbolic constant (e.g. an enum value) which signals what the message object represents. Only positive values and zero can be used -- negative values are reserved for use by the simulation kernel.

The following lines show some examples of message creation:

cMessage *msg1 = new cMessage();
cMessage *msg2 = new cMessage("timeout");
cMessage *msg3 = new cMessage("timeout", KIND_TIMEOUT);

Once a message has been created, its basic data members can be set with the following methods:

void setName(const char *name);
void setKind(short k);
void setTimestamp();
void setTimestamp(simtime_t t);
void setSchedulingPriority(short p);

The argument-less setTimeStamp() method is equivalent to setTimeStamp(simTime()).

The corresponding getter methods are:

const char *getName() const;
short getKind() const;
simtime_t getTimestamp() const;
short getSchedulingPriority() const;

The getName()/setName() methods are inherited from a generic base class in the simulation library, cNamedObject.

Two more interesting methods:

bool isPacket() const;
simtime_t getCreationTime() const;

The isPacket() method returns true if the particular message object is a subclass of cPacket, and false otherwise. As isPacket() is implemented as a virtual function that just contains a return false or a return true statement, it might be faster than calling dynamic_cast<cPacket*>.

The getCreationTime() method returns the creation time of the message. It is worthwhile to mention that with cloned messages (see dup() later), the creation time of the original message is returned and not the time of the cloning operation. This is particularly useful when modeling communication protocols, because many protocols clone the transmitted packages to be able to do retransmissions and/or segmentation/reassembly.

5.2.2 Duplicating Messages

It is often necessary to duplicate a message or a packet, for example, to send one and keep a copy. Duplication can be done in the same way as for any other OMNeT++ object:

cMessage *copy = msg->dup();

The resulting message (or packet) will be an exact copy of the original including message parameters and encapsulated messages, except for the message ID field. The creation time field is also copied, so for cloned messages getCreationTime() will return the creation time of the original, not the time of the cloning operation.

When subclassing cMessage or cPacket, one needs to reimplement dup(). The recommended implementation is to delegate to the copy constructor of the new class:

class FooMessage : public cMessage {
  public:
    FooMessage(const FooMessage& other) {...}
    virtual FooMessage *dup() const {return new FooMessage(*this);}
    ...
};

For generated classes (chapter [6]), this is taken care of automatically.

5.2.3 Message IDs

Every message object has a unique numeric message ID. It is normally used for identifying the message in a recorded event log file, but may occasionally be useful for other purposes as well. When a message is cloned (msg->dup()), the clone will have a different ID.

There is also another ID called tree ID. The tree ID is initialized to the message ID. However, when a message is cloned, the clone will retain the tree ID of the original. Thus, messages that have been created by cloning the same message or its clones will have the same tree ID. Message IDs are of the type long, which is is usually enough so that IDs remain unique during the simulation run (i.e. the counter does not wrap).

The methods for obtaining message IDs:

long getId() const;
long getTreeId() const;

5.2.4 Control Info

One of the main application areas of OMNeT++ is the simulation of telecommunication networks. Here, protocol layers are usually implemented as modules which exchange packets. Packets themselves are represented by messages subclassed from cPacket.

However, communication between protocol layers requires sending additional information to be attached to packets. For example, a TCP implementation sending down a TCP packet to IP will want to specify the destination IP address and possibly other parameters. When IP passes up a packet to TCP after decapsulation from the IP header, it will want to let TCP know at least the source IP address.

This additional information is represented by control info objects in OMNeT++. Control info objects have to be subclassed from cObject (a small footprint base class with no data members), and can be attached to any message. cMessage has the following methods for this purpose:

void setControlInfo(cObject *controlInfo);
cObject *getControlInfo() const;
cObject *removeControlInfo();

When a "command" is associated with the message sending (such as TCP OPEN, SEND, CLOSE, etc), the message kind field (getKind(), setKind() methods of cMessage) should carry the command code. When the command doesn't involve a data packet (e.g. TCP CLOSE command), a dummy packet (empty cMessage) can be sent.

An object set as control info via setControlInfo() will be owned by the message object. When the message is deallocated, the control info object is deleted as well.

5.2.5 Information About the Last Arrival

The following methods return the sending and arrival times that correspond to the last sending of the message.

simtime_t getSendingTime() const;
simtime_t getArrivalTime() const;

The following methods can be used to determine where the message came from and which gate it arrived on (or will arrive if it is currently scheduled or under way.) There are two sets of methods, one returning module/gate Ids, and the other returning pointers.

int getSenderModuleId() const;
int getSenderGateId() const;
int getArrivalModuleId() const;
int getArrivalGateId() const;
cModule *getSenderModule() const;
cGate *getSenderGate() const;
cModule *getArrivalModule() const;
cGate *getArrivalGate() const;

There are further convenience functions to tell whether the message arrived on a specific gate given with id or with name and index.

bool arrivedOn(int gateId) const;
bool arrivedOn(const char *gatename) const;
bool arrivedOn(const char *gatename, int gateindex) const;

5.2.6 Display String

Display strings affect the message's visualization in graphical user interfaces like Tkenv and Qtenv. Message objects do not store a display string by default, but contain a getDisplayString() method that can be overridden in subclasses to return the desired string. The method:

const char *getDisplayString() const;

Since OMNeT++ version 5.1, cPacket's default getDisplayString() implementation is such so that a packet “inherits” the display string of its encapsulated packet, provided it has one. Thus, in the model of a network stack, the appearance of e.g. an application layer packet will be preserved even after multiple levels of encapsulation.

See section for more information on message display string syntax and possibilities.

5.3 Self-Messages

5.3.1 Using a Message as Self-Message

Messages are often used to represent events internal to a module, such as a periodically firing timer to represent expiry of a timeout. A message is termed self-message when it is used in such a scenario -- otherwise self-messages are normal messages of class cMessage or a class derived from it.

When a message is delivered to a module by the simulation kernel, the isSelfMessage() method can be used to determine if it is a self-message; that is, whether it was scheduled with scheduleAt(), or sent with one of the send...() methods. The isScheduled() method returns true if the message is currently scheduled. A scheduled message can also be cancelled (cancelEvent()).

bool isSelfMessage() const;
bool isScheduled() const;

The methods getSendingTime() and getArrivalTime() are also useful with self-messages: they return the time the message was scheduled and arrived (or will arrive; while the message is scheduled, arrival time is the time it will be delivered to the module).

5.3.2 Context Pointer

cMessage contains a context pointer of type void*, which can be accessed by the following functions:

void setContextPointer(void *p);
void *getContextPointer() const;

The context pointer can be used for any purpose by the simulation programmer. It is not used by the simulation kernel, and it is treated as a mere pointer (no memory management is done on it).

Intended purpose: a module which schedules several self-messages (timers) will need to identify a self-message when it arrives back to the module, ie. the module will have to determine which timer went off and what to do then. The context pointer can be made to point at a data structure kept by the module which can carry enough “context” information about the event.

5.4 The cPacket Class

5.4.1 Basic Usage

The cPacket constructor is similar to the cMessage constructor, but it accepts an additional bit length argument:

cPacket(const char *name=nullptr, short kind=0, int64 bitLength=0);

The most important field cPacket has over cMessage is the message length. This field is kept in bits, but it can also be set/get in bytes. If the bit length is not a multiple of eight, the getByteLength() method will round it up.

void setBitLength(int64_t l);
void setByteLength(int64_t l);
void addBitLength(int64_t delta);
void addByteLength(int64_t delta);
int64_t getBitLength() const;
int64_t getByteLength() const;

Another extra field is the bit error flag. It can be accessed with the following methods:

void setBitError(bool e);
bool hasBitError() const;

5.4.2 Identifying the Protocol

In OMNeT++ protocol models, the protocol type is usually represented in the message subclass. For example, instances of class IPv6Datagram represent IPv6 datagrams and EthernetFrame represents Ethernet frames. The C++ dynamic_cast operator can be used to determine if a message object is of a specific protocol.

An example:

cMessage *msg = receive();
if (dynamic_cast<IPv6Datagram *>(msg) != nullptr)
{
    IPv6Datagram *datagram = (IPv6Datagram *)msg;
    ...
}

5.4.3 Information About the Last Transmission

When a packet has been received, some information can be obtained about the transmission, namely the transmission duration and the is-reception-start flag. They are returned by the following methods:

simtime_t getDuration() const;
bool isReceptionStart() const;

5.4.4 Encapsulating Packets

When modeling layered protocols of computer networks, it is commonly needed to encapsulate a packet into another. The following cPacket methods are associated with encapsulation:

void encapsulate(cPacket *packet);
cPacket *decapsulate();
cPacket *getEncapsulatedPacket() const;

The encapsulate() function encapsulates a packet into another one. The length of the packet will grow by the length of the encapsulated packet. An exception: when the encapsulating (outer) packet has zero length, OMNeT++ assumes it is not a real packet but an out-of-band signal, so its length is left at zero.

A packet can only hold one encapsulated packet at a time; the second encapsulate() call will result in an error. It is also an error if the packet to be encapsulated is not owned by the module.

Decapsulation, that is, removing the encapsulated packet, is done by the decapsulate() method. decapsulate() will decrease the length of the packet accordingly, except if it was zero. If the length would become negative, an error occurs.

The getEncapsulatedPacket() function returns a pointer to the encapsulated packet, or nullptr if no packet is encapsulated.

Example usage:

cPacket *data = new cPacket("data");
data->setByteLength(1024);

UDPPacket *udp = new UDPPacket("udp"); // subclassed from cPacket
udp->setByteLength(8);

udp->encapsulate(data);
EV << udp->getByteLength() << endl; // --> 8+1024 = 1032

And the corresponding decapsulation code:

cPacket *payload = udp->decapsulate();

5.4.5 Reference Counting

Since the 3.2 release, OMNeT++ implements reference counting of encapsulated packets, meaning that when a packet containing an encapsulated packet is cloned (dup()), the encapsulated packet will not be duplicated, only a reference count is incremented. Duplication of the encapsulated packet is deferred until decapsulate() actually gets called. If the outer packet is deleted without its decapsulate() method ever being called, then the reference count of the encapsulated packet is simply decremented. The encapsulated packet is deleted when its reference count reaches zero.

Reference counting can significantly improve performance, especially in LAN and wireless scenarios. For example, in the simulation of a broadcast LAN or WLAN, the IP, TCP and higher layer packets won't be duplicated (and then discarded without being used) if the MAC address doesn't match in the first place.

The reference counting mechanism works transparently. However, there is one implication: one must not change anything in a packet that is encapsulated into another! That is, getEncapsulatedPacket() should be viewed as if it returned a pointer to a read-only object (it returns a const pointer indeed), for quite obvious reasons: the encapsulated packet may be shared between several packets, and any change would affect those other packets as well.

5.4.6 Encapsulating Several Packets

The cPacket class does not directly support encapsulating more than one packet, but one can subclass cPacket or cMessage to add the necessary functionality.

Encapsulated packets can be stored in a fixed-size or a dynamically allocated array, or in a standard container like std::vector. In addition to storage, object ownership needs to be taken care of as well. The message class has to take ownership of the inserted messages, and release them when they are removed from the message. These tasks are done via the take() and drop() methods.

Here is an example that assumes that the class has an std::list member called messages for storing message pointers:

void MultiMessage::insertMessage(cMessage *msg)
{
    take(msg);  // take ownership
    messages.push_back(msg);  // store pointer
}

void MultiMessage::removeMessage(cMessage *msg)
{
    messages.remove(msg);  // remove pointer
    drop(msg);  // release ownership
}

One also needs to provide an operator=() method to make sure that message objects are copied and duplicated properly. Section [7.12] covers requirements and conventions associated with deriving new classes in more detail.

5.5 Attaching Objects To a Message

When parameters or objects need to be added to a message, the preferred way to do that is via message definitions, described in chapter [6].

5.5.1 Attaching Objects

The cMessage class has an internal cArray object which can carry objects. Only objects that are derived from cObject can be attached. The addObject(), getObject(), hasObject(), removeObject() methods use the object's name (as returned by the getName() method) as the key to the array.

An example where the sender attaches an object, and the receiver checks for the object's existence and obtains a pointer to it:

// sender:
cHistogram *histogram = new cHistogram("histogram");
msg->addObject(histogram);

// receiver:
if (msg->hasObject("histogram")) {
   cObject *obj = msg->getObject("histogram");
   cHistogram *histogram = check_and_cast<cHistogram *>(obj);
   ...
}

One needs to take care that names of the attached objects don't conflict with each other. Note that message parameters (cMsgPar, see next section) are also attached the same way, so their names also count.

When no objects are attached to a message (and getParList() is not invoked), the internal cArray object is not created. This saves both storage and execution time.

Non-cObject data can be attached to messages by wrapping them into cObject, for example into cMsgPar which has been designed expressly for this purpose. cMsgPar will be covered in the next section.

5.5.2 Attaching Parameters

The preferred way of extending messages with new data fields is to use message definitions (see chapter [6]).

The old, deprecated way of adding new fields to messages is via attaching cMsgPar objects. There are several downsides of this approach, the worst being large memory and execution time overhead. cMsgPar's are heavy-weight and fairly complex objects themselves. It has been reported that using cMsgPar message parameters might account for a large part of execution time, sometimes as much as 80%. Using cMsgPar is also error-prone because cMsgPar objects have to be added dynamically and individually to each message object. In contrast, subclassing benefits from static type checking: if one mistypes the name of a field in the C++ code, the compiler can detect the mistake.

If one still needs cMsgPars for some reason, here is a short summary. At the sender side, one can add a new named parameter to the message with the addPar() member function, then set its value with one of the methods setBoolValue(), setLongValue(), setStringValue(), setDoubleValue(), setPointerValue(), setObjectValue(), and setXMLValue(). There are also overloaded assignment operators for the corresponding C/C++ types.

At the receiver side, one can look up the parameter object on the message by name and obtain a reference to it with the par() member function. hasPar() can be used to check first whether the message object has a parameter object with the given name. Then the value can be read with the methods boolValue(), longValue(), stringValue(), doubleValue(), pointerValue(), objectValue(), xmlValue(), or by using the provided overloaded type cast operators.

Example usage:

msg->addPar("destAddr");
msg->par("destAddr").setLongValue(168);
...
long destAddr = msg->par("destAddr").longValue();

Or, using overloaded operators:

msg->addPar("destAddr");
msg->par("destAddr") = 168;
...
long destAddr = msg->par("destAddr");



[Prev] [Next] [TOC] [Chapters]