All of the project code you write must operate inside the infrastructure we're distributing as the project 1 base code. This page gives an overview of that infrastructure. Armed with that overview, the code you see, which inevitably contains a lot of details, should be manageable.
The two key points that provide context for the code you'll see are:
In contrast, the dynamic loading scheme employed by the software is evident everywhere, and so it's useful to understand the basics of what it does. The figure immediately below gives a static view of this process.
Execution starts in the main()
of ConsoleStart
.
ConsoleStart
statically references NetBase
,
so initially these two classes are loaded in the JVM.
ConsoleStart
provides the mechanism required for the user
to pick a configuration file. Configuration files contain arguments
to programs that run in the JVM. For instance, the configuration file
may list an IP address that should be used as the default for
the remote machine to connect to when applications are run.
The entries in the configuration file is documented here.
ConsoleStart
hands the chosen configuration file to NetBase
.
Among the information in the configuration file are the names of Java classes for
which we want an object created in the JVM.
NetBase
reads these names and creates a single instance of each.
Each of these objects registers an "app name" with the infrastructure.
For instance, the object created from class EchoRPC
might register
itself with the name echorpc
. (This separates choosing good class
names from choosing names that you might have to type a lot.)
Once all objects are created,
ConsoleStart
transfers control to a specially designated "shell application."
Here's the picture of the JVM at this point.
The shell application prints
a prompt, and optionally lists the app names of all loaded applications.
The user enters the name of an application to run,
and the shell transfers control to the run()
method
of the chosen application.
In this infrastructure,
the ping
and dataxfer
clients you write are applications.
To be loaded as applications, they must implement the NetLoadableConsoleAppInterface
interface. That requires implementing a constructor that takes no arguments,
a run()
method that is invoked when the user requests execution of your application,
and a shutdown()
method called when the entire JVM is coming
down.
In contrast, the dataxfer
service you write is a service.
It must implement the NetLoadableServiceInterface
.
During construction it creates UDP and TCP sockets. For each socket, it creates
a thread. The thread sits in a loop waiting for a packet to arrive (UDP) or a connection to
be made (TCP). When that happens, the thread sends the required amount of data back
to the client, then waits for another incoming packet or connection.
A timeout must be set so that the thread will occasionally wake up even if there
is no incoming packet connection. That allows the thread to check whether the
entire application is trying to terminate.
(If so, your service's shutdown()
method will have been called, and you can have set a flag in the object that the threads
can check.)
During execution, any component can access the configuration file selected
when the infrastructure was brought up, and extract from it parameter values
that affect that components behavior.
The configuration file is represented in memory by a ConfigManager
object (found in Eclipse project util
).
A reference to that object is available as NetBase.theNetbase().config()
.
In many cases, the infrastructure uses Java interfaces
as a form
of documentation. The idea is to have a short file that shows the public interfaces
of a class - something much shorter than the full source file,
which is cluttered with implementation and comments.
Interface files are easy to spot: we followed a convention of having the string Interface
in the file name.
An interface file for a fully implemented component of the infrastructure shows
the methods we think you should be interested in, and doesn't show ones we think
you shouldn't be interested in. (Method attributes related to Java's protection
scheme (public
, private
, and the like) are unreliable
indicators of what we expect you to be able to use. A method may be public
to ease implementation of the infrastructure itself without that being a sign that
we are inviting you to invoke it.)
Interface files for classes you implement are intended to make clear which methods we may rely on in our testing code. You must provide those interfaces.
The infrastructure implements a class intended to make it easy to run repeated
experiments and aggregate sampling information from them. The implementation is in
file SampledStatistic.java
in Eclipse package util
.
Because the classes it uses are static, there is no interface file for it, but here
is a brief description of how to use it.
The main classes you deal with are ElapsedTime
and TransferRate
. Each manages a dynamically created set of counters. Counters are identified
by string names you make up. A new counter is created when a new name is seen.
ElapsedTime
counters measure time intervals. TransferRate
counters measure transfer rates, which also involves measuring time intervals.
Counters have start
and stop
methods that demarcate the
time interval.
Here's an overview of how to use ElapsedTime
. TransferRate
is nearly identical, except that you have to provide the amount of data transferred
on the stop
event.
ElapsedTime.clear(); // destroy all existing ElapsedTime counters
for (...) {
ElapsedTime.start("foo"); // create a foo counter, if necessary
...
ElapsedTime.start("bar"); // create the bar counter, if necessary
...
ElapsedTime.stop("bar"); // take a new sample - the time since start("bar")
..
ElapsedTime.stop("foo"); // new foo sample
}
System.out.println(ElapsedTime.get("bar").mean()); // print mean of bar counter
Each start
of a counter must be terminated with either a stop
or an abort
call before another start
can be performed on the same counter.
(abort
records that a failed experiment occurred, but doesn't use
the sample in estimating, for instance, the mean.)
Other information is available besides just the mean;
look at the SampleSet
class. There is no requirement
that start
and stop
of distinct counters must nest;
the two stop
calls above could be reversed, for instance.
The config file contains fields whose values you must you (e.g., net.timeout.socket
).
See the config file documentation page.
You must run a client and a server. To contact the server, the client must know what the IP address of the server, and the port of the application or service it wants to contact on that server.
The default server IP is given by field net.server.ip
in the config file.
You can modify your applications to prompt the user for an IP, though, if you prefer.
Most services use "ephemeral ports," meaning that the port they listen on is chosen
at run time, and so there are no config file entries specifying ports.
Instead, on the server run the dumpservicestate
application. It
iterates over all loaded services, asking each for its status string, which is then printd.
A server's status string will contain its port number.
Eclipse provides an interactive debugger that works well for both console and Android execution (including setting breakpoints and the like in code running on a physical phone). Despite that, you might find it useful to print some debugging/logging information.
The infrastructure supports this via the Log
class, which provides
methods with names like e()
, w()
, and d()
to print messages at error, warning, and debug levels, respectively. (For instance,
Log.e(TAG, "some error occurred");
prints a message at the error level.)
The config file contains two properties that control
Log
printing.
Setting debug.enable
to 0 turns
off printing entirely, while setting it to 1 (the default) allows printing.
Property debug.level
is set to a value between 2 (meaning verbose
level) and 6 (meaning error level).
Log messages at levels lower than debug.level
are suppressed.
You'll need to run at least two instances of your code, one as the client and one as the server. Because config files contain things like server IP addresses, your choice of where to run and the contents of your config files are related.
We distribute two config files, client.config.ini
and server.config.ini
. They assume you will run both the
client and the server instance on the machine you're sitting
at, localhost
. This turns out to be very handy for
debugging. It also avoids a lot of things that can go wrong trying to
communicate in a symmetric fashion between two arbitrarily located
machines. (We'll get to these things in lecture a little later in the
course.) So, our advice is to run both instances on the same machine.
If you do decide to run distributed, that's great, but remember that
you'll almost certainly have to edit the config files.
If you want to run over the network, you'll need to move your code to some remote
machine. The easiest way to do this is to export the code as a jar
.
The easiest way to do that is to right-click on the ConsoleApps
project, then Export...
, expand the Java
entry and
select Runnable JAR file
, then Next. For Launch Configuration,
choose ConsoleStart. Choose some directory into which the jar
should
be written for the Export destination, make the name of the file ConsoleApps.jar
, and hit Finish.
Now copy ConsoleApps.jar
to the remote machine. Put it in a directory
that also contains a single configuration file
(whose name ends with config.ini
), and invoke it with:
java -jar ConsoleApps.jar -d .
(The -H
switch will cause a help message to be printed, listing
other command line options.)
It's possible that you might get errors printed indicating Java can find some required library.
That library is in the Lib
directory of Eclipse project. You should move a copy
to the remote machine. (Note that all libraries we use have some flavor of open source licensing.
You should still make sure that you follow the license when making copies.)