13.4 Some Useful Applications

     

In this section, you'll see several Internet servers and clients that use DatagramPacket and DatagramSocket . Some of these will be familiar from previous chapters because many Internet protocols have both TCP and UDP implementations. When an IP packet is received by a host, the host determines whether the packet is a TCP packet or a UDP datagram by inspecting the IP header. As I said earlier, there's no connection between UDP and TCP ports; TCP and UDP servers can share the same port number without problems. By convention, if a service has both TCP and UDP implementations , it uses the same port for both, although there's no technical reason this has to be the case.

13.4.1 Simple UDP Clients

Several Internet services need to know only the client's address and port; they ignore any data the client sends in its datagrams. Daytime, quote of the day, time, and chargen are four such protocols. Each of these responds the same way, regardless of the data contained in the datagram, or indeed regardless of whether there actually is any data in the datagram. Clients for these protocols simply send a UDP datagram to the server and read the response that comes back. Therefore, let's begin with a simple client called UDPPoke , shown in Example 13-5, which sends an empty UDP packet to a specified host and port and reads a response packet from the same host.

The UDPPoke class has three private fields. The bufferSize field specifies how large a return packet is expected. An 8,192-byte buffer is large enough for most of the protocols that UDPPoke is useful for, but it can be increased by passing a different value to the constructor. The DatagramSocket object socket will be used to both send and receive datagrams. Finally, the DatagramPacket object outgoing is the message sent to the individual servers.

The constructors initialize all three fields using an InetAddress for the host and int s for the port, the buffer length, and the number of milliseconds to wait before timing out. These last three become part of the DatagramSocket field socket . If the buffer length is not specified, 8,192 bytes is used. If the timeout is not given, 30 seconds (30,000 milliseconds) is used. The host, port, and buffer size are also used to construct the outgoing DatagramPacket . Although in theory you should be able to send a datagram with no data at all, bugs in some Java implementations require that you add at least one byte of data to the datagram. The simple servers we're currently considering ignore this data.

Once a UDPPoke object has been constructed , clients will call its poke( ) method to send an empty outgoing datagram to the target and read its response. The response is initially set to null. When the expected datagram appears, its data is copied into the response field. This method returns null if the response doesn't come quickly enough or never comes at all.

The main( ) method merely reads the host and port to connect to from the command line, constructs a UDPPoke object, and pokes it. Most of the simple protocols that this client suits will return ASCII text, so we'll attempt to convert the response to an ASCII string and print it. Not all VMs support the ASCII character encoding, so we'll provide the possibility of using the ASCII superset Latin-1 (8859-1) as a backup.

Example 13-5. The UDPPoke class
 import java.net.*; import java.io.*; public class UDPPoke {   private int             bufferSize; // in bytes   private DatagramSocket socket;   private DatagramPacket outgoing;       public UDPPoke(InetAddress host, int port, int bufferSize,     int timeout) throws SocketException {     outgoing = new DatagramPacket(new byte[1], 1, host, port);     this.bufferSize = bufferSize;     socket = new DatagramSocket(0);     socket .connect(host, port); // requires Java 2     socket .setSoTimeout(timeout);   }      public UDPPoke(InetAddress host, int port, int bufferSize)     throws SocketException {     this(host, port, bufferSize, 30000);   }      public UDPPoke(InetAddress host, int port)     throws SocketException {     this(host, port, 8192, 30000);   }      public byte[] poke( ) throws IOException {                byte[] response = null;     try {       socket .send(outgoing);       DatagramPacket incoming         = new DatagramPacket(new byte[bufferSize], bufferSize);       // next line blocks until the response is received       socket .receive(incoming);       int numBytes = incoming.getLength( );       response = new byte[numBytes];       System.arraycopy(incoming.getData( ), 0, response, 0, numBytes);      }     catch (IOException ex) {       // response will be null     }      // may return null      return response;             }   public static void main(String[] args) {     InetAddress host;     int port = 0;     try {       host = InetAddress.getByName(args[0]);        port = Integer.parseInt(args[1]);       if (port < 1  port > 65535) throw new Exception( );     }     catch (Exception ex) {       System.out.println("Usage: java UDPPoke host port");       return;     }     try {       UDPPoke poker = new UDPPoke(host, port);       byte[] response = poker.poke( );       if (response == null) {               System.out.println("No response within allotted time");               return;       }       String result = "";       try {         result = new String(response, "ASCII");       }       catch (UnsupportedEncodingException e) {               // try a different encoding               result = new String(response, "8859_1");       }       System.out.println(result);     }     catch (Exception ex) {       System.err.println(ex);               ex.printStackTrace( );     }   }  // end main } 

For example, this connects to a daytime server over UDP:

 D:\JAVA\JNP3\examples>  java UDPPoke rama.poly.edu 13  Sun Oct  3 13:04:22 1999 

This connects to a chargen server:

 D:\JAVA\JNP3\examples>  java UDPPoke rama.poly.edu 19  123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuv 

Given this class, UDP daytime, time, chargen, and quote of the day clients are almost trivial. Example 13-6 demonstrates a time client. The most complicated part is converting the four raw bytes returned by the server to a java.util.Date object. The same algorithm as in Example 10-5 is used here, so I won't repeat that discussion. The other protocols are left as exercises for the reader.

Example 13-6. A UDP time client
 import java.net.*; import java.util.*; public class UDPTimeClient {      public final static int DEFAULT_PORT = 37;   public final static String DEFAULT_HOST = "time-a.nist.gov";      public static void main(String[] args) {          InetAddress host;     int port = DEFAULT_PORT;     try {       if (args.length > 0) {         host = InetAddress.getByName(args[0]);       }       else {         host = InetAddress.getByName(DEFAULT_HOST);       }      }     catch (Exception ex) {       System.out.println("Usage: java UDPTimeClient host port");       return;     }     if (args.length > 1) {       try {         port = Integer.parseInt(args[1]);         if (port <= 0  port > 65535) port = DEFAULT_PORT;;       }       catch (Exception ex){       }     }     try {       UDPPoke poker = new UDPPoke(host, port);       byte[] response = poker.poke( );         if (response == null) {         System.out.println("No response within allotted time");         return;         }         else if (response.length != 4) {         System.out.println("Unrecognized response format");         return;                  }                  // The time protocol sets the epoch at 1900,       // the Java Date class at 1970. This number        // converts between them.            long differenceBetweenEpochs = 2208988800L;       long secondsSince1900 = 0;       for (int i = 0; i < 4; i++) {         secondsSince1900           = (secondsSince1900 << 8)  (response[i] & 0x000000FF);       }       long secondsSince1970         = secondsSince1900 - differenceBetweenEpochs;              long msSince1970 = secondsSince1970 * 1000;       Date time = new Date(msSince1970);              System.out.println(time);     }     catch (Exception ex) {       System.err.println(ex);               ex.printStackTrace( );     }             }     } 

13.4.2 UDPServer

Clients aren't the only programs that benefit from a reusable implementation. The servers for these protocols are very similar. They all wait for UDP datagrams on a specified port and reply to each datagram with another datagram. The servers differ only in the content of the datagram that they return. Example 13-7 is a simple UDPServer class that can be subclassed to provide specific servers for different protocols.

The UDPServer class has two fields, the int bufferSize and the DatagramSocket socket , the latter of which is protected so it can be used by subclasses. The constructor opens the DatagramSocket socket on a specified local port to receive datagrams of no more than bufferSize bytes.

UDPServer extends Thread so that multiple instances can run in parallel. Its run( ) method contains an infinite loop that repeatedly receives an incoming datagram and responds by passing it to the abstract respond( ) method. This method will be overridden by particular subclasses in order to implement different kinds of servers.

UDPServer is a very flexible class. Subclasses can send zero, one, or many datagrams in response to each incoming datagram. If a lot of processing is required to respond to a packet, the respond( ) method can spawn a thread to do it. However, UDP servers tend not to have extended interactions with a client. Each incoming packet is treated independently of other packets, so the response can usually be handled directly in the respond( ) method without spawning a thread.

Example 13-7. The UDPServer class
 import java.net.*; import java.io.*; public abstract class UDPServer extends Thread {   private   int             bufferSize; // in bytes   protected DatagramSocket socket;       public UDPServer(int port, int bufferSize)     throws SocketException {     this.bufferSize = bufferSize;     this.socket = new DatagramSocket(port);   }      public UDPServer(int port) throws SocketException {     this(port, 8192);   }      public void run( ) {        byte[] buffer = new byte[bufferSize];     while (true) {       DatagramPacket incoming = new DatagramPacket(buffer, buffer.length);       try {         socket.receive(incoming);         this.respond(incoming);       }       catch (IOException ex) {         System.err.println(ex);       }           } // end while   }  // end run      public abstract void respond(DatagramPacket request); } 

The easiest protocol to handle is discard. All we need to do is write a main( ) method that sets the port and start the thread. respond( ) is a do-nothing method. Example 13-8 is a high-performance UDP discard server that does nothing with incoming packets.

Example 13-8. A high-performance UDP discard server
 import java.net.*; public class FastUDPDiscardServer extends UDPServer {   public final static int DEFAULT_PORT = 9;      public FastUDPDiscardServer( ) throws SocketException {     super(DEFAULT_PORT);   }      public void respond(DatagramPacket packet) {}   public static void main(String[] args) {      try {      UDPServer server = new FastUDPDiscardServer( );      server.start( );    }    catch (SocketException ex) {      System.err.println(ex);    }     } } 

Example 13-9 is a slightly more interesting discard server that prints the incoming packets on System.out .

Example 13-9. A UDP discard server
 import java.net.*; public class LoggingUDPDiscardServer extends UDPServer {   public final static int DEFAULT_PORT = 9999;      public LoggingUDPDiscardServer( ) throws SocketException {     super(DEFAULT_PORT);   }      public void respond(DatagramPacket packet) {          byte[] data = new byte[packet.getLength( )];     System.arraycopy(packet.getData( ), 0, data, 0, packet.getLength( ));     try {       String s = new String(data, "8859_1");       System.out.println(packet.getAddress( ) + " at port "         + packet.getPort( ) + " says " + s);     }     catch (java.io.UnsupportedEncodingException ex) {       // This shouldn't happen     }      }   public static void main(String[] args) {      try {      UDPServer erver = new LoggingUDPDiscardServer( );      server.start( );    }    catch (SocketException ex) {      System.err.println(ex);    }     } } 

It isn't much harder to implement an echo server, as Example 13-10 shows. Unlike a stream-based TCP echo server, multiple threads are not required to handle multiple clients.

Example 13-10. A UDP echo server
 import java.net.*; import java.io.*; public class UDPEchoServer extends UDPServer {   public final static int DEFAULT_PORT = 7;   public UDPEchoServer( ) throws SocketException {     super(DEFAULT_PORT);    }   public void respond(DatagramPacket packet) {     try {       DatagramPacket outgoing = new DatagramPacket(packet.getData( ),         packet.getLength( ), packet.getAddress( ), packet.getPort( ));       socket.send(outgoing);     }     catch (IOException ex) {       System.err.println(ex);     }        }   public static void main(String[] args) {      try {      UDPServer server = new UDPEchoServer( );      server.start( );    }    catch (SocketException ex) {      System.err.println(ex);    }     } } 

A daytime server is only slightly more complex. The server listens for incoming UDP datagrams on port 13. When it detects an incoming datagram, it returns the current date and time at the server as a one-line ASCII string. Example 13-11 demonstrates this.

Example 13-11. The UDP daytime server
 import java.net.*; import java.io.*; import java.util.*; public class UDPDaytimeServer extends UDPServer {   public final static int DEFAULT_PORT = 13;   public UDPDaytimeServer( ) throws SocketException {     super(DEFAULT_PORT);    }   public void respond(DatagramPacket packet) {     try {       Date now = new Date( );       String response = now.toString( ) + "\r\n";       byte[] data = response.getBytes("ASCII");       DatagramPacket outgoing = new DatagramPacket(data,         data.length, packet.getAddress( ), packet.getPort( ));       socket.send(outgoing);     }     catch (IOException ex) {       System.err.println(ex);     }        }   public static void main(String[] args) {      try {      UDPServer server = new UDPDaytimeServer( );      server.start( );    }    catch (SocketException ex) {      System.err.println(ex);    }     } } 

13.4.3 A UDP Echo Client

The UDPPoke class implemented earlier isn't suitable for all protocols. In particular, protocols that require multiple datagrams require a different implementation. The echo protocol has both TCP and UDP implementations. Implementing the echo protocol with TCP is simple; it's more complex with UDP because you don't have I/O streams or the concept of a connection to work with. A TCP-based echo client can send a message and wait for a response on the same connection. However, a UDP-based echo client has no guarantee that the message it sent was received. Therefore, it cannot simply wait for the response; it needs to be prepared to send and receive data asynchronously.

This behavior is fairly simple to implement using threads, however. One thread can process user input and send it to the echo server, while a second thread accepts input from the server and displays it to the user . The client is divided into three classes: the main UDPEchoClient class, the SenderThread class, and the ReceiverThread class.

The UDPEchoClient class should look familiar. It reads a hostname from the command line and converts it to an InetAddress object. UDPEchoClient uses this object and the default echo port to construct a SenderThread object. This constructor can throw a SocketException , so the exception must be caught. Then the SenderThread starts. The same DatagramSocket that the SenderThread uses is used to construct a ReceiverThread , which is then started. It's important to use the same DatagramSocket for both sending and receiving data because the echo server will send the response back to the port the data was sent from. Example 13-12 shows the code for the UDPEchoClient .

Example 13-12. The UDPEchoClient class
 import java.net.*; import java.io.*; public class UDPEchoClient {   public final static int DEFAULT_PORT = 7;   public static void main(String[] args) {     String hostname = "localhost";     int port = DEFAULT_PORT;     if (args.length > 0) {       hostname = args[0];     }     try {       InetAddress ia = InetAddress.getByName(hostname);       Thread sender = new SenderThread(ia, DEFAULT_PORT);       sender.start( );       Thread receiver = new ReceiverThread(sender.getSocket( ));       receiver.start( );     }     catch (UnknownHostException ex) {       System.err.println(ex);     }     catch (SocketException ex) {       System.err.println(ex);     }   }  // end main } 

The SenderThread class reads input from the console a line at a time and sends it to the echo server. It's shown in Example 13-13. The input is provided by System.in , but a different client could include an option to read input from a different streamperhaps opening a FileInputStream to read from a file. The three fields of this class define the server to which it sends data, the port on that server, and the DatagramSocket that does the sending, all set in the single constructor. The DatagramSocket is connected to the remote server to make sure all datagrams received were in fact sent by the right server. It's rather unlikely that some other server on the Internet is going to bombard this particular port with extraneous data, so this is not a big flaw. However, it's a good habit to make sure that the packets you receive come from the right place, especially if security is a concern.

The run( ) method processes user input a line at a time. To do this, the BufferedReader userInput is chained to System.in . An infinite loop reads lines of user input. Each line is stored in theLine . A period on a line by itself signals the end of user input and breaks out of the loop. Otherwise, the bytes of data are stored in the data array using the getBytes( ) method from java.lang.String . Next, the data array is placed in the payload part of the DatagramPacket output , along with information about the server, the port, and the data length. This packet is then sent to its destination by socket . This thread then yields to give other threads an opportunity to run.

Example 13-13. The SenderThread class
 import java.net.*; import java.io.*; public class SenderThread extends Thread {   private InetAddress server;   private DatagramSocket socket;   private boolean stopped = false;   private int port;      public SenderThread(InetAddress address, int port)     throws SocketException {     this.server = address;     this.port = port;     this.socket = new DatagramSocket( );     this.socket.connect(server, port);   }      public void halt( ) {     this.stopped = true;    }      public DatagramSocket getSocket( ) {     return this.socket;    }   public void run( ) {     try {       BufferedReader userInput         = new BufferedReader(new InputStreamReader(System.in));       while (true) {         if (stopped) return;         String theLine = userInput.readLine( );         if (theLine.equals(".")) break;         byte[] data = theLine.getBytes( );         DatagramPacket output           = new DatagramPacket(data, data.length, server, port);         socket.send(output);         Thread.yield( );       }     }  // end try     catch (IOException ex) {       System.err.println(ex);     }   }  // end run } 

The ReceiverThread class shown in Example 13-14 waits for datagrams to arrive from the network. When a datagram is received, it is converted to a String and printed on System.out for display to the user. A more advanced EchoClient could include an option to send the output elsewhere.

This class has two fields. The more important is the DatagramSocket , theSocket , which must be the same DatagramSocket used by the SenderThread . Data arrives on the port used by that DatagramSocket ; any other DatagramSocket would not be allowed to connect to the same port. The second field, stopped , is a boolean used to halt this thread without invoking the deprecated stop( ) method.

The run( ) method is an infinite loop that uses socket 's receive( ) method to wait for incoming datagrams. When an incoming datagram appears, it is converted into a String with the same length as the incoming data and printed on System.out . As in the input thread, this thread then yields to give other threads an opportunity to execute.

Example 13-14. The ReceiverThread class
 import java.net.*; import java.io.*; class ReceiverThread extends Thread {   DatagramSocket socket;   private boolean stopped = false;   public ReceiverThread(DatagramSocket ds) throws SocketException {     this.socket = ds;   }   public void halt( ) {     this.stopped = true;    }   public void run( ) {     byte[] buffer = new byte[65507];     while (true) {       if (stopped) return;       DatagramPacket dp = new DatagramPacket(buffer, buffer.length);       try {         socket.receive(dp);         String s = new String(dp.getData( ), 0, dp.getLength( ));         System.out.println(s);         Thread.yield( );       }       catch (IOException ex) {         System.err.println(ex);       }            }        } } 

You can run the echo client on one machine and connect to the echo server on a second machine to verify that the network is functioning properly between them.



Java Network Programming
Java Network Programming, Third Edition
ISBN: 0596007213
EAN: 2147483647
Year: 2003
Pages: 164

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net