Echo

Echo is a simple framework for building client-server applications.

The echo client is a simple console user interface that perpetually prompts its user for a request, forwards the request to a server, then displays the server's response.

Upon receiving a request, the server spawns a request handler thread connected to the client, then resumes listening for more requests.

The request handler's run method begins a request-response-loop with the client. The loop ends when the client sends the "quit" request.

Responses are created by the request handler's response method:

protected String response(String request) throws Exception {
     return "echo: " + request;
}

More interesting responses can be generated by overriding this method in a request handler subclass.

Exceptions thrown by the response method are caught in the run method where the session is terminated.

Here's a sequence diagram showing a simple interaction:

Notes

·        SimpleClient and RequestHandler are multi-instances. I'm using this to indicate that there can be many clients and many handlers.

·        Many details are missing from this diagram.

·        To further simplify the diagram I am using a few wrong types of arrows.

·        The black line represents a process/computer boundary.

·        repl and listen are called found messages. They come from a user.

Design and Implementation of Echo

The echo client and request handlers extend echo's Correspondent class:

Sockets

The correspondent provides subclasses with a socket. A socket is the Internet's version of a telephone. A socket has an input stream and an output stream. The input stream of the client's socket is connected to the output stream of the request handler's socket and vice-versa. Connected sockets can be on the same computer or different computers connected to the Internet.

Source Code

Some of the methods in these files have missing code. In Lab 1 you will be asked to provide this code.

·        Correspondent.java

·        SimpleClient.java

·        RequestHandler.java

The Echo Server

The basic echo server perpetually listens to its server socket for incoming requests. When one is received, it uses its makeHandler method to create a handler. It connects the handler's socket to the client's socket, starts the handler in its own thread, then resumes listening for more client requests.

If a socket is the Internet's version of a telephone, then a server socket is the Internet's version of a switchboard. We can think of the server as a switchboard operator who connects incoming customer calls to the next available representative.

A socket server is identified by the URL or IP address of its host computer (localhost by default) and a port number (between 0 and 65000). In our telephone analogy think of the host as the area code and the port as the phone number.

60 Top Telephone Switchboard Operator Pictures, Photos, & Images ...

Creating Request Handlers

An echo server only creates one type of request handler—MathHandler, DataBaseHandler, GameHandler, etc. How do we specify which type of handler a server should create? There are several strategies.

Strategy 1: Overriding makeHandler

By default, the server's makeHandler method creates an instance of the RequestHandler class (which unhelpfully echoes client requests back to them). This method can be overridden in server subclasses to create more interesting types of request handlers.

The problem with this technique is that each client-server application requires a different type of server.

Strategy 2: Abstract Factories

Alternatively, we could provide the server with an abstract factory for making request handlers.

This strategy requires programmers to define a factory for every type of handler.

Strategy 3: Reflection

The most interesting class in the Java library is the meta-class Class:

class Class<T> { ... }

Instances of Class represent (wait for it ...) classes!

Before we begin, take a moment to study Notes on Reflection.

The reflection strategy equips the server with an instance of Class representing the type of request handler to create:

The user can simply specify the name of the request handler class when starting the server:

> java echo.Server math.MathHandler

The server creates an instance of Class from the name:

Class<?> handlerType = (Class.forName(handlerName));

The makeHandler method uses this object to create new instances of the request handler:

RequestHandler handler =
   (RequestHandler) handlerType.getDeclaredConstructor().newInstance();

Several precautions should be taken when using Class:

·        Class.newInstance() assumes a default (parameterless) constructor exists.

·        The request handler's .class file must be present on the server's host computer.

·        Class methods throw lots of exceptions.

Implementation

You will also need to complete this code for Lab 1:

·        Server.java

Lab 1: Complete the implementation of the Echo framework

Complete and test the echo server.

Testing Echo

To test echo you'll need to run the server and several clients in different Java virtual machines (JVMs). This can be done easily within the IntelliJ IDE. Simply right click the Server in the Project tab and select "run". Then do the same for SimpleClient. By selecting "allow multiple instances" in the run configuration under "modify options" drop down menu you can have multiple simple clients talking to the server.

A computer screen shot of a computer screen

Description automatically generated

Using Command Windows

It's useful to first export the project's binaries to the desktop or some other easily accessible directory.

Next, open a command window in the exported bin directory, above the echo package.

In this example we will start the server listening on the default port (5555), of the default host (localhost) and using echo.RequestHandler as the request handler class:

C:\Users\smith\Desktop\bin>java echo.Server
server address: 0.0.0.0/0.0.0.0
Server listening at port 5555

The command line synopsis is:

java echo.Server service port host

Starting the Client

In a separate command window on the same directory start the simple client:

C:\Users\000030278\Desktop\bin>java echo.SimpleClient
-> hello
sending: hello
received: echo: hello
-> bye
sending: bye
received: echo:bye
-> quit
bye

Here's the output on the server side:

Server listening at port 5555
received: hello
sending: echo hello
received: bye
sending: echo bye
received: quit
request handler shutting down

Note

In Windows you'll need to download and install the JDK, then set up environment variables in order to run Java from the command line.

Customizing echo

Here's an example of an echo customizaton: Casino.

Lab 2: MathHandler

Implement a math handler extending the request handler able to execute commands of the form:

command ::= operator num num etc.
operator ::= add | mul | sub | div
num ::= any number

Starting the math server:

C:\Users\smith\Desktop\bin>java echo.Server math.MathHandler
server address: 0.0.0.0/0.0.0.0
Server listening at port 5555

Starting the client:

C:\Users\000030278\Desktop\bin>java echo.SimpleClient
-> add 2 3 4
9.0
-> mul 2 3 4
24.0
-> quit
bye

Proxy Servers

We can use the Proxy Design Pattern to enhance an existing server without modifying it.

A proxy is a request handler that simply relays incoming requests to a peer correspondent. The peer's responses are relayed back to the client.

The peer could be another proxy, and so proxies can be chained together to form pipelines:

We can create subclasses of ProxyHandler that pre-process incoming requests and/or post-process returning. Examples of proxies include cache proxies, security proxies, and firewall proxies:

Pipelines and Decorators

Notice how the stdin and stdout streams were created in SimpleClient:

stdout = new PrintWriter(
           new BufferedWriter(
                new OutputStreamWriter(System.out)), true);
stdin = new BufferedReader(
           new InputStreamReader(System.in));

System.in and System.out are rudimentary streams that only allow users to read and write a byte at a time. But we can put them at the ends of pipelines in which each stage adds higher level read and write capabilities. (Like the ability to read and write numbers, strings, and objects.) These stages are called filters or decorators. It's essentially the same as the proxy pattern.

Connecting the "pipes"

But how does a proxy handler initialize its peer? We can provide an initPeer method, but who will call it and how will the host and peer be known?

public class ProxyHandler extends RequestHandler {
  
   protected Correspondent peer;

   public void initPeer(String host, int port) {
     peer = new Correspondent();
   peer.requestConnection(host, port);
   }
  
   // etc.
}

One idea is to have this done by the server. When the server creates a handler, it can call the initPeer method:

RequestHandler handler = super.makeHandler(s);
((ProxyHandler)handler).initPeer(peerHost, peerPort);

Unfortunately, we'll need to create a ProxyServer subclass of Server to make these changes:

Here's the proposed synopsis for the proxy server startup:

java echo.ProxyServer service peer-port port peer-host host

Killing Zombies

When a proxy handlershuts down it must shut down it's peer as well, otherwise the peer will be a zombie. I included a method called shutdown in RequestHandler that doesn't do anything, but is called when a request handler receives the quit command. The idea is that the programmers can override this method to clean up any messes they may have made. For example, a proxy handler can override this method to send a quit command to its proxy.

Example: Hit Count Handler

Hit Count Handler that is a proxy that keeps track of the number of clients that have connected to the server since the last time the server was started.

·        HitCountHandler.java

Lab 3: Implement and test ProxyHandler and ProxyServer

To test, you will need to type the following commands into three different command windows:

java echo. Server math.MathHandler

java echo.ProxyServer echo.ProxyHandler 5555 6666

java echo.SimpleClient 6666

Here's a start: ProxyServer.java

Cache Proxy

When a cache proxy receives a request, it looks in its cache to see if this request was made before. If so, it returns the cached response. If not, if forwards the request to its peer. When it receives a response back from the peer, it updates its cache before forwarding the response to the client.

Diagram

Description automatically generated

Keep in mind that there may be many cache handlers running on the same machine. They must all share the same cache, which means the cache must be thread safe. SafeTable overrides the inherited get and put methods as synchronized methods.

Lab 4: Implement and Test the Cache Proxy

Put some diagnostic printouts in your proxy handler so that you can see if it's actually updating and searching the cache correctly.

Start a second simple client connected to the same cache server as the first. In the first client give the command "add 2 3 4". Now give the same command to the second client to see if the response is found in the cache.

Lab 5: Security Proxies

A security proxy maintains a user table of the form:

user

password

jones

abc

smith

xyz

 

 

A client can create an entry in this table with the request:

new user password

The request handler creates an entry in the table (assuming the user name is unique) then terminates the session.

To send a request to the server, a client must first log in with the request:

login user password

If the request handler is able to verify the login, all subsequent requests are forwarded to the peer. Otherwise the session is terminated.

Lab 6: Exchanging Objects

1. Add object input and output streams to the Correspondent class. Initialize them with the socket's input and output streams in the initStreams method. Provide methods:

void writeObject(Object msg) { ... }
Object readObject() { ... }

2. Add the following Message class to the echo package:

public class Message<T extends Serializable> implements Serializable {
   T content;
   public Message(T content) {
     this.content = content;
   }
   public String toString() {
     return "[" + content + "]";
   }
}

Sending a message through a socket is similar to writing it to a file and reading it back. The message must implement the Serializable interface. But its content type, T, must also. Notice how "extends" is used to bound the potential values of T to only classes that are themselves serializable.

3. Modify SimpleClient so that it wraps the user input in a message, then sends it to the server using sendObject:

sendObject(new Message<String>(msg));

Use receivedObject to receive the server's response.

4. Also modify RequestHandler so that it receives requests using requestObject. If the request is an instance of Message<String>, then pass its content to the response method. Wrap the return value into a message and send it back to the client using sendObject.

5. Test using the MathHandler and the CacheHandler.

Distributed Objects Architecture and Remote Method Invocation

As an alternative to the client-server architecture, we can view networks as collections of distributed objects. Method invocation looks the same regardless of whether someObject is local or remote:

   x = someObject.someMethod(a, b, c);

This is achieved through a technique called remote method invocation (RMI). (RMI is available in Java.)

The idea is to provide the client with a stub object. The stub implements the same interface as the remote object and is therefore indistinguishable from it.

The stub implements each method by sending a request to a request handler called the skeleton, which is local to the remote object.

Here's the set up in UML:

Here's a sketch of the code:

interface IRemote {
   String serviceA(String x);
   String serviceB(String x);
}

class RemoteObject implements IRemote {
   String serviceA(String x) { return "aaa" + x; }
   String serviceB(String x) { return "bbb" + x; }
}

class Stub extends Correspondent implements IRemote {
   String serviceA(String x) {
     sendObject(new Message("serivceA", x));
     return (String)receiveObject();
   }
   String serviceB(String x) {
     sendObject(new Message("serivceB", x));
     return (String)receiveObject();
   }
}

class Client {
   IRemote remoteObject = new Stub();
   void doSomething() {
   System.out.println(remoteObject.serviceA("ccc"));
   System.out.println(remoteObject.serviceB("ccc"));
   }
}

class Skeleton extends RequestHandler {
   IRemote obj = new RemoteObject();
   String response(Message m) {
     if (m.method == "serviceA") return obj.serviceA(m.input);
     if (m.method == "serviceB") return obj.serviceB(m.input);
      return null;
   }
}