Thinking in Enterprise Java

Thinking in Enterprise Java
by Bruce Eckel et. Al.

Revision 1.1, 5-06-2003
Please note this document is under development and incomplete. Updates to this document can be found at http://www.Mindview.net

Best viewed with Mozilla! (free at www.Mozilla.org) (Even though this document was created with MS Word, IE6 garbles lines with footnote markers on them. Mozilla seems to do a much better job).

___________________________________________

Note: This document requires the installation of the fonts Georgia, Verdana and Andale Mono (code font) for proper viewing. These can be found at: http://sourceforge.net/project/showfiles.php?group_id=34153&release_id=105355

Modifications in Revision 1.1:

·         Removed superscripts to improve viewing with IE

Modifications in Revision 1.0:

·         Raw assembly of initial document. Please don’t expect too much at this point, we’re just getting started.

·         Some material is carried forward from Thinking in Java 2nd edition, some material is new.

·         No feedback mechanism in place at this time


Contents


Introduction to Enterprise Programming  6

Prerequisites. 7

Summary. 7

Network programming with Sockets & Channels  8

Identifying a machine. 8

Servers and clients  10

Testing programs without a network  10

Port: a unique place  within the machine  11

Sockets. 11

A simple server and client 13

Serving multiple clients. 18

Datagrams. 23

Using URLs from within an applet. 23

Reading a file from the server  25

Selector Based Multiplexing in JDK1.4. 26

More to networking. 40

Exercises. 40

Remote Method Invocation (RMI) 42

Remote interfaces. 42

Implementing the remote interface. 43

Setting up the registry  44

Creating stubs and skeletons. 46

Using the remote object. 47

Summary. 48

Exercises. 48

Connecting to Databases  49

Getting the example to work. 52

Step 1: Find the JDBC Driver  52

Step 2: Configure the database  53

Step 3: Test the configuration  53

Step 4: Generate your SQL query  54

Step 5: Modify and paste in your query  55

A GUI version of the lookup program.. 55

Why the JDBC API  seems so complex. 58

A more sophisticated example. 59

Summary. 66

Exercises. 66

Servlets  67

The basic servlet. 68

Servlets and multithreading. 71

Handling sessions with servlets. 72

The Cookie class  73

The Session class  73

Running the servlet examples. 76

Summary. 77

Exercises. 77

JavaServer Pages  78

Implicit objects. 79

JSP directives. 80

JSP scripting elements. 81

Extracting fields and values. 83

JSP page attributes and scope. 84

Manipulating sessions in JSP. 85

Creating and modifying cookies. 87

JSP summary. 88

Exercises. 88

Custom Tags  90

What do custom tags give us?. 91

Using Tags and JavaBeans. 98

Tags that manipulate their body content. 102

Tags that iterate. 106

Tags within Tags. 112

The Tag Classes. 113

Using TagExtraInfo classes. 115

Tag Library Descriptors Revisited. 116

Deploying Tag Libraries. 119

Using Third Party Tag Libraries. 121

Enterprise JavaBeans  123

Enterprise JavaBean Flavors. 126

EJB Roles. 129

The Basic APIs. 130

JNDI 130

EJB. 134

EJB Container internals. 136

Software. 138

The example application. 138

Your first Enterprise JavaBean. 144

Home Interface. 145

Component Interface. 147

Primary Key. 148

Implementation Class. 148

Deployment Descriptor. 153

Packaging. 155

Deployment. 156

Building it all 158

An EJB client application. 158

Simplifying EJB Development 162

Implementing a session bean. 167

EJB Local Interfaces. 175

Bulk accessors and value objects. 180

Finder Methods and EJB-QL. 183

Entity Relationships. 191

Summary. 208

Resources. 208

XML  210

What is XML?. 211

XML Elements. 215

XML Attributes. 215

Character Sets. 216

XML Technologies. 217

JAXP – Processing XML. 217

XML Namespaces. 217

Well-Formed and Valid XML. 218

Validating Parsers: SAX and DOM.. 220

SAX   221

DOM    223

Plus and Minus of SAX and DOM    226

XML Serialization. 226

Xerces Serialization. 226

DOM Level 3 Serialization. 228

XPath. 228

XML Transformations. 228

XML to HTML: Displaying a menu. 229

The Root Node. 230

XLink and XPointer. 234

Summary. 234




Introduction to Enterprise Programming

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 JavaServer 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 Extensible Markup Language (XML), 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.

Please note that each subject is voluminous and by itself the subject of entire books, so this book 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).

Prerequisites

This book assumes you have read (and understood most of) Thinking in Java, 3rd Edition (Prentice-Hall, 2003, available for download at www.MindView.net).

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.


Network programming with Sockets & Channels

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

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.

2.      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[1] (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.

// {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.

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.

// {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.

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.

// {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:

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.

// {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.

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.

// {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.

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.

Selector Based Multiplexing in JDK1.4

When you are reading or writing to a socket, you need to make the transfer of data efficient. Let us consider the write operation first. When you write data at the application layer (TCP or UDP socket), you are writing data into the operating system buffer. This data eventually forms the payload of a (TCP or UDP) packet that needs to be transferred to the destination machine through the network. When you write to the socket and if there isn't enough room available in the buffer, the write call will block. If you read from a socket and there isn't enough information to read from the operating system buffer that has the data received from the network, the read call will block. If a thread blocks on a read or write operation, that thread is unable to do anything else and may be slowing the performance of your program. Prior to JDK1.4 there was no way to break this thread from the blocked state. With channels you can perform an asynchronous close operation on the channel and the thread blocked on this channel recieves the AsynchronousCloseException.

The asynchronous IO in Java achieves the same thing as achieved by the select() call on unix systems. You can give a list of descriptors (readable or writable) to the select() function and it monitors these descriptors for some change of event. For a descriptor representing a socket you are reading from, data in the operating systems buffer for this descriptor represents event. For a descriptor representing a socket you are writing to, availability of space to write in the internal operating system buffer for this socket represents an event. Hence the select() call watches multiple descriptors to check for events.

What if you just read and write to descriptors whenever you want? Select can handle multiple descriptors thus allowing you to monitor multiple sockets. Consider an example of a chat server where the server has a connection to various clients. The type of data arriving at the server is intermittent. The server is suppose to read data from the sockets and flash it on a GUI that is shown to every client - to achieve this you read data from every client and write this data to every other client. Consider 5 clients 1, 2, 3, 4, and 5. If the server was programmed to perform a read on 1, write on 2, 3, 4, and 5, next read on 2 and write on 1, 3, 4, 5 and so on, then it may so happen that while the server thread is blocked on read on one of the client sockets, there may be data available from other sockets. One solution would be to allocate a different thread for each of the clients (pre JDK1.4). But this would not be scalable. Instead you can have a selector based mechanism which watches all the client sockets. It knows which socket has data that can be read without blocking. But if a single server thread does all this work (selection and write on each client) it would not be responsive. Hence in such a situation one thread monitors the sockets for read, selects which socket can be read, and delegates other responsibility (writing to other clients) to another thread(s) or a thread pool.

This pattern is called the reactor pattern where events are decoupled from the action associated with events (Pattern Oriented Software Architecture - Doug Schmidt).

In JDK 1.4 you create a channel, register a Selector object with the channel that will watch the channel for events. Many channels register with the same Selector object. A single thread that calls the Selector.select(), watches multiple channels. The classes ServerSocket, Socket and DatagramSocket, each have a getChannel() method but it returns null except if a channel was created using the open() call (DatagramChannel.open(), SocketChannel.open(), ServerSocketChannel.open()). You then need to associate a socket with this channel.

You multiplex several channels (hence sockets) using a Selector. The static call Selector.select() blocks for an event to occur on one of the channels. There is also a non blocking version to this method that takes, the number of milliseconds to sleep or block before it returns.

ByteBuffer is used to copy data from and into a channel. ByteBuffer is a stream of octets and you have to decode this stream as characters. At the client end in MultiJabberClient.java this was done using Writer and OuputStreamWriter classes. These classes converted the characters into a stream of bytes.

The program below NonBlockingIO.java explains how you can used Selector and Channel to do the multiplexing. This program needs Server running. It causes an exception at server, but its aim is not to communicate with the server but to show how select() works.

//: TIEJ:X1:NonBlockingIO.java
// Socket and selector configuration for non-blocking
// Connects to JabberServer.java
// {RunByHand}
import java.net.*;
import java.nio.channels.*;
import java.util.*;
import java.io.*;
/**
 * Aim: Shows how to use selector. No reading/writing
 *      just shows the readiness of operations.
 *
 * PseudoCode:
 * -> Create a selector.
 * -> Create a channel
 * -> Bind the socket associated with this channel to a
 *    <client-port>
 * -> Configure the channel as non-blocking
 * -> Register the channel with selector.
 * -> Invoke select() so that it blocks until registered
 *    channel is ready. (as opposed to select(long timeout)
 * -> Get the set of keys whose underlying channel is ready
 *    for the operation they showed interest when they
 *    registered with the selector.
 * -> Iterate through the keys.
 * -> For every key check if the underlying channel is ready
 *    for the operation it is interested in.
 * -> If ready print message of readiness.
 *
 * Notes:
 * -> Need MultiJabberServer running on the local machine.
 *    You run it to connect to the local MultiJabberServer
 * -> It causes and exception at MultiJabberServer but
 *    this exception is expected.
 */
public class NonBlockingIO {
  public static void main(String[] args)
    throws IOException {
    if(args.length < 2) {
      System.out.println(
        "Usage: java <client port> <local server port>");
      System.exit(1);
    }
    int cPort = Integer.parseInt(args[0]);
    int sPort = Integer.parseInt(args[1]);
    SocketChannel ch = SocketChannel.open();
    Selector sel = sel = Selector.open();
    try {
      ch.socket().bind(new InetSocketAddress(cPort));
      ch.configureBlocking(false);
      // channel interested in performing read/write/connect
      ch.register(sel, SelectionKey.OP_READ
        | SelectionKey.OP_WRITE | SelectionKey.OP_CONNECT);
      // Unblocks when ready to read/write/connect
      sel.select();
      // Keys whose underlying channel is ready, the
      // operation this channel is interested in can be
      // performed without blocking.
      Iterator it = sel.selectedKeys().iterator();
      while(it.hasNext()) {
        SelectionKey key = (SelectionKey)it.next();
        it.remove();
        // Is underlying channel of key ready to connect?
     // if((key.readyOps() & SelectionKey.OP_CONNECT) != 0) {
        if(key.isConnectable()) {
          InetAddress ad = InetAddress.getLocalHost();
          System.out.println("Connect will not block");
          //You must check the return value of connect to make
          //sure that it has connected. This call being
          //non-blocking may return without connecting when
          //there is no server where you are trying to connect
          //Hence you call finishConnect() that finishes the
          //connect operation.
          if(!ch.connect(new InetSocketAddress(ad, sPort)))
            ch.finishConnect();
        }
        // Is underlying channel of key ready to read?
        // if((key.readyOps() & SelectionKey.OP_READ) != 0)
        if(key.isReadable())
          System.out.println("Read will not block");
        // Is underlying channel of key ready to write?
        // if((key.readyOps() & SelectionKey.OP_WRITE) != 0)
        if(key.isWritable())
          System.out.println("Write will not block");
      }
    } finally {
      ch.close();
      sel.close();
    }
  }
} ///:~

 

As mentioned above you need to create a channel using the open() call. SocketChannel.open() creates a channel. Since it extends from AbstractSelectableChannel (DatagramChannel and SocketChannel) it has the functionality for registering itself to a selector. The register call does this. It takes as an argument the Selector with to register the channel with and the events that are of interest to this channel. Here the SocketChannel is shown to be interested in connect, read and write - hence SelectionKey.OP_CONNECT,  SelectionKey.OP_READ and SelectionKey.OP_WRITE are specified in the register call while registering the channel with the Selector.

The static call Selector.select() watches all the channels that are registered with it for the events they registered for (second argument to register). You can have a channel interested in more than one event.

The next is an example that works like the JabberClient1.java but uses Selector.

//: TIEJ:X1:JabberClient1.java
// Very simple client that just sends lines to the server
// and reads lines that the server sends.
// {RunByHand}
import java.net.*;
import java.util.*;
import java.io.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;

public class JabberClient1 {
  public static void main(String[] args)
      throws IOException {
    if(args.length < 1) {
      System.out.println(
        "Usage: java JabberClient1 <client-port>");
      System.exit(1);
    }
    int clPrt = Integer.parseInt(args[0]);
    SocketChannel sc = SocketChannel.open();
    Selector sel = Selector.open();
    try {
      sc.configureBlocking(false);
      sc.socket().bind(new InetSocketAddress(clPrt));
      sc.register(sel, SelectionKey.OP_READ |
        SelectionKey.OP_WRITE  | SelectionKey.OP_CONNECT);
      int i = 0;
      // Because of the asynchronous nature you do not know
      // when reading and writing is done, hence you need to
      // keep track of this, boolean written is used to
      // alternate between read and write. Whatever is written
      // is echoed and should be read.
      // boolean done is used to check when to break out of
      // the loop
      boolean written = false, done = false;
      //JabberServer.java to which this client connects writes
      //using BufferedWriter.println(). This method performs
      //encoding according to the defualt charset
      String encoding = System.getProperty("file.encoding");
      Charset cs = Charset.forName(encoding);
      ByteBuffer buf = ByteBuffer.allocate(16);
      while(!done) {
        sel.select();
        Iterator it = sel.selectedKeys().iterator();
        while(it.hasNext()) {
          SelectionKey key = (SelectionKey)it.next();
          it.remove();
          sc = (SocketChannel)key.channel();
          if(key.isConnectable() && !sc.isConnected()) {
            InetAddress addr = InetAddress.getByName(null);
            boolean success = sc.connect(
              new InetSocketAddress(addr,
                JabberServer.PORT));
            if(!success) sc.finishConnect();
          }
          if(key.isReadable() && written) {
            if(sc.read((ByteBuffer)buf.clear()) > 0) {
              written = false;
              String response = cs.decode(
                (ByteBuffer) buf.flip()).toString();
              System.out.print(response);
              if(response.indexOf("END") != -1) done = true;
            }
          }
          if(key.isWritable() && !written) {
            if(i < 10) sc.write(ByteBuffer.wrap(
                new String("howdy "+ i + '/n').getBytes()));
            else if(i == 10) sc.write(ByteBuffer.wrap(
                new String("END/n").getBytes()));
            written = true;
            i++;
          }
        }
      }
    } finally {
      sc.close();
      sel.close();
    }
  }
} ///:~

 

The next example below shows a simple selector based mechanism for the MultiJabberServer discussed earlier. This server works the same way as the old one did but is more efficient in that it does not need a separate thread to handle each client.

//: TIEJ:X1:MultiJabberServer1.java
// Has the same semantics as multi-threaded
// MultiJabberServer
// {RunByHand}
import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.util.*;
/**
 * The Server accepts connections in non-blocking fashion.
 * A connection when established, a socket is created,
 * which is registered for read/write with the selector.
 * Reading/Writing is performed on this socket when the
 * selector unblocks.
 * This program works exactly the same way as MultiJabberServer.
 */
public class MultiJabberServer1 {
  public static final int PORT = 8080;
  public static void main(String[] args)
      throws IOException {
    //Channel read from data will be in ByteBuffer form
    //written by PrintWriter.println(). Decoding of this
    //byte stream requires character set of default encoding.
    String encoding = System.getProperty("file.encoding");
    //Had to initialized here since we do not wish to create
    //a new instance of Charset everytime it is required
    //Charset cs = Charset.forName(
    //  System.getProperty("file.encoding"));
    Charset cs = Charset.forName(encoding);
    ByteBuffer buffer = ByteBuffer.allocate(16);
    SocketChannel ch = null;
    ServerSocketChannel ssc = ServerSocketChannel.open();
    Selector sel = Selector.open();
    try {
      ssc.configureBlocking(false);
      //Local address on which it will listen for connections
      //Note: Socket.getChannel() returns null unless a channel
      //is associated with it as shown below.
      //i.e the expression (ssc.socket().getChannel() != null) is true
      ssc.socket().bind(new InetSocketAddress(PORT));
      // Channel is interested in OP_ACCEPT events
      SelectionKey key =
        ssc.register(sel, SelectionKey.OP_ACCEPT);
      System.out.println("Server on port: " + PORT);
      while(true) {
        sel.select();
        Iterator it = sel.selectedKeys().iterator();
        while(it.hasNext()) {
          SelectionKey skey = (SelectionKey)it.next();
          it.remove();
          if(skey.isAcceptable()) {
            ch = ssc.accept();
            System.out.println(
              "Accepted connection from:" + ch.socket());
            ch.configureBlocking(false);
            ch.register(sel, SelectionKey.OP_READ);
          } else {
            // Note no check performed if the channel
            // is writable or readable - to keep it simple
            ch = (SocketChannel)skey.channel();
            ch.read(buffer);
            CharBuffer cb = cs.decode(
              (ByteBuffer)buffer.flip());
            String response = cb.toString();
            System.out.print("Echoing : " + response);
            ch.write((ByteBuffer)buffer.rewind());
            if(response.indexOf("END") != -1) ch.close();
            buffer.clear();
          }
        }
      }
    } finally {
      if(ch != null) ch.close();
      ssc.close();
      sel.close();
    }
  }
} ///:~

 

Here is a simple implementation of a Thread Pool.  There are no polling (busy-wait) threads in this implementation. It is completely based on wait() and notify().

//: TIEJ:X1:Worker.java
// Instances of Worker are pooled in threadpool
// {Clean: WorkerErr.log, WorkerErr.log.lck}
// {RunByHand}
import java.io.*;
import java.util.logging.*;
public class Worker extends Thread {
  public static final Logger logger =
    Logger.getLogger("Worker");
  private String workerId;
  private Runnable task;
  // Needs a reference of threadpool in which it exists so
  // that it can add itself to this threadpool when done.
  private ThreadPool threadpool;
  static {
    try {
      logger.setUseParentHandlers(false);
      FileHandler ferr = new FileHandler("WorkerErr.log");
      ferr.setFormatter(new SimpleFormatter());
      logger.addHandler(ferr);
    } catch(IOException e) {
      System.out.println("Logger not initialized..");
    }
  }
  public Worker(String id, ThreadPool pool) {
    workerId = id;
    threadpool = pool;
    start();
  }
  //ThreadPool when schedules a task uses this method
  //to delegate task to a Worker thread. In addition to setting
  //the task (of type Runnable) it also triggers the waiting
  //run() method to start executing the task.
  public void setTask(Runnable t) {
    task = t;
    synchronized(this) {
      notify();
    }
  }
  public void run() {
    try {
      while(!threadpool.isStopped()) {
        synchronized(this) {
          if(task != null) {
            try {
              task.run(); // run the task
            } catch(Exception e) {
              logger.log(Level.SEVERE,
                "Exception in source Runnable task", e);
            }
            // return itself to threadpool
            threadpool.putWorker(this);
          }
          wait();
        }
      }
      System.out.println(this + " Stopped");
    } catch(InterruptedException e) {
      throw new RuntimeException(e);
    }
  }
  public String toString() {
    return "Worker : " + workerId;
  }
} ///:~

 

This the basic algorithm:
while true:

1.check the queue of tasks

2.if empty, wait on this queue till a task gets added

(addTask() call that adds a task will notify this queue to unblock)

3.Try to get a worker thread from the thread pool.

4.If none is available, wait on the thread pool.
     (When there is a free thread available notify this threadpool to unblock)

5.At this point there is a task in the queue and also a free worker thread

6.Delegate task from the queue to a worker thread.

end while:

//: TIEJ:X1:ThreadPool.java

// Thread that polls and executes tasks in pool.

// {RunByHand}

import java.util.*;

public class ThreadPool extends Thread {

  private static final int DEFAULT_NUM_WORKERS = 5;

  private LinkedList

    workerPool = new LinkedList(),

    taskList = new LinkedList();

  private boolean stopped = false;

  public ThreadPool() {

    this(DEFAULT_NUM_WORKERS);

  }

  public ThreadPool(int numOfWorkers) {

    for(int i = 0; i < numOfWorkers; i++)

      workerPool.add(new Worker("" + i, this));

    start();

  }

  public void run() {

    try {

      while(!stopped) {

        if(taskList.isEmpty()) {

          synchronized(taskQueue) {

            // If queue is empty, wait for tasks to be added

            taskList.wait();

          }

        } else if(workerPool.isEmpty()) {

          synchronized(workerPool) {

            // If no worker threads available, wait till

            // one is available

            workerPool.wait();

          }

        }

        // Run the next task from the tasks scheduled

        getWorker().setTask(

          (Runnable)taskList.removeLast());

      }

    } catch(InterruptedException e) {

      throw new RuntimeException(e);

    }

  }

  public void addTask(Runnable task) {

    taskList.addFirst(task);

    synchronized(taskList) {

      taskList.notify(); // If new task added, notify

    }

  }

  public void putWorker(Worker worker) {

    workerPool.addFirst(worker);

    //There may be cases when you have a pool of 5 threads

    //and the requirement exceeds this. That is when a Worker is required

    //but none is available (or free), it just blocks on threadpool.

    //This is the event that there is now a free Worker thread in

    //threadpool. Hence this thread does a notification that unblocks

    //the ThreadPool thread waiting on threadpool

    synchronized(workerPool) {

      workerPool.notify();

    }

  }

  private Worker getWorker() {

    return (Worker)workerPool.removeLast();

  }

  public boolean isStopped() {

    return stopped;   

  }

  public void stopThreads() {

    stopped = true;

    Iterator it = workerPool.iterator();

    while(it.hasNext()) {

      Worker w = (Worker)it.next();

      synchronized(w) {

        w.notify();

      }

    }

  } // Junit test

  public void testThreadPool() {

    ThreadPool tp = new ThreadPool();

    for(int i = 0; i < 10; i++) {

      tp.addTask(new Runnable() {

        public void run() {

          System.out.println("A");

        }

      });

    }   

    tp.stopThreads();

  }

} ///:~

 

Next is MultiJabberServer2.java that uses threadpool. This is the Reactor pattern. As stated above,  the events are decoupled from their associated actions. The threadpool asynchronously decouples the actions associated with the events. In an enterprise system this decoupling is typically achieved using Java Messaging System.(JMS)

//: TIEJ:X1:MultiJabberServer2.java

// Same semantics as MultiJabberServer1 using thread pooling.

// {RunByHand}

import java.io.*;

import java.net.*;

import java.nio.*;

import java.nio.channels.*;

import java.nio.charset.*;

import java.util.*;

 

class ServeOneJabber implements Runnable {

  private SocketChannel channel;

  private Selector sel;

  public ServeOneJabber(SocketChannel ch)

    throws IOException {

    channel = ch;

    sel = Selector.open();

  }

  public void run() {  

    ByteBuffer buffer = ByteBuffer.allocate(16);

    boolean read = false, done = false;

    String response = null;

    try {

      channel.register(sel, SelectionKey.OP_READ |

        SelectionKey.OP_WRITE);

      while(!done) {

        sel.select();

        Iterator it = sel.selectedKeys().iterator();

        while(it.hasNext()) {

          SelectionKey key = (SelectionKey) it.next();

          it.remove();

          if(key.isReadable() && !read) {

            if(channel.read(buffer) > 0) read = true;

            CharBuffer cb = MultiJabberServer2.CS.decode(

              (ByteBuffer)buffer.flip());

            response = cb.toString();

          }

          if(key.isWritable() && read) {

            System.out.print("Echoing : " + response);

            channel.write((ByteBuffer)buffer.rewind());

            if(response.indexOf("END") != -1) done = true;

            buffer.clear();

            read = false;

          }

        }

      }

    } catch(IOException e) {

      // will be caught by Worker.java and logged.

      // Need to throw runtime exception since we cannot

      // keep it as IOException

      throw new RuntimeException(e);

    } finally {

      try {

        channel.close();

      } catch(IOException e) {

        System.out.println("Channel not closed.");

        // Throw it so that worker thread can log it.

        throw new RuntimeException(e);

      }

    }

  }

}

 

public class MultiJabberServer2 {

  public static final int PORT = 8080;

  private static String encoding =

    System.getProperty("file.encoding");

  public static final Charset CS =

    Charset.forName(encoding);

  // Make thread pool with 20 Worker threads.

  private static ThreadPool pool = new ThreadPool(20);

  public static void main(String[] args)

      throws IOException {

    ServerSocketChannel ssc = ServerSocketChannel.open();

    Selector sel = Selector.open();

    try {

      ssc.configureBlocking(false);

      ssc.socket().bind(new InetSocketAddress(PORT));

      SelectionKey key =

        ssc.register(sel, SelectionKey.OP_ACCEPT);

      System.out.println("Server on port: " + PORT);

      while(true) {

        sel.select();

        Iterator it = sel.selectedKeys().iterator();

        while(it.hasNext()) {

          SelectionKey skey = (SelectionKey) it.next();

          it.remove();

          if(skey.isAcceptable()) {

            SocketChannel channel = ssc.accept();

            System.out.println("Accepted connection from:" +

              channel.socket());

            channel.configureBlocking(false);

            // Decouple event and associated action

            pool.addTask(new ServeOneJabber(channel));

          }

        }

      }

    } finally {

      ssc.close();

      sel.close();

    }

  }

} ///:~

 

This is a minor update to JabberServer.java. Initially when a client sent 'END' JabberServer did not echo it. This version, JabberServer echoes the string 'END'. This change was made to make JabberClient1.java simpler.

//: TIEJ:X1: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:

        BufferedWriter out =

            new BufferedWriter(

              new OutputStreamWriter(

                socket.getOutputStream()));

        while(true) { 

          String str = in.readLine();         

          System.out.println("Echoing: " + str);         

          out.write(str, 0, str.length());

          out.newLine();

          out.flush();

          if(str.equals("END")) break;

        }

      // Always close the two sockets...

      } finally {

        System.out.println("closing...");

        socket.close();

      }

    } finally {

      s.close();

    }

  }

} ///:~

 

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

Exercises

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

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

1.   Modify the server in Exercise 2 so that it uses multithreading to handle multiple clients.

2.   Modify JabberClient.java so that output flushing doesn’t occur and observe the effect.

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

4.   Starting with ShowHTML.java, create an applet that is a password-protected gateway to a particular portion of your Web site.


Remote Method Invocation (RMI)

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.

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.

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

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

·         The remote interface must extend the interface java.rmi.Remote.

·         Each method in the remote interface must declare java.rmi.RemoteException in its throws clause in addition to any application-specific exceptions.

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

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

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.

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.

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: 

·       Create and install a security manager that supports RMI. The only one available for RMI as part of the Java distribution is RMISecurityManager. 

·       Create one or more instances of a remote object. Here, you can see the creation of the PerfectTime object. 

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

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:

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:

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:[2]

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. 

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. 

With all this in mi

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值