Computer Science 461

Distributed Computing and Networking

Fall 1997


Assignment 2

In this assignment, you will implement client-server communication in AcmeNet, using the reliable communication facility you built in Assignment 1.

You will build two classes, AcmeNet.Assn2.ClientServerNI and AcmeNet.Assn2.Connection, plus any auxiliary classes you may need. As usual, the auxiliary classes should be non-public classes in the AcmeNet.Assn2 package. We will provide you with code for two classes, AcmeNet.Assn2.Service and AcmeNet.Assn2.ServiceThread.

Your solution will implement three new features: connection-based communication, services, and dead-connection detection.

Connection-Based Communication

The reliable communication service you implemented in Assignment 1 is like the mail system; you communicate in discrete units (packets in the computer network, or letters in the mail system) and you can send a packet to anyone, or receive a packet from anyone, through your network interface (which is like the mailbox in front of a house).

Connection-based communication is like the phone system: you make a connection between two parties, and then either party can talk into the connection and the other party hears whatever they say. You can't talk to someone without making a connection to them, and each connection goes to exactly one place. Connections are bidirectional, and they last until you explicitly terminate them.

Services

Another distinguishing characteristic of client-server communication is that connection establishment is asymmetric. One process offers a service to everyone on the network, and anybody can connect to a service.

Think of a service as being like a big room full of phones and "operators standing by" to answer calls to an 800 number. You (a client) can call an 800 number whenever you want, but it never calls you. Many clients can dial the same 800 number at the same time, and they can all get through because there are lots of operators. (In the computer system, a fresh "operator" --- a thread --- is spawned to handle every phone call.)

Every ClientServerNI has a database of services, with each service assigned a locally-unique service number. When a remote client says "connect me to service number 19 at NetAddress n" a new thread is spawned at n to handle the client, and a new connection is built between the client and the newly created thread. The job of the newly created thread is to talk to the client and perform some job for it. When that job is done --- when the connection to the client is broken --- the thread dies. Every thread created in the service talks to one and only one client.

Services sometimes run for a long time. For example, a printer server (which allows processes to send documents to a remote printer) might run for weeks at a time.

Dead-Connection Detection

In the real phone system, whenever you lose a connection, you find out about it --- you hear a dial tone. This happens any time the connection is broken, whether it is broken by somebody hanging up or by a glitch in the phone system. Dead-connection establishment means doing the same thing in a computer network: detecting broken connections automatically, and gracefully shutting them down.

This is usually implemented by having each side periodically (every five minutes) send a "ping" packet to the other side. Upon receiving a ping packet, a process immediately responds with a "pong" packet to tell the pinger that it is still alive. If you send somebody a ping and five minutes passes without an answering pong, you can assume that they are dead or that the network between you is seriously messed up, so you should close down any connections you have to them.

Detecting dead connections keeps long-lived services from getting clogged up with useless dead connections. By cleaning up the garbage left behind by failed processes, we can allow a server to live for a long time.

Details

As usual, you are required to follow a particular protocol and packet format so your solution can interoperate with code written by other people. You will be graded partly on how well your code interoperates with ours.

Identifying Services

A ClientServerNI may have several services living on it. Each service is identified by an integer service number that is chosen by the service itself. A service calls the bind method of ClientServerNI to register itself under a particular service number. Once a service has bound to a service number, clients can connect to the service by giving its service number.

Identifying Connections

Applications see connections as objects of type AcmeNet.Assn2.Connection. However, your implementation should identify each connection internally with a unique integer serial number. It doesn't matter how you assign these numbers. Different ClientServerNI's may have connections with the same numbers (the numbers only have meaning locally), but you should never use the same number twice in the same ClientServerNI.

For each connection, you will have to remember the connection ID you are using locally to identify that connection, as well as the connection ID the other end uses.

Sometimes your code will have to send somebody a packet that refers to one of their connections. Whenever a packet mentions a connection, it will do so by using the connection's ID number. The protocol specification will always say in which ClientServerID that number should be interpreted.

Packet Formats and Protocol

You should use an AcmeNet.Assn1.ReliableNI to send and receive all of your packets. The packet formats we define in this assignment specify the format of the stuff that should be given to the ReliableNI.

There are seven types of packets, as defined in the AcmeNet.Assn2.PacketTypeCode class. Every packet starts with a one-byte value identifying the packet type. After that, the format depends on the packet type.

Ping and Pong Packets

Ping and Pong packets are identified by the codes AcmeNet.Assn2.PacketTypeCode.Ping and AcmeNet.Assn2.PacketTypeCode.Pong, respectively.

Every five minutes, a process sends a Ping packet for each of its connections. The Ping packet contains the one-byte type code followed by the integer ID number the recipient uses to identify the connection.

Upon receiving a Ping packet, the recipient immediately replies with a Pong packet. The Pong packet contains the one-byte type code followed by the integer ID number that the recipient of the Pong packet uses to identify the connection.

Connect, Accept, and Reject Packets

Connect, Accept, and Reject packets are identified by the codes AcmeNet.Assn2.PacketTypeCode.Connect, AcmeNet.Assn2.PacketTypeCode.Accept, and AcmeNet.Assn2.PacketTypeCode.Reject, respectively.

When a client wants to connect to a service, the client sends a Connect packet to the server. The Connect packet contains the one-byte packet type code, followed by the integer service number that the client wants to connect to, followed by the integer connection ID number that the client will use to refer to the connection (assuming it is successfully established).

Upon receiving a Connect packet, a ClientServerNI checks to see whether it has a service with the requested service number. If there is such a service number, it responds by sending an Accept packet back to the client; otherwise, it responds by sending a Reject packet back to the client.

An Accept packet contains the one-byte packet type code, followed by the integer connection ID number that the recipient of the Accept packet is using to identify the connection (the ID number sent in the Connect packet), followed by the integer connection ID number that the sender of the Accept packet will use to identify the connection.

A Reject packet contains the one-byte packet type code, followed by the integer connection ID number that the recipient of the Reject packet was using to identify the connection (the ID number sent in the Connect packet).

Data Packets

Data packets are identified by the code AcmeNet.Assn2.PacketTypeCode.Data. A process sends a Data packet whenever it wants to transmit data along a Connection.

A Data packet consists of the one-byte packet type code, followed by the integer connection ID number that the recipient of the Data packet is using to identify the connection, followed by the actual data.

EndOfData Packets

EndOfData packets are identified by the code AcmeNet.Assn2.PacketTypeCode.EndOfData. A process sends an EndOfData packet when it is done sending data along a Connection.

An EndOfData packet consist of the one-byte packet type code, followed by the integer connection ID number that the recipient of the EndOfData packet is using to identify the connection.

More Details

Packets vs. Data Streams

Until now, all of your code communicated using discrete packets. However, your implementation of AcmeNet.Assn2.Connection is required to provide applications with InputStream and OutputStream objects they can use to send and receive data. Since InputStreams and OutputStreams deal with continuous byte streams rather than packets, this means you must write code that breaks up outgoing data into packets and reassembled incoming packets into a data stream.

On the outgoing side, you should do this by buffering data as it is put into the stream. (You could send every byte of data as a separate packet, but that would be hopelessly inefficient.) When the amount of buffered data gets to be pretty large, or when the application flushes or closes the OutputStream, you should send the buffered data as a single packet. (Be sure not to exceed the packet-size limits imposed by your network interfaces.)

On the incoming side, your task is a bit easier. Since incoming packets are already presented to you as InputStreams, all you have to do is write a "wrapper" InputStream that switches from PacketInputStream to the next one when the first one hits end-of-stream.

The Semantics of EndOfData

An application sees its end of a Connection as of two data streams, one incoming and one outgoing. The two streams are independent as far as closing/shutdown is concerned. In other words, if A and B are communicating over a Connection, A can close its outgoing stream while B keeps sending data over its outgoing stream for a long time.

When A closes its OutputStream, the closing of the stream should not be visible to B until B has read all of the data that was sent before the stream was closed. Once B has consumed all of that data, its next attempt to read from the stream should cause it to see an end-of-stream.

Cleaning Up After Closed Connections

When a Connection has reached the end of its life, its state should be cleaned up so that the memory it consumes can be re-used. Normally, Java's garbage collection takes care of cleaning up objects that aren't being used any more. But you will almost certainly keep a table that maps connection ID numbers to Connection objects. Unless you remove old Connections from that table, they won't be garbage-collected.

You know a Connection is "dead enough" to remove from the table when you know that the process on the other end of the Connection will never send you another packet mentioning the Connection. And you know this is true once you have sent an EndOfData packet to the other end, and an EndOfData packet from the other end has arrived at your end.

After this occurs, there may still be data in the Connection waiting to be consumed, but that shouldn't keep you from removing the Connection from your table: if the application is still using the Connection, the application will still have a reference to the Connection object, so it is in no danger of being garbage-collected.

Details of Handling Broken Connections

Suppose you send a Ping on some Connection, but five minutes elapse without an answering Pong showing up. You have to close down the Connection. Here's how to do it: Shut down the incoming side of the Connection by pretending that you just received an EndOfData packet. Shut down the outgoing side by pretending that the application just closed the ConnectionOutputStream. (Further attempts by the application to write data to the ConnectionOutputStream should cause a java.io.IOException to be thrown.) Don't forget to clean up properly after the now-closed Connection.

Testing Your Solution

You can start testing your solution by creating a service and then making connections to it.

We will also set up an "Echo Service" process at NetAddress("idea.cs.princeton.edu", 9005), service number 44. After you connect to this service, it will echo back all of the data that you send along the Connection. After you close your outgoing stream, the Echo Service will respond by closing its outgoing stream.


Copyright (c) 1997 by Edward W. Felten

Last modified: Thursday, October 02, 1997 03:42 PM