by David W. Baker
For many Internet developers, UDP is a much less-often used protocol when compared to TCP. UDP does not isolate you so neatly from the details of implementing a continuous network communication. For many Java applications, however, choosing UDP as the tool to create a network linkage may be the most prudent option.
Programming with UDP has significant ramifications. Understanding these factors will guide and educate your network programming efforts.
UDP is a good choice for applications in which communications can be separated into discrete messages, where a single query from a client invokes a single response from a server. Time-dependent data is particularly suited to UDP. UDP requires much less overhead, but the burden of engineering any necessary reliability into the system is your responsibility. For instance, if clients never receive responses to their queries--perfectly possible and legitimate with UDP--you might want to program the clients to retransmit the request or perhaps display an informative message indicating communication difficulties.
As discussed in Chapter 23, "Communications and Networking," UDP behaves very differently than TCP. UDP is described as unreliable, connectionless, and message-oriented. A common analogy that explains UDP is that of communicating with postcards.
A dialog with UDP must be quanticized into small messages that fit within a small packet of a specific size, although some packets can hold more data than others. When you send out a message, you can never be certain that you will receive a return message. Unless you do receive a return message, you have no idea if your message was received--your message could have been lost en route, the recipient's confirmation could have been lost, or the recipient might be ignoring your message.
The postcards you will be exchanging between network programs are referred to as datagrams. Within a datagram, you can store an array of bytes. A receiving application can extract this array and decode your message, possibly sending a return datagram response.
As with TCP, you will program in UDP using the socket programming abstraction. However, UDP sockets are very different from TCP sockets. Extending the analogy, UDP sockets are much like creating a mailbox.
A mailbox is identified by your address, but you don't construct a new one for each person to whom you will be sending a message. (However, you might create a new mailbox to receive newspapers, which shouldn't go into your normal mailbox.) Instead, you place an address on the postcard that indicates to whom the message is being sent. You place the postcard in the mailbox and it is (eventually) sent on its way.
When receiving a message, you could potentially wait forever until one arrives in your mailbox. Once one does, you can read the postcard. Meta-information appears on the postcard that identifies the sender through the return address.
As the previous analogies suggest, UDP programming involves the following general tasks:
The java.net package has the tools that are necessary to perform UDP communications. For creating datagrams, Java provides the DatagramPacket class. When receiving a UDP datagram, you also use the DatagramPacket class to read the data, sender, and meta-information.
To create a datagram to send to a remote system, the following constructor is provided:
public DatagramPacket(byte[] ibuf, int length, InetAddress iaddr, int iport);
ibuf is the array of bytes that encodes the data of the message, while length is the length of the byte array to place into the datagram. This factor determines the size of the datagram. iaddr is an InetAddress object, as explained in Chapter 24, which stores the IP address of the intended recipient. port identifies which port the datagram should be sent to on the receiving host.
See "Java TCP Socket Classes," Chapter 24
In order to receive a datagram, you must use another DatagramPacket constructor in which the incoming data will be stored. This constructor has the prototype of:
public DatagramPacket(byte[] ibuff, int ilength);
ibuf is the byte array into which the data portion of the datagram will be copied. ilength is the number of bytes to copy from the datagram into the array corresponding to the size of the datagram. If ilength is less than the size of the UDP datagram received by the machine, the extra bytes will be silently ignored by Java.
NOTE: Programming with TCP sockets relieves you from breaking your data down into discrete chunks for transmission over a network. When creating a UDP-based client-server protocol, you must specify some expected length of the datagrams or create a means for determining this at runtime. According to the TCP/IP specification, the largest datagram possible is one that contains 65,507 bytes of data. However, a host is only required to receive datagrams with up to 548 bytes of data. Most platforms support larger datagrams of at least 8,192 bytes in length. Large datagrams are likely to be fragmented at the IP layer. If, during transmission, any one of the IP packets which contains a fragment of the datagram is lost, the entire UDP datagram will be silently lost. The point is that you must design your application with the datagram size in mind. It is prudent to limit this size to a reasonable length.
public int getLength(); public byte[] getData(); public InetAddress getAddress(); public int getPort();
The getLength() method is used to obtain the number of bytes contained within the data portion of the datagram. The getData() method is used to obtain a byte array containing the data received. getAddress() provides an InetAddress object identifying the sender, while getPort() indicates the UDP port used.
Performing the sending and receiving of these datagrams is accomplished with the DatagramSocket class, which creates a UDP socket. Three constructors are available:
public DatagramSocket() throws IOException; public DatagramSocket(int port) throws IOException; public DatagramSocket(int port, InetAddress localAddr) throws IOException;
The first constructor allows you to create a socket at an unused ephemeral port, generally used for client applications. The second constructor allows you to specify a particular port, which is useful for server applications. As with TCP, most systems require super-user privileges in order to bind UDP ports below 1024. The final constructor is useful for machines with multiple IP interfaces. You can use this constructor to send and listen for datagrams from one of the IP addresses assigned to the machine. On such a host, datagrams sent to any of the machine's IP addresses will be received by a DatagramSocket created with the first two constructors, while the last constructor will obtain only datagrams sent to the specific IP address.
You can use this socket to send properly addressed DatagramPacket instances created with the first constructor described by using this DatagramSocket method:
public void send(DatagramPacket p) throws IOException;
Once a DatagramPacket has been created with the second constructor described, a datagram can be received:
public synchronized void receive(DatagramPacket p) throws IOException;
Note that the receive() method blocks until a datagram is received. Because UDP is unreliable, your application cannot expect receive() ever to return unless a timeout is enabled. Such a timeout, named the SO_TIMEOUT option from the name of the Berkeley sockets API option, can be set with this method from the DatagramSocket class:
public synchronized void setSoTimeout(int timeout) throws SocketException;
timeout is a value in milliseconds. If set to 0, the receive() method exhibits an infinite timeout--the default behavior. When greater than zero, a subsequent receive() method invocation will wait only the specified timeout before an InterruptedIOException is thrown.
NOTE: Your host's UDP implementation has a limited queue for incoming datagrams. If your application cannot process these datagrams rapidly enough, they will be silently discarded. Neither the sender nor the receiver is notified when datagrams are dropped from a queue overflow. Such is the unreliable nature of UDP.
Once communications through the UDP socket are completed, that socket should be closed:
public synchronized void close();
In this section, you will learn how to create a basic UDP server which will respond to simple client requests. The practical example used here is to create a daytime server.
Daytime is a simple service that runs on many systems. For example, most UNIX systems run daytime out of inetd, as listed in /etc/inetd.conf. On Windows NT, the daytime server is available through the Simple TCP/IP Services within the Services Control Panel. Daytime is generally run on UDP port 13. When sent a datagram, it responds with a datagram containing the date in a format such as:
Friday, July 30, 1993 19:25:00
Listing 25.1 shows the Java code used to implement this service.
import java.net.*; // Import the package names used import java.util.*; import java.io.*; import java.text.*; /** * This is an application which runs the * daytime service. * @author David W. Baker * @version 1.2 */ public class DaytimeServer { // The daytime service runs on this well-known port. private static final int TIME_PORT = 13; private DatagramSocket timeSocket = null; private static final int SMALL_ARRAY = 1; private static final int TIME_ARRAY = 100; // A boolean to keep the server looping until stopped. private boolean keepRunning = true; /** * This method starts the application, creating and * instance and telling it to start accepting * requests. * @param args Command line arguments - ignored. */ public static void main(String[] args) { DaytimeServer server = new DaytimeServer(); server.startServing(); } /** * This constructor creates a datagram socket to * listen on. */ public DaytimeServer() { try { timeSocket = new DatagramSocket(TIME_PORT); } catch(SocketException excpt) { System.err.println("Unable to open socket: " + excpt); } } /** * This method does all of the work of listening for * and responding to clients. */ public void startServing() { DatagramPacket datagram; // For a UDP datagram. InetAddress clientAddr; // Address of the client. int clientPort; // Port of the client. byte[] dataBuffer; // To construct a datagram. String timeString; // The time as a string. // Keep looping while you have a socket. while(keepRunning) { try { // Create a DatagramPacket to receive query. dataBuffer = new byte[SMALL_ARRAY]; datagram = new DatagramPacket(dataBuffer, dataBuffer.length); timeSocket.receive(datagram); // Get the meta-info on the client. clientAddr = datagram.getAddress(); clientPort = datagram.getPort(); // Place the time into byte array. dataBuffer = getTimeBuffer(); // Create and send the datagram. datagram = new DatagramPacket(dataBuffer, dataBuffer.length,clientAddr,clientPort); timeSocket.send(datagram); } catch(IOException excpt) { System.err.println("Failed I/O: " + excpt); } } timeSocket.close(); } /** * This method is used to create a byte array * containing the current time in the special daytime * server format. * @return The byte array with the time. */ protected byte[] getTimeBuffer() { String timeString; SimpleDateFormat daytimeFormat; Date currentTime; // Get the current time. currentTime = new Date(); // Create a SimpleDateFormat object with the time // pattern specified. // EEEE - print out complete text for day // MMMM - print out complete text of month // dd - print out the day in month in two digits // yyyy - print out the year in four digits // HH - print out the hour in the day, from 0-23 // in two digits // mm - print out the minutes in the hour in two // digits // ss - print out the seconds in the minute in // two digits daytimeFormat = new SimpleDateFormat("EEEE, MMMM dd, yyyy HH:mm:ss"); // Create the special time format. timeString = daytimeFormat.format(currentTime); // Convert the String to an array of bytes using the // platform's default character encoding. return timeString.getBytes(); } /** * This method provides an interface to stopping * the server. */ protected void stop() { if (keepRunning) { keepRunning = false; } } /** * Just in case, do some cleanup. */ public void finalize() { if (timeSocket != null) { timeSocket.close(); } } }
The DaytimeServer class uses a number of static final variables as constants, many of which are used to create the date string in the proper format. The main() method creates a DaytimeServer object and then invokes its startServing() method so that it accepts incoming requests.
The DaytimeServer constructor merely creates a UDP socket at the specified port. Note that as written, the server may require super-user privileges in order to run because it binds port 13. If you don't have permission to bind this port, the attempt to create a DatagramSocket will throw an exception. The constructor catches this and fails gracefully, informing you of the problem.
The DaytimeServer is an iterative server, whereas the server created in Chapter 24, "TCP Sockets," was a concurrent server. DaytimeServer processes each request in serial as they come in. Given the nature of the protocol--a single datagram comes in and the server immediately sends back a datagram with the time--an iterative server is most appropriate and is simpler to program.
The startServing() method is where the serving logic is implemented. While the application is intended to be running, it loops through a number of steps: It creates a small byte array and uses this array to create a DatagramPacket. The application then receives a datagram from the DatagramSocket. From the datagram, it obtains the IP address and port of the requesting application. The startServing() method need not read any information from the incoming datagram, as the datagram's arrival plus the meta-information it contains is sufficient for the server to understand the request.
The getTimeBuffer() method is called to obtain a byte array that contains the time in an appropriate format. By using this information, this method creates a new DatagramPacket. Finally, it sends this information through the DatagramSocket. The server loops through this process until interrupted externally.
This protected method creates an instance of Date class containing the current time, and then instantiates a SimpleDateFormat with a specific time pattern. It uses the SimpleDateFormat object to create a String with the data in the proper format. Finally, it returns the byte array corresponding to that String.
To run the server, first compile it with javac. Then, if necessary, log in as the super-user (for example, "root") and use java to run the server. If this is not possible, modify the TIME_PORT variable so that it binds to a port over 1024.
In the next example, you create a client to connect to this server.
The example used to create a UDP client makes use of the daytime server demonstrated previously but also illustrates communications with multiple servers through a single UDP socket. TimeCompare is a Java program that requests the time from a series of servers, receives their responses, and displays the difference between the remote system's times and the time of the local machine.
One of the most important aspects of this client is designing it so that an unanswered query does not hang the program. You cannot expect that every query will be answered. Thus, we need to use the setSoTimeout() method of the DatagramSocket instance before we call receive().
Listing 25.2 shows this application.
import java.io.*; // Import the package names used. import java.net.*; import java.util.*; import java.text.*; /** * This is an application to obtain the times from * various remote systems via UDP and then report * a comparison. * @author David W. Baker * @version 1.2 */ public class TimeCompare { private static final int TIME_PORT = 13; // Daytime port. private static final int TIMEOUT = 10000; // UDP timeout. // This is the size of the datagram data to send // for the query - intentially small. private static final int SMALL_ARRAY = 1; // This is the size of the datagram you expect to receive. private static final int TIME_ARRAY = 100; // A socket to send and receive datagrams. DatagramSocket timeSocket = null; // An array of addresses to the machines to query. private InetAddress[] remoteMachines; // The time on this machine. private Date localTime; // An array of datagram responses from remote machines. private DatagramPacket[] timeResponses; /** * This method starts the application. * @param args Command line arguments - remote hosts. */ public static void main(String[] args) { if (args.length < 1) { System.out.println( "Usage: TimeCompare host1 (host2 ... hostn)"); System.exit(1); } // Create an instance. TimeCompare runCompare = new TimeCompare(args); // Tell it to print out its data. runCompare.printTimes(); System.exit(0); // Exit. } /** * The constructor looks up the remote hosts and * creates a UDP socket. * @param hosts The hosts to contact. */ public TimeCompare(String[] hosts) { remoteMachines = new InetAddress[hosts.length]; // Look up all hosts and place in InetAddress[] array. for(int hostsFound = 0; hostsFound < hosts.length; hostsFound++) { try { remoteMachines[hostsFound] = InetAddress.getByName(hosts[hostsFound]); } catch(UnknownHostException excpt) { remoteMachines[hostsFound] = null; System.err.println("Unknown host " + hosts[hostsFound] + ": " + excpt); } } try { timeSocket = new DatagramSocket(); } catch(SocketException excpt) { System.err.println("Unable to bind UDP socket: " + excpt); System.exit(1); } // Perform the UDP communications. getTimes(); } /** * This method is the thread of execution where you * send out requests for times and then receive the * responses. */ public void getTimes() { DatagramPacket timeQuery; // A datagram to send as a query. DatagramPacket response; // A datagram response. byte[] emptyBuffer; // A byte array to build datagrams. int datagramsSent = 0; // # of queries successfully sent. // Send out a small UDP datagram to each machine, // asking it to respond with its time. for(int ips = 0;ips < remoteMachines.length; ips++) { if (remoteMachines[ips] != null) { try { emptyBuffer = new byte[SMALL_ARRAY]; timeQuery = new DatagramPacket(emptyBuffer, emptyBuffer.length, remoteMachines[ips], TIME_PORT); timeSocket.send(timeQuery); datagramsSent++; } catch(IOException excpt) { System.err.println("Unable to send to " + remoteMachines[ips] + ": " + excpt); } } } // Get current time to base the comparisons. localTime = new Date(); // Create an array in which to place responses. timeResponses = new DatagramPacket[datagramsSent]; // Set the socket timeout value. try { timeSocket.setSoTimeout(TIMEOUT); } catch(SocketException e) {} // Loop through and receive the number of responses // you are expecting. You break from this loop prematurely // if an InterruptedIOException occurs - that is, if // you wait more than TIMEOUT to receive another datagram. try { for(int got = 0; got < timeResponses.length; got++) { // Create a new buffer and datagram. emptyBuffer = new byte[TIME_ARRAY]; response = new DatagramPacket(emptyBuffer, emptyBuffer.length); // Receive a datagram, timing out if necessary. timeSocket.receive(response); // Now that you've received a response, add it // to the array of received datagrams. timeResponses[got] = response; } } catch(InterruptedIOException excpt) { System.err.println("Timeout on receive: " + excpt); } catch(IOException excpt) { System.err.println("Failed I/O: " + excpt); } // Close the socket. timeSocket.close(); timeSocket = null; } /** * This prints out a report comparing the times * sent from the remote hosts with the local * time. */ protected void printTimes() { Date remoteTime; String timeString; long secondsOff; InetAddress dgAddr; SimpleDateFormat daytimeFormat; System.out.print("TIME COMPARISON\n\tCurrent time " + "is: " + localTime + "\n\n"); // Iterate through each host. for(int hosts = 0; hosts < remoteMachines.length; hosts++) { if (remoteMachines[hosts] != null) { boolean found = false; int dataIndex; // Iterate through each datagram received. for(dataIndex = 0; dataIndex < timeResponses.length; dataIndex++) { // If the datagram element isn't null: if (timeResponses[dataIndex] != null) { dgAddr = timeResponses[dataIndex].getAddress(); // See if there's a match. if(dgAddr.equals(remoteMachines[hosts])) { found = true; break; } } } System.out.println("Host: " + remoteMachines[hosts]); // If there was a match, print comparison. if (found) { timeString = new String(timeResponses[dataIndex].getData()); int endOfLine = timeString.indexOf("\n"); if (endOfLine != -1) { timeString = timeString.substring(0,endOfLine); } // Create a SimpleDateFormat object with the time // pattern specified. // EEEE - print out complete text for day // MMMM - print out complete text of month // dd - print out the day in month in two digits // yyyy - print out the year in four digits // HH - print out the hour in the day, from 0-23 // in two digits // mm - print out the minutes in the hour in two // digits // ss - print out the seconds in the minute in // two digits daytimeFormat = new SimpleDateFormat("EEEE, MMMM dd, yyyy HH:mm:ss"); // Parse the string based on the pattern into a // Date object. remoteTime = daytimeFormat.parse(timeString, new ParseStatus()); // Find the difference. secondsOff = (localTime.getTime() - remoteTime.getTime()) / 1000; secondsOff = Math.abs(secondsOff); System.out.println("Time: " + timeString); System.out.println("Difference: " + secondsOff + " seconds\n"); } else { System.out.println("Time: NO RESPONSE FROM " + "HOST\n"); } } } } /** * This method performs any necessary cleanup. */ protected void finalize() { // If the socket is still open, close it. if (timeSocket != null) { timeSocket.close(); } } }
The main() method instantiates a TimeCompare object, passing it the command line arguments that correspond to the hosts to query. main() instructs the instance to print out its data and then exits.
The TimeCompare constructor uses the InetAddress.getByName() static method to look up the set of remote hosts, placing the returned InetAddress instances into an array of these objects. If it is unable to look up one of the hosts, this constructor ensures that the element is set to null and loops through the other hosts. The constructor creates a DatagramSocket at a dynamically allocated port and finally calls the getTimes() method to perform the queries.
The first thing this method does is iterate through the remoteMachines array. For each element that is not null, the getTimes() method creates a small byte array, uses it to construct an appropriately addressed DatagramPacket, and then sends the datagram using the UDP socket. Once sent, the method uses datagramsSent to keep track of how many queries were successfully sent.
Now that a datagram has been sent to each remote host, TimeCompare prepares to receive the responses. At this point, getTimes() collects the current time, used as a basis for comparison against the remote systems' times. It creates an array of type DatagramPacket. The length of this array is equal to the number of successful queries sent, which is the number of expected responses. getTimes() then invokes the setSoTimeout() method of the DatagramSocket instance so that accept() will not block forever if a server fails to respond.
getTimes() next enters a loop, attempting to receive a DatagramPacket for each query successfully sent. When a response is obtained, it places it into the timeResponses array. If the timeout on the receive() method expires, it will break out of the loop.
Once getTimes() has completed the loop to receive responses, it closes the DatagramSocket.
This method takes an array of UDP packets and prints out a comparison of the times contained therein. The outer for loop iterates through the machines contacted, while the inner for loop matches the host to a received datagram. If a match is found, printTimes() calculates the difference in times and prints the data. If no match is found, printTimes() indicates that a response from that host was not received.
Compile TimeCompare.java with the Java compiler and then execute it with the Java interpreter. Each argument to TimeCompare should be a host name of a remote machine to include in the comparison. For instance, to check your machine's time against www.sgi.com and www.paramount.com, you would type
java TimeCompare www.sgi.com www.paramount.com
and you would see a report that appeared as
TIME COMPARISON Current time is: Mon Aug 19 08:03:09 PDT 1996 Host: www.sgi.com/204.94.214.4 Time: Mon Aug 19 08:02:55 1996 Difference: 14 seconds Host: www.paramount.com/192.216.189.10 Time: Mon Aug 19 08:07:58 1996 Difference: 288 seconds
Internet Protocol (IP) is the means by which all information on the Internet is transmitted. UDP datagrams are encapsulated within IP packets in order to send them to the appropriate machines on the network.
See "Internet Protocol (IP)," Chapter 23
Most uses of IP involve unicasting--sending a packet from one host to another. However, IP is not limited to this mode and includes the ability to multicast. With multicasting, a message is addressed to a targeted set of hosts. One message is sent, and the entire group can receive it.
Multicasting is particularly suited to high-bandwidth applications, such as sending video and audio over the network, because a separate transmission need not be established (which could saturate the network). Other possible applications include chat sessions, distributed data storage, and online, interactive games. Also, multicasting may be used by a client searching for an appropriate server on the network--it can send a multicast solicitation, and any listening servers could contact the client to begin a transaction.
In order to support IP multicasting, a certain range of IP addresses are set aside solely for this purpose. These IP addresses are class D addresses, those within the range of 224.0.0.0 and 239.255.255.255. Each of these addresses is referred to as a multicast group. Any IP packet addressed to that group will be received by any machine which has joined that group. Group membership is dynamic and will change over time. To send a message to a group, a host need not be a member of that group.
When a machine joins a multicast group, it begins accepting messages sent to that IP multicast address. Extending the previous analogy from the section "UDP Socket Characteristics," joining a group is similar to constructing a new mailbox that accepts messages intended for the group. Each machine that wants to join the group constructs its own mailbox to receive the same message. If a multicast packet is distributed to a network, any machine that is listening for the message has an opportunity to receive it. That is, with IP multicasting, there is no mechanism for restricting which machines on the same network may join the group.
Multicast groups are mapped to hardware addresses on interface cards. Thus, IP multicast datagrams that reach an uninterested host can usually be rapidly discarded by the interface card. However, more than one multicast group maps to a single hardware address, making for imperfect hardware-level filtering. Some filtering must still be performed at the device driver or IP level.
Multicasting has its limitations, however--particularly the task of routing multicast packets throughout the Internet. A special TCP/IP protocol, Internet Group Management Protocol (IGMP), is used to manage memberships in a multicast group. A router that supports multi-casting can use IGMP to determine if local machines are subscribed to a particular group; such hosts respond with a report about groups they have joined using IGMP. Based on these communications, a multicast router can determine if it is appropriate to forward on a multicast packet.
CAUTION:
Realize that there is no formal way of reserving a multicast group for your own use. Certain groups are reserved for particular uses, assigned by the Internet Assigned Numbers Authority (IANA). These reserved groups are listed in RFC 1700, which can be obtained from:
ftp://ftp.internic.net/rfc/rfc1700.txtOther than avoiding a reserved group, there are few rules to choosing a group. The groups from 224.0.0.0 through 224.0.0.225 should never be passed on by a multicast router, restricting communications using them to the local subnet. Try picking an arbitrary address between 224.0.1.27 and 224.0.1.225.
If you happen to choose a group already being used, your communications will be disrupted by those other machines. Should this occur, quit your application and try another address.
TIP: Choose a TTL parameter as small as possible. A large TTL value can cause unnecessary bandwidth use throughout the Internet. Furthermore, you are more likely to disrupt other multicast communications in diverse areas that happen to be using the same group.
If your communications should be isolated to machines on the local network, choose a TTL of 1. When communicating with machines that are not on the local network, try to determine how many multicast routers exist along the way and set your TTL to be one more than that value.
The Java MulticastSocket class is the key to utilizing this powerful Internet networking feature. MulticastSocket allows you to send or receive UDP datagrams that use multicast IP. To send a datagram, you use the default constructor:
public MulticastSocket() throws IOException;
Then you must create an appropriately formed DatagramPacket addressed to a multicast group between 224.0.0.0 and 239.255.255.255. Once created, the datagram can be sent with the send() method, which requires a TTL value. The TTL indicates how many routers the packets should be allowed to go through. Avoid setting the TLL to a high value, which could cause the data to propagate through a large portion of the Internet. Here is an example:
int multiPort = 2222; int ttl = 1; InetAddress multiAddr = InetAddress.getByName("239.10.10.10"); byte[] multiBytes = new byte[256]; DatagramPacket multiDatagram = new DatagramPacket(multiBytes, multiBytes.length, multiAddr,multiPort); MulticastSocket multiSocket = new MulticastSocket(); multiSocket.send(multiDatagram, ttl);
To receive datagrams, an application must create a socket at a specific UDP port. Then, it must join the group of recipients. Through the socket, the application can then receive UDP datagrams:
MulticastSocket receiveSocket = new MulticastSocket(multiPort); receiveSocket.joinGroup(multiAddr); receiveSocket.receive(multiDatagram);
When the joinGroup() method is invoked, the machine now pays attention to any IP packets transmitted along the network for that particular multicast group. The host should also use IGMP to appropriately report the usage of the group. For machines with multiple IP addresses, the interface through which datagrams should be sent can be configured:
receiveSocket.setInterface(oneOfMyLocalAddrs);
To leave a multicast group, the leaveGroup() method is available. A MulticastSocket should be closed when communications are done.
receiveSocket.leaveGroup(multiAddr); receiveSocket.close();
As is apparent, using the MulticastSocket is very similar to using the normal UDP socket class DatagramSocket. The essential differences are:
The following two examples show a very simple use of multicasting. Listing 25.3 is a program that sends datagrams to a specific multicast IP address. The program is run with two arguments: the first specifying the multicast IP address to send the datagrams, the other specifying the UDP port of the listening applications. The main() method ensures that these arguments have been received and then instantiates a MultiCastSender object.
The constructor creates an InetAddress instance with the String representation of the multicast IP address. It then creates a MulticastSocket at a dynamically allocated port for sending datagrams. The constructor enters a while loop, reading in from standard input line- by-line. The program packages the first 256 bytes of each line into an appropriately addressed DatagramPacket, sending that datagram through the MulticastSocket.
import java.net.*; // Import package names used. import java.io.*; /** * This is a program which sends data from the command * line to a particular multicast group. * @author David W. Baker * @version 1.2 */ class MultiCastSender { // The number of Internet routers through which this // message should be passed. Keep this low. 1 is good // for local LAN communications. private static final byte TTL = 1; // The size of the data sent - basically the maximum // length of each line typed in at a time. private static final int DATAGRAM_BYTES = 512; private int mcastPort; private InetAddress mcastIP; private BufferedReader input; private MulticastSocket mcastSocket; /** * This starts up the application. * @param args Program arguments - <ip> <port> */ public static void main(String[] args) { // This must be the same port and IP address used // by the receivers. if (args.length != 2) { System.out.print("Usage: MultiCastSender <IP addr>" + " <port>\n\t<IP addr> can be one of 224.x.x.x " + "- 239.x.x.x\n"); System.exit(1); } MultiCastSender send = new MultiCastSender(args); System.exit(0); } /** * The constructor does all of the work of opening * the socket and sending datagrams through it. * @param args Program arguments - <ip> <port> */ public MultiCastSender(String[] args) { DatagramPacket mcastPacket; // UDP datagram. String nextLine; // Line from STDIN. byte[] mcastBuffer; // Buffer for datagram. byte[] lineData; // The data typed in. int sendLength; // Length of line. input = new BufferedReader(new InputStreamReader(System.in)); try { // Create a multicasting socket. mcastIP = InetAddress.getByName(args[0]); mcastPort = Integer.parseInt(args[1]); mcastSocket = new MulticastSocket(); } catch(UnknownHostException excpt) { System.err.println("Unknown address: " + excpt); System.exit(1); } catch(IOException excpt) { System.err.println("Unable to obtain socket: " + excpt); System.exit(1); } try { // Loop and read lines from standard input. while ((nextLine = input.readLine()) != null) { mcastBuffer = new byte[DATAGRAM_BYTES]; // If line is longer than your buffer, use the // length of the buffer available. if (nextLine.length() > mcastBuffer.length) { sendLength = mcastBuffer.length; // Otherwise, use the line's length. } else { sendLength = nextLine.length(); } // Convert the line of input to bytes. lineData = nextLine.getBytes(); // Copy the data into the blank byte array // whihc you will use to create the DatagramPacket. for (int i = 0; i < sendLength; i++) { mcastBuffer[i] = lineData[i]; } mcastPacket = new DatagramPacket(mcastBuffer, mcastBuffer.length,mcastIP,mcastPort); // Send the datagram. try { System.out.println("Sending:\t" + nextLine); mcastSocket.send(mcastPacket,TTL); } catch(IOException excpt) { System.err.println("Unable to send packet: " + excpt); } } } catch(IOException excpt) { System.err.println("Failed I/O: " + excpt); } mcastSocket.close(); // Close the socket. } }
Listing 25.4 complements the sender by receiving multicasted datagrams. The application takes two arguments that must correspond to the IP address and port with which the MultiCastSender was invoked. The main() method checks the command line arguments and then creates a MultiCastReceiver object.
The object's constructor creates an InetAddress and then a MulticastSocket at the port used to invoke the application. It joins the multicast group at the address contained within the InetAddress instance and then enters a loop. The object's constructor receives a datagram from the socket and prints the data contained within the datagram, indicating the machine and port from where the packet was sent.
import java.net.*; // Import package names used. import java.io.*; /** * This is a program which allows you to listen * at a particular multicast IP address/port and * print out incoming UDP datagrams. * @author David W. Baker * @version 1.1 */ class MultiCastReceiver { // The length of the data portion of incoming // datagrams. private static final int DATAGRAM_BYTES = 512; private int mcastPort; private InetAddress mcastIP; private MulticastSocket mcastSocket; // Boolean to tell the client to keep looping for // new datagrams. private boolean keepReceiving = true; /** * This starts up the application * @param args Program arguments - <ip> <port> */ public static void main(String[] args) { // This must be the same port and IP address // used by the sender. if (args.length != 2) { System.out.print("Usage: MultiCastReceiver <IP " + "addr> <port>\n\t<IP addr> can be one of " + "224.x.x.x - 239.x.x.x\n"); System.exit(1); } MultiCastReceiver send = new MultiCastReceiver(args); System.exit(0); } /** * The constructor does the work of opening a socket, * joining the multicast group, and printing out * incoming data. * @param args Program arguments - <ip> <port> */ public MultiCastReceiver(String[] args) { DatagramPacket mcastPacket; // Packet to receive. byte[] mcastBuffer; // byte[] array buffer InetAddress fromIP; // Sender address. int fromPort; // Sender port. String mcastMsg; // String of message. try { // First, set up your receiving socket. mcastIP = InetAddress.getByName(args[0]); mcastPort = Integer.parseInt(args[1]); mcastSocket = new MulticastSocket(mcastPort); // Join the multicast group. mcastSocket.joinGroup(mcastIP); } catch(UnknownHostException excpt) { System.err.println("Unknown address: " + excpt); System.exit(1); } catch(IOException excpt) { System.err.println("Unable to obtain socket: " + excpt); System.exit(1); } while (keepReceiving) { try { // Create a new datagram. mcastBuffer = new byte[DATAGRAM_BYTES]; mcastPacket = new DatagramPacket(mcastBuffer, mcastBuffer.length); // Receive the datagram. mcastSocket.receive(mcastPacket); fromIP = mcastPacket.getAddress(); fromPort = mcastPacket.getPort(); mcastMsg = new String(mcastPacket.getData()); // Print out the data. System.out.println("Received from " + fromIP + " on port " + fromPort + ": " + mcastMsg); } catch(IOException excpt) { System.err.println("Failed I/O: " + excpt); } } try { mcastSocket.leaveGroup(mcastIP); // Leave the group. } catch(IOException excpt) { System.err.println("Socket problem leaving group: " + excpt); } mcastSocket.close(); // Close the socket. } /** * This method provides a way to stop the program. */ public void stop() { if (keepReceiving) { keepReceiving = false; } } }
To run the applications, first compile MultiCastSender and MultiCastReceiver. Then, transfer the MultCastReceiver to other machines, so you can demonstrate more than one participant receiving messages. Finally, run the applications with the Java interpreter.
For instance, to send multicast messages to the group 224.0.1.30 on port 1111, you could do the following:
~/classes -> java MultiCastSender 224.0.1.30 1111 This is a test multicast message. Sending: This is a test multicast message. Have you received it? Sending: Have you received it?
To receive these messages, you would run the MultiCastReceiver application on one or more systems. You join the same multicast group, 224.0.1.30, and listen to the same port number, 1111.
~/classes -> java MultiCastReceiver 224.0.1.30 1111 Received from 204.160.73.131 on port 32911: This is a test multicast message. Received from 204.160.73.131 on port 32911: Have you received it?