Messages and Packets¶
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 name field is a string (
const char *), which can be freely used by the simulation programmer. The message’s name is displayed in many places in the graphical runtime interface, so it is generally useful to choose a descriptive name. The message’s name is inherited from cObject (see section The cObject Base Class).Message kind is an integer field. Some negative values are reserved by the simulation library, but zero and positive values can be freely used in the model for any purpose. The message kind is typically used to carry a value that conveys the role, type, category, or identity of the message.
The scheduling priority field is used by the simulation kernel to determine the delivery order of messages that have the same arrival time values. This field is rarely used in practice.
The send time, arrival time, source module, source gate, destination module, destination gate fields store information about the message’s last sending or scheduling, and should not be modified from the model. These fields are primarily used internally by the simulation kernel while the message is in the future events set (FES), but the information is still in the message object when the message is delivered to a module.
Time stamp (not to be confused with arrival time) is a utility field that the programmer can freely use for any purpose. The time stamp is not examined or changed by the simulation kernel at all.
The parameter list, control info, and context pointer fields make some simulation tasks easier to program, and they will be discussed later.
The cPacket class extends cMessage with fields that are useful for representing network packets:
The packet length field represents the length of the packet in bits. It is used by the simulation kernel to compute the transmission duration when a packet travels through a connection that has an assigned data rate, and also for error modeling on channels with a nonzero bit error rate.
The encapsulated packet field helps in modeling protocol layers by supporting the concept of encapsulation and decapsulation.
The bit error flag field carries the result of error modeling after the packet is sent through a channel that has a nonzero packet error rate (PER) or bit error rate (BER). It is up to the receiver to examine this flag after receiving the packet and to act upon it.
The duration field carries the transmission duration after the packet was sent through a channel with a data rate.
The is-reception-start flag tells whether this packet represents the start or the end of the reception after the packet has travelled through a channel with a data rate. This flag is controlled by the deliver-on-reception-start flag of the receiving gate.
The cMessage Class¶
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.
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. [1]
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 Message Definitions), this is taken care of automatically.
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;
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.
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;
Display String¶
Display strings affect the message’s visualization in graphical user interfaces like 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.
Self-Messages¶
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 the expiry of a timeout. A message is termed a self-message when it is used in such a scenario – otherwise, self-messages are normal messages of the cMessage class 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 using 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).
Context Pointer¶
The cMessage class 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 is not used or memory-managed by the simulation kernel. It is typically used in modules that manage multiple self-messages or timers to distinguish which specific timer has triggered upon message arrival. By pointing to a module’s internal data structure, this pointer can convey essential information about the event’s context.
The cPacket Class¶
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 that 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;
Identifying the Protocol¶
In the OMNeT++ protocol models, the protocol type is usually represented in the
message subclass. For example, instances of the IPv6Datagram
class 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;
...
}
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;
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();
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: 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.
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 Deriving New Classes covers requirements and conventions associated with deriving new classes in more detail.
Attaching Objects To a Message¶
When parameters or objects need to be added to a message, the preferred way to do so is via message definitions, as described in chapter Message Definitions.
Attaching Objects¶
The cMessage class has an internal cArray object that can carry objects. Only objects derived from cObject can be attached. The addObject(), getObject(), hasObject(), and 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 ensure that the names of the attached objects don’t conflict with each other. Note that message parameters (cMsgPar, see the next section) are also attached in 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 specifically for this purpose. cMsgPar will be covered in the next section.
Attaching Parameters¶
The preferred way to extend messages with new data fields is to use message definitions (see chapter Message Definitions).
The old and deprecated way of adding new fields to messages is by attaching cMsgPar objects. There are several downsides to this approach, with the worst being large memory and execution time overhead. cMsgPars are heavyweight 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, and 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 first check 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");