Manuale di Network Simulator v.2

Capitolo 12 - Header e formati dei pacchetti

a cura di Sandro Petrizzelli


 

Introduzione

 

Le procedure e le funzioni descritte in questo capitolo possono essere trovate nei seguenti file:

Gli oggetti della classe “Packet” sono le unità fondamentali di scambio tra gli oggetti di una simulazione.

Nuovi protocolli possono definire gli header dei propri pacchetti oppure possono estendere gli header di formati preesistenti con campi addizionali.

Come vedremo dettagliatamente più avanti, gli header dei nuovi pacchetti vengono introdotti nel simulatore definendo una struttura C++ contenente i campi desiderati, definendo una classe statica che garantisca il linkage con OTcl ( [1] ) e infine modificando alcuni dei codici di inizializzazione del simulatore al fine di individuare, in ciascun pacchetto, la posizione (offset) del nuovo header rispetto a quella degli altri header.

Quando il simulatore viene inizializzato tramite OTcl, un utilizzatore può scegliere di abilitare solo un sottoinsieme di header di pacchetto da inserire nei pacchetti da simulare, il che consente di ridurre l’occupazione di memoria durante l’esecuzione delle simulazioni. Nella configurazione di default del simulatore, sono invece abilitati tutti i tipi di header di pacchetto presenti, nonostante la maggior parte di essi siano in realtà inutili: ad esempio, viene incluso anche l’header TCP quando si compie una simulazione sul protocollo RTP, che tradizionalmente si appoggia su UDP come protocollo di trasporto.

La gestione di quali formati di pacchetti devono essere correntemente abilitati durante una simulazione è effettuata da uno speciale oggetto (il packet header manager, ossia il gestore degli header dei pacchetti), del quale parleremo più avanti. Questo oggetto supporta un metodo OTcl da usarsi per specificare quali header utilizzare in una data simulazione: questo significa che, se il simulatore dovesse far uso di un campo di un header che non è stato abilitato, verrebbe restituito un messaggio di errore e la simulazione verrebbe arrestata.

 

12.1 Header di un pacchetto di un protocollo specifico

 

Gli sviluppatori di protocolli generalmente preferiscono definire uno specifico tipo di header da usare nei pacchetti delle proprie simulazioni. Questa scelta fa si che l’implementazione di un nuovo protocollo eviti di sovrapporre i nuovi campi con campi già esistenti in altri header predefiniti.

Come esempio, consideriamo una versione semplificata del protocollo RTP: in particolare, supponiamo che l’header di un pacchetto RTP contenga solo un campo “sequence number” ed un campo “source identifier”. Per creare un simile header, basta usare le seguenti definizioni di classi e procedure ( [2] ):

 

dal file rtp.h:

 

struct hdr_rtp {

u_int32_t srcid_;

int seqno_;

 

/* per-field member functions */

u_int32_t& srcid() { return (srcid_);

int& seqno() { return (seqno_); }

 

/* Packet header access functions */

static int offset_;

inline static int& offset() { return offset_; }

inline static hdr_rtp* access(const Packet* p)

{

return (hdr_rtp*) p->access(offset_);

}

};

 

dal file rtp.cc:

 

class RTPHeaderClass : public PacketHeaderClass {

public:

RTPHeaderClass() : PacketHeaderClass("PacketHeader/RTP",

sizeof(hdr_rtp))

{

bind_offset(&hdr_rtp::offset_);

}

} class_rtphdr;

 

void RTPAgent::sendpkt()

{

Packet* p = allocpkt();

hdr_rtp *rh = hdr_rtp::access(p);

lastpkttime_ = Scheduler::instance().clock();

 

/* Fill in srcid_ and seqno */

rh->seqno() = seqno_++;

rh->srcid() = session_->srcid();

target_->recv(p, 0);

}

 

RTPAgent::RTPAgent(): session_(0), lastpkttime_(-1e6)

{

type_ = PT_RTP;

bind("seqno_", &seqno_);

}

 

Notiamo in primo luogo che il file rtp.h ( [3] )  serve solo a contenere la definizione della struttura corrispondente all’header RTP considerato. Tale struttura, denominata “hdr_rtp”, definisce dunque il layout dell’header del pacchetto RTP, in termini di word e del loro spiazzamento (offset) ( [4] ): essa cioè definisce i campi contenuti (denominati “srcid_” e “seqno_”) e la loro dimensione. Questa definizione di struttura viene solo usata dal compilatore per calcolare l’offset (espresso in byte) dei vari campi; nessun oggetto di questo tipo di struttura viene mai direttamente allocato.

La struttura hdr_rtp fornisce inoltre due funzioni membro (denominate “offset” e “access”) che rappresentano una sorta di strato di “copertura dei dati” (data hiding) per quegli oggetti che desiderano leggere o modificare i campi dell’header. Si tratta cioè di “funzioni membro pubbliche di accesso ai dati” della struttura, che consentono operazioni sui dati nascondendo, agli oggetti che richiedono tali operazioni, la struttura interna.

Viene inoltre definita una variabile membro statica, chiamata “offset_”, che viene usata per individuare l’offset (sempre in byte) col quale l’header RTP è localizzato in un generico pacchetto di ns. Le due funzioni membro, di cui si diceva poco fa, servono proprio ad utilizzare questa variabile per accedere a questo particolare header in ciascun pacchetto:

 

 

Per spiegarci meglio, supponiamo ad esempio di voler accedere all’header RTP di un pacchetto puntato dalla variabile p (che quindi è una variabile puntatore): è sufficiente usare l’istruzione

 

hdr_rtp::access(p)

 

Essa restituisce un puntatore all’inizio dell’header RTP nel pacchetto specificato.

Notiamo inoltre che la variabile offset_, essendo definita in C++, necessita di un binding on una analoga variabile OTcl affinché la si possa usare direttamente tramite l’interprete OTcl: a questo pensano opportune routine nei file ~ns/tcl/lib/ns-packet.tcl e ~ns/packet.cc.

Notiamo un’altra cosa: la parola chiave const, usata per passare l’argomento alla funzione access(), serve a garantire un accesso di sola lettura al pacchetto specificato (cioè quello puntato dal puntatore fornito appunto come argomento). E’ quindi una forma di “protezione” del contenuto del pacchetto.

Passando adesso al contenuto del file rtp.cc, notiamo per prima cosa la definizione di una classe, denominata “RTPHeaderClass”, derivata dalla classe “PacketHeaderClass”: l’oggetto statico “class_rtphdr” di tale classe viene usato per fornire il linkage all’OTcl quando l’header RTP viene abilitato per le simulazioni, al momento della configurazione delle simulazioni stesse (configuration time). Quando viene eseguito il simulatore, questo oggetto statico chiama il costruttore della classe base PacketHeaderClass, passandogli due argomenti: la stringa “PacketHeader/RTP” e il risultato restituito dalla funzione “sizeof(hdr_rtp)”. Così facendo, la dimensione dell’header RTP viene memorizzata e resa disponibile per il packet header manager ( [5] ). Dopo questa chiamata, viene avviato il costruttore vero e proprio della classe RTPHeaderClass, il quale provvede semplicemente a eseguire l’istruzione

 

bind_offset(&hdr_rtp::offset_)

 

In tal modo, il packet header manager sa dove memorizzare l’offset di questo particolare header di pacchetto.

Dopo la definizione della classe PacketHeaderClass, viene definita la funzione membro “sendpkt()” per la classe RTPAgent: essa consente di creare ed inviare un nuovo pacchetto. Vediamo cosa fa questa funzione:

 

·      per prima cosa, essa chiama a sua volta il metodo “allocpkt()”, il quale si occupa di allocare un nuovo pacchetto e di assegnare i valori a tutti i campi dell’header di livello rete (in questo caso, si tratta dell’header IP, mentre protocolli diversi da IP sono trattati separatamente). Tale funzione restituisce un puntatore al pacchetto appena allocato;

·      successivamente, c’è una ulteriore chiamata, che ora riguarda il metodo “Packet::access(void)”: per il pacchetto appena allocato, questo metodo restituisce l’indirizzo del primo byte di un buffer di memoria usato per conservare le informazioni sull’header ( [6] ). Il valore restituito da questa funzione viene usato come puntatore all’header di interesse;

·      vengono a questo punto chiamate altre funzioni membro dell’oggetto RTPHeader, le quali accedono ai singoli campi dell’header del pacchetto;

·      infine, viene invocato il metodo “recv()” della destinazione cui è diretto il pacchetto appena costruito, in modo da simularne la consegna.

   

 

12.1.1 Aggiungere un nuovo tipo di header di pacchetto

 

Se volessimo creare un nuovo header, chiamato ad esempio “newhdr”, dovremmo seguire i seguenti passi ( [7] ):

 

·      creare una nuova struttura (chiamata “hdr_newhdr”) definendo i campi di interesse, la variabile “offset_” e i metodi di accesso;

·      definire le funzioni membro per i campi desiderati;

·      creare una classe statica per effettuare il linkage tra C++ e OTcl, in modo che in OTcl sia disponibile la classe denominata PacketHeader/Newhdr; in particolare, dobbiamo aver cura di inserire, nel costruttore della suddetta classe statica, l’istruzione “bind_offset()”;

·      editare il file ~ns/tcl/lib/ns-packet.tcl al fine di abilitare il nuovo formato di header ( [8] ).

 

 

 

12.1.2 Scelta degli header da usare in una simulazione

 

Per default, NS include, in OGNI pacchetto di OGNI simulazione, TUTTI gli header di pacchetto di TUTTI i protocolli implementati nel programma. Evidentemente, questo comporta un sovraccarico di memoria non indifferente, che risulta tanto maggiore quanto più cresce il numero di protocolli implementati. Ad esempio, allo stato attuale (30 agosto 2000), la dimensione degli header di pacchetto di tutti i protocolli implementati in NS è di circa 1.9 kbyte; se invece scegliamo esplicitamente di usare solo l’header IP e l’header TCP, tale dimensione scende a circa 100 byte.

Per includere solo gli header di pacchetto che sono di interesse per una data simulazione, si possono seguire due strade:

 

·      la prima è quella di rimuovere selettivamente gli header che non interessano; ad esempio, se volessimo escludere, dalla nostra simulazione, solo gli header AODV e ARP, ci basterebbe scrivere, prima di avviare la simulazione, quanto segue:

 

remove-packet-header AODV ARP

……

set ns [new Simulator]

 

E’ importante sottolineare che l’istruzione “remove-packet-header” deve essere eseguita prima di creare l’oggetto Simulatore.

Ricordiamo inoltre che i nomi di tutti gli header di pacchetto sono sempre nella forma PacketHeader/[hdr], per cui l’utente deve semplicemente fornire la parte finale, [hdr], tralasciando il prefisso.

Per trovare i nomi degli header di pacchetto correntemente usati dal simulatore, si può leggere il file ~ns/tcl/lib/ns-packet.tcl oppure si possono eseguire i seguenti semplici comandi (ovviamente dopo aver avviato ns):

 

foreach cl [PacketHeader inf sublacc] {

  puts $cl

}

 

·      la seconda strada è quella di rimuovere prima tutti i formati esistenti ( [9] ) e poi di aggiungere solo quelli di interesse; ad esempio, se fossimo interessati solo agli header IP, UDP e RTP, dovremmo scrivere quanto segue:

 

remove-all-packet-header

add-packet-header IP UDP RTP

……

set ns [new Simulator]

 

E’ importante sottolineare che non bisogna mai rimuovere il cosiddetto “common header” da una simulazione.

 

 

 

12.2 Classi Packet

 

Ci sono quattro importanti classi C++ per la manipolazione dei pacchetti e dei loro header:

 

  1. la classe “Packet” definisce il tipo di tutti i pacchetti nella simulazione; è una sottoclasse della classe “Event”, il che consente di schedulare i pacchetti;

  2. la classe “P_Info” mantiene tutte le rappresentazioni testuali dei nomi dei pacchetti;

  3. la classe “PacketHeader” costituisce la classe base con cui configurare un qualsiasi header di pacchetto da usarsi nelle simulazioni; essa essenzialmente fornisce uno stato interno sufficiente per allocare ogni particolare header di pacchetto nell’insieme di tutti gli header presenti in un dato pacchetto;

  4. infine, la classe “PacketHeaderManager” è usata per gestire tutti gli header correntemente utilizzati. Essa viene invocata da un metodo, disponibile in OTcl, nel momento di configurazione della simulazione, al fine di abilitare l’ insieme di header di pacchetto che si intende usare.

 

 

 

12.2.1 Classe Packet

 

Questa classe definisce la struttura di un pacchetto e fornisce le funzioni membro per manipolare una libera lista di oggetti di questo tipo. Essa viene illustrata nella prossima figura:

 

 

La classe Packet è definita nel modo seguente, all’interno del file packet.h:

 

class Packet : public Event {

 

private:

friend class PacketQueue;

u_char* bits_;

u_char* data_; /* variable size buffer for ’data’ */

u_int datalen_; /* length of variable size buffer */

 

protected:

static Packet* free_;

 

public:

Packet* next_; /* for queues and the free list */

static int hdrlen_;

Packet() : bits_(0), datalen_(0), next_(0) {}

u_char* const bits() { return (bits_); }

Packet* copy() const;

static Packet* alloc();

static Packet* alloc(int);

inline void allocdata(int);

static void free(Packet*);

inline u_char* access(int off)

{

if (off < 0)

abort();

return (&bits_[off]);

}

inline u_char* accessdata() { return data_; }

};

 

Come si può subito vedere, la classe Packet è derivata (in modo public) dalla classe base Event.

Essa contiene in primo luogo tre variabili private e una funzione di tipo friend. Tali tre variabili private sono due puntatori (“bits_” e “data_”) ed un intero (“datalen”). Segue una variabile statica protetta (il puntatore “free_” ad un altro oggetto di classe Packet) e poi un certo numero di variabili e funzioni membro pubbliche.

Questa classe fornisce un puntatore (“bits_”) ad un generico vettore di caratteri non segnati (unsigned characters), comunemente chiamato BOB (che sta per “bag of bits”): in questo vettore sono memorizzati i campi degli header di pacchetto. La variabile “bits_” contiene appunto l’indirizzo del primo byte del vettore BOB. Tale vettore è concretamente implementato come una concatenazione di tutte le strutture definite per ciascun header di pacchetto. Per convenzione, tali strutture hanno nomi che cominciano con “hdr_<qualcosa>”.

Generalmente, il vettore BOB rimane di dimensione fissa durante una simulazione e la sua dimensione è registrata nella variabile membro statica “Packet::hdrlen. La dimensione del vettore BOB viene impostata, direttamente tramite OTcl, quando viene configurata la simulazione e, come detto, rimane generalmente invariata durante la simulazione, anche se è previsto che la si possa variare dinamicamente.

Gli altri metodi della classe Packet servono per creare nuovi pacchetti e per memorizzare quelli non più utilizzati in una lista libera privata. Questa opera di allocazione e deallocazione è effettuata dalle funzioni realizzate con il seguente codice (nel file ~ns/packet.h):

 

inline Packet* Packet::alloc()

{

Packet* p = free_;

if (p != 0)

free_ = p->next_;

else

{

p = new Packet;

p->bits_ = new u_char[hdrsize_];

if (p == 0 || p->bits_ == 0)

abort();

}

return (p);

}

 

/* allocate a packet with an n byte data buffer */

inline Packet* Packet::alloc(int n)

{

Packet* p = alloc();

if (n > 0)

p->allocdata(n);

return (p);

}

 

/* allocate an n byte data buffer to an existing packet */

inline void Packet::allocdata(int n)

{

datalen_ = n;

data_ = new u_char[n];

if (data_ == 0)

abort();

}

 

inline void Packet::free(Packet* p)

{

p->next_ = free_;

free_ = p;

if (p->datalen_)

{

delete p->data_;

p->datalen_ = 0;

}

}

 

inline Packet* Packet::copy() const

{

Packet* p = alloc();

memcpy(p->bits(), bits_, hdrlen_);

if (datalen_)

{

p->datalen_ = datalen_;

p->data_ = new u_char[datalen_];

memcpy(p->data_, data_, datalen_);

}

return (p);

}

 

La prima funzione riportata è “alloc()”: si tratta di una funzione di supporto comunemente usata per la creazione di nuovi pacchetti. Questa funzione viene a sua volta tipicamente chiamata, in favore di un dato Agente, dal metodo “Agent::allocpkt()”, mentre invece generalmente non viene invocata direttamente da altri oggetti. Essa tenta per prima cosa di allocare un vecchio pacchetto nella lista libera e, se tale operazione non riesce (il che accade quando l’indirizzo contenuto nella variabile “free_” è nullo), alloca un nuovo pacchetto usando l’operatore C++ denominato “new”.

E’ importante sottolineare che gli oggetti della classe Packet e il vettore BOB vengono allocati separatamente.

La funzione “free()” serve invece a “liberare” un pacchetto, restituendolo alla lista libera. Facciamo notare che i pacchetti non vengono mai restituiti all’allocatore della memoria di sistema: al contrario, essi sono memorizzati in una lista libera quando la funzione “free()” viene invocata.

Infine, la funzione membro “copy()” crea una nuova e identica copia di un dato pacchetto, con l’eccezione del campo denominato “uid_”, che invece deve essere unico per ciascun pacchetto ( [10] ). Questa funzione è tipicamente usata dagli oggetti della classe “Replicator” per supportare la distribuzione multicast dei pacchetti e le simulazioni sulle LAN.

 

 

 

12.2.2 Classe p_Info

 

Questa classe è usata come “colla” per collegare i valori numerici corrispondenti ai vari tipi di pacchetti con i loro relativi nomi simbolici.

Quando un nuovo tipo di pacchetti viene definito, il suo codice numerico deve essere aggiunto alla variabile “packet_t” ( [11] ) ed il suo nome simbolico deve essere aggiunto al costruttore della classe p_Info:

 

enum packet_t

{

PT_TCP,

...

PT_NTYPE // This MUST be the LAST one

};

 

class p_info

{

public:

p_info()

{

name_[PT_TCP]= "tcp";

...

}

}

 

Come si vede, la variabile “packet_t” è un enumerato, il cui ultimo elemento deve sempre essere costituito dalla variabile PT_NTYPE.

La classe “p_info”, invece, presenta un omonimo costruttore che esegue il predetto collegamento tra codici numerici e nomi simbolici dei pacchetti.

 

 

 

12.2.3 Classe hdr_cmn

 

Ogni pacchetto, in una simulazione, presenta un “common header”, secondo la struttura descritta nella seguente figura:

 

 

Il common header è definito, sempre nel file ~ns/packet.h, nel modo seguente:

 

struct hdr_cmn

{

double ts_;             /* timestamp: for q-delay measurement */

packet_t ptype_;        /* packet type (see above) */

int uid_;                   /* unique id */

int size_;              /* simulated packet size */

int iface_;             /* receiving interface (label) */

 

/* Packet header access functions */

static int offset_;

inline static int& offset() { return offset_; }

inline static hdr_cmn* access(Packet* p)

{

return (hdr_cmn*) p->access(offset_);

}

 

/* per-field member functions */

int& ptype() { return (ptype_); }

int& uid() { return (uid_); }

int& size() { return (size_); }

int& iface() { return (iface_);

double& timestamp() { return (ts_); }

};

 

Questa struttura descrive innanzitutto i campi usati per “tracciare” il flusso dei pacchetti o per misurare altre quantità di possibile interesse:

 

 

 

 

12.2.4 Classe PacketHeaderManager

 

Un oggetto di questa classe viene usato per gestire l’insieme dei tipi di pacchetti correntemente utilizzati dalla simulazione e per assegnare a ciascuno di essi un offset univoco all’interno del vettore BOB.

Questa classe è definita sia nel codice C++ sia nel codice OTcl, come possiamo osservare qui di seguito:

 

dal file tcl/lib/ns-packet.tcl:

 

PacketHeaderManager set hdrlen_ 0

......

foreach prot {

AODV

ARP

aSRM

Common

CtrMcast

Diffusion

......

TORA

UMP

} {

add-packet-header $prot

}

 

Simulator instproc create_packetformat {}

{

PacketHeaderManager instvar tab_

set pm [new PacketHeaderManager]

foreach cl [PacketHeader info subclass]

{

if [info exists tab_($cl)]

{

set off [$pm allochdr $cl]

$cl offset $off

}

}

$self set packetManager_ $pm

}

 

PacketHeaderManager instproc allochdr cl

{

set size [$cl set hdrlen_]

$self instvar hdrlen_

set NS_ALIGN 8 ;# round up to nearest NS_ALIGN bytes, (needed on sparc/solaris)

set incr [expr ($size + ($NS_ALIGN-1)) & ~($NS_ALIGN-1)]

set base $hdrlen_

incr hdrlen_ $incr

return $base

}

 

dal file packet.cc:

 

/* manages active packet header types */

class PacketHeaderManager : public TclObject

{

public:

PacketHeaderManager()

{

bind("hdrlen_", &Packet::hdrlen_);

}

};

 

Il codice nel file ~ns/tcl/lib/ns-packet.tcl viene eseguito quando il simulatore viene inizializzato. Quindi, lo statement “foreach” viene eseguito prima che cominci la simulazione: it initializes the OTcl class array tab_ to contain the mapping between the class name and the names of the currently active packet header classes. Come già detto in precedenza, gli header dei pacchetti possono essere acceduti usando il metodo “hdr_<hdrname>::access()”.

La procedura “create::packetformat{}” è parte della classe Simulator e viene chiamata una sola volta, durante la configurazione della simulazione: essa crea un singolo oggetto della classe “PacketHeaderManager”. Il costruttore C++ collega la variabile OTcl denominata “hdrlen_” (appartenente proprio alla classe PacketHeaderManager) alla variabile membro C++ “hdrlen_” della classe Packet. Questa operazione ha l’effetto di settare la suddetta variabile “Packet::hdrlen_” al valore zero.

Dopo aver creato il gestore dei pacchetti (packet manager), il ciclo “foreach” abilita ciascuno degli header di pacchetto di interesse. Questo ciclo itera attraverso una lista di predefiniti header di pacchetto, nella forma (hi,oi), dove hi è il nome dell’i-simo header, mentre oi è il nome della variabile contenente la posizione dell’header hi nel vettore BOB.

Il posizionamento degli header è effettuato dalla procedura “allochdr” della classe PacketHeaderManager. Questa procedura mantiene una variabile dinamica “hdrlen_” al valore dell’attuale lunghezza del vettore BOB, in modo che sempre nuovi header di pacchetto possano essere abilitati. It also arranges for 8-byte alignment for any newly-enabled packet header. This is needed to ensure that when double-world length quantities are used in packet headers on machines where double-word alignment is required, access faults are not produced.

 

 

12.3 Comandi principali

 

Quella che segue è una lista dei principali comandi OTcl legati agli header dei pacchetti:

 

This is an internal simulator procedure and is called once during the simulator configuration to setup a packetHeaderManager object.

This is another internal procedure of Class PacketHeaderManager that keeps track of a variable called hdrlen_ as new packet-headers are enabled. It also allows 8-byte allignment for any newly-enabled pkt header.

Takes a list of arguments, each of which is a packet header name (without PacketHeader/prefix). This global proc will tell simulator to include the specified packet header(s) in your simulation.

This statement operates in the same syntax, but it removes the specified headers from your simulation; notice that it does not remove the common header even it is instructed to do so.

This is a global Tcl proc. It takes no argument and removes all packet headers, except thecommon header, from your simulation. add-all-packet-headers is its counterpart.



[1] Si veda il capitolo 3 in proposito

[2] Si vedano, in proposito, i file ~ns/rtp.h e ~ns/rtp.cc

[3] che è un file di intestazione secondo la terminologia propria del C++, data l’estensione finale “.h”

[4] Ci si riferisce, cioè, alla posizione dei vari campi nell’header: se si pensa ad una lista di elementi, l’i-simo elemento della lista è individuabile conoscendo il suo offset rispetto all’inizio della lista stessa; analogamente, il generico campo, all’interno di una sequenza di campi costituenti un dato header, è individuato dal suo offset rispetto all’inizio del primo campo dello stesso header.

[5] Vedere in proposito il paragrafo 12.2.4

[6] Ne parleremo più avanti.

[7] Si faccia il confronto con quanto visto nel precedente paragrafo a proposito dell’header RTP semplificato

[8] Si vedano, in proposito, i paragrafi 12.2.2 e 12.2.4

[9] Ovviamente, si tratta di rimuovere l’uso e non certo le definizioni!

[10] Come vedremo, il campo “uid_” appartiene al “common header” posseduto da ciascun pacchetto usato in una simulazione.

[11] Si veda in proposito il file ~ns/packet.h

 

 


Continua con:        Indice       Capitolo 11      Capitolo 13


Aggiornamento: 18 aprile 2001

home