Our framework uses the Master-Slave Design Pattern. The server plays the role of master. It perpetually listens for requests from clients. When a request is received, the server creates a request handler slave to interact with the client. The handler runs in its own thread so that the master can resume listening for more client requests.
There are two types of client-handler interactions: stateless and stateful. In a stateless interaction, the handler replies to the client's request and the interaction ends. (HTTP is an example of a stateless interaction. Cookies or other tricks must be used to emulate statefulness.) In a stateful interaction there may be an unlimited number of exchanges between client and handler. Such exchanges must follow some sort of protocol, often represented by a state diagram. For example:
It's the handler's job to keep track of the state of the conversation. Our framework is stateful.
A correspondent can send and receive messages through a socket connected to another correspondent:
· Clients and request handlers are correspondents.
· Request handlers are active objects. They run in their own thread.
· Messages are sent across process/machine boundaries using sockets.
· A socket s has an output stream (s.out) and an input stream (s.in). In the example below scs.out is connected to rhs.in, and vice-versa. (See below for more details about sockets.)
· When a request handler receives a message from a client, it uses its response method to construct a reply that is sent back to the client.
· In this diagram I am representing socket-to-socket messages as asynchronous messages. Although, this might be more appropriate for UDP sockets.
The server's listen() method perpetually listens for client requests. The makeHandler() method creates a request handler and connects it to the client.
· If a socket is analogous to a telephone, then a server socket is analogous to a switchboard. It connects the client's socket to the handler's socket.
· Each server running on a particular computer is associated with a port number. Unix, for example, provides over 65K port numbers.
· By default, makeHandler returns an instance of RequestHandler. This method should be overridden in subclasses.
The server perpetually listens to its server socket for incoming connection requests from clients. When a request is received, the server creates a handler thread to respond to interact with the client, then resumes listening:
Our framework will be contained in the cs package. Customizations, such as the command processor customization shown below, are implemented in dependent packages:
Once a correspondent receives a socket, it wraps its input and output stream with filters (i.e., decorators) to enhance their usability:
sockIn = new BufferedReader(new
InputStreamReader(sock.getInputStream()));
sockOut = new PrintWriter(sock.getOutputStream(), true);
The simple client provides a console interface:
while(true) {
prompt user
read user command
send command to server
read server response
display response
}
Users can specify the host and port of the desired server as command line arguments (in main). The default host is "localhost". The default port number is 5555.
The RequestHandler implements Java's Runnable interface, which requires the handler to implement a run() method.
The server creates a slave thread associated with the handler, then calls the slave's start method. This calls the handler's run method:
Thread slave = new
Thread(handler);
slave.start(); // calls handler.run
The run method perpetually interacts with the client:
while(message != "quit") {
receive message from client
if quit break and close socket
construct a response
send response to client
}
The default implementation of the response method is simply to echo the client's message. This method should be overridden in customizations.
The server's listen method perpetually listens for client requests:
while(true) {
Socket socket = mySocket.accept(); // this blocks if no pending requests
make request handler for client
launch request handler in its own thread
}
The server's main method allows users to specify the port number as a command line argument.
As an example, we extend the RequestHandler and override the response method so it executes commands of the form:
add NUMBER ... NUMBER
mul NUMBER ... NUMBER
sub NUMBER NUMBER
div NUMBER NUMBER
In a separate package (cp) we implement an extension of the request handler called a command processor and an extension of the server called a command server:
The command processor's response method:
1. Splits the input command into an array of tokens (i.e., strings that don't contain white space characters).
2. Starting from position 1, parses tokens into numbers.
3. Performs the arithmetic combination of the numbers specified by token 0.
4. Throws exceptions if any token is invalid or if division by 0 is attempted.
Private helper methods implement the arithmetic operations.
The command server's makeHandler method returns a new command processor. The main method creates a command server and invokes its listen method.
It's useful to first export the project's binaries to the desktop or some other easily accessible directory.
Next, open a command window on the bin directory, above the cs and cp packages.
In this example we will start the server listening on port 6666:
C:\Users\smith\Desktop\bin>java cp.CommandServer 6666
server address: 0.0.0.0/0.0.0.0
Server listening at port 6666
In a separate command window on the same directory start the simple client:
C:\Users\000030278\Desktop\bin>java cs.SimpleClient 6666
-> add 2 3 4
sending: add 2 3 4
received: 9.0
-> mul 2 3 4
sending: mul 2 3 4
received: 24.0
-> quit
bye
Here's the output on the server side:
Server listening at port 6666
received: add 2 3 4
sending: 9.0
received: mul 2 3 4
sending: 24.0
received: quit
request handler shutting down
· Create the framework by completing Server.java and RequestHandler.java.
· Create and test the command processor customization of the framework.
A cache proxy sits between the simple client and the command processor. It's a middleman. As such it is both a client and a server.
Upon receiving a command from a client, the cache proxy creates a cache handler. The handler searches a cache to determine if the command has been seen before and if so, what the result was. If not, the handler forwards the command to the server, which replies with the result. The cache handler updates the cache and returns the result to its client.
We only need to extend the RequestHandler from the framework:
1. Because a correspondent only has a single socket, the cache handler needs a cache client which connects to the server.
2. Note that the cache itself is a static member of CacheHandler. This insure a single, globally accessible class.
3. Of course with many cache handlers running simultaneously, synchronization issues could arise. Therefore we are using Java's ConcurrentMap interface to guarantee atomicity of handler access to the cache.
Create a third window opened onto the bin directory.
Start another server. (We are assuming the command processor server is still running on port 6666.) The new server listens and port 7777 and will use instances of your CacheHandler as slaves:
C:\Users\000030278\Desktop\bin>java cs.Server 7777
cs.CacheHandler
server address: 0.0.0.0/0.0.0.0
Server listening at port 7777
Restart SimpleClient. But this time have it connect to port 7777:
C:\Users\000030278\Desktop\bin>java cs.SimpleClient 7777
-> add 2 3 4
sending: add 2 3 4
received: 9.0
-> add 2 3 4
sending: add 2 3 4
received: 9.0
Here's the output in the cache server window:
Server listening at port 7777
received: add 2 3 4
sending: 9.0
received: add 2 3 4
found it
sending: 9.0
Note that the second time the command was found in the cache.
The two important protocols of the Internet's transport layer are the User Datagram Protocol (UDP) and the Transmission Control Protocol (TCP). UDP is the asynchronous message=passing protocol while TCP is the synchronous message-passing protocol.
In TCP processes exchange bytes using stream-mode sockets. A stream-mode socket, s, consists of two streams, an input stream (s.in) and an output stream (s.out). Assume processes A and B want to have a conversation, then A and B will each need a stream-mode socket, A.s and B.s. Furthermore, A.s.out must be connected to B.s.in and B.s.out must be connected to A.s.in:
Think of a socket as a telephone. For A to call B, both need phones (sockets). A needs to know the number (address) of B's phone, the transmitter (output stream) of A's phone must be connected to the receiver (input stream) of B's phone, and the receiver of A's phone (input stream) must be connected to the transmitter of B's phone (output stream).