Changes between Version 4 and Version 5 of Tutorials/oMF/tut3


Ignore:
Timestamp:
Oct 21, 2014, 1:44:23 PM (10 years ago)
Author:
wontoniii
Comment:

Legend:

Unmodified
Added
Removed
Modified
  • Tutorials/oMF/tut3

    v4 v5  
    1 = Exercise 3: Socket Programming using New MobilityFirst NetAPI =
     1= Exercise 3: Socket Programming using New !MobilityFirst NetAPI =
    22
    33[[TOC(Tutorials/oMF*, depth=2)]]
     
    88=== Objective: ===
    99
    10 In this exercise we will learn to write, compile, and run a simple content distribution application using !MobilityFirst's new socket API. We will then modify the program to utilize !MobilityFirst's native support for point to multi-point delivery services such as anycast and multicast to enable more flexible delivery options.
     10In this exercise we will learn to write, compile, and run a simple content distribution application using !MobilityFirst's new socket API. In particular we will focus on its Java release. We will then introduce a general view on !MobilityFirst's native support for point to multi-point delivery services such as anycast and multicast to enable more flexible delivery options.
     11
     12=== Pre-requisites ===
     13
     14 * Experimenters are expected to have basic networking knowledge and familiarity with Linux OS and some of its tools (command line tools, ssh, etc.).
     15 * An ORBIT user account.
     16 * Some familiarity with the !MobilityFirst terminology.
     17
     18=== Deploy the Network ===
     19
     20This tutorial assumes that a 4 nodes topology has been already established in one of the Orbit sandboxes or the grid:
     21
     22[[Image(Tutorials/oMF:MFTurorialNetwork.png)]]
     23
     24If not coming from [wiki:Tutorials/oMF/tut1 exercise 1] follow these instructions on how to setup the topology. Running exercise 1 at least once before moving to exercise 2 is advised to understand the steps and software components involved.
     25
     26[[CollapsibleStart(4 nodes topology setup)]]
     27
     28First of all, log in into the grid console using SSH:
     29
     30{{{
     31#!sh
     32
     33ssh username@console.grid.orbit-lab.org
     34}}}
     35
     36For simplicity, open 3 different consoles on your laptop and access the grid's console with all of them; you will need them in the continuation of the exercise.
     37From the console we will start loading the !Mobilityfirst image into the four nodes that have been assigned to you:
     38
     39{{{
     40#!sh
     41
     42omf load -i 'mf-release-latest.ndz' -t system:topo:mf-groupX
     43}}}
     44
     45''system:topo:mf-groupX'' represents the topology of 4 nodes that has been assigned to you're group and ''mf-groupX'' has to be replaced by the group id assigned to you.
     46
     47For example, ''mf-group1'' will load the image on nodes 'node20-20,node20-19,node19-19,node19-20'
     48
     49If at the end of the execution, the final output of your console looks similar to:
     50
     51{{{
     52#!sh
     53
     54INFO exp:  -----------------------------
     55 INFO exp:  Imaging Process Done
     56 INFO exp:  4 nodes successfully imaged - Topology saved in '/tmp/pxe_slice-2014-10-15t02.10.16.594-04.00-topo-success.rb'
     57 INFO exp:  -----------------------------
     58 INFO EXPERIMENT_DONE: Event triggered. Starting the associated tasks.
     59 INFO NodeHandler:
     60 INFO NodeHandler: Shutting down experiment, please wait...
     61 INFO NodeHandler:
     62 INFO NodeHandler: Shutdown flag is set - Turning Off the resources
     63 INFO run: Experiment pxe_slice-2014-10-15t02.10.16.594-04.00 finished after 1:50
     64}}}
     65
     66your nodes have been imaged correctly.
     67
     68=== Deploy Network ===
     69
     70Software and experiment control in the ORBIT testbed can be automated greatly using the OMF framework. An OMF control script is written in Ruby and allows the experimenter to specify the set of nodes, their network configuration, to specify software components and arguments, and to control their execution on one or more nodes. We will use an OMF script to bring up 4 ORBIT nodes to host our topology, with the corresponding software components.
     71
     72We will first introduce the main details of the scripts that will be run and then we will step to the execution process itself.
     73
     74==== Software Component Specification ====
     75
     76The following snippet shows the specification of the !MobilityFirst components along with the required arguments. A typical application will have at least a brief description, a path for the associated binary to execute and a list of properties that correspond to the parameters  that will be passed once starting the executable.
     77
     78{{{
     79#!ruby
     80
     81#Application definition of a MobilityFirst access router
     82defApplication('MF-Router', 'router') {|app|
     83        app.shortDescription = "Click-based MobilityFirst Access Router"
     84        app.path = "/usr/local/src/mobilityfirst/eval/orbit/tutorial/scripts/ARWrapper.sh"
     85        # click options
     86        app.defProperty('num_threads', 'number of threads', "-t",{:type => :integer, :mandatory => true, :default => 4, :order => 1})
     87        app.defProperty('ctrl_port', 'port for Click control socket', "-c",{:type => :integer, :order => 2})
     88        # click config file
     89        app.defProperty('config_file', 'Click configuration file', "-C",{:type => :string,:mandatory=> true})
     90        # keyword parameters used in click config file
     91        app.defProperty('my_GUID', 'router GUID', "-m",{:type => :string, :mandatory => true})
     92        app.defProperty('topo_file', 'path to topology file', "-f",{:type => :string, :mandatory => true})
     93        app.defProperty('core_dev', 'core network interface', "-d",{:type => :string,:mandatory => true})
     94        app.defProperty('GNRS_server_ip', 'IP of local GNRS server', "-s",{:type => :string,:mandatory => true})
     95        app.defProperty('GNRS_server_port', 'Port of GNRS server', "-p",{:type => :string,:mandatory => true})
     96        app.defProperty('GNRS_listen_ip', 'IP to listen for GNRS response', "-i",{:type => :string,:default => "0.0.0.0"})
     97        app.defProperty('GNRS_listen_port', 'port to listen for GNRS response', "-P",{:type => :string,:default => "10001"})
     98        app.defProperty('edge_dev', 'edge network interface', "-D",{:type => :string,:mandatory => true})
     99        app.defProperty('edge_dev_ip', 'IP assigned to edge interface', "-I",{:type => :string,:mandatory => true})
     100}
     101
     102#Application definition of a GNRS server
     103defApplication('MF-GNRS', 'gnrs') {|app|
     104        app.shortDescription = "GNRS Server"
     105        app.path = "/usr/local/src/mobilityfirst/eval/orbit/tutorial/scripts/GNRSWrapper.sh"
     106        app.defProperty('log4j_config_file', 'log 4j configuration file', "-d",{:type => :string, :order => 1})
     107        app.defProperty('jar_file', 'server jar file with all dependencies', "-j" ,{:type => :string, :mandatory=> true, :default => "/usr/local/src/mobilityfirst/gnrs/jserver/target/gnrs-server-1.0.0-SNAPSHOT-jar-with-dependencies.jar", :order => 2})
     108        app.defProperty('config_file', 'server configuration file', "-c",{:type => :string, :mandatory=> true, :order => 3})
     109}
     110
     111
     112#Application definition of the client network protocol stack
     113defApplication('MF-HostStack', 'hoststack') {|app|
     114        app.shortDescription = "MF host network stack"
     115        app.path = "/usr/local/bin/mfstack"
     116        app.defProperty('log_level', 'log level', nil,{:type => :string, :mandatory => true, :order => 1, :default => "-e"}) # default is 'error'
     117        app.defProperty('config_file', 'stack configuration file', nil,{:type => :string, :mandatory => true, :order => 2})
     118}
     119}}}
     120
     121A few considerations on the defined applications:
     122
     123 * As seen above, the router is configured with both 'core' (''core_dev'') and 'edge' (''edge_dev'') interfaces. Different router configurations are available depending on the required functionality. In this case we use what we call a !MobilityFirst Access Router, which has the particularity of having the core interfaces connected towards the core of the network, while the edge interface enables hosts to connect and access the !MobilityFirst network.
     124
     125 * For this basic setup, the GNRS has been configured to be running as a single server instance, but in a larger experiment, it is designed to be a distributed system deployed at different locations.
     126
     127 * Most of the client settings are located in a configuration file pre-loaded on the ORBIT image in the folder ''/usr/local/src/mobilityfirst/eval/orbit/conf/''.
     128
     129==== Setting up the Software Node Groups ====
     130
     131The following snippet shows how the node groups for the routers are setup in the OMF control scripts. Node groups allow experimenters to use single statements to set configuration (e.g. network interfaces) and execute commands across all nodes belonging to the group.
     132
     133{{{
     134#!ruby
     135
     136#Create router groups
     137for i in 1..num_routers
     138        #Create a topology with a single router in it
     139        defTopology("topo:router_#{i}") { |t|
     140                aNode = routersTopo.getNodeByIndex(i-1)
     141                t.addNode(aNode)
     142                info aNode, " assigned role of router with GUID: #{i}"
     143        }
     144 
     145        #Through the group definition we set up the applications to run
     146        defGroup("router_#{i}", "topo:router_#{i}") {|node|
     147                node.addApplication('MF-Router') {|app|
     148                        app.setProperty('num_threads', router_threads)
     149                        app.setProperty('config_file', click_conf)
     150                        app.setProperty('my_GUID', router_guid[i-1])
     151                        app.setProperty('topo_file', rtr_topo_file)
     152                        app.setProperty('core_dev', core_dev)
     153                        app.setProperty('GNRS_server_ip', GNRS_server_ip)
     154                        app.setProperty('GNRS_server_port', GNRS_server_port)
     155                        app.setProperty('GNRS_listen_ip', "192.168.100.#{i}")
     156                        app.setProperty('GNRS_listen_port', GNRS_listen_port)
     157                        app.setProperty('edge_dev', edge_dev)
     158                        app.setProperty('edge_dev_ip', router_ether_if_ip[i-1])
     159                }
     160
     161          #If it is the first router add the GNRS
     162          if i == 1
     163                aNode = routersTopo.getNodeByIndex(i-1)
     164                info aNode, " will also host the GNRS server"
     165                node.addApplication('MF-GNRS') {|app|
     166                      app.setProperty('log4j_config_file', GNRS_log_file)
     167                      app.setProperty('jar_file', GNRS_jar_file)
     168                      app.setProperty('config_file', GNRS_conf_file)
     169                }
     170          end
     171       
     172          #Setup the node interfaces
     173          #The first ethernet interface is used as the core interface
     174          node.net.e0.ip = "192.168.100.#{i}"
     175          node.net.e0.netmask = '255.255.255.0'
     176   
     177          #The first wireless interface is used to give access to clients
     178          node.net.w0.mode = "adhoc"
     179          node.net.w0.type = 'g'
     180          node.net.w0.channel = "11"
     181          node.net.w0.essid = "SSID_group_#{i}"
     182          node.net.w0.ip = "192.168.#{i}.1"
     183        }
     184end
     185
     186#Create host groups
     187for i in 1..num_hosts
     188        #Create a topology with a single router in it
     189        defTopology("topo:host_#{i}") { |t|
     190                aNode = hostsTopo.getNodeByIndex(i-1)
     191                t.addNode(aNode)
     192                info aNode, " assigned role of client with GUID: #{100 + i}"
     193        }
     194 
     195        #Through the group definition we set up the applications to run
     196        defGroup("host_#{i}", "topo:host_#{i}") {|node|
     197                node.addApplication('MF-HostStack') {|app|
     198                        app.setProperty('config_file', hoststack_conf_file[i-1])
     199                        app.setProperty('log_level', log_level)
     200                }
     201   
     202          #The first wifi interface is used to connect to the Access Router
     203          node.net.w0.mode = "adhoc"
     204          node.net.w0.type = 'g'
     205          node.net.w0.channel = "11"
     206          node.net.w0.essid = "SSID_group_#{i}"
     207          node.net.w0.ip = "192.168.#{i}.2"
     208      }
     209end
     210}}}
     211
     212As it can be seen above, once defining applications that each group will execute, the application properties are set accordingly. While we do not want to enter the details of each parameter, it is important to notice how by simple use of counters, the different nodes can be assigned different values.
     213
     214Moreover, resources such node interfaces and their corresponding IP addresses have to be set up in this phase of the experiment. As we discussed earlier the router is configured with both edge and core interfaces. An ethernet interface is used to connect to 2 core routers, while a wireless interface is used to provide access for the clients.
     215
     216[[CollapsibleEnd]]
    11217
    12218=== Develop Sender and Receiver Applications with MF Socket API ===
    13219
    14 The following Java application shows the key pieces of the sender application that sends a file to a receiver known to the sender by its GUID and to then receive a confirmation message from the receiver:
     220As per the goal of the exercise, we will introduce the reader to the new MF Socket API. Two simple Java applications skeletons have been made available on the nodes. These applications consist of a sender, that takes as an input a file and transmit it to a receiver that once has received the complete file anwers back with an acknowledgement.
     221Let's first analyze the sender code:
    15222
    16223{{{
    17224#!java
    18 //Simple class used to test the java api
    19 
    20 
    21 //jmfapi needs to be in the classpath
    22 import java.io.*;
    23 import java.util.*;
    24 import java.nio.file.*;
    25 import edu.rutgers.winlab.jmfapi.*;
    26 import edu.rutgers.winlab.jmfapi.GUID;
    27 
    28 class Sender{
    29     private static void usage(){
    30         System.out.println("Usage:");
    31         System.out.println("sender <my_GUID> <file_to_send> <dst_GUID>");
    32     }
    33     public static void main(String []argv){
    34         if(argv.length < 3){
    35             usage();
    36             return;
    37         }
    38         String scheme = "basic";
    39         GUID srcGUID = null, dstGUID;
    40         srcGUID = new GUID(Integer.parseInt(argv[0]));
    41         Path file = FileSystems.getDefault().getPath(argv[1]);
    42         dstGUID = new GUID(Integer.parseInt(argv[2]));
    43         JMFAPI sender = new JMFAPI();
    44         try{
    45             if(srcGUID!=null) sender.jmfopen(scheme, srcGUID);
    46             else sender.jmfopen(scheme);
    47             byte[] fileArray;
    48             try {
    49                 fileArray = Files.readAllBytes(file);
    50             } catch (IOException e){
    51                 System.out.println("ERROR");
    52                 return;
    53             }
    54             byte[] tempArray;
    55             int ret, read = 0;
    56             while(fileArray.length - read>=1000000){
    57                 tempArray = Arrays.copyOfRange(fileArray, 0, 999999);
    58                 sender.jmfsend(tempArray,1000000, dstGUID);
    59             }
    60             tempArray = Arrays.copyOfRange(fileArray, 0, fileArray.length - read - 1);
    61             sender.jmfsend(tempArray,fileArray.length - read, dstGUID);
    62             sender.jmfclose();
    63             System.out.println("Transmitted file");
    64 
    65                        //TODO receive confirmation
    66 
    67             System.out.println("Received confirmation");
    68 
    69         } catch (JMFException e){
    70             System.out.println(e.toString());
    71         }
    72     }
     225public static void main(String []argv){
     226                if(argv.length < 2){
     227                        usage();
     228                        return;
     229                }
     230               
     231                //The profile describes the nature of the communication that will follow and
     232                //      is used by the network stack to select the best end-to-end transport
     233                //For this application a 'basic' profile is selected providing only a message based
     234                //      transport with no added realiability on top of what offered by the network.
     235                String profile = "basic";
     236                GUID srcGUID = null;
     237               
     238                //A GUID class is used for name based communications
     239                //The destination of the fie has been passed as a parameter
     240                GUID dstGUID = new GUID(Integer.parseInt(argv[1]));
     241                //The source is optional. If a source is not specified, the default GUID of the device is used
     242                if(argv.length == 3) srcGUID = new GUID(Integer.parseInt(argv[2]));
     243               
     244                Path file = FileSystems.getDefault().getPath(argv[0]);
     245               
     246                //The JMFAPI object represents the socket and the API to interact with it
     247                JMFAPI sender = new JMFAPI();
     248               
     249                try{
     250                       
     251                        //The open call creates the communication socket and initializes the resources
     252                        if(srcGUID!=null) sender.jmfopen(profile, srcGUID);
     253                        else sender.jmfopen(profile);
     254                       
     255                        byte[] fileArray;
     256                        try {
     257                                fileArray = Files.readAllBytes(file);
     258                        } catch (IOException e){
     259                                System.out.println("ERROR");
     260                                return;
     261                        }
     262                        System.out.println("Transferring a file of size " + fileArray.length);
     263                        byte[] sizeArray = Utils.intToByteArray(fileArray.length);
     264                        sender.jmfsend(sizeArray, 4, dstGUID);
     265                        int sentBytes;
     266                       
     267                        byte[] tempArray;
     268                        int ret, read = 0;
     269                        int bytesToSend = fileArray.length;
     270                        while(bytesToSend>1000000){
     271                                tempArray = Arrays.copyOfRange(fileArray, 0, 999999);
     272                                //Messages are sent up to 10MB at a time (which is the default buffer size for the socket)
     273                                sentBytes = sender.jmfsend(tempArray,1000000, dstGUID);
     274                                bytesToSend -= sentBytes;
     275                                System.out.println("Transmitted " + sentBytes);
     276                        }
     277                        tempArray = Arrays.copyOfRange(fileArray, 0, bytesToSend - 1);
     278                        sentBytes = sender.jmfsend(tempArray,bytesToSend, dstGUID);
     279                        System.out.println("Transmitted " + sentBytes);
     280                       
     281                        //Receive the confirmation from the receiver
     282                        //The first parameter is set to null but could be used to obtain the GUID of the message source
     283                        sender.jmfrecv_blk(null,tempArray, 1000000);
     284                        int receivedBytes = Utils.byteArrayToInt(tempArray, 0);
     285                        System.out.println("The receiver received " + receivedBytes + " succesfully");
     286                       
     287                        //Close the socket and clear the resources
     288                        sender.jmfclose();
     289                        System.out.println("Transfer completed");
     290                       
     291                } catch (JMFException e){
     292                        //Exceptions related to events occured in the network protocol stack are defined as JMFException
     293                        System.out.println(e.toString());
     294                }
     295        }
     296}}}
     297
     298While a very simple application a few concepts can be taken away from code just presented:
     299 * Communication profiling:
     300 * Named operations:
     301
     302The receiver code is now presented:
     303
     304{{{
     305#!java
     306public static void main(String []argv){
     307                //The profile describes the nature of the communication that will follow and
     308                //      is used by the network stack to select the best end-to-end transport
     309                //For this application a 'basic' profile is selected providing only a message based
     310                //      transport with no added realiability on top of what offered by the network.
     311                String scheme = "basic";
     312               
     313                GUID srcGUID = null;
     314                GUID senderGUID = new GUID();
     315                int i = 0;
     316               
     317                //A GUID class is used for name based communications
     318                //The source is optional. If a source is not specified, the default GUID of the device is used
     319                if(argv.length == 1) srcGUID = new GUID(Integer.parseInt(argv[0]));
     320               
     321                Path file = FileSystems.getDefault().getPath("temp.txt");
     322                try{
     323                        Files.createFile(file);
     324                } catch(IOException e){
     325                        try{
     326                                Files.delete(file);
     327                                Files.createFile(file);
     328                        } catch(IOException e2){
     329                                System.out.println(e2.toString());
     330                                return;
     331                        }
     332                }
     333               
     334                byte[] buf = new byte[1000000];
     335                int ret;
     336                JMFAPI receiver = new JMFAPI();
     337                try{
     338                        if(srcGUID!=null) receiver.jmfopen(scheme, srcGUID);
     339                        else receiver.jmfopen(scheme);
     340                       
     341                        //First message will include the size of the transfered file
     342                        ret = receiver.jmfrecv_blk(senderGUID, buf, 1000000);
     343                        int fileSize = Utils.byteArrayToInt(buf, 0);
     344                        System.out.println("I will receive a file of size " + fileSize + " bytes from host with GUID " + senderGUID.getGUID());
     345                       
     346                        int total = 0;
     347                        while(i < fileSize){
     348                                ret = receiver.jmfrecv_blk(null, buf, 1000000);
     349                                total+=ret;
     350                                System.out.println("Received " + ret + " bytes");
     351                                try{
     352                                        Files.write(file, Arrays.copyOfRange(buf, 0, ret), StandardOpenOption.APPEND);
     353                                } catch (IOException e){
     354                                        System.out.println(e.toString());
     355                                }
     356                                i += ret;
     357
     358                        }
     359                       
     360                        //Send back an acknowledgement with the amount of bytes received
     361                        byte[] answer = Utils.intToByteArray(total);
     362                        receiver.jmfsend(answer,4, senderGUID);
     363                       
     364                        receiver.jmfclose();
     365                } catch (JMFException e){
     366                        System.out.println(e.toString());
     367                }
     368                System.out.println("Transfer completed");
     369        }
    73370}
    74371}}}
    75372
    76 The following shows the corresponding receiver code:
    77 
    78 {{{
    79 #!java
    80 //Simple class used to test the java api
    81 
    82 import java.io.*;
    83 import java.util.*;
    84 import java.nio.file.*;
    85 import edu.rutgers.winlab.jmfapi.*;
    86 
    87 class Receiver{
    88     private static void usage(){
    89         System.out.println("Usage:");
    90         System.out.println("receiver [<my_GUID>]");
    91     }
    92     public static void main(String []argv){
    93         String scheme = "basic";
    94         GUID srcGUID = null;
    95         int i = 0;
    96         if(argv.length == 1) srcGUID = new GUID(Integer.parseInt(argv[0]));
    97         Path file = FileSystems.getDefault().getPath("temp.txt");
    98         try{
    99             Files.createFile(file);
    100         } catch(IOException e){
    101             try{
    102                 Files.delete(file);
    103                 Files.createFile(file);
    104             } catch(IOException e2){
    105                 return;
    106             }
    107         }
    108         byte[] buf = new byte[1000000];
    109         int ret;
    110         JMFAPI receiver = new JMFAPI();
    111         try{
    112             if(srcGUID!=null) receiver.jmfopen(scheme, srcGUID);
    113             else receiver.jmfopen(scheme);
    114             while(i < 24954287){
    115                 ret = receiver.jmfrecv_blk(null, buf, 1000000);
    116                 try{
    117                     Files.write(file, buf, StandardOpenOption.APPEND);
    118                 } catch (IOException e){
    119                     System.out.println(e.toString());
    120                 }
    121                 i += ret;
    122 
    123             }
    124                 System.out.println("Received file");
    125 
    126                         //TODO send confirmation
    127 
    128             receiver.jmfclose();
    129         } catch (JMFException e){
    130             System.out.println(e.toString());
    131         }
    132 
    133     }
    134 }
    135 }}}
    136373
    137374
    138375== Execute ==
    139376
    140 
    141 ==== Test !Sender/Receiver Applications ====
    142 
    143 ==== Add Second Receiver End Point to Topology ====
    144 
    145 ==== Modify Delivery Service Option to Add Multi-point Delivery ====
    146 
    147 The following code snippet shows the modified portion of the code to request Multicast delivery option while transfering the file:
    148 
    149 {{{
    150 #!java
    151 
    152 }}}
     377As in the previous tutorials, you will need to start the experiment via an OMF script. Download the script to the orbit console copying and pasting the following command in your terminal:
     378
     379{{{
     380#!sh
     381    wget www.winlab.rutgers.edu/~bronzino/downloads/orbit/exercise3.rb
     382}}}
     383
     384Once the file has been downloaded, execute it with the following command:
     385
     386{{{
     387#!sh
     388    omf exec exercise3.rb
     389}}}
     390
     391Once your experiment is showing you the following line:
     392
     393{{{
     394#!sh
     395    INFO exp: Request from Experiment Script: Wait for 10000s....
     396}}}
     397
     398you can proceed with the execution of the applications. Using the previously opened consoles log in into the two host nodes (GUIDs 101 and 102) that will be used to run the simple 'mfping' application. In order to access a running Orbit node ssh into it as follow:
     399
     400{{{
     401#!sh
     402ssh root@nodex-y
     403}}}
     404
     405Where ''x-y'' has to be replaced by the actual numbers identifying the node.
     406
     407Once logged in, run on the node with GUID 102 the receiver component:
     408
     409{{{
     410#!sh
     411mfping -s -m 102 -o 101
     412}}}
     413
     414where "-s" specifies that the host is running as server, "-m" specifies the source guid and "-o" the destination one
     415
     416To run the mfping 'client' start on the client with GUID 101 the command:
     417
     418{{{
     419#!sh
     420mfping -c -m 101 -o 102 -n 10
     421}}}
     422
     423Where "-c" specifies the client is running and "-n" specifies the number of pings between the two nodes.
     424
     425==== What's next ====
     426
    153427
    154428
    155429== Finish ==
     430
     431Once the application has successfully completed its task, follow these steps to complete the experiments:
     432 * On the grid's console running the experiment script, interrupt the experiment using the Ctrl-C key combination.
     433This will stop all the applications and will conclude the experiment.
     434
     435=== References ===
     436
     437For more information regarding the !MobilityFirst project, visit the project page: http://mobilityfirst.winlab.rutgers.edu/
     438
     439For more information regarding the prototype design and updated status, visit the wiki page: https://mobilityfirst.orbit-lab.org/