Client-server applications were covered in our discussion of applets. How do client and server programs communicate over the Internet? They use a connected pair of abstract communication channels called sockets. A socket provides an input stream for receiving data, and an output stream for sending data. The input stream of one socket is connected to the output stream of the socket it is connected to:
How are connected sockets created? A server socket is an abstract listening device associated with an IP address and a port number. (The IP address is associated with the computer that runs the server. Most computers provide 64,000 port numbers to choose from.) Here is how a server socket is created by the server:
Java now provides a web-based, client-server framework where applets are the clients and servlets are the servers. An applet is run by a Java virtual machine embedded in a browser. A servlet is run by a servlet engine embedded in a web server. Before the servlet framework was available, I used the server framework described below.
A Generic Server
A generic server instantiates the Master-Slave pattern. The server (i.e., the master) perpetually listens at a server socket for an incoming client request. When a request arrives, the server constructs and starts a handler thread (i.e. the slave) to service the request. Each slave is provided with a unique identification number for debugging and a socket connection to the client. The server constructs the handler using a handler factory object, which is passed to the server's constructor. Thus, creating a specific type of server requires programmers to write a handler and a handler factory object.
Notice that the attributes of Server are protected so they can be freely accessed by concrete server class extensions.
// listens for a
request, then creates & starts a handler
public void listen()
{
int id = 0;
try
{
while(true)
{
System.out.println("Server: listening");
Socket request = mySocket.accept(); // blocks
RequestHandler handler =
myHandlerFactory.makeHandler(request, nextID++);
handler.start();
} // while
System.out.println("Server: halting");
} // try
catch(IOException ioe)
{
System.err.println("Failed to accept socket, " + ioe);
System.exit(1);
} // catch
} // listen
protected ServerSocket
mySocket;
protected int myPort;
protected InetAddress
myAddr;
protected HandlerFactory
myHandlerFactory;
protected static
int nextID = 0;
} // Server
A request handler instantiates a class derived from the abstract RequestHandler class, which is derived from the Thread class. An abstract request handler creates reader and writer character streams from the input and output byte streams associated with the client socket (this seems to be what JDK 1.1 wants us to do, although data streams would be another logical candidate). Notice that these character streams, together with the client socket and the id number, are declared protected, hence are available to the concrete handler.
The run() method of the abstract request handler calls an abstract processRequest() method. This must be defined in the concrete request handler.
public void run()
{
boolean more = true;
while(more) more = processRequest();
}
public abstract boolean processRequest();
protected Socket
request;
protected BufferedReader
in;
protected PrintWriter
out;
protected int myId;
} // RequestHandler
The only requirement of a handler factory is that it provide a method called makeHandler(), which expects a socket and id number as input, and returns a request handler as output. We can enforce this with a handler factory interface:
Applets can only communicate with their host servers. Reflectors are a Java idiom for working around this restriction. A reflector is a server running on the host server and listening to a socket for messages coming from applets. When a message arrives, the reflector forwards the message to a fixed destination object.
To use the server framework defined above, we need to extend the RequestHandler class with a reflector handler class that provides a processRequest() method. In this case processRequest() reads an applet message (a String) on its inherited BufferedReader, in, then writes the message to a PrintWriter connected by a socket to the destination object. The IP address and port number of the destination object is specified in the reflector handler's constructor:
public boolean processRequest()
{
try
{
String msg = in.readLine();
destOut.println(cmmd);
}
catch(IOException ioe)
{
System.err.println("Request read failure; " + ioe.toString());
stop();
} // catch
return false;
} // processRequest
private BufferedReader
destIn;
private PrintWriter
destOut;
} // ReflectorHandler
public ReflectorFactory(String
loc, int port)
{
destLoc = loc;
destPort = port;
}
public RequestHandler
makeHandler(Socket s, int id)
{
return new ReflectorHandler(s, destLoc, destPort);
}
} // ReflectorFactory
A database server is a special type of server that provides database access to clients. The handler for a database server (called an SQL handler) reads an SQL command from the client, executes the command using the execute method provided by Database instances, then sends a formatted string representing the result back to the client:
public SQLHandler(Socket
s, int id, Database db)
{
super(s, id);
myDbase = db;
}
public boolean processRequest()
{
try
{
String sql = in.readLine();
String result = myDbase.execute(sql);
out.writeBytes(result);
request.close();
} // try
catch(IOException ioe) {} // catch
return false;
} // processRequest
} // SQLHandler
public SQLHandlerFactory(String
dm, String url,
String user, String pswd)
{
myDbase = new Database(dm, url, user, pswd);
}
public RequestHandler
makeHandler(Socket s, int id)
{
return new SQLHandler(s, id, myDbase);
}
} // SQLHandlerFactory