A Discrete-Event Network Simulator
Tutorial

Ιχνηλασία

Ιστορικό

Όπως αναφέρεται στο Χρησιμοποιώντας το Σύστημα Ιχνηλασίας, το νόημα της λειτουργίας μιας προσομοίωσης ns-3 είναι να παράγει έξοδο για μελέτη. Έχετε δύο βασικές στρατηγικές για την απόκτηση εξόδου από ns-3: τη χρήση γενικών μαζικών μηχανισμών παραγωγής και την ανάλυση του περιεχομένου τους για να εξαχθούν ενδιαφέρουσες πληροφορίες, ή με κάποιο τρόπο την ανάπτυξη ενός μηχανισμού εξόδου που αποπνέει ακριβώς (και ίσως μόνο) τις ζητούμενες πληροφορίες.

Η χρήση μαζικών μηχανισμών εξόδου έχει το πλεονέκτημα ότι δεν απαιτεί αλλαγές στον ns-3, αλλά μπορεί να απαιτήσει τη συγγραφή σεναρίων για την ανάλυση και το φιλτράρισμα των δεδομένων του ενδιαφέροντος. Συχνά, PCAP ή NS_LOG μηνύματα εξόδου που συγκεντρώνονται κατά τη διάρκεια προσομοιώσεων τρέχουν και ξεχωριστά τρέχουν μέσα από σενάρια που χρησιμοποιούν grep, sed ή awk για να αναλύσουν τα μηνύματα και να μειώσουν και να μετατρέψουν τα δεδομένα σε μία διαχειρίσιμη μορφή. Τα προγράμματα πρέπει να είναι γραμμένα ώστε να μετατρέπονται, έτσι αυτό δεν έρχεται δωρεάν. Η έξοδος του NS_LOG δεν θεωρείται μέρος του ns-3 API, και μπορεί να αλλάξει χωρίς προειδοποίηση μεταξύ των εκδόσεων. Επιπλέον, η έξοδος του NS_LOG είναι διαθέσιμη μόνο σε εκδόσεις εντοπισμού σφαλμάτων, έτσι επικαλούμενη επιβάλλει ποινή απόδοσης. Φυσικά, αν η πληροφορία που ενδιαφέρει δεν υπάρχει σε κανένα από τους προκαθορισμένους μηχανισμούς εξόδου, η προσέγγιση αυτή αποτυγχάνει.

Εάν χρειάζεστε να προσθέσετε μερικές εξειδικευμένες πληροφορίες στους μαζικούς μηχανισμούς, αυτό σίγουρα μπορεί να γίνει΄ και αν χρησιμοποιήσετε έναν από τους ns-3 μηχανισμούς, μπορείτε να πάρετε τον κώδικά σας προστιθέμενο ως εισφορά.

Ο ns-3 παρέχει έναν άλλο μηχανισμό, που ονομάζεται Tracing, ο οποίος αποφεύγει ορισμένα από τα προβλήματα που συνδέονται με τους μαζικούς μηχανισμούς εξόδου. Έχει αρκετά σημαντικά πλεονεκτήματα. Κατ ‘αρχάς, μπορείτε να μειώσετε την ποσότητα των δεδομένων που θα πρέπει να διαχειριστείτε από μόνο τον εντοπισμό των εκδηλώσεων που σας ενδιαφέρουν (για τις μεγάλες προσομοιώσεις, τοποθετώντας τα πάντα στο δίσκο για την μετα-επεξεργασία μπορεί να δημιουργήσει I / O σημεία συμφόρησης). Δεύτερον, αν χρησιμοποιείτε αυτή τη μέθοδο, μπορείτε να ελέγξετε τη μορφοποίηση της εξόδου άμεσα, έτσι ώστε να αποφευχθεί το στάδιο της μετα-επεξεργασίας με sed, awk, perl ή python σενάρια. Αν επιθυμείτε, η παραγωγή σας μπορεί να διαμορφωθεί άμεσα σε μορφή αποδεκτή από gnuplot, για παράδειγμα (βλέπε επίσης GnuplotHelper). Μπορείτε να προσθέσετε άγκιστρα στον πυρήνα που μπορούν να προσπελαστούν από άλλους χρήστες, αλλά οι οποίες δε θα παράγουν καμία πληροφορία εκτός αν σας ζητηθεί ρητά να το πράξετε. Για αυτούς τους λόγους, πιστεύουμε ότι το σύστημα ανίχνευσης ns-3 είναι ο καλύτερος τρόπος για να πάρετε πληροφορίες από μια προσομοίωση και είναι επίσης, ως εκ τούτου ένας από τους πιο σημαντικούς μηχανισμούς για να καταλάβουμε τον ns-3.

Εξειδικευμένα εργαλεία

Υπάρχουν πολλοί τρόποι για να πάρετε πληροφορίες από ένα πρόγραμμα. Ο πιο απλός τρόπος είναι να εκτυπώσετε μόνο τις πληροφορίες απευθείας στην κανονική έξοδο, όπως και στην

#include <iostream>
...
void
SomeFunction (void)
{
  uint32_t x = SOME_INTERESTING_VALUE;
  ...
  std::cout << "The value of x is " << x << std::endl;
  ...
}

Κανείς δεν πρόκειται να σας αποτρέψει από το να πηγαίνετε βαθιά στον πυρήνα του ns-3 και προσθέτοντας τις δηλώσεις εκτύπωσης. Αυτό είναι τρομερά εύκολο να γίνει και μετά, έχετε τον πλήρη έλεγχο του δικού σας υποκατάστημα ns-3. Αυτό πιθανόν να μην αποδειχθεί ότι είναι ικανοποιητικό σε μακροπρόθεσμη βάση, όμως.

Καθώς ο αριθμός των καταστάσεων εκτύπωσης αυξάνει στα προγράμματά σας, το έργο της αντιμετώπισης του μεγάλου αριθμού των αποτελεσμάτων θα γίνονται όλο και περισσότερο περίπλοκα. Τελικά, μπορεί να αισθανθείτε την ανάγκη να ελέγχετε ό,τι πληροφορίες εκτυπώνετε με κάποιο τρόπο, ίσως ενεργοποιώντας και απενεργοποιώντας ορισμένες κατηγορίες εκτυπώσεις, ή αυξάνοντας ή μειώνοντας το την ποσότητα των πληροφοριών που θέλετε. Εάν συνεχίσουμε αυτήν την πορεία σας μπορεί να ανακαλύψετε ότι έχετε εκ νέου σε εφαρμογή το μηχανισμό NS_LOG (βλέπε Χρησιμοποιώντας την Ενότητα Καταγραφής). Προκειμένου να αποφευχθεί αυτό, ένα από τα πρώτα πράγματα που θα μπορούσατε να εξετάσετε είναι να χρησιμοποιείσετε μόνο του το NS_LOG.

Μας αναφέρθηκε παραπάνω ότι ένας τρόπος για να πάρετε πληροφορίες από τον ns-3 είναι να αναλύσει τις υπάρχουσες εξόδους NS_LOG για ενδιαφέρουσες πληροφορίες. Αν ανακαλύψετε ότι χρειάζεστε κάποια εξειδικευμένη πληροφορία που δεν είναι παρών σε υφιστάμενες εξόδους αρχείων καταγραφής, μπορείτε να επεξεργαστείτε τον πυρήνα του ns-3 και απλά προσθέστε ενδιαφέρουσες πληροφορίες σας στη ροή εξόδου. Τώρα, αυτό είναι σίγουρα καλύτερο από την προσθήκη των δικών σας δηλώσεων εκτύπωσης, δεδομένου ότι ακολουθεί ο ns-3 συμβάσεις κωδικοποίησης και θα μπορούσε ενδεχομένως να είναι χρήσιμο σε άλλους ανθρώπους ως ένα patch στον υφιστάμενο πυρήνα.

Ας πάρουμε ένα τυχαίο παράδειγμα. Αν θέλετε να προσθέσετε περισσότερη καταγραφή στον ns-3 υποδοχή TCP (tcp-socket-base.cc) θα μπορούσατε απλά να προσθέσετε ένα νέο μήνυμα κάτω στην εφαρμογή. Σημειώστε ότι στο TcpSocketBase::ReceivedAck() δεν υπάρχει log μήνυμα για την περίπτωση του no ACK. Θα μπορούσατε απλά να προσθέσετε ένα, αλλάζοντας τον κώδικα. Εδώ είναι η αρχική

/** Process the newly received ACK */
void
TcpSocketBase::ReceivedAck (Ptr<Packet> packet, const TcpHeader& tcpHeader)
{
  NS_LOG_FUNCTION (this << tcpHeader);

  // Received ACK. Compare the ACK number against highest unacked seqno
  if (0 == (tcpHeader.GetFlags () & TcpHeader::ACK))
    { // Ignore if no ACK flag
    }
  ...

Για να συνδεθείτε στην περίπτωση του no ACK, μπορείτε να προσθέσετε ένα νέο `` NS_LOG_LOGIC`` στο `` if`` σώμα δήλωσης

/** Process the newly received ACK */
void
TcpSocketBase::ReceivedAck (Ptr<Packet> packet, const TcpHeader& tcpHeader)
{
  NS_LOG_FUNCTION (this << tcpHeader);

  // Received ACK. Compare the ACK number against highest unacked seqno
  if (0 == (tcpHeader.GetFlags () & TcpHeader::ACK))
    { // Ignore if no ACK flag
      NS_LOG_LOGIC ("TcpSocketBase " << this << " no ACK flag");
    }
  ...

Αυτό μπορεί να φαίνεται αρκετά απλό και ικανοποιητικό με την πρώτη ματιά, αλλά κάτι που πρέπει να δούμε είναι ότι θα πρέπει να γράψετε κώδικα για να προσθέσετε δηλώσεις NS_LOG και θα πρέπει επίσης να γράψετε κώδικα (όπως στο grep, sed ή awk σενάρια) για να αναλύσει το αρχείο καταγραφής εξόδου, προκειμένου να απομονώσουν τα στοιχεία σας. Αυτό οφείλεται στο γεγονός ότι, ακόμη και αν έχετε κάποιο έλεγχο πάνω στο τι είναι η έξοδος από το σύστημα καταγραφής, έχετε μόνο τον έλεγχο στο συγκεκριμένο επίπεδο log, το οποίο είναι συνήθως ένα ολόκληρο αρχείο πηγαίου κώδικα.

Αν θέλετε να προσθέσετε κώδικα σε μια υπάρχουσα μονάδα, θα πρέπει επίσης να ακολουθείτε την έξοδο που κάθε άλλος προγραμματιστής έχει βρει ενδιαφέρουσα. Μπορείτε να διαπιστώσετε ότι, προκειμένου να πάρετε τις λίγες πληροφορίες που χρειάζεστε, μπορεί να χρειαστείτε να εντρυφήσετε μέσα από την τεράστια ποσότητα μηνυμάτων που προέρχονται από ξένα μηνύματα που δεν παρουσιάζουν κανένα ενδιαφέρον για εσάς. Μπορεί να αναγκαστείτε να αποθηκεύσετε τεράστια αρχεία καταγραφής στο δίσκο και να τα επεξεργαστείτε με σκοπό να κάνετε την δουλειά σας.

Δεδομένου ότι δεν υπάρχουν εγγυήσεις στον ns-3 σχετικά με τη σταθερότητα της εξόδου NS_LOG, μπορείτε επίσης να ανακαλύψετε ότι τα κομμάτια της παραγωγής εξόδου τα οποία είναι για εξαφάνιση ή για αλλαγή μεταξύ διαφορετικών εκδόσεων. Αν εξαρτάστε στη δομή της παραγωγής, μπορείτε να βρείτε και άλλα μηνύματα που προστίθενται ή διαγράφονται τα οποία μπορεί να επηρεάσουν την ανάλυση του κώδικα.

Τέλος, η έξοδος NS_LOG είναι διαθέσιμη μόνο σε εκδόσεις εντοπισμού σφαλμάτων, δεν μπορείτε να πάρετε συνδεθείτε εξόδου από βελτιστοποιημένη χτίζει, που τρέχουν περίπου δύο φορές πιο γρήγορα. Στηριζόμενη στην NS_LOG επιβάλλει ποινή απόδοσης.

Για τους λόγους αυτούς, θεωρούμε τις εκτυπώσεις στο std::cout και τα μηνύματα NS_LOG να είναι γρήγορα και απλοί τρόποι για να πάρετε περισσότερες πληροφορίες από τον ns-3, αλλά δεν είναι κατάλληλο για σοβαρή δουλειά.

Είναι επιθυμητό να έχουμε μια σταθερή εγκατάσταση, χρησιμοποιώντας σταθερά APIs που επιτρέπουν σε κάποιον να φτάσει στον πυρήνα του συστήματος και να πάρει μόνο τις πληροφορίες που απαιτούνται. Είναι επιθυμητό να είναι σε θέση να το κάνει αυτό χωρίς να χρειάζεται να αλλάξει και να μεταγλωττίσει ξανά τον πυρήνα του συστήματος. Ακόμα καλύτερα θα είναι ένα σύστημα που κοινοποίησε τον κωδικό χρήστη, όταν ένα στοιχείο του ενδιαφέροντος αλλάξει ή μια ενδιαφέρουσα εκδήλωση έγινε έτσι ο χρήστης θα έχει αυτά που του χρειάζονται.

Το σύστημα εντοπισμού του ns-3 έχει σχεδιαστεί για να λειτουργεί προς αυτή την κατεύθυνση και είναι καλά ενσωματωμένο με το Attribute και Config υποσυστήματα που επιτρέπουν την σχετικά απλή χρήση σεναρίων.

Επισκόπηση

Το σύστημα ανίχνευσης του ns-3 είναι χτισμένο στις έννοιες των ανεξάρτητων απο τον εντοπισμό πηγών και τον εντοπισμό συλλεκτών, μαζί με ένα ενιαίο μηχανισμό για τη σύνδεση πηγών σε συλλέκτες.

Πηγές εντοπισμού είναι οντότητες που μπορούν να σηματοδοτήσουν τα γεγονότα που συμβαίνουν σε μια προσομοίωση και να παρέχουν πρόσβαση σε ενδιαφέροντα υποκείμενα δεδομένα. Για παράδειγμα, μια πηγή ίχνους θα μπορούσε να υποδείξει πότε ένα πακέτο παραλαμβάνεται από μια συσκευή δικτύου και να παρέχει πρόσβαση στα περιεχόμενα του πακέτου για τους ενδιαφερόμενους συλλέκτες εντοπισμού. Μια πηγή ίχνους μπορεί επίσης να αναφέρει πότε μια ενδιαφέρουσα αλλαγή κατάστασης συμβαίνει σε ένα μοντέλο. Για παράδειγμα, το παράθυρο συμφόρησης του μοντέλου TCP είναι πρώτος υποψήφιος για μια πηγή ίχνους. Κάθε φορά που αλλάζει το παράθυρο συμφόρησης που είναι συνδεδεμένο με συλλέκτες ίχνους ενημερώνεται με την παλαιά και νέα τιμή.

Οι πηγές εντοπισμού δεν είναι χρήσιμες από μόνες τους. Θα πρέπει να συνδεθούν με άλλα κομμάτια του κώδικα που κάνουν πραγματικά κάτι χρήσιμο με τις πληροφορίες που παρέχονται από την πηγή. Οι οντότητες που καταναλώνουν πληροφορίες ίχνους ονομάζονται συλλέκτες ίχνους. Οι πηγές εντοπισμού είναι γεννήτριες των δεδομένων και οι συλλέκτες ίχνους είναι οι καταναλωτές. Αυτή η ρητή κατανομή επιτρέπει για ένα μεγάλο αριθμό πηγών ίχνους να είναι διάσπαρτα σε όλο το σύστημα σε χώρους που συγγραφείς μοντέλων πιστεύουν ότι μπορεί να είναι χρήσιμο. Η τοποθέτηση πηγών ίχνους εισάγει μία πολύ μικρή γενικά εκτέλεση.

Μπορεί να υπάρχουν μηδέν ή περισσότεροι καταναλωτές από ίχνη γεγονότων που παράγεται από μια πηγή ίχνους. Κάποιος μπορεί να σκεφτεί μια πηγή ίχνους, ως ένα είδος point-to-multipoint σύνδεσης πληροφοριών. Ο κώδικάς σας ψάχνει για ίχνη γεγονότων από ένα συγκεκριμένο κομμάτι του πηγαίου κώδικα, θα μπορούσε ευτυχώς να συνυπάρχει με άλλους κώδικες να κάνει κάτι εντελώς διαφορετικό από την ίδια πληροφορία.

Αν ένας χρήστης δεν συνδέσει ένα συλλέκτη ίχνους σε μια από αυτές τις πηγές, τίποτα δεν θα υπάρχει στην έξοδο. Με τη χρήση του συστήματος εντοπισμού, τόσο εσείς όσο και άλλοι άνθρωποι είναι συνδεδεμένοι με την ίδια πηγή ίχνους παίρνουν ακριβώς αυτό που θέλουν και μόνο ό,τι θέλουν έξω από το σύστημα. Ούτε εσείς επηρεάζετε κάθε άλλο χρήστη αλλάζοντας ποιά πληροφορία είναι έξοδος από το σύστημα. Αν συμβεί να προσθέσετε μια πηγή ίχνους, το έργο σας ως καλός πολίτης ανοικτού κώδικα μπορεί να επιτρέψει σε άλλους χρήστες για την παροχή νέων υπηρεσιών κοινής ωφελείας που είναι ίσως πολύ χρήσιμο συνολικά, χωρίς να κάνει οποιεσδήποτε αλλαγές στον πυρήνα ns-3.

Απλό Παράδειγμα

Ας πάρουμε μερικά λεπτά και βήμα βήμα ακολουθήστε ένα απλό παράδειγμα εντοπισμού. Θα χρειαστείτε την Επανάκληση να καταλάβετε τι συμβαίνει στο παράδειγμα, οπότε πρέπει να πάρετε μια μικρή παράκαμψη αμέσως.

Επανάκληση

Ο στόχος του συστήματος επανάκλησης ns-3 είναι να επιτρέψει σε ένα κομμάτι του κώδικα να καλέσει μια συνάρτηση (ή μέθοδο σε C++), χωρίς καμία συγκεκριμένη μεταξύ των μονάδων εξάρτηση. Αυτό σημαίνει ότι, τελικά, θα πρέπει να έχετε κάποιο είδος εμμεσότητας – αντιμετωπίζεις τη διεύθυνση της κληθήσας συνάρτησης ως μια μεταβλητή. Η μεταβλητή αυτή ονομάζεται μεταβλητή(pointer-to-function). Η σχέση μεταξύ της συνάρτησης και της μεταβλητής(pointer-to-function) πραγματικά δεν διαφέρει από αυτήν του αντικειμένου(object) και του δείκτη προς το αντικείμενο(pointer-to-object).

Στη C, το κανονικό παράδειγμα της μεταβλητής(pointer-to-function) δείκτη-συνάρτησης είναι ένας δείκτης-σε-συνάρτηση-επιστρέφοντας-ακέραιο (PFI - pointer-to-function-returning-integer). Λαμβάνοντας μία παράμετρο int, όπως,

int (*pfi)(int arg) = 0;

(Αλλά διαβάστε το C++-FAQ Section 33 πριν συντάξετε κώδικα σαν αυτόν!) Αυτό που μπορείτε να πάρετε από αυτό είναι μια μεταβλητή που ονομάζεται απλά pfi που έχει την τιμή 0. Αν θέλετε να αρχικοποιήσετε αυτό το δείκτη σε κάτι σημαντικό, θα πρέπει να έχετε μια συνάρτηση με μια υπογραφή που να ταιριάζουν. Σε αυτήν την περίπτωση, θα μπορείτε να προσφέρετε μια συνάρτηση που μοιάζει με

int MyFunction (int arg) {}

Εάν έχετε αυτό το στόχο, μπορείτε να προετοιμάσετε τη μεταβλητή στο σημείο της συνάρτησή σας

pfi = MyFunction;

Στη συνέχεια μπορείτε να καλέσετε την MyFunction έμμεσα χρησιμοποιώντας την πιο υποβλητική μορφή της κλήσης

int result = (*pfi) (1234);

Αυτό είναι ενδεικτικό, απο τη στιγμη που διαφοροποιήστε στο δείκτη συνάρτησης ακριβώς όπως θα κάνατε την διαφορά στον κάθε δείκτη. Συνήθως, όμως, οι άνθρωποι θα επωφεληθούν από το γεγονός ότι ο μεταγλωττιστής(compiler) ξέρει τι συμβαίνει και θα χρησιμοποιήσει μόνο μια μικρότερη μορφή

int result = pfi (1234);

Αυτό μοιάζει σαν να καλείτε μια συνάρτηση που ονομάζεται pfi, αλλά ο μεταγλωττιστής(compiler) είναι αρκετά έξυπνος για να ξέρει να καλέσει μέσω της μεταβλητής pfi έμμεσα τη συνάρτηση MyFunction.

Θεωρητικά, αυτό είναι σχεδόν ακριβώς πώς λειτουργεί το σύστημα εντοπισμού. Βασικά, ένα ίχνος καταβόθρας είναι μια επανάκληση. Όταν ένα ίχνος καταβόθρας(trace sink) εκφράζει ενδιαφέρον λαμβάνοντας γεγονότα ίχνους, η ίδια προσθέτει ως επανάκληση σε έναν κατάλογο Επανακλήσεων εσωτερικά διατηρημένα από την πηγή ίχνους. Όταν μια ενδιαφέρουσα εκδήλωση συμβαίνει, η πηγή ίχνους επικαλείται τον χειριστή της operator(...) παρέχοντας μηδέν ή περισσότερα ορίσματα. Ο χειριστής operator(...) περιπλανιέται τελικά κάτω στο σύστημα και κάνει κάτι σημαντικό όπως η έμμεση κλήση που μόλις είδατε, παρέχοντας μηδέν ή περισσότερες παραμέτρους, έτσι όπως ακριβώς και η κλήση για pfi παραπάνω περάσει μία παράμετρο για την συνάρτηση στόχο MyFunction.

Η σημαντική διαφορά ότι το σύστημα εντοπισμού προσθέτει, είναι ότι για κάθε πηγή ίχνους υπάρχει μια εσωτερική λίστα Επανακλήσεων. Αντί να κάνουμε απλώς μια έμμεση κλήση, μια πηγή ίχνους μπορεί να καλέσει πολλές Επανακλήσεις. Όταν μία καταβόθρα ίχνους εκφράζει το ενδιαφέρον σε ειδοποιήσεις από μια πηγή ίχνους, ουσιαστικά φροντίζει μόνο να προσθέσει τη δική του συνάρτηση στη λίστα επανάκλησης.

Εάν ενδιαφέρεστε για περισσότερες λεπτομέρειες σχετικά με το πώς είναι πραγματικά τοποθετημένα στον ns-3, μη διστάσετε να μελετήσετε την ενότητα επανάκλησης του Εγχειριδίου(Manual) ns-3.

Οδηγός Διασύνδεσης: fourth.cc

Έχουμε προβάλει κάποιο κώδικα για να εφαρμόσουμε αυτό που είναι πραγματικά το πιο απλό παράδειγμα του εντοπισμού που μπορεί να συναρμολογηθεί. Μπορείτε να βρείτε τον κώδικα σε αυτό τον κατάλογο ως fourth.cc. Ας δούμε μέσα από αυτό

/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation;
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include "ns3/object.h"
#include "ns3/uinteger.h"
#include "ns3/traced-value.h"
#include "ns3/trace-source-accessor.h"

#include <iostream>

using namespace ns3;

Το μεγαλύτερο μέρος αυτού του κώδικα θα πρέπει να είναι αρκετά γνωστό σε εσάς. Όπως αναφέρθηκε παραπάνω, το σύστημα ίχνους κάνει βαριά χρήση του Αντικειμένου και του Χαρακτηριστικού στα συστήματα( Object and Attribute systems), έτσι θα πρέπει να τα συμπεριλάβετε. Τα δύο πρώτα περιεχόμενα φέρουν πάνω σε δηλώσεις για τα συστήματα αυτά ρητά. Θα μπορούσατε να χρησιμοποιήσετε τον πυρήνα κεφαλίδα ενότητας για να πάρετε τα πάντα με τη μία, αλλά κάνουμε τα περιεχόμενα ρητά εδώ για να επεξηγήσουμε πόσο πραγματικά απλό είναι αυτό όλο.

Το αρχείο, traced-value.h φέρνει τις απαιτούμενες δηλώσεις για τον εντοπισμό των δεδομένων που υπακούει στη σημασιολογική αξία. Σε γενικές γραμμές, η σημασιολογική αξία ακριβώς σημαίνει ότι μπορείτε να περάσετε το ίδιο το αντικείμενο γύρω, αντί να μεταθέσετε τη διεύθυνση του αντικειμένου. Αυτό σημαίνει πραγματικά ότι θα είστε σε θέση να εντοπίζετε όλες τις αλλαγές που γίνονται σε ένα TracedValue σε ένα πολύ απλό τρόπο.

Δεδομένου ότι το σύστημα ανίχνευσης είναι ενσωματωμένο με Χαρακτηριστικά, και τα Χαρακτηριστικά δουλεύουν με Αντικείμενα, πρέπει να υπάρχει ένας ns-3 Object για την πηγή ίχνους που υπάρχει. Το επόμενο απόσπασμα κώδικα δηλώνει και ορίζει ένα απλό Αντικείμενο που μπορούμε να εργαστούμε.

class MyObject : public Object
{
public:
  static TypeId GetTypeId (void)
  {
    static TypeId tid = TypeId ("MyObject")
      .SetParent (Object::GetTypeId ())
      .AddConstructor<MyObject> ()
      .AddTraceSource ("MyInteger",
                       "An integer value to trace.",
                       MakeTraceSourceAccessor (&MyObject::m_myInt),
                       "ns3::Traced::Value::Int32Callback")
      ;
    return tid;
  }

  MyObject () {}
  TracedValue<int32_t> m_myInt;
};

Οι δύο σημαντικές γραμμές κώδικα, παραπάνω, σε σχέση με τον εντοπισμό είναι το .AddTraceSource και η TracedValue δήλωση m_myInt.

Το .AddTraceSource παρέχει τα “άγκιστρα” που χρησιμοποιούνται για τη σύνδεση της πηγής ίχνους με τον έξω κόσμο μέσω του συστήματος Config. Το πρώτο όρισμα είναι ένα όνομα για αυτήν την πηγή ίχνους, το οποίο το καθιστά ορατό στο σύστημα Config. Το δεύτερο όρισμα είναι μία βοήθεια απο string. Τώρα κοιτάξτε τον τρίτο όρισμα, στην πραγματικότητα εστίασε στο όρισμα από το τρίτο όρισμα: &MyObject::m_myInt. Αυτή είναι η TracedValue η οποία προστίθεται στην κλάση(class), είναι πάντα ένα μέλος κλάσης δεδομένων. (Το τελευταίο όρισμα είναι το όνομα typedef για τον τύπο TracedValue, ως συμβολοσειρά(string). Αυτό χρησιμοποιείται για να δημιουργήσετε τεκμηρίωση για τη σωστή υπογραφή Επανάκλησης συνάρτησης, η οποία είναι χρήσιμη ειδικά για πιο γενικούς τύπους Επανακλήσεων.)

Η δήλωση TracedValue<> παρέχει την υποδομή που οδηγεί την διαδικασία επανάκλησης. Κάθε φορά που η υποκείμενη αξία είναι αλλαγμένη ο μηχανισμός TracedValue θα παρέχει τόσο την παλαιά όσο και την νέα τιμή της μεταβλητής, σε αυτή την περίπτωση μία αξία int32_t. Η συνάρτηση της καταβόθρας ίχνους για το TracedValue θα χρειαστεί την υπογραφή

void (* TracedValueCallback)(const int32_t oldValue, const int32_t newValue);

Όλες οι καταβόθρες ίχνους συνδέοντας αυτή την πηγή ίχνους πρέπει να έχουν αυτή την υπογραφή. Θα συζητήσουμε παρακάτω πώς μπορείτε να προσδιορίσετε την απαιτούμενη υπογραφή επανάκλησης σε άλλες περιπτώσεις.

Συνεχίζοντας με το fourth.cc βλέπουμε

void
IntTrace (int32_t oldValue, int32_t newValue)
{
  std::cout << "Traced " << oldValue << " to " << newValue << std::endl;
}

Αυτός είναι ο ορισμός μιάς καταβόθρας ίχνους. Αντιστοιχεί άμεσα με την υπογραφή της συνάρτησης επανάκλησης. Μόλις συνδεθεί, η συνάρτηση αυτή θα καλείται όταν το TracedValue αλλάζει.

Έχουμε δει τώρα την πηγή ίχνους και την καταβόθρα ίχνους. Αυτό που απομένει είναι ο κώδικας να συνδέσει την πηγή στην καταβόθρα, η οποία συμβαίνει στο main

int
main (int argc, char *argv[])
{
  Ptr<MyObject> myObject = CreateObject<MyObject> ();
  myObject->TraceConnectWithoutContext ("MyInteger", MakeCallback(&IntTrace));

  myObject->m_myInt = 1234;
}

Εδώ εμείς πρώτα δημιουργούμε το παράδειγμα MyObject στο οποίο η πηγή ίχνους υπάρχει.

Το επόμενο βήμα, το TraceConnectWithoutContext, αποτελεί τη σύνδεση μεταξύ της πηγής ίχνους και της καταβόθρας ίχνους. Το πρώτο όρισμα είναι ακριβώς το όνομα της πηγής ίχνους “MyInteger”, είδαμε παραπάνω. Παρατήρησε την συνάρτηση πρότυπο MakeCallback. Αυτή η συνάρτηση κάνει τη λειτουργία που απαιτείται για να δημιουργήσει το υποκείμενο Αντικείμενο επανάκλησης ns-3 και το συνδέουν με την συνάρτηση IntTrace. Το TraceConnect κάνει την σχέση μεταξύ της παρεχόμενης συνάρτησής σας και υπερφορτωμένα operator() στην εντοπισμένη μεταβλητή που αναφέρεται από το Χαρακτηριστικό “MyInteger”. Μετά από αυτή την ένωση, η πηγή ίχνους θα πάρει “φωτιά” στην παρεχόμενη συνάρτηση επανάκλησης.

Ο κώδικας για να κάνει όλα αυτά να συμβούν είναι, φυσικά, μη-τετριμμένο, αλλά η ουσία είναι ότι οργανώνετε για κάτι που μοιάζει ακριβώς όπως το παράδειγμα παραπάνω pfi() να κληθεί από την πηγή ίχνους. Η δήλωση του TracedValue<int32_t> m_myInt; στο ίδιο το Αντικείμενο εκτελεί τη λειτουργία που απαιτείται για την παροχή των υπερφορτωμένων τελεστών ανάθεσης που θα χρησιμοποιήσει ο operator() για να επικαλεστεί πραγματικά την επανάκληση με τις επιθυμητές παραμέτρους. Το .AddTraceSource εκτελεί τη λειτουργία για να συνδέσετε την Επανάκληση στο σύστημα Config, και το `` TraceConnectWithoutContext`` εκτελεί τη λειτουργία για να συνδέσετε τη συνάρτησή σας με την πηγή ίχνους, η οποία καθορίζεται με βάση το όνομα του Χαρακτηριστικού.

Ας αγνοήσουμε για λίγο το κομμάτι σχετικά με το περιεχόμενο.

Τέλος, η γραμμή αποδίδοντας μία αξία σε m_myInt

myObject->m_myInt = 1234;

θα πρέπει να ερμηνευθεί ως επίκληση του operator= για τη μεταβλητή μέλους m_myInt με τον ακέραιο 1234 πέρασε ως μία παράμετρος.

Από τη στιγμή που το m_myInt είναι ένα TracedValue, ο φορέας αυτός ορίζεται να εκτελέσει μία επανάκληση που επιστρέφει κενό και παίρνει δύο ακέραιες τιμές ως παραμέτρους — μια παλιά τιμή και μια νέα τιμή για τον εν λόγω ακέραιο. Αυτή είναι ακριβώς η υπογραφή συνάρτηση για την συνάρτηση επανάκλησης που παρείχαμε — IntTrace.

Για να συνοψίσουμε, μια πηγή ίχνους είναι, στην ουσία, μια μεταβλητή που κρατά μια λίστα επανακλήσεων(callbacks). Μία καταβόθρα ίχνους είναι μια συνάρτηση που χρησιμοποιείται ως στόχος της επανάκλησης. Τα συστήματα πληροφόρησης τύπου Χαρακτηριστικό και Αντικείμενο χρησιμοποιούνται για να παρέχουν έναν τρόπο για να συνδέσετε πηγές ίχνους για τον εντοπισμό καταβόθρων. Η ενέργεια “χτυπήματος” μιάς πηγής ίχνους εκτελείται σε ένα φορέα στην πηγή ίχνους που εκτοξεύει επανακλήσεις. Αυτά τα αποτελέσματα επανακλήσεων στη καταβόθρα ίχνους τα οποία καταχωρούν ενδιαφέρον στην πηγή καλούνται με τις παραμέτρους που παρέχονται από την πηγή.

Αν τώρα οικοδομήσουμε και να τρέξουμε αυτό το παράδειγμα,

$ ./waf --run fourth

θα δείτε την έξοδο από την συνάρτηση IntTrace να εκτελεί το συντομότερο δυνατό η πηγή ίχνους χτύπημα:

Traced 0 to 1234

Όταν έχουμε την εκτέλεση του κώδικα, myObject->m_myInt = 1234;, η πηγή ίχνους εκτελείται γρήγορα και παρέχει αυτόματα τις τιμές πριν και μετά στη καταβόθρα ίχνους. Η συνάρτηση IntTrace στη συνέχεια εκτύπωσε αυτό στην κανονική έξοδο.

Σύνδεση με Config

Η κλήση του TraceConnectWithoutContext φαίνεται στο παραπάνω απλό παράδειγμα το οποίο χρησιμοποιείται στην πραγματικότητα πολύ σπάνια στο σύστημα. Πιο τυπικά, το υποσύστημα Config χρησιμοποιείται για να επιλέξετε μια πηγή ίχνους στο σύστημα, χρησιμοποιώντας αυτό που ονομάζεται Config path. Είδαμε ένα παράδειγμα απο αυτό στην προηγούμενη ενότητα, όπου γαντζώθηκε στην “CourseChange” εκδήλωση, όταν πειραματιζόμασταν με το third.cc.

Υπενθυμίζουμε ότι ορίσαμε μία καταβόθρα ίχνους για να εκτυπώσουμε πληροφορίες και να αλλάξει πληροφορίες απο την κινητικότητα των μοντέλων απο την προσομοίωσή μας. Θα πρέπει τώρα να είναι πολύ πιο σαφές σε σας τι κάνει αυτή η συνάρτηση

void
CourseChange (std::string context, Ptr<const MobilityModel> model)
{
  Vector position = model->GetPosition ();
  NS_LOG_UNCOND (context <<
    " x = " << position.x << ", y = " << position.y);
}

Όταν συνδέσαμε την πηγή ίχνους “CourseChange” στην παραπάνω καταβόθρα ίχνους, χρησιμοποιήσαμε ένα Config path για να προσδιορίσουμε την πηγή όταν κανονίσαμε μια σύνδεση μεταξύ της προ-καθορισμένης πηγής ίχνους και της νέας καταβόθρας ίχνους

std::ostringstream oss;
oss << "/NodeList/"
    << wifiStaNodes.Get (nWifi - 1)->GetId ()
    << "/$ns3::MobilityModel/CourseChange";

Config::Connect (oss.str (), MakeCallback (&CourseChange));

Ας προσπαθήσουμε να καταλάβουμε του τι θεωρείται μερικές φορές σχετικά μυστηριώδης κώδικας. Για τους σκοπούς της συζήτησης, ας υποθέσουμε ότι ο αριθμός κόμβου(Node) που επιστρέφεται από το GetId () είναι “7”. Σε αυτήν την περίπτωση, η διαδρομή ανωτέρω αποδεικνύεται ότι είναι

"/NodeList/7/$ns3::MobilityModel/CourseChange"

Το τελευταίο τμήμα της διαδρομής config path πρέπει να είναι Attribute ενός Object. Στην πραγματικότητα, αν είχατε πρακτικά ένα δείκτη στο Object που έχει το “CourseChange” Attribute, θα μπορούσατε να γράψετε αυτό, ακριβώς όπως κάναμε στο προηγούμενο παράδειγμα. Τυπικά αποθηκεύουμε δείκτες στους (κόμβους) Nodes σε ένα NodeContainer. Στο παράδειγμα third.cc, οι κόμβοι του ενδιαφέροντος είναι αποθηκευμένοι στο NodeContainer wifiStaNodes. Στην πραγματικότητα, βάζοντας τη διαδρομή μαζί, χρησιμοποιήσαμε αυτό το container για να πάρει ένα Ptr<Node> που είχαμε συνηθίσει να λέμε GetId(). Θα μπορούσαμε να χρησιμοποιήσουμε αυτό το Ptr<Node> να καλέσει άμεσα μια μέθοδο Σύνδεσης

Ptr<Object> theObject = wifiStaNodes.Get (nWifi - 1);
theObject->TraceConnectWithoutContext ("CourseChange", MakeCallback (&CourseChange));

Στο παράδειγμα third.cc, θέλαμε πραγματικά ένα πρόσθετο “περιεχόμενο” για να παραδοθεί μαζί με τις παραμέτρους Επανάκλησης (η οποία θα εξηγηθεί παρακάτω) έτσι θα μπορούσαμε να χρησιμοποιήσουμε πραγματικά τον ακόλουθο ισοδύναμο κώδικα

Ptr<Object> theObject = wifiStaNodes.Get (nWifi - 1);
theObject->TraceConnect ("CourseChange", MakeCallback (&CourseChange));

Αυτό μας δίνει ότι ο εσωτερικός κώδικας για Config::ConnectWithoutContext και Config::Connect βρήκε πραγματικά μια Ptr<Object> και κάλεσε την κατάλληλη μέθοδο TraceConnect στο χαμηλότερο επίπεδο .

Οι συναρτήσεις του Config λαμβάνουν μια διαδρομή που αντιπροσωπεύει μια αλυσίδα από δείκτες Object. Κάθε τμήμα του μονοπατιού απαντάει σε ένα Χαρακτηριστικό Αντικείμενο. Το τελευταίο τμήμα είναι το Χαρακτηριστικό του ενδιαφέροντος, και πριν από τα τμήματα πρέπει να είναι τυπωμένα να περιέχουν ή να βρούν Αντικείμενα. Ο κώδικας Config αναλύει και “περπατάει” αυτό το μονοπάτι μέχρι να φτάσει στο τελικό τμήμα της διαδρομής. Στη συνέχεια ερμηνεύει το τελευταίο τμήμα ως Attribute στο τελευταίο Αντικείμενο που βρέθηκε, περπατώντας τη διαδρομή. Οι συναρτήσεις Config τότε καλούν την κατάλληλη TraceConnect ή μέθοδο TraceConnectWithoutContext για το τελικό Αντικείμενο. Ας δούμε τι θα συμβεί σε λίγο με περισσότερες λεπτομέρειες, όταν η παραπάνω διαδρομή περπάτησε.

Η κορυφαία χαρακτήρας “/” στη διαδρομή αναφέρεται σε ένα λεγόμενο namespace. Μία από τις προκαθορισμένες namespaces στο σύστημα config είναι “NodeList”, η οποία είναι μια λίστα με όλους τους κόμβους στην προσομοίωση. Τα στοιχεία στην λίστα αναφέρονται στους δείκτες της λίστας, έτσι “/ NodeList / 7” αναφέρεται στον όγδοο Κόμβο στη λίστα των κόμβων που δημιουργούνται κατά τη διάρκεια της προσομοίωσης (ανάκληση δεικτών ξεκινούν από το 0’). Αυτή η αναφορά είναι στην πραγματικότητα ένα ``Ptr<Node>` και έτσι είναι μια υποκατηγορία ενός ns3::Object.

Όπως περιγράφεται στην ενότητα Object Model της ns-3 Εγχειρίδιο(Manual), κάνουμε εκτεταμένη χρήση της συνάθροισης αντικειμένου. Αυτό μας επιτρέπει να σχηματίσουμε τη σύνδεση μεταξύ διαφορετικών Αντικειμένων χωρίς την οικοδόμηση ενός δέντρου πολύπλοκης κληρονομικότητας ή να προ-αποφασίσουμε ποια αντικείμενα θα είναι μέρος ενός Κόμβου. Κάθε Αντικείμενο σε ένα σύνολο μπορεί να επιτευχθεί από τα άλλα Αντικείμενα.

Στο παράδειγμά μας, το επόμενο τμήμα της διαδρομής που έχει περπατηθεί ξεκινά με το χαρακτήρα “$”. Αυτό δηλώνει στο σύστημα config ότι το τμήμα είναι το όνομα ενός τύπου Αντικειμένου, ώστε η κλήση GetObject θα πρέπει να ψάχνει για αυτό το είδος. Μας βγάζει ότι η MobilityHelper χρησιμοποιείται στο third.cc και κανονίζει στο σύνολο ή συνδέει, ένα μοντέλο κινητικότητας σε κάθε ασύρματο Κόμβο Nodes. Όταν προσθέσετε το “$” ρωτάτε για ένα άλλο Αντικείμενο που έχει πιθανώς προηγουμένως αθροιστεί. Μπορείτε να σκεφτείτε αυτό ως εναλλαγή δεικτών από την αρχική Ptr<Node>, όπως καθορίζεται από το “/NodeList/7” στο ίδιο συνδεδεμένο μοντέλο κινητικότητάς του — το οποίο είναι τύπου ns3::MobilityModel. Εάν είστε εξοικειωμένοι με το GetObject, ζητήσαμε από το σύστημα να κάνετε τα εξής

Ptr<MobilityModel> mobilityModel = node->GetObject<MobilityModel> ()

Είμαστε τώρα στο τελευταίο Αντικείμενο στο μονοπάτι, έτσι ώστε να στρέψουμε την προσοχή μας προς τα Χαρακτηριστικά αυτού του Αντικειμένου. Η κλάση MobilityModel ορίζει ένα Χαρακτηριστικό που ονομάζεται “CourseChange”. Μπορείτε να δείτε αυτό κοιτάζοντας τον πηγαίο κώδικα σε src/mobility/model/mobility-model.cc και αναζητώντας για “CourseChange” στο αγαπημένο σας επεξεργαστή(editor). Θα πρέπει να βρείτε

.AddTraceSource ("CourseChange",
                 "The value of the position and/or velocity vector changed",
                 MakeTraceSourceAccessor (&MobilityModel::m_courseChangeTrace),
                 "ns3::MobilityModel::CourseChangeCallback")

το οποίο θα έπρεπε να φαίνεται εξοικειωμένο σε αυτό το σημείο.

Αν κοιτάξετε για την αντίστοιχη δήλωση της υποκείμενης εντοπισμένης μεταβλητής στο mobility-model.h θα βρείτε

TracedCallback<Ptr<const MobilityModel> > m_courseChangeTrace;

Ο τύπος της δήλωσης TracedCallback προσδιορίζει m_courseChangeTrace ως μία ειδική λίστα των Επανακλήσεων που μπορεί να συνδεθεί με τις συναρτήσεις Config που περιγράφονται παραπάνω. Το typedef για την υπογραφή συνάρτησης επανάκλησης είναι επίσης ορισμένο στο αρχείο κεφαλίδας

typedef void (* CourseChangeCallback)(Ptr<const MobilityModel> * model);

Η κλάση MobilityModel έχει σχεδιαστεί για να είναι μια βασική κλάση που παρέχει μια κοινή διεπαφή για όλες τις συγκεκριμένες υποκατηγορίες. Εάν κάνετε αναζήτηση προς τα κάτω στο τέλος του αρχείου, θα δείτε μια μέθοδο που λέγεται NotifyCourseChange()

void
MobilityModel::NotifyCourseChange (void) const
{
  m_courseChangeTrace(this);
}

Οι παραγόμενες κλάσεις θα καλέσουν σε αυτή τη μέθοδο κάθε φορά που κάνουν μια αλλαγή πορείας για την υποστήριξη ανίχνευσης. Η μέθοδος αυτή επικαλείται τον operator() σχετικά με το υποκείμενο m_courseChangeTrace, το οποίο, με τη σειρά του, επικαλείται το σύνολο των εγγεγραμμένων Επανακλήσεων, καλώντας όλες τις καταβόθρες ίχνους που έχουν εγγραφεί με ενδιαφέρον για την πηγή ίχνους καλώντας μία συνάρτηση Config.

Έτσι, στο παράδειγμα third.cc κοιτάξαμε, κάθε φορά που μια αλλαγή πορείας γίνεται σε μία απο τις εγκαταστημένες περιπτώσεις RandomWalk2dMobilityModel, θα υπάρχει μία κλήση NotifyCourseChange() που καλεί επάνω σε μία κλάση βάσης `` MobilityModel``. Όπως είδαμε παραπάνω, αυτό επικαλείται operator() στη m_courseChangeTrace, η οποία με τη σειρά της, καλεί οποιαδήποτε εγγεγραμμένη καταβόθρα ίχνους. Στο παράδειγμα, ο μοναδικός κώδικας, καταγράφοντας ένα ενδιαφέρον ήταν ο κώδικας που έδωσε το μονοπάτι Config. Ως εκ τούτου, η συνάρτηση CourseChange που γαντζώθηκε από τον αριθμό του κόμβου επτά θα είναι η μόνη που ονομάζεται Επανάκληση.

Το τελευταίο κομμάτι του παζλ είναι το “περιεχόμενο”. Υπενθυμίζουμε ότι είδαμε μια έξοδο που αναζητά κάτι σαν το παρακάτω από το third.cc

/NodeList/7/$ns3::MobilityModel/CourseChange x = 7.27897, y =
2.22677

Το πρώτο μέρος της εξόδου είναι το περιεχόμενο. Είναι απλά η διαδρομή μέσω της οποίας ο κώδικας config βρίσκεται στη πηγή ίχνους. Στην περίπτωση που εμείς κοιτάζαμε ότι μπορεί να υπάρχει οποιοσδήποτε αριθμός των πηγών ίχνους στο σύστημα απαντά σε οποιονδήποτε αριθμό των κόμβων με τα μοντέλα κινητικότητας. Πρέπει να υπάρχει κάποιος τρόπος να προσδιορίσει ποια πηγή ίχνους είναι στην πραγματικότητα αυτός που έβαλε φωτιά στην Επανάκληση. Ο εύκολος τρόπος είναι να συνδέσετε με Config::Connect, αντί για Config::ConnectWithoutContext

Βρίσκοντας Πηγές

Το πρώτο ερώτημα που έρχεται αναπόφευκτα για τους νέους χρήστες του συστήματος Ανίχνευσης είναι, “Εντάξει, ξέρω ότι πρέπει να υπάρχουν πηγές ίχνους στον πυρήνα προσομοίωσης, αλλά πώς μπορώ να μάθω τι ίχνους πηγές είναι διαθέσιμες για εμένα;”

Το δεύτερο ερώτημα είναι, “Εντάξει, βρήκα μια πηγή ίχνους, πώς μπορώ να καταλάβω το μονοπάτι Config για να χρησιμοποιήσω όταν συνδεθώ σε αυτό;”

Το τρίτο ερώτημα είναι, “Εντάξει, βρήκα μια πηγή ίχνους και το μονοπάτι Config, πώς μπορώ να καταλάβω ποιο είναι το είδος επιστροφής και ποια πρέπει να είναι τα επίσημα ορίσματα της συνάρτησης επανάκλησής μου;”

Το τέταρτο ερώτημα είναι, “Εντάξει, εγώ τα πληκτρολόγησα όλα σωστά και πήρα αυτό το απίστευτα παράξενο μήνυμα σφάλματος, τι στον κόσμο σημαίνει αυτό;”

Θα τις απαντήσουμε κάθε μια από αυτές με τη σειρά.

Διαθέσιμες Πηγές

Εντάξει, ξέρω ότι πρέπει να υπάρχουν πηγές ίχνους στον πυρήνα προσομοίωσης, αλλά πώς μπορώ να μάθω τι πηγές ίχνους είναι διαθέσιμες για εμένα;

Η απάντηση στο πρώτο ερώτημα βρίσκεται στην τεκμηρίωση ns-3 API. Αν πάτε στην ιστοσελίδα του έργου, ns-3 project, θα βρείτε ένα σύνδεσμο στο “Documentation” στη γραμμή πλοήγησης. Αν επιλέξετε αυτό το σύνδεσμο, θα μεταφερθείτε στη σελίδα τεκμηρίωσης. Υπάρχει μια σύνδεση με τα “Latest Release” που θα σας μεταφέρει στην τεκμηρίωση για την τελευταία σταθερή έκδοση του ns-3. Εάν επιλέξετε το σύνδεσμο “API Documentation”, θα πρέπει να ληφθούν για την ns-3 API σελίδα τεκμηρίωσης.

Στην πλαϊνή γραμμή θα πρέπει να δείτε μια ιεραρχία που αρχίζει

  • ns-3
  • ns-3 Documentation
  • All TraceSources
  • All Attributes
  • All GlobalValues

Η λίστα που μας ενδιαφέρει είναι “All TraceSources”. Προχωρήστε και επιλέξτε το σύνδεσμο. Θα δείτε, ίσως όχι και τόσο έκπληκτα, μια λίστα με όλες τις πηγές ίχνους διαθέσιμο στον ns-3.

Ως παράδειγμα, μετακινηθείτε προς τα κάτω για να ns3::MobilityModel. Θα βρείτε μια καταχώρηση για

CourseChange: The value of the position and/or velocity vector changed

Θα πρέπει να αναγνωρίσουμε αυτό ως πηγή ίχνους που χρησιμοποιείται στο παράδειγμα third.cc. Θα είναι χρήσιμη η περιεργασία της λίστας.

Μονοπάτια Config

Εντάξει, βρήκα μια πηγή ίχνους, πώς μπορώ να καταλάβω την πορεία Config για να την χρησιμοποιήσω όταν συνδεθώ σε αυτή;

Εάν γνωρίζετε ποιο αντικείμενο σας ενδιαφέρει, το τμήμα “Detailed Description” για την κλάση θα εμφανίσει όλες τις διαθέσιμες πηγές ίχνους. Για παράδειγμα, ξεκινώντας από τη λίστα των “All TraceSources”, κάντε κλικ στο σύνδεσμο ns3::MobilityModel, το οποίο θα σας μεταφέρει στην τεκμηρίωση για την κλάση MobilityModel. Σχεδόν στην κορυφή της σελίδας είναι μία γραμμή σύντομης περιγραφής της κλάσης, που καταλήγει σε ένα σύνδεσμο “More...”. Κάντε κλικ σε αυτό το σύνδεσμο για να παρακάμψετε την περίληψη API και πηγαίνετε στο “Detailed Description” της κλάσης. Στο τέλος της περιγραφής, θα είναι (έως) τρεις λίστες:

  • Config Paths: μια λίστα των τυπικών διαδρομών Config για αυτή την κλάση.
  • Attributes μια λίστα με όλα τα χαρακτηριστικά που παρέχονται από αυτή την κλάση.
  • TraceSources: μια λίστα με όλα τα διαθέσιμα TraceSources από αυτή την κλάση.

Πρώτα θα συζητήσουμε τις διαδρομές Config.

Ας υποθέσουμε ότι έχετε μόλις βρεί την πηγή ίχνους “CourseChange” στη λίστα “All TraceSources” και θέλετε να βρείτε πώς να συνδεθείτε με αυτή. Ξέρετε ότι χρησιμοποιείτε (και πάλι, από το παράδειγμα third.cc) ένα ns3::RandomWalk2dMobilityModel. Έτσι, είτε να κάνετε κλικ στο όνομα της κλάσης στη λίστα “All TraceSources”, ή να βρείτε ns3::RandomWalk2dMobilityModel στην “Class List”. Είτε έτσι είτε αλλιώς θα πρέπει τώρα να εξετάσουμε τη σελίδα “ns3::RandomWalk2dMobilityModel Class Reference”.

Αν τώρα μετακινηθείτε προς τα κάτω στην ενότητα “Detailed Description”, μετά από τον ενοποιημένη λίστα των μεθόδων και χαρακτηριστικών κλάσης (ή απλά κάντε κλικ στο σύνδεσμο “More...” στο τέλος της κλάσης σύντομη περιγραφή στο πάνω μέρος της σελίδας) θα δείτε τη συνολική τεκμηρίωση για την κλάση. Συνεχίζοντας προς τα κάτω, βρείτε τη λίστα “Config Paths”:

Config Paths

ns3::RandomWalk2dMobilityModel is accessible through the following paths with Config::Set and Config::Connect:

  • “/NodeList/[i]/$ns3::MobilityModel/$ns3::RandomWalk2dMobilityModel”

Η τεκμηρίωση σας λέει πώς να φτάσετε στο Αντικείμενο RandomWalk2dMobilityModel. Συγκρίνετε τη σειρά πάνω από τη σειρά που πράγματι χρησιμοποιήσαμε στο παράδειγμα κώδικα

"/NodeList/7/$ns3::MobilityModel"

Η διαφορά αυτή οφείλεται στο γεγονός ότι οι δύο κλήσεις GetObject υπονοούν στη σειρά που βρίσκονται στην τεκμηρίωση. Η πρώτη, για $ns3::MobilityModel θα ζητήσει το σύνολα για την βάση της κλάσης. Η δεύτερη κλήση GetObject, για $ns3::RandomWalk2dMobilityModel, χρησιμοποιείται για να ρίχνει την βάση της κλάσης για την συγκεκριμένη κλάση εφαρμογής. Η τεκμηρίωση δείχνει τις δύο αυτές λειτουργίες για εσάς. Αποδεικνύεται ότι η πραγματική πηγή ίχνους που ψάχνετε βρίσκεται στη βάση της κλάσης.

Δείτε πιο κάτω στην ενότητα “Detailed Description” για τη λίστα των πηγών ίχνους. Θα βρείτε

No TraceSources are defined for this type.

TraceSources defined in parent class ``ns3::MobilityModel``

  • CourseChange: The value of the position and/or velocity vector changed.

    Callback signature: ns3::MobilityModel::CourseChangeCallback

Αυτό είναι ακριβώς ό,τι χρειάζεται να ξέρετε. Η πηγή ίχνους του ενδιαφέροντος βρίσκεται στο ns3::MobilityModel (που ξέρατε ούτως ή άλλως). Το ενδιαφέρον πράγμα σε αυτό το κομμάτι της τεκμηρίωσης του API λέει ότι δεν χρειάζονται επιπλέον απορρίμματα στο μονοπάτι config παραπάνω για να φτάσουμε στην συγκεκριμένη κλάση, δεδομένου ότι η πηγή ίχνους είναι στην πραγματικότητα στην βάση της κλάσης. Ως εκ τούτου, η πρόσθετη GetObject δεν απαιτείται και μπορείτε απλά να χρησιμοποιήσετε τη διαδρομή

"/NodeList/[i]/$ns3::MobilityModel"

που ταιριάζει απόλυτα με το παράδειγμα διαδρομή

"/NodeList/7/$ns3::MobilityModel"

Παρεμπιπτόντως, ένας άλλος τρόπος για να βρείτε το μονοπάτι Config είναι το grep γύρω στον κώδικα βάσης ns-3 για κάποιον που έχει ήδη καταλάβει. Πρέπει πάντα να προσπαθείτε να αντιγράψετε τον κώδικα εργασίας κάποιου άλλου πριν ξεκινήσετε να γράφετε τα δικά σας. Δοκιμάστε κάτι σαν:

$ find . -name '*.cc' | xargs grep CourseChange | grep Connect

και μπορείτε να βρείτε την απάντησή σας, μαζί με τον κώδικα εργασίας. Για παράδειγμα, στην περίπτωση αυτή, src/mobility/examples/main-random-topology.cc έχει κάτι που σας περιμένει να χρησιμοποιήσετε

Config::Connect ("/NodeList/*/$ns3::MobilityModel/CourseChange",
  MakeCallback (&CourseChange));

Θα επανέλθω σε αυτό το παράδειγμα σε λίγο.

Στίγματα Επανάκλησης

Εντάξει, βρήκα μια πηγή ίχνους και το μονοπάτι Config, πώς μπορώ να υπολογίσω ποιος είναι ο τύπος επιστροφής και τα επίσημα ορίσματα της συνάρτησης επανάκλησης μου;

Ο ευκολότερος τρόπος είναι να εξετάσετε την υπογραφή επανάκλησης typedef, η οποία δίνεται στη “Callback signature” της πηγής ίχνους στο “Detailed Description” της κλάσης, όπως φαίνεται παραπάνω.

Η επανάληψη καταχώρησης πηγής ίχνους της “CourseChange” από ns3::RandomWalk2dMobilityModel έχουμε:

  • CourseChange: The value of the position and/or velocity vector changed.

    Callback signature: ns3::MobilityModel::CourseChangeCallback

Η υπογραφή ή το στίγμα επανάκλησης δίνεται ως ένας σύνδεσμος με τη σχετική typedef, όπου βρίσκουμε

typedef void (* CourseChangeCallback)(const std::string context, Ptr<const MobilityModel> * model);

TracedCallback signature for course change notifications.

Αν η επανάκληση συνδέεται με τη χρήση ConnectWithoutContext παραλείψτε το context όρισμα από το στίγμα.

Parameters:

[in] context The context string supplied by the Trace source.
[in] model The MobilityModel which is changing course.

Όπως και παραπάνω, για να δείτε αυτό κατά τη χρήση grep γύρω στον κώδικα βάσης ns-3 για παράδειγμα. Το παραπάνω παράδειγμα, από src/mobility/examples/main-random-topology.cc, συνδέει την πηγή ίχνους “CourseChange” στην συνάρτηση CourseChange στο ίδιο αρχείο

static void
CourseChange (std::string context, Ptr<const MobilityModel> model)
{
  ...
}

Σημειώστε ότι αυτή η συνάρτηση:

  • Επιστρέφει void.

Αν, κατά τύχη, το στίγμα επανάκλησης δεν έχει τεκμηριωθεί, και δεν υπάρχουν παραδείγματα για εργασία, καθόρισε το σωστό στίγμα της συνάρτησης επανάκλησης που μπορεί να είναι, επίσης, δύσκολο πραγματικά να υπολογιστεί από τον πηγαίο κώδικα.

Πριν προβώντας σε μια περιδιάβαση του κώδικα, θα είμαι ευθύς και απλά να σας πω έναν απλό τρόπο για να το καταλάβετε: Η τιμή επιστροφής της επανάκλησής σας θα είναι πάντα void. Η επίσημη λίστα παραμέτρων για μια TracedCallback μπορεί να βρεθεί από τη λίστα της παραμέτρου προτύπου στη δήλωση. Υπενθυμίζεται ότι για το τρέχον παράδειγμά μας, αυτό είναι mobility-model.h, όπου βρήκαμε προηγουμένως

TracedCallback<Ptr<const MobilityModel> > m_courseChangeTrace;

Υπάρχει μια αντιστοιχία ένα-προς-ένα μεταξύ της λίστας προτύπου παραμέτρου στη δήλωση και τα επίσημα ορίσματα της συνάρτησης επανάκλησης. Εδώ, υπάρχει μια παράμετρος προτύπου, η οποία είναι ένα Ptr<const MobilityModel>. Αυτό σας λέει ότι χρειάζεστε μια συνάρτηση που επιστρέφει κενό και παίρνει ένα Ptr<const MobilityModel>. Για παράδειγμα

void
CourseChange (Ptr<const MobilityModel> model)
{
  ...
}

Αυτό είναι το μόνο που χρειάζεστε, αν θέλετε να Config::ConnectWithoutContext. Αν θέλετε ένα περιεχόμενο, θα πρέπει να Config::Connect και να χρησιμοποιήσετε μια συνάρτηση Επανάκλησης που παίρνει ένα περιεχόμενο string, τότε τα ορίσματα πρότυπα

void
CourseChange (const std::string context, Ptr<const MobilityModel> model)
{
  ...
}

Αν θέλετε να εξασφαλίσετε ότι η συνάρτησή σας CourseChangeCallback είναι ορατή μόνο σε τοπικό αρχείο σας, μπορείτε να προσθέσετε τη λέξη-κλειδί static και να καταλήξετε σε

static void
CourseChange (const std::string path, Ptr<const MobilityModel> model)
{
  ...
}

το οποίο είναι ακριβώς αυτό που χρησιμοποιείται στο παράδειγμα third.cc.

Εφαρμογή

Αυτή η ενότητα είναι εντελώς προαιρετική. Πρόκειται να είναι μια δύσκολη διαδρομή, ειδικά για όσους δεν είναι εξοικειωμένοι με τις λεπτομέρειες των προτύπων. Ωστόσο, εάν μπορείτε να πάρετε μέσα από αυτό, θα έχετε μια πολύ καλή λαβή για πολλά από τα ns-3 ιδιώματα χαμηλού επιπέδου.

Έτσι, και πάλι, ας υπολογίσουμε τι στίγμα της συνάρτησης επανάκλησης απαιτείται για την πηγή ίχνους “CourseChange”. Αυτό πρόκειται να είναι οδυνηρό, αλλά χρειάζεται να το κάνετε αυτό μια φορά. Μετά μπορείτε να πάρετε μέσα από αυτό, θα είστε σε θέση να εξετάσετε ένα TracedCallback και να το κατανοήσετε.

Το πρώτο πράγμα που πρέπει να εξετάσουμε είναι η δήλωση της πηγής ίχνους. Θυμηθείτε ότι αυτό είναι mobility-model.h, όπου έχουμε διαπιστώσει στο παρελθόν

TracedCallback<Ptr<const MobilityModel> > m_courseChangeTrace;

Αυτή η δήλωση είναι για ένα πρότυπο. Η παράμετρος πρότυπο είναι μέσα στη παρένθεση, ώστε πραγματικά ενδιαφερόμαστε να ανακαλύψουμε τι είναι το TracedCallback<>. Αν δεν έχετε απολύτως καμία ιδέα για το πού αυτό θα μπορούσε να βρεθεί, το grep είναι ο φίλος σου.

Πρόκειται πιθανώς να ασχοληθούμε για κάποιο είδος δήλωσης της πηγής ns-3, οπότε πρώτη αλλαγή στον κατάλογο src. Στη συνέχεια, γνωρίζουμε ότι η δήλωση αυτή θα πρέπει να είναι σε κάποιο είδος του αρχείου header, έτσι απλά grep για να το χρησιμοποιείτε:

$ find . -name '*.h' | xargs grep TracedCallback

Θα δείτε 303 γραμμές να πετούν από (σας δείχνω αυτό μέσω wc για να δείτε πόσο άσχημο ήταν). Αν και αυτά μπορεί να φαίνονται πολλά, αλλά αυτά δεν είναι πραγματικά πολλά. Απλά διοχέτευσε την έξοδο μέσω more και ξεκινήστε τη σάρωση μέσα από αυτό. Στην πρώτη σελίδα, θα δείτε κάποια πολύ ύποπτα πρότυπα - να αναζητούν πράγματα.

TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::TracedCallback ()
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::ConnectWithoutContext (c ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::Connect (const CallbackB ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::DisconnectWithoutContext ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::Disconnect (const Callba ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::operator() (void) const ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::operator() (T1 a1) const ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::operator() (T1 a1, T2 a2 ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::operator() (T1 a1, T2 a2 ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::operator() (T1 a1, T2 a2 ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::operator() (T1 a1, T2 a2 ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::operator() (T1 a1, T2 a2 ...
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::operator() (T1 a1, T2 a2 ...

Αποδεικνύεται ότι όλο αυτό προέρχεται από το αρχείο κεφαλίδας traced-callback.h που ακούγεται πολύ ελπιδοφόρα. Στη συνέχεια μπορείτε να ρίξετε μια ματιά στο mobility-model.h και να δούμε ότι υπάρχει μια γραμμή που επιβεβαιώνει αυτό το προαίσθημα

#include "ns3/traced-callback.h"

Φυσικά, θα μπορούσατε να έχετε πάει σε αυτό από την άλλη κατεύθυνση και να αρχίσετε κοιτάζοντας τα περιεχόμενα του mobility-model.h και παρατηρώντας το περιεχόμενο του traced-callback.h και να συμπεράνετε ότι αυτό πρέπει να είναι το αρχείο που θέλετε.

Σε κάθε περίπτωση, το επόμενο βήμα είναι να ρίξετε μια ματιά στο src/core/model/traced-callback.h στο αγαπημένο σας επεξεργαστή κειμένου για να δείτε τι συμβαίνει.

Θα δείτε ένα σχόλιο στην αρχή του αρχείου που θα πρέπει να είναι παρήγορο:

An ns3::TracedCallback has almost exactly the same API as a normal ns3::Callback but instead of forwarding calls to a single function (as an ns3::Callback normally does), it forwards calls to a chain of ns3::Callback.

Αυτό θα πρέπει να ακούγεται πολύ οικείο και να ξέρετε ότι είστε στο σωστό δρόμο.

Λίγο μετά από αυτό το σχόλιο, θα βρείτε

template<typename T1 = empty, typename T2 = empty,
         typename T3 = empty, typename T4 = empty,
         typename T5 = empty, typename T6 = empty,
         typename T7 = empty, typename T8 = empty>
class TracedCallback
{
  ...

Αυτό σας λέει ότι TracedCallback είναι templated κλάση. Έχει οκτώ πιθανές τύπου παραμέτρους με προκαθορισμένες τιμές. Πηγαίνετε πίσω και να την συγκρίνετε με τη δήλωση που προσπαθείτε να καταλάβετε

TracedCallback<Ptr<const MobilityModel> > m_courseChangeTrace;

Το typename T1 στην templated δήλωση της κλάσης αντιστοιχεί στο Ptr<const MobilityModel> στην παραπάνω δήλωση. Όλες οι άλλες παράμετροι τύπου παραμένουν ως προεπιλογές. Κοιτάζοντας τον κατασκευαστή πραγματικά δεν σας λέει πολλά. Το ένα μέρος όπου μπορείτε να έχετε δει μια σύνδεση μεταξύ της συνάρτησης επανάκλησής σας και το σύστημα εντοπισμού είναι σε Connect και οι συναρτήσεις ConnectWithoutContext. Αν μετακινηθείτε προς τα κάτω, θα δείτε μια μέθοδο ConnectWithoutContext εδώ

template<typename T1, typename T2,
         typename T3, typename T4,
         typename T5, typename T6,
         typename T7, typename T8>
void
TracedCallback<T1,T2,T3,T4,T5,T6,T7,T8>::ConnectWithoutContext ...
{
  Callback<void,T1,T2,T3,T4,T5,T6,T7,T8> cb;
  cb.Assign (callback);
  m_callbackList.push_back (cb);
}

Είστε τώρα στην κοιλιά του κτήνους. Όταν το πρότυπο έχει παραδείγματα για τη δήλωση παραπάνω, ο compiler θα αντικαταστήσει τον T1 με Ptr<const MobilityModel>.

void
TracedCallback<Ptr<const MobilityModel>::ConnectWithoutContext ... cb
{
  Callback<void, Ptr<const MobilityModel> > cb;
  cb.Assign (callback);
  m_callbackList.push_back (cb);
}

Μπορείτε να δείτε τώρα την εφαρμογή του όλα όσα έχουμε μιλήσει. Ο κώδικας δημιουργεί μία επανάκληση του σωστού τύπου και αναθέτει τη συνάρτησή σας σε αυτό. Αυτό είναι το ισοδύναμο του pfi = MyFunction που συζητήσαμε στην αρχή του παρόντος τμήματος. Ο κώδικας στη συνέχεια προσθέτει την Επανάκληση στην λίστα των Επανακλήσεων για αυτήν την πηγή. Το μόνο που μένει είναι να δούμε τον ορισμό της Επανάκλησης. Χρησιμοποιώντας το ίδιο τέχνασμα grep όπως συνηθίζαμε να βρούμε TracedCallback, θα είστε σε θέση να βρείτε ότι το αρχείο ./core/callback.h είναι αυτό που πρέπει να εξετάσουμε.

Αν κοιτάξετε κάτω από το αρχείο, θα δείτε μια πολύ πιθανώς σχεδόν ακατανόητο κώδικα προτύπου. Εσείς τελικά θα καταλήξετε σε κάποια API Τεκμηρίωση για την κλάση Επανάκλησης πρότυπο, όμως. Ευτυχώς, υπάρχουν κάποια αγγλικά:

Callback template class.

This class template implements the Functor Design Pattern. It is used to declare the type of a Callback:

  • the first non-optional template argument represents the return type of the callback.
  • the reminaining (optional) template arguments represent the type of the subsequent arguments to the callback.
  • up to nine arguments are supported.

Προσπαθούμε να καταλάβουμε τι

Callback<void, Ptr<const MobilityModel> > cb;

σημαίνει η δήλωση. Τώρα είμαστε σε θέση να καταλάβουμε ότι το πρώτο (μη προαιρετικό) πρότυπο όρισμα , void, αντιπροσωπεύει τον τύπο επιστροφής της Επανάκλησης. Το δεύτερο (προαιρετικά) πρότυπο όρισμα, Ptr<const MobilityModel> αντιπροσωπεύει τον τύπο του πρώτου ορίσματος επανάκλησης.

Η Επανάκληση σε ερώτηση είναι η συνάρτησή σας για να λαμβάνετε τα γεγονότα ίχνους. Από αυτό μπορείτε να συμπεράνετε ότι χρειάζεστε μια συνάρτηση που επιστρέφει void και παίρνει ένα Ptr<const MobilityModel>. Για παράδειγμα,

void
CourseChangeCallback (Ptr<const MobilityModel> model)
{
  ...
}

Αυτό είναι το μόνο που χρειάζεστε, αν θέλετε να Config::ConnectWithoutContext. Αν θέλετε ένα πλαίσιο, θα πρέπει να Config::Connect και να χρησιμοποιήσετε μια συνάρτηση Επανάκλησης που παίρνει ένα περιβάλλον συμβολοσειράς. Αυτό οφείλεται στο γεγονός ότι η συνάρτηση Connect θα παρέχει το περιεχόμενο για εσάς. Θα χρειαστείτε

void
CourseChangeCallback (std::string context, Ptr<const MobilityModel> model)
{
  ...
}

Αν θέλετε να διασφαλίσετε ότι το CourseChangeCallback είναι ορατό μόνο σε τοπικό αρχείο σας, μπορείτε να προσθέσετε τη λέξη-κλειδί static και να καταλήξετε σε

static void
CourseChangeCallback (std::string path, Ptr<const MobilityModel> model)
{
  ...
}

το οποίο είναι ακριβώς αυτό που χρησιμοποιήσαμε στο παράδειγμα third.cc. Ίσως θα πρέπει να πάτε πίσω και να διαβάσετε πάλι την προηγούμενη ενότητα.

Εάν ενδιαφέρεστε για περισσότερες λεπτομέρειες σχετικά με την εφαρμογή των Επανακλήσεων, μη διστάσετε να ρίξετε μια ματιά στο manual του ns-3. Αποτελούν ένα από τα πιο συχνά χρησιμοποιούμενα κατασκευάσματα στα τμήματα χαμηλού επιπέδου του ns-3. Είναι, κατά τη γνώμη μου, ένα πολύ κομψό πράγμα.

TracedValues

Νωρίτερα σε αυτή την ενότητα, παρουσιάσαμε ένα απλό κομμάτι του κώδικα που χρησιμοποιείται στο TracedValue<int32_t> να αποδείξει τα βασικά στοιχεία του κώδικα ανίχνευσης. Εμείς απλά προσπαθήσαμε να καταλάβουμε το τι είναι πραγματικά η TracedValue και πώς να βρείτε τον τύπο επιστροφής και τα επίσημα ορίσματα για την επανάκληση.

Όπως αναφέραμε, το αρχείο, traced-value.h φέρνει στις απαιτούμενες δηλώσεις για τον εντοπισμό των δεδομένων που υπακούει στη σημασιολογική αξία. Σε γενικές γραμμές, η σημασιολογική αξία απλά σημαίνει ότι μπορείτε να περάσετε το ίδιο το αντικείμενο γύρω του, αντί να περάσεις την διεύθυνση του αντικειμένου. Επεκτείνουμε την απαίτηση να συμπεριλάβουμε το σύνολο των φορέων ανάθεσης-στιλ που είναι προκαθορισμένα σε plain-old-data (POD) τύπους:

operator= (assignment)
operator*= operator/=
operator+= operator-=
operator++ (both prefix and postfix)
operator-- (both prefix and postfix)
operator<<= operator>>=
operator&= operator|=
operator%= operator^=

Αυτό σημαίνει πραγματικά ότι θα είστε σε θέση να εντοπίζετε όλες τις αλλαγές που γίνονται με τη χρήση των εν λόγω φορέων σε αντικείμενο C++ που έχει σημασιολογική αξία.

Η δήλωση TracedValue<> που είδαμε παραπάνω παρέχει την υποδομή που επιβαρύνει τους φορείς που αναφέρονται ανωτέρω και κινεί τη διαδικασία επανάκλησης. Σχετικά με τη χρήση οποιουδήποτε από τα παραπάνω φορέων με ένα TracedValue θα παρέχει τόσο την παλιά όσο και τη νέα τιμή της μεταβλητής, σε αυτή την περίπτωση μία αξία int32_t. Με την επιθεώρηση της δήλωσης TracedValue, γνωρίζουμε ότι η συνάρτηση της καταβόθρας ίχνους θα έχει ορίσματα (const int32_t oldValue, const int32_t newValue). Ο τύπος επιστροφής για μία συνάρτηση Επανάκλησης TracedValue είναι πάντα void, έτσι ώστε το αναμενόμενο στίγμα Επανάκλησης θα είναι

void (* TracedValueCallback)(const int32_t oldValue, const int32_t newValue);

Το .AddTraceSource στη μέθοδο GetTypeId παρέχει το “αγκίστρι” που χρησιμοποιείται για τη σύνδεση της πηγής ίχνους με τον έξω κόσμο μέσω του συστήματος Config. Έχουμε ήδη συζητήσει τα τρία πρώτα ορίσματα στο AddTraceSource: το όνομα του Χαρακτηριστικού για το σύστημα Config, μια συμβολοσειρά βοήθεια, και τη διεύθυνση του μέλους κλάσης δεδομένων TracedValue.

Το τελευταίο όρισμα συμβολοσειρών, στο παράδειγμα “ns3::Traced::Value::Int32”, είναι το όνομα ενός typedef για το στίγμα της συνάρτησης επανάκλησης. Χρειαζόμαστε αυτές τις υπογραφές-στίγματα που θα καθοριστούν, και να δώσουμε το πλήρως αναγνωρισμένο όνομα τύπου στο AddTraceSource, έτσι ώστε η τεκμηρίωση API μπορεί να συνδέσει μια πηγή ίχνους στο στίγμα της συνάρτησης. Για το στίγμα TracedValue είναι απλό, για TracedCallbacks έχουμε ήδη δει τα API Έγγραφα που πραγματικά βοηθούν.

Πραγματικό Παράδειγμα

Ας κάνουμε ένα παράδειγμα από ένα βιβλίο, από τα πιο γνωστά βιβλία σχετικά με το πρωτόκολλο TCP γύρω. Είναι ένα κλασικό βιβλίο “TCP/IP Illustrated, Volume 1: The Protocols,” από τον W. Richard Stevens. Απλά άφησα το βιβλίο ανοιχτό και έτρεξα ένα ωραίο σύνολο αριθμών τόσο το παράθυρο συμφόρησης όσο και τη σειρά συναρτήσει του χρόνου στη σελίδα 366. Ο Stevens το αποκαλεί αυτό “Figure 21.10. Value of cwnd and send sequence number while data is being transmitted.” Ας δημιουργήσουμε πάλι το μέρος cwnd αυτού του συνόλου σε ns-3 με τη χρήση του συστήματος εντοπισμού και gnuplot.

Διαθέσιμες Πηγές

Το πρώτο πράγμα που πρέπει να σκεφτούμε είναι το πώς θέλουμε να βγάλουμε τα στοιχεία έξω. Τι είναι αυτό που χρειαζόμαστε να εντοπίσουμε; Ας συμβουλευτούμε τη λίστα “All Trace Sources” για να δούμε με τι θα εργαστούμε. Θυμηθείτε ότι αυτό βρίσκεται στο ns-3 API Documentation. Αν μετακινηθείτε μέσα στη λίστα, θα βρείτε τελικά:

ns3::TcpNewReno

  • CongestionWindow: The TCP connection’s congestion window
  • SlowStartThreshold: TCP slow start threshold (bytes)

Αποδεικνύεται ότι η ns-3 TCP implementation υπάρχει (κυρίως) στο αρχείο src/internet/model/tcp-socket-base.cc ενώ οι παραλλαγές ελέγχου συμφόρησης στα αρχεία, όπως src/internet/model/tcp-newreno.cc. Αν δεν γνωρίζετε αυτό το a priori, μπορείτε να χρησιμοποιήσετε το αναδρομικό grep τέχνασμα:

$ find . -name '*.cc' | xargs grep -i tcp

Θα βρείτε τη σελίδα μετά τη σελίδα των περιεχομένων του TCP που δείχνουν προς αυτό το αρχείο.

Φέρνοντας την τεκμηρίωση της κλάσης για TcpNewReno και παρακάμπτοντας τη λίστα των TraceSources θα βρείτε

TraceSources

  • CongestionWindow: The TCP connnection’s congestion window

    Callback signature: ns3::Traced::Value::Uint322Callback

Κάνοντας κλικ στο σύνδεσμο επανάκλησης typedef βλέπουμε την υπογραφή και ξέρουμε τώρα να περιμένουμε

typedef void(* ns3::Traced::Value::Int32Callback)(const int32_t oldValue, const int32_t newValue)

Θα πρέπει να καταλάβετε τώρα αυτόν τον κώδικα εντελώς. Αν έχουμε ένα δείκτη προς το TcpNewReno, μπορούμε να TraceConnect στη πηγή ίχνους “CongestionWindow” αν παρέχουμε τον κατάλληλο στόχο επανάκλησης. Αυτό είναι το ίδιο είδος της πηγής ίχνους που είδαμε στο απλό παράδειγμα κατά την έναρξη του παρόντος τμήματος, εκτός από το ότι μιλάμε για uint32_t αντί int32_t. Και ξέρουμε ότι πρέπει να παρέχουμε μια συνάρτηση επανάκλησης με αυτή την υπογραφή.

Βρίσκοντας Παραδείγματα

Είναι πάντα καλύτερο να προσπαθήσουμε να βρούμε τον κώδικα που δουλεύει, και μπορείτε να τροποποιήσετε, αντί να αρχίσετε από το μηδέν. Έτσι, η πρώτη σειρά των εργασιών είναι τώρα να βρείτε κάποιο κώδικα που ενώνεται ήδη στη πηγή ίχνους “CongestionWindow” και να δούμε αν μπορούμε να το τροποποιήσουμε. Ως συνήθως, ο grep είναι ο φίλος σας:

$ find . -name '*.cc' | xargs grep CongestionWindow

Θα επισημάνω μερικούς ελπιδοφόρους υποψηφίους: examples/tcp/tcp-large-transfer.cc και src/test/ns3tcp/ns3tcp-cwnd-test-suite.cc.

Δεν έχουμε επισκεφθεί κάποιο κώδικα δοκιμής ακόμα, οπότε ας ρίξουμε μια ματιά εκεί. Θα βρείτε συνήθως ότι ο κώδικας της δοκιμής είναι αρκετά μηδαμινός, έτσι αυτό είναι ίσως ένα πολύ καλό στοίχημα. Ανοίξτε src/test/ns3tcp/ns3tcp-cwnd-test-suite.cc στο αγαπημένο σας επεξεργαστή και αναζητήστε για “CongestionWindow”. Θα βρείτε,

ns3TcpSocket->TraceConnectWithoutContext ("CongestionWindow",
  MakeCallback (&Ns3TcpCwndTestCase1::CwndChange, this));

Αυτό θα έπρεπε να είναι γνωστό σε σας. ναφερθήκαμε παραπάνω ότι αν είχαμε ένα δείκτη στο TcpNewReno, θα μπορούσαμε να TraceConnect στη πηγή ίχνους “CongestionWindow”. Αυτό είναι ακριβώς αυτό που έχουμε εδώ. Έτσι, αποδεικνύεται ότι αυτή η γραμμή του κώδικα κάνει ακριβώς αυτό που θέλουμε. Ας πάμε μπροστά και να εξάγουμε τον κώδικα που χρειαζόμαστε από τη συνάρτηση αυτή (Ns3TcpCwndTestCase1::DoRun (void)). Αν κοιτάξετε αυτή τη συνάρτηση, θα διαπιστώσετε ότι μοιάζει ακριβώς όπως ένα σενάριο ns-3. Αποδεικνύεται ότι είναι ακριβώς αυτό που είναι. Πρόκειται για ένα σενάριο που εκτελείται από το πλαίσιο της δοκιμής, ώστε να μπορούμε να το τραβήξουμε ακριβώς έξω και να το τυλίξουμε σε main αντί για DoRun. Αντί να περπατήσετε μέσα από αυτό, βήμα, βήμα, παρέχουμε το αρχείο που προκύπτει από το porting της δοκιμής πίσω σε ένα μητρικό σενάριο ns-3examples/tutorial/fifth.cc.

Δυναμικές Πηγές Εντοπισμού

Το παράδειγμα `` fifth.cc`` καταδεικνύει ένα εξαιρετικά σημαντικό κανόνα που πρέπει να καταλάβετε πριν από τη χρήση κάθε είδους πηγή ίχνους: θα πρέπει να βεβαιωθείτε ότι ο στόχος της εντολής Config::Connect υπάρχει πριν προσπαθήσετε να το χρησιμοποιήσετε. Αυτό δεν είναι διαφορετικό από το να λέμε ένα αντικείμενο πρέπει να αρχικοποιείται πριν προσπαθήσετε να το καλέσετε. Αν και αυτό μπορεί να φαίνεται προφανές, όταν δηλώθηκε αυτός ο τρόπος, κάνοντας τρικλοποδιά σε πολλούς ανθρώπους που προσπαθούν να χρησιμοποιήσουν το σύστημα για πρώτη φορά.

Ας επιστρέψουμε στα βασικά για μια στιγμή. Υπάρχουν τρεις βασικές φάσεις εκτέλεσης που υπάρχουν σε κάθε σενάριο ns-3. Η πρώτη φάση μερικές φορές ονομάζεται “Configuration Time” ή “Setup Time”, και υπάρχει κατά τη διάρκεια της περιόδου, όταν η συνάρτηση main από το σενάριό σας τρέχει, αλλά πριν ο Simulator::Run έχει καλεστεί. Η δεύτερη φάση μερικές φορές ονομάζεται “Simulation Time” και υπάρχει κατά τη διάρκεια της χρονικής περιόδουπου είναι ενεργά εκτελέσιμα τα γεγονότα του Simulator::Run. Μετά την ολοκλήρωση της εκτέλεσης της προσομοίωσης, Simulator::Run θα επιστρέψει στον έλεγχο πίσω στη συνάρτηση main. Όταν συμβαίνει αυτό, τα σενάρια εισέρχονται σε αυτό το τι μπορεί να ονομάζεται “Teardown Phase”, η οποία είναι όταν οι κατασκευές και τα αντικείμενα που δημιουργήθηκαν κατά τη διάρκεια της εγκατάστασης ληφθούν χώρια και κυκλοφορήσουν.

Ίσως το πιο κοινό λάθος που γίνεται στην προσπάθειά τους να χρησιμοποιήσουν το σύστημα εντοπισμού υποθέτει ότι οι οντότητες που δημιουργούνται δυναμικά κατά τη διάρκεια της προσομοίωσης είναι διαθέσιμες κατά τη διάρκεια της ρύθμισης. Ειδικότερα, ένας Socket ns-3 είναι ένα δυναμικό αντικείμενο που δημιουργείται συχνά από Applications που επικοινωνούν μεταξύ Nodes. Ένα Application ns-3 έχει πάντα ένα “Start Time” και ένα “Stop Time” που συνδέονται με αυτό. Στη συντριπτική πλειονότητα των περιπτώσεων, ένα Application δεν θα επιχειρήσει να δημιουργήσει ένα δυναμικό αντικείμενο έως ότου η μέθοδός του StartApplication καλείται σε κάποιο “Start Time”. Αυτό γίνεται για να εξασφαλιστεί ότι η προσομοίωση είναι πλήρως διαμορφωμένη πριν το App προσπαθήσει να κάνει κάτι (ό,τι θα συνέβαινε αν προσπαθούσε να συνδεθεί σε έναν κόμβο που δεν υπήρχε ακόμα κατά τη διάρκεια της ρύθμισης). Ως αποτέλεσμα, κατά τη διάρκεια της φάσης διαμόρφωσης δεν μπορείτε να συνδέσετε μια πηγή ίχνους σε μία καταβόθρα ίχνους αν μία από αυτές δημιουργείται δυναμικά κατά τη διάρκεια της προσομοίωσης.

Οι δύο λύσεις για αυτό το connundrum είναι

#. Δημιουργήστε ένα συμβάν προσομοιωτή που εκτελείται μετά το δυναμικό αντικείμενο που έχει δημιουργηθεί και συνδέστε το ίχνος, όταν εκτελείται αυτό το γεγονός, ή

#. Δημιουργήστε το δυναμικό αντικείμενο κατά το χρόνο διαμόρφωσης, κρατήστε’το στη συνέχεια, και δώστε το αντικείμενο στο σύστημα για να το χρησιμοποιήσει κατά τη διάρκεια της προσομοίωσης.

Πήραμε τη δεύτερη προσέγγιση στο παράδειγμα fifth.cc. Αυτή η απόφαση μας απαίτησε να δημιουργήσουμε το MyApp Application, ο ολόκληρος σκοπός της οποίας είναι να ρίξετε μια Socket ως παράμετρο.

Πέρασμα: fifth.cc

Τώρα, ας ρίξουμε μια ματιά στο πρόγραμμα παράδειγμα που κατασκευάσαμε με ανατομή το τεστ παράθυρο συμφόρησης. Ανοίξτε το examples/tutorial/fifth.cc στο αγαπημένο σας επεξεργαστή. Θα πρέπει να δείτε κάποιο γνωστό κώδικα

/* -*- Mode:C++; c-file-style:"gnu"; indent-tabs-mode:nil; -*- */
/*
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License version 2 as
 * published by the Free Software Foundation;
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Include., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */

#include <fstream>
#include "ns3/core-module.h"
#include "ns3/network-module.h"
#include "ns3/internet-module.h"
#include "ns3/point-to-point-module.h"
#include "ns3/applications-module.h"

using namespace ns3;

NS_LOG_COMPONENT_DEFINE ("FifthScriptExample");

Όλα αυτά έχουν καλυφθεί, γι ‘αυτό δεν θα το αναμασήσουμε. Οι επόμενες γραμμές του κώδικα είναι η εικόνα του δικτύου και ένα σχόλιο αντιμετώπισης του προβλήματος που περιγράφηκε παραπάνω με το Socket.

// ===========================================================================
//
//         node 0                 node 1
//   +----------------+    +----------------+
//   |    ns-3 TCP    |    |    ns-3 TCP    |
//   +----------------+    +----------------+
//   |    10.1.1.1    |    |    10.1.1.2    |
//   +----------------+    +----------------+
//   | point-to-point |    | point-to-point |
//   +----------------+    +----------------+
//           |                     |
//           +---------------------+
//                5 Mbps, 2 ms
//
//
// We want to look at changes in the ns-3 TCP congestion window.  We need
// to crank up a flow and hook the CongestionWindow attribute on the socket
// of the sender.  Normally one would use an on-off application to generate a
// flow, but this has a couple of problems.  First, the socket of the on-off
// application is not created until Application Start time, so we wouldn't be
// able to hook the socket (now) at configuration time.  Second, even if we
// could arrange a call after start time, the socket is not public so we
// couldn't get at it.
//
// So, we can cook up a simple version of the on-off application that does what
// we want.  On the plus side we don't need all of the complexity of the on-off
// application.  On the minus side, we don't have a helper, so we have to get
// a little more involved in the details, but this is trivial.
//
// So first, we create a socket and do the trace connect on it; then we pass
// this socket into the constructor of our simple application which we then
// install in the source node.
// ===========================================================================
//

Αυτό θα πρέπει επίσης να είναι αυτονόητο.

Το επόμενο μέρος είναι η δήλωση του MyApp Application που έχουμε βάλει μαζί για να καταστεί δυνατή η Socket για να δημιουργηθούν κατά το χρόνο διαμόρφωσης.

class MyApp : public Application
{
public:

  MyApp ();
  virtual ~MyApp();

  void Setup (Ptr<Socket> socket, Address address, uint32_t packetSize,
    uint32_t nPackets, DataRate dataRate);

private:
  virtual void StartApplication (void);
  virtual void StopApplication (void);

  void ScheduleTx (void);
  void SendPacket (void);

  Ptr<Socket>     m_socket;
  Address         m_peer;
  uint32_t        m_packetSize;
  uint32_t        m_nPackets;
  DataRate        m_dataRate;
  EventId         m_sendEvent;
  bool            m_running;
  uint32_t        m_packetsSent;
};

Ξεκινώντας/Σταματώντας Εφαρμογών

Αξίζει τον κόπο να περάσετε λίγο χρόνο εξηγώντας πώς τα γεγονότα πραγματικά ξεκίνησαν στο σύστημα. Αυτή είναι μία άλλη αρκετά βαθιά εξήγηση, και μπορεί να αγνοηθεί αν δεν κάνετε σχεδιασμό για τα εγχειρήματα μέσα στα σπλάχνα του συστήματος. Είναι χρήσιμο, ωστόσο, ότι η συζήτηση αγγίζει σχετικά με το πώς ορισμένα πολύ σημαντικά μέρη εργασίας στον ns-3 και εκθέτει κάποια σημαντικά ιδιώματα. Εάν σχεδιάζετε για την εφαρμογή νέων μοντέλων ίσως πρέπει να καταλάβετε αυτή την ενότητα.

Ο πιο συνηθισμένος τρόπος για να ξεκινήσετε την άντληση των γεγονότων είναι να ξεκινήσει μια Application. Αυτό γίνεται ως αποτέλεσμα των ακόλουθων (ελπίζουμε) οικείων γραμμών του σεναρίου ns-3

ApplicationContainer apps = ...
apps.Start (Seconds (1.0));
apps.Stop (Seconds (10.0));

Ο κώδικας της εφαρμογής (βλέπε src/network/helper/application-container.h αν σας ενδιαφέρει) κάνει επαναλήψεις μέσω των εφαρμογών και των κλήσεών του,

app->SetStartTime (startTime);

ως αποτέλεσμα της κλήσης apps.Start και

app->SetStopTime (stopTime);

ως αποτέλεσμα της κλήσης apps.Stop.

Το τελικό αποτέλεσμα αυτών των κλήσεων είναι ότι θέλουμε να έχουμε τον προσομοιωτή να κάνει αυτόματα τις κλήσεις στις Applications για να τους πεί πότε θα ξεκινήσουν και να σταματήσουν. Στην περίπτωση της MyApp, κληρονομεί από την κλάση Application και παρακάμπτει StartApplication, και StopApplication. Αυτές είναι οι συναρτήσεις που θα κληθούν από τον προσομοιωτή την κατάλληλη στιγμή. Στην περίπτωση της MyApp θα διαπιστώσετε ότι η MyApp::StartApplication κάνει την αρχική Bind, και Connect στην υποδοχή και στη συνέχεια ξεκινά η ροή δεδομένων καλώντας την MyApp::SendPacket. Η MyApp::StopApplication σταματάει να παράγει πακέτα, ακυρώνοντας κάθε γεγονώς που εκκρεμεί και στη συνέχεια κλείνει την υποδοχή.

Ένα από τα ωραία πράγματα για τον ns-3 είναι ότι μπορείτε να αγνοήσετε εντελώς τις λεπτομέρειες της εφαρμογής του πώς το Application σας “automagically” καλείται από τον προσομοιωτή στο σωστό χρόνο. Αλλά δεδομένου ότι έχουμε ήδη αποτολμήσει βαθιά μέσα στον ns-3 ήδη, ας πάμε για αυτό.

Αν κοιτάξετε στο src/network/model/application.cc θα διαπιστώσετε ότι η μέθοδος SetStartTime από ένα Application θέτει μόνο τη μεταβλητή μέλος m_startTime και τη μέθοδο SetStopTime καθορίζει ακριβώς το m_stopTime. Από εκεί, χωρίς κάποιες συμβουλές, η διαδρομή τελειώνει.

Το κλειδί για να πάρει το μονοπάτι ξανά είναι να γνωρίζουμε ότι υπάρχει μια παγκόσμια λίστα με όλους τους κόμβους του συστήματος. Κάθε φορά που δημιουργείτε ένα κόμβο σε μια προσομοίωση, ένας δείκτης σε αυτόν τον κόμβο προστίθεται στο παγκόσμιο NodeList.

Ρίξτε μια ματιά σε αυτό src/network/model/node-list.cc και ψάξτε για αυτό NodeList::Add. Η δημόσια στατική εφαρμογή καλεί σε μια ιδιωτική εφαρμογή που ονομάζεται NodeListPriv::Add. Αυτό είναι ένα σχετικά κοινό idom στον ns-3. Έτσι, ρίξτε μια ματιά στο NodeListPriv::Add. Εκεί θα βρείτε,

Simulator::ScheduleWithContext (index, TimeStep (0), &Node::Initialize, node);

Αυτό σας λέει ότι κάθε φορά που ένας Κόμβος δημιουργείται σε μια προσομοίωση, ως παρενέργεια, μια κλήση στη μέθοδο του κόμβου Initialize έχει προγραμματιστεί για εσάς που συμβαίνει σε χρόνο μηδέν. Μην διαβάζετε πάρα πολύ σε αυτό το όνομα, ακόμα. Αυτό δεν σημαίνει ότι ο Κόμβος πρόκειται να αρχίσει να κάνει κάτι, κι αυτό μπορεί να ερμηνευθεί ως μια ενημερωτική κλήση στον Κόμβο λέγοντάς του ότι η προσομοίωση έχει ξεκινήσει, δεν είναι μια κλήση για δράση λέγοντας ο κόμβος να αρχίσει να κάνει κάτι.

Έτσι, το NodeList::Add έμμεσα προγραμματίζει μια κλήση στο Node::Initialize σε χρόνο μηδέν για να συμβουλεύσει ένα νέο Κόμβο που η προσομοίωση έχει ξεκινήσει. Αν κοιτάξετε στο src/network/model/node.h``δε θα βρείτε μια μέθοδο που ονομάζεται ``Node::Initialize. Αποδεικνύεται ότι η μέθοδος Initialize κληρονομείται από την κλάση Object. Όλα τα αντικείμενα του συστήματος μπορεί να ενημερώνονται όταν αρχίζει η προσομοίωση, και τα αντικείμενα της κλάσης Node είναι μόνο ένα είδος αυτών των αντικειμένων.

Ρίξτε μια ματιά εδώ src/core/model/object.cc και αναζητήστε για Object::Initialize. Αυτός ο κώδικας δεν είναι τόσο απλός όσο αναμένεται τη στιγμή που ο ns-3 Objects υποστηρίζει τη συσσωμάτωση. Ο κώδικας στη Object::Initialize κάνει επαναλήψεις και στη συνέχεια μέσω όλων των αντικειμένων που έχουν συγκεντρωτικά μαζί και καλεί τη μέθοδο DoInitialize. Αυτό είναι άλλο ένα ιδίωμα που είναι πολύ συχνό στον ns-3, μερικές φορές ονομάζεται “template design pattern.”: μια δημόσια μέθοδο μη-εικονική API, η οποία παραμένει σταθερή σε όλες τις εφαρμογές, και καλεί μια ιδιωτική μέθοδο εικονικής εφαρμογής που κληρονόμησε και έχει εφαρμοστεί απο υποκατηγορίες. Τα ονόματα είναι συνήθως κάτι σαν MethodName για το κοινό API και DoMethodName για το ιδιωτικό API.

Αυτό μας λέει ότι πρέπει να κοιτάξουμε για μια μέθοδο Node::DoInitialize στο src/network/model/node.cc για τη μέθοδο που θα συνεχίσει το μονοπάτι μας. Εάν εντοπίσετε τον κώδικα, θα βρείτε μια μέθοδο που κάνει επαναλήψεις μέσα από όλες τις συσκευές στον κόμβο και στη συνέχεια όλες τις εφαρμογές στον κόμβο καλώντας device->Initialize και application->Initialize αντίστοιχα.

Ίσως γνωρίζετε ήδη ότι οι κλάσεις Device και Application κληρονομούν από την κλάση Object και έτσι το επόμενο βήμα θα είναι να εξετάσουμε τι συμβαίνει όταν καλείται το Application::DoInitialize. Ρίξτε μια ματιά σε src/network/model/application.cc και θα βρείτε

void
Application::DoInitialize (void)
{
  m_startEvent = Simulator::Schedule (m_startTime, &Application::StartApplication, this);
  if (m_stopTime != TimeStep (0))
    {
      m_stopEvent = Simulator::Schedule (m_stopTime, &Application::StopApplication, this);
    }
  Object::DoInitialize ();
}

Εδώ,ερχόμαστε τελικά στο τέλος της διαδρομής. Αν το έχετε κρατήσει όλο “ευθεία”, όταν εφαρμόζετε ένα ns-3 Application, η νέα εφαρμογή σας κληρονομεί από την κλάση Application. Μπορείτε να παρακάμψετε τις μεθόδους StartApplication και StopApplication και παρέχει μηχανισμούς για την εκκίνηση και τη διακοπή της ροής των δεδομένων από το νέο Application σας. Όταν ένας Κόμβος δημιουργείται στην προσομοίωση, προστίθεται σε ένα παγκόσμιο NodeList. Η πράξη της προσθήκης ενός Κόμβου σε αυτό το NodeList προκαλεί ένα γεγονώς του προσομοιωτή που έχει προγραμματιστεί για το χρόνο μηδέν το οποίο καλεί την μέθοδο Node::Initialize από το νέο προστιθέμενο κόμβο που θα καλείται όταν ξεκινά η προσομοίωση. Δεδομένου ότι ο Κόμβος κληρονομεί από Object, αυτό καλεί τη μέθοδο Object::Initialize στον κόμβο που, με τη σειρά του, καλεί τις μεθόδους DoInitialize για όλα τα Objects που συγκεντρώνονται σε κόμβους (σκεφτείτε μοντέλα κινητικότητας). Δεδομένου ότι ο Κόμβος Object έχει παρακαμφθεί το DoInitialize, η μέθοδος καλείται όταν ξεκινά η προσομοίωση. Η μέθοδος Node::DoInitialize καλεί τις μεθόδους Initialize όλων των Applications στον κόμβο. Δεδομένου ότι τα Applications είναι επίσης Objects, αυτό προκαλεί να καλείται το Application::DoInitialize. Όταν καλείται το Application::DoInitialize, προγραμματίζει τα γεγονότα για το StartApplication και το StopApplication καλεί την Application. Αυτές οι κλήσεις έχουν σχεδιαστεί για να ξεκινήσει και να σταματήσει τη ροή των δεδομένων από τις Application

Αυτό ήταν ένα άλλο αρκετά μακρύ ταξίδι, αλλά χρειάζεται να γίνει μια φορά, και καταλαβαίνετε τώρα ένα άλλο πολύ βαθύ κομμάτι του ns-3.

Η Εφαρμογή MyApp

Το MyApp Application χρειάζεται έναν κατασκευαστή και καταστροφέα, φυσικά

MyApp::MyApp ()
  : m_socket (0),
    m_peer (),
    m_packetSize (0),
    m_nPackets (0),
    m_dataRate (0),
    m_sendEvent (),
    m_running (false),
    m_packetsSent (0)
{
}

MyApp::~MyApp()
{
  m_socket = 0;
}

Η ύπαρξη του επόμενου κομματιού του κώδικα είναι ολόκληρος λόγος για τον οποίο γράψαμε αυτό το Application στην πρώτη θέση.

void
MyApp::Setup (Ptr<Socket> socket, Address address, uint32_t packetSize,
                     uint32_t nPackets, DataRate dataRate)
{
  m_socket = socket;
  m_peer = address;
  m_packetSize = packetSize;
  m_nPackets = nPackets;
  m_dataRate = dataRate;
}

Αυτός ο κώδικας θα πρέπει να είναι αρκετά αυτονόητος. Κάνουμε αρχικοποίηση μεταβλητών μέλους. Το σημαντικό από την άποψη του εντοπισμού είναι η Ptr<Socket> socket που χρειαζόμασταν για να παρέχει στην εφαρμογή κατά τη διάρκεια της ρύθμισης. Υπενθυμίζουμε ότι πρόκειται να δημιουργήσουμε το Socket ως TcpSocket (το οποίο υλοποιείται από το TcpNewReno) και πιάνεται απο την πηγή ίχνους του “CongestionWindow” πριν από τη διοχέτευση προς την μέθοδο Setup.

void
MyApp::StartApplication (void)
{
  m_running = true;
  m_packetsSent = 0;
  m_socket->Bind ();
  m_socket->Connect (m_peer);
  SendPacket ();
}

Ο παραπάνω κώδικας είναι ο προσπελάσιμος της εφαρμογή Application::StartApplication που θα κληθεί αυτόματα από τον προσομοιωτή για να ξεκινήσει η Application να τρέχει την κατάλληλη στιγμή. Μπορείτε να δείτε ότι κάνει μία λειτουργία Socket Bind. Εάν είστε εξοικειωμένοι με Berkeley Sockets αυτό δεν πρέπει να αποτελεί έκπληξη. Εκτελεί τις απαιτούμενες εργασίες για την τοπική πλευρά της σύνδεσης ακριβώς όπως μπορείτε να φανταστείτε. Το ακόλουθο Connect θα κάνει ό,τι χρειάζεται για να δημιουργήσει μια σύνδεση με το TCP σε Address m_peer. Θα πρέπει τώρα να είναι σαφές για ποιό λόγο θα πρέπει να αναβάλει πολύ αυτό το χρόνο προσομοίωσης, αφού ο Connect θα χρειαστεί ένα πλήρως λειτουργικό δίκτυο για να ολοκληρωθεί. Μετά τον Connect, η Application ξεκινά στη συνέχεια τη δημιουργία γεγονότων προσομοίωσης καλώντας SendPacket.

Το επόμενο κομμάτι του κώδικα εξηγεί στο Application πώς να σταματήσουμε να δημιουργούμε γεγονότα προσομοίωσης.

void
MyApp::StopApplication (void)
{
  m_running = false;

  if (m_sendEvent.IsRunning ())
    {
      Simulator::Cancel (m_sendEvent);
    }

  if (m_socket)
    {
      m_socket->Close ();
    }
}

Κάθε φορά που μια εκδήλωση προσομοίωσης έχει προγραμματιστεί, ένα Event δημιουργείται. Αν το Event εκκρεμεί η εκτέλεσή του, η μέθοδος της IsRunning θα επιστρέψει true. Σε αυτόν τον κώδικα, αν IsRunning() επιστρέφει true, εμείς Cancel το γεγονός που αφαιρεί από την ουρά του προσομοιωτή εκδήλωσης. Με τον τρόπο αυτό, θα σπάσει την αλυσίδα των γεγονότων που η Application χρησιμοποιεί για να κρατήσει την αποστολή Packets και η Application πηγαίνει ήσυχη. Αφού ηρεμήσει η Application εμείς Close την υποδοχή που κατεδαφίζει τη σύνδεση TCP.

Η υποδοχή είναι στην πραγματικότητα διαγραμένη από τον καταστροφέα, όταν η m_socket = 0 εκτελείται. Αυτό αφαιρεί την τελευταία αναφορά στην υποκείμενη Ptr<Socket> που προκαλεί τον καταστροφέα του αντικειμένου που πρόκειται να κληθεί.

Υπενθυμίζουμε ότι StartApplication κάλεσε SendPacket για να ξεκινήσει η αλυσίδα των γεγονότων που περιγράφει τη συμπεριφορά Application.

void
MyApp::SendPacket (void)
{
  Ptr<Packet> packet = Create<Packet> (m_packetSize);
  m_socket->Send (packet);

  if (++m_packetsSent < m_nPackets)
    {
      ScheduleTx ();
    }
}

Εδώ, μπορείτε να δείτε ότι SendPacket κάνει ακριβώς αυτό. Δημιουργεί ένα Packet και στη συνέχεια κάνει ένα Send η οποία, αν ξέρετε Berkeley Sockets, είναι πιθανώς ακριβώς αυτό που περιμένατε να δείτε.

Είναι ευθύνη του Application να κρατήσει τον προγραμματισμό της αλυσίδας των γεγονότων, έτσι ώστε οι επόμενες γραμμές καλούν τη ScheduleTx να προγραμματίσει μια άλλη περίπτωση μετάδοσης (μία SendPacket) μέχρι το Application αποφασίσει ότι έχει στείλει αρκετά.

void
MyApp::ScheduleTx (void)
{
  if (m_running)
    {
      Time tNext (Seconds (m_packetSize * 8 / static_cast<double> (m_dataRate.GetBitRate ())));
      m_sendEvent = Simulator::Schedule (tNext, &MyApp::SendPacket, this);
    }
}

Εδώ, μπορείτε να δείτε ότι το ScheduleTx κάνει ακριβώς αυτό. Εάν η Application τρέχει (αν το StopApplication δεν έχει κληθεί) θα προγραμματίσετε μια νέα εκδήλωση, η οποία καλεί SendPacket ξανά. Ο αναγνώστης προειδοποίησης θα εντοπίσει κάτι που σκοντάφτουν και νέοι χρήστες. Ο ρυθμός δεδομένων ενός Application είναι ακριβώς αυτός. Δεν έχει τίποτε να κάνει με το ρυθμό δεδομένων ενός υποκείμενου Channel. Αυτός είναι ο ρυθμός με τον οποίο η Application παράγει κομμάτια(bits). Δεν λαμβάνει υπόψη οποιαδήποτε επιβάρυνση για τα διάφορα πρωτόκολλα ή τα κανάλια που χρησιμοποιεί για τη μεταφορά των δεδομένων. Εάν ορίσετε το ρυθμό δεδομένων ενός Application στον ίδιο ρυθμό μετάδοσης δεδομένων ως υποκείμενο Channel θα υπερχειλίσει η μνήμης σας.

Πηγές Ίχνους

Ο σκοπός αυτής της εργασίας είναι οι επανακλήσεις από το TCP υποδεικνύοντας ότι το παράθυρο συμφόρησης έχει ενημερωθεί. Το επόμενο τμήμα κώδικα υλοποιεί την αντίστοιχη πηγή ίχνους:

static void
CwndChange (uint32_t oldCwnd, uint32_t newCwnd)
{
  NS_LOG_UNCOND (Simulator::Now ().GetSeconds () << "\t" << newCwnd);
}

Αυτό θα πρέπει να σας φαίνεται αρκετά οικείο, οπότε δεν θα εμβαθύνουμε σε λεπτομέρειες. Η συνάρτηση αυτή απλά καταγράφει την τρέχουσα ώρα προσομοίωσης και τη νέα τιμή του παραθύρου συμφόρησης κάθε φορά που αυτή αλλάζει. Μπορείτε πιθανώς να φανταστείτε ότι θα μπορούσατε να φορτώσει τα αποτελέσματα που προκύπτουν σε ένα πρόγραμμα γραφικών (gnuplot ή Excel) και να δείτε αμέσως ένα ωραίο γράφημα της συμπεριφοράς του παραθύρου συμφόρησης σε σχέση με τον χρόνο.

Προσθέσαμε ένα νέο ίχνος νεροχύτη για να δείξουμε που απορρίπτονται πακέτα. Πρόκειται να προσθέσουμε ένα πρότυπο μοντέλο σφάλματος στον εν λόγω κώδικα, γι ‘αυτό θα θέλαμε να το δούμε στη πράξη.

static void
RxDrop (Ptr<const Packet> p)
{
  NS_LOG_UNCOND ("RxDrop at " << Simulator::Now ().GetSeconds ());
}

Αυτή η πηγή ίχνους θα πρέπει να συνδεθεί με τη “PhyRxDrop” πηγή ίχνους του από άκρο σε άκρο NetDevice. Αυτή η πηγή ίχνους πυροδοτείται όταν ένα πακέτο απορρίπτεραι από το φυσικό στρώμα ενός `` NetDevice``. Εάν μεταβείται στο (src/point-to-point/model/point-to-point-net-device.cc), θα δείτε ότι αυτή η πηγή ίχνος αναφέρεται στο PointToPointNetDevice::m_phyRxDropTrace. Αν στη συνέχεια να αναζητήσετε στο src/point-to-point/model/point-to-point-net-device.h για αυτή τη μεταβλητή μέλος, θα διαπιστώσετε ότι έχει δηλωθεί ως TracedCallback<Ptr<const Packet> >. Αυτό σας δείχνει ‘ότι ο στόχος επανάκλησης θα πρέπει να είναι μια συνάρτηση που επιστρέφει κενό και παίρνει μια μοναδική παράμετρο η οποία είναι μια Ptr<const Packet> (υποθέτοντας ότι χρησιμοποιούμε ConnectWithoutContext) - ακριβώς αυτό που έχουμε και παραπάνω.

Κυρίως Πρόγραμμα

Ο παρακάτω κώδικας θα πρέπει να σας είναι πολύ οικείος:

int
main (int argc, char *argv[])
{
  NodeContainer nodes;
  nodes.Create (2);

  PointToPointHelper pointToPoint;
  pointToPoint.SetDeviceAttribute ("DataRate", StringValue ("5Mbps"));
  pointToPoint.SetChannelAttribute ("Delay", StringValue ("2ms"));

  NetDeviceContainer devices;
  devices = pointToPoint.Install (nodes);

Αυτό δημιουργεί δύο κόμβους με ένα από άκρο σε άκρο κανάλι μεταξύ τους, όπως φαίνεται στην εικόνα στην αρχή του αρχείου.

Οι επόμενες γραμμές κώδικα δείχνουν κάτι νέο. Αν παρακολουθήσουμε μια σύνδεση που συμπεριφέρεται άρτια, θα καταλήξουμε με ένα μονοτονικά αύξων παράθυρο συμφόρησης. Για να δούμε κάποια ενδιαφέρουσα συμπεριφορά, θα πρέπει να εισάγουμε σφάλματα συνδέσμων που θα απορρίπτουν πακέτα, θα προκαλούν διπλά ACKs και θα ενεργοποιήσουν τις πιο ενδιαφέρουσες συμπεριφορές του παραθύρου συμφόρησης.

Ο ns-3 παρέχει αντικείμενα τύπου ErrorModel που μπορούν να συνδεθούν σε Channels. Χρησιμοποιούμε το `` RateErrorModel`` το οποίο μας επιτρέπει να εισάγουμε σφάλματα σε ένα Channel για ένα δοσμένο ρυθμό.

Ptr<RateErrorModel> em = CreateObject<RateErrorModel> ();
em->SetAttribute ("ErrorRate", DoubleValue (0.00001));
devices.Get (1)->SetAttribute ("ReceiveErrorModel", PointerValue (em));

The above code instantiates a RateErrorModel Object, and we set the “ErrorRate” Attribute to the desired value. We then set the resulting instantiated RateErrorModel as the error model used by the point-to-point NetDevice. This will give us some retransmissions and make our plot a little more interesting.

InternetStackHelper stack;
stack.Install (nodes);

Ipv4AddressHelper address;
address.SetBase ("10.1.1.0", "255.255.255.252");
Ipv4InterfaceContainer interfaces = address.Assign (devices);

The above code should be familiar. It installs internet stacks on our two nodes and creates interfaces and assigns IP addresses for the point-to-point devices.

Since we are using TCP, we need something on the destination Node to receive TCP connections and data. The PacketSink Application is commonly used in ns-3 for that purpose.

uint16_t sinkPort = 8080;
Address sinkAddress (InetSocketAddress(interfaces.GetAddress (1), sinkPort));
PacketSinkHelper packetSinkHelper ("ns3::TcpSocketFactory",
  InetSocketAddress (Ipv4Address::GetAny (), sinkPort));
ApplicationContainer sinkApps = packetSinkHelper.Install (nodes.Get (1));
sinkApps.Start (Seconds (0.));
sinkApps.Stop (Seconds (20.));

This should all be familiar, with the exception of,

PacketSinkHelper packetSinkHelper ("ns3::TcpSocketFactory",
  InetSocketAddress (Ipv4Address::GetAny (), sinkPort));

This code instantiates a PacketSinkHelper and tells it to create sockets using the class ns3::TcpSocketFactory. This class implements a design pattern called “object factory” which is a commonly used mechanism for specifying a class used to create objects in an abstract way. Here, instead of having to create the objects themselves, you provide the PacketSinkHelper a string that specifies a TypeId string used to create an object which can then be used, in turn, to create instances of the Objects created by the factory.

The remaining parameter tells the Application which address and port it should Bind to.

The next two lines of code will create the socket and connect the trace source.

Ptr<Socket> ns3TcpSocket = Socket::CreateSocket (nodes.Get (0),
  TcpSocketFactory::GetTypeId ());
ns3TcpSocket->TraceConnectWithoutContext ("CongestionWindow",
  MakeCallback (&CwndChange));

The first statement calls the static member function Socket::CreateSocket and provides a Node and an explicit TypeId for the object factory used to create the socket. This is a slightly lower level call than the PacketSinkHelper call above, and uses an explicit C++ type instead of one referred to by a string. Otherwise, it is conceptually the same thing.

Once the TcpSocket is created and attached to the Node, we can use TraceConnectWithoutContext to connect the CongestionWindow trace source to our trace sink.

Recall that we coded an Application so we could take that Socket we just made (during configuration time) and use it in simulation time. We now have to instantiate that Application. We didn’t go to any trouble to create a helper to manage the Application so we are going to have to create and install it “manually”. This is actually quite easy:

Ptr<MyApp> app = CreateObject<MyApp> ();
app->Setup (ns3TcpSocket, sinkAddress, 1040, 1000, DataRate ("1Mbps"));
nodes.Get (0)->AddApplication (app);
app->Start (Seconds (1.));
app->Stop (Seconds (20.));

The first line creates an Object of type MyApp – our Application. The second line tells the Application what Socket to use, what address to connect to, how much data to send at each send event, how many send events to generate and the rate at which to produce data from those events.

Next, we manually add the MyApp Application to the source Node and explicitly call the Start and Stop methods on the Application to tell it when to start and stop doing its thing.

We need to actually do the connect from the receiver point-to-point NetDevice drop event to our RxDrop callback now.

devices.Get (1)->TraceConnectWithoutContext("PhyRxDrop", MakeCallback (&RxDrop));

It should now be obvious that we are getting a reference to the receiving Node NetDevice from its container and connecting the trace source defined by the attribute “PhyRxDrop” on that device to the trace sink RxDrop.

Finally, we tell the simulator to override any Applications and just stop processing events at 20 seconds into the simulation.

  Simulator::Stop (Seconds(20));
  Simulator::Run ();
  Simulator::Destroy ();

  return 0;
}

Recall that as soon as Simulator::Run is called, configuration time ends, and simulation time begins. All of the work we orchestrated by creating the Application and teaching it how to connect and send data actually happens during this function call.

As soon as Simulator::Run returns, the simulation is complete and we enter the teardown phase. In this case, Simulator::Destroy takes care of the gory details and we just return a success code after it completes.

Running fifth.cc

Since we have provided the file fifth.cc for you, if you have built your distribution (in debug mode since it uses NS_LOG – recall that optimized builds optimize out NS_LOG) it will be waiting for you to run.

$ ./waf --run fifth
Waf: Entering directory `/home/craigdo/repos/ns-3-allinone-dev/ns-3-dev/build'
Waf: Leaving directory `/home/craigdo/repos/ns-3-allinone-dev/ns-3-dev/build'
'build' finished successfully (0.684s)
1       536
1.0093  1072
1.01528 1608
1.02167 2144
...
1.11319 8040
1.12151 8576
1.12983 9112
RxDrop at 1.13696
...

You can probably see immediately a downside of using prints of any kind in your traces. We get those extraneous waf messages printed all over our interesting information along with those RxDrop messages. We will remedy that soon, but I’m sure you can’t wait to see the results of all of this work. Let’s redirect that output to a file called cwnd.dat:

$ ./waf --run fifth > cwnd.dat 2>&1

Now edit up “cwnd.dat” in your favorite editor and remove the waf build status and drop lines, leaving only the traced data (you could also comment out the TraceConnectWithoutContext("PhyRxDrop", MakeCallback (&RxDrop)); in the script to get rid of the drop prints just as easily.

You can now run gnuplot (if you have it installed) and tell it to generate some pretty pictures:

$ gnuplot
gnuplot> set terminal png size 640,480
gnuplot> set output "cwnd.png"
gnuplot> plot "cwnd.dat" using 1:2 title 'Congestion Window' with linespoints
gnuplot> exit

You should now have a graph of the congestion window versus time sitting in the file “cwnd.png” that looks like:

figures/cwnd.png

Using Mid-Level Helpers

In the previous section, we showed how to hook a trace source and get hopefully interesting information out of a simulation. Perhaps you will recall that we called logging to the standard output using std::cout a “blunt instrument” much earlier in this chapter. We also wrote about how it was a problem having to parse the log output in order to isolate interesting information. It may have occurred to you that we just spent a lot of time implementing an example that exhibits all of the problems we purport to fix with the ns-3 tracing system! You would be correct. But, bear with us. We’re not done yet.

One of the most important things we want to do is to is to have the ability to easily control the amount of output coming out of the simulation; and we also want to save those data to a file so we can refer back to it later. We can use the mid-level trace helpers provided in ns-3 to do just that and complete the picture.

We provide a script that writes the cwnd change and drop events developed in the example fifth.cc to disk in separate files. The cwnd changes are stored as a tab-separated ASCII file and the drop events are stored in a PCAP file. The changes to make this happen are quite small.

Walkthrough: sixth.cc

Let’s take a look at the changes required to go from fifth.cc to sixth.cc. Open examples/tutorial/sixth.cc in your favorite editor. You can see the first change by searching for CwndChange. You will find that we have changed the signatures for the trace sinks and have added a single line to each sink that writes the traced information to a stream representing a file.

static void
CwndChange (Ptr<OutputStreamWrapper> stream, uint32_t oldCwnd, uint32_t newCwnd)
{
  NS_LOG_UNCOND (Simulator::Now ().GetSeconds () << "\t" << newCwnd);
  *stream->GetStream () << Simulator::Now ().GetSeconds () << "\t" << oldCwnd << "\t" << newCwnd << std::endl;
}

static void
RxDrop (Ptr<PcapFileWrapper> file, Ptr<const Packet> p)
{
  NS_LOG_UNCOND ("RxDrop at " << Simulator::Now ().GetSeconds ());
  file->Write(Simulator::Now(), p);
}

We have added a “stream” parameter to the CwndChange trace sink. This is an object that holds (keeps safely alive) a C++ output stream. It turns out that this is a very simple object, but one that manages lifetime issues for the stream and solves a problem that even experienced C++ users run into. It turns out that the copy constructor for std::ostream is marked private. This means that std::ostreams do not obey value semantics and cannot be used in any mechanism that requires the stream to be copied. This includes the ns-3 callback system, which as you may recall, requires objects that obey value semantics. Further notice that we have added the following line in the CwndChange trace sink implementation:

*stream->GetStream () << Simulator::Now ().GetSeconds () << "\t" << oldCwnd << "\t" << newCwnd << std::endl;

This would be very familiar code if you replaced *stream->GetStream () with std::cout, as in:

std::cout << Simulator::Now ().GetSeconds () << "\t" << oldCwnd << "\t" << newCwnd << std::endl;

This illustrates that the Ptr<OutputStreamWrapper> is really just carrying around a std::ofstream for you, and you can use it here like any other output stream.

A similar situation happens in RxDrop except that the object being passed around (a Ptr<PcapFileWrapper>) represents a PCAP file. There is a one-liner in the trace sink to write a timestamp and the contents of the packet being dropped to the PCAP file:

file->Write(Simulator::Now(), p);

Of course, if we have objects representing the two files, we need to create them somewhere and also cause them to be passed to the trace sinks. If you look in the main function, you will find new code to do just that:

AsciiTraceHelper asciiTraceHelper;
Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("sixth.cwnd");
ns3TcpSocket->TraceConnectWithoutContext ("CongestionWindow", MakeBoundCallback (&CwndChange, stream));

...

PcapHelper pcapHelper;
Ptr<PcapFileWrapper> file = pcapHelper.CreateFile ("sixth.pcap", std::ios::out, PcapHelper::DLT_PPP);
devices.Get (1)->TraceConnectWithoutContext("PhyRxDrop", MakeBoundCallback (&RxDrop, file));

In the first section of the code snippet above, we are creating the ASCII trace file, creating an object responsible for managing it and using a variant of the callback creation function to arrange for the object to be passed to the sink. Our ASCII trace helpers provide a rich set of functions to make using text (ASCII) files easy. We are just going to illustrate the use of the file stream creation function here.

The CreateFileStream function is basically going to instantiate a std::ofstream object and create a new file (or truncate an existing file). This std::ofstream is packaged up in an ns-3 object for lifetime management and copy constructor issue resolution.

We then take this ns-3 object representing the file and pass it to MakeBoundCallback(). This function creates a callback just like MakeCallback(), but it “binds” a new value to the callback. This value is added as the first argument to the callback before it is called.

Essentially, MakeBoundCallback(&CwndChange, stream) causes the trace source to add the additional “stream” parameter to the front of the formal parameter list before invoking the callback. This changes the required signature of the CwndChange sink to match the one shown above, which includes the “extra” parameter Ptr<OutputStreamWrapper> stream.

In the second section of code in the snippet above, we instantiate a PcapHelper to do the same thing for our PCAP trace file that we did with the AsciiTraceHelper. The line of code,

Ptr<PcapFileWrapper> file = pcapHelper.CreateFile ("sixth.pcap",
"w", PcapHelper::DLT_PPP);

creates a PCAP file named “sixth.pcap” with file mode “w”. This means that the new file is truncated (contents deleted) if an existing file with that name is found. The final parameter is the “data link type” of the new PCAP file. These are the same as the PCAP library data link types defined in bpf.h if you are familar with PCAP. In this case, DLT_PPP indicates that the PCAP file is going to contain packets prefixed with point to point headers. This is true since the packets are coming from our point-to-point device driver. Other common data link types are DLT_EN10MB (10 MB Ethernet) appropriate for csma devices and DLT_IEEE802_11 (IEEE 802.11) appropriate for wifi devices. These are defined in src/network/helper/trace-helper.h if you are interested in seeing the list. The entries in the list match those in bpf.h but we duplicate them to avoid a PCAP source dependence.

A ns-3 object representing the PCAP file is returned from CreateFile and used in a bound callback exactly as it was in the ASCII case.

An important detour: It is important to notice that even though both of these objects are declared in very similar ways,

Ptr<PcapFileWrapper> file ...
Ptr<OutputStreamWrapper> stream ...

The underlying objects are entirely different. For example, the Ptr<PcapFileWrapper> is a smart pointer to an ns-3 Object that is a fairly heavyweight thing that supports Attributes and is integrated into the Config system. The Ptr<OutputStreamWrapper>, on the other hand, is a smart pointer to a reference counted object that is a very lightweight thing. Remember to look at the object you are referencing before making any assumptions about the “powers” that object may have.

For example, take a look at src/network/utils/pcap-file-wrapper.h in the distribution and notice,

class PcapFileWrapper : public Object

that class PcapFileWrapper is an ns-3 Object by virtue of its inheritance. Then look at src/network/model/output-stream-wrapper.h and notice,

class OutputStreamWrapper : public
SimpleRefCount<OutputStreamWrapper>

that this object is not an ns-3 Object at all, it is “merely” a C++ object that happens to support intrusive reference counting.

The point here is that just because you read Ptr<something> it does not necessarily mean that something is an ns-3 Object on which you can hang ns-3 Attributes, for example.

Now, back to the example. If you build and run this example,

$ ./waf --run sixth

you will see the same messages appear as when you ran “fifth”, but two new files will appear in the top-level directory of your ns-3 distribution.

sixth.cwnd  sixth.pcap

Since “sixth.cwnd” is an ASCII text file, you can view it with cat or your favorite file viewer.

1       0       536
1.0093  536     1072
1.01528 1072    1608
1.02167 1608    2144
...
9.69256 5149    5204
9.89311 5204    5259

You have a tab separated file with a timestamp, an old congestion window and a new congestion window suitable for directly importing into your plot program. There are no extraneous prints in the file, no parsing or editing is required.

Since “sixth.pcap” is a PCAP file, you can fiew it with tcpdump.

reading from file sixth.pcap, link-type PPP (PPP)
1.136956 IP 10.1.1.1.49153 > 10.1.1.2.8080: Flags [.], seq 17177:17681, ack 1, win 32768, options [TS val 1133 ecr 1127,eol], length 504
1.403196 IP 10.1.1.1.49153 > 10.1.1.2.8080: Flags [.], seq 33280:33784, ack 1, win 32768, options [TS val 1399 ecr 1394,eol], length 504
...
7.426220 IP 10.1.1.1.49153 > 10.1.1.2.8080: Flags [.], seq 785704:786240, ack 1, win 32768, options [TS val 7423 ecr 7421,eol], length 536
9.630693 IP 10.1.1.1.49153 > 10.1.1.2.8080: Flags [.], seq 882688:883224, ack 1, win 32768, options [TS val 9620 ecr 9618,eol], length 536

You have a PCAP file with the packets that were dropped in the simulation. There are no other packets present in the file and there is nothing else present to make life difficult.

It’s been a long journey, but we are now at a point where we can appreciate the ns-3 tracing system. We have pulled important events out of the middle of a TCP implementation and a device driver. We stored those events directly in files usable with commonly known tools. We did this without modifying any of the core code involved, and we did this in only 18 lines of code:

static void
CwndChange (Ptr<OutputStreamWrapper> stream, uint32_t oldCwnd, uint32_t newCwnd)
{
  NS_LOG_UNCOND (Simulator::Now ().GetSeconds () << "\t" << newCwnd);
  *stream->GetStream () << Simulator::Now ().GetSeconds () << "\t" << oldCwnd << "\t" << newCwnd << std::endl;
}

...

AsciiTraceHelper asciiTraceHelper;
Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("sixth.cwnd");
ns3TcpSocket->TraceConnectWithoutContext ("CongestionWindow", MakeBoundCallback (&CwndChange, stream));

...

static void
RxDrop (Ptr<PcapFileWrapper> file, Ptr<const Packet> p)
{
  NS_LOG_UNCOND ("RxDrop at " << Simulator::Now ().GetSeconds ());
  file->Write(Simulator::Now(), p);
}

...

PcapHelper pcapHelper;
Ptr<PcapFileWrapper> file = pcapHelper.CreateFile ("sixth.pcap", "w", PcapHelper::DLT_PPP);
devices.Get (1)->TraceConnectWithoutContext("PhyRxDrop", MakeBoundCallback (&RxDrop, file));

Trace Helpers

The ns-3 trace helpers provide a rich environment for configuring and selecting different trace events and writing them to files. In previous sections, primarily Δημιουργία Τοπολογιών, we have seen several varieties of the trace helper methods designed for use inside other (device) helpers.

Perhaps you will recall seeing some of these variations:

pointToPoint.EnablePcapAll ("second");
pointToPoint.EnablePcap ("second", p2pNodes.Get (0)->GetId (), 0);
csma.EnablePcap ("third", csmaDevices.Get (0), true);
pointToPoint.EnableAsciiAll (ascii.CreateFileStream ("myfirst.tr"));

What may not be obvious, though, is that there is a consistent model for all of the trace-related methods found in the system. We will now take a little time and take a look at the “big picture”.

There are currently two primary use cases of the tracing helpers in ns-3: device helpers and protocol helpers. Device helpers look at the problem of specifying which traces should be enabled through a (node, device) pair. For example, you may want to specify that PCAP tracing should be enabled on a particular device on a specific node. This follows from the ns-3 device conceptual model, and also the conceptual models of the various device helpers. Following naturally from this, the files created follow a <prefix>-<node>-<device> naming convention.

Protocol helpers look at the problem of specifying which traces should be enabled through a protocol and interface pair. This follows from the ns-3 protocol stack conceptual model, and also the conceptual models of internet stack helpers. Naturally, the trace files should follow a <prefix>-<protocol>-<interface> naming convention.

The trace helpers therefore fall naturally into a two-dimensional taxonomy. There are subtleties that prevent all four classes from behaving identically, but we do strive to make them all work as similarly as possible; and whenever possible there are analogs for all methods in all classes.

  PCAP ASCII
Device Helper \checkmark \checkmark
Protocol Helper \checkmark \checkmark

We use an approach called a mixin to add tracing functionality to our helper classes. A mixin is a class that provides functionality when it is inherited by a subclass. Inheriting from a mixin is not considered a form of specialization but is really a way to collect functionality.

Let’s take a quick look at all four of these cases and their respective mixins.

Device Helpers

PCAP

The goal of these helpers is to make it easy to add a consistent PCAP trace facility to an ns-3 device. We want all of the various flavors of PCAP tracing to work the same across all devices, so the methods of these helpers are inherited by device helpers. Take a look at src/network/helper/trace-helper.h if you want to follow the discussion while looking at real code.

The class PcapHelperForDevice is a mixin provides the high level functionality for using PCAP tracing in an ns-3 device. Every device must implement a single virtual method inherited from this class.

virtual void EnablePcapInternal (std::string prefix, Ptr<NetDevice> nd, bool promiscuous, bool explicitFilename) = 0;

The signature of this method reflects the device-centric view of the situation at this level. All of the public methods inherited from class PcapUserHelperForDevice reduce to calling this single device-dependent implementation method. For example, the lowest level PCAP method,

void EnablePcap (std::string prefix, Ptr<NetDevice> nd, bool promiscuous = false, bool explicitFilename = false);

will call the device implementation of EnablePcapInternal directly. All other public PCAP tracing methods build on this implementation to provide additional user-level functionality. What this means to the user is that all device helpers in the system will have all of the PCAP trace methods available; and these methods will all work in the same way across devices if the device implements EnablePcapInternal correctly.

Methods
void EnablePcap (std::string prefix, Ptr<NetDevice> nd, bool promiscuous = false, bool explicitFilename = false);
void EnablePcap (std::string prefix, std::string ndName, bool promiscuous = false, bool explicitFilename = false);
void EnablePcap (std::string prefix, NetDeviceContainer d, bool promiscuous = false);
void EnablePcap (std::string prefix, NodeContainer n, bool promiscuous = false);
void EnablePcap (std::string prefix, uint32_t nodeid, uint32_t deviceid, bool promiscuous = false);
void EnablePcapAll (std::string prefix, bool promiscuous = false);

In each of the methods shown above, there is a default parameter called promiscuous that defaults to false. This parameter indicates that the trace should not be gathered in promiscuous mode. If you do want your traces to include all traffic seen by the device (and if the device supports a promiscuous mode) simply add a true parameter to any of the calls above. For example,

Ptr<NetDevice> nd;
...
helper.EnablePcap ("prefix", nd, true);

will enable promiscuous mode captures on the NetDevice specified by nd.

The first two methods also include a default parameter called explicitFilename that will be discussed below.

You are encouraged to peruse the API Documentation for class PcapHelperForDevice to find the details of these methods; but to summarize ...

  • You can enable PCAP tracing on a particular node/net-device pair by providing a Ptr<NetDevice> to an EnablePcap method. The Ptr<Node> is implicit since the net device must belong to exactly one Node. For example,

    Ptr<NetDevice> nd;
    ...
    helper.EnablePcap ("prefix", nd);
    
  • You can enable PCAP tracing on a particular node/net-device pair by providing a std::string representing an object name service string to an EnablePcap method. The Ptr<NetDevice> is looked up from the name string. Again, the <Node> is implicit since the named net device must belong to exactly one Node. For example,

    Names::Add ("server" ...);
    Names::Add ("server/eth0" ...);
    ...
    helper.EnablePcap ("prefix", "server/ath0");
    
  • You can enable PCAP tracing on a collection of node/net-device pairs by providing a NetDeviceContainer. For each NetDevice in the container the type is checked. For each device of the proper type (the same type as is managed by the device helper), tracing is enabled. Again, the <Node> is implicit since the found net device must belong to exactly one Node. For example,

    NetDeviceContainer d = ...;
    ...
    helper.EnablePcap ("prefix", d);
    
  • You can enable PCAP tracing on a collection of node/net-device pairs by providing a NodeContainer. For each Node in the NodeContainer its attached NetDevices are iterated. For each NetDevice attached to each Node in the container, the type of that device is checked. For each device of the proper type (the same type as is managed by the device helper), tracing is enabled.

    NodeContainer n;
    ...
    helper.EnablePcap ("prefix", n);
    
  • You can enable PCAP tracing on the basis of Node ID and device ID as well as with explicit Ptr. Each Node in the system has an integer Node ID and each device connected to a Node has an integer device ID.

    helper.EnablePcap ("prefix", 21, 1);
    
  • Finally, you can enable PCAP tracing for all devices in the system, with the same type as that managed by the device helper.

    helper.EnablePcapAll ("prefix");
    
Filenames

Implicit in the method descriptions above is the construction of a complete filename by the implementation method. By convention, PCAP traces in the ns-3 system are of the form <prefix>-<node id>-<device id>.pcap

As previously mentioned, every Node in the system will have a system-assigned Node id; and every device will have an interface index (also called a device id) relative to its node. By default, then, a PCAP trace file created as a result of enabling tracing on the first device of Node 21 using the prefix “prefix” would be prefix-21-1.pcap.

You can always use the ns-3 object name service to make this more clear. For example, if you use the object name service to assign the name “server” to Node 21, the resulting PCAP trace file name will automatically become, prefix-server-1.pcap and if you also assign the name “eth0” to the device, your PCAP file name will automatically pick this up and be called prefix-server-eth0.pcap.

Finally, two of the methods shown above,

void EnablePcap (std::string prefix, Ptr<NetDevice> nd, bool promiscuous = false, bool explicitFilename = false);
void EnablePcap (std::string prefix, std::string ndName, bool promiscuous = false, bool explicitFilename = false);

have a default parameter called explicitFilename. When set to true, this parameter disables the automatic filename completion mechanism and allows you to create an explicit filename. This option is only available in the methods which enable PCAP tracing on a single device.

For example, in order to arrange for a device helper to create a single promiscuous PCAP capture file of a specific name my-pcap-file.pcap on a given device, one could:

Ptr<NetDevice> nd;
...
helper.EnablePcap ("my-pcap-file.pcap", nd, true, true);

The first true parameter enables promiscuous mode traces and the second tells the helper to interpret the prefix parameter as a complete filename.

ASCII

The behavior of the ASCII trace helper mixin is substantially similar to the PCAP version. Take a look at src/network/helper/trace-helper.h if you want to follow the discussion while looking at real code.

The class AsciiTraceHelperForDevice adds the high level functionality for using ASCII tracing to a device helper class. As in the PCAP case, every device must implement a single virtual method inherited from the ASCII trace mixin.

virtual void EnableAsciiInternal (Ptr<OutputStreamWrapper> stream,
                                  std::string prefix,
                                  Ptr<NetDevice> nd,
                                  bool explicitFilename) = 0;

The signature of this method reflects the device-centric view of the situation at this level; and also the fact that the helper may be writing to a shared output stream. All of the public ASCII-trace-related methods inherited from class AsciiTraceHelperForDevice reduce to calling this single device- dependent implementation method. For example, the lowest level ascii trace methods,

void EnableAscii (std::string prefix, Ptr<NetDevice> nd, bool explicitFilename = false);
void EnableAscii (Ptr<OutputStreamWrapper> stream, Ptr<NetDevice> nd);

will call the device implementation of EnableAsciiInternal directly, providing either a valid prefix or stream. All other public ASCII tracing methods will build on these low-level functions to provide additional user-level functionality. What this means to the user is that all device helpers in the system will have all of the ASCII trace methods available; and these methods will all work in the same way across devices if the devices implement EnablAsciiInternal correctly.

Methods
void EnableAscii (std::string prefix, Ptr<NetDevice> nd, bool explicitFilename = false);
void EnableAscii (Ptr<OutputStreamWrapper> stream, Ptr<NetDevice> nd);

void EnableAscii (std::string prefix, std::string ndName, bool explicitFilename = false);
void EnableAscii (Ptr<OutputStreamWrapper> stream, std::string ndName);

void EnableAscii (std::string prefix, NetDeviceContainer d);
void EnableAscii (Ptr<OutputStreamWrapper> stream, NetDeviceContainer d);

void EnableAscii (std::string prefix, NodeContainer n);
void EnableAscii (Ptr<OutputStreamWrapper> stream, NodeContainer n);

void EnableAsciiAll (std::string prefix);
void EnableAsciiAll (Ptr<OutputStreamWrapper> stream);

void EnableAscii (std::string prefix, uint32_t nodeid, uint32_t deviceid, bool explicitFilename);
void EnableAscii (Ptr<OutputStreamWrapper> stream, uint32_t nodeid, uint32_t deviceid);

You are encouraged to peruse the API Documentation for class AsciiTraceHelperForDevice to find the details of these methods; but to summarize ...

  • There are twice as many methods available for ASCII tracing as there were for PCAP tracing. This is because, in addition to the PCAP-style model where traces from each unique node/device pair are written to a unique file, we support a model in which trace information for many node/device pairs is written to a common file. This means that the <prefix>-<node>-<device> file name generation mechanism is replaced by a mechanism to refer to a common file; and the number of API methods is doubled to allow all combinations.
  • Just as in PCAP tracing, you can enable ASCII tracing on a particular (node, net-device) pair by providing a Ptr<NetDevice> to an EnableAscii method. The Ptr<Node> is implicit since the net device must belong to exactly one Node. For example,
Ptr<NetDevice> nd;
...
helper.EnableAscii ("prefix", nd);
  • The first four methods also include a default parameter called explicitFilename that operate similar to equivalent parameters in the PCAP case.

    In this case, no trace contexts are written to the ASCII trace file since they would be redundant. The system will pick the file name to be created using the same rules as described in the PCAP section, except that the file will have the suffix .tr instead of .pcap.

  • If you want to enable ASCII tracing on more than one net device and have all traces sent to a single file, you can do that as well by using an object to refer to a single file. We have already seen this in the “cwnd” example above:

    Ptr<NetDevice> nd1;
    Ptr<NetDevice> nd2;
    ...
    Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("trace-file-name.tr");
    ...
    helper.EnableAscii (stream, nd1);
    helper.EnableAscii (stream, nd2);
    

    In this case, trace contexts are written to the ASCII trace file since they are required to disambiguate traces from the two devices. Note that since the user is completely specifying the file name, the string should include the ,tr suffix for consistency.

  • You can enable ASCII tracing on a particular (node, net-device) pair by providing a std::string representing an object name service string to an EnablePcap method. The Ptr<NetDevice> is looked up from the name string. Again, the <Node> is implicit since the named net device must belong to exactly one Node. For example,

    Names::Add ("client" ...);
    Names::Add ("client/eth0" ...);
    Names::Add ("server" ...);
    Names::Add ("server/eth0" ...);
    ...
    helper.EnableAscii ("prefix", "client/eth0");
    helper.EnableAscii ("prefix", "server/eth0");
    
    This would result in two files named ``prefix-client-eth0.tr`` and
    ``prefix-server-eth0.tr`` with traces for each device in the
    respective trace file.  Since all of the ``EnableAscii`` functions
    are overloaded to take a stream wrapper, you can use that form as
    well::
    
    Names::Add ("client" ...);
    Names::Add ("client/eth0" ...);
    Names::Add ("server" ...);
    Names::Add ("server/eth0" ...);
    ...
    Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("trace-file-name.tr");
    ...
    helper.EnableAscii (stream, "client/eth0");
    helper.EnableAscii (stream, "server/eth0");
    

    This would result in a single trace file called trace-file-name.tr that contains all of the trace events for both devices. The events would be disambiguated by trace context strings.

  • You can enable ASCII tracing on a collection of (node, net-device) pairs by providing a NetDeviceContainer. For each NetDevice in the container the type is checked. For each device of the proper type (the same type as is managed by the device helper), tracing is enabled. Again, the <Node> is implicit since the found net device must belong to exactly one Node. For example,

    NetDeviceContainer d = ...;
    ...
    helper.EnableAscii ("prefix", d);
    
    This would result in a number of ASCII trace files being created,
    each of which follows the ``<prefix>-<node id>-<device id>.tr``
    convention.
    

    Combining all of the traces into a single file is accomplished similarly to the examples above:

    NetDeviceContainer d = ...;
    ...
    Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("trace-file-name.tr");
    ...
    helper.EnableAscii (stream, d);
    
  • You can enable ASCII tracing on a collection of (node, net-device) pairs by providing a NodeContainer. For each Node in the NodeContainer its attached NetDevices are iterated. For each NetDevice attached to each Node in the container, the type of that device is checked. For each device of the proper type (the same type as is managed by the device helper), tracing is enabled.

    NodeContainer n;
    ...
    helper.EnableAscii ("prefix", n);
    

    This would result in a number of ASCII trace files being created, each of which follows the <prefix>-<node id>-<device id>.tr convention. Combining all of the traces into a single file is accomplished similarly to the examples above.

  • You can enable PCAP tracing on the basis of Node ID and device ID as well as with explicit Ptr. Each Node in the system has an integer Node ID and each device connected to a Node has an integer device ID.

    helper.EnableAscii ("prefix", 21, 1);
    

    Of course, the traces can be combined into a single file as shown above.

  • Finally, you can enable PCAP tracing for all devices in the system, with the same type as that managed by the device helper.

    helper.EnableAsciiAll ("prefix");
    

    This would result in a number of ASCII trace files being created, one for every device in the system of the type managed by the helper. All of these files will follow the <prefix>-<node id>-<device id>.tr convention. Combining all of the traces into a single file is accomplished similarly to the examples above.

Filenames

Implicit in the prefix-style method descriptions above is the construction of the complete filenames by the implementation method. By convention, ASCII traces in the ns-3 system are of the form <prefix>-<node id>-<device id>.tr

As previously mentioned, every Node in the system will have a system-assigned Node id; and every device will have an interface index (also called a device id) relative to its node. By default, then, an ASCII trace file created as a result of enabling tracing on the first device of Node 21, using the prefix “prefix”, would be prefix-21-1.tr.

You can always use the ns-3 object name service to make this more clear. For example, if you use the object name service to assign the name “server” to Node 21, the resulting ASCII trace file name will automatically become, prefix-server-1.tr and if you also assign the name “eth0” to the device, your ASCII trace file name will automatically pick this up and be called prefix-server-eth0.tr.

Several of the methods have a default parameter called explicitFilename. When set to true, this parameter disables the automatic filename completion mechanism and allows you to create an explicit filename. This option is only available in the methods which take a prefix and enable tracing on a single device.

Protocol Helpers

PCAP

The goal of these mixins is to make it easy to add a consistent PCAP trace facility to protocols. We want all of the various flavors of PCAP tracing to work the same across all protocols, so the methods of these helpers are inherited by stack helpers. Take a look at src/network/helper/trace-helper.h if you want to follow the discussion while looking at real code.

In this section we will be illustrating the methods as applied to the protocol Ipv4. To specify traces in similar protocols, just substitute the appropriate type. For example, use a Ptr<Ipv6> instead of a Ptr<Ipv4> and call EnablePcapIpv6 instead of EnablePcapIpv4.

The class PcapHelperForIpv4 provides the high level functionality for using PCAP tracing in the Ipv4 protocol. Each protocol helper enabling these methods must implement a single virtual method inherited from this class. There will be a separate implementation for Ipv6, for example, but the only difference will be in the method names and signatures. Different method names are required to disambiguate class Ipv4 from Ipv6 which are both derived from class Object, and methods that share the same signature.

virtual void EnablePcapIpv4Internal (std::string prefix,
                                     Ptr<Ipv4> ipv4,
                                     uint32_t interface,
                                     bool explicitFilename) = 0;

The signature of this method reflects the protocol and interface-centric view of the situation at this level. All of the public methods inherited from class PcapHelperForIpv4 reduce to calling this single device-dependent implementation method. For example, the lowest level PCAP method,

void EnablePcapIpv4 (std::string prefix, Ptr<Ipv4> ipv4, uint32_t interface, bool explicitFilename = false);

will call the device implementation of EnablePcapIpv4Internal directly. All other public PCAP tracing methods build on this implementation to provide additional user-level functionality. What this means to the user is that all protocol helpers in the system will have all of the PCAP trace methods available; and these methods will all work in the same way across protocols if the helper implements EnablePcapIpv4Internal correctly.

Methods

These methods are designed to be in one-to-one correspondence with the Node- and NetDevice- centric versions of the device versions. Instead of Node and NetDevice pair constraints, we use protocol and interface constraints.

Note that just like in the device version, there are six methods:

void EnablePcapIpv4 (std::string prefix, Ptr<Ipv4> ipv4, uint32_t interface, bool explicitFilename = false);
void EnablePcapIpv4 (std::string prefix, std::string ipv4Name, uint32_t interface, bool explicitFilename = false);
void EnablePcapIpv4 (std::string prefix, Ipv4InterfaceContainer c);
void EnablePcapIpv4 (std::string prefix, NodeContainer n);
void EnablePcapIpv4 (std::string prefix, uint32_t nodeid, uint32_t interface, bool explicitFilename);
void EnablePcapIpv4All (std::string prefix);

You are encouraged to peruse the API Documentation for class PcapHelperForIpv4 to find the details of these methods; but to summarize ...

  • You can enable PCAP tracing on a particular protocol/interface pair by providing a Ptr<Ipv4> and interface to an EnablePcap method. For example,

    Ptr<Ipv4> ipv4 = node->GetObject<Ipv4> ();
    ...
    helper.EnablePcapIpv4 ("prefix", ipv4, 0);
    
  • You can enable PCAP tracing on a particular node/net-device pair by providing a std::string representing an object name service string to an EnablePcap method. The Ptr<Ipv4> is looked up from the name string. For example,

    Names::Add ("serverIPv4" ...);
    ...
    helper.EnablePcapIpv4 ("prefix", "serverIpv4", 1);
    
  • You can enable PCAP tracing on a collection of protocol/interface pairs by providing an Ipv4InterfaceContainer. For each Ipv4 / interface pair in the container the protocol type is checked. For each protocol of the proper type (the same type as is managed by the device helper), tracing is enabled for the corresponding interface. For example,

    NodeContainer nodes;
    ...
    NetDeviceContainer devices = deviceHelper.Install (nodes);
    ...
    Ipv4AddressHelper ipv4;
    ipv4.SetBase ("10.1.1.0", "255.255.255.0");
    Ipv4InterfaceContainer interfaces = ipv4.Assign (devices);
    ...
    helper.EnablePcapIpv4 ("prefix", interfaces);
    
  • You can enable PCAP tracing on a collection of protocol/interface pairs by providing a NodeContainer. For each Node in the NodeContainer the appropriate protocol is found. For each protocol, its interfaces are enumerated and tracing is enabled on the resulting pairs. For example,

    NodeContainer n;
    ...
    helper.EnablePcapIpv4 ("prefix", n);
    
  • You can enable PCAP tracing on the basis of Node ID and interface as well. In this case, the node-id is translated to a Ptr<Node> and the appropriate protocol is looked up in the node. The resulting protocol and interface are used to specify the resulting trace source.

    helper.EnablePcapIpv4 ("prefix", 21, 1);
    
  • Finally, you can enable PCAP tracing for all interfaces in the system, with associated protocol being the same type as that managed by the device helper.

    helper.EnablePcapIpv4All ("prefix");
    
Filenames

Implicit in all of the method descriptions above is the construction of the complete filenames by the implementation method. By convention, PCAP traces taken for devices in the ns-3 system are of the form “<prefix>-<node id>-<device id>.pcap”. In the case of protocol traces, there is a one-to-one correspondence between protocols and Nodes. This is because protocol Objects are aggregated to Node Objects. Since there is no global protocol id in the system, we use the corresponding Node id in file naming. Therefore there is a possibility for file name collisions in automatically chosen trace file names. For this reason, the file name convention is changed for protocol traces.

As previously mentioned, every Node in the system will have a system-assigned Node id. Since there is a one-to-one correspondence between protocol instances and Node instances we use the Node id. Each interface has an interface id relative to its protocol. We use the convention “<prefix>-n<node id>-i<interface id>.pcap” for trace file naming in protocol helpers.

Therefore, by default, a PCAP trace file created as a result of enabling tracing on interface 1 of the Ipv4 protocol of Node 21 using the prefix “prefix” would be “prefix-n21-i1.pcap”.

You can always use the ns-3 object name service to make this more clear. For example, if you use the object name service to assign the name “serverIpv4” to the Ptr<Ipv4> on Node 21, the resulting PCAP trace file name will automatically become, “prefix-nserverIpv4-i1.pcap”.

Several of the methods have a default parameter called explicitFilename. When set to true, this parameter disables the automatic filename completion mechanism and allows you to create an explicit filename. This option is only available in the methods which take a prefix and enable tracing on a single device.

ASCII

The behavior of the ASCII trace helpers is substantially similar to the PCAP case. Take a look at src/network/helper/trace-helper.h if you want to follow the discussion while looking at real code.

In this section we will be illustrating the methods as applied to the protocol Ipv4. To specify traces in similar protocols, just substitute the appropriate type. For example, use a Ptr<Ipv6> instead of a Ptr<Ipv4> and call EnableAsciiIpv6 instead of EnableAsciiIpv4.

The class AsciiTraceHelperForIpv4 adds the high level functionality for using ASCII tracing to a protocol helper. Each protocol that enables these methods must implement a single virtual method inherited from this class.

virtual void EnableAsciiIpv4Internal (Ptr<OutputStreamWrapper> stream,
                                      std::string prefix,
                                      Ptr<Ipv4> ipv4,
                                      uint32_t interface,
                                      bool explicitFilename) = 0;

The signature of this method reflects the protocol- and interface-centric view of the situation at this level; and also the fact that the helper may be writing to a shared output stream. All of the public methods inherited from class PcapAndAsciiTraceHelperForIpv4 reduce to calling this single device- dependent implementation method. For example, the lowest level ASCII trace methods,

void EnableAsciiIpv4 (std::string prefix, Ptr<Ipv4> ipv4, uint32_t interface, bool explicitFilename = false);
void EnableAsciiIpv4 (Ptr<OutputStreamWrapper> stream, Ptr<Ipv4> ipv4, uint32_t interface);

will call the device implementation of EnableAsciiIpv4Internal directly, providing either the prefix or the stream. All other public ASCII tracing methods will build on these low-level functions to provide additional user-level functionality. What this means to the user is that all device helpers in the system will have all of the ASCII trace methods available; and these methods will all work in the same way across protocols if the protocols implement EnablAsciiIpv4Internal correctly.

Methods
void EnableAsciiIpv4 (std::string prefix, Ptr<Ipv4> ipv4, uint32_t interface, bool explicitFilename = false);
void EnableAsciiIpv4 (Ptr<OutputStreamWrapper> stream, Ptr<Ipv4> ipv4, uint32_t interface);

void EnableAsciiIpv4 (std::string prefix, std::string ipv4Name, uint32_t interface, bool explicitFilename = false);
void EnableAsciiIpv4 (Ptr<OutputStreamWrapper> stream, std::string ipv4Name, uint32_t interface);

void EnableAsciiIpv4 (std::string prefix, Ipv4InterfaceContainer c);
void EnableAsciiIpv4 (Ptr<OutputStreamWrapper> stream, Ipv4InterfaceContainer c);

void EnableAsciiIpv4 (std::string prefix, NodeContainer n);
void EnableAsciiIpv4 (Ptr<OutputStreamWrapper> stream, NodeContainer n);

void EnableAsciiIpv4All (std::string prefix);
void EnableAsciiIpv4All (Ptr<OutputStreamWrapper> stream);

void EnableAsciiIpv4 (std::string prefix, uint32_t nodeid, uint32_t deviceid, bool explicitFilename);
void EnableAsciiIpv4 (Ptr<OutputStreamWrapper> stream, uint32_t nodeid, uint32_t interface);

You are encouraged to peruse the API Documentation for class PcapAndAsciiHelperForIpv4 to find the details of these methods; but to summarize ...

  • There are twice as many methods available for ASCII tracing as there were for PCAP tracing. This is because, in addition to the PCAP-style model where traces from each unique protocol/interface pair are written to a unique file, we support a model in which trace information for many protocol/interface pairs is written to a common file. This means that the <prefix>-n<node id>-<interface> file name generation mechanism is replaced by a mechanism to refer to a common file; and the number of API methods is doubled to allow all combinations.

  • Just as in PCAP tracing, you can enable ASCII tracing on a particular protocol/interface pair by providing a Ptr<Ipv4> and an interface to an EnableAscii method. For example,

    Ptr<Ipv4> ipv4;
    ...
    helper.EnableAsciiIpv4 ("prefix", ipv4, 1);
    

    In this case, no trace contexts are written to the ASCII trace file since they would be redundant. The system will pick the file name to be created using the same rules as described in the PCAP section, except that the file will have the suffix ”.tr” instead of ”.pcap”.

  • If you want to enable ASCII tracing on more than one interface and have all traces sent to a single file, you can do that as well by using an object to refer to a single file. We have already something similar to this in the “cwnd” example above:

    Ptr<Ipv4> protocol1 = node1->GetObject<Ipv4> ();
    Ptr<Ipv4> protocol2 = node2->GetObject<Ipv4> ();
    ...
    Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("trace-file-name.tr");
    ...
    helper.EnableAsciiIpv4 (stream, protocol1, 1);
    helper.EnableAsciiIpv4 (stream, protocol2, 1);
    

    In this case, trace contexts are written to the ASCII trace file since they are required to disambiguate traces from the two interfaces. Note that since the user is completely specifying the file name, the string should include the ”,tr” for consistency.

  • You can enable ASCII tracing on a particular protocol by providing a std::string representing an object name service string to an EnablePcap method. The Ptr<Ipv4> is looked up from the name string. The <Node> in the resulting filenames is implicit since there is a one-to-one correspondence between protocol instances and nodes, For example,

    Names::Add ("node1Ipv4" ...);
    Names::Add ("node2Ipv4" ...);
    ...
    helper.EnableAsciiIpv4 ("prefix", "node1Ipv4", 1);
    helper.EnableAsciiIpv4 ("prefix", "node2Ipv4", 1);
    

    This would result in two files named “prefix-nnode1Ipv4-i1.tr” and “prefix-nnode2Ipv4-i1.tr” with traces for each interface in the respective trace file. Since all of the EnableAscii functions are overloaded to take a stream wrapper, you can use that form as well:

    Names::Add ("node1Ipv4" ...);
    Names::Add ("node2Ipv4" ...);
    ...
    Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("trace-file-name.tr");
    ...
    helper.EnableAsciiIpv4 (stream, "node1Ipv4", 1);
    helper.EnableAsciiIpv4 (stream, "node2Ipv4", 1);
    

    This would result in a single trace file called “trace-file-name.tr” that contains all of the trace events for both interfaces. The events would be disambiguated by trace context strings.

  • You can enable ASCII tracing on a collection of protocol/interface pairs by providing an Ipv4InterfaceContainer. For each protocol of the proper type (the same type as is managed by the device helper), tracing is enabled for the corresponding interface. Again, the <Node> is implicit since there is a one-to-one correspondence between each protocol and its node. For example,

    NodeContainer nodes;
    ...
    NetDeviceContainer devices = deviceHelper.Install (nodes);
    ...
    Ipv4AddressHelper ipv4;
    ipv4.SetBase ("10.1.1.0", "255.255.255.0");
    Ipv4InterfaceContainer interfaces = ipv4.Assign (devices);
    ...
    ...
    helper.EnableAsciiIpv4 ("prefix", interfaces);
    

    This would result in a number of ASCII trace files being created, each of which follows the <prefix>-n<node id>-i<interface>.tr convention. Combining all of the traces into a single file is accomplished similarly to the examples above:

    NodeContainer nodes;
    ...
    NetDeviceContainer devices = deviceHelper.Install (nodes);
    ...
    Ipv4AddressHelper ipv4;
    ipv4.SetBase ("10.1.1.0", "255.255.255.0");
    Ipv4InterfaceContainer interfaces = ipv4.Assign (devices);
    ...
    Ptr<OutputStreamWrapper> stream = asciiTraceHelper.CreateFileStream ("trace-file-name.tr");
    ...
    helper.EnableAsciiIpv4 (stream, interfaces);
    
  • You can enable ASCII tracing on a collection of protocol/interface pairs by providing a NodeContainer. For each Node in the NodeContainer the appropriate protocol is found. For each protocol, its interfaces are enumerated and tracing is enabled on the resulting pairs. For example,

    NodeContainer n;
    ...
    helper.EnableAsciiIpv4 ("prefix", n);
    

    This would result in a number of ASCII trace files being created, each of which follows the <prefix>-<node id>-<device id>.tr convention. Combining all of the traces into a single file is accomplished similarly to the examples above.

  • You can enable PCAP tracing on the basis of Node ID and device ID as well. In this case, the node-id is translated to a Ptr<Node> and the appropriate protocol is looked up in the node. The resulting protocol and interface are used to specify the resulting trace source.

    helper.EnableAsciiIpv4 ("prefix", 21, 1);
    

    Of course, the traces can be combined into a single file as shown above.

  • Finally, you can enable ASCII tracing for all interfaces in the system, with associated protocol being the same type as that managed by the device helper.

    helper.EnableAsciiIpv4All ("prefix");
    

    This would result in a number of ASCII trace files being created, one for every interface in the system related to a protocol of the type managed by the helper. All of these files will follow the <prefix>-n<node id>-i<interface.tr convention. Combining all of the traces into a single file is accomplished similarly to the examples above.

Filenames

Implicit in the prefix-style method descriptions above is the construction of the complete filenames by the implementation method. By convention, ASCII traces in the ns-3 system are of the form “<prefix>-<node id>-<device id>.tr”

As previously mentioned, every Node in the system will have a system-assigned Node id. Since there is a one-to-one correspondence between protocols and nodes we use to node-id to identify the protocol identity. Every interface on a given protocol will have an interface index (also called simply an interface) relative to its protocol. By default, then, an ASCII trace file created as a result of enabling tracing on the first device of Node 21, using the prefix “prefix”, would be “prefix-n21-i1.tr”. Use the prefix to disambiguate multiple protocols per node.

You can always use the ns-3 object name service to make this more clear. For example, if you use the object name service to assign the name “serverIpv4” to the protocol on Node 21, and also specify interface one, the resulting ASCII trace file name will automatically become, “prefix-nserverIpv4-1.tr”.

Several of the methods have a default parameter called explicitFilename. When set to true, this parameter disables the automatic filename completion mechanism and allows you to create an explicit filename. This option is only available in the methods which take a prefix and enable tracing on a single device.

Summary

ns-3 includes an extremely rich environment allowing users at several levels to customize the kinds of information that can be extracted from simulations.

There are high-level helper functions that allow users to simply control the collection of pre-defined outputs to a fine granularity. There are mid-level helper functions to allow more sophisticated users to customize how information is extracted and saved; and there are low-level core functions to allow expert users to alter the system to present new and previously unexported information in a way that will be immediately accessible to users at higher levels.

This is a very comprehensive system, and we realize that it is a lot to digest, especially for new users or those not intimately familiar with C++ and its idioms. We do consider the tracing system a very important part of ns-3 and so recommend becoming as familiar as possible with it. It is probably the case that understanding the rest of the ns-3 system will be quite simple once you have mastered the tracing system