CSE 461: Introduction to Computer Communication Networks, Spring 2012
  CSE Home   About Us   Search   Contact Info 
 
Course Home
  Home
Administration
  Overview
  Course email
  Anonymous feedback
  View feedback
 
Assignment Utilities
  Home Virtual Machines
  Homework Turnin
  Class GoPost Forum
  Gradebook
 
Most Everything
  Schedule
  Hw/Project List
    Project 3: RPC
Out: Tuesday April 17
Due: Tuesday May 1 (11:59 PM)
Note: There is a midterm on 4/25
Turnin: Online
Teams: Pairs


Assignment Overview

Communicating over raw sockets is difficult. Sockets transmit bytes, leaving data encoding and message format implementation up to each application programmer. Sockets provide minimal inherent synchronization and protocol, mechanisms, leaving to each application programmer to invent protocols and enforce them.

An enhancement to raw sockets is Remote Procedure Call (RPC). As the name implies, it provides an abstraction as similar as possible to procedure call. The caller simply invokes a procedure on a remote machine. Arguments are sent, with details of data encoding worked out by the RPC implementation. The calling thread is suspended (blocks) until a message from the remote procedure arrives indicating that it has completed, or encountered an error. That message can contain return values (i.e., it can be remote function call).

In this project you'll implement a simple RPC mechanism, and use it to build a simple application (ping).

RPC Protocol Overview

This section mostly talks about the RPC protocotol, but also a bit about its implementation. A more complete overview of the implementation is in next section.

RPCs take place over a TCP connection. There is an initial RPC handshake performed when the TCP connection is established. In theory, that can be followed by one or more remote calls ("persistent connection"); in practice, we allow only a single call and then close the TCP connection ("non-persistent connection").

All messages are JSON encoded JSONObjects, sent as TCPMessageHandler encoded strings. There is an RPC header, represented by those JSONObject fields that exist at the top level. The message payload contains the arguments the caller is sending to the remote callee, or the return value from the callee. The payload is required to be a JSONObject.

The Handshake

The initial handshake is communication between the calling RPC service and the remote RPC service. It is not part of application communication, i.e., not application to application.

The handshake begins with the calling RPC service opening a TCP connection to the remote RPC service, and sending a connect control message. Connect messages look like this. (This is JSON notation for what is basically a map. Ordering of the fields isn't significant.)

{"id":2,"host":"cse461","action":"connect","type":"control"}
All of this is what you can think of as RPC header. The id field is a unique ID for this message. (While RPC service implementations can simply count up by one to generate these IDs, they are not sequence numbers; the recipient won't necessarily see consecutive IDs.) The type field indicates this is a control message, i.e., the intended recipient is the remote RPC service, not some application on the remote machine. The host field identifies the sending RPC service. It's a unique string name, useful in Project 4 but of no further significance in this project. The action field indicates what kind of control message this is. (There's only one, but we send this field to allow later extension of the protocol.)

If the remote RPC service is willing to connect, it sends a success response. For our example, the response looks like:

{"id":1,"host":"","callid":2,"type":"OK"}
The id is, again, a unique ID for this message. The name of this remote host is the null string. The callid field gives the id of the message for which this is a response. The type field indicates success.

An error response to the same call looks like this:

{"id":1,"host":"","callid":2,"type":"ERROR","msg":"Max connections exceeded"}
The msg field is a free form error message, intended to help the caller understand what the problem is. If an error response is received, the caller should close the TCP connection, and not try to send any further RPC requests on it.

RPC Invocation

An invocation message looks like this:

{"id":4,"app":"echo","host":"cse461","args":{"msg":"test message"},"method":"echo","type":"invoke"}
The type field indicates this is an application-to-application invocation message. The app field is the (unique) name of the remote application. The method field indicates which of the remote application's methods the caller wants to invoke. The args field provides the arguments for that invocation. These are always bundled up as a JSONObject. The arguments in this call are a single string, named msg, with value test message.

A success response looks like this:

{"id":3,"host":"","callid":4,"value":{"msg":"test message"},"type":"OK"}
The value field is the return value of the invocation. It too must be a JSONObject. In this case, the application is echo, which just returns anything sent to it, so the return value matches the invocation arguments.

An error response looks like tis:

{"message":"some error message","id":3,"host":"","callid":4,"type":"ERROR",
callargs":{"id":6,"app":"echo","host":"cse461","args":{"msg":"test message"},"method":"echo","type":"invoke"}}
The new field here is callargs, which simply sends back the invocation arguments. It can be useful in debugging the caller.

System Architecture

The RPC service you'll build fits into a larger architecture. That serves two purposes. First, it's useful for debugging. Some of the code can be viewed as simple test programs. Second, our architecture mimics in many ways that of real systems. That is a natural side-effect of trying to build a flexible system - we'll be using the RPC system in succeeding projects.

The architecture looks like this:

The blue components are completely implemented in the code provided to you. The white components are things you will implement. (Additionally, you'll implement an Android edition of the Ping client.) The grey component, DDNS (Dynamic DNS (Domain Name System)) is part of Project 4, shown here so you'll know where we're headed.

The upper layer components, which we call applications, implement user interfaces. This picture shows the "console applications," those whose interface is textual, and so can run on arbitrary workstations. (The only Android application is Ping.) The OS and components below it are "services." They have no UI.

There is no fundamental difference between applications and services, though. Although some implementation details distinguish them, both start at system boot and run until system shutdown. There can be at most one instance of any component type. This is the usual notion of a system service, but not of an application. It is similar in some ways to the Android notion of an application, however.

Both applications and services can use the RPC system to make calls, or to expose methods they implement to remote callers. If they have methods callable by RPC, they must provide a unique, "well known name" that can be used to identify them.

Component Roles

OS
The OS starts services. It does this by dynamically (at run time) loading the required Java class into the JVM and creating an instance of the class. It maintains a data structure mapping the unique name of the service (e.g., "rpc" for the RPC service) to the Java service object. It provides an interface that takes a service name as an argument and returns a reference to the Java service object.

Additionally, the OS maintains a Java Properties object that contains parameter values set in a .ini configuration file (discussed later), and provides mechanisms for components to access those values.

RPC Service
The RPC Service implements the RPC protocol. It has two main components, one supporting calls from the local system to remote systems and the other supporting calls from remote systems to the local one.

RPC is implemented over TCP. To receive remote calls, the RPC Service creates a Java ServerSocket, binds it to a local port, and executes accept() on it. accept() returns a new Java Socket object when a remote TCP connection is established. The RPC code then engages in its handshake using that socket, and waits for invocation messages. When those arrive, it makes a call to the application (or service) and method named in the invocation message, collects the return value (or error response), and sends that back to caller. That process more or less requires threads. One thread is required to sit in a loop performing accept() calls on the ServerSocket, since that is a blocking call. Additionally, a new thread should be created to handle each new connection. That makes it easier to keep track of what state the protocol is in than if you try to serve multiple connections from a single thread.

On the outgoing call side, the RPC Service exposes a method for making remote invocations. That method opens a TCP connection to a remote RPC ServerSocket, engages in the RPC handshake, sends the invocation message, collects the response, and delivers it back to the local caller. Additionally, it must block the calling thread until the response can be returned. The easiest way to do that is to simply use the calling thread to execute all of the code involved in making a remote call.

Echo Service
The Echo Service makes a single method available via RPC, echo(). It simply returns any arguments it is sent.

AppManager
The AppManager performs dynamic loading of the Java classes that implement applications, and creates an instance of each. It then enters a loop asking the user to select an application to run, and invokes the run() method of the named application.

Echo
The Echo application asks the user to identify a remote system by giving its IP address and the port of the RPC service running there, then to input a line of text. It contacts the remote Echo Service, handing it the line of text, and prints whatever value is returned.

WhoAmI
The WhoAmI service prints the identity of the system on which it's running. That identity is the IP address of the host and the port to which the RPC ServerSocket is bound.

Ping
Ping measures (and then reports) how long it takes to make an minimal RPC call to some remote system. By "minimal" we mean a call to the remote Echo Service passing an empty string as the argument. Ping should repeat the call five times, reporting the time taken by each test.

(Ping presents a natural motivation for implementing persistent connections: rather than closing the TCP connection after an RPC call has completed, instead leave it open in case further calls to the same remote system are issued. With persistent connections, we expect ping will report a longer time for the first call than the subsequent ones, since the first includes the RPC handshake but the subsequent ones would not. Persistent connections are by no means required. Their implementation requires a bit more programming, involving timers and threads, but if you're familiar with those things it shouldn't be too bad.)

RPC Implementation

The RPC implementation consists of two main classes: RPCService, which implements the receiving side (accepting calls from remote systems), and RPCCallerSocket, which implements sending remote calls. Minimal skeleton code exists, mainly to define the public Java methods they must implement.

The RPCService implementation is by far the more complicated piece. Part of what it does is create a Java ServerSocket for incoming connections, and implement the RPC protocol over those connections. (There are some details in the RPC Protocol section above.)

Another part of what it does is to accept registration of RPC-callable methods from services (and potentially applications). A service registers a method, and when an RPC comes in for it, a callback to that method occurs. It turns out that implementing this process is a little tricky in Java, so the skeleton code includes a helper class, RPCCallable.RPCCallableMethod, that does the hard part. Have a look at the comments in its file for an explanation of what it does, and in EchoService.java for an example of how it's used.

Other Implementation Issues

You're writing two applications, ping for the console and ping for Android. The console-based ping should display five ping results. The Android-based one need display only a single ping result (but you can display five as well if you'd like).

Console-based ping should be very easy. You can model it on the Echo console application. Android-based ping is straightforward as well, except that you have to do some potentially new things: create a new Android project in Eclipse, then deal with Android. You can use the Android app from Projects 1/2 as example code, and there are lots more examples at developer.android.com. Your Android ping will also want to borrow code from OS.main() to load and initialize the various system components.

One issue that would be easy to overlook, and whose symtoms end up being a little confusing, is that an Android application must declare any system permissions it requires in its AndroidManifest.xml file. Your application will need at least INTERNET permission. If you omit a permission you require, some call will fail, but probably won't hint that the problem is you don't have the required permission in the manifest file.

Note that the usual Java code structuring scheme of creating a jar file and using it as a library may not work as you expect. Source compiled as an Eclipse Java project may not run on Android, and vice versa. You therefore need to either create two, system-specific jars, or else take the approach already incorporated in the distributed Eclipse project. That approach is to structure the code into natural, library-like Eclipse projects, but to have each project using a "library" include the library's source using Eclipse folder links, so that it's compiled for the appropriate target system.

You're also implementing RPC, which is a core service in a somewhat large body of code. It makes calls into other components (the OS), and other components make calls into it. This inevitably brings up the issue of what code you can modify.

I'm hoping you don't find a reason to want to modify existing code, beyond RPC itself. I have an existence proof that an implementation exists that does not require modifying it, but the main problem with working with existing code is that it can be harder than just writing your own. All the code from this project will be used in the next one(s), so the more you change interfaces the harder it will be to integrate new code distributions. I've worried about this issue since beginning work on the project, so I hope the existing code doesn't get in your way.

There's one implementation issue I'm not confident my code can be totally resilient to: exceptions. In general, existing code expects RPC methods to throw some kind of exception when anything goes wrong. Depending on what libraries you use, though, your RPC implementation may throw exception classes I didn't encounter. To try to deal with this, all existing code that calls RPC methods anticipates that an Exception might be thrown. I don't know Java well enough to know if this handles everything possible, though. In the "tricky bit" of the RPC callbacks I implemented, something came back that wasn't an Exception, so I know there were things I didn't know, and I don't know there aren't more.

Finally, you can't connect to machines running behind NATs from machines not also behind the NAT. If you can't connect to your home machine from the UW, you won't be able to connect to your Project 3 application running on your home machine from the UW. The other direction should be fine, though: an instance on your home machine should be able to connect to an instance running on a CSE machine.

Additional Android Information

There is a separate page elaborating on Android implementation issues.

Config Files

There are a number of system settings that you might like to set on a per-invocation basis. These are placed in a configuration file. The infrastructure reads the configuration on startup. It's values are available at runtime through various OS methods. Among other things, the config file lets you specify the port that the RPC's ServerSocket should be bound to. (Remember that only one instance of a ServerSocket can be bound to any given port, system wide.)

Here's an example config file. It's contains some information relevant to Project 4, but almost superfluous in Project 3. In particular, it has the notion of a "host name." For now, host names are just strings. In Project 4, they will have structure like DNS names (.e.g., www.cs.washington.edu). This file is for the host named foo.bar. (Peculiarly, the name 'foo.bar' is actually a short version of the full name, 'foo.bar.'.)

# This file contains device-specific configuration information
# that is read by the infrastructure at launch.

#------------------------------------------------
# host config values
#   host.name must be a full name (ends with a .)
#------------------------------------------------
host.name=foo.bar.

#------------------------------------------------
# ddns config
#------------------------------------------------
ddns.rootserver=localhost
ddns.rootport=46120

ddns.cachettl=60

#------------------------------------------------
# rpc config
#------------------------------------------------
rpc.timeout=10000
rpc.serverport=

#------------------------------------------------
# debug config
#   Levels: v:2  d:3  i:4  w:5  e:6
#------------------------------------------------

debug.enable=1
debug.level=4
The ddns section is totally irrelevant in this project. The rpc.timeout is intended to let you specify how long your code should wait for something to happen on a socket before giving up. (You have to implement code to use this value.) The rpc.serverport value lets you tell the code to what port number you'd like to bind the RPC ServerSocket. The null value shown here means "any port". (Again, you implement the use of these settings.)

The debug settings are used to control the behavior of android.util.Log, which prints debug logging messages. An implementation is distributed with the skeleton code so you can write debug log statements that work both when running in console mode, on any system, and on Android. You can turn debug printing off entirely by setting debug.enable to 0. You can affect the amount of logging that is printed by (a) choosing a log level at which to print a specific message (e..g, calling Log.v() vs. Log.i()), and then setting debug.level to the minimum level for which you want messages printed. See the Android documentation for more information on log levels.

The Source and Running It

Javadoc for the source is here, as well as in the doc folder of the distribution. Additional non-Javadoc comments are in the source.

The source is at /cse/courses/cse461/12sp/461Project3.jar. Unjar'ing it produces a 461Project3 directory that is an Eclipse workspace. Open Eclipse, and if it opens an old workspace, choose Switch Workspace from the File menu and navigate to the new workspace. Once Eclipse re-opens, if the Package Explorer pane is empty, right-click on it, choose Import..., then General, then Existing Projects into Workspace, Next, browse to the new workspace folder, and click Finish.

You're very likely to have to fix some broken absolute paths Eclipse keeps to external jars, following the procedure described in the Project 1 setup page. You'll also have to fix the output destinations given in the xxx.jardesc files: double-click on them and the output path is specified in the first dialog. The output should go to directory lib/ of your project main project directory.

To run the code as a console application, you should invoke the main method of class AppManager. You can run a single instance through Eclipse. (Make sure you set the invocation arguments to something like -f ../OS/foo.bar.config.ini). The echo (or your ping) application in that instance can talk to the echo service in the same system instance, and will use RPC to do so.

If you want to run more than one instance, there is an updated run.sh in the lib directory that helps. To run an instance of the system that uses config file OS/foo.bar.config.ini, you say:

./run.sh console foo.bar
For this to work, you must first update lib/OSConsoleApps.jar by right-clicking on OSConsoleApps.jardesc in Eclipse and selecting Create JAR whenever you change your code and want to run. (If it complains, you probably didn't update the destination of the the .jardesc. See the instruction above.)

We're running a "fully operational" system 23.5/7 at cse461.cs.washington.edu:46120. You can use it for debugging. You can, and probably should, also try to interoperate with implementations of other teams. (Note that the code lets you use a DNS host name string like cse461.cs.washington.edu whenever an IP address is required.)

What to Turn In

We'd like to automate running your code, in console mode. Ideally, we should be able to take your RPCService.java, RPCCallerSocket.java, and Ping.java, insert them into our unmodified infrastructure code, and run. Use your informed judgement about which source files that means you need to provide.

In addition, hand in a report.pdf file containing:

  • the names of your team
  • a brief description of your RPC implementation, with elaboration as appropriate if you think you've done anything we might find surprising
  • a description of what works and what doesn't, if anything doesn't
  • the cut-and-paste output of your console-based ping to our server on cse461.cs.washington.edu
  • the cut-and-paste output of native ping (the one your machine's OS provides) to the same server, performed at about the same time

Notes

  • Our RPC implementation is fairly typical of remote invocation mechanisms, but leaves out one important facility that is often associated with the term "RPC" (say, in an interview): type checking. A purer RPC system will at least try to verify that the arguments provided by the caller match what the callee requires. Our system does almost the opposite: it requires that a JSONObject be used to pass arguments, making pretty much any old thing pass Java's type checking, and therefore making it uncommonly easy to create type errors on invocations. I'm bringing this up just in case you're someday talking with someone about your RPC implementation, say in an interview.

  • This is a very complicated assignment to try to write instructions for. There is somewhat sophisticated Java. There is Eclipse, and all of its settings. There are the Android plug-ins, and Android development. There's the fact that we're using real networks, which have real configurations, and so real potential problems. All of that means I can't possibly anticipate, much less address, everything that could go wrong, or tell you everything that might be new to you individually (which will vary from person to person). Nonetheless, while I think you'll find the assignment challenging, I'm sure it's well within the scope of what you can do. (And, as a bonus, it's a real development experience, not just an instruction-following one. You either already know you can do that, or you'll be glad to discover that you can.)

  • The hardest part of this assignment is that your code has to operate correctly despite all kinds of potential errors. And, you'll encounter those errors when debugging. The errors could be network issues, although those are the least likely. More importantly, your code will be talking to other RPC implementations. Some of those will have outright bugs. Some will have implemented something incompatible with yours, because the specifications weren't specific enough.

  • Among the common errors are remote services that simply fail to respond. You're waiting for a response, but the remote service neither tells you that it will never come (by closing the connection) nor actually gives you a response. You can't hang forever. You must therefore arrange for operations involving remote agents to time out, and be prepared for that to happen. The skeleton code includes the few lines required to set up time outs. Your code needs to realize they can happen.

  • I have in mind that we'll all bring phones loaded with our implementations to the class following the due date, and try to ping each other. In theory, all our implementations should interoperate just fine. The protocol spec is designed to minimize the opportunity for incompatible implementations. But, we'll see.

Computer Science & Engineering
University of Washington
Box 352350
Seattle, WA  98195-2350
(206) 543-1695 voice, (206) 543-2969 FAX
[comments to zahorjan at cs.washington.edu]