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. |
| 10 | In 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 | |
| 20 | This 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 | |
| 24 | If 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 | |
| 28 | First of all, log in into the grid console using SSH: |
| 29 | |
| 30 | {{{ |
| 31 | #!sh |
| 32 | |
| 33 | ssh username@console.grid.orbit-lab.org |
| 34 | }}} |
| 35 | |
| 36 | For 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. |
| 37 | From the console we will start loading the !Mobilityfirst image into the four nodes that have been assigned to you: |
| 38 | |
| 39 | {{{ |
| 40 | #!sh |
| 41 | |
| 42 | omf 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 | |
| 47 | For example, ''mf-group1'' will load the image on nodes 'node20-20,node20-19,node19-19,node19-20' |
| 48 | |
| 49 | If at the end of the execution, the final output of your console looks similar to: |
| 50 | |
| 51 | {{{ |
| 52 | #!sh |
| 53 | |
| 54 | INFO 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 | |
| 66 | your nodes have been imaged correctly. |
| 67 | |
| 68 | === Deploy Network === |
| 69 | |
| 70 | Software 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 | |
| 72 | We 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 | |
| 76 | The 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 |
| 82 | defApplication('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 |
| 103 | defApplication('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 |
| 113 | defApplication('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 | |
| 121 | A 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 | |
| 131 | The 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 |
| 137 | for 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 | } |
| 184 | end |
| 185 | |
| 186 | #Create host groups |
| 187 | for 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 | } |
| 209 | end |
| 210 | }}} |
| 211 | |
| 212 | As 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 | |
| 214 | Moreover, 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]] |
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 | | } |
| 225 | public 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 | |
| 298 | While a very simple application a few concepts can be taken away from code just presented: |
| 299 | * Communication profiling: |
| 300 | * Named operations: |
| 301 | |
| 302 | The receiver code is now presented: |
| 303 | |
| 304 | {{{ |
| 305 | #!java |
| 306 | public 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 | } |