Distributed Computing
Distributed Computing
Historically, programming across multiple
machines has been error-prone, difficult, and complex.
The programmer had to know many details about the
network and sometimes even the hardware. You usually needed to understand the
various “layers” of the networking protocol, and there were a lot of
different functions in each different networking library concerned with
connecting, packing, and unpacking blocks of information; shipping those blocks
back and forth; and handshaking. It was a daunting task.
However, the basic idea of distributed computing is
not so difficult, and is abstracted very nicely in the Java libraries. You want
to:
- Get some information from that machine over there and
move it to this machine here, or vice versa. This is accomplished with basic
network programming.
- Connect to a database, which may live across a network.
This is accomplished with Java DataBase Connectivity (JDBC), which is an
abstraction away from the messy, platform-specific details of SQL (the
structured query language used for most database transactions).
- Provide services via a Web server. This is accomplished
with Java’s servlets and Java
Server Pages
(JSPs).
- Execute methods on Java objects that live on remote
machines transparently, as if those objects were resident on local machines.
This is accomplished with Java’s Remote Method Invocation (RMI).
- Use code written in other languages, running on other
architectures. This is accomplished using the Common Object Request Broker
Architecture (CORBA), which is directly supported by Java.
- Isolate business logic from connectivity issues,
especially connections with databases including transaction management and
security. This is accomplished using Enterprise JavaBeans (EJBs). EJBs
are not actually a distributed architecture, but the resulting applications are
usually used in a networked client-server system.
- Easily, dynamically, add and remove devices from a
network representing a local system. This is accomplished with Java’s
Jini.
Each topic will be given a
light introduction in this chapter. Please note that each subject is voluminous
and by itself the subject of entire books, so this chapter is only meant to
familiarize you with the topics, not make you an expert (however, you can go a
long way with the information presented here on network programming, servlets
and JSPs)
Network programming
One of Java’s great strengths is painless
networking. The Java network library designers have made it quite similar to
reading and writing files, except that the “file” exists on a remote
machine and the remote machine can decide exactly what it wants to do about the
information you’re requesting or sending. As much as possible, the
underlying details of networking have been abstracted away and taken care of
within the JVM and local machine installation of Java. The programming model you
use is that of a file; in fact, you actually wrap the network connection (a
“socket”) with stream objects, so you end up using the same method
calls as you do with all other streams. In addition, Java’s built-in
multithreading is exceptionally handy when dealing with another networking
issue: handling multiple connections at once.
This section introduces Java’s networking
support using easy-to-understand examples.
Identifying a machine
Of course, in order to tell one machine from another
and to make sure that you are connected with a particular machine, there must be
some way of uniquely identifying machines on a network. Early networks were
satisfied to provide unique names for machines within the local network.
However, Java works within the Internet, which requires a way to uniquely
identify a machine from all the others in the world. This is accomplished
with the IP (Internet Protocol) address which can exist in two forms“:
- The familiar DNS (Domain Name System) form. My
domain name is bruceeckel.com, and if I have a computer called
Opus in my domain, its domain name would be Opus.bruceeckel.com.
This is exactly the kind of name that you use when you send email to people, and
is often incorporated into a World Wide Web address.
- Alternatively, you can use the dotted quad” form,
which is four numbers separated by dots, such as
123.255.28.120.
In both cases,
the IP address is represented internally as a 32-bit number (so each of the quad
numbers cannot exceed 255), and you can get a special Java object to represent
this number from either of the forms above by using the static
InetAddress.getByName( ) method that’s in java.net. The
result is an object of type InetAddress that you can use to build a
“socket,” as you will see later.
As a simple example of using
InetAddress.getByName( ), consider what happens if you have a
dial-up Internet service provider (ISP). Each time you dial up, you are assigned
a temporary IP address. But while you’re connected, your IP address has
the same validity as any other IP address on the Internet. If someone connects
to your machine using your IP address then they can connect to a Web server or
FTP server that you have running on your machine. Of course, they need to know
your IP address, and since a new one is assigned each time you dial up, how can
you find out what it is?
The following program uses
InetAddress.getByName( ) to produce your IP address. To use it, you
must know the name of your computer. On Windows 95/98, go to
“Settings,” “Control Panel,” “Network,” and
then select the “Identification” tab. “Computer name” is
the name to put on the command line.
//:
c15:WhoAmI.java
//
Finds out your network address when
//
you're connected to the Internet.
import
java.net.*;
public
class
WhoAmI {
public
static
void
main(String[] args)
throws
Exception {
if(args.length
!= 1) {
System.err.println(
"Usage: WhoAmI
MachineName");
System.exit(1);
}
InetAddress a =
InetAddress.getByName(args[0]);
System.out.println(a);
}
}
///:~
In this case, the machine is called
“peppy.” So, once I’ve connected to my ISP I run the
program:
java
WhoAmI peppy
I get back a message like this (of course, the address
is different each time):
peppy/199.190.87.75
If I tell my friend this address and I have a Web
server running on my computer, he can connect to it by going to the URL
http://199.190.87.75 (only as long as I continue to stay connected during
that session). This can sometimes be a handy way to distribute information to
someone else, or to test out a Web site configuration before posting it to a
“real” server.
Servers and clients
The whole point of a network is to allow two machines
to connect and talk to each other. Once the two machines have found each other
they can have a nice, two-way conversation. But how do they find each other?
It’s like getting lost in an amusement park: one machine has to stay in
one place and listen while the other machine says, “Hey, where are
you?”
The machine that “stays in one place” is
called the server, and the one that seeks is called the client.
This distinction is important only while the client is trying to connect to the
server. Once they’ve connected, it becomes a two-way communication process
and it doesn’t matter anymore that one happened to take the role of server
and the other happened to take the role of the client.
So the job of the server is to listen for a
connection, and that’s performed by the special server object that you
create. The job of the client is to try to make a connection to a server, and
this is performed by the special client object you create. Once the connection
is made, you’ll see that at both server and client ends, the connection is
magically turned into an I/O stream object, and from then on you can treat the
connection as if you were reading from and writing to a file. Thus, after the
connection is made you will just use the familiar I/O commands from Chapter 11.
This is one of the nice features of Java networking.
Testing programs without a network
For many reasons, you might not have a client machine,
a server machine, and a network available to test your programs. You might be
performing exercises in a classroom situation, or you could be writing programs
that aren’t yet stable enough to put onto the network. The creators of the
Internet Protocol were aware of this issue, and they created a special address
called localhost to be the “local loopback” IP address for
testing without a network. The generic way to produce this address in Java
is:
InetAddress
addr =
InetAddress.getByName(null);
If you hand getByName( ) a null, it
defaults to using the localhost. The InetAddress is what you use
to refer to the particular machine, and you must produce this before you can go
any further. You can’t manipulate the contents of an InetAddress
(but you can print them out, as you’ll see in the next example). The
only way you can create an InetAddress is through one of that
class’s overloaded static member methods getByName( )
(which is what you’ll usually use), getAllByName( ), or
getLocalHost( ).
You can also produce the local loopback address by
handing it the string localhost:
InetAddress.getByName("localhost");
(assuming “localhost” is configured in
your machine’s “hosts” table), or by using its dotted quad
form to name the reserved IP number for the loopback:
InetAddress.getByName("127.0.0.1");
All three forms produce the same result.
Port: a unique place
within the machine
An IP address isn’t enough to identify a unique
server, since many servers can exist on one machine. Each IP machine also
contains ports, and when you’re setting up a client or a server you
must choose a port where both client and server agree to connect; if
you’re meeting someone, the IP address is the neighborhood and the port is
the bar.
The port is not a physical location in a machine, but
a software abstraction (mainly for bookkeeping purposes). The client program
knows how to connect to the machine via its IP address, but how does it connect
to a desired service (potentially one of many on that machine)? That’s
where the port numbers come in as a second level of addressing. The idea is that
if you ask for a particular port, you’re requesting the service
that’s associated with the port number. The time of day is a simple
example of a service. Typically, each service is associated with a unique port
number on a given server machine. It’s up to the client to know ahead of
time which port number the desired service is running on.
The system services reserve the use of ports 1 through
1024, so you shouldn’t use those or any other port that you know to be in
use. The first choice for examples in this book will be port 8080 (in memory of
the venerable old 8-bit Intel 8080 chip in my first computer, a CP/M machine).
Sockets
The socket is the software abstraction used to
represent the “terminals” of a connection between two machines. For
a given connection, there’s a socket on each machine, and you can imagine
a hypothetical “cable” running between the two machines with each
end of the “cable” plugged into a socket. Of course, the physical
hardware and cabling between machines is completely unknown. The whole point of
the abstraction is that we don’t have to know more than is necessary.
In Java, you create a socket to make the connection to
the other machine, then you get an InputStream and OutputStream
(or, with the appropriate converters, Reader and Writer)
from the socket in order to be able to treat the connection as an I/O stream
object. There are two stream-based socket classes: a ServerSocket that a
server uses to “listen” for incoming connections and a Socket
that a client uses in order to initiate a connection. Once a client makes a
socket connection, the ServerSocket returns (via the
accept( ) method) a corresponding Socket through which
communications will take place on the server side. From then on, you have a true
Socket to Socket connection and you treat both ends the same way
because they are the same. At this point, you use the methods
getInputStream( ) and getOutputStream( ) to produce the
corresponding InputStream and OutputStream objects from each
Socket. These must be wrapped inside buffers and formatting classes just
like any other stream object described in Chapter 11.
The use of the term ServerSocket would seem to
be another example of a confusing naming scheme in the Java libraries. You might
think ServerSocket would be better named “ServerConnector” or
something without the word “Socket” in it. You might also think that
ServerSocket and Socket should both be inherited from some common
base class. Indeed, the two classes do have several methods in common, but not
enough to give them a common base class. Instead, ServerSocket’s
job is to wait until some other machine connects to it, then to return an actual
Socket. This is why ServerSocket seems to be a bit misnamed, since
its job isn’t really to be a socket but instead to make a Socket
object when someone else connects to it.
However, the ServerSocket does create a
physical “server” or listening socket on the host machine. This
socket listens for incoming connections and then returns an
“established” socket (with the local and remote endpoints defined)
via the accept( ) method. The confusing part is that both of these
sockets (listening and established) are associated with the same server socket.
The listening socket can accept only new connection requests and not data
packets. So while ServerSocket doesn’t make much sense
programmatically, it does “physically.”
When you create a ServerSocket, you give it
only a port number. You don’t have to give it an IP address because
it’s already on the machine it represents. When you create a
Socket, however, you must give both the IP address and the port number
where you’re trying to connect. (However, the Socket that comes
back from ServerSocket.accept( ) already contains all this
information.)
A simple server and client
This example makes the simplest use of servers and
clients using sockets. All the server does is wait for a connection, then uses
the Socket produced by that connection to create an InputStream
and OutputStream. These are converted to a Reader and a
Writer, then wrapped in a BufferedReader and a
PrintWriter. After that, everything it reads from the
BufferedReader it echoes to the PrintWriter until it receives the
line “END,” at which time it closes the connection.
The client makes the connection to the server, then
creates an OutputStream and performs the same wrapping as in the server.
Lines of text are sent through the resulting PrintWriter. The client also
creates an InputStream (again, with appropriate conversions and wrapping)
to hear what the server is saying (which, in this case, is just the words echoed
back).
Both the server and client use the same port number
and the client uses the local loopback address to connect to the server on the
same machine so you don’t have to test it over a network. (For some
configurations, you might need to be connected to a network for the
programs to work, even if you aren’t communicating over that
network.)
Here is the server:
//:
c15:JabberServer.java
//
Very simple server that just
//
echoes whatever the client sends.
import
java.io.*;
import
java.net.*;
public
class
JabberServer {
// Choose a port outside of the
range 1-1024:
public
static
final
int
PORT = 8080;
public
static
void
main(String[] args)
throws
IOException {
ServerSocket s =
new
ServerSocket(PORT);
System.out.println("Started:
" + s);
try
{
// Blocks until a connection
occurs:
Socket socket = s.accept();
try
{
System.out.println(
"Connection accepted:
"+ socket);
BufferedReader in =
new
BufferedReader(
new
InputStreamReader(
socket.getInputStream()));
// Output is automatically
flushed
// by
PrintWriter:
PrintWriter out =
new
PrintWriter(
new
BufferedWriter(
new
OutputStreamWriter(
socket.getOutputStream())),true);
while
(true)
{
String str = in.readLine();
if
(str.equals("END"))
break;
System.out.println("Echoing:
" + str);
out.println(str);
}
// Always close the two
sockets...
}
finally
{
System.out.println("closing...");
socket.close();
}
}
finally
{
s.close();
}
}
}
///:~
You can see that the ServerSocket just needs a
port number, not an IP address (since it’s running on this
machine!). When you call accept( ), the method blocks until
some client tries to connect to it. That is, it’s there waiting for a
connection, but other processes can run (see Chapter 14). When a connection is
made, accept( ) returns with a Socket object representing
that connection.
The responsibility for cleaning up the sockets is
crafted carefully here. If the ServerSocket constructor fails, the
program just quits (notice we must assume that the constructor for
ServerSocket doesn’t leave any open network sockets lying around if
it fails). For this case, main( ) throws IOException
so a try block is not necessary. If the ServerSocket constructor
is successful then all other method calls must be guarded in a
try-finally block to ensure that, no matter how the block is left, the
ServerSocket is properly closed.
The same logic is used for the Socket returned
by accept( ). If accept( ) fails, then we must assume
that the Socket doesn’t exist or hold any resources, so it
doesn’t need to be cleaned up. If it’s successful, however, the
following statements must be in a try-finally block so that if they fail
the Socket will still be cleaned up. Care is required here because
sockets use important nonmemory resources, so you must be diligent in order to
clean them up (since there is no destructor in Java to do it for
you).
Both the ServerSocket and the Socket
produced by accept( ) are printed to System.out. This means
that their toString( ) methods are automatically called. These
produce:
ServerSocket[addr=0.0.0.0,PORT=0,localport=8080]
Socket[addr=127.0.0.1,PORT=1077,localport=8080]
Shortly, you’ll see how these fit together with
what the client is doing.
The next part of the program looks just like opening
files for reading and writing except that the InputStream and
OutputStream are created from the Socket object. Both the
InputStream and OutputStream objects are converted to Reader
and Writer objects using the “converter” classes
InputStreamReader and OutputStreamWriter, respectively. You could
also have used the Java 1.0 InputStream and OutputStream classes
directly, but with output there’s a distinct advantage to using the
Writer approach. This appears with PrintWriter, which has an
overloaded constructor that takes a second argument, a boolean flag that
indicates whether to automatically flush the output at the end of each
println( ) (but not print( )) statement. Every
time you write to out, its buffer must be flushed so the information goes
out over the network. Flushing is important for this particular example because
the client and server each wait for a line from the other party before
proceeding. If flushing doesn’t occur, the information will not be put
onto the network until the buffer is full, which causes lots of problems in this
example.
When writing network programs you need to be careful
about using automatic flushing. Every time you flush the buffer a packet must be
created and sent. In this case, that’s exactly what we want, since if the
packet containing the line isn’t sent then the handshaking back and forth
between server and client will stop. Put another way, the end of a line is the
end of a message. But in many cases, messages aren’t delimited by lines so
it’s much more efficient to not use auto flushing and instead let the
built-in buffering decide when to build and send a packet. This way, larger
packets can be sent and the process will be faster.
Note that, like virtually all streams you open, these
are buffered. There’s an exercise at the end of this chapter to show you
what happens if you don’t buffer the streams (things get slow).
The infinite while loop reads lines from the
BufferedReader in and writes information to System.out and to the
PrintWriter out. Note that in and out could be any
streams, they just happen to be connected to the network.
When the client sends the line consisting of
“END,” the program breaks out of the loop and closes the
Socket.
Here’s the client:
//:
c15:JabberClient.java
//
Very simple client that just sends
//
lines to the server and reads lines
//
that the server sends.
import
java.net.*;
import
java.io.*;
public
class
JabberClient {
public
static
void
main(String[] args)
throws
IOException {
// Passing null to getByName()
produces the
// special "Local Loopback" IP
address, for
// testing on one machine w/o a
network:
InetAddress addr =
InetAddress.getByName(null);
// Alternatively, you can use
// the address or
name:
// InetAddress addr =
//
InetAddress.getByName("127.0.0.1");
// InetAddress addr =
//
InetAddress.getByName("localhost");
System.out.println("addr =
" + addr);
Socket socket =
new
Socket(addr, JabberServer.PORT);
// Guard everything in a
try-finally to make
// sure that the socket is
closed:
try
{
System.out.println("socket =
" + socket);
BufferedReader in =
new
BufferedReader(
new
InputStreamReader(
socket.getInputStream()));
// Output is automatically
flushed
// by
PrintWriter:
PrintWriter out =
new
PrintWriter(
new
BufferedWriter(
new
OutputStreamWriter(
socket.getOutputStream())),true);
for(int
i = 0; i < 10; i ++) {
out.println("howdy
" + i);
String str = in.readLine();
System.out.println(str);
}
out.println("END");
}
finally
{
System.out.println("closing...");
socket.close();
}
}
}
///:~
In main( ) you can see all three ways to
produce the InetAddress of the local loopback IP address: using
null, localhost, or the explicit reserved address
127.0.0.1. Of course, if you want to connect to a machine across a
network you substitute that machine’s IP address. When the InetAddress
addr is printed (via the automatic call to its toString( )
method) the result is:
localhost/127.0.0.1
By handing getByName( ) a null, it
defaulted to finding the localhost, and that produced the special address
127.0.0.1.
Note that the Socket called socket is
created with both the InetAddress and the port number. To understand what
it means when you print one of these Socket objects, remember that an
Internet connection is determined uniquely by these four pieces of data:
clientHost, clientPortNumber, serverHost, and
serverPortNumber. When the server comes up, it takes up its assigned port
(8080) on the localhost (127.0.0.1). When the client comes up, it is allocated
to the next available port on its machine, 1077 in this case, which also happens
to be on the same machine (127.0.0.1) as the server. Now, in order for data to
move between the client and server, each side has to know where to send it.
Therefore, during the process of connecting to the “known” server,
the client sends a “return address” so the server knows where to
send its data. This is what you see in the example output for the server
side:
Socket[addr=127.0.0.1,port=1077,localport=8080]
This means that the server just accepted a connection
from 127.0.0.1 on port 1077 while listening on its local port (8080). On the
client side:
Socket[addr=localhost/127.0.0.1,PORT=8080,localport=1077]
which means that the client made a connection to
127.0.0.1 on port 8080 using the local port 1077.
You’ll notice that every time you start up the
client anew, the local port number is incremented. It starts at 1025 (one past
the reserved block of ports) and keeps going up until you reboot the machine, at
which point it starts at 1025 again. (On UNIX machines, once the upper limit of
the socket range is reached, the numbers will wrap around to the lowest
available number again.)
Once the Socket object has been created, the
process of turning it into a BufferedReader and PrintWriter is the
same as in the server (again, in both cases you start with a Socket).
Here, the client initiates the conversation by sending the string
“howdy” followed by a number. Note that the buffer must again be
flushed (which happens automatically via the second argument to the
PrintWriter constructor). If the buffer isn’t flushed, the whole
conversation will hang because the initial “howdy” will never get
sent (the buffer isn’t full enough to cause the send to happen
automatically). Each line that is sent back from the server is written to
System.out to verify that everything is working correctly. To terminate
the conversation, the agreed-upon “END” is sent. If the client
simply hangs up, then the server throws an exception.
You can see that the same care is taken here to ensure
that the network resources represented by the Socket are properly cleaned
up, using a try-finally block.
Sockets produce a “dedicated” connection
that persists until it is explicitly disconnected. (The dedicated connection can
still be disconnected unexplicitly if one side, or an intermediary link, of the
connection crashes.) This means the two parties are locked in communication and
the connection is constantly open. This seems like a logical approach to
networking, but it puts an extra load on the network. Later in this chapter
you’ll see a different approach to networking, in which the connections
are only temporary.
Serving multiple clients
The JabberServer works, but it can handle only
one client at a time. In a typical server, you’ll want to be able to deal
with many clients at once. The answer is multithreading, and in languages that
don’t directly support multithreading this means all sorts of
complications. In Chapter 14 you saw that multithreading in Java is about as
simple as possible, considering that multithreading is a rather complex topic.
Because threading in Java is reasonably straightforward, making a server that
handles multiple clients is relatively easy.
The basic scheme is to make a single
ServerSocket in the server and call accept( ) to wait for a
new connection. When accept( ) returns, you take the resulting
Socket and use it to create a new thread whose job is to serve that
particular client. Then you call accept( ) again to wait for a new
client.
In the following server code, you can see that it
looks similar to the JabberServer.java example except that all of the
operations to serve a particular client have been moved inside a separate thread
class:
//:
c15:MultiJabberServer.java
//
A server that uses multithreading
//
to handle any number of clients.
import
java.io.*;
import
java.net.*;
class
ServeOneJabber
extends
Thread {
private
Socket socket;
private
BufferedReader in;
private
PrintWriter out;
public
ServeOneJabber(Socket s)
throws
IOException {
socket = s;
in =
new
BufferedReader(
new
InputStreamReader(
socket.getInputStream()));
// Enable
auto-flush:
out =
new
PrintWriter(
new
BufferedWriter(
new
OutputStreamWriter(
socket.getOutputStream())),
true);
// If any of the above calls throw
an
// exception, the caller is
responsible for
// closing the socket. Otherwise
the thread
// will close
it.
start(); // Calls
run()
}
public
void
run() {
try
{
while
(true)
{
String str = in.readLine();
if
(str.equals("END"))
break;
System.out.println("Echoing:
" + str);
out.println(str);
}
System.out.println("closing...");
}
catch(IOException
e) {
System.err.println("IO
Exception");
}
finally
{
try
{
socket.close();
}
catch(IOException
e) {
System.err.println("Socket not
closed");
}
}
}
}
public
class
MultiJabberServer {
static
final
int
PORT = 8080;
public
static
void
main(String[] args)
throws
IOException {
ServerSocket s =
new
ServerSocket(PORT);
System.out.println("Server
Started");
try
{
while(true)
{
// Blocks until a connection
occurs:
Socket socket = s.accept();
try
{
new
ServeOneJabber(socket);
}
catch(IOException
e) {
// If it fails, close the
socket,
// otherwise the thread will close
it:
socket.close();
}
}
}
finally
{
s.close();
}
}
}
///:~
The ServeOneJabber thread takes the
Socket object that’s produced by accept( ) in
main( ) every time a new client makes a connection. Then, as before,
it creates a BufferedReader and auto-flushed PrintWriter object
using the Socket. Finally, it calls the special Thread method
start( ), which performs thread initialization and then calls
run( ). This performs the same kind of action as in the previous
example: reading something from the socket and then echoing it back until it
reads the special “END” signal.
The responsibility for cleaning up the socket must
again be carefully designed. In this case, the socket is created outside of the
ServeOneJabber so the responsibility can be shared. If the
ServeOneJabber constructor fails, it will just throw the exception to the
caller, who will then clean up the thread. But if the constructor succeeds, then
the ServeOneJabber object takes over responsibility for cleaning up the
thread, in its run( ).
Notice the simplicity of the MultiJabberServer.
As before, a ServerSocket is created and accept( ) is called
to allow a new connection. But this time, the return value of
accept( ) (a Socket) is passed to the constructor for
ServeOneJabber, which creates a new thread to handle that connection.
When the connection is terminated, the thread simply goes away.
If the creation of the ServerSocket fails, the
exception is again thrown through main( ). But if the creation
succeeds, the outer try-finally guarantees its cleanup. The inner
try-catch guards only against the failure of the ServeOneJabber
constructor; if the constructor succeeds, then the ServeOneJabber thread
will close the associated socket.
To test that the server really does handle multiple
clients, the following program creates many clients (using threads) that connect
to the same server. The maximum number of threads allowed is determined by the
final int MAX_THREADS.
//:
c15:MultiJabberClient.java
//
Client that tests the MultiJabberServer
//
by starting up multiple clients.
import
java.net.*;
import
java.io.*;
class
JabberClientThread
extends
Thread {
private
Socket socket;
private
BufferedReader in;
private
PrintWriter out;
private
static
int
counter = 0;
private
int
id = counter++;
private
static
int
threadcount = 0;
public
static
int
threadCount() {
return
threadcount;
}
public
JabberClientThread(InetAddress addr) {
System.out.println("Making client
" + id);
threadcount++;
try
{
socket =
new
Socket(addr, MultiJabberServer.PORT);
}
catch(IOException
e) {
System.err.println("Socket
failed");
// If the creation of the socket
fails,
// nothing needs to be cleaned
up.
}
try
{
in =
new
BufferedReader(
new
InputStreamReader(
socket.getInputStream()));
// Enable
auto-flush:
out =
new
PrintWriter(
new
BufferedWriter(
new
OutputStreamWriter(
socket.getOutputStream())),
true);
start();
}
catch(IOException
e) {
// The socket should be closed on
any
// failures other than the socket
//
constructor:
try
{
socket.close();
}
catch(IOException
e2) {
System.err.println("Socket not
closed");
}
}
// Otherwise the socket will be
closed by
// the run() method of the
thread.
}
public
void
run() {
try
{
for(int
i = 0; i < 25; i++) {
out.println("Client
" + id +
":
" + i);
String str = in.readLine();
System.out.println(str);
}
out.println("END");
}
catch(IOException
e) {
System.err.println("IO
Exception");
}
finally
{
// Always close
it:
try
{
socket.close();
}
catch(IOException
e) {
System.err.println("Socket not
closed");
}
threadcount--; // Ending this
thread
}
}
}
public
class
MultiJabberClient {
static
final
int
MAX_THREADS = 40;
public
static
void
main(String[] args)
throws
IOException, InterruptedException {
InetAddress addr =
InetAddress.getByName(null);
while(true)
{
if(JabberClientThread.threadCount()
< MAX_THREADS)
new
JabberClientThread(addr);
Thread.currentThread().sleep(100);
}
}
}
///:~
The JabberClientThread constructor takes an
InetAddress and uses it to open a Socket. You’re probably
starting to see the pattern: the Socket is always used to create some
kind of Reader and/or Writer (or InputStream and/or
OutputStream) object, which is the only way that the Socket can be
used. (You can, of course, write a class or two to automate this process instead
of doing all the typing if it becomes painful.) Again, start( )
performs thread initialization and calls run( ). Here, messages are
sent to the server and information from the server is echoed to the screen.
However, the thread has a limited lifetime and eventually completes. Note that
the socket is cleaned up if the constructor fails after the socket is created
but before the constructor completes. Otherwise the responsibility for calling
close( ) for the socket is relegated to the run( )
method.
The threadcount keeps track of how many
JabberClientThread objects currently exist. It is incremented as part of
the constructor and decremented as run( ) exits (which means the
thread is terminating). In MultiJabberClient.main( ), you can see
that the number of threads is tested, and if there are too many, no more are
created. Then the method sleeps. This way, some threads will eventually
terminate and more can be created. You can experiment with MAX_THREADS to
see where your particular system begins to have trouble with too many
connections.
Datagrams
The examples you’ve seen so far use the
Transmission Control Protocol (TCP, also known as stream-based
sockets), which is designed for ultimate reliability and guarantees that the
data will get there. It allows retransmission of lost data, it provides multiple
paths through different routers in case one goes down, and bytes are delivered
in the order they are sent. All this control and reliability comes at a cost:
TCP has a high overhead.
There’s a second protocol, called User
Datagram Protocol (UDP), which doesn’t guarantee that the packets will
be delivered and doesn’t guarantee that they will arrive in the order they
were sent. It’s called an “unreliable protocol” (TCP is a
“reliable protocol”), which sounds bad, but because it’s much
faster it can be useful. There are some applications, such as an audio signal,
in which it isn’t so critical if a few packets are dropped here or there
but speed is vital. Or consider a time-of-day server, where it really
doesn’t matter if one of the messages is lost. Also, some applications
might be able to fire off a UDP message to a server and can then assume, if
there is no response in a reasonable period of time, that the message was lost.
Typically, you’ll do most of your direct network
programming with TCP, and only occasionally will you use UDP. There’s a
more complete treatment of UDP, including an example, in the first edition of
this book (available on the CD ROM bound into this book, or as a free download
from www.BruceEckel.com).
Using URLs from within an
applet
It’s possible for an applet to cause the display
of any URL through the Web browser the applet is running within. You can do this
with the following line:
getAppletContext().showDocument(u);
in which u is the URL object.
Here’s a simple example that redirects you to another Web page. Although
you’re just redirected to an HTML page, you could also redirect to the
output of a CGI program.
//:
c15:ShowHTML.java
//
<applet code=ShowHTML width=100 height=50>
//
</applet>
import
javax.swing.*;
import
java.awt.*;
import
java.awt.event.*;
import
java.net.*;
import
java.io.*;
import
com.bruceeckel.swing.*;
public
class
ShowHTML
extends
JApplet {
JButton send =
new
JButton("Go");
JLabel l =
new
JLabel();
public
void
init() {
Container cp = getContentPane();
cp.setLayout(new
FlowLayout());
send.addActionListener(new
Al());
cp.add(send);
cp.add(l);
}
class
Al
implements
ActionListener {
public
void
actionPerformed(ActionEvent ae) {
try
{
// This could be a CGI program
instead of
// an HTML
page.
URL u =
new
URL(getDocumentBase(),
"FetcherFrame.html");
// Display the output of the URL
using
// the Web browser, as an ordinary
page:
getAppletContext().showDocument(u);
}
catch(Exception
e) {
l.setText(e.toString());
}
}
}
public
static
void
main(String[] args) {
Console.run(new
ShowHTML(), 100, 50);
}
}
///:~
The beauty of the URL class is how much it
shields you from. You can connect to Web servers without knowing much at all
about what’s going on under the covers.
Reading a file from the server
A variation on the above program reads a file located
on the server. In this case, the file is specified by the
client:
//:
c15:Fetcher.java
//
<applet code=Fetcher width=500 height=300>
//
</applet>
import
javax.swing.*;
import
java.awt.*;
import
java.awt.event.*;
import
java.net.*;
import
java.io.*;
import
com.bruceeckel.swing.*;
public
class
Fetcher
extends
JApplet {
JButton fetchIt=
new
JButton("Fetch the
Data");
JTextField f =
new
JTextField("Fetcher.java",
20);
JTextArea t =
new
JTextArea(10,40);
public
void
init() {
Container cp = getContentPane();
cp.setLayout(new
FlowLayout());
fetchIt.addActionListener(new
FetchL());
cp.add(new
JScrollPane(t));
cp.add(f); cp.add(fetchIt);
}
public
class
FetchL
implements
ActionListener {
public
void
actionPerformed(ActionEvent e) {
try
{
URL url =
new
URL(getDocumentBase(),
f.getText());
t.setText(url +
"\n");
InputStream is = url.openStream();
BufferedReader in =
new
BufferedReader(
new
InputStreamReader(is));
String line;
while
((line = in.readLine()) !=
null)
t.append(line +
"\n");
}
catch(Exception
ex) {
t.append(ex.toString());
}
}
}
public
static
void
main(String[] args) {
Console.run(new
Fetcher(), 500, 300);
}
}
///:~
The creation of the URL object is similar to
the previous example—getDocumentBase( ) is the starting point
as before, but this time the name of the file is read from the
JTextField. Once the URL object is created, its String
version is placed in the JTextArea so we can see what it looks like. Then
an InputStream is procured from the URL, which in this case will
simply produce a stream of the characters in the file. After converting to a
Reader and buffering, each line is read and appended to the
JTextArea. Note that the JTextArea has been placed inside a
JScrollPane so that scrolling is handled automatically.
More to networking
There’s actually a lot more to networking than
can be covered in this introductory treatment. Java networking also provides
fairly extensive support for URLs, including protocol handlers for different
types of content that can be discovered at an Internet site. You can find other
Java networking features fully and carefully described in Java Network
Programming by Elliotte Rusty Harold (O’Reilly, 1997
|