Table Of Contents
Table Of Contents

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 classes

  • repeated 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 of FieldsChunk)

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.