13.2 The DatagramPacket Class

     

UDP datagrams add very little to the IP datagrams they sit on top of. Figure 13-1 shows a typical UDP datagram. The UDP header adds only eight bytes to the IP header. The UDP header includes source and destination port numbers, the length of everything that follows the IP header, and an optional checksum. Since port numbers are given as 2-byte unsigned integers, 65,536 different possible UDP ports are available per host. These are distinct from the 65,536 different TCP ports per host. Since the length is also a 2-byte unsigned integer, the number of bytes in a datagram is limited to 65,536 minus the 8 bytes for the header. However, this is redundant with the datagram length field of the IP header, which limits datagrams to between 65,467 and 65,507 bytes. (The exact number depends on the size of the IP header.) The checksum field is optional and not used in or accessible from application layer programs. If the checksum for the data fails, the native network software silently discards the datagram; neither the sender nor the receiver is notified. UDP is an unreliable protocol, after all.

Figure 13-1. The structure of a UDP datagram
figs/jnp3_1301.gif

Although the theoretical maximum amount of data in a UDP datagram is 65,507 bytes, in practice there is almost always much less. On many platforms, the actual limit is more likely to be 8,192 bytes (8K). And implementations are not required to accept datagrams with more than 576 total bytes, including data and headers. Consequently, you should be extremely wary of any program that depends on sending or receiving UDP packets with more than 8K of data. Most of the time, larger packets are simply truncated to 8K of data. For maximum safety, the data portion of a UDP packet should be kept to 512 bytes or less, although this limit can negatively affect performance compared to larger packet sizes. (This is a problem for TCP datagrams too, but the stream-based API provided by Socket and ServerSocket completely shields programmers from these details.)

In Java, a UDP datagram is represented by an instance of the DatagramPacket class:

 public final class DatagramPacket extends Object 

This class provides methods to get and set the source or destination address from the IP header, to get and set the source or destination port, to get and set the data, and to get and set the length of the data. The remaining header fields are inaccessible from pure Java code.

13.2.1 The Constructors

DatagramPacket uses different constructors depending on whether the packet will be used to send data or to receive data. This is a little unusual. Normally, constructors are overloaded to let you provide different kinds of information when you create an object, not to create objects of the same class that will be used in different contexts. In this case, all six constructors take as arguments a byte array that holds the datagram's data and the number of bytes in that array to use for the datagram's data. When you want to receive a datagram, these are the only arguments you provide; in addition, the array should be empty. When the socket receives a datagram from the network, it stores the datagram's data in the DatagramPacket object's buffer array, up to the length you specified.

The second set of DatagramPacket constructors is used to create datagrams you will send over the network. Like the first, these constructors require a buffer array and a length, but they also require the InetAddress and port to which the packet is to be sent. In this case, you will pass to the constructor a byte array containing the data you want to send and the destination address and port to which the packet is to be sent. The DatagramSocket reads the destination address and port from the packet; the address and port aren't stored within the socket, as they are in TCP.

13.2.1.1 Constructors for receiving datagrams

These two constructors create new DatagramPacket objects for receiving data from the network:

 public DatagramPacket(byte[] buffer, int length)  public DatagramPacket(byte[] buffer, int offset, int length) // Java 1.2 

When a socket receives a datagram, it stores the datagram's data part in buffer beginning at buffer[0] and continuing until the packet is completely stored or until length bytes have been written into the buffer . If the second constructor is used, storage begins at buffer[offset] instead. Otherwise , these two constructors are identical. length must be less than or equal to buffer.length-offset . If you try to construct a DatagramPacket with a length that will overflow the buffer , the constructor throws an IllegalArgumentException . This is a RuntimeException , so your code is not required to catch it. It is okay to construct a DatagramPacket with a length less than buffer.length-offset . In this case, at most the first length bytes of buffer will be filled when the datagram is received. For example, this code fragment creates a new DatagramPacket for receiving a datagram of up to 8,192 bytes:

 byte[] buffer = new byte[8192]; DatagramPacket dp = new DatagramPacket(buffer, buffer.length); 

The constructor doesn't care how large the buffer is and would happily let you create a DatagramPacket with megabytes of data. However, the underlying native network software is less forgiving , and most native UDP implementations don't support more than 8,192 bytes of data per datagram. The theoretical limit for an IPv4 datagram is 65,507 bytes of data, and a DatagramPacket with a 65,507-byte buffer can receive any possible IPv4 datagram without losing data. IPv6 datagrams raise the theoretical limit to 65,536 bytes. In practice, however, many UDP-based protocols such as DNS and TFTP use packets with 512 bytes of data per datagram or fewer. The largest data size in common usage is 8,192 bytes for NFS. Almost all UDP datagrams you're likely to encounter will have 8K of data or fewer. In fact, many operating systems don't support UDP datagrams with more than 8K of data and either truncate, split, or discard larger datagrams. If a large datagram is too big and as a result the network truncates or drops it, your Java program won't be notified of the problem. (UDP is an unreliable protocol, after all.) Consequently, you shouldn't create DatagramPacket objects with more than 8,192 bytes of data.

13.2.1.2 Constructors for sending datagrams

These four constructors create new DatagramPacket objects for sending data across the network:

 public DatagramPacket(byte[] data, int length,   InetAddress destination, int port) public DatagramPacket(byte[] data, int offset, int length,   InetAddress destination, int port) // Java 1.2  public DatagramPacket(byte[] data, int length,   SocketAddress destination, int port) // Java 1.4 public DatagramPacket(byte[] data, int offset, int length,   SocketAddress destination, int port) // Java 1.4 

Each constructor creates a new DatagramPacket to be sent to another host. The packet is filled with length bytes of the data array starting at offset or 0 if offset is not used. If you try to construct a DatagramPacket with a length that is greater than data.length , the constructor throws an IllegalArgumentException . It's okay to construct a DatagramPacket object with an offset and a length that will leave extra, unused space at the end of the data array. In this case, only length bytes of data will be sent over the network. The InetAddress or SocketAddress object destination points to the host you want the packet delivered to; the int argument port is the port on that host.

Choosing a Datagram Size

The correct amount of data to stuff into one packet depends on the situation. Some protocols dictate the size of the packet. For example, rlogin transmits each character to the remote system almost as soon as the user types it. Therefore, packets tend to be short: a single byte of data, plus a few bytes of headers. Other applications aren't so picky. For example, file transfer is more efficient with large buffers; the only requirement is that you split files into packets no larger than the maximum allowable packet size.

Several factors are involved in choosing the optimal packet size. If the network is highly unreliable, such as a packet radio network, smaller packets are preferable since they're less likely to be corrupted in transit. On the other hand, very fast and reliable LANs should use the largest packet size possible. Eight kilobytesthat is, 8,192 bytesis a good compromise for many types of networks.


It's customary to convert the data to a byte array and place it in data before creating the DatagramPacket , but it's not absolutely necessary. Changing data after the datagram has been constructed and before it has been sent changes the data in the datagram; the data isn't copied into a private buffer. In some applications, you can take advantage of this. For example, you could store data that changes over time in data and send out the current datagram (with the most recent data) every minute. However, it's more important to make sure that the data doesn't change when you don't want it to. This is especially true if your program is multithreaded, and different threads may write into the data buffer. If this is the case, synchronize the data variable or copy the data into a temporary buffer before you construct the DatagramPacket .

For instance, this code fragment creates a new DatagramPacket filled with the data "This is a test" in ASCII. The packet is directed at port 7 (the echo port) of the host www. ibiblio .org:

 String s = "This is a test"; byte[] data = s.getBytes("ASCII"); try {   InetAddress ia = InetAddress.getByName("www.ibiblio.org");   int port = 7;   DatagramPacket dp = new DatagramPacket(data, data.length, ia, port);   // send the packet... } catch (IOException ex) } 

Most of the time, the hardest part of creating a new DatagramPacket is translating the data into a byte array. Since this code fragment wants to send an ASCII string, it uses the getBytes( ) method of java.lang.String . The java.io.ByteArrayOutputStream class can also be very useful for preparing data for inclusion in datagrams.

13.2.2 The get Methods

DatagramPacket has six methods that retrieve different parts of a datagram: the actual data plus several fields from its header. These methods are mostly used for datagrams received from the network.

13.2.2.1 public InetAddress getAddress( )

The getAddress( ) method returns an InetAddress object containing the address of the remote host. If the datagram was received from the Internet, the address returned is the address of the machine that sent it (the source address). On the other hand, if the datagram was created locally to be sent to a remote machine, this method returns the address of the host to which the datagram is addressed (the destination address). This method is most commonly used to determine the address of the host that sent a UDP datagram, so that the recipient can reply.

13.2.2.2 public int getPort( )

The getPort( ) method returns an integer specifying the remote port. If this datagram was received from the Internet, this is the port on the host that sent the packet. If the datagram was created locally to be sent to a remote host, this is the port to which the packet is addressed on the remote machine.

13.2.2.3 public SocketAddress getSocketAddress( ) // Java 1.4

The getSocketAddress() method returns a SocketAddress object containing the IP address and port of the remote host. As is the case for getInetAddress( ) , if the datagram was received from the Internet, the address returned is the address of the machine that sent it (the source address). On the other hand, if the datagram was created locally to be sent to a remote machine, this method returns the address of the host to which the datagram is addressed (the destination address). You typically invoke this method to determine the address and port of the host that sent a UDP datagram before you reply. The net effect is not noticeably different than calling getAddress( ) and getPort( ) , but if you're using Java 1.4 this saves one method call. Also, if you're using non-blocking I/O, the DatagramChannel class accepts a SocketAddress but not an InetAddress and port.

13.2.2.4 public byte[] getData( )

The getData( ) method returns a byte array containing the data from the datagram. It's often necessary to convert the bytes into some other form of data before they'll be useful to your program. One way to do this is to change the byte array into a String using the following String constructor:

 public String(byte[] buffer, String encoding) 

The first argument, buffer , is the array of bytes that contains the data from the datagram. The second argument contains the name of the encoding used for this string, such as ASCII or ISO-8859-1. Thus, given a DatagramPacket dp received from the network, you can convert it to a String like this:

 String s = new String(dp.getData( ), "ASCII"); 

If the datagram does not contain text, converting it to Java data is more difficult. One approach is to convert the byte array returned by getData() into a ByteArrayInputStream using this constructor:

 public ByteArrayInputStream(byte[] buffer, int offset, int length) 

buffer is the byte array to be used as an InputStream . It's important to specify the portion of the buffer that you want to use as an InputStream using the offset and length arguments. When converting datagram data into InputStream objects, offset is either 0 (Java 1.1) or given by the DatagramPacket object's getOffset( ) method (Java 2), and length is given by the DatagramPacket object's getLength( ) method. For example:

 InputStream in = new ByteArrayInputStream(packet.getData( ),   packet.getOffset( ), packet.getLength( )); 

You must specify the offset and the length when constructing the ByteArrayInputStream . Do not use the ByteArrayInputStream( ) constructor that takes only an array as an argument. The array returned by packet.getData( ) probably has extra space in it that was not filled with data from the network. This space will contain whatever random values those components of the array had when the DatagramPacket was constructed.

The ByteArrayInputStream can then be chained to a DataInputStream :

 DataInputStream din = new DataInputStream(in); 

The data can then be read using the DataInputStream 's readInt( ) , readLong( ) , readChar( ) , and other methods. Of course, this assumes that the datagram's sender uses the same data formats as Java; it's probably the case when the sender is written in Java, and is often (though not necessarily ) the case otherwise. (Most modern computers use the same floating point format as Java, and most network protocols specify two complement integers in network byte order, which also matches Java's formats.)

13.2.2.5 public int getLength( )

The getLength( ) method returns the number of bytes of data in the datagram. This is not necessarily the same as the length of the array returned by getData( ) , i.e., getData( ).length . The int returned by getLength( ) may be less than the length of the array returned by getData( ) .

13.2.2.6 public int getOffset( ) // Java 1.2

This method simply returns the point in the array returned by getData( ) where the data from the datagram begins.

Example 13-1 uses all the methods covered in this section to print the information in the DatagramPacket . This example is a little artificial; because the program creates a DatagramPacket , it already knows what's in it. More often, you'll use these methods on a DatagramPacket received from the network, but that will have to wait for the introduction of the DatagramSocket class in the next section.

Example 13-1. Construct a DatagramPacket to receive data
 import java.net.*; public class DatagramExample {   public static void main(String[] args) {        String s = "This is a test.";        byte[] data = s.getBytes( );     try {       InetAddress ia = InetAddress.getByName("www.ibiblio.org");       int port = 7;       DatagramPacket dp         = new DatagramPacket(data, data.length, ia, port);       System.out.println("This packet is addressed to "         + dp.getAddress( ) + " on port " + dp.getPort( ));       System.out.println("There are " + dp.getLength( )         + " bytes of data in the packet");       System.out.println(         new String(dp.getData( ), dp.getOffset( ), dp.getLength( )));     }     catch (UnknownHostException e) {       System.err.println(e);     }        } } 

Here's the output:

 %  java DatagramExample  This packet is addressed to www.ibiblio.org/152.2.254.81 on port 7 There are 15 bytes of data in the packet This is a test. 

13.2.3 The set Methods

Most of the time, the six constructors are sufficient for creating datagrams. However, Java also provides several methods for changing the data, remote address, and remote port after the datagram has been created. These methods might be important in a situation where the time to create and garbage collect new DatagramPacket objects is a significant performance hit. In some situations, reusing objects can be significantly faster than constructing new ones: for example, in a networked twitch game like Quake that sends a datagram for every bullet fired or every centimeter of movement. However, you would have to use a very speedy connection for the improvement to be noticeable relative to the slowness of the network itself.

13.2.3.1 public void setData(byte[] data)

The setData( ) method changes the payload of the UDP datagram. You might use this method if you are sending a large file (where large is defined as "bigger than can comfortably fit in one datagram") to a remote host. You could repeatedly send the same DatagramPacket object, just changing the data each time.

13.2.3.2 public void setData(byte[] data, int offset, int length) // Java 1.2

This overloaded variant of the setData() method provides an alternative approach to sending a large quantity of data. Instead of sending lots of new arrays, you can put all the data in one array and send it a piece at a time. For instance, this loop sends a large array in 512-byte chunks :

 int offset = 0; DatagramPacket dp = new DatagramPacket(bigarray, offset, 512); int bytesSent = 0; while (bytesSent < bigarray.length) {      socket.send(dp);      bytesSent += dp.getLength( );      int bytesToSend = bigarray.length - bytesSent;      int size = (bytesToSend > 512) ? 512 : bytesToSend;      dp.setData(bigarray, bytesSent, 512); } 

On the other hand, this strategy requires either a lot of confidence that the data will in fact arrive or, alternatively, a disregard for the consequences of its not arriving. It's relatively difficult to attach sequence numbers or other reliability tags to individual packets when you take this approach.

13.2.3.3 public void setAddress(InetAddress remote)

The setAddress( ) method changes the address a datagram packet is sent to. This might allow you to send the same datagram to many different recipients. For example:

 String s = "Really Important Message"; byte[] data = s.getBytes("ASCII"); DatagramPacket dp = new DatagramPacket(data, data.length);  dp.setPort(2000); int network = "128.238.5."; for (int host = 1; host < 255; host++) {   try {     InetAddress remote = InetAddress.getByName(network + host);     dp.setAddress(remote);     socket.send(dp);   }   catch (IOException ex) {     // slip it; continue with the next host   } } 

Whether this is a sensible choice depends on the application. If you're trying to send to all the stations on a network segment, as in this fragment, you'd probably be better off using the local broadcast address and letting the network do the work. The local broadcast address is determined by setting all bits of the IP address after the network and subnet IDs to 1. For example, Polytechnic University's network address is 128.238.0.0. Consequently, its broadcast address is 128.238.255.255. Sending a datagram to 128.238.255.255 copies it to every host on that network (although some routers and firewalls may block it, depending on its origin).

For more widely separated hosts , you're probably better off using multicasting. Multicasting actually uses the same DatagramPacket class described here. However, it uses different IP addresses and a MulticastSocket instead of a DatagramSocket . We'll discuss this further in Chapter 14.

13.2.3.4 public void setPort(int port)

The setPort( ) method changes the port a datagram is addressed to. I honestly can't think of many uses for this method. It could be used in a port scanner application that tried to find open ports running particular UDP-based services such as FSP. Another possibility might be some sort of networked game or conferencing server where the clients that need to receive the same information are all running on different ports as well as different hosts. In this case, setPort( ) could be used in conjunction with setAddress( ) to change destinations before sending the same datagram out again.

13.2.3.5 public void setAddress(SocketAddress remote) // Java 1.4

The setSocketAddress() method changes the address and port a datagram packet is sent to. You can use this when replying. For example, this code fragment receives a datagram packet and responds to the same address with a packet containing the ASCII string "Hello there":

 DatagramPacket  input = newDatagramPacket(new byte[8192], 8192); socket.receive(input); SocketAddress address = input.getSocketAddress( ); DatagramPacket output = new DatagramPacket("Hello there"                                             .getBytes("ASCII"), 11); output.setAddress(address); socket.send(output); 

You could certainly write the same code using InetAddress objects and ports instead of a SocketAddress . Indeed, in Java 1.3 and earlier, you have to. The code would be just a few lines longer:

 DatagramPacket  input = newDatagramPacket(new byte[8192], 8192); socket.receive(input); InetAddress address = input.getAddress( ); int port = input.getPort( ); DatagramPacket output = new DatagramPacket("Hello there".getBytes("ASCII"), 11); output.setAddress(address); output.setPort(port); socket.send(output); 

13.2.3.6 public void setLength(int length)

The setLength( ) method changes the number of bytes of data in the internal buffer that are considered to be part of the datagram's data as opposed to merely unfilled space. This method is useful when receiving datagrams, as we'll explore later in this chapter. When a datagram is received, its length is set to the length of the incoming data. This means that if you try to receive another datagram into the same DatagramPacket , it's limited to no more than the number of bytes in the first. That is, once you've received a 10-byte datagram, all subsequent datagrams will be truncated to 10 bytes; once you've received a 9-byte datagram, all subsequent datagrams will be truncated to 9 bytes; and so on. This method lets you reset the length of the buffer so that subsequent datagrams aren't truncated.



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