Java UDP编程 - DatagramSocket

Published on 2017 - 05 - 07

To send or receive a DatagramPacket, you must open a datagram socket. In Java, a datagram socket is created and accessed through the DatagramSocket class:

public class DatagramSocket extends Object

All datagram sockets bind to a local port, on which they listen for incoming data and which they place in the header of outgoing datagrams. If you’re writing a client, you don’t care what the local port is, so you call a constructor that lets the system assign an unused port (an anonymous port). This port number is placed in any outgoing datagrams and will be used by the server to address any response datagrams. If you’re writing a server, clients need to know on which port the server is listening for incoming datagrams; therefore, when a server constructs a DatagramSocket, it specifies the local port on which it will listen. However, the sockets used by clients and servers are otherwise identical: they differ only in whether they use an anonymous (system-assigned) or a well-known port. There’s no distinction between client sockets and server sockets, as there is with TCP. There’s no such thing as a DatagramServerSocket.

The Constructors

The DatagramSocket constructors are used in different situations, much like the DatagramPacket constructors. The first constructor opens a datagram socket on an anonymous local port. The second constructor opens a datagram socket on a well-known local port that listens to all local network interfaces. The last two constructors open a datagram socket on a well-known local port on a specific network interface. All constructors deal only with the local address and port. The remote address and port are stored in the DatagramPacket, not the DatagramSocket. Indeed, one DatagramSocket can send and receive datagrams from multiple remote hosts and ports.

public DatagramSocket() throws SocketException

This constructor creates a socket that is bound to an anonymous port. For example:

try {
  DatagramSocket client = new DatagramSocket();
  // send packets...
} catch (SocketException ex) {
  System.err.println(ex);
}

Pick this constructor for a client that initiates a conversation with a server. In this scenario, you don’t care what port the socket is bound to because the server will send its response to the port from which the datagram originated. Letting the system assign a port means that you don’t have to worry about finding an unused port.

The same socket can receive the datagrams that a server sends back to it. The constructor throws a SocketException if the socket can’t bind to a port. It’s unusual for this constructor to throw an exception; it’s hard to imagine situations in which the socket could not be opened, because the system gets to choose an available port.

public DatagramSocket(int port) throws SocketException

This constructor creates a socket that listens for incoming datagrams on a particular port, specified by the port argument. Use this constructor to write a server that listens on a well-known port. A SocketException is thrown if the socket can’t be created. There are two common reasons for the constructor to fail: the specified port is already occupied, or you are trying to connect to a port below 1024 and you don’t have sufficient privileges (i.e., you are not root on a Unix system; for better or worse, other platforms allow anyone to connect to low-numbered ports).

TCP ports and UDP ports are not related. Two different programs can use the same port number if one uses UDP and the other uses TCP. Example 4 is a port scanner that looks for UDP ports in use on the local host. It decides that the port is in use if the DatagramSocket constructor throws an exception. As written, it looks at ports from 1024 and up to avoid Unix’s requirement that it run as root to bind to ports below 1024. You can easily extend it to check ports below 1024, however, if you have root access or are running it on Windows.

import java.net.*;

public class UDPPortScanner {

  public static void main(String[] args) {
    for (int port = 1024; port <= 65535; port++) {
      try {
        // the next line will fail and drop into the catch block if
        // there is already a server running on port i
        DatagramSocket server = new DatagramSocket(port);
        server.close();
      } catch (SocketException ex) {
        System.out.println("There is a server on port " + port + ".");
      }
    }
  }
}

Here are the results from the Linux workstation on which much of the code in this book was written:

% java UDPPortScanner
There is a server on port 2049.
There is a server on port 32768.
There is a server on port 32770.
There is a server on port 32771.

The first port, 2049, is an NFS server. The high-numbered ports in the 30,000 range are Remote Procedure Call (RPC) services. Along with RPC, common protocols that use UDP include NFS, TFTP, and FSP.

It’s much harder to scan UDP ports on a remote system than to scan for remote TCP ports. Whereas there’s always some indication that a listening port, regardless of application layer protocol, has received your TCP packet, UDP provides no such guarantees. To determine that a UDP server is listening, you have to send it a packet it will recognize and respond to.

public DatagramSocket(int port, InetAddress interface) throws SocketException

This constructor is primarily used on multihomed hosts; it creates a socket that listens for incoming datagrams on a specific port and network interface. The port argument is the port on which this socket listens for datagrams. As with TCP sockets, you need to be root on a Unix system to create a DatagramSocket on a port below 1024. The address argument is an InetAddress object matching one of the host’s network addresses. A SocketException is thrown if the socket can’t be created. There are three common reasons for this constructor to fail: the specified port is already occupied, you are trying to connect to a port below 1024 and you’re not root on a Unix system, or address is not the address of one of the system’s network interfaces.

public DatagramSocket(SocketAddress interface) throws SocketException

This constructor is similar to the previous one except that the network interface address and port are read from a SocketAddress. For example, this code fragment creates a socket that only listens on the local loopback address:

SocketAddress address = new InetSocketAddress("127.0.0.1", 9999);
DatagramSocket socket = new DatagramSocket(address);

protected DatagramSocket(DatagramSocketImpl impl) throws SocketException

This constructor enables subclasses to provide their own implementation of the UDP protocol, rather than blindly accepting the default. Unlike sockets created by the other four constructors, this socket is not initially bound to a port. Before using it, you have to bind it to a SocketAddress using the bind() method:

public void bind(SocketAddress addr) throws SocketException

You can pass null to this method, binding the socket to any available address and port.

Sending and Receiving Datagrams

The primary task of the DatagramSocket class is to send and receive UDP datagrams. One socket can both send and receive. Indeed, it can send and receive to and from multiple hosts at the same time.

public void send(DatagramPacket dp) throws IOException

Once a DatagramPacket is created and a DatagramSocket is constructed, send the packet by passing it to the socket’s send() method. For example, if theSocket is a DatagramSocket object and theOutput is a DatagramPacket object, send theOutput using theSocket like this:

theSocket.send(theOutput);

If there’s a problem sending the data, this method may throw an IOException. However, this is less common with DatagramSocket than Socket or ServerSocket, because the unreliable nature of UDP means you won’t get an exception just because the packet doesn’t arrive at its destination. You may get an IOException if you’re trying to send a larger datagram than the host’s native networking software supports, but then again you may not. This depends heavily on the native UDP software in the OS and the native code that interfaces between this and Java’s DatagramSocketImpl class. This method may also throw a SecurityException if the SecurityManager won’t let you communicate with the host to which the packet is addressed. This is primarily a problem for applets and other remotely loaded code.

Example 5 is a UDP-based discard client. It reads lines of user input from System.in and sends them to a discard server, which simply discards all the data. Each line is stuffed in a DatagramPacket. Many of the simpler Internet protocols, such as discard and echo, have both TCP and UDP implementations.

import java.net.*;
import java.io.*;

public class UDPDiscardClient {

  public final static int PORT = 9;

  public static void main(String[] args) {

    String hostname = args.length > 0 ?  args[0] : "localhost";

    try (DatagramSocket theSocket = new DatagramSocket()) {
      InetAddress server = InetAddress.getByName(hostname);
      BufferedReader userInput
          = new BufferedReader(new InputStreamReader(System.in));
      while (true) {
        String theLine = userInput.readLine();
        if (theLine.equals(".")) break;
        byte[] data = theLine.getBytes();
        DatagramPacket theOutput
            = new DatagramPacket(data, data.length, server, PORT);
        theSocket.send(theOutput);
      } // end while
    } catch (IOException ex) {
      System.err.println(ex);
    }
  }
}

The UDPDiscardClient class should look familiar. It has a single static field, PORT, which is set to the standard port for the discard protocol (port 9), and a single method, main(). The main() method reads a hostname from the command line and converts that hostname to the InetAddress object called server. A BufferedReader is chained to System.in to read user input from the keyboard. Next, a DatagramSocket object called theSocket is constructed. After creating the socket, the program enters an infinite while loop that reads user input line by line using readLine(). Example 5 is careful, however, to use only readLine() to read data from the console, the one place where it is guaranteed to work as advertised. Because the discard protocol deals only with raw bytes, it can ignore character encoding issues.

In the while loop, each line is converted to a byte array using the getBytes() method, and the bytes are stuffed in a new DatagramPacket, theOutput. Finally, theOutput is sent over theSocket, and the loop continues. If at any point the user types a period on a line by itself, the program exits. The DatagramSocket constructor may throw a SocketException, so that needs to be caught. Because this is a discard client, you don’t need to worry about data coming back from the server.

public void receive(DatagramPacket dp) throws IOException

This method receives a single UDP datagram from the network and stores it in the preexisting DatagramPacket object dp. Like the accept() method in the ServerSocket class, this method blocks the calling thread until a datagram arrives. If your program does anything besides wait for datagrams, you should call receive() in a separate thread.

The datagram’s buffer should be large enough to hold the data received. If it’s not, receive() places as much data in the buffer as it can hold; the rest is lost. Remember that the maximum size of the data portion of a UDP datagram is 65,507 bytes. (That’s the 65,536-byte maximum size of an IP datagram minus the 20-byte size of the IP header and the 8-byte size of the UDP header.) Some application protocols that use UDP further restrict the maximum number of bytes in a packet; for instance, NFS uses a maximum packet size of 8,192 bytes.

If there’s a problem receiving the data, receive() may throw an IOException. In practice, this is rare because problems like dropped packets that would shut down a TCP stream are silently discarded by the network or network stack before Java ever sees them.

Example 6 shows a UDP discard server that receives incoming datagrams. Just for fun, it logs the data in each datagram to System.out so that you can see who’s sending what to your discard server.

import java.net.*;
import java.io.*;

public class UDPDiscardServer {

  public final static int PORT = 9;
  public final static int MAX_PACKET_SIZE = 65507;

  public static void main(String[] args) {

    byte[] buffer = new byte[MAX_PACKET_SIZE];

    try (DatagramSocket server = new DatagramSocket(PORT)) {
      DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
      while (true) {
        try {
          server.receive(packet);
          String s = new String(packet.getData(), 0, packet.getLength(), "8859_1");
          System.out.println(packet.getAddress() + " at port "
              + packet.getPort() + " says " + s);
          // reset the length for the next packet
          packet.setLength(buffer.length);
        } catch (IOException ex) {
          System.err.println(ex);
        }
       } // end while
    } catch (SocketException  ex) {
      System.err.println(ex);
    }
  }
}

This is a simple class with a single method, main(). It reads the port the server listens to from the command line. If the port is not specified on the command line, it listens on port 9. It then opens a DatagramSocket on that port and creates a DatagramPacket with a 65,507-byte buffer—large enough to receive any possible packet. Then the server enters an infinite loop that receives packets and prints the contents and the originating host on the console. There’s no particular encoding expected for discard packets. Indeed, there’s no particular reason these packets have to be text at all. I somewhat arbitrarily picked the Latin-1 ISO 8859-1 encoding because it’s ASCII compatible and defines a character for every byte.

As each datagram is received, the length of packet is set to the length of the data in that datagram. Consequently, as the last step of the loop, the length of the packet is reset to the maximum possible value. Otherwise, the incoming packets would be limited to the minimum size of all previous packets. You can run the discard client on one machine and connect to the discard server on a second machine to verify that the network is working.

public void close()

Calling a DatagramSocket object’s close() method frees the port occupied by that socket. As with streams and TCP sockets, you’ll want to take care to close the datagram socket in a finally block:

DatagramSocket server = null
try {
  server = new DatagramSocket();
  // use the socket...
} catch (IOException ex) {
  System.err.println(ex);
} finally {
  try {
    if (server != null) server.close();
  } catch (IOException ex) {
  }
}

In Java 7, DatagramSocket implements AutoCloseable so you can use try-with-resources:

try (DatagramSocket server = new DatagramSocket()) {
  // use the socket...
}

It’s never a bad idea to close a DatagramSocket when you’re through with it; it’s particularly important to close an unneeded socket if the program will continue to run for a significant amount of time. For example, the close() method was essential in Example 4, UDPPortScanner: if this program did not close the sockets it opened, it would tie up every UDP port on the system for a significant amount of time. On the other hand, if the program ends as soon as you’re through with the DatagramSocket, you don’t need to close the socket explicitly; the socket is automatically closed upon garbage collection. However, Java won’t run the garbage collector just because you’ve run out of ports or sockets, unless by lucky happenstance you run out of memory at the same time. Closing unneeded sockets never hurts and is good programming practice.

public int getLocalPort()

A DatagramSocket’s getLocalPort() method returns an int that represents the local port on which the socket is listening. Use this method if you created a DatagramSocket with an anonymous port and want to find out what port the socket has been assigned. For example:

DatagramSocket ds = new DatagramSocket();
System.out.println("The socket is using port " + ds.getLocalPort());
public InetAddress getLocalAddress()

A DatagramSocket’s getLocalAddress() method returns an InetAddress object that represents the local address to which the socket is bound. It’s rarely needed in practice. Normally, you either already know or simply don’t care which address a socket is listening to.

public SocketAddress getLocalSocketAddress()

The getLocalSocketAddress() method returns a SocketAddress object that wraps the local interface and port to which the socket is bound. Like getLocalAddress(), it’s a little hard to imagine a realistic use case here. This method probably exists mostly for parallelism with setLocalSocketAddress().

Managing Connections

Unlike TCP sockets, datagram sockets aren’t very picky about whom they’ll talk to. In fact, by default they’ll talk to anyone; but this is often not what you want. For instance, applets are only allowed to send datagrams to and receive datagrams from the applet host. An NFS or FSP client should accept packets only from the server it’s talking to. A networked game should listen to datagrams only from the people playing the game. The next five methods let you choose which host you can send datagrams to and receive datagrams from, while rejecting all others’ packets.

public void connect(InetAddress host, int port)

The connect() method doesn’t really establish a connection in the TCP sense. However, it does specify that the DatagramSocket will only send packets to and receive packets from the specified remote host on the specified remote port. Attempts to send packets to a different host or port will throw an IllegalArgumentException. Packets received from a different host or a different port will be discarded without an exception or other notification.

A security check is made when the connect() method is invoked. If the VM is allowed to send data to that host and port, the check passes silently. If not, a SecurityException is thrown. However, once the connection has been made, send() and receive() on that DatagramSocket no longer make the security checks they’d normally make.

public void disconnect()

The disconnect() method breaks the “connection” of a connected DatagramSocket so that it can once again send packets to and receive packets from any host and port.

public int getPort()

If and only if a DatagramSocket is connected, the getPort() method returns the remote port to which it is connected. Otherwise, it returns –1.

public InetAddress getInetAddress()

If and only if a DatagramSocket is connected, the getInetAddress() method returns the address of the remote host to which it is connected. Otherwise, it returns null.

public InetAddress getRemoteSocketAddress()

If a DatagramSocket is connected, the getRemoteSocketAddress() method returns the address of the remote host to which it is connected. Otherwise, it returns null.

Socket Options

Java supports six socket options for UDP:

  • SO_TIMEOUT
  • SO_RCVBUF
  • SO_SNDBUF
  • SO_REUSEADDR
  • SO_BROADCAST
  • IP_TOS

SO_TIMEOUT

SO_TIMEOUT is the amount of time, in milliseconds, that receive() waits for an incoming datagram before throwing an InterruptedIOException, which is a subclass of IOException. Its value must be nonnegative. If SO_TIMEOUT is 0, receive() never times out. This value can be changed with the setSoTimeout() method and inspected with the getSoTimeout() method:

public void setSoTimeout(int timeout) throws SocketException
public int getSoTimeout() throws IOException

The default is to never time out, and indeed there are few situations in which you need to set SO_TIMEOUT. You might need it if you were implementing a secure protocol that required responses to occur within a fixed amount of time. You might also decide that the host you’re communicating with is dead (unreachable or not responding) if you don’t receive a response within a certain amount of time.

The setSoTimeout() method sets the SO_TIMEOUT field for a datagram socket. When the timeout expires, a blocked receive() method throws a SocketTimeoutException. Set this option before you call receive(). You cannot change it while receive() is waiting for a datagram. The timeout argument must be greater than or equal to zero. For example:

try {
  byte[] buffer = new byte[2056];
  DatagramPacket dp = new DatagramPacket(buffer, buffer.length);
  DatagramSocket ds = new DatagramSocket(2048);
  ds.setSoTimeout(30000); // block for no more than 30 seconds
  try {
   ds.receive(dp);
    // process the packet...
  } catch (SocketTimeoutException ex) {
    ss.close();
    System.err.println("No connection within 30 seconds");
  }
} catch (SocketException ex) {
  System.err.println(ex);
} catch (IOException ex) {
  System.err.println("Unexpected IOException: " + ex);
}

The getSoTimeout() method returns the current value of this DatagramSocket object’s SO_TIMEOUT field. For example:

public void printSoTimeout(DatagramSocket ds) {
  int timeout = ds.getSoTimeOut();
  if (timeout > 0) {
    System.out.println(ds + " will time out after "
     + timeout + "milliseconds.");
  } else if (timeout == 0) {
    System.out.println(ds + " will never time out.");
  } else {
    System.out.println("Something is seriously wrong with " + ds);
  }
}

SO_RCVBUF

The SO_RCVBUF option of DatagramSocket is closely related to the SO_RCVBUF option of Socket. It determines the size of the buffer used for network I/O. Larger buffers tend to improve performance for reasonably fast (say, Ethernet-speed) connections because they can store more incoming datagrams before overflowing. Sufficiently large receive buffers are even more important for UDP than for TCP, because a UDP datagram that arrives when the buffer is full will be lost, whereas a TCP datagram that arrives at a full buffer will eventually be retransmitted. Furthermore, SO_RCVBUF sets the maximum size of datagram packets that can be received by the application. Packets that won’t fit in the receive buffer are silently discarded.

DatagramSocket has methods to set and get the suggested receive buffer size used for network input:

public void setReceiveBufferSize(int size) throws SocketException
public int getReceiveBufferSize() throws SocketException

The setReceiveBufferSize() method suggests a number of bytes to use for buffering input from this socket. However, the underlying implementation is free to ignore this suggestion. For instance, many 4.3 BSD-derived systems have a maximum receive buffer size of about 52K and won’t let you set a limit higher than this. My Linux box was limited to 64K. Other systems raise this to about 240K. The details are highly platform-dependent. Consequently, you may wish to check the actual size of the receive buffer with getReceiveBufferSize() after setting it. The getReceiveBufferSize() method returns the number of bytes in the buffer used for input from this socket.

Both methods throw a SocketException if the underlying socket implementation does not recognize the SO_RCVBUF option. This might happen on a non-POSIX operating system. The setReceiveBufferSize() method will throw an IllegalArgumentException if its argument is less than or equal to zero.

SO_SNDBUF

DatagramSocket has methods to get and set the suggested send buffer size used for network output:

public void setSendBufferSize(int size)  throws SocketException
public int getSendBufferSize() throws SocketException

The setSendBufferSize() method suggests a number of bytes to use for buffering output on this socket. Once again, however, the operating system is free to ignore this suggestion. Consequently, you’ll want to check the result of setSendBufferSize() by immediately following it with a call to getSendBufferSize() to find out the real buffer size.

Both methods throw a SocketException if the underlying native network software doesn’t understand the SO_SNDBUF option. The setSendBufferSize() method also throws an IllegalArgumentException if its argument is less than or equal to zero.

SO_REUSEADDR

The SO_REUSEADDR option does not mean the same thing for UDP sockets as it does for TCP sockets. For UDP, SO_REUSEADDR controls whether multiple datagram sockets can bind to the same port and address at the same time. If multiple sockets are bound to the same port, received packets will be copied to all bound sockets. This option is controlled by these two methods:

public void setReuseAddress(boolean on) throws SocketException
public boolean getReuseAddress() throws SocketException

For this to work reliably, setReuseAddress() must be called before the new socket binds to the port. This means the socket must be created in an unconnected state using the protected constructor that takes a DatagramImpl as an argument. In other words, it won’t work with a plain vanilla DatagramSocket. Reusable ports are most commonly used for multicast sockets, which will be discussed in the next chapter. Datagram channels also create unconnected datagram sockets that can be configured to reuse ports.

SO_BROADCAST

The SO_BROADCAST option controls whether a socket is allowed to send packets to and receive packets from broadcast addresses such as 192.168.254.255, the local network broadcast address for the network with the local address 192.168.254.*. UDP broadcasting is often used for protocols such as DHCP that need to communicate with servers on the local net whose addresses are not known in advance. This option is controlled with these two methods:

public void setBroadcast(boolean on) throws SocketException
public boolean getBroadcast() throws SocketException

Routers and gateways do not normally forward broadcast messages, but they can still kick up a lot of traffic on the local network. This option is turned on by default, but if you like you can disable it thusly:

socket.setBroadcast(false);

This option can be changed after the socket has been bound.

IP_TOS

Because the traffic class is determined by the value of the IP_TOS field in each IP packet header, it is essentially the same for UDP as it is for TCP. After all, packets are actually routed and prioritized according to IP, which both TCP and UDP sit on top of. There’s really no difference between the setTrafficClass() and getTrafficClass() methods in DatagramSocket and those in Socket. They just have to be repeated here because DatagramSocket and Socket don’t have a common superclass. These two methods let you inspect and set the class of service for a socket using these two methods:

public int getTrafficClass() throws SocketException
public void setTrafficClass(int trafficClass) throws SocketException

The traffic class is given as an int between 0 and 255. Because this value is copied to an eight-bit field in the TCP header, only the low-order byte of this int is used; and values outside this range cause IllegalArgumentExceptions.

This code fragment sets a socket to use Expedited Forwarding by setting the traffic class to 10111000:

DatagramSocket s = new DatagramSocket();
s.setTrafficClass(0xB8); // 10111000 in binary

The underlying socket implementation is not required to respect any of these requests. Some implementations ignore these values completely. Android in particular treats the setTrafficClass() method as a noop. If the native network stack is unable to provide the requested class of service, Java may but is not required to throw a SocketException.

Some Useful Applications

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

Simple UDP Clients

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

The UDPPoke class has four private fields. The bufferSize field specifies how large a return packet is expected. An 8,192-byte buffer is large enough for most of the protocols that UDPPoke is useful for, but it can be increased by passing a different value to the constructor. The timeout field specifies how long to wait for a response. The host and the port fields specify the remote host to connect to.

If the buffer length is not specified, 8,192 bytes is used. If the timeout is not given, 30 seconds (30,000 milliseconds) is used. The host, port, and buffer size are also used to construct the outgoing DatagramPacket. Although in theory you should be able to send a datagram with no data at all, bugs in some Java implementations require that you add at least one byte of data to the datagram. The simple servers we’re currently considering ignore this data.

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

The main() method merely reads the host and port to connect to from the command line, constructs a UDPPoke object, and pokes it. Most of the simple protocols that this client suits will return ASCII text, so this example attempts to convert the response to an ASCII string and print it.

import java.io.*;
import java.net.*;

public class UDPPoke {

  private int bufferSize; // in bytes
  private int timeout; // in milliseconds
  private InetAddress host;
  private int port;

 public UDPPoke(InetAddress host, int port, int bufferSize, int timeout) {
    this.bufferSize = bufferSize;
    this.host = host;
    if (port < 1 || port > 65535) {
      throw new IllegalArgumentException("Port out of range");
    }

    this.port = port;
    this.timeout = timeout;
  }

  public UDPPoke(InetAddress host, int port, int bufferSize) {
    this(host, port, bufferSize, 30000);
  }

  public UDPPoke(InetAddress host, int port) {
    this(host, port, 8192, 30000);
  }

  public byte[] poke() {
    try (DatagramSocket socket = new DatagramSocket(0)) {
      DatagramPacket outgoing = new DatagramPacket(new byte[1], 1, host, port);
      socket.connect(host, port);
      socket.setSoTimeout(timeout);

      socket.send(outgoing);
      DatagramPacket incoming
          = new DatagramPacket(new byte[bufferSize], bufferSize);
      // next line blocks until the response is received
      socket.receive(incoming);
      int numBytes = incoming.getLength();
      byte[] response = new byte[numBytes];
      System.arraycopy(incoming.getData(), 0, response, 0, numBytes);
      return response;
    } catch (IOException ex) {
      return null;
    }
  }

  public static void main(String[] args) {
    InetAddress host;
    int port = 0;
    try {
      host = InetAddress.getByName(args[0]);
      port = Integer.parseInt(args[1]);
    } catch (RuntimeException | UnknownHostException ex) {
      System.out.println("Usage: java UDPPoke host port");
      return;
    }

    try {
      UDPPoke poker = new UDPPoke(host, port);
      byte[] response = poker.poke();
      if (response == null) {
        System.out.println("No response within allotted time");
        return;
    }
      String result = new String(response, "US-ASCII");
      System.out.println(result);
    } catch (UnsupportedEncodingException ex) {
      // Really shouldn't happen
      ex.printStackTrace();
    }
  }
}

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

$ java UDPPoke rama.poly.edu 13
Sun Oct  3 13:04:22 2009

This connects to a chargen server:

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

Given this class, UDP daytime, time, chargen, and quote of the day clients are almost trivial. A time client is only slightly harder, and only because you need to convert the four raw bytes returned by the server to a java.util.Date object.

import java.net.*;
import java.util.*;

public class UDPTimeClient {

  public final static int PORT = 37;
  public final static String DEFAULT_HOST = "time.nist.gov";

  public static void main(String[] args) {

    InetAddress host;
    try {
      if (args.length > 0) {
        host = InetAddress.getByName(args[0]);
      } else {
        host = InetAddress.getByName(DEFAULT_HOST);
      }
    } catch (RuntimeException | UnknownHostException ex) {
      System.out.println("Usage: java UDPTimeClient [host]");
      return;
    }

    UDPPoke poker = new UDPPoke(host, PORT);
    byte[] response = poker.poke();
    if (response == null) {
      System.out.println("No response within allotted time");
      return;
    } else if (response.length != 4) {
      System.out.println("Unrecognized response format");
      return;
    }

    // The time protocol sets the epoch at 1900,
    // the Java Date class at 1970. This number
    // converts between them.

    long differenceBetweenEpochs = 2208988800L;

    long secondsSince1900 = 0;
    for (int i = 0; i < 4; i++) {
      secondsSince1900
          = (secondsSince1900 << 8) | (response[i] & 0x000000FF);
    }

    long secondsSince1970
        = secondsSince1900 - differenceBetweenEpochs;
    long msSince1970 = secondsSince1970 * 1000;
    Date time = new Date(msSince1970);

    System.out.println(time);
  }
}

UDPServer

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

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

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

Assuming this class may be used as part of other programs that do more than just run one server, you need a way to shut it down. This is provided by the shutDown() method, which sets a flag. The main loop checks this flag each pass to see if it should exit. Because the receive() call can block indefinitely if there’s no traffic, you also set a timeout on the socket. This will wake it up once every 10 seconds to check for shutdown whether there’s traffic or not.

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

import java.io.*;
import java.net.*;
import java.util.logging.*;

public abstract class UDPServer implements Runnable {

  private final int bufferSize; // in bytes
  private final int port;
  private final Logger logger = Logger.getLogger(UDPServer.class.getCanonicalName());
  private volatile boolean isShutDown = false;

  public UDPServer(int port, int bufferSize) {
    this.bufferSize = bufferSize;
    this.port = port;
  }

  public UDPServer(int port) {
    this(port, 8192);
  }

  @Override
  public void run() {
    byte[] buffer = new byte[bufferSize];
    try (DatagramSocket socket = new DatagramSocket(port)) {
      socket.setSoTimeout(10000); // check every 10 seconds for shutdown
      while (true) {
        if (isShutDown) return;
        DatagramPacket incoming = new DatagramPacket(buffer, buffer.length);
        try {
          socket.receive(incoming);
          this.respond(socket, incoming);
        } catch (SocketTimeoutException ex) {
          if (isShutDown) return;
        } catch (IOException ex) {
          logger.log(Level.WARNING, ex.getMessage(), ex);
        }
      } // end while
    } catch (SocketException ex) {
      logger.log(Level.SEVERE, "Could not bind to port: " + port, ex);
    }
  }

  public abstract void respond(DatagramSocket socket, DatagramPacket request)
      throws IOException;

public void shutDown() {
    this.isShutDown = true;
  }

}

The easiest protocol to handle is discard. All that’s needed is a main() method that sets the port and starts the thread. respond() is a do-nothing method. Example 10 is a high-performance UDP discard server that does nothing with incoming packets.

import java.net.*;

public class FastUDPDiscardServer extends UDPServer {

  public final static int DEFAULT_PORT = 9;

  public FastUDPDiscardServer() {
    super(DEFAULT_PORT);
  }

  public static void main(String[] args) {
    UDPServer server = new FastUDPDiscardServer();
    Thread t = new Thread(server);
    t.start();
  }

  @Override
  public void respond(DatagramSocket socket, DatagramPacket request) {
  }
}

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

import java.io.*;
import java.net.*;

public class UDPEchoServer extends UDPServer {

  public final static int DEFAULT_PORT = 7;

  public UDPEchoServer() {
    super(DEFAULT_PORT);
  }

  @Override
  public void respond(DatagramSocket socket, DatagramPacket packet)
      throws IOException {
    DatagramPacket outgoing = new DatagramPacket(packet.getData(),
        packet.getLength(), packet.getAddress(), packet.getPort());
    socket.send(outgoing);
  }

  public static void main(String[] args) {
    UDPServer server = new UDPEchoServer();
    Thread t = new Thread(server);
    t.start();
  }
}

A UDP Echo Client

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

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

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

import java.net.*;

public class UDPEchoClient {

  public final static int PORT = 7;

  public static void main(String[] args) {

    String hostname = "localhost";
    if (args.length > 0) {
      hostname = args[0];
    }

    try {
      InetAddress ia = InetAddress.getByName(hostname);
      DatagramSocket socket = new DatagramSocket();
      SenderThread sender = new SenderThread(socket, ia, PORT);
      sender.start();
      Thread receiver = new ReceiverThread(socket);
      receiver.start();
    } catch (UnknownHostException ex) {
      System.err.println(ex);
    } catch (SocketException ex) {
      System.err.println(ex);
    }
  }
}

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

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

import java.io.*;
import java.net.*;

class SenderThread extends Thread {

  private InetAddress server;
  private DatagramSocket socket;
  private int port;
  private volatile boolean stopped = false;

  SenderThread(DatagramSocket socket, InetAddress address, int port) {
    this.server = address;
    this.port = port;
    this.socket = socket;
    this.socket.connect(server, port);
  }

  public void halt() {
    this.stopped = true;
  }

  @Override
  public void run() {
    try {
      BufferedReader userInput
          = new BufferedReader(new InputStreamReader(System.in));
      while (true) {
        if (stopped) return;
        String theLine = userInput.readLine();
        if (theLine.equals(".")) break;
        byte[] data = theLine.getBytes("UTF-8");
        DatagramPacket output
            = new DatagramPacket(data, data.length, server, port);
        socket.send(output);
        Thread.yield();
      }
    } catch (IOException ex) {
      System.err.println(ex);
    }
  }
}

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

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

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

import java.io.*;
import java.net.*;

class ReceiverThread extends Thread {

  private DatagramSocket socket;
  private volatile boolean stopped = false;

  ReceiverThread(DatagramSocket socket) {
    this.socket = socket;
  }

  public void halt() {
    this.stopped = true;
  }

  @Override
  public void run() {
    byte[] buffer = new byte[65507];
    while (true) {
      if (stopped) return;
      DatagramPacket dp = new DatagramPacket(buffer, buffer.length);
      try {
        socket.receive(dp);
        String s = new String(dp.getData(), 0, dp.getLength(), "UTF-8");
        System.out.println(s);
        Thread.yield();
      } catch (IOException ex) {
        System.err.println(ex);
      }
    }
  }
}

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

Reference