Базы данныхИнтернетКомпьютерыОперационные системыПрограммированиеСетиСвязьРазное
Поиск по сайту:
Подпишись на рассылку:

Назад в раздел

Chapter 12 -- Network Programming with Java

Chapter 12

Network Programming with Java


CONTENTS


Because you're reading this book, you probably don't need to be told what the Internet is. On the other hand, it's easy to use the World Wide Web and various high-level features of the Internet and never think about the amazing kinds of hardware and protocol standards that are humming away efficiently behind the scenes. You could fill a library with all the books and papers that have been written about how the Internet works, but luckily you don't have to. Java programs typically access network resources at a very high level of abstraction, and the gory details are largely hidden from view.

The java.net package contains the classes that make network programming so easy in Java. These classes are described in Chapter 3, "An Introduction to Java Classes," and in the Java API. The examples in this chapter make heavy use of them.

The Protocol Zoo

The Internet started out around 1969 as a kind of science fair project by the U.S. military, and it was known as the ARPANET. That network no longer exists, but various protocols that were invented for it are still in use. These protocols allow a collection of physical networks to link up over gateways and become a sort of internetwork, or Internet. These physical networks generally are Ethernet LANs, dial-up lines, and so on. The Internet is a logical network cobbled together out of various physical networks that cooperate so they appear to the user as one large network.

This vague definition leaves undefined the word cooperate, which is where things get slightly complicated. The various protocols that implement this cooperation have acronymic titles such as IP (Internet Protocol), TCP (Transfer Control Protocol), UDP (User Datagram Protocol), ICMP (Internet Control Message Protocol), ARP (Address Resolution Protocol), and so on. You certainly can tell it's a defense department project!

As a user of the Internet, you need only a basic understanding of what IP, TCP, and UDP can do for you, as well as a nodding acquaintance with DNS domain names. The other low-level protocols are of interest mainly to network administrators, hackers, and generally anybody with just too much time on their hands.

IP

The Internet is the Frankenstein monster of packet-switching networks. Packet switching means that data is sent down a wire (or other medium) in little packets (or datagrams) with a destination address and control fields slapped on the front. It's sort of like the postal system; you have to address your letters correctly. You've probably seen IP addresses hanging around on the Internet, looking like 123.45.67.89 or something similar (this is called dotted quad notation). These addresses are 32-bit numbers that identify a particular network device on the Internet (your Ethernet card, for example), via a mapping known as Address Resolution Protocol (ARP). The Internet Protocol (IP) defines the basics of how datagrams are addressed and routed, as well as who receives or forwards them.

Whatever you might think of the post office, it at least tries to get your letter delivered reliably. The IP protocol makes no such guarantee. Nor can it; the underlying hardware is intentionally made to be occasionally unreliable in the interests of speed and economy of design. Packets can be dropped (intentionally!) without notice; they may arrive out of sequence or with their data garbled.

Techno-geeks sum this up by saying that IP lives on the network layer. In a conceptual tower of ISO networking layers, this is the second floor; it assumes only that there is a way of sending packets down the wire to a specific destination. It's up to the higher layers to provide as little or as much error control and recovery as necessary.

UDP

The next level up from the network layer is called the transport layer. This layer defines end-to-end communications in two flavors: UDP and TCP. An endpoint for communication is defined by a socket, which is the conjunction of an IP address and a port number. A port number is a positive integer that identifies a logical "port" on your machine. These ports are not physical entities; they simply distinguish data arriving over a single physical connection so that it can be delivered to any of hundreds of different "sockets" owned by the various applications on the machine. This provides for a virtually limitless number of end-to-end connections through a single network interface.

The User Datagram Protocol (UDP) lets you send datagrams of your own to another host and port number. Essentially, you are just using IP, but with the addition of port numbers and with the various details of IP headers and such hidden from view. Just as with IP, there is no guarantee of delivery, error detection, or sequencing of datagrams.

If you want to use UDP from Java, the java.net.DatagramSocket and java.net.DatagramPacket classes are what you need. Examples of their use follow.

TCP

The Transfer Control Protocol (TCP) slaps a reliable and sequenced connection on top of the unreliable, unsequenced functionality of IP. This is why you often will see the whole setup referred to as TCP/IP. A TCP socket is again the conjunction of an IP address with a port number. The data you send over a TCP socket is divvied up into datagrams and stamped with sequence numbers and error-detection codes. At the receiving end, the packets are checked for errors and then lined up in sequence order. Protocol Packets are resent as necessary, so that the whole stream of bytes arrives intact at the receiving end. It's kind of like registered mail.

Because of the reliable and sequenced nature of TCP sockets, they often are called stream sockets; you can read and write data in continuous streams of bytes without worrying about packets, headers, and so on. Because streams figure so prominently in the java.io classes, it's logical to suppose that TCP sockets are a natural in Java-and they are! Stream socket functionality in Java is provided by the classes java.net.ServerSocket and java.net.Socket.

DNS

Not really a protocol, but more like an entire subsystem of the Internet, the Domain Name Service (DNS) makes it easier on the carbon unit peripherals (humans!) when it comes to remembering IP addresses. DNS is a collection of programs and protocols that allow a central authority to assign symbolic names to Internet hosts based on a hierarchy of domains and subdomains. It's a huge, distributed database of nicknames-one for every Internet host registered in a domain. Suppose that you work at Widgets Galore Company, and your machine has the nickname wallaby. Your DNS name will look like this:

wallaby.WidgetsGalore.com

The .com ending signifies a commercial institution. Reading from right to left, you move from the general to the specific in your DNS hierarchy: not just any commercial interest, but one called WidgetsGalore. Not any machine at Widgets Galore, but the one known as wallaby. The left-most word is always the local host name for the machine in question. The remaining words specify the domain of that machine. There may be other machines in the world named wallaby, but not in your domain.

The name servers and resolver libraries on the various hosts in your domain know how to answer queries about wallaby; they return the actual IP address of your machine-a 32-bit number. From the user's point of view, this all happens more or less automatically, which lets us poor humans forget about memorizing 32-bit IP addresses and get on with memorizing important stuff, like 20-bit phone numbers.

In Java, IP addresses are encapsulated in the class java.net.InetAddress, whether they are given as symbolic DNS names or 32-bit numbers in dotted quad form.

World Wide Web

For many people, the World Wide Web is the Internet. It certainly is the most painless and fun way to be a virtual tourist, zooming around looking at pictures of people you've never met and places you'd like to visit. It's also full of ways to search for information on any topic whatsoever. Most importantly for Java enthusiasts, the WWW is the natural habitat for Java applets.

Applets are embedded in Web pages using the APPLET tag. When you tell your Web browser to load a certain Web page, it opens a socket connection to a Web server specified in the URL of the page. (A URL is a Uniform Resource Locator-the address of a resource on the Web. For a general discussion of URLs, see java.net.URL in Chapter 3.) The Web server then writes the contents of the page over the socket connection. Your browser decodes the contents and then displays them according to their type. If your browser understands applets, it reads the APPLET tag to figure out what additional data it must request from the Web server. This data consists of the Java class files defining your applet, which your browser then must load and execute. A Java-compatible Web browser knows how to be a Java virtual machine.

The language of the Web is HyperText Transfer Protocol (HTTP). The browser sends HTTP requests, such as GET and POST, to the server, and the server responds with an HTTP header and body. The header defines (among other things) the content type and content encoding of the body. There are many standard types and encodings, which are known as MIME types. The MIME type for standard Web pages is text/html, which also is known as HyperText Markup Language (html). I'm not going to talk much about html because you know all about that anyway.

Java allows for flexible interaction with Web servers, including the GET and POST methods of HTTP. But the Web is more than hypertext alone; a URL can specify any one of a number of different protocols to be used for fetching a resource. These include FTP, Gopher, NNTP (Usenet news), and so on. Better yet, you can teach Java about these or any protocol because the way in which java.net.URL objects interpret their contents is fully extensible, via the classes java.net.URLStreamHandler and java.net.ContentHandler (and other related classes).

Sockets in Java

To open a socket in Java, you specify an IP address and a port number. IP addresses in Java are represented by the class java.net.InetAddress, which doesn't care whether you want to specify the dotted quad or the DNS name of a host. You can get an InetAddress for the fictional machine wallaby.WidgetsGalore.com, for example, by using the static method getByName():

InetAddress wally = InetAddress.getByName("wallaby.WidgetsGalore.com");

On the other hand, you could give the string in dotted quad form:

InetAddress wally = InetAddress.getByName("123.45.67.89");

It really doesn't matter. To get the address of the local host (the machine you're running on), you can pass the null reference to getByName(), or you can use InetAddress.getLocalHost(). These methods throw an UnknownHostException if (you guessed it!) the host is not known to DNS. An InetAddress object has a constant value; you can't change it, so just throw it away when you're done with it.

An InetAddress alone does not make a socket. You must have a port number. In a typically asymmetric client/server application, only the server side needs to worry about which port number it uses, because only the server side needs to be found at a well-known socket location. So, you can specify a port number or use the magic port number 0, which means, give me any available port. If you ask for a port that is already in use, you trigger an exception. Users generally should avoid using ports 1 through 1024 because these are reserved for system-based services.

Stream Socket Classes

The handiest sockets for use in Java are TCP stream sockets. For this reason, they receive prime consideration in Java's naming scheme, and they simply are called java.net.Socket. A Socket's whole raison d'etre is to be connected to another Socket, so think about how this happens. In order to get a connection happening between two machines, one of them has to ask first, or initiate the connection. This machine is playing the role of client, and the one who answers is a server (these distinctions are a bit arbitrary-after the connection is established, it is symmetrical).

ServerSocket

If the client asks for a connection, somebody had better be listening on the server end. This somebody is a java.net.ServerSocket-an object that creates a passive socket on your local host and then sits and listens to a specific port. Listing 12.1 is a fragment containing a very minimal server class, which accepts connections and then does nothing with them. The main() method calls the handle_connection() method to do any actual work with the socket.


Listing 12.1. A minimal server.
public class SillyServer
{
  // A bare-bones example: exception handling omitted!

  public static void main( String args[] )
  {
    ServerSocket serv;
    serv = new ServerSocket( 8081 );
    while ( true )  {
      Socket s;
      s = serv.accept();          // Wait for a connection
      handle_connection( s );     // Got one, now do something!
      s.close();
    }
  }
}

This code fragment just listens forever on port number 8081 until a connection is requested and then calls the method handle_connection() to deal with the new connection (the name handle_connection() is arbitrary here; it serves to describe a place to put your application-specific code). The accept() method blocks forever until a connection is received, at which point it returns a new Socket object representing the connection; you should close the Socket when you're done with it. After accepting a connection, the server socket returns to its listening state, and you can accept() further connections on the same ServerSocket as often as you want.

The infinite looping behavior in this server program is typical. After all, you want servers to hang around and service any number of requests. In case another request for a connection arrives before you have a chance to accept it, it is kept waiting in a queue of pending connections. You can specify the length of this queue by an optional second argument to the ServerSocket constructor. It typically defaults to five. When the queue is full of unserviced connections, further connections are refused. In practice, though, handling requests in a single thread is not a good idea unless each connection can be handled very quickly. Most real-life server programs should start a new thread for each connection accepted.

A connected Socket always knows the address and port of the remote socket it's connected to; these are made available by the Socket methods getInetAddress() and getPort(). This allows your server to reject certain connections out of hand; just close the Socket if you don't like the client's address.

Socket

On the client's end, you need to ask the server for a connection. This is done with a java.net.Socket object, which you construct yourself, as shown in Listing 12.2.


Listing 12.2. A minimal client.
import java.io.*;
import java.net.*;
public class SillyClient
{
  Socket sock;
  DataInputStream in;
  DataOutputStream out;

  public SillyClient( String server, int port ) throws IOException
  {
    // Create a socket connected to the server:
    sock = new Socket( server, port );

    // Okay, now attach some streams to it.
    in = new DataInputStream( sock.getInputStream() );
    out = new DataOutputStream( sock.getOutputStream() );
  }
}

Notice how the client needs to know the exact port number of the server's socket as well as the IP address. You can specify the address as a String or as an InetAddress instance. If the Socket constructor succeeds (throws no exception), you're connected! Also, you never specify which local port is to be used. For the client end, it simply doesn't matter beforehand which port you are using. After connecting, you can find out the local port number by calling getLocalPort().

These are supposed to be stream sockets, so there had better be a way to get a stream attached to them. The methods getInputStream() and getOutputStream() do just that. They return an object of class InputStream or OutputStream, which allows you to read or write using the given socket. A socket can have a stream of either type or both attached to it; sockets provide a two-way flow of information. When you're done with the stream, it's good programming practice to close() it before you close the associated socket. If you use a form of BufferedOutputStream, you might want to explicitly flush() it on occasion; it certainly never hurts to do so.

For security reasons, applets usually are allowed to connect only to sockets that live at the same IP address as their own class files. This prevents various antisocial behaviors, but it also puts a real crimp in what you can do from a typical Web browser environment. An applet can find out the InetAddress of its home machine by examining its getCodeBase() URL. But keep your applets portable-don't hard code the IP address!

These restrictions don't apply to stand-alone programs (unless your SecurityManager is quite paranoid).

A Basic Client/Server Applet

You'll now do a simple client/server transaction. The server will be a stand-alone program, listening on a specific port on the machine wallaby.WidgetsGalore.com, and the client will be an applet. The client calls up the server, and the server prints a short sales report back to the client. Then the transaction is complete. For this, you just need to flesh out the SillyServer class used earlier-call it SimpleServer, as shown in Listing 12.3.


Listing 12.3. A simple server program.
import java.io.*;
import java.net.*;
public class SimpleServer
{
  static final int DEFAULT_PORT       = 8081;

  static void handle_connection( Socket s ) throws IOException
  {
    PrintStream out = new PrintStream(s.getOutputStream());
    DataInputStream in = new DataInputStream(new FileInputStream( "sales.txt" ));
    String line;
    System.err.println( "Connection from " + s.getInetAddress() );
    while ( (line = in.readLine()) != null )
      out.println( line );
    in.close();
    out.close();
  }

  public static void main( String args[] )
  {
    int port;
    ServerSocket serv;

    if (args.length == 1)
      port = new Integer(args[0]).intValue();
    else
      port = DEFAULT_PORT;

    try  {
      serv = new ServerSocket( port );
    } catch ( IOException e ) {
      System.err.println( "I/O exception: " + e );
      return;
    }

    System.err.println( "listening on port " + port );

    while ( true )  {
      Socket s = null;
      try  {
        s = serv.accept();              // Wait for a connection
        handle_connection( s );         // Got one, now do something!
      } catch ( IOException e ) {
        System.err.println( "I/O exception: " + e );
      } finally  {
        if (s!=null) try { s.close(); } catch (IOException e) {}
      }
    }
  }
}

Notice that the server program still does everything sequentially in a single thread. You can get away with this only because the entire transaction lasts just a fraction of a second.

The corresponding applet, SimpleClient, has a button named load, which you click to open a socket and copy the data to a TextArea. Listing 12.4 shows the important parts of the applet code.


Listing 12.4. A simple client applet.
public class SimpleClient extends Applet
{
  String server;
  int port;
  Label title = new Label( "A Simple Client Applet", Label.CENTER );
  TextArea text = new TextArea();
  Button load = new Button( "Load Sales Report" );

  public void init()
  {
    // Initialize the applet ...
  }

  void loadSales()
  {
    Socket s = null;

    try  {
      s = new Socket( server, port );
      DataInputStream in = new DataInputStream(s.getInputStream());
      String line;
      while ( ( line = in.readLine() ) != null )
        text.appendText( line + "n" );
      in.close();
    } catch (IOException e)  {
      showStatus( "I/O Exception: " + e );
    } finally  {
      if (s!=null) try { s.close(); } catch (IOException e) {}
    }
  }

  public boolean action(Event e, Object o)
{
    if ( e.target == load )  {
      loadSales();
      return true;
    }
    return false;
  }
}

Now, on the server end, you must provide a file sales.txt, with any text you want in it. After the client-side user clicks the Load Sales Report button, the contents of that file are transferred over the socket and printed in the TextArea. Because applets often are disallowed from opening local files, this is a useful alternative.

In a more complicated application, you need to define a protocol for client/server interaction over the stream socket. The basics of how you connect and use the socket always remain the same, however. It is worth pointing out that for large client/server and distributed applications, packages are available to ease the task. One such package is called Remote Method Invocation (RMI), which provides a transparent way to call on the methods of a remote Java object. Later, I'll discuss another approach that is used in ChatApplet.java.

Datagram Sockets

Stream sockets provide a reliable connection to a fixed destination socket, but you pay for this convenience. A slight overhead is involved in checking for errors and correctly sequencing the packets. Moreover, for some applications, using stream sockets for every transmission would be prohibitively complex. Consider the example of a chat applet, which would be a member of a network of similar applets. These applets are to send messages typed by the user to all the members of the network. Using stream sockets, this requires each applet to open a socket for every other applet in the network, so in a network of N machines, there will be roughly N ¥ N sockets open, occupying just as many ports. This is quite wasteful and, in any case, the nature of a chat network is discrete; messages are dispatched in small quantities of text. For this purpose, datagrams are ideal. A datagram or UDP socket occupies one port but can send datagrams to any remote UDP socket. The datagram is sent immediately, and the sending thread does not block waiting for its delivery, regardless of whether anyone actually receives the packet. Short of using some kind of IP multicasting or broadcasting protocol, this is the most efficient way to reach a large number of peers with discrete update-type information.

Although user datagrams are not checked for data errors, you can choose to implement your own checksums. You also might want to have a mechanism for verifying that a datagram has arrived.

DatagramSocket

The java.net.DatagramSocket class represents a UDP socket on the local host. You can create it on a specific port:

DatagramSocket ds = new DatagramSocket( 9876 );

or on any available port:

DatagramSocket ds = new DatagramSocket();

After the socket is created, you can send and receive datagram packets using the send() and the receive() methods. The packets themselves are represented by java.net.DatagramPacket objects, which you can construct from an array of bytes and addressing information. The send() method requires the DatagramPacket to be fully constructed; it must be filled in with a correct data buffer, data length, remote InetAddress, and remote port. The receive() method requires only a data buffer and a maximum length to receive. The receive() method blocks until a datagram arrives and then fills in the buffer of the specified datagram. The actual length of the data received then is available via getLength(). For example, you can receive string data like this:

DatagramSocket sock = new DatagramSocket();
byte b[] = new byte[1000];
DatagramPacket p = new DatagramPacket( b, 1000 );
sock.receive( p ); // wait for a packet
byte data[] = p.getData();
int len = p.getLength();
String s = new String( data, 0, 0, len );

Similarly, data to be sent is constructed from an array of bytes. Suppose that you want to send a string. You can use this code:

String message;
InetAddress remoteaddr;
int remoteport;
DatagramSocket sock = new DatagramSocket();

// Set the message, remoteaddr and remoteport ...

byte b[] = new byte[ message.length() ];
message.getBytes( 0, m.length(), b, 0 );
DatagramPacket p = new DatagramPacket(b, m.length(), remoteaddr, remoteport);
try  {
  sock.send( p );
} catch (IOException e)  {
  // Uh-oh! Do something.
}

A Basic Datagram Application

This section looks at a very basic use of datagrams: the two-way chat program DatagramChat.java, as shown in Figure 12.1. The user is expected to enter the remote host name and port number, so this program is not really very user-friendly. However, this does avoid the complication of having a central hookup server, which otherwise would be necessary in order to let the chat windows find each other's sockets.

Figure 12.1 : A datagram chat applet.

After the user types a message in the input TextField and then clicks Send, the message is sent in a DatagramPacket, along with a leading byte with the value PRINT or EchO (in this case, PRINT). Listing 12.5 shows the code that the DatagramChat class uses to send a message datagram.


Listing 12.5. Sending a datagram in class DatagramChat.
void sendMessage()
{
  if ( remoteaddr==null )  {
    status.setText( "You must specify a remote host and port!" );
    return;
  }

  String m = input.getText().trim();
  if (m.length() < 1) return;

  byte b[] = new byte[ m.length()+1 ];
  b[0] = PRINT;
  m.getBytes( 0, m.length(), b, 1 );
  DatagramPacket p = new DatagramPacket(b, m.length()+1, remoteaddr, remoteport);

  try  {
    sock.send( p );
    status.setText( "Message sent to " + remoteaddr );
  } catch (IOException e)  {
    status.setText( "Message send failed: " + e.getMessage() );
  }
}

The same window must be able to receive messages from the remote peer. To accomplish this, it implements the Runnable interface and has its run() method execute in a separate Thread. All the run() method does is wait for messages and then respond-an infinite loop. The response to a message is to print it in a TextArea, and (if it is marked PRINT) send back the same message, now marked EchO. Messages marked EchO also are printed to the TextArea, but they are not returned to the sender. This means that the chat window supports remote echoing; if you don't see the message in the TextArea, chances are the remote user doesn't see it either. Listing 12.6 shows the run() method of the DatagramChat class.


Listing 12.6. The run() method of DatagramChat.
public void run()
{
  byte b[] = new byte[MTU];

  while ( sock != null )  {
    try  {
      DatagramPacket p = new DatagramPacket( b, MTU );
      sock.receive( p );

      byte data[] = p.getData();
      int len = p.getLength();
      byte op = data[0];
      String s = new String( data, 0, 1, len-1 );
      print( s );

      if ( op==PRINT )  {
        // Echo the packet back to the sender
        data[0] = EchO;
        DatagramPacket q  = new DatagramPacket(data, len, p.getAddress(),
        
Âp.getPort());
        sock.send(q);
      }
    } catch (IOException e) {}
  }
}

The troublesome thing about this program is that it is hard to run as an applet (it is provided as DatagramChatApplet.java). The Web browser usually will not permit you to receive datagrams in an applet (although my applet viewer lets me do this if I load the applet locally). Of course, it's possible to run this as a stand-alone application (java DatagramChat), but that defeats much of the purpose of having applets at all.

For this reason, chat applets in Java will have to use a stream socket connection to a server program running on their home server-the Web server where the applet's class files reside. This star-like configuration loads down the server and eats up several port numbers. Not only that-it requires a carefully designed protocol to make sure that client and server can accomplish essentially asynchronous tasks in a sequential conversation and still recover from unexpected conditions. Worse, it makes the response time unacceptably slow if chat groups of a few dozen applets or more exist. Nevertheless, any applet that wants to communicate with other applets using sockets has to live with these restrictions. The next section discusses one way to achieve this.

A Chat Applet

The program ChatApplet.java on the book's accompanying CD-ROM is a more realistic attempt at a chat applet (see Figure 12.2). It uses a stream socket to talk to a central server program (ChatServer.java), located on the same machine as the applet's code. The server program coordinates connections to several clients, grouping them into chat pools (different channels or rooms). The trans package provides a uniform way for client and server to communicate requests and commands to each other. The server's port number defaults to 8081, but any port number can be given as the single command-line argument. The applet itself is just a panel containing a ChatWindow, in which you can set the server name, port number, and initial screen name by parameters in the APPLET tag (server, port, and name). It won't be necessary to specify the server, because the applet figures out its home server by using the getCodeBase() method (don't forget to have the server running, though). The ChatApplet class contains a single instance of class ChatWindow, which does all the work. The ChatWindow class can also run as a stand-alone program.

Figure 12.2 : The ChatApplet.java applet.

The server program has most of the smarts about which chat pools are defined and who is in them. The client applets just act as a user interface to the server, which updates the pools at will. Ideally, the smarts would be distributed somewhat more democratically in a web of interconnected applets, but due to security restrictions (discussed earlier), you are forced into the star configuration. This limits the usefulness of the applet, because large numbers of clients will bring the server to a crawl, and because the system is not fault-tolerant.

A detailed discussion of the mechanics of these programs is really beyond the scope of this chapter, but this section will go over the basic idea of how the client/server transactions are performed.

The trans Package

The trans package used by ChatApplet.java and ChatServer.java provides an artificial kind of remote procedure call functionality. Transactions are sent back and forth over a stream socket in a semi-asynchronous way. The transactions are instances of a subclass of the abstract class Transaction. The socket is encapsulated in an object of type TChannel. This is a smart channel for initiating and responding to transactions. Either side can be the client (or initiator) of a transaction (depending only on the transaction type itself), so there is no essential asymmetry built in.

Each Transaction object exists only to be converted to and from its properties, which are pairs of name/value strings written in a TProps object, which is essentially a jazzed-up Hashtable with better read and write methods than the java.util.Properties class has. The exact class of the transaction determines its type, so the inheritance mechanism is used here basically as a way to discriminate among the different types of transactions while treating them on an equal footing as objects characterized by their properties. The properties are accessed via the getProperty() and setProperty() methods. On creation, the constructor sets the properties to reflect the transaction parameters. The transaction is sent by passing it to the initiate() method of an opened TChannel object. The ChatServer, for example, greets the client and asks for its screen name by initiating a transaction of class Hello, as Listing 12.7 shows.


Listing 12.7. How the ChatServer says hello.
class ClientManager extends TChannel
{
  // ...
}

class HandleClient extends Thread
{
  ClientManager cm;

  // ...

  try  {
    Hello h = new Hello();   // a Hello object has no initial props
    cm.initiate(h);          // start the transaction
    cm.name = h.name;        // this gets the result: a screen name
    // ...
  } catch ( IOException e ) {}
}

Initiating a transaction means that the following items are written to the stream socket:

  • An opcode byte (indicating REQ (request) or ACK (acknowledge)
  • A serial number (this serves to match replies to previous requests)
  • The name of the transaction class (here, it is Hello)
  • The properties of the transaction (a TProps object, written out)

The best way to write primitive Java types over a stream socket is to use a DataOutputStream attached to the socket (see Chapter 3 for a full discussion of data I/O). The opcode is written using writeByte(), the serial number with writeInt(), and the various strings with writeUTF(). The receiving socket then can read the data using a DataInputStream, in a parallel fashion.

The most important methods in a Transaction class are its build() and decode() methods. The build() method is run on the receiving end, and it accepts a single TProps object as an argument. These properties are the ones that were written on the socket at the initiating end, and the build() method's job is to take some action based on the values of these properties. The build() method can use setProperty() to set some return properties as well (on entry to build(), the receiver-side transaction's properties are guaranteed to be empty).

When the build() method finishes, the result properties are written back (even if they remain empty), with an opcode of ACK (acknowledge). It then is the job of the decode() method to make sense of these return properties on the initiating end. The result of a Hello transaction is a screen name, for example. This means that the build() and the decode() method each take half the responsibility for the encoding and decoding, whereas the constructor need not do anything at all. Listing 12.8 shows the complete code of the Hello class (here, ChatTransaction extends Transaction).


Listing 12.8. The Hello Transaction class.
import java.net.*;
import trans.TProps;

// A Hello transaction requests that the client identify itself.

public class Hello extends ChatTransaction
{
  static String NAME = "name";

  String name;

  public Hello() {}

  protected void build( TProps p )
  {
    setProperty( NAME, getWindow().screenname );
  }

  protected void decode()
  {
    name = getProperty( NAME );
  }
}

Even though the constructor is empty here, you still must declare it, if only to make it public. The trans package requires that all Transactions be public classes with a public constructor that takes no arguments (a null constructor). This is because the receiving TChannel actually instantiates a new transaction of the correct type by using the class descriptor's newInstance() method:

// This code instantiates a Transaction of class named in the String tClass:

try  {
    Class c = Class.forName( tClass );
    t = (Transaction)c.newInstance();
} catch (Exception ex)  {
    throw new TransactionException();
}

Transactions appear synchronous to the initiating thread; the initiate() method does not return until the transaction's return properties have been read and decoded. The same TChannel can act as both initiator and receiver simultaneously, however, and each received transaction has its build() method executed in a separate thread. If this build() method decides to initiate() another transaction back to the sender, you have a subtransaction, and that's okay, too. In this way, entire conversations of transactions and subtransactions can be carried out more or less transparently. The instigating thread doesn't see any of the subtransactions, because it is blocked, waiting on the outcome of the entire process. This multithreading requires that some care be taken in determining which methods should be synchronized and which should not, in order to avoid deadlock.

The downside of the approach is that there currently is no way to provide a time-out period for transactions to complete. Also, in the event of an error, there is no defined way to restore the TChannel to a sane state. It also would be nice to have a form of initiate() that returns immediately, executing the transaction in another thread and optionally notifying the caller by executing an agreed-upon method call when the transaction is completed (this is best done by defining an interface-for example, TransactionObserver). Performing these improvements would be fairly straightforward (after reading about multithreaded programs), so consider it an exercise!

The WWW in Java

The main reason behind the phenomenal rise of Java is its potential to become the lingua franca of interactive applications on the Internet. The exponential growth of the World Wide Web (WWW) has started a drive to redistribute the burden of computation, which has until now rested almost completely on the Web server. The CGI scripts so common today require no cleverness on the client end, but this simplicity comes at a price. The variety and semantics of CGI interaction are limited by the html form syntax and the strictures of HTTP, and a single server is forced to think on behalf of a potentially limitless number of clients. Java, on the other hand, is a real programming language that loads once and executes at the client end. This makes an enormous difference in terms of efficiency, both from the client's point of view (faster execution) and from the point of view of the network (lower bandwidth). So, although Java will soon be powering telephones and stereos, it is on the Web that Java finds its first natural home.

In this section, you'll see how Java programs can interact with Web servers and also with existing CGI programs. Java can convert URLs (Web addresses) into usable program objects. On a lower level, Java can read the raw contents of a URL. Java applets can also request certain actions of their host browser, such as displaying a status line or loading a new document.

The URL Class

A URL refers to a specific resource on the Web: an html file, image, animation, or whatever the case may be. URLs look like this:

http://www.WidgetsGalore.com/~MrWidget/sales.html

URLs specify a protocol (http), a server (www.WidgetsGalore.com), and a path to the resource in question. In Java, URLs are encapsulated in a constant object of class java.net.URL, which is described in Chapter 3.

An applet can use a URL to load a new browser page, as shown in this code:

class MyApplet extends Applet
{
  // Other stuff ...

  public void showNewPage( URL u )
  {
    getAppletContext().showDocument( u );
  }
}

If you use framesets in your document, there is also an optional second argument to specify the target frame or window.

Java URL objects have a getContent() method for converting the contents of the URL into a Java object, assuming that such a conversion is defined. A URL can reference an image, and then getContent() returns an Object whose runtime class is some subclass of Image, for example. The content type of most Web pages is text/html, which has no defined conversion. You can use the URL object to open an InputStream to read the contents of the URL, however:

URL url = new URL("http://www.WidgetsGalore.com/~MrWidget/home.html");
DataInputStream in = new DataInputStream(url.openStream());
String line;
while ( ( line = in.readLine() ) != null )
  System.out.println( line );
in.close();

When it is defined, the getContent() conversion is performed by a ContentHandler object. The following sections tell you how to define custom ContentHandlers for your own types of objects.

The URLConnection Class

The URL class may get a lot of credit, but the URLConnection class really does most of the work. A URLConnection object manages an interaction with a Web server at a given URL. When you call on a URL's getContent() method, the URL object creates a URLConnection to do the dirty work. You can retrieve the URLConnection associated with a URL by calling the openConnection() method of the URL. This can give you access to the values of the HTTP header fields, which tell you such things as the last modification date. It lets you specify details of the interaction, such as retrieval conditional on the last modification time (use setIfModifiedSince() or set the If-Modified-Since header field using setRequestProperty()).

A URLConnection object remains unconnected until its connect() method is called, which happens when some operation such as getContent() requires that a connection be made. Before it is connected, you are free to modify the fields that control the transaction. After it is connected, this is an error. URLConnections that use the HTTP protocol can perform the HTTP GET and POST methods because they are smart enough to wait for you to access their I/O streams before connecting to the Web server and issuing a GET or a POST request; basically, the POST method is used only if you actually write some output. You'll see how to do this soon.

The built-in functionality of URLConnections is centered on the HTTP protocol, but a URL object can use any protocol name that has an associated URLStreamHandler. A URLStreamHandler object must implement the abstract method openConnection(), which returns a URLConnection object given a URL. The URL knows where to get the stream handler for a given protocol by consulting the URLStreamHandlerFactory. You can define your own protocols by writing a class implementing the URLStreamHandlerFactory interface and then setting it to be the stream-handler factory using the static method URL.setStreamHandlerFactory(). In practice, though, you're almost always better off using HTTP and defining new content handlers corresponding to custom MIME types. The exception is if you're trying to write a reusable set of classes to perform FTP or some other such protocol that already exists.

Caution
Don't assume that your Java implementation supports a protocol type just because it is common; my implementation doesn't understand ftp or news (moreover, the file protocol completely ignores the server field and loads a local file regardless). This isn't all that surprising; for example, a URLStreamHandler for the news protocol would have to produce a URLConnection object that knows how to talk to a news server and serve up a new Java object corresponding to each news URL. This functionality is too specific to be part of a standard API.

MIME Types and Content Handlers

How does a resource sitting on a Web server get converted into a usable Java object? First, when the Web server returns the contents of the corresponding URL, it includes header fields indicating the MIME type and encoding of the content to follow. For a GIF image, the MIME type is identified by the string image/gif. For html, it is text/html. When a URL or URLConnection performs its getContent() method to create a new object from a URL, it needs to find a content handler for whatever MIME type the server returns. The java.net package automatically parses the MIME type from the appropriate HTTP header field and then passes it to a ContentHandlerFactory, which returns the required handler or null (or throws a runtime exception). If you want to know the MIME type explicitly, the getContentType() method is available in the class URLConnection.

A content handler is an object of class java.net.ContentHandler, with a method getContent(), which takes a URLConnection and returns an Object. A different content-handler class exists for each recognized MIME type, because they each must do different things with the data from the URL.

The correspondence between MIME types and ContentHandlers is established by the ContentHandlerFactory. A Java program has one ContentHandlerFactory: an object implementing the interface java.net.ContentHandlerFactory, which has been installed as the ContentHandlerFactory, as shown in this code:

URLConnection.setContentHandlerFactory( chf );

The ContentHandlerFactory interface specifies one method; the createContentHandler() method accepts a MIME type name string and returns a ContentHandler object for that MIME type, or null if one is not known. You never call on a ContentHandler or ContentHandlerFactory directly; they are there to be used by the higher level classes URL and URLConnection, and to be subclassed by programmers who want to add new MIME-type capabilities to Java.

Now look at a functioning but simple example. Consider a Java class called Colleague:

class Colleague
{
  String name;
  String phone;
  String fax;
  Date birthday;
  Date nextMeeting;
}

To prepare for loading Colleague objects from a URL using HTTP, you perform the following steps:

  • Define a new MIME type to the HTTP server.
  • Implement a ContentHandler class for the type.
  • Install a custom ContentHandlerFactory that understands the new type.

Defining a New MIME Type

You can call the new MIME type anything, as long as it doesn't mess up other MIME types on the same server. Call it application/java-Colleague to avoid trouble. To tell the Web server about the new MIME type, you have to add a local configuration directive. For ncSA httpd, one of the most common servers, you put the following line in an .htaccess file in the same directories as your Colleague URLs:

AddType application/java-Colleague .coll

The server then recognizes any URLs with the .coll file name extension as belonging to the MIME type application/java-Colleague and reports them as such. This won't work if your site disallows per-directory access configuration for httpd. Talk to your system administrator if you're not sure; she may prefer to define the new type for you in a global configuration file. Also, other servers provide this functionality in different ways.

Writing a Custom ContentHandler

Listing 12.9 shows a very simple ContentHandler for the class Colleague.


Listing 12.9. A Custom ContentHandler class.
class ColleagueHandler extends ContentHandler
{
  // Read from the URL and create a Colleague:

  public Object getContent( URLConnection c ) throws IOException
  {
    DataInputStream in = new DataInputStream( c.getInputStream() );
    Colleague coll = new Colleague();
    String s;

    coll.name = in.readLine();
    coll.phone = in.readLine();
    coll.fax = in.readLine();
    try  {
      coll.birthday = new Date( in.readLine() );
      coll.nextMeeting = new Date( in.readLine() );
    } catch (Exception e)  {
      throw new IOException( e.getMessage() );
    }
    return coll;
  }
}

This class knows how to read from a URLConnection and construct a new Colleague object. It defines the format; there are five lines in a .coll file, representing the name, phone, fax, birthday, and next meeting date of that given colleague. For a real application, you should use tagged fields (for example, Birthday: Jun 5 1972). This makes it easier to define new versions of your format without invalidating old URLs.

Using a Custom ContentHandlerFactory

Writing a content handler factory class is easy, but it's a good idea to make it flexible (because you only get to install it once!). You might want to make a class like the one shown in Listing 12.10, which lets your program add new MIME types at will.


Listing 12.10. A custom ContentHandlerFactory.
class MyContentHandlerFactory implements ContentHandlerFactory
{
  Hashtable handlers = new Hashtable();

  void addMimeType( String mimetype, ContentHandler handler )
  {
    handlers.put( mimetype, handler );
  }

  public ContentHandler createContentHandler( String mimetype )
  {
    return (ContentHandler)handlers.get( mimetype );
  }
}

This class is flexible, because other classes from the same package can add new ContentHandlers on-the-fly by calling addMimeType(). Listing 12.11 shows a little demo program which does this (ContentHandlerDemo.java).


Listing 12.11. ContentHandlerDemo.java: using a custom ContentHandler.
public class ContentHandlerDemo
{
  public static void main(String args[])
  {
    if ( args.length != 1 )  {
      System.err.println( "please give a URL argument!" );
      System.exit(1);
    }

    MyContentHandlerFactory chf = new MyContentHandlerFactory();
    ColleagueHandler collHandler = new ColleagueHandler();
    chf.addMimeType( "application/java-Colleague", collHandler );
    URLConnection.setContentHandlerFactory( chf );

    try  {
      URL url = new URL( args[0] );
      Colleague coll = (Colleague)url.getContent();
      System.out.println("Loaded colleague:");
      System.out.println("name = " + coll.name);
      System.out.println("phone = " + coll.phone);
      System.out.println("fax = " + coll.fax);
      System.out.println("birthday = " + coll.birthday);
      System.out.println("next meeting = " + coll.nextMeeting);
    } catch ( Exception e )  {
      System.err.println( "Exception: " + e ) ;
      System.exit(1);
    }
  }
}

To use this, define a colleague in a file. Fred.coll, for example, could contain this text:

Fred Fredrickson
800-8001
805-8123
Jan 12 1970
Jul 26 1996 3:15 PM

Then, by giving the URL to Fred.coll, the ContentHandlerDemo program should be able to load the information from the server, as this example shows (using the UNIX command prompt as an example):

wallaby$ java ContentHandlerDemo http://www.WidgetsGalore.com/~MrWidget/colleagues/ÂFred coll
Loaded colleague:
name = Fred Fredrickson
phone = 800-8001
fax = 805-8123
birthday = Mon Jan 12 00:00:00 PST 1970
next meeting = Fri Jul 26 15:15:00 PDT 1996

If you can't get this working, make sure that your file is in a location where your Web server expects to find it. Also make sure that you have defined a new MIME type to the server and provided the correct URL on the command line.

Tip
If an HTTP error occurs, the server returns an error screen of type text/html. Be warned that there is no predefined ContentHandler for this type, so you might want to detect this by explicitly examining the content type of the URLConnection object (if you don't, the java.net package throws a rather nondescriptive runtime exception (ClassNotFoundException) of its own.

The GET method

The GET method is the usual HTTP method for retrieving a URL, so you actually are using it every time you follow a hypertext link that isn't a Submit button on an html form. Some URLs expect to be fed additional information, however, which is appended onto the URL after a question mark. This so-called query information is useful for passing argument values to executable CGI scripts, such as WWW pages interfacing to databases or search engines. The query information is a sequence of attribute names and values, as this code shows:

http://www.myserver.edu/searchquery.cgi?text=My+Favorite+Foods&name=Fred

This query information encodes the name/value pairs "text=My Favorite Foods" and "name=Fred". Any CGI application that expects to receive query information must be able to decode this information. The encoding is a standard one, in which space characters are mapped to + and any troublesome characters (control codes, +, %, and so on) are mapped to %xx, where xx is the hexadecimal value of the character. Separating name/value pairs by ampersands (&) is the usual thing to do when there is more than one pair.

To interact with such a CGI program, you can use the java.net.URLEncoder class to encode the URL. Somewhat surprisingly, the URLEncoder class maps & and = to their hexadecimal code equivalents, so if you want to use the syntax described in the preceding paragraph, you need to encode each value separately and concatenate the results with ampersands and equal signs yourself. The CGI program must retrieve the information from the environment variable QUERY_STRING and reverse the encoding process described earlier. Look at some established CGI scripts if you're not sure how this works.

To use the information returned by the CGI script, you can read an InputStream furnished by URL.openStream() or URLConnection.getInputStream(). Alternatively, you can furnish a content handler for the MIME type of the object returned and just do a getContent() on the encoded URL. To avoid extensibility problems, you might not want to provide content handlers for common MIME types such as text/html (unless you are writing a Web browser in Java!).

The POST Method

The POST method is the method used when submitting an html form. Generally, it enables you to write to a URLConnection so that whatever you write is fed to the referenced object (usually a CGI program) on its standard input stream. Because of the way HTTP transactions work, you should write and close the output stream before you read the input or do anything else that would cause the URLConnection to connect. The server can require that the content length of the input be specified along with the request, and this can be done only before connecting. This also lets the URLConnection object realize what you're up to and then specify the POST method rather than GET.

Listing 12.12 shows a simple example of how to use the POST method to interface with a CGI program. The PostMethodDemo applet posts the contents of a TextArea to a little CGI program called wordcount.cgi and then shows the result. You have to call this applet by its full URL, because it uses its code base to construct a URL to the CGI program, which should be in the same directory. Depending on your server configuration, you might not be allowed to execute CGI programs in user directories. Then you should move wordcount.cgi to your cgi-bin directory on the Web server and modify the applet source to supply the correct URL.


Listing 12.12. PostMethodDemo.java: using the POST method.
import java.io.*;
import java.net.*;
import java.awt.*;
import java.applet.Applet;

public class PostMethodDemo extends Applet
{
  Label title = new Label( "An Applet using the POST method", Label.CENTER );
  TextArea text = new TextArea();
  Button load = new Button( "Post!" );

  public void init()
  {
    // Initialize the applet
    setLayout( new BorderLayout() ); add( "North", title );
    add( "Center", text );
    Panel p = new Panel();
    p.add( load );
    add( "South", p );
    validate();
  }

  void postText()
  {
    try  {
      String s;
      URL u = new URL( getCodeBase(), "wordcount.cgi" );
      URLConnection c = u.openConnection();
      PrintStream out = new PrintStream(c.getOutputStream());
      out.print( text.getText().trim() );
      out.close();
      DataInputStream in = new DataInputStream(c.getInputStream());
      text.setText("");
      while ( ( s = in.readLine() ) != null )
        text.appendText( s + "n" );
      in.close();
    } catch (IOException e)  {
      text.appendText( "Exception: " + e + "n" ); return;
    }
  }

  public boolean action(Event e, Object o)
{
    if ( e.target == load )  {
      postText();
      return true;
    }
    return false;
  }
}

The CGI program wordcount.cgi is just a simple Perl script that counts words and lines. You should make sure that it has world execute permissions and that it finds your local Perl compiler correctly (try running it by hand!). Notice that the first thing it prints is a header line indicating the content type, text/plain, followed by a blank line. The blank line is mandatory, and it is not part of the content. It separates the head information from the content body:

#!/usr/bin/perl

$words = 0;
$lines = 0;

print "Content-type: text/plainnn";

while (<>)  {
  $lines++;
  $words += s/S+//g;
}

print "You entered $words words on $lines lines.n";

Using the POST method doesn't prevent you from also passing query information along with the URL if you choose, just as with the GET method (of course, this depends on the CGI program being smart enough to look for the query string).

Summary

In this chapter, you've explored the various networking capabilities of Java. Several levels of functionality are available: the entire gamut from plain sockets (both TCP and UDP), to manipulating URLs and URL connections on the Web, right up to fetching Java objects from URLs by using content handlers. Though applets are hobbled in their connectivity by security restrictions, there is still quite a bit that can be done. Stand-alone programs can avoid the problem entirely, and new Java environments are being developed that should make the security interface more user-configurable.

Java complements and enhances existing CGI programs, which are still appropriate for a number of database lookup tasks. Java is also ideal for writing stand-alone server programs to service socket requests, and could even serve as a CGI language itself.

In Chapter 13, "General Purpose Classes," you'll learn about the utility classes provided in Java. These general-purpose classes provide some of the handiest data structures that every programmer needs. It's important to understand what they can do so that you can use them to save time and trouble in solving your own programming problems.


Next Previous Contents




  • Главная
  • Новости
  • Новинки
  • Скрипты
  • Форум
  • Ссылки
  • О сайте




  • Emanual.ru – это сайт, посвящённый всем значимым событиям в IT-индустрии: новейшие разработки, уникальные методы и горячие новости! Тонны информации, полезной как для обычных пользователей, так и для самых продвинутых программистов! Интересные обсуждения на актуальные темы и огромная аудитория, которая может быть интересна широкому кругу рекламодателей. У нас вы узнаете всё о компьютерах, базах данных, операционных системах, сетях, инфраструктурах, связях и программированию на популярных языках!
     Copyright © 2001-2024
    Реклама на сайте