The client/server model is an application development architecture designed to separate the presentation of data from its internal processing and storage. This paradigm is based on the theory that the rules which operate on data do not change no matter how many applications are accessing that data. An airline might want to allow customers to purchase tickets on the Web, through a travel agent, or even through ATM-like machines located in airports and malls. No matter which interface the customer uses, the number of seats available does not change, nor does the fact that you cannot sell a 21-day advance fare ten days before the flight. The client/server model would enable the airline to build the application so that each interface accesses the same seat availability data and calls the exact same rules to validate the sale of a ticket.
With the rapid rise of the World Wide Web, the power of the client/server paradigm now serves the masses. In feeding the same exact data to multiple client browsers across the Internet, even the most basic HTML page fits the client/server model in its simplest form. Tools like CGI, and now Java, enhance the Web's use of the client/server model by consolidating application rules on the Web server.
The Web provides an excellent example of the most basic reason for separating the presentation of data from its storage and processing. As a Web developer, you have absolutely no control over the platforms and software with which users are accessing your data. You might consider writing your application for each potential platform that you are targeting. For the airline ticketing system discussed in the previous example, this approach forces you to recode the rule for a 21-day advance fare three times if that rule changes. Obviously, writing the application for each possible platform is a recipe for a maintenance nightmare.
In addition to having little control over the systems being used for the presentation of your data, a complex application often has different needs, which are often best met by different hardware or operating systems. An ATM-like machine in a mall designed specifically for selling airline tickets has no need for the hardware required by home computers to provide a graphical user interface. Similarly, the user's home computer is generally not well-suited for acting as a massive data storage device on the level required by such an application.
The primary selling point of Java as a programming language, the Java virtual machine, also provides its primary selling point as a tool in client/server development. With code portability difficult to deliver in any other programming language, Java instead enables developers to write the user interface code once, distribute it to the client machine, and have the client machine interpret that presentation in a manner that makes sense for that system.
Beyond architecture independence, Java provides a rich library of network enabled classes that allow applications ready access to network resources in the form of traditional TCP/IP addressing and URL referencing. New tools, such as JavaSoft's Remote Method Invocation (RMI), promise only to extend Java's network usability.
The final beauty of Java in client/server development involves deployment strategies. In traditional client/server systems, deployment of an application requires users to physically install the client portion of the application on their machine. A Java client system, on the other hand, can be executed from across the network. As a result, client machines are always running the most current version of the application.
Client systems generally have a clear separation from the servers with which they work. The underlying mechanics of the system are hidden from users who generally only need a portion of the functionality provided by the server system. The client application serves a particular problem domain, such as order entry, accounting, game playing, or ticket purchasing, and talks to the server through a narrow, well-defined network interface.
The server portion of a client/server application manages resources shared among multiple users, often accessing the server through multiple client front-ends. A Web server, for example, delivers the same HTML pages across the Internet to Web users. More complex applications, such as business database applications, enable clients to make query requests through the server and receive the results.
Developers commonly use one of two client/server architectures in system design:
The simple retrieval and display of information part of serving HTML pages is an example of a two-tier client/server. On one end, or tier, data is stored and served to clients. On the other end, that data is displayed in a format that fits the situation.
On many systems, however, the retrieval and display of data forms only a fraction of the system. A complex business system generally involves the processing of data before it can be displayed or saved back into storage. In a two-tier system, the client handles a majority of this extra processing. This heavily loaded client is often referred to as a "fat client."
A two-tier design using a fat client provides a quick and dirty architecture for building small, non-critical systems. The fat client architecture shows its dirty side in maintenance and scalability. With data processing so tightly coupled to the GUI presentation, user interface changes necessitate working around the more complex business rules. In addition, the two-tier system ties the client and server together so tightly that distributing the data across databases becomes difficult.
Three-tier client/server design mandates that data processing should be separated from the user interface and data storage layers. Stored procedures provide the most common method of intervening between user interface and data storage in a third tier. A stored procedure performs complex database queries and updates, returning the results to the client.
While two-tier development simply separates data storage from presentation, the three-tier system consists of the following layers:
In isolating application functionality in three-tier development, the system becomes easier to maintain and modify. The user interface, for example, no longer cares where or how the system stores its data. Changes in data storage, such as distributing the data across multiple databases, ends up having a much smaller impact on the system as a whole.
The Web provides developers with an ideal application deployment infrastructure, especially when Java is part of the picture. Unlike other client/server development tools, Java is distributed at runtime across the Internet to client machines. By storing the application on a central server and downloading it at runtime, the user is always using the latest release of the software. Consider the following URL:
appletviewer http://www.strongsoft.com/Java/test.html
TCP/IP sockets form the basic mode of data communication on the Internet. The Java Development Kit addresses TCP/IP programming requirements through a high level suite of APIs and TCP/IP streams. An application can use these streams to enable network input and output to be manipulated in a variety of ways. The java.io package from the standard Java release defines these forms, which include DataInputStreams, DataOutputStreams, and PrintStreams.
The package java.net has the backbone TCP/IP classes provided by Java. From these classes, the application can create the data I/O streams from java.io that it needs for network communication. Java provides these network classes in java.net for its socket support:
Platform-specific implementations extend the abstract class SocketImpl to perform the low-level network interfacing, which varies from system to system. The high level Socket and ServerSocket classes in turn reference the system's particular SocketImpl class for network access. Because the default implementations of the basic JDK classes are not firewall-aware, it is possible to extend the SocketImplFactory and SocketImpl classes to provide firewall functionality.
Starting with a ticker tape applet, TickerTape, the following listing features a facility for subscribing to a broadcast ticker tape server. The server is designed to broadcast messages to a list of connected clients. Because the reception of particular messages in a ticker tape system is unimportant, this example uses the simplest form of sockets, the DatagramSocket. A datagram uses UDP, or Unreliable Datagram Protocol. UDP is a broadcast protocol that does not guarantee the reception of its messages. Because they do no reception checking, however, UDPs have a lower resource overhead than protocols which perform error checking. If you are sending out roughly the same information repeatedly (as is being done in Listing 34.1) then the resource savings outweigh any problems related to losing a packet every now and then.
Listing 34.1. The client applet, TickerTape.java.
import java.awt.Color;
import java.awt.Graphics;
import java.net.*;
import java.io.InputStream;
import java.util.Date;
/**
Scrolls a line of text read from a UDP server
g
@version 0.1, 28 Apr 1996
*/
public class TickerTape extends java.applet.Applet implements Runnable {
String message_error = "Error - No Message ";
String message;
/** The width of the string in pixels. No need to know this since
we can reset the string */
int messageWidth;
/** keep track of where we are printing the current string */
int position;
/** Thread */
Thread ticker = null;
/** Just a way to check if the thread is suspended or not. If
through some bug this gets set wrong it just prints
wrong commands */
boolean suspend = false;
/* The amount of time to rest between redrawing line */
int rest = 100;
/** amount of space to jump. Hopefully negative numbers will
move it in the other direction */
int jump = 5;
public void init() {
String tmpParam;
tmpParam = getParameter("jump");
if( tmpParam != null ) {
jump = new Integer(tmpParam).intValue();
if( jump == 0 ) {
jump = 5;
System.out.println("Zero value for jump: using 5");
}
}
tmpParam = getParameter("rest");
if( tmpParam != null ) {
rest = new Integer(tmpParam).intValue();
if( rest < 0 ) {
rest = 100;
System.out.println("Negative rest value: using 100");
}
}
message = getMessage();
if( message == null ) message = message_error;
messageWidth = getFontMetrics(getFont()).stringWidth(message);
position = (jump < 0) ? -messageWidth : size().width;
setForeground(Color.red);
setBackground(Color.white);
}
public void start() {
if( ticker == null ) {
ticker = new Thread(this);
ticker.start();
}
}
public void stop() {
ticker = null;
}
public void run() {
while( ticker != null ) {
repaint();
if( ((jump < 0) && (position > size().width)) ||
(position < -messageWidth) )
position = (jump < 0) ? -messageWidth : size().width;
try Thread.sleep(rest);
catch( InterruptedException e ) ticker = null;
position -= jump;
}
}
public void paint(Graphics g) {
g.drawString(message, position, getFont().getSize());
}
public String getMessage() {
int port;
InetAddress address;
DatagramSocket socket;
DatagramPacket packet;
byte[] sendBuf = new byte[256];
try {
socket = new DatagramSocket();
port = 1111;
address = InetAddress.getByName("StrongSun");
packet = new DatagramPacket(sendBuf, 256, address, port);
socket.send(packet);
packet = new DatagramPacket(sendBuf, 256);
socket.receive(packet);
message = new String(packet.getData(), 0);
socket.close();
}
catch( Exception e ) {
System.err.println("Exception: " + e);
e.printStackTrace();
}
return message;
}
}
The HTML code for this applet's Web page follows:
<TITLE>Ticker Tape Applet Using UDP</TITLE>
<H1>Test of the TickerTape Datagram Applet</H1>
<HR>
<APPLET CODE="TickerTape" WIDTH=400 HEIGHT=25>
<PARAM NAME=jump VALUE="7">
TickerTape applet not loaded!
</APPLET>
<HR>
The TickerTape applet uses a loop in a second thread which repeatedly calls the getMessage() method. This method returns a String to use as the scrolling text from the ticker tape server. It does this first by instantiating a new DatagramSocket and sending a request to the server. The server responds with a String to be painted for the user. Listing 34.2 provides the server code.
Listing 34.2. The TickerTapeServer application, TickerTapeServer.java.
import java.io.*;
import java.net.*;
import java.util.*;
class TickerTapeServer extends Thread{
private DatagramSocket socket = null;
private String broadcastMessage = "TickerTapeServer Messages Here!";
TickerTapeServer() {
super("TickerTapeServer");
try {
socket = new DatagramSocket(1111);
System.out.println("TickerTapeServer listening on port: " +
Âsocket.getLocalPort());
} catch (java.net.SocketException e) {
System.err.println("Could not create datagram socket.");
}
}
public static void main(String[] args) {
if (args.length != 1) {
System.out.println("Usage: java TickerTapeServer " +
"'your text here in quotes' ");
return;
}
TickerTapeServer tts = new TickerTapeServer();
tts.start();
tts.setBroadcastMessage (args[0]);
}
public void run() {
if (socket == null) return;
while (true) {
try {
byte[] buf = new byte[256];
DatagramPacket packet;
InetAddress address;
int port;
String dString = null;
packet = new DatagramPacket(buf, 256);
socket.receive(packet);
address = packet.getAddress();
port = packet.getPort();
dString = getBroadcastMessage();
dString.getBytes(0, dString.length(), buf, 0);
packet = new DatagramPacket(buf, buf.length, address, port);
socket.send(packet);
} catch (Exception e) {
System.err.println("Exception: " + e);
e.printStackTrace();
}
}
}
public String getBroadcastMessage () {
return broadcastMessage;
}
public void setBroadcastMessage (String s) {
broadcastMessage = s;
}
}
Naturally, before any client can connect, you need to start the server application. To start it, you simply issue the following command:
java TickerTapeServer 'My broadcast message'
Client applications see whatever message you specify on the command line. In a second thread, the server application waits for clients to connect before sending them the broadcast message. Once it receives the client request, the server application grabs the client's address and sends the broadcast message back to the client.
TCP is a more reliable form of communication than UDP. Unlike UDP, TCP sockets perform error-checking to ensure the packets are delivered to their destination. TCP sockets are connection-based sockets, meaning that a TCP socket is a two-way form of communication maintained until one side or the other breaks it off. This contrasts with the connectionless broadcast essence of UDP.
In order to create a TCP-based client/server example, you first need to build a framework for network access.
Creating a TCP connection to a server involves only the following code fragment:
java.net.Socket connection;
try {
connection = new java.net.Socket("athens.imaginary.com", 1701)
} catch( Exception e ) {
}
The constructor for the Socket class requires a host with which to connect, in this case "athens.imaginary.com", and a port number, which is the port of a mud server. If the server is up and running, the code creates a new Socket instance and continues running. If the code encounters a problem with connecting, it catches the problem in the form of an exception. To disconnect from the server, the application should call:
connection.disconnect();
A simple socket client looks like the following:
public class BasicClient {
boolean active;
java.net.Socket connection;
public BasicClient(String address, int port) {
try {
connection = new java.net.Socket(address, port);
active = true;
}
catch( java.io.IOException e ) {
active = false;
}
}
public void done() {
if( !active ) return;
connection.close();
active = false;
}
}
Socket I/O is blocking in nature, meaning that when an application tries to read from a socket, all processing in that thread comes to a halt until something is read from that socket. Fortunately, Java is very friendly to multithreaded programming. Socket programmers can use Java threads to read from a socket in one thread and write to it in another, and perhaps perform additional processing in another. This extended version of our basic client implements it with a multi-threaded structure:
public class BasicClient implements Runnable{
private boolean active = false;
private java.net.Socket connection = null;
private Thread thread = null;
private String address;
private int port;
public BasicClient(String addr, int p) {
address = addr;
port = p;
}
public void start() {
if( thread == null ) {
thread = new Thread(this);
thread.start();
}
}
public void stop() {
if( thread != null ) {
thread.stop();
thread = null;
}
if( active ) {
if( connection != null ) {
try {
connection.close();
active = false;
connection = null;
}
catch( java.io.IOException e ) {
}
}
else active = false;
}
}
public void run() {
try {
connection = new java.net.Socket(address, port);
active = true;
}
catch( java.io.IOException e ) {
active = false;
System.out.println("Failed to connect to server.");
}
}
public boolean isActive() {
return active;
}
}
Retail applications are simple client/server uses of the Internet that provide a perfect example of how to structure such a program in Java. Any retail application first requires a server program that provides data to customers and takes their orders; then it needs a client program that provides the interface that allows them to view a product line and enter purchase requests.
Of course, any system involving the exchange of money over the Internet has some hefty security requirements. For the sake of simplicity, however, we will ignore security requirements and deal with the basic building blocks of socket-based client/server programming. Our application, a music store for the Web, should therefore have the following functionality:
Though most of the time it is simply listening for incoming client connections, the server is responsible for quite a bit of work. Listing 34.3 provides the server portion of the application.
Listing 34.3. The Web music store server.
import java.net.Socket;
import java.net.ServerSocket;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
public class Server extends Thread {
private Connection connection;
private Socket socket;
public Server(Socket sock, Connection conn) {
socket = sock;
connection = conn;
}
public void run() {
java.io.DataInputStream input;
java.io.PrintStream output;
try {
String tmp;
java.util.StringTokenizer tokens;
java.util.Vector albums = new java.util.Vector();
boolean transacting = true;
Statement statement;
ResultSet result_set;
input = new java.io.DataInputStream(socket.getInputStream());
output = new java.io.PrintStream(socket.getOutputStream());
statement = connection.createStatement();
result_set = statement.executeQuery("SELECT album_id, artist, title, "+
&quo t;price " +
& nbsp;"FROM album ");
while( result_set.next() ) {
String id, artist, title, price;
id = result_set.getString(1);
artist = result_set.getString(2);
title = result_set.getString(3);
price = result_set.getString(4);
albums.addElement(id + ":" + artist + ":" + title + ":" + price);
}
output.println(albums.size());
for(int i = 0; i<albums.size(); i++)
output.println((String)albums.elementAt(i));
while( transacting ) {
tmp = input.readLine();
tokens = new java.util.StringTokenizer(tmp);
tmp = tokens.nextToken();
if( tmp.equals("exit") ) {
transacting = false;
}
else if( tmp.equals("purchase") ) {
String credit_card;
String id;
if( tokens.countTokens() != 2 ) {
output.println("error Invalid command");
socket.close();
return;
}
id = tokens.nextToken();
credit_card = tokens.nextToken();
statement = connection.createStatement();
statement.executeUpdate("INSERT INTO purchase (" +
"credit_card, album) " +
"VALUES('" + credit_card + "', '" +
id + "')");
output.println("ok");
}
}
}
catch( Exception e );
finally {
try {
socket.close();
}
catch( java.io.IOException e );
}
}
static public void main(String args[]) {
ServerSocket port_socket;
String driver;
String url;
int port;
if( args.length != 3 ) {
System.err.println("Syntax: java Server <JDBC driver> <JDBC URL> " +
"<port>");
System.exit(-1);
return;
}
driver = args[0];
url = args[1];
try {
port = Integer.parseInt(args[2]);
}
catch( NumberFormatException e ) {
System.err.println("Invalid port number: " + args[2]);
System.exit(-1);
return;
}
try {
port_socket = new ServerSocket(port);
}
catch( java.io.IOException e ) {
System.err.println("Failed to listen to port: " + e.getMessage());
System.exit(-1);
return;
}
while( true ) {
try {
Connection conn;
Server server;
Socket client_sock = port_socket.accept();
conn = java.sql.DriverManager.getConnection(url, "user", "pass");
server = new Server(client_sock, conn);
server.start();
}
catch( java.io.IOException e ) {
System.err.println("Connection failed.");
}
catch( java.sql.SQLException e ) {
System.err.println("Failed to connect to database.");
}
}
}
}
The application uses the main thread of the server simply to listen to the network for connections. Each time it accepts a connection, it creates a new instance of itself to handle the client/server communication in a separate thread.
The most tedious and most difficult aspect of client/server programming with sockets involves the actual protocol you create for the communication. The music store server uses a very simple protocol for communicating with a client. It simply sends it a full list of all titles in stock, then waits for either a purchase request or an end processing notification. Even with this simplistic protocol, however, we have to handle the parsing of each string sent by the client.
The core Java libraries do help simplify protocol management through the StringTokenizer utility. This class breaks up a string into individual tokens based on a delimiter. By default, the delimiter is a space. With purchase requests, we expect a string in the form "purchase <album id> <credit card number>". The first token thus is the purchase command, the second token the album ID, and the third token the credit card number used to purchase the album.
Listing 34.4 provides the socket code for the client end of the application. It assumes that some sort of user interface is built on top of it.
Listing 34.4. The music store client socket code.
import java.io.DataInputStream;
import java.io.PrintStream;
import java.net.Socket;
public class Client {
private Socket socket;
private String host;
private int port;
private java.util.Vector albums = new java.util.Vector();
private DataInputStream input;
private PrintStream output;
public Client(String h, int p) throws java.io.IOException {
String data[] = new String[4];
java.util.StringTokenizer tokens;
String tmp;
int x;
host = h;
port = p;
socket = new Socket(host, port);
input = new DataInputStream(socket.getInputStream());
output = new PrintStream(socket.getOutputStream());
tmp = input.readLine();
try {
x = Integer.parseInt(tmp);
}
catch( NumberFormatException e ) {
throw new java.io.IOException("Communication error, invalid " +
"number of albums.");
}
while( x-- > 0 ) {
tmp = input.readLine();
tokens = new java.util.StringTokenizer(tmp, ":");
if( tokens.countTokens() != 4 ) {
throw new java.io.IOException("Invalid album format.");
}
for(int i=1; i<=4; i++) data[i] = tokens.nextToken();
albums.addElement(data);
}
}
public synchronized void close() throws java.io.IOException {
output.println("exit");
socket.close();
}
public String[][] getAlbums() {
String album_data[][];
synchronized(albums) {
album_data = new String[albums.size()][];
albums.copyInto(album_data);
return album_data;
}
}
public synchronized void purchaseAlbum(String id, String cc)
throws java.io.IOException {
String tmp;
output.println("purchase " + id + " " + cc.trim());
tmp = input.readLine();
if( tmp.equals("ok") ) return;
else throw new java.io.IOException(tmp);
}
}
Again, protocol negotiation differentiates this code from the basic client code shown earlier in the chapter. When the applet or application creates this Client object, it connects to the Server program and gets a list of all albums. Upon receiving an album, it uses the StringTokenizer in a slightly different fashion to break up the string into its components. As you saw in the Server code, information about an album is packed into a single string separated by a ":". By default, the StringTokenizer splits the string on a space. To change the delimiter, it needs a second argument to its constructor, the string to serve as the delimiter. In this case, we passed a ":".
The rest of this client code simply provides methods for the user interface to communicate with the server. Specifically, it allows the user interface to close the connection, get the list of albums, and purchase an album.
The basic building blocks for client/server programming are the IP sockets that form the communication layer of the Internet. While socket programming can be very tedious and time consuming, Java has provided classes designed to minimize this tedium to enable developers to harness the power of client/server programming. The DatagramSocket, ServerSocket, and Socket classes all provide access to the IP protocols themselves. The DataInputStream, DataOutputStream, and PrintStream classes provide access to the data. Finally, the StringTokenizer class provides simple data manipulation.
The most important factor in creating a socket communication layer is understanding exactly what your application should communicate. You cannot create the necessary communication protocol if you do not fully understand what the client and server need to be saying to each other.