MindView Inc.

Thinking in Java, 3rd ed. Revision 2.0


[ Viewing Hints ] [ Book Home Page ] [ Free Newsletter ]
[ Seminars ] [ Seminars on CD ROM ] [ Consulting ]

Previous Next Title Page Index Contents

16: 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. Comment

However, the basic idea of distributed computing is not so difficult, and is abstracted very nicely in the Java libraries. You want to:

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). Comment

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. Comment

This section introduces Java’s networking support using easy-to-understand examples. Comment

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“: Comment

  1. 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.Comment
  2. Alternatively, you can use the dotted quad” form, which is four numbers separated by dots, such as 123.255.28.120.Comment

In both cases, the IP address is represented internally as a 32-bit number[90] (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. Comment

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? Comment

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.Comment

//: c15:WhoAmI.java
// Finds out your network address when
// you're connected to the Internet.
// {RunByHand} Must be connected to the Internet
// {Args: www.google.com}
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. Comment

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?” Comment

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. Comment

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. Comment

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:Comment

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( ). Comment

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. Comment

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. Comment

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. Comment

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). Comment

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. Comment

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. Comment

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. Comment

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.” Comment

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.) Comment

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. Comment

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). Comment

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.) Comment

Here is the server:

//: c15:JabberServer.java
// Very simple server that just
// echoes whatever the client sends.
// {RunByHand}
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. Comment

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. Comment

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). Comment

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. Comment

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. Comment

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. Comment

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). Comment

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. Comment

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.
// {RunByHand}
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: Comment

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. Comment

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: Comment

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. Comment

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.) Comment

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. Comment

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. Comment

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. Comment

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. Comment

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. Comment

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.
// {RunByHand}
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. Comment

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( ). Comment

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. Comment

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. Comment

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.
// {RunByHand}
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. Comment

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. Comment

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. Comment

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. Comment

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). Comment

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. Comment

//: 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. Comment

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. Comment

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). Comment

Java Database Connectivity (JDBC)

It has been estimated that half of all software development involves client/server operations. A great promise of Java has been the ability to build platform-independent client/server database applications. This has come to fruition with Java DataBase Connectivity (JDBC). Comment

One of the major problems with databases has been the feature wars between the database companies. There is a “standard” database language, Structured Query Language (SQL-92), but you must usually know which database vendor you’re working with despite the standard. JDBC is designed to be platform-independent, so you don’t need to worry about the database you’re using while you’re programming. However, it’s still possible to make vendor-specific calls from JDBC so you aren’t restricted from doing what you must. Comment

One place where programmers may need to use SQL type names is in the SQL TABLE CREATE statement when they are creating a new database table and defining the SQL type for each column. Unfortunately there are significant variations between SQL types supported by different database products. Different databases that support SQL types with the same semantics and structure may give those types different names. Most major databases support an SQL data type for large binary values: in Oracle this type is called a LONG RAW, Sybase calls it IMAGE, Informix calls it BYTE, and DB2 calls it LONG VARCHAR FOR BIT DATA. Therefore, if database portability is a goal you should try to use only generic SQL type identifiers. Comment

Portability is an issue when writing for a book where readers may be testing the examples with all kinds of unknown data stores. I have tried to write these examples to be as portable as possible. You should also notice that the database-specific code has been isolated in order to centralize any changes that you may need to perform to get the examples operational in your environment. Comment

JDBC, like many of the APIs in Java, is designed for simplicity. The method calls you make correspond to the logical operations you’d think of doing when gathering data from a database: connect to the database, create a statement and execute the query, and look at the result set. Comment

To allow this platform independence, JDBC provides a driver manager that dynamically maintains all the driver objects that your database queries will need. So if you have three different kinds of vendor databases to connect to, you’ll need three different driver objects. The driver objects register themselves with the driver manager at the time of loading, and you can force the loading using Class.forName( ). Comment

To open a database, you must create a “database URL” that specifies:

  1. That you’re using JDBC with “jdbc.” Comment
  2. The “subprotocol”: the name of the driver or the name of a database connectivity mechanism. Since the design of JDBC was inspired by ODBC, the first subprotocol available is the “jdbc-odbc bridge,” specified by “odbc.” Comment
  3. The database identifier. This varies with the database driver used, but it generally provides a logical name that is mapped by the database administration software to a physical directory where the database tables are located. For your database identifier to have any meaning, you must register the name using your database administration software. (The process of registration varies from platform to platform.) Comment

All this information is combined into one string, the “database URL.” For example, to connect through the ODBC subprotocol to a database identified as “people,” the database URL could be: Comment

String dbUrl = "jdbc:odbc:people";


If you’re connecting across a network, the database URL will contain the connection information identifying the remote machine and can become a bit intimidating. Here is an example of a CloudScape database being called from a remote client utilizing RMI: Comment

jdbc:rmi://192.168.170.27:1099/jdbc:cloudscape:db


This database URL is really two jdbc calls in one. The first part “jdbc:rmi://192.168.170.27:1099/” uses RMI to make the connection to the remote database engine listening on port 1099 at IP Address 192.168.170.27. The second part of the URL, “jdbc:cloudscape:db” conveys the more typical settings using the subprotocol and database name but this will only happen after the first section has made the connection via RMI to the remote machine. Comment

When you’re ready to connect to the database, call the static method DriverManager.getConnection( ) and pass it the database URL, the user name, and a password to get into the database. You get back a Connection object that you can then use to query and manipulate the database. Comment

The following example opens a database of contact information and looks for a person’s last name as given on the command line. It selects only the names of people that have email addresses, then prints out all the ones that match the given last name:

//: c15:jdbc:Lookup.java
// Looks up email addresses in a 
// local database using JDBC.
// {Broken}
import java.sql.*;

public class Lookup {
  public static void main(String[] args) 
  throws SQLException, ClassNotFoundException {
    String dbUrl = "jdbc:odbc:people";
    String user = "";
    String password = "";
    // Load the driver (registers itself)
    Class.forName(
      "sun.jdbc.odbc.JdbcOdbcDriver");
    Connection c = DriverManager.getConnection(
      dbUrl, user, password);
    Statement s = c.createStatement();
    // SQL code:
    ResultSet r = 
      s.executeQuery(
        "SELECT FIRST, LAST, EMAIL " +
        "FROM people.csv people " +
        "WHERE " +
        "(LAST='" + args[0] + "') " +
        " AND (EMAIL Is Not Null) " +
        "ORDER BY FIRST");
    while(r.next()) {
      // Capitalization doesn't matter:
      System.out.println(
        r.getString("Last") + ", " 
        + r.getString("fIRST")
        + ": " + r.getString("EMAIL") );
    }
    s.close(); // Also closes ResultSet
  }
} ///:~


You can see the creation of the database URL as previously described. In this example, there is no password protection on the database so the user name and password are empty strings. Comment

Once the connection is made with DriverManager.getConnection( ), you can use the resulting Connection object to create a Statement object using the createStatement( ) method. With the resulting Statement, you can call executeQuery( ), passing in a string containing an SQL-92 standard SQL statement. (You’ll see shortly how you can generate this statement automatically, so you don’t have to know much about SQL.) Comment

The executeQuery( ) method returns a ResultSet object, which is an iterator: the next( ) method moves the iterator to the next record in the statement, or returns false if the end of the result set has been reached. You’ll always get a ResultSet object back from executeQuery( ) even if a query results in an empty set (that is, an exception is not thrown). Note that you must call next( ) once before trying to read any record data. If the result set is empty, this first call to next( ) will return false. For each record in the result set, you can select the fields using (among other approaches) the field name as a string. Also note that the capitalization of the field name is ignored—it doesn’t matter with an SQL database. You determine the type you’ll get back by calling getInt( ), getString( ), getFloat( ), etc. At this point, you’ve got your database data in Java native format and can do whatever you want with it using ordinary Java code. Comment

Getting the example to work

With JDBC, understanding the code is relatively simple. The confusing part is making it work on your particular system. The reason this is confusing is that it requires you to figure out how to get your JDBC driver to load properly, and how to set up a database using your database administration software. Comment

Of course, this process can vary radically from machine to machine, but the process I used to make it work under 32-bit Windows might give you clues to help you attack your own situation. Comment

Step 1: Find the JDBC Driver

The program above contains the statement:

Class.forName("sun.jdbc.odbc.JdbcOdbcDriver");


This implies a directory structure, which is deceiving. With this particular installation of JDK 1.1, there was no file called JdbcOdbcDriver.class, so if you looked at this example and went searching for it you’d be frustrated. Other published examples use a pseudo name, such as “myDriver.ClassName,” which is less than helpful. In fact, the load statement above for the jdbc-odbc driver (the only one that actually comes with the JDK) appears in only a few places in the online documentation (in particular, a page labeled “JDBC-ODBC Bridge Driver”). If the load statement above doesn’t work, then the name might have been changed as part of a Java version change, so you should hunt through the documentation again. Comment

If the load statement is wrong, you’ll get an exception at this point. To test whether your driver load statement is working correctly, comment out the code after the statement and up to the catch clause; if the program throws no exceptions it means that the driver is loading properly. Comment

Step 2: Configure the database

Again, this is specific to 32-bit Windows; you might need to do some research to figure it out for your own platform. Comment

First, open the control panel. You might find two icons that say “ODBC.” You must use the one that says “32bit ODBC,” since the other one is for backward compatibility with 16-bit ODBC software and will produce no results for JDBC. When you open the “32bit ODBC” icon, you’ll see a tabbed dialog with a number of tabs, including “User DSN,” “System DSN,” “File DSN,” etc., in which “DSN” means “Data Source Name.” It turns out that for the JDBC-ODBC bridge, the only place where it’s important to set up your database is “System DSN,” but you’ll also want to test your configuration and create queries, and for that you’ll also need to set up your database in “File DSN.” This will allow the Microsoft Query tool (that comes with Microsoft Office) to find the database. Note that other query tools are also available from other vendors. Comment

The most interesting database is one that you’re already using. Standard ODBC supports a number of different file formats including such venerable workhorses as DBase. However, it also includes the simple “comma-separated ASCII” format, which virtually every data tool has the ability to write. In my case, I just took my “people” database that I’ve been maintaining for years using various contact-management tools and exported it as a comma-separated ASCII file (these typically have an extension of .csv). In the “System DSN” section I chose “Add,” chose the text driver to handle my comma-separated ASCII file, and then un-checked “use current directory” to allow me to specify the directory where I exported the data file. Comment

You’ll notice when you do this that you don’t actually specify a file, only a directory. That’s because a database is typically represented as a collection of files under a single directory (although it could be represented in other forms as well). Each file usually contains a single table, and the SQL statements can produce results that are culled from multiple tables in the database (this is called a join). A database that contains only a single table (like my “people” database) is usually called a flat-file database. Most problems that go beyond the simple storage and retrieval of data generally require multiple tables that must be related by joins to produce the desired results, and these are called relational databases. Comment

Step 3: Test the configuration

To test the configuration you’ll need a way to discover whether the database is visible from a program that queries it. Of course, you can simply run the JDBC program example above, up to and including the statement:

Connection c = DriverManager.getConnection(
  dbUrl, user, password);


If an exception is thrown, your configuration was incorrect. Comment

However, it’s useful to get a query-generation tool involved at this point. I used Microsoft Query that came with Microsoft Office, but you might prefer something else. The query tool must know where the database is, and Microsoft Query required that I go to the ODBC Administrator’s “File DSN” tab and add a new entry there, again specifying the text driver and the directory where my database lives. You can name the entry anything you want, but it’s helpful to use the same name you used in “System DSN.” Comment

Once you’ve done this, you will see that your database is available when you create a new query using your query tool. Comment

Step 4: Generate your SQL query

The query that I created using Microsoft Query not only showed me that my database was there and in good order, but it also automatically created the SQL code that I needed to insert into my Java program. I wanted a query that would search for records that had the last name that was typed on the command line when starting the Java program. So as a starting point, I searched for a specific last name, “Eckel.” I also wanted to display only those names that had email addresses associated with them. The steps I took to create this query were:

  1. Start a new query and use the Query Wizard. Select the “people” database. (This is the equivalent of opening the database connection using the appropriate database URL.)
  2. Select the “people” table within the database. From within the table, choose the columns FIRST, LAST, and EMAIL.
  3. Under “Filter Data,” choose LAST and select “equals” with an argument of “Eckel.” Click the “And” radio button.
  4. Choose EMAIL and select “Is not Null.”
  5. Under “Sort By,” choose FIRST.

The result of this query will show you whether you’re getting what you want. Comment

Now you can press the SQL button and without any research on your part, up will pop the correct SQL code, ready for you to cut and paste. For this query, it looked like this:

SELECT people.FIRST, people.LAST, people.EMAIL
FROM people.csv people
WHERE (people.LAST='Eckel') AND 
(people.EMAIL Is Not Null)
ORDER BY people.FIRST


Especially with more complicated queries it’s easy to get things wrong, but by using a query tool you can interactively test your queries and automatically generate the correct code. It’s hard to argue the case for doing this by hand. Comment

Step 5: Modify and paste in your query

You’ll notice that the code above looks different from what’s used in the program. That’s because the query tool uses full qualification for all of the names, even when there’s only one table involved. (When more than one table is involved, the qualification prevents collisions between columns from different tables that have the same names.) Since this query involves only one table, you can optionally remove the “people” qualifier from most of the names, like this: Comment

SELECT FIRST, LAST, EMAIL
FROM people.csv people
WHERE (LAST='Eckel') AND 
(EMAIL Is Not Null)
ORDER BY FIRST


In addition, you don’t want this program to be hard coded to look for only one name. Instead, it should hunt for the name given as the command-line argument. Making these changes and turning the SQL statement into a dynamically-created String produces: Comment

"SELECT FIRST, LAST, EMAIL " +
"FROM people.csv people " +
"WHERE " +
"(LAST='" + args[0] + "') " +
" AND (EMAIL Is Not Null) " +
"ORDER BY FIRST");


SQL has another way to insert names into a query called stored procedures, which is used for speed. But for much of your database experimentation and for your first cut, building your own query strings in Java is fine. Comment

You can see from this example that by using the tools currently available—in particular the query-building tool—database programming with SQL and JDBC can be quite straightforward. Comment

A GUI version of the lookup program

It’s more useful to leave the lookup program running all the time and simply switch to it and type in a name whenever you want to look someone up. The following program creates the lookup program as an application/applet, and it also adds name completion so the data will show up without forcing you to type the entire last name:Comment

//: c15:jdbc:VLookup.java
// GUI version of Lookup.java.
// <applet code=VLookup
// width=500 height=200></applet>
// {Broken}
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.event.*;
import java.sql.*;
import com.bruceeckel.swing.*;

public class VLookup extends JApplet {
  String dbUrl = "jdbc:odbc:people";
  String user = "";
  String password = "";
  Statement s;
  JTextField searchFor = new JTextField(20);
  JLabel completion = 
    new JLabel("                        ");
  JTextArea results = new JTextArea(40, 20);
  public void init() {
    searchFor.getDocument().addDocumentListener(
      new SearchL());
    JPanel p = new JPanel();
    p.add(new Label("Last name to search for:"));
    p.add(searchFor);
    p.add(completion);
    Container cp = getContentPane();
    cp.add(p, BorderLayout.NORTH);
    cp.add(results, BorderLayout.CENTER);
    try {
      // Load the driver (registers itself)
      Class.forName(
        "sun.jdbc.odbc.JdbcOdbcDriver");
      Connection c = DriverManager.getConnection(
        dbUrl, user, password);
      s = c.createStatement();
    } catch(Exception e) {
      results.setText(e.toString());
    }
  }
  class SearchL implements DocumentListener {
    public void changedUpdate(DocumentEvent e){}
    public void insertUpdate(DocumentEvent e){
      textValueChanged();
    }
    public void removeUpdate(DocumentEvent e){
      textValueChanged();
    }
  }
  public void textValueChanged() {
    ResultSet r;
    if(searchFor.getText().length() == 0) {
      completion.setText("");
      results.setText("");
      return;
    }
    try {
      // Name completion:
      r = s.executeQuery(
        "SELECT LAST FROM people.csv people " +
        "WHERE (LAST Like '" +
        searchFor.getText()  + 
        "%') ORDER BY LAST");
      if(r.next()) 
        completion.setText(
          r.getString("last"));
      r = s.executeQuery(
        "SELECT FIRST, LAST, EMAIL " +
        "FROM people.csv people " +
        "WHERE (LAST='" + 
        completion.getText() +
        "') AND (EMAIL Is Not Null) " +
        "ORDER BY FIRST");
    } catch(Exception e) {
      results.setText(
        searchFor.getText() + "\n");
      results.append(e.toString());
      return; 
    }
    results.setText("");
    try {
      while(r.next()) {
        results.append(
          r.getString("Last") + ", " 
          + r.getString("fIRST") + 
          ": " + r.getString("EMAIL") + "\n");
      }
    } catch(Exception e) {
      results.setText(e.toString());
    }
  }
  public static void main(String[] args) {
    Console.run(new VLookup(), 500, 200);
  }
} ///:~


Much of the database logic is the same, but you can see that a DocumentListener is added to listen to the JTextField (see the javax.swing.JTextField entry in the Java HTML documentation from java.sun.com for details), so that whenever you type a new character it first tries to do a name completion by looking up the last name in the database and using the first one that shows up. (It places it in the completion JLabel, and uses that as the lookup text.) This way, as soon as you’ve typed enough characters for the program to uniquely find the name you’re looking for, you can stop. Comment

Why the JDBC API
seems so complex

When you browse the online documentation for JDBC it can seem daunting. In particular, in the DatabaseMetaData interface—which is just huge, contrary to most of the interfaces you see in Java—there are methods such as dataDefinitionCausesTransactionCommit( ), getMaxColumnNameLength( ), getMaxStatementLength( ), storesMixedCaseQuotedIdentifiers( ), supportsANSI92IntermediateSQL( ), supportsLimitedOuterJoins( ), and so on. What’s this all about? Comment

As mentioned earlier, databases have seemed from their inception to be in a constant state of turmoil, primarily because the demand for database applications, and thus database tools, is so great. Only recently has there been any convergence on the common language of SQL (and there are plenty of other database languages in common use). But even with an SQL “standard” there are so many variations on that theme that JDBC must provide the large DatabaseMetaData interface so that your code can discover the capabilities of the particular “standard” SQL database that it’s currently connected to. In short, you can write simple, transportable SQL, but if you want to optimize speed your coding will multiply tremendously as you investigate the capabilities of a particular vendor’s database. Comment

This, of course, is not Java’s fault. The discrepancies between database products are just something that JDBC tries to help compensate for. But bear in mind that your life will be easier if you can either write generic queries and not worry quite as much about performance, or, if you must tune for performance, know the platform you’re writing for so you don’t need to write all that investigation code. Comment

A more sophisticated example

A more interesting example[91] involves a multitable database that resides on a server. Here, the database is meant to provide a repository for community activities and to allow people to sign up for these events, so it is called the Community Interests Database (CID). This example will only provide an overview of the database and its implementation, and is not intended to be an in-depth tutorial on database development. There are numerous books, seminars, and software packages that will help you in the design and development of a database. Comment

In addition, this example presumes the prior installation of an SQL database on a server (although it could also be run on a local machine), and the interrogation and discovery of an appropriate JDBC driver for that database. Several free SQL databases are available, and some are even automatically installed with various flavors of Linux. You are responsible for making the choice of database and locating the JDBC driver; the example here is based on an SQL database system called “Cloudscape,” which can be freely downloaded for development (not deployment) from http://www.Cloudscape.com. You’ll need to follow the instructions in the download in order to properly install and set up Cloudscape. Comment

To keep changes in the connection information simple, the database driver, database URL, user name, and password are placed in a separate class:

//: c15:jdbc:CIDConnect.java
// Database connection information for
// the community interests database (CID).

public class CIDConnect {
  // All the information specific to CloudScape:
  public static String dbDriver = 
    "COM.cloudscape.core.JDBCDriver";
  public static String dbURL =
    "jdbc:cloudscape:d:/docs/_work/JSapienDB";
  public static String user = "";
  public static String password = "";
} ///:~


In this example, there is no password protection on the database so the user name and password are empty strings. With Cloudscape, the dbURL contains the directory path where the database is located, but other JDBC drivers will use other ways to encode this information. This example assumes that the database “JSapienDB” has already been created, but in order to get the example to work you’ll need to use the cview tool that comes with Cloudscape in order to create the new database, and then you must change the above dbURL to reflect the path of the database you created.Comment

The database consists of a set of tables that have a structure as shown here:

TIJ328.png

“Members” contains community member information, “Events” and “Locations” contain information about the activities and where they take place, and “Evtmems” connects events and members that would like to attend that event. You can see that a data member in one table produces a key in another table. Comment

The following class contains the SQL strings that will create these database tables (refer to an SQL guide for an explanation of the SQL code): Comment

//: c15:jdbc:CIDSQL.java
// SQL strings to create the tables for the CID.

public class CIDSQL {
  public static String[] sql = {
    // Create the MEMBERS table:
    "drop table MEMBERS",
    "create table MEMBERS " +
    "(MEM_ID INTEGER primary key, " +
    "MEM_UNAME VARCHAR(12) not null unique, "+
    "MEM_LNAME VARCHAR(40), " +
    "MEM_FNAME VARCHAR(20), " +
    "ADDRESS VARCHAR(40), " +
    "CITY VARCHAR(20), " +
    "STATE CHAR(4), " +
    "ZIP CHAR(5), " +
    "PHONE CHAR(12), " +
    "EMAIL VARCHAR(30))",
    "create unique index " +
    "LNAME_IDX on MEMBERS(MEM_LNAME)",
    // Create the EVENTS table
    "drop table EVENTS",
    "create table EVENTS " +
    "(EVT_ID INTEGER primary key, " +
    "EVT_TITLE VARCHAR(30) not null, " +
    "EVT_TYPE VARCHAR(20), " +
    "LOC_ID INTEGER, " +
    "PRICE DECIMAL, " +
    "DATETIME TIMESTAMP)",
    "create unique index " +
    "TITLE_IDX on EVENTS(EVT_TITLE)",
    // Create the EVTMEMS table
    "drop table EVTMEMS",
    "create table EVTMEMS " +
    "(MEM_ID INTEGER not null, " +
    "EVT_ID INTEGER not null, " +
    "MEM_ORD INTEGER)",
    "create unique index " +
    "EVTMEM_IDX on EVTMEMS(MEM_ID, EVT_ID)",
    // Create the LOCATIONS table
    "drop table LOCATIONS",
    "create table LOCATIONS " +
    "(LOC_ID INTEGER primary key, " +
    "LOC_NAME VARCHAR(30) not null, " +
    "CONTACT VARCHAR(50), " +
    "ADDRESS VARCHAR(40), " +
    "CITY VARCHAR(20), " +
    "STATE VARCHAR(4), " +
    "ZIP VARCHAR(5), " +
    "PHONE CHAR(12), " +
    "DIRECTIONS VARCHAR(4096))",
    "create unique index " +
    "NAME_IDX on LOCATIONS(LOC_NAME)",
  };
} ///:~


The following program uses the CIDConnect and CIDSQL information to load the JDBC driver, make a connection to the database, and then create the table structure diagrammed above. To connect with the database, you call the static method DriverManager.getConnection( ), passing it the database URL, the user name, and a password to get into the database. You get back a Connection object that you can use to query and manipulate the database. Once the connection is made you can simply push the SQL to the database, in this case by marching through the CIDSQL array. However, the first time this program is run, the “drop table” command will fail, causing an exception, which is caught, reported, and then ignored. The reason for the “drop table” command is to allow easy experimentation: you can modify the SQL that defines the tables and then rerun the program, causing the old tables to be replaced by the new. Comment

In this example, it makes sense to let the exceptions be thrown out to the console:

//: c15:jdbc:CIDCreateTables.java
// Creates database tables for the
// community interests database.
// {Broken}
import java.sql.*;

public class CIDCreateTables {
  public static void main(String[] args) 
  throws SQLException, ClassNotFoundException,
  IllegalAccessException {
    // Load the driver (registers itself)
    Class.forName(CIDConnect.dbDriver);
    Connection c = DriverManager.getConnection(
      CIDConnect.dbURL, CIDConnect.user, 
      CIDConnect.password);
    Statement s = c.createStatement();
    for(int i = 0; i < CIDSQL.sql.length; i++) {
      System.out.println(CIDSQL.sql[i]);
      try {
        s.executeUpdate(CIDSQL.sql[i]);
      } catch(SQLException sqlEx) {
        System.err.println(
          "Probably a 'drop table' failed");
      }
    }
    s.close();
    c.close();
  }
} ///:~


Note that all changes in the database can be controlled by changing Strings in the CIDSQL table, without modifying CIDCreateTables. Comment

executeUpdate( ) will usually return the number of rows that were affected by the SQL statement. executeUpdate( ) is more commonly used to execute INSERT, UPDATE, or DELETE statements that modify one or more rows. For statements such as CREATE TABLE, DROP TABLE, and CREATE INDEX, executeUpdate( ) always returns zero. Comment

To test the database, it is loaded with some sample data. This requires a series of INSERTs followed by a SELECT to produce result set. To make additions and changes to the test data easy, the test data is set up as a two-dimensional array of Objects, and the executeInsert( ) method can then use the information in one row of the table to create the appropriate SQL command.

//: c15:jdbc:LoadDB.java
// Loads and tests the database.
// {Broken}
import java.sql.*;

class TestSet {
  Object[][] data = {
    { "MEMBERS", new Integer(1),
      "dbartlett", "Bartlett", "David",
      "123 Mockingbird Lane",
      "Gettysburg", "PA", "19312",
      "123.456.7890",  "bart@you.net" },
    { "MEMBERS", new Integer(2),
      "beckel", "Eckel", "Bruce",
      "123 Over Rainbow Lane",
      "Crested Butte", "CO", "81224",
      "123.456.7890", "beckel@you.net" },
    { "MEMBERS", new Integer(3),
      "rcastaneda", "Castaneda", "Robert",
      "123 Downunder Lane",
      "Sydney", "NSW", "12345",
      "123.456.7890", "rcastaneda@you.net" },
    { "LOCATIONS", new Integer(1),
      "Center for Arts",
      "Betty Wright", "123 Elk Ave.",
      "Crested Butte", "CO", "81224",
      "123.456.7890",
      "Go this way then that." },
    { "LOCATIONS", new Integer(2),
      "Witts End Conference Center",
      "John Wittig", "123 Music Drive",
      "Zoneville", "PA", "19123",
      "123.456.7890",
      "Go that way then this." },
    { "EVENTS", new Integer(1),
      "Project Management Myths",
      "Software Development",
      new Integer(1), new Float(2.50),
      "2000-07-17 19:30:00" },
    { "EVENTS", new Integer(2),
      "Life of the Crested Dog",
      "Archeology",
      new Integer(2), new Float(0.00),
      "2000-07-19 19:00:00" },
    // Match some people with events
    {  "EVTMEMS", 
      new Integer(1),  // Dave is going to
      new Integer(1),  // the Software event.
      new Integer(0) },
    { "EVTMEMS", 
      new Integer(2),  // Bruce is going to
      new Integer(2),  // the Archeology event.
      new Integer(0) },
    { "EVTMEMS", 
      new Integer(3),  // Robert is going to
      new Integer(1),  // the Software event.
      new Integer(1) },
    { "EVTMEMS", 
      new Integer(3), // ... and 
      new Integer(2), // the Archeology event.
      new Integer(1) },
  };
  // Use the default data set:
  public TestSet() {}
  // Use a different data set:
  public TestSet(Object[][] dat) { data = dat; }
}

public class LoadDB {
  Statement statement;
  Connection connection;
  TestSet tset;
  public LoadDB(TestSet t) throws SQLException {
    tset = t;
    try {
      // Load the driver (registers itself)
      Class.forName(CIDConnect.dbDriver);
    } catch(java.lang.ClassNotFoundException e) {
      e.printStackTrace(System.err);
    }
    connection = DriverManager.getConnection(
      CIDConnect.dbURL, CIDConnect.user, 
      CIDConnect.password);
    statement = connection.createStatement();
  }
  public void cleanup() throws SQLException {
    statement.close();
    connection.close();
  }
  public void executeInsert(Object[] data) {
    String sql = "insert into " 
      + data[0] + " values(";
    for(int i = 1; i < data.length; i++) {
      if(data[i] instanceof String)
        sql += "'" + data[i] + "'";
      else
        sql += data[i];
      if(i < data.length - 1)
        sql += ", ";
    }
    sql += ')';
    System.out.println(sql);
    try {
      statement.executeUpdate(sql);
    } catch(SQLException sqlEx) {
      System.err.println("Insert failed.");
      while (sqlEx != null) {
        System.err.println(sqlEx.toString());
        sqlEx = sqlEx.getNextException();
      }
    } 
  }
  public void load() {
    for(int i = 0; i< tset.data.length; i++)
      executeInsert(tset.data[i]);
  }
  // Throw exceptions out to console:
  public static void main(String[] args) 
  throws SQLException {
    LoadDB db = new LoadDB(new TestSet());
    db.load();
    try {
      // Get a ResultSet from the loaded database:
      ResultSet rs = db.statement.executeQuery(
        "select " +
        "e.EVT_TITLE, m.MEM_LNAME, m.MEM_FNAME "+
        "from EVENTS e, MEMBERS m, EVTMEMS em " +
        "where em.EVT_ID = 2 " +
        "and e.EVT_ID = em.EVT_ID " +
        "and m.MEM_ID = em.MEM_ID");
      while (rs.next())
        System.out.println(
          rs.getString(1) + "  " + 
          rs.getString(2) + ", " +
          rs.getString(3));
    } finally {
      db.cleanup();
    }
  }
} ///:~


The TestSet class contains a default set of data that is produced if you use the default constructor; however, you can also create a TestSet object using an alternate data set with the second constructor. The set of data is held in a two-dimensional array of Object because it can be any type, including String or numerical types. The executeInsert( ) method uses RTTI to distinguish between String data (which must be quoted) and non-String data as it builds the SQL command from the data. After printing this command to the console, executeUpdate( ) is used to send it to the database. Comment

The constructor for LoadDB makes the connection, and load( ) steps through the data and calls executeInsert( ) for each record. cleanup( ) closes the statement and the connection; to guarantee that this is called, it is placed inside a finally clause. Comment

Once the database is loaded, an executeQuery( ) statement produces a sample result set. Since the query combines several tables, it is an example of a join. Comment

There is more JDBC information available in the electronic documents that come as part of the Java distribution from Sun. In addition, you can find more in the book JDBC Database Access with Java (Hamilton, Cattel, and Fisher, Addison-Wesley, 1997). Other JDBC books appear regularly. Comment

RMI (Remote Method Invocation)

Traditional approaches to executing code on other machines across a network have been confusing as well as tedious and error-prone to implement. The nicest way to think about this problem is that some object happens to live on another machine, and that you can send a message to the remote object and get a result as if the object lived on your local machine. This simplification is exactly what Java Remote Method Invocation (RMI) allows you to do. This section walks you through the steps necessary to create your own RMI objects. Comment

Remote interfaces

RMI makes heavy use of interfaces. When you want to create a remote object, you mask the underlying implementation by passing around an interface. Thus, when the client gets a reference to a remote object, what they really get is an interface reference, which happens to connect to some local stub code that talks across the network. But you don’t think about this, you just send messages via your interface reference. Comment

When you create a remote interface, you must follow these guidelines:

  1. The remote interface must be public (it cannot have “package access,” that is, it cannot be “friendly”). Otherwise, a client will get an error when attempting to load a remote object that implements the remote interface.
  2. The remote interface must extend the interface java.rmi.Remote.
  3. Each method in the remote interface must declare java.rmi.RemoteException in its throws clause in addition to any application-specific exceptions.
  4. A remote object passed as an argument or return value (either directly or embedded within a local object) must be declared as the remote interface, not the implementation class.

Here’s a simple remote interface that represents an accurate time service: Comment

//: c15:rmi:PerfectTimeI.java
// The PerfectTime remote interface.
package c15.rmi;
import java.rmi.*;

public interface PerfectTimeI extends Remote {
  long getPerfectTime() throws RemoteException;
} ///:~


It looks like any other interface except that it extends Remote and all of its methods throw RemoteException. Remember that all the methods of an interface are automatically public. Comment

Implementing the remote interface

The server must contain a class that extends UnicastRemoteObject and implements the remote interface. This class can also have additional methods, but only the methods in the remote interface are available to the client, of course, since the client will get only a reference to the interface, not the class that implements it. Comment

You must explicitly define the constructor for the remote object even if you’re only defining a default constructor that calls the base-class constructor. You must write it out since it must throw RemoteException. Comment

Here’s the implementation of the remote interface PerfectTimeI:

//: c15:rmi:PerfectTime.java
// The implementation of 
// the PerfectTime remote object.
// {Broken}
package c15.rmi;
import java.rmi.*;
import java.rmi.server.*;
import java.rmi.registry.*;
import java.net.*;

public class PerfectTime 
extends UnicastRemoteObject
implements PerfectTimeI {
  // Implementation of the interface:
  public long getPerfectTime() 
      throws RemoteException {
    return System.currentTimeMillis();
  }
  // Must implement constructor 
  // to throw RemoteException:
  public PerfectTime() throws RemoteException {
    // super(); // Called automatically
  }
  // Registration for RMI serving. Throw 
  // exceptions out to the console.
  public static void main(String[] args) 
  throws Exception {
    System.setSecurityManager(
    new RMISecurityManager());
    PerfectTime pt = new PerfectTime();
    Naming.bind(
      "//peppy:2005/PerfectTime", pt);
    System.out.println("Ready to do time");
  }
} ///:~


Here, main( ) handles all the details of setting up the server. When you’re serving RMI objects, at some point in your program you must: Comment

  1. Create and install a security manager that supports RMI. The only one available for RMI as part of the Java distribution is RMISecurityManager. Comment
  2. Create one or more instances of a remote object. Here, you can see the creation of the PerfectTime object. Comment
  3. Register at least one of the remote objects with the RMI remote object registry for bootstrapping purposes. One remote object can have methods that produce references to other remote objects. This allows you to set it up so the client must go to the registry only once, to get the first remote object. Comment

Setting up the registry

Here, you see a call to the static method Naming.bind( ). However, this call requires that the registry be running as a separate process on the computer. The name of the registry server is rmiregistry, and under 32-bit Windows you say: Comment

start rmiregistry


to start it in the background. On Unix, the command is:

rmiregistry &


Like many network programs, the rmiregistry is located at the IP address of whatever machine started it up, but it must also be listening at a port. If you invoke the rmiregistry as above, with no argument, the registry’s port will default to 1099. If you want it to be at some other port, you add an argument on the command line to specify the port. For this example, the port is located at 2005, so the rmiregistry should be started like this under 32-bit Windows: Comment

start rmiregistry 2005


or for Unix:

rmiregistry 2005 &


The information about the port must also be given to the bind( ) command, as well as the IP address of the machine where the registry is located. But this brings up what can be a frustrating problem if you’re expecting to test RMI programs locally the way the network programs have been tested so far in this chapter. In the JDK 1.1.1 release, there are a couple of problems:[92] Comment

  1. localhost does not work with RMI. Thus, to experiment with RMI on a single machine, you must provide the name of the machine. To find out the name of your machine under 32-bit Windows, go to the control panel and select “Network.” Select the “Identification” tab, and you’ll see your computer name. In my case, I called my computer “Peppy.” It appears that capitalization is ignored. Comment
  2. RMI will not work unless your computer has an active TCP/IP connection, even if all your components are just talking to each other on the local machine. This means that you must connect to your Internet service provider before trying to run the program or you’ll get some obscure exception messages. Comment

With all this in mind, the bind( ) command becomes: Comment

Naming.bind("//peppy:2005/PerfectTime", pt);


If you are using the default port 1099, you don’t need to specify a port, so you could say: Comment

Naming.bind("//peppy/PerfectTime", pt);


You should be able to perform local testing by leaving off the IP address and using only the identifier: Comment

Naming.bind("PerfectTime", pt);


The name for the service is arbitrary; it happens to be PerfectTime here, just like the name of the class, but you could call it anything you want. The important thing is that it’s a unique name in the registry that the client knows to look for to procure the remote object. If the name is already in the registry, you’ll get an AlreadyBoundException. To prevent this, you can always use rebind( ) instead of bind( ), since rebind( ) either adds a new entry or replaces the one that’s already there. Comment

Even though main( ) exits, your object has been created and registered so it’s kept alive by the registry, waiting for a client to come along and request it. As long as the rmiregistry is running and you don’t call Naming.unbind( ) on your name, the object will be there. For this reason, when you’re developing your code you need to shut down the rmiregistry and restart it when you compile a new version of your remote object. Comment

You aren’t forced to start up rmiregistry as an external process. If you know that your application is the only one that’s going to use the registry, you can start it up inside your program with the line:

LocateRegistry.createRegistry(2005);


Like before, 2005 is the port number we happen to be using in this example. This is the equivalent of running rmiregistry 2005 from a command line, but it can often be more convenient when you’re developing RMI code since it eliminates the extra steps of starting and stopping the registry. Once you’ve executed this code, you can bind( ) using Naming as before. Comment

Creating stubs and skeletons

If you compile and run PerfectTime.java, it won’t work even if you have the rmiregistry running correctly. That’s because the framework for RMI isn’t all there yet. You must first create the stubs and skeletons that provide the network connection operations and allow you to pretend that the remote object is just another local object on your machine. Comment

What’s going on behind the scenes is complex. Any objects that you pass into or return from a remote object must implement Serializable (if you want to pass remote references instead of the entire objects, the object arguments can implement Remote), so you can imagine that the stubs and skeletons are automatically performing serialization and deserialization as they “marshal” all of the arguments across the network and return the result. Fortunately, you don’t have to know any of this, but you do have to create the stubs and skeletons. This is a simple process: you invoke the rmic tool on your compiled code, and it creates the necessary files. So the only requirement is that another step be added to your compilation process. Comment

The rmic tool is particular about packages and classpaths, however. PerfectTime.java is in the package c15.rmi, and even if you invoke rmic in the same directory in which PerfectTime.class is located, rmic won’t find the file, since it searches the classpath. So you must specify the location off the class path, like so:

rmic c15.rmi.PerfectTime


You don’t have to be in the directory containing PerfectTime.class when you execute this command, but the results will be placed in the current directory. Comment

When rmic runs successfully, you’ll have two new classes in the directory:

PerfectTime_Stub.class
PerfectTime_Skel.class


corresponding to the stub and skeleton. Now you’re ready to get the server and client to talk to each other. Comment

Using the remote object

The whole point of RMI is to make the use of remote objects simple. The only extra thing that you must do in your client program is to look up and fetch the remote interface from the server. From then on, it’s just regular Java programming: sending messages to objects. Here’s the program that uses PerfectTime:Comment

//: c15:rmi:DisplayPerfectTime.java
// Uses remote object PerfectTime.
// {Broken}
package c15.rmi;
import java.rmi.*;
import java.rmi.registry.*;

public class DisplayPerfectTime {
  public static void main(String[] args) 
  throws Exception {
    System.setSecurityManager(
      new RMISecurityManager());
    PerfectTimeI t = 
      (PerfectTimeI)Naming.lookup(
        "//peppy:2005/PerfectTime");
    for(int i = 0; i < 10; i++)
      System.out.println("Perfect time = " +
        t.getPerfectTime());
  }
} ///:~


The ID string is the same as the one used to register the object with Naming, and the first part represents the URL and port number. Since you’re using a URL, you can also specify a machine on the Internet. Comment

What comes back from Naming.lookup( ) must be cast to the remote interface, not to the class. If you use the class instead, you’ll get an exception. Comment

You can see in the method call

t.getPerfectTime()


that once you have a reference to the remote object, programming with it is indistinguishable from programming with a local object (with one difference: remote methods throw RemoteException). Comment

Summary

This chapter has introduced some, but not all, of the components that Sun refers to as J2EE: the Java 2 Enterprise Edition. The goal of J2EE is to create a set of tools that allows the Java developer to build server-based applications more quickly than before, and in a platform-independent way. It’s not only difficult and time-consuming to build such applications, but it’s especially hard to build them so that they can be easily ported to other platforms, and also to keep the business logic separated from the underlying details of the implementation. J2EE provides a framework to assist in creating server-based applications; these applications are in demand now, and that demand appears to be increasing. Comment

Exercises

Solutions to selected exercises can be found in the electronic document The Thinking in Java Annotated Solution Guide, available for a small fee from www.BruceEckel.com.

  1. Compile and run the JabberServer and JabberClient programs in this chapter. Now edit the files to remove all of the buffering for the input and output, then compile and run them again to observe the results. Comment
  2. Create a server that asks for a password, then opens a file and sends the file over the network connection. Create a client that connects to this server, gives the appropriate password, then captures and saves the file. Test the pair of programs on your machine using the localhost (the local loopback IP address 127.0.0.1 produced by calling InetAddress.getByName(null)). Comment
  3. Modify the server in Exercise 2 so that it uses multithreading to handle multiple clients. Comment
  4. Modify JabberClient.java so that output flushing doesn’t occur and observe the effect. Comment
  5. Modify MultiJabberServer so that it uses thread pooling. Instead of throwing away a thread each time a client disconnects, the thread should put itself into an “available pool” of threads. When a new client wants to connect, the server will look in the available pool for a thread to handle the request, and if one isn’t available, make a new one. This way the number of threads necessary will naturally grow to the required quantity. The value of thread pooling is that it doesn’t require the overhead of creating and destroying a new thread for each new client. Comment
  6. Starting with ShowHTML.java, create an applet that is a password-protected gateway to a particular portion of your Web site. Comment
  7. Modify CIDCreateTables.java so that it reads the SQL strings from a text file instead of CIDSQL. Comment
  8. Configure your system so that you can successfully execute CIDCreateTables.java and LoadDB.java. Comment
  9. (More challenging) Take the VLookup.java program and modify it so that when you click on the resulting name it automatically takes that name and copies it to the clipboard (so you can simply paste it into your email). You’ll need to look back at Chapter 13 to remember how to use the clipboard in JFC. Comment

[90] This means a maximum of just over four billion numbers, which is rapidly running out. The new standard for IP addresses will use a 128-bit number, which should produce enough unique IP addresses for the foreseeable future.

[91] Created by Dave Bartlett.

[92] Many brain cells died in agony to discover this information.


Previous Next Title Page Index Contents