Working with Packets¶
Overview¶
The INET Packet API is designed to ease the implementation of communication protocols and applications by providing many useful C++ components. In the following sections, we introduce the Packet API in detail, and we shed light on many common API usages through examples.
Note
Code fragments in this chapter have been somewhat simplified for brevity. For
example, some const
modifiers and const
casts have been omitted,
setting fields have been omitted, and some algorithms have been simplified to
ease understanding.
The representation of packets is essential for
communication network simulation. Applications and communication
protocols construct, deconstruct, encapsulate, fragment, aggregate, and
manipulate packets in many ways. In order to ease the implementation of
these behavioral patterns, INET provides a feature-rich general data structure,
the Packet
class.
The Packet
data structure is capable of representing application
packets, TCP segments, IP datagrams,
Ethernet frames, IEEE 802.11 frames, and all
kinds of digital data. It is designed to provide efficient storage,
duplication, sharing, encapsulation, aggregation, fragmentation,
serialization, and data representation selection. Additional functionality,
such as support for enqueueing data for transmisson and buffering received
data for reassembly and/or for reordering, is provided as separate C++
data structures on top of Packet
.
Representing Data¶
The Packet
data structure builds on top of another set of data
structures called chunks. Chunks provide several alternatives to represent
a piece of data.
INET provides the following built-in chunk C++ classes:
Chunk
, the base class for all chunk classesrepeated byte or bit chunk (
ByteCountChunk
,BitCountChunk
)raw bytes or bits chunk (
BytesChunk
,BitsChunk
)ordered sequence of chunks (
SequenceChunk
)slice of another chunk designated by offset and length (
SliceChunk
)many protocol specific field based chunks (e.g.
Ipv4Header
subclass ofFieldsChunk
)
In addition, communication protocols and applications often define
their own chunk types. User-defined chunks are normally defined in
msg
files as a subclass of FieldsChunk
, which the
OMNeT++ MSG compiler turns into C++ code.
It is also possible to write a user defined chunk from scratch.
Chunks usually represent application data and protocol headers. The following examples demonstrate the construction of various chunks.
auto bitCountData = makeShared<BitCountChunk>(b(3), 0); // 3 zero bits
auto byteCountData = makeShared<ByteCountChunk>(B(10), '?'); // 10 '?' bytes
auto rawBitsData = makeShared<BitsChunk>();
rawBitsData->setBits({1, 0, 1}); // 3 raw bits
auto rawBytesData = makeShared<BytesChunk>(); // 10 raw bytes
rawBytesData->setBytes({243, 74, 19, 84, 81, 134, 216, 61, 4, 8});
auto fieldBasedHeader = makeShared<UdpHeader>(); // create new UDP header
fieldBasedHeader->setSrcPort(1000); // set some fields
In general, chunks must be constructed with a call to the makeShared
function instead of the standard C++ new
operator, because chunks
are shared among packets using C++ shared pointers.
Packets most often contain several chunks, inserted by different protocols, as they are passed through the protocol layers. The most common way to represent packet contents is to form a compound chunk by concatenation.
auto sequence = makeShared<SequenceChunk>(); // create empty sequence
sequence->insertAtBack(makeShared<UdpHeader>()); // append UDP header
sequence->insertAtBack(makeShared<ByteCountChunk>(B(10), 0)); // 10 bytes
Protocols often need to slice data, for example to provide fragmentation, which is also directly supported by the chunk API.
auto udpHeader = makeShared<UdpHeader>(); // create 8 bytes UDP header
auto firstHalf = udpHeader->peek(B(0), B(4)); // first 4 bytes of header
auto secondHalf = udpHeader->peek(B(4), B(4)); // second 4 bytes of header
In order to avoid cluttered data representation due to slicing, the chunk API provides automatic merging for consecutive chunk slices.
auto sequence = makeShared<SequenceChunk>(); // create empty sequence
sequence->insertAtBack(firstHalf); // append first half
sequence->insertAtBack(secondHalf); // append second half
auto merged = sequence->peek(B(0), B(8)); // automatically merge slices
Alternative representations can be easily converted into one another using automatic serialization as a common ground.
auto raw = merged->peek<BytesChunk>(B(0), B(8)); // auto serialization
auto original = raw->peek<UdpHeader>(B(0), B(8)); // auto deserialization
The following MSG fragment is a more complete example which shows how a UDP header could be defined:
enum CrcMode
{
CRC_DISABLED = 0; // CRC is not set, serializable
CRC_DECLARED = 1; // CRC is correct without the value, not serializable
CRC_COMPUTED = 2; // CRC is potentially incorrect, serializable
}
class UdpHeader extends FieldsChunk
{
chunkLength = B(8); // UDP header length is always 8 bytes
int sourcePort = -1; // source port field is undefined by default
int destinationPort = -1; // destination port field is undefined by default
B lengthField = B(-1); // length field is undefined by default
uint16_t crc = 0; // checksum field is 0 by default
CrcMode crcMode = CRC_DISABLED; // checksum mode is disabled by default
}
It’s important to distinguish the two length related fields in the
UdpHeader chunk. One is the length of the chunk itself
(chunkLength
), the other is the value in the length field of the
header (lengthField
).
Representing Packets¶
The Packet
data structure uses a single chunk data structure to
represent its contents. The contents may be as simple as raw bytes
(BytesChunk
), but most likely it will be the concatenation
(SequenceChunk
) of various protocol specific headers (e.g.,
FieldsChunk
subclasses) and application data (e.g.,
ByteCountChunk
).
Packets can be created by both applications and communication protocols. As packets are passed down through the protocol layers at the sender node, new protocol specific headers and trailers are inserted during processing.
auto emptyPacket = new Packet("ACK"); // create empty packet
auto data = makeShared<ByteCountChunk>(B(1000));
auto dataPacket = new Packet("DATA", data); // create new packet with data
auto moreData = makeShared<ByteCountChunk>(B(1000));
dataPacket->insertAtBack(moreData); // insert more data at the end
auto udpHeader = makeShared<UdpHeader>(); // create new UDP header
dataPacket->insertAtFront(udpHeader); // insert header into packet
In order to facilitate packet processing by communication protocols at
the receiver node, Packet
maintains two offsets into the packet data
that divide the data into three regions: front popped
part, data part, and back popped part. During packet processing, as the
packet is passed through the protocol layers, headers and trailers are
popped from the beginning and from the end of the packet, moving the
corresponding offsets. This effectively reduces the remaining
unprocessed part called the data part, but it doesn’t affect
the data stored in the packet.
packet->popAtFront<MacHeader>(); // pop specific header from packet
packet->popAtBack<MacTrailer>(); // pop specific trailer from packet
auto data = packet->peekData(); // peek remaining data in packet
Representing Signals¶
Protocols and applications use the Packet
data structure to
represent digital data during the processing within the network node. In
contrast, the wireless transmission medium uses a different data
structure called Signal
to represent the physical phenomena used
to transmit packets.
auto signal = new Signal(transmission);
signal->setDuration(duration);
signal->encapsulate(packet);
Signals always encapsulate a packet and also contain a description of the analog domain representation. The most important physical properties of a signal are the signal duration and the signal power.
Representing Transmission Errors¶
An essential part of communication network simulation is the understanding of protocol behavior in the presence of errors. The Packet API provides several alternatives for representing errors. The alternatives range from simple, but computationally cheap, to accurate, but computationally expensive solutions.
mark erroneous packets (simple)
mark erroneous chunks (good compromise)
change bits in raw chunks (accurate)
The first example shows how to represent transmission erros on the packet level. A packet is marked as erroneous based on its length and the associated bit error rate. This representation doesn’t give too much chance for a protocol to do anything else than discard an erroneous packet.
Packet *ErrorModel::corruptPacket(Packet *packet, double ber)
{
auto length = packet->getTotalLength();
auto hasErrors = hasProbabilisticError(length, ber); // decide randomly
auto corruptedPacket = packet->dup(); // cheap operation
corruptedPacket->setBitError(hasErrors); // set bit error flag
return corruptedPacket;
}
The second example shows how to represent transmission errors on the chunk level. Similarly to the previous example, a chunk is also marked as erroneous based on its length and the associated bit error rate. This representation allows a protocol to discard only certain parts of the packet. For example, an aggregated packet may be partially discarded and processed.
Packet *ErrorModel::corruptChunks(Packet *packet, double ber)
{
b offset = b(0); // start from the beginning
auto corruptedPacket = new Packet("Corrupt"); // create new packet
while (auto chunk = packet->peekAt(offset)->dupShared()) { // for each chunk
auto length = chunk->getChunkLength();
auto hasErrors = hasProbabilisticError(length, ber); // decide randomly
if (hasErrors) // if erroneous
chunk->markIncorrect(); // set incorrect bit
corruptedPacket->insertAtBack(chunk); // append chunk to corrupt packet
offset += chunk->getChunkLength(); // increment offset with chunk length
}
return corruptedPacket;
}
The last example shows how to actually represent transmission errors on the byte level. In contrast with the previous examples, this time the actual data of the packet is modified. This allows a protocol to discard or correct any part based on checksums.
Packet *ErrorModel::corruptBytes(Packet *packet, double ber)
{
vector<uint8_t> corruptedBytes; // bytes of corrupted packet
auto data = packet->peekAllAsBytes(); // data of original packet
for (auto byte : data->getBytes()) { // for each original byte do
if (hasProbabilisticError(B(1), ber)) // if erroneous
byte = ~byte; // invert byte (simplified corruption)
corruptedBytes.push_back(byte); // store byte in corrupted data
}
auto corruptedData = makeShared<BytesChunk>(); // create new data
corruptedData->setBytes(corruptedBytes); // store corrupted bits
return new Packet("Corrupt", corruptedData); // create new packet
}
The physical layer models support the above mentioned different error representations via configurable parameters. Higher layer protocols detect errors by chechking the error bit on packets and chunks, and by standard CRC mechanisms.
Packet Tagging¶
Within network nodes, supplementary data often needs to be transmitted alongside a packet. For instance, when an application-layer module intends to transfer data using TCP, it must specify a connection identifier for TCP. Similarly, when TCP transmits a segment via IP, IP requires a destination address, and when IP sends a datagram to an Ethernet interface for transmission, a destination MAC address must be specified. These additional details are attached to a packet as tags.
The following code fragment demonstrates how packet tags could be set in the IPv4 protocol module:
void Ipv4::sendDown(Packet *packet, Ipv4Address nextHopAddr, int interfaceId)
{
auto macAddressReq = packet->addTag<MacAddressReq>(); // add new tag for MAC
macAddressReq->setSrcAddress(selfAddress); // source is our MAC address
auto nextHopMacAddress = resolveMacAddress(nextHopAddr); // simplified ARP
macAddressReq->setDestAddress(nextHopMacAddress); // destination is next hop
auto interfaceReq = packet->addTag<InterfaceReq>(); // add tag for dispatch
interfaceReq->setInterfaceId(interfaceId); // set designated interface
auto packetProtocolTag = packet->addTagIfAbsent<PacketProtocolTag>();
packetProtocolTag->setProtocol(&Protocol::ipv4); // set protocol of packet
send(packet, "out"); // send to MAC protocol module of designated interface
}
Packet tags are not transmitted from one network node to another. All physical layer protocols delete all packet tags from a packet before sending it to the connected peer or to the transmission medium.
For more details on what kind of tags are there see the
Region Tagging¶
To gather certain statistics, it might be necessary to add metadata to various regions of packet data. For instance, determining the end-to-end delay of a TCP stream necessitates labeling data regions at the source with their creation timestamp. Subsequently, as the data arrives, the receiver calculates the end-to-end delay for each region.
void ClientApp::send()
{
auto data = makeShared<ByteCountChunk>(); // create new data chunk
auto creationTimeTag = data->addTag<CreationTimeTag>(); // add new tag
creationTimeTag->setCreationTime(simTime()); // store current time
auto packet = new Packet("Data", data); // create new packet
socket.send(packet); // send packet using TCP socket
}
Within a TCP stream, the data may be split, rearranged, and combined in various ways by the underlying network. The packet data representation is responsible for preserving the associated region tags as if they were individually attached to each bit. To prevent a cluttered data representation resulting from the aforementioned characteristics, the tag API offers automatic merging for successive, equivalent tag regions.
void ServerApp::receive(Packet *packet)
{
auto data = packet->peekData(); // get all data from the packet
auto regions = data->getAllTags<CreationTimeTag>(); // get all tag regions
for (auto& region : regions) { // for each region do
auto creationTime = region.getTag()->getCreationTime(); // original time
auto delay = simTime() - creationTime; // compute delay
cout << region.getOffset() << region.getLength() << delay; // use data
}
}
The above loop could execute once for the entirety of the data, or it could execute multiple times, depending on the data’s creation at the sender and the operation of the underlying network.
Dissecting Packets¶
Understanding what’s inside a packet is a very important and often used
functionality. Simply using the representation may be insufficient,
because the Packet
may be represented with a BytesChunk
,
for exmple. The Packet API provides a PacketDissector
class which
analyzes a packet solely based on the assigned packet protocol and the
actual data it contains.
The analysis is done according to the protocol logic as opposed to the
actual representation of the data. The PacketDissector
works
similarly to a parser. Basically, it walks through each part (such as
protocol headers) of a packet in order. For each part, it determines the
corresponding protocol and the most specific representation for that
protocol.
The PacketDissector
class relies on small registered
protocol-specific dissector classes (e.g. Ipv4ProtocolDissector
)
subclassing the required ProtocolDissector
base class.
Implementors are expected to use the PacketDissector::ICallback
interface to notify the parser about the packet structure.
void startProtocolDataUnit(Protocol *protocol);
void endProtocolDataUnit(Protocol *protocol);
void markIncorrect();
void visitChunk(Ptr<Chunk>& chunk, Protocol *protocol);
void dissectPacket(Packet *packet, Protocol *protocol);
In order to use the PacketDissector
, the user is expected to
implement a PacketDissector::ICallback
interface. The callback
interface will be notified for each part of the packet as the
PacketDissector
goes through it.
auto& registry = ProtocolDissectorRegistry::getInstance();
PacketDissector dissector(registry, callback);
auto packetProtocolTag = packet->findTag<PacketProtocolTag>();
auto protocol = packetProtocolTag->getProtocol();
dissector.dissectPacket(packet, protocol);
Filtering Packets¶
Filtering packets based on the actual data they contain is another widely used and very important feature. With the help of the packet dissector, it is very simple to create arbitrary custom packet filters. Packet filters are generally used for recording packets and visualizing various packet related information.
In order to simplify filtering, the Packet API provides a generic
expression based packet filter which is implemented in the
PacketFilter
class. The expression syntax is the same as other
OMNeT++ expressions, and the data filter is matched against individual
chunks of the packet as found by the packet dissector.
For example, the packet filter expression “ping*” matches all packets having the name prefix ’ping’, and the packet chunk filter expression “inet::Ipv4Header and srcAddress(10.0.0.*)” matches all packets that contain an IPv4 header with a ’10.0.0’ source address prefix.
PacketFilter filter; // patterns for the whole packet and for the data
filter.setPattern("ping*", "Ipv4Header and sourceAddress(10.0.0.*)");
filter.matches(packet); // returns boolean value
Printing Packets¶
During model development, packets often need to be displayed in a human
readable form. The Packet API provides a PacketPrinter
class
which is capable of forming a human readable string representation of
Packet
’s. The PacketPrinter
class relies on small
registered protocol-specific printer classes (e.g.
Ipv4ProtocolPrinter
subclassing the required
ProtocolPrinter
base class.
The packet printer is automatically used by the OMNeT++ runtime user interface to display packets in the packet log window. The packet printer contributes several log window columns into the user interface: ’Source’, ’Destination’, ’Protocol’, ’Length’, and ’Info’. These columns display packet data similarly to the well-known Wireshark protocol analyzer.
PacketPrinter printer; // turns packets into human readable strings
printer.printPacket(std::cout, packet); // print to standard output
The PacketPrinter
provides a few other functions which have
additional options to control the details of the resulting human
readable form.
Recording PCAP¶
Exporting the packets from a simulation into a PCAP file allows further
processing with 3rd party tools. The Packet API provides a
PcapDump
class for creating PCAP files. Packet filtering can be
used to reduce the file size and increase performance.
PcapDump dump;
dump.openPcap("out.pcap", 65535, 0); // maximum length and PCAP type
dump.writePacket(simTime(), packet); // record with current time
Encapsulating Packets¶
Many communication protocols work with simple packet encapsulation. They encapsulate packets with their own protocol specific headers and trailers at the sender node, and they decapsulate packets at the reciver node. The headers and trailers carry the information that is required to provide the protocol specific service.
For example, the Ethernet MAC protocol encapsulates an IP datagram by prepending the packet with an Ethernet MAC header, and also by appending the packet with an optional padding and an Ethernet FCS. The following example shows how a MAC protocol could encapsulate a packet:
void Mac::encapsulate(Packet *packet)
{
auto header = makeShared<MacHeader>(); // create new header
header->setChunkLength(B(8)); // set chunk length to 8 bytes
header->setLengthField(packet->getDataLength()); // set length field
header->setTransmitterAddress(selfAddress); // set other header fields
packet->insertAtFront(header); // insert header into packet
auto trailer = makeShared<MacTrailer>(); // create new trailer
trailer->setChunkLength(B(4)); // set chunk length to 4 bytes
trailer->setFcsMode(FCS_MODE_DECLARED); // set trailer fields
packet->insertAtBack(trailer); // insert trailer into packet
}
When receiving a packet, the Ethernet MAC protocol removes an Ethernet MAC header and an Ethernet FCS from the packet, and passes the resulting IP datagram along. The following example shows how a MAC protocol could decapsulate a packet:
void Mac::decapsulate(Packet *packet)
{
auto header = packet->popAtFront<MacHeader>(); // pop header from packet
auto lengthField = header->getLengthField();
cout << header->getChunkLength() << endl; // print chunk length
cout << lengthField << endl; // print header length field
cout << header->getReceiverAddress() << endl; // print other header fields
auto trailer = packet->popAtBack<MacTrailer>(); // pop trailer from packet
cout << trailer->getFcsMode() << endl; // print trailer fields
assert(packet->getDataLength() == lengthField); // if the packet is correct
}
Although the popAtFront
and popAtBack
functions change the remaining
unprocessed part of the packet, they don’t have effect on the actual packet data.
That is when the packet reaches high level protocol, it still contains all the
received data but the remaining unprocessed part is smaller.
Fragmenting Packets¶
Communication protocols often provide fragmentation to overcome various physical limits (e.g. length limit, error rate). They split packets into smaller pieces at the sender node, which send them one-by-one. They form the original packet at the receiver node by combining the received fragments.
For example, the IEEE 802.11 protocol fragments packets to overcome the increasing probability of packet loss of large packets. The following example shows how a MAC protocol could fragment a packet:
vector<Packet *> *Mac::fragment(Packet *packet, vector<b>& sizes)
{
auto offset = b(0); // start from the packet's beginning
auto fragments = new vector<Packet *>(); // result collection
for (auto size : sizes) { // for each received size do
auto fragment = new Packet("Fragment"); // header + data part + trailer
auto header = makeShared<MacHeader>(); // create new header
header->setFragmentOffset(offset); // set fragment offset for reassembly
fragment->insertAtFront(header); // insert header into fragment
auto data = packet->peekAt(offset, size); // get data part from packet
fragment->insertAtBack(data); // insert data part into fragment
auto trailer = makeShared<MacTrailer>(); // create new trailer
fragment->insertAtBack(trailer); // insert trailer into fragment
fragments->push_back(fragment); // collect fragment into result
offset += size; // increment offset with size of data part
}
return fragments;
}
When receiving fragments, protocols need to collect the coherent fragments of the same packet until all fragments becomes available. The following example shows how a MAC protocol could form the original packet from a set of coherent fragments:
Packet *Mac::defragment(vector<Packet *>& fragments)
{
auto packet = new Packet("Original"); // create new concatenated packet
for (auto fragment : fragments) {
fragment->popAtFront<MacHeader>(); // pop header from fragment
fragment->popAtBack<MacTrailer>(); // pop trailer from fragment
packet->insertAtBack(fragment->peekData()); // concatenate fragment data
}
return packet;
}
Aggregating Packets¶
Communication protocols often provide aggregation to better utilize the communication channel by reducing protocol overhead. They wait for several packets to arrive at the sender node, then they form a large aggregated packet which is in turn sent at once. At the receiver node the aggregated packet is split into the original packets, and they are passed along.
For example, the IEEE 802.11 protocol aggregates packets for better channel utilization at both MSDU and MPDU levels. The following example shows a version of how a MAC protocol could create an aggregate packet:
Packet *Mac::aggregate(vector<Packet *>& packets)
{
auto aggregate = new Packet("Aggregate"); // create concatenated packet
for (auto packet : packets) { // for each received packet do
auto header = makeShared<SubHeader>(); // create new subheader
header->setLengthField(packet->getDataLength()); // set subframe length
aggregate->insertAtBack(header); // insert subheader into aggregate
auto data = packet->peekData(); // get packet data
aggregate->insertAtBack(data); // insert data into aggregate
}
auto header = makeShared<MacHeader>(); // create new header
header->setAggregate(true); // set aggregate flag
aggregate->insertAtFront(header); // insert header into aggregate
auto trailer = makeShared<MacTrailer>(); // create new trailer
aggregate->insertAtBack(trailer); // insert trailer into aggregate
return aggregate;
}
The following example shows a version of how a MAC protocol could disaggregate a packet:
vector<Packet *> *Mac::disaggregate(Packet *aggregate)
{
aggregate->popAtFront<MacHeader>(); // pop header from packet
aggregate->popAtBack<MacTrailer>(); // pop trailer from packet
vector<Packet *> *packets = new vector<Packet *>(); // result collection
b offset = aggregate->getFrontOffset(); // start after header
while (offset != aggregate->getBackOffset()) { // up to trailer
auto header = aggregate->peekAt<SubHeader>(offset); // peek sub header
offset += header->getChunkLength(); // increment with header length
auto size = header->getLengthField(); // get length field from header
auto data = aggregate->peekAt(offset, size); // peek following data part
auto packet = new Packet("Original"); // create new packet
packet->insertAtBack(data); // insert data into packet
packets->push_back(packet); // collect packet into result
offset += size; // increment offset with data size
}
return packets;
}
Serializing Packets¶
In real communication systems packets are usually stored as a sequence of bytes directly in network byte order. In contrast, INET usually stores packets in small field based C++ classes (generated by the OMNeT++ MSG compiler) to ease debugging. In order to calculate checksums or to communicate with real hardware, all protocol specific parts must be serializable to a sequence of bytes.
The protocol header serializers are separate classes from the actual
protocol headers. They must be registered in the
ChunkSerializerRegistry
in order to be used. The following
example shows how a MAC protocol header could be serialized to a
sequence of bytes:
void MacHeaderSerializer::serialize
(MemoryOutputStream& stream, Ptr<Chunk>& chunk)
{
auto header = staticPtrCast<MacHeader>(chunk);
stream.writeUint16Be(header->getType()); // unsigned 16 bits, big endian
stream.writeMacAddress(header->getTransmitterAddress());
stream.writeMacAddress(header->getReceiverAddress());
}
Deserialization is somewhat more complicated than serialization, because it must be prepared to handle incomplete or even incorrect data due to errors introduced by the network. The following example shows how a MAC protocol header could be deserialized from a sequence of bytes:
Ptr<Chunk> MacHeaderSerializer::deserialize(MemoryInputStream& stream)
{
auto header = makeShared<MacHeader>(); // create new header
header->setType(stream.readUint16Be()); // unsigned 16 bits, big endian
header->setTransmitterAddress(stream.readMacAddress());
header->setReceiverAddress(stream.readMacAddress());
return header;
}
Emulation Support¶
In order to be able to communicate with real hardware, packets must be converted to and from a sequence of bytes. The reason is that the programming interface of operating systems and external libraries work with sending and receiving raw data.
All protocol headers and data chunks which are present in a packet must have a registered serializer to be able to create the raw sequence of bytes. Protocol modules must also be configured to either disable or compute checksums, because serializers cannot carry out the checksum calculation.
The following example shows how a packet could be converted to a sequence of bytes to send through an external interface:
vector<uint8_t>& ExternalInterface::prepareToSend(Packet *packet)
{
auto data = packet->peekAllAsBytes(); // convert to a sequence of bytes
return data->getBytes(); // actual bytes to send
}
The following example shows how a packet could be converted from a sequence of bytes when receiving from an external interface:
Packet *ExternalInterface::prepareToReceive(vector<uint8_t>& bytes)
{
auto data = makeShared<BytesChunk>(bytes); // create chunk with bytes
return new Packet("Emulation", data); // create packet with data
}
In INET, all protocols automatically support hardware emulation due to
the dual representation of packets. The above example creates a packet
which contains a single chunk with a sequence of bytes. As the packet is
passed through the protocols, they can interpret the data (e.g. by
calling peekAtFront
) as they see fit. The Packet API always
provides the requested representation, either because it’s already
available in the packet, or because it gets automatically deserialized.
Queueing Packets¶
Some protocols store packet data temporarily at the sender node before actual processing can occur. For example, the TCP protocol must store the outgoing data received from the application in order to be able to provide transmission flow control.
The following example shows how a transport protocol could store the received data temporarily until the data is actually used:
class TransportSendQueue
{
ChunkQueue queue; // stores application data
B sequenceNumber; // position in stream
void enqueueApplicationData(Packet *packet);
Packet *createSegment(b length);
};
void TransportSendQueue::enqueueApplicationData(Packet *packet)
{
queue.push(packet->peekData()); // store received data
}
Packet *TransportSendQueue::createSegment(b maxLength)
{
auto packet = new Packet("Segment"); // create new segment
auto header = makeShared<TransportHeader>(); // create new header
header->setSequenceNumber(sequenceNumber); // store sequence number for reordering
packet->insertAtFront(header); // insert header into segment
if (queue.getLength() < maxLength)
maxLength = queue.getLength(); // reduce length if necessary
auto data = queue.pop(maxLength); // pop requested amount of data
packet->insertAtBack(data); // insert data into segment
sequenceNumber += data->getChunkLength(); // increase sequence number
return packet;
}
The ChunkQueue
class acts similarly to a binary FIFO queue except
it works with chunks. Similarly to the Packet
it also
automatically merge consecutive data and selects the most appropriate
representation.
Buffering Packets¶
Protocols at the receiver node often need to buffer incoming packet data until the actual processing can occur. For example, packets may arrive out of order, and the data they contain must be reassembled or reordered before it can be passed along.
INET provides a few special purpose C++ classes to support data buffering:
ChunkBuffer
provides automatic merging for large data chunks from out of order smaller data chunks.ReassemblyBuffer
provides reassembling for out of order data according to an expected length.ReorderBuffer
provides reordering for out of order data into a continuous data stream from an expected offset.
All buffers deal with only the data, represented by chunks, instead of
packets. They automatically merge consecutive data and select the most
appropriate representation. Protocols using these buffers automatically
support all data representation provided by INET, and any combination
thereof. For example, ByteCountChunk
, BytesChunk
,
FieldsChunk
, and SliceChunk
can be freely mixed in the
same buffer.
Reassembling Packets¶
Some protocols may use an unreliable service to transfer a large piece of data over the network. The unreliable service requires the receiver node to be prepared for receiving parts out of order and potentially duplicated.
For example, the IP protocol must store incoming fragments at the receiver node, because it must wait until the datagram becomes complete, before it can be passed along. The IP protocol must also be prepared for receiving the individual fragments out of order and potentially duplicated.
The following example shows how a network protocol could store and reassemble the data of the incoming packets into a whole packet:
class NetworkProtocolDefragmentation
{
ReassemblyBuffer buffer; // stores received data
void processDatagram(Packet *packet); // processes incoming packes
Packet *getReassembledDatagram(); // reassembles the original packet
};
void NetworkProtocolDefragmentation::processDatagram(Packet *packet)
{
auto header = packet->popAtFront<NetworkProtocolHeader>(); // remove header
auto fragmentOffset = header->getFragmentOffset(); // determine offset
auto data = packet->peekData(); // get data from packet
buffer.replace(fragmentOffset, data); // overwrite data in buffer
}
Packet *NetworkProtocolDefragmentation::getReassembledDatagram()
{
if (!buffer.isComplete()) // if reassembly isn't complete
return nullptr; // there's nothing to return
auto data = buffer.getReassembledData(); // complete reassembly
return new Packet("Datagram", data); // create new packet
}
The ReassemblyBuffer
supports replacing the stored data at a
given offset, and it also provides the complete reassembled data with
the expected length if available.
Reordering Packets¶
Some protocols may use an unreliable service to transfer a long data stream over the network. The unreliable service requires the sender node to resend unacknowledged parts, and it also requires the receiver node to be prepared for receiving parts out of order and potentially duplicated.
For example, the TCP protocol must buffer the incoming data at the receiver node, because the TCP segments may arrive out of order and potentially duplicated or overlapping, and TCP is required to provide the data to the application in the correct order and only once.
The following example shows how a transport protocol could store and reorder the data of incoming packets, which may arrive out of order, and also how such a protocol could pass along only the available data in the correct order:
class TransportReceiveQueue
{
ReorderBuffer buffer; // stores receive data
B sequenceNumber;
void processSegment(Packet *packet);
Packet *getAvailableData();
};
void TransportReceiveQueue::processSegment(Packet *packet)
{
auto header = packet->popAtFront<TransportHeader>(); // pop transport header
auto sequenceNumber = header->getSequenceNumber();
auto data = packet->peekData(); // get all packet data
buffer.replace(sequenceNumber, data); // overwrite data in buffer
}
Packet *TransportReceiveQueue::getAvailableData()
{
if (buffer.getAvailableDataLength() == b(0)) // if no data available
return nullptr;
auto data = buffer.popAvailableData(); // remove all available data
return new Packet("Data", data);
}
The ReorderBuffer
supports replacing the stored data at a given
offset, and it provides the available data from the expected offset if
any.