Datagram Channels

Data is sent across the Internet in unreliable packets called IP datagrams . More often than not, these packets are automatically reassembled into the correct sequence using a higher-level protocol called TCP. Lost and corrupted packets are retransmitted automatically. The end result is something that looks very much like a stream or a channel. Indeed, Java's interface to TCP data is through streams or channels (your choice).

However, some protocols, such as NFS, SIP, and DNS, can send data over UDP instead. UDP still detects and drops corrupted datagrams, but that's it. UDP does not guarantee that packets arrive in the order they were sent, or indeed that the packets arrive at all. UDP can be faster than TCP though, if you can live with or compensate for its unreliability.

UDP data arrives in raw packets of bytes. A packet does not necessarily have any relation to the previous packet or the next packet. You may get nothing for several seconds, or even minutes, and then suddenly have to deal with a few hundred packets. Packets arriving close together in time may be part of the same transmission, two transmissions, or several transmissions.

java.nio.channels includes a DatagramChannel class for UDP. This 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 initially listens to and sends from an anonymous (system-selected) port. Servers that need to listen on a particular port can bind to that port through the channel's peer DatagramSocket object. This is returned by the socket( ) method:

public abstract DatagramSocket socket( )

For example, this code fragment binds a channel to port 4567:

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

DatagramChannel has both read( ) and write( ) methods. That is, it implements both ReadableByteChannel and WritableByteChannel. It also implements GatheringByteChannel and ScatteringByteChannel. However, more often than not you read and write data with its two special methods, send( ) and receive( ), instead:

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

UDP by its nature is connectionless. That is, a single UDP channel can send packets to and receive packets from multiple hosts. As a result, when sending, you need to specify the address of the system to which you're sending. When receiving, you'll probably want to know the address of the system from which the packet originated. Both are provided as java.net.SocketAddress objects. For example, this code fragment sends a UDP packet containing the byte 100 to the server at time.nist.gov:

ByteBuffer buffer = ByteBuffer.allocate(512);
buffer.put((byte) 100);
// Don't forget to flip the buffer or nothing will be sent
buffer.flip( );
SocketAddress address = new InetSocketAddress("time.nist.gov", 37);
socket.send(buffer, address);

This code fragment receives a packet from some server and then prints the address of the originating host:

ByteBuffer receipt = ByteBuffer.allocate(8192);
SocketAddress sender = socket.receive(receipt)
System.out.println(address);

If the two code fragments are run in sequence, chances are the second fragment will print the same address that was used to send the first fragment. That is, the server will have responded with a UDP packet to the sender. However, that's not guaranteed. If for some reason a different system sent a UDP packet to this host and port at the right time, that packet would be received instead.

By default, both of these methods block. That is, they do not return until a UDP datagram has been sent or received. (We'll see how to change this in the next chapter.) For sending, this is normally not a problem as long as the network hardware is working. For receiving, it can be. Because UDP is unreliable, there's no warning if the server fails to respond or if its response is lost in transit. If the program does anything other than wait for incoming packets, you should either put the call to receive( ) in a separate thread or set the socket's SO_TIMEOUT. For instance, this statement sets the timeout to 3 seconds (3000 milliseconds):

channel.socket( ).setSoTimeout(3000);

If the specified time passes with no incoming packet, receive( ) tHRows a java.net.SocketTimeoutException, a subclass of IOException.

Furthermore, if a datagram arrives with more data than the buffer has space remaining, the extra data is thrown away with no notification of the problem. There is no BufferOverflowException or anything similar. UDP is unreliable, after all.

On the other hand, if you try to send more data from a buffer than can fit into a single datagram, the send( ) method sends nothing and returns 0. send( ) will not fragment the data into multiple UDP packets: it writes everything or nothing. You're probably okay up to 8K of data on a modern system, and you may be okay somewhat beyond that. However, 64K (indeed, a little less than that when space for IP headers and such is set aside) is the absolute maximum that can ever fit into one UDP datagram. If you have more than 8K or so of data, you should probably break it up into multiple calls to send( ) and design a higher-level protocol for reassembling and perhaps retransmitting them. You can use the ByteBuffer's limit( ) methods to set the limit no more than 8192 bytes ahead of the position before sending.

Like all channels, a datagram 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 send data to or receive data from a closed channel throws a ClosedChannelException. If you're uncertain whether a channel has been closed, check with isOpen( ):

public boolean isOpen( )

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

Example 15-7 is a complete program that both sends and receives some data over UDP. Specifically, it sends a request to the time server at time.nist.gov. It then receives a UDP datagram containing the number of seconds since midnight, January 1, 1900. Of course, with UDP, you're not guaranteed to get anything back, so it's important to set a timeout on the socket operation. Example 15-7 waits at most five seconds.

Example 15-7. A UDP time client

import java.io.IOException;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.*;
public class UDPTimeClient {
 public static void main(String[] args) throws IOException {
 DatagramChannel channel = null;
 try {
 channel = DatagramChannel.open( );
 // port 0 selects any available port
 SocketAddress address = new InetSocketAddress(0);
 DatagramSocket socket = channel.socket( );
 socket.setSoTimeout(5000);
 socket.bind(address);
 SocketAddress server = new InetSocketAddress("time.nist.gov", 37);
 ByteBuffer buffer = ByteBuffer.allocate(8192);
 // time protocol always uses big-endian order
 buffer.order(ByteOrder.BIG_ENDIAN);
 // Must put at least one byte of data in the buffer;
 // it doesn't matter what it is.
 buffer.put((byte) 65);
 buffer.flip( );
 channel.send(buffer, server);

 buffer.clear( );
 buffer.put((byte) 0).put((byte) 0).put((byte) 0).put((byte) 0);
 channel.receive(buffer);
 buffer.flip( );
 long secondsSince1900 = buffer.getLong( );
 // The time protocol sets the epoch at 1900,
 // the java.util.Date class at 1970. This number
 // converts between them.
 long differenceBetweenEpochs = 2208988800L;
 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( );
 }
 finally {
 if (channel != null) channel.close( );
 }
 }
}

The time protocol encodes the time as an unsigned big-endian 4-byte int. Java doesn't have any such data type. We could manually read the bytes and form a long from them. However, it's a little more obvious and informative to use the buffer class in a tricky way. Before receiving the data, I put four zeros in the buffer's first four positions. Then I receive the next four bytes from the server. A total of eight bytes, which is the desired value, can then be read as a signed long using getLong( ).

Here's the output from running the program, along with the output from the Unix date command for comparison:

$ date;java UDPTimeClient
Wed Oct 15 07:37:40 EDT 2005
Wed Oct 15 07:37:41 EDT 2005

The server-supplied time is only a second off from the time measured by the client computer's clock. The error is likely a combination of clock drift between the two systems and the time it takes for the UDP request and response to travel between my local computer in Brooklyn and the server in Boulder.

Unlike socket-based programs, there's not a huge amount of difference between the UDP server API and the UDP client API. Example 15-8 shows a UDP server implemented using these same classes and methods. Here, the server waits for a client to send a datagram rather than initiating the communication, but the methods and classes are all the same.

Example 15-8. A UDP time server

import java.io.IOException;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.util.*;
public class UDPTimeServer {
 public final static int DEFAULT_PORT = 37;
 public static void main(String[] args) throws IOException {
 int port = 37;
 if (args.length > 0) {
 try {
 port = Integer.parseInt(args[1]);
 if (port <= 0 || port > 65535) port = DEFAULT_PORT;;
 }
 catch (Exception ex){
 }
 }
 ByteBuffer in = ByteBuffer.allocate(8192);
 ByteBuffer out = ByteBuffer.allocate(8);
 out.order(ByteOrder.BIG_ENDIAN);
 SocketAddress address = new InetSocketAddress(port);
 DatagramChannel channel = DatagramChannel.open( );
 DatagramSocket socket = channel.socket( );
 socket.bind(address);
 System.err.println("bound to " + address);
 while (true) {
 try {
 in.clear( );
 SocketAddress client = channel.receive(in);
 System.err.println(client);
 long secondsSince1900 = getTime( );
 out.clear( );
 out.putLong(secondsSince1900);
 out.flip( );
 // skip over the first four bytes to make this an unsigned int
 out.position(4);
 channel.send(out, client);
 }
 catch (Exception ex) {
 System.err.println(ex);
 }
 }
 }
 private static long getTime( ) {
 long differenceBetweenEpochs = 2208988800L;
 Date now = new Date( );

 long secondsSince1970 = now.getTime( ) / 1000;
 long secondsSince1900 = secondsSince1970 + differenceBetweenEpochs;
 return secondsSince1900;
 }
}

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.

15.6.1. Connecting

Unlike regular sockets and socket channels, datagram channels can normally send data to and receive data from any host. However, you can force a DatagramChannel to communicate with only one specified host using the connect( ) method:

public abstract DatagramChannel connect(SocketAddress remote)
 throws IOException

UDP is a connectionless protocol. Unlike the connect( ) method of SocketChannel, this method does not actually send or receive any packets across the network. Instead, it simply changes the datagram socket object so it will refuse to send packets to other hosts (i.e., throw an IllegalArgumentException) and ignore packets received from other hosts. Thus, this method returns fairly quickly and never blocks.

The isConnected( ) method returns true if the DatagramSocket is connected and false otherwise:

public abstract boolean isConnected( )

However, this just tells you whether the DatagramChannel is limited to one host. Unlike a SocketChannel, a DatagramChannel does not 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 data to and receive data from multiple hosts.

15.6.2. 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 be used only 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 whom they'll be talking to than for servers that must accept input from multiple hosts at the same time.

Each of these three methods reads only a single datagram packet from the network. As much data from that datagram as possible is stored in the ByteBuffer. 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 nonblocking 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 can hold, the extra data is thrown away with no notification of the problem. You do not receive a BufferOverflowException or anything similar.

15.6.3. 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 be used only 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 is guaranteed to write the complete contents of the buffer(s). If the value returned is less (or more) than the amount of data expected in the packet, you may have sent a corrupted packet. The protocol needs some way of recognizing and discarding such packets on the other end. Furthermore, in this case you'll probably want to retransmit the original packet from the beginning.

The write( ) method works best for simple protocols such as echo and chargen that accept more or less arbitrary data in more or less arbitrary order. However, to the extent that packet boundaries matter, the send( ) method is more reliable here since it always sends everything or nothing.

Example 15-9 revises the UDP time client program so that it first connects to the server. It sends a packet to the server using the write( ) method and gets the result back using the read( ) method.

Example 15-9. A connected time client

import java.io.IOException;
import java.net.*;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.*;
public class ConnectedTimeClient {
 public static void main(String[] args) throws IOException {
 DatagramChannel channel = DatagramChannel.open( );
 SocketAddress address = new InetSocketAddress(0);
 DatagramSocket socket = channel.socket( );
 socket.bind(address);
 SocketAddress server = new InetSocketAddress("time-a.nist.gov", 37);
 channel.connect(server);
 ByteBuffer buffer = ByteBuffer.allocate(8);
 buffer.order(ByteOrder.BIG_ENDIAN);
 // send a byte of data to the server
 buffer.put((byte) 0);
 buffer.flip( );
 channel.write(buffer);
 // get the buffer ready to receive data
 buffer.clear( );
 // fill the first four bytes with zeros
 buffer.putInt(0);
 channel.read(buffer);
 buffer.flip( );
 // convert seconds since 1900 to a java.util.Date
 long secondsSince1900 = buffer.getLong( );
 long differenceBetweenEpochs = 2208988800L;
 long secondsSince1970
 = secondsSince1900 - differenceBetweenEpochs;
 long msSince1970 = secondsSince1970 * 1000;
 Date time = new Date(msSince1970);
 System.out.println(time);
 }
}

This program is a little simpler than the previous version, mostly because it uses a connected channel. One other small change: this program calls buffer.putInt(0) to store zeros in the first four bytes of the buffer rather than putting the byte 0 four times.

Basic I/O

Introducing I/O

Output Streams

Input Streams

Data Sources

File Streams

Network Streams

Filter Streams

Filter Streams

Print Streams

Data Streams

Streams in Memory

Compressing Streams

JAR Archives

Cryptographic Streams

Object Serialization

New I/O

Buffers

Channels

Nonblocking I/O

The File System

Working with Files

File Dialogs and Choosers

Text

Character Sets and Unicode

Readers and Writers

Formatted I/O with java.text

Devices

The Java Communications API

USB

The J2ME Generic Connection Framework

Bluetooth

Character Sets



Java I/O
Java I/O
ISBN: 0596527500
EAN: 2147483647
Year: 2004
Pages: 244

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