All Categories :
Java
Chapter 25
Client/Server Fundamentals
by George Reese
CONTENTS
A network connects two or more computers together to perform tasks
individual computers cannot do on their own. In business, government,
education, and research, the network has been a common way of
extending the power of the computer. On the simplest level, a
network enables multiple users to share the same work. For example,
almost anywhere you see a network, you see people working on the
same documents and spreadsheets.
On a more complex level, a network enables a single computer to
share the hardware resources of other computers. One computer
may be a powerful graphics machine and another may be a powerful
number cruncher. If you put the two together, you can perform
tasks that require both heavy graphics and heavy number crunching
in an optimized fashion.
Over the past decade, the client/server architecture has grown
to be the most common design for network applications. Client/server
is based on the idea that one computer specializing in information
presentation displays the data stored and processed on a remote
machine.
Today, the Internet provides home computers with the same networking
power institutions outside the home have traditionally used. Many
of the Internet applications you have come to know are client/server
applications: the Web, e-mail, ftp, telnet, and so on. Specifically,
your home PC serves as the client side of the architecture.
It displays information located on servers around the world.
This chapter takes a look at the basics of client/server programming
and how Java supports the client/server architecture. The first
step, of course, is to understand exactly what it is that the
client/server architecture tries to accomplish. We then take a
look at the basic building blocks of client/server programming:
sockets. Finally, we take a look at ways we can extend that architecture
to build powerful Internet applications.
The most basic form of the client/server architecture involves
two computers: one computer, the server, is responsible for storing
some sort of data and handing it to the other computer, the client,
for user interaction. The user can modify that data and save it
back to the server. The Web implements this simple form of client/server
architecture for multiple client machines. Your computer, the
client, uses a Web browser to display HTML documents stored across
the Internet on a Web server.
There are four software components to the Web system:
- A browser such as Netscape that
displays HTML documents on a client machine.
- A server program running on the
server that hands HTML documents to client browsers.
- The HTML documents stored on the server
machine.
- The communications protocol that handles
the communication of data between the client and server.
The diagram in Figure 25.1 shows how this architecture fits together.
Figure 25.1: The client/server architecture of the Web.
Dividing the Work
The client/server architecture provides us with a logical breakdown
of application processing. In an ideal environment, the server
side of the application handles all common processing, and the
client side handles user-specific processing. With the Web, the
server stores the HTML documents that are shown to all the clients.
Each client, on the other hand, has different display needs. For
example, a user at a dummy terminal is limited to using the character-mode
Lynx client. A Windows user, on the other hand, can use a GUI
browser to use the power of the graphical interface to display
the document using full multimedia. The presentation of the HTML
documents presented by the server is thus left up to the client.
Client/Server Communication
Internet applications communicate using Internet Protocol (IP)
sockets. IP is a basic networking protocol on top of which other
protocols exist to serve varying purposes. The details of how
IP works are very boring and, outside of addressing, are fortunately
unimportant to anyone who wants to write network applications.
Each computer in an IP network has an IP address, which
is a 32-bit number usually broken into four 8-bit quads. An IP
address looks like this: 206.11.201.18.
The first two numbers (the high-order bits) form the network address.
The low-order bits specify which computer on that network the
address is for. Using this multinetwork addressing scheme, computers
on different networks can communicate with each other.
IPv6: THE NEXT GENERATION |
The current 32-bit scheme can address 4 billion hosts on 16.7 million networks. Optimistic projections suggest that we will run out of these addresses some time between 2005 and 2011. On November 17, 1994, the Internet Engineering Task Force (IETF) accepted a recommendation for a new version of IP called IPng (IP: Next Generation) to handle this problem and related issues. This new IP specification, also called IPv6, uses a 128-bit addressing scheme. Under pessimistic projections, IPng provides 1,564 IP addresses for each square meter of the planet Earth. Optimistic projections suggest that it will provide over 3 quintillion addresses for each square meter of the Earth's surface. This new specification is designed to interoperate with the existing IP standards so that each machine on the Internet can simply do a software upgrade when ready.
|
When I want to send some information from my machine to yours,
my application uses your machine's IP address to send that data
to your machine. If your machine has the address 199.199.181.120,
for example, my machine first checks whether it knows where that
specific IP address is. Because my machine is a simple client
on the 206.11 network, it
is very unlikely that it has any idea where your machine is. But
it does know of a default gateway machine to which
it sends data for all unknown computers.
When a gateway receives data addressed for a specific IP, it in
turn checks to see whether it knows about the specific computer
in question. In this case, my default gateway is likely a router
for my local network. It probably knows about the existence of
machines only on the local network. It thus forwards my data onto
its default gateway, which is responsible for knowing about a
lot of networks. Although this router also does not know where
the 199.199.181.120 machine
is, it does have a specific gateway for the 199.199
network. It therefore forwards the data to that gateway. After
traveling through a series of gateways, the data eventually reaches
a machine that knows exactly where your machine can be found.
Figure 25.2 shows the flow of how a machine handles each set of
IP data (also referred to as a packet).
Figure 25.2: How a computer handles an IP packet.
On any given machine, you may have a lot of applications communicating
with other computers on the network. The packet I sent you now
has to be able to tell your machine exactly which application
it is destined for. It does this using the final piece to IP addressing:
the port number. Just as an apartment number tells the post office
which apartment in a building a specific letter should be sent
to, a port number tells a computer which application should receive
an IP packet.
Note |
With all this talk about IP numbers, you may be wondering how IP names fit into the picture. IP names are actually aliases placed on top of IP through a system called DNS (Domain Name Service). DNS provides a system for turning names such as byzantium.imaginary.com into numbers like 206.11.201.18 and then reversing the process. It is important to note that IP itself has no knowledge of machines names; such processing is handled at a higher level.
|
However, all you really need to know about IP specifically is
how to address the data you want to send. In fact, the port number
just described is not even part of the IP. IP simply describes
how to get a packet from point A to point B. Any network application
you write will instead be coded against higher level protocols
which, in turn, handle IP management. The two most common protocols
are these:
- TCP/IP (Transmission Control Protocol/Internet
Protocol)
- UDP/IP (User Datagram Protocol/Internet
Protocol)
TCP/IP, referred to in older texts as DARPA Internet Protocols,
is actually a suite of protocols that provides applications with
a reliable data communication layer. When you send a TCP/IP packet,
you know either that the packet will reach its destination or
that you will be informed of any problems with the transmission
of the data. All this is done by encapsulating packet transmission
inside a network session. When you want to communicate with another
computer using TCP/IP, you create a connection that allows you
to send multiple packets. The target application is listening
to the network on a target port. Your client application connects
to that listen port and negotiates a new private port through
which data can be transmitted for as long as the connection is
open.
The downside to TCP/IP is the overhead required to manage all
that error handling as well as the need to take up ports on both
machines to maintain a constant connection. UDP/IP, on the other
hand, is a protocol for transmitting packets across the network
without the reliability overhead incurred by TCP/IP. If you use
UDP/IP, your application sends individual packets (called datagrams)
to the target computer's listen port and hopes for the best. Sometimes
the packets get to their destination, sometimes they do not.
For a detailed discussion of socket programming, take a look at
Chapter 26, "Java Socket Programming."
Our client/server application starts with the server. Java provides
a ServerSocket class that
listens to a port and waits for clients to connect. When an application
creates a ServerSocket, it
passes a port number to the constructor. The application then
repeatedly calls the ServerSocket
accept() method. That method
blocks application processing until a client connects. Once a
client does connect, accept()
returns a Java Socket object
representing the connection to the client machine. The server
then creates a new thread for the processing of data related to
this connection. The listen thread calls accept()
to wait for the next connection. Listing 25.1 shows the basic
flow of server processing.
Listing 25.1. Basic server processing using the Java TCP/IP
classes.
import java.net.ServerSocket;
import java.net.Socket;
public class Server implements Runnable {
private Socket client;
public Server(Socket socket) {
Thread thread;
client = socket;
thread = new Thread(this);
thread.start();
}
static public void main(String args[]) {
ServerSocket listen_socket;
try {
listen_socket = new ServerSocket(10000);
}
catch( java.io.IOException e ) {
System.err.println("Failed to create listen socket.");
e.printStackTrace();
System.exit(-1);
return;
}
while( true ) {
Socket socket;
try {
socket = listen_socket.accept();
new Server(socket);
}
catch( java.io.IOException e ) {
e.printStackTrace();
}
}
}
public void run() {
// Handle all processing for a specific client here
}
}
On both the client and server ends, your application sends and
receives data through Socket
objects. The run() method
in the Server in Listing
25.1 is used to get and receive information from the client. For
each instance of the Server
class created from the static main()
method, the application has a corresponding instance of the Socket
class to communicate with a specific client.
As with other forms of I/O, socket I/O is managed with the Java
streams. Listing 25.2 shows how we can implement the run()
method of a Server class
to simply echo information the client sends back to it.
Listing 25.2. The run()
method from the Server
class.
public void run() {
java.io.DataInputStream input;
java.io.PrintStream output;
String data;
try {
input = new java.io.DataInputStream(client.getInputStream());
output = new java.io.PrintStream(client.getOutputStream());
}
catch( java.io.IOException e ) {
e.printStackTrace();
return;
}
while( true ) {
try {
data = input.readLine();
output.println("Received (" + data.length() + "): " + data);
}
catch( java.io.IOException e ) {
break;
}
}
}
You can test this program using the telnet client to connect to
port 10000 on the machine running the Server
application. To do this, type telnet
localhost 10000 at your UNIX or DOS command line.
When using the telnet application for testing, however, you should
keep in mind that telnet is a protocol on top of TCP/IP. It actually
sends more characters than you type as part of its protocol. Because
the Server application does
not know the telnet protocol, it ends up sending back to you what
you typed plus all the telnet protocol information.
Of course, it may be more useful to try out the Server
application using an application that shows how a client uses
Java sockets. Listing 25.3 shows such a client application.
Listing 25.3. A Java client application.
import java.net.Socket;
public class Client {
static public void main(String args[]) {
Socket socket;
try {
java.io.DataInputStream input;
java.io.PrintStream output;
socket = new Socket("localhost", 10000);
while( true ) {
try {
String tmp;
java.io.DataInputStream user_input =
new java.io.DataInputStream(System.in);
input = new java.io.DataInputStream(socket.getInputStream());
output = new java.io.PrintStream(socket.getOutputStream());
tmp = user_input.readLine();
output.println(tmp);
tmp = input.readLine();
System.out.println(tmp);
}
catch( java.io.IOException e ) {
e.printStackTrace();
return;
}
}
}
catch( java.io.IOException e ) {
e.printStackTrace();
}
}
}
Because of the way Java sockets are engineered, client and server
operations are nearly identical. The major differences are that
the server communicates with multiple clients but the client communicates
with only a single server, and that the server has to create a
listen port for initial connections.
What we have developed in this chapter so far simply takes raw
data from one end and sends it right back. In a real application,
we probably want something more like a dialog to occur between
the client and server applications-we want to interpret the data.
Once you have the basic blocks for communicating between the client
and server, you need to build your own customer protocol on top
of that communication layer to give that data meaning.
You may wonder at first why you would use an unreliable communications
protocol like UDP. After all, if you are sending data, can't you
assume that you want it to get to its destination? Not necessarily.
Sometimes, an application sends information, and the arrival of
individual packets is unimportant. For example, a server repeatedly
broadcasting sports scores 24 hours a day does not really care
whether a given score arrives at its destination. It does care,
however, about the overhead any error correction might introduce.
Such an application is a perfect situation for UDP/IP.
Listing 25.4 shows a DatagramServer
class that performs two main tasks:
- Listens for incoming datagram packets from clients that request
scores to be sent to them.
- Sends out scores to all clients who have shown interest.
Listing 25.4. A simple datagram server.
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class DatagramServer implements Runnable {
private java.util.Hashtable listeners = new java.util.Hashtable();
public DatagramServer() {
Thread thread = new Thread(this);
thread.start();
}
static public void main(String args[]) {
DatagramSocket socket;
DatagramPacket packet;
DatagramServer server;
byte[] buffer = new byte[255];
server = new DatagramServer();
packet = new DatagramPacket(buffer, buffer.length);
try {
socket = new DatagramSocket(10000);
}
catch( java.net.SocketException e ) {
e.printStackTrace();
return;
}
while( true ) {
String tmp;
try {
socket.receive(packet);
tmp = new String(buffer, 0, 0, packet.getLength());
if( tmp.equals("close") ) {
server.removeListener(packet.getAddress());
}
else {
server.addListener(packet.getAddress());
}
}
catch( java.io.IOException e ) {
e.printStackTrace();
}
}
}
public void run() {
while(true) {
synchronized(listeners) {
java.util.Enumeration addresses = listeners.keys();
// Send a score to the current list
while( addresses.hasMoreElements() ) {
InetAddress addr = (InetAddress)addresses.nextElement();
DatagramPacket packet;
DatagramSocket socket;
String str = "a score";
byte[] msg = new byte[str.length()];
try {
socket = new DatagramSocket();
}
catch( java.net.SocketException e ) {
e.printStackTrace();
break;
}
str.getBytes(0, msg.length, msg, 0);
try {
packet = new DatagramPacket(msg, msg.length, addr, 11000);
socket.send(packet);
}
catch( java.io.IOException e ) {
e.printStackTrace();
}
}
}
// unsynchronize the listeners and allow any new additions
try {
Thread.sleep(500);
}
catch( InterruptedException e ) {
}
// remove old listeners
synchronized(listeners) {
java.util.Enumeration addresses = listeners.keys();
while(addresses.hasMoreElements()) {
InetAddress addr = (InetAddress)addresses.nextElement();
int count = ((Integer)listeners.get(addr)).intValue();
if( count > 1200 ) {
listeners.remove(addr);
}
else {
listeners.put(addr, new Integer(count + 1));
}
}
}
}
}
public void addListener(InetAddress ip) {
listeners.put(ip, new Integer(0));
}
public void removeListener(InetAddress ip) {
listeners.remove(ip);
}
}
For simplicity's sake, this example simply sends the string a
score out repeatedly. For a real application, you
would, of course, want to provide the server with some way of
retrieving real scores instead. Listing 25.5 is equally simple.
It connects to the server and displays the scores as they come
across.
Listing 25.5. A corresponding datagram client for displaying
scores from the server.
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
public class DatagramClient {
static public void main(String args[]) {
DatagramSocket socket;
DatagramPacket packet;
byte[] buffer = new byte[255];
int count = 0;
packet = new DatagramPacket(buffer, buffer.length);
try {
socket = new DatagramSocket(11000);
}
catch( java.net.SocketException e ) {
e.printStackTrace();
return;
}
try {
DatagramPacket p;
DatagramSocket s;
String str = "keep alive";
byte[] msg = new byte[str.length()];
count = 0;
try {
s = new DatagramSocket();
str.getBytes(0, msg.length, msg, 0);
p = new DatagramPacket(msg, msg.length,
InetAddress.getByName("localhost"), 10000);
s.send(packet);
}
catch( java.net.SocketException e ) {
e.printStackTrace();
}
}
catch( java.io.IOException e ) {
e.printStackTrace();
}
while( true ) {
String tmp;
count++;
try {
socket.receive(packet);
tmp = new String(buffer, 0, 0, packet.getLength());
System.out.println(tmp);
}
catch( java.io.IOException e ) {
e.printStackTrace();
}
if( count > 5000 ) {
DatagramPacket p;
DatagramSocket s;
String str = "keep alive";
byte[] msg = new byte[str.length()];
count = 0;
try {
s = new DatagramSocket();
}
catch( java.net.SocketException e ) {
e.printStackTrace();
break;
}
str.getBytes(0, msg.length, msg, 0);
try {
p = new DatagramPacket(msg, msg.length,
InetAddress.getByName("localhost"), 10000);
s.send(packet);
}
catch( java.io.IOException e ) {
e.printStackTrace();
}
}
}
}
}
UDP/IP sockets require a lot more base manipulation than do TCP/IP
sockets because you have to make a new connection for every single
packet you send. The payoff is enhanced performance for communication,
which does not depend on any one socket actually arriving at its
destination.
Now that you understand how to make computers talk to one another
on the Internet with Java, it helps to understand how to design
any client/server application you might build. As discussed earlier,
the client/server architecture assigns processing responsibility
where it logically belongs. A simple system can be broken into
two layers: a server where data and common processing occurs and
a client where user-specific processing occurs. This kind of architecture
is more commonly known as a two-tier architecture. For
the types of applications we have discussed, a simple two-tier
breakdown works well.
Business applications-and, increasingly, Internet applications-are
generally much more complex than the applications discussed in
this chapter. These kinds of applications can involve relational
databases and complex server-side processing. Client machines
are becoming increasingly powerful. At the same time, the benefit
of client/server development has enabled applications to move
processing off the server and onto the client to facilitate the
use of cheaper servers. This trend has led to what is known as
the problem of the fat client.
A fat client in a client/server system is a client that
has absorbed an inordinate amount of the system's processing needs.
Although a fat client architecture is as capable as any other
client/server configuration, it is harder to scale as your application
grows over time. Using a common client/server tool such as PowerBuilder,
your client application has direct knowledge of exactly how your
data is stored and what it looks like in the data store (usually
a database). If you ever change where that data is stored or how
it is stored, you have to do significant rework of your client
application.
The solution to the problem of the fat client is a three-tier
client/server architecture that creates another layer of processing
across the network. In Figure 25.3, you can see how the three-tier
design divides application work into the following three tasks:
Figure 25.3: The three-tier client/server architecture.
- User interface
- Data processing or business rules
- Data storage
One of the primary advantages of a three-tier architecture is
that, as your data storage needs grow, you can change the way
data is stored without affecting your clients. The middle layer
of the system, commonly referred to as the application server,
can thus concentrate on centralizing business rule processing.
(Business rule processing is the processing of data going to and
from clients in a way that is common to all clients.)
This chapter has outlined the nuts and bolts of how communication
between machines works in a client/server environment and how
you might construct client/server applications to best perform
the tasks you need them to perform. Much of the code you have
seen in this chapter, unfortunately, really has little to do with
the central job your application is doing. It is simply about
making two or more computers talk to each other.
New technologies are on the horizon to help deliver you from the
tedium of socket programming in a client/server environment. The
most exciting of these technologies is distributed objects. A
distributed application is a single application that has
individual objects located on many machines. In an ideal world,
these objects communicate with one another through simple method
calls. Unfortunately, the ideal world is not here yet.
With the 1.1 release, Java provides a new API designed to allow
you to distribute your Java applications. This new API, called
Remote Method Invocation (RMI), enables a program on one machine
to communicate with a program on another machine using simple
Java method calls. Instead of writing a complex socket interface
and application-specific communication protocol, your application
acts as if all the separate pieces were part of a single program
on one machine. You call methods in any object, no matter where
they exist, just as you do any other Java method.
A discussion of RMI is beyond the scope of this chapter. Nevertheless,
as a seamless method-based communication API, RMI does provide
an attractive alternative to writing socket code. Unfortunately,
RMI works only when all the pieces of your application are Java
pieces. In a hybrid system, sockets provide the best method of
enabling communication among networked machines.
The client/server architecture is a very powerful design used
by almost every application you use on the Internet. At its core,
it is simply about making an application on one computer talk
with an application on another computer. On the Internet, this
is always done using the keystone of the Internet: IP.
In most of the applications you build today, you use Java's socket
objects to do TCP/IP and UDP/IP communication. Understanding how
these protocols work is not only critical to writing client/server
applications that use the Java socket code, but it is also key
to understanding the problems that can occur using any protocols
built on top of them-such as RMI.
Contact
reference@developer.com with questions or comments.
Copyright 1998
EarthWeb Inc., All rights reserved.
PLEASE READ THE ACCEPTABLE USAGE STATEMENT.
Copyright 1998 Macmillan Computer Publishing. All rights reserved.