13.5 DatagramChannel

     

Java 1.4 adds a DatagramChannel class for use in non-blocking UDP applications, just as it adds SocketChannel and ServerSocketChannel for use in non-blocking TCP applications. Like SocketChannel and ServerSocketChannel , DatagramChannel is a subclass of SelectableChannel that can be registered with a Selector . This is useful in servers where one thread can manage communications with multiple different clients. However, UDP is by its nature much more asynchronous than TCP so the net effect is smaller. In UDP it's always been the case that a single datagram socket can process requests from multiple clients for both input and output. What the DatagramChannel class adds is the ability to do this in a non-blocking fashion, so methods return quickly if the network isn't immediately ready to receive or send data.

13.5.1 Using DatagramChannel

DatagramChannel is a near-complete alternate abstraction for UDP I/O. You still need to use the DatagramSocket class to bind a channel to a port. However, you do not have to use it thereafter, nor do you ever use DatagramPacket . Instead, you read and write ByteBuffer s, just as you do with a SocketChannel .

13.5.1.1 Opening a socket

The java.nio.channels.DatagramChannel class does not have any public constructors. Instead, you create a new DatagramChannel object using the static open ( ) method:

 public static DatagramChannel open( ) throws IOException 

For example:

 DatagramChannel channel = DatagramChannel .open( ); 

This channel is not initially bound to any port. To bind it, you need to access the channel's peer DatagramSocket object using the socket() method:

 public abstract DatagramSocket socket( ) 

For example, this binds a channel to port 3141:

 SocketAddress address = new InetSocketAddress(3141); DatagramSocket socket = channel.socket( ); socket.bind(address); 

13.5.1.2 Connecting

Like DatagramSocket , a DatagramChannel can be connected; that is, it can be configured to only receive datagrams from and send datagrams to one host. This is accomplished with the connect() method:

 public abstract DatagramChannel connect(SocketAddress remote)                                                             throws IOException 

However, unlike the connect( ) method of SocketChannel , this method does not actually send or receive any packets across the network because UDP is a connectionless protocol. Thus this method returns fairly quickly, and doesn't block in any meaningful sense. There's no need here for a finishConnect() or isConnectionPending( ) method. There is an isConnected() method that returns true if and only if the DatagramSocket is connected:

 public abstract boolean isConnected( ) 

This tells you whether the DatagramChannel is limited to one host. Unlike SocketChannel , a DatagramChannel doesn't have to be connected to transmit or receive data.

Finally, there is a disconnect() method that breaks the connection:

 public abstract DatagramChannel disconnect( ) throws IOException 

This doesn't really close anything because nothing was really open in the first place. It just allows the channel to once again send and receive data from multiple hosts .

Connected channels may be marginally faster than unconnected channels in sandbox environments such as applets because the virtual machine only needs to check whether the connection is allowed on the initial call to the connect( ) method, not every time a packet is sent or received. As always, only concern yourself with this if profiling indicates it is a bottleneck.

13.5.1.3 Receiving

The receive( ) method reads one datagram packet from the channel into a ByteBuffer . It returns the address of the host that sent the packet:

 public abstract SocketAddress receive(ByteBuffer dst) throws IOException 

If the channel is blocking (the default) this method will not return until a packet has been read. If the channel is non-blocking, this method will immediately return null if no packet is available to read.

If the datagram packet has more data than the buffer can hold, the extra data is thrown away with no notification of the problem . You do not receive a BufferOverflowException or anything similar. UDP is unreliable, after all. This behavior introduces an additional layer of unreliability into the system. The data can arrive safely from the network and still be lost inside your own program.

Using this method, we can reimplement the discard server to log the host sending the data as well as the data sent. Example 13-15 demonstrates . It avoids the potential loss of data by using a buffer that's big enough to hold any UDP packet and clearing it before it's used again.

Example 13-15. A UDPDiscardServer based on channels
 import java.net.*; import java.io.*; import java.nio.*; import java.nio.channels.*; public class UDPDiscardServerWithChannels {   public final static int DEFAULT_PORT = 9;   public final static int MAX_PACKET_SIZE = 65507;   public static void main(String[] args) {     int port = DEFAULT_PORT;     try {       port = Integer.parseInt(args[0]);     }     catch (Exception ex) {     }          try {       DatagramChannel channel = DatagramChannel.open( );       DatagramSocket socket = channel.socket( );       SocketAddress address = new InetSocketAddress(port);       socket.bind(address);       ByteBuffer buffer = ByteBuffer.allocateDirect(MAX_PACKET_SIZE);       while (true) {         SocketAddress client = channel.receive(buffer);         buffer.flip( );         System.out.print(client + " says ");         while (buffer.hasRemaining( )) System.out.write(buffer.get( ));         System.out.println( );         buffer.clear( );       } // end while     }  // end try     catch (IOException ex) {       System.err.println(ex);     }  // end catch   }  // end main } 

13.5.1.4 Sending

The send( ) method writes one datagram packet into the channel from a ByteBuffer to the address specified as the second argument:

 public abstract int send(ByteBuffer src, SocketAddress target) throws                                           IOException 

The source ByteBuffer can be reused if you want to send the same data to multiple clients. Just don't forget to rewind it first.

The send( ) method returns the number of bytes written. This will either be the number of bytes remaining in the output buffer or zero. It is zero if there's not enough room in the network interface's output buffer for the amount of data you're trying to send. Don't overstuff the buffer. If you put more data in the buffer than the network interface can handle, it will never send anything. This method will not fragment the data into multiple packets. It writes everything or nothing.

Example 13-16 demonstrates with a simple echo server based on channels. The receive( ) method reads a packet, much as it did in Example 13-15. However, this time, rather than logging the packet on System.out , it returns the same data to the client that sent it.

Example 13-16. A UDPEchoServer based on channels
 import java.net.*; import java.io.*; import java.nio.*; import java.nio.channels.*; public class UDPEchoServerWithChannels {   public final static int DEFAULT_PORT = 7;   public final static int MAX_PACKET_SIZE = 65507;   public static void main(String[] args) {     int port = DEFAULT_PORT;     try {       port = Integer.parseInt(args[0]);     }     catch (Exception ex) {     }          try {       DatagramChannel channel = DatagramChannel.open( );       DatagramSocket socket = channel.socket( );       SocketAddress address = new InetSocketAddress(port);       socket.bind(address);       ByteBuffer buffer = ByteBuffer.allocateDirect(MAX_PACKET_SIZE);       while (true) {         SocketAddress client = channel.receive(buffer);         buffer.flip( );         channel.send(buffer, client);         buffer.clear( );       } // end while     }  // end try     catch (IOException ex) {       System.err.println(ex);     }  // end catch   }  // end main } 

This program is blocking and synchronous. This is much less of a problem for UDP-based protocols than for TCP protocols. The unreliable, packet-based, connectionless nature of UDP means that the server at most has to wait for the local buffer to clear. It does not have to and does not wait for the client to be ready to receive data. There's much less opportunity for one client to get held up behind a slower client.

13.5.1.5 Reading

Besides the special purpose receive( ) method, DatagramChannel has the usual three read( ) methods:

 public abstract int read(ByteBuffer dst) throws IOException public final long read(ByteBuffer[] dsts)throws IOException public final long read(ByteBuffer[] dsts, int offset, int length)                                                          throws IOException 

However, these methods can only be used on connected channels. That is, before invoking one of these methods, you must invoke connect( ) to glue the channel to a particular remote host. This makes them more suitable for use with clients that know who they'll be talking to than for servers that must accept input from multiple hosts at the same time that are normally not known prior to the arrival of the first packet.

Each of these three methods only reads a single datagram packet from the network. As much data from that datagram as possible is stored in the argument ByteBuffer (s). Each method returns the number of bytes read or -1 if the channel has been closed. This method may return 0 for any of several reasons, including:

  • The channel is non-blocking and no packet was ready.

  • A datagram packet contained no data.

  • The buffer is full.

As with the receive( ) method, if the datagram packet has more data than the ByteBuffer (s) can hold, the extra data is thrown away with no notification of the problem . You do not receive a BufferOverflowException or anything similar.

13.5.1.6 Writing

Naturally, DatagramChannel has the three write methods common to all writable, scattering channels, which can be used instead of the send() method:

 public abstract int write(ByteBuffer src) throws IOException public final long write(ByteBuffer[] dsts)throws IOException public final long write(ByteBuffer[] dsts, int offset, int length)                                                           throws IOException 

However, these methods can only be used on connected channels; otherwise they don't know where to send the packet. Each of these methods sends a single datagram packet over the connection. None of these methods are guaranteed to write the complete contents of the buffer(s). However, the cursor-based nature of buffers enables you to easily call this method again and again until the buffer is fully drained and the data has been completely sent, possibly using multiple datagram packets. For example:

 while (buffer.hasRemaining( ) && channel.write(buffer) != -1) ; 

We can use the read and write methods to implement a simple UDP echo client. On the client side, it's easy to connect before sending. Because packets may be lost in transit (always remember UDP is unreliable), we don't want to tie up the sending while waiting to receive a packet. Thus, we can take advantage of selectors and non-blocking I/O. These work for UDP pretty much exactly like they worked for TCP in Chapter 12. This time, though, rather than sending text data, let's send one hundred ints from 0 to 99. We'll print out the values returned so it will be easy to figure out if any packets are being lost. Example 13-17 demonstrates.

Example 13-17. A UDP echo client based on channels
 import java.net.*; import java.io.*; import java.nio.*; import java.nio.channels.*; import java.util.*; public class UDPEchoClientWithChannels {   public  final static int DEFAULT_PORT = 7;   private final static int LIMIT = 100;     public static void main(String[] args) {     int port = DEFAULT_PORT;     try {       port = Integer.parseInt(args[1]);     }     catch (Exception ex) {     }          SocketAddress remote;     try {       remote = new InetSocketAddress(args[0], port);     }     catch (Exception ex) {       System.err.println("Usage: java UDPEchoClientWithChannels host [port]");       return;        }          try {       DatagramChannel channel = DatagramChannel.open( );       channel.configureBlocking(false);       channel.connect(remote);              Selector selector = Selector.open( );       channel.register(selector, SelectionKey.OP_READ  SelectionKey.OP_WRITE);              ByteBuffer buffer = ByteBuffer.allocate(4);       int n = 0;       int numbersRead = 0;       while (true) {         // wait one minute for a connection         selector.select(60000);         Set readyKeys = selector.selectedKeys( );         if (readyKeys.isEmpty( ) && n == LIMIT) {            // All packets have been written and it doesn't look like any            // more are will arrive from the network           break;         }         else {           Iterator iterator = readyKeys.iterator( );           while (iterator.hasNext( )) {             SelectionKey key = (SelectionKey) iterator.next( );             iterator.remove( );             if (key.isReadable( )) {               buffer.clear( );               channel.read(buffer);               buffer.flip( );               int echo = buffer.getInt( );               System.out.println("Read: " + echo);               numbersRead++;             }              if (key.isWritable( )) {               buffer.clear( );               buffer.putInt(n);               buffer.flip( );               channel.write(buffer);               System.out.println("Wrote: " + n);               n++;              if (n == LIMIT) {                 // All packets have been written; switch to read-only mode                 key.interestOps(SelectionKey.OP_READ);               } // end if             }  // end while           } // end else         }  // end while       }  // end while       System.out.println("Echoed " + numbersRead + " out of " + LIMIT +                                                                 " sent");       System.out.println("Success rate: " + 100.0 * numbersRead / LIMIT +                                                                 "%");       }  // end try     catch (IOException ex) {       System.err.println(ex);     }  // end catch   }  // end main } 

There is one major difference between selecting TCP channels and selecting datagram channels. Because datagram channels are truly connectionless (despite the connect( ) method), you need to notice when the data transfer is complete and shut down. In this example, we assume the data is finished when all packets have been sent and one minute has passed since the last packet was received. Any expected packets that have not been received by this point are assumed to be lost in the ether .

A typical run produced output like this:

 Wrote: 0 Read: 0 Wrote: 1 Wrote: 2 Read: 1 Wrote: 3 Read: 2 Wrote: 4 Wrote: 5 Wrote: 6 Wrote: 7 Wrote: 8 Wrote: 9 Wrote: 10 Wrote: 11 Wrote: 12 Wrote: 13 Wrote: 14 Wrote: 15 Wrote: 16 Wrote: 17 Wrote: 18 Wrote: 19 Wrote: 20 Wrote: 21 Wrote: 22 Read: 3 Wrote: 23 ... Wrote: 97 Read: 72 Wrote: 98 Read: 73 Wrote: 99 Read: 75 Read: 76 ... Read: 97 Read: 98 Read: 99 Echoed 92 out of 100 sent Success rate: 92.0% 

Connecting to a remote server a couple of miles and seven hops away (according to traceroute), I saw between 90% and 98% of the packets make the round trip.

13.5.1.7 Closing

Just as with regular datagram sockets, a channel should be closed when you're done with it to free up the port and any other resources it may be using:

 public void close( ) throws IOException 

Closing an already closed channel has no effect. Attempting to write data to or read data from a closed channel throws an exception. If you're uncertain whether a channel has been closed, check with isOpen( ) :

 public boolean isOpen( ) 

This returns false if the channel is closed, true if it's open.



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