Java UDP编程 - DatagramChannel

Published on 2017 - 05 - 07

The DatagramChannel class is used for nonblocking UDP applications, in the same way as SocketChannel and ServerSocketChannel are used for nonblocking TCP applications. Like SocketChannel and ServerSocketChannel, DatagramChannel is a subclass of SelectableChannel that can be registered with a Selector. This is useful in servers where one thread can manage communications with multiple clients. However, UDP is by its nature much more asynchronous than TCP so the net effect is smaller. In UDP, a single datagram socket can process requests from multiple clients for both input and output. What the DatagramChannel class adds is the ability to do this in a nonblocking fashion, so methods return quickly if the network isn’t immediately ready to receive or send data.

DatagramChannel is a near-complete alternate API for UDP. In Java 6 and earlier, you still need to use the DatagramSocket class to bind a channel to a port. However, you do not have to use it thereafter, and you don’t have to use it all in Java 7 and later. Nor do you ever use DatagramPacket. Instead, you read and write byte buffers, just as you do with a SocketChannel.

Opening a socket

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

DatagramChannel channel = DatagramChannel.open();

This channel is not initially bound to any port. To bind it, you access the channel’s peer DatagramSocket object using the socket() method. For example, this binds a channel to port 3141:

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

Java 7 adds a convenient bind() method directly to DatagramChannel, so you don’t have to use a DatagramSocket at all. For example:

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

Receiving

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

public SocketAddress receive(ByteBuffer dst) throws IOException

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

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

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

import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;

public class UDPDiscardServerWithChannels {

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

  public static void main(String[] args) {

    try {
      DatagramChannel channel = DatagramChannel.open();
      DatagramSocket socket = channel.socket();
      SocketAddress address = new InetSocketAddress(PORT);
      socket.bind(address);
      ByteBuffer buffer = ByteBuffer.allocateDirect(MAX_PACKET_SIZE);
      while (true) {
        SocketAddress client = channel.receive(buffer);
        buffer.flip();
        System.out.print(client + " says ");
        while (buffer.hasRemaining()) System.out.write(buffer.get());
        System.out.println();
        buffer.clear();
      }
    } catch (IOException ex) {
      System.err.println(ex);
    }
  }
}

Sending

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

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

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

The send() method returns the number of bytes written. This will either be the number of bytes that were available in the buffer to be written or zero, nothing in between. It is zero if the channel is in nonblocking mode and the data can’t be sent immediately. Otherwise, if the channel is not in nonblocking mode, send() simply waits to return until it can send all the data in the buffer.

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

import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;

public class UDPEchoServerWithChannels {

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

  public static void main(String[] args) {

    try {
      DatagramChannel channel = DatagramChannel.open();
      DatagramSocket socket = channel.socket();
      SocketAddress address = new InetSocketAddress(PORT);
      socket.bind(address);
      ByteBuffer buffer = ByteBuffer.allocateDirect(MAX_PACKET_SIZE);
      while (true) {
        SocketAddress client = channel.receive(buffer);
        buffer.flip();
        channel.send(buffer, client);
        buffer.clear();
      }
    } catch (IOException ex) {
      System.err.println(ex);
    }
  }
}

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

Connecting

Once you’ve opened a datagram channel, you connect it to a particular remote address using the connect() method:

SocketAddress remote = new InetSocketAddress("time.nist.gov", 37);
channel.connect(remote);

The channel will only send data to or receive data from this host. Unlike the connect() method of SocketChannel, this method alone does not send or receive any packets across the network because UDP is a connectionless protocol. It merely establishes the host it will send packets to when there’s data ready to be sent. Thus, connect() returns fairly quickly, and doesn’t block in any meaningful sense. There’s no need here for a finishConnect() or isConnectionPending() method. There is an isConnected() method that returns true if and only if the DatagramSocket is connected:

public boolean isConnected()

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

Finally, the disconnect() method breaks the connection:

public DatagramChannel disconnect() throws IOException

This doesn’t really close anything because nothing was really open in the first place. It just allows the channel to be connected to a different host in the future.

Reading

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

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

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

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

  • The channel is nonblocking and no packet was ready.
  • A datagram packet contained no data.
  • The buffer is full.

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

Writing

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

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

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

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

You can use the read and write methods to implement a simple UDP echo client. On the client side, it’s easy to connect before sending. Because packets may be lost in transit (always remember UDP is unreliable), you don’t want to tie up the sending while waiting to receive a packet. Thus, you can take advantage of selectors and nonblocking I/O. This time, though, rather than sending text data, let’s send one hundred ints from 0 to 99. You’ll print out the values returned so it will be easy to figure out if any packets are being lost. Example 17 demonstrates.

import java.io.*;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.util.*;

public class UDPEchoClientWithChannels {

  public  final static int PORT = 7;
  private final static int LIMIT = 100;

  public static void main(String[] args) {

    SocketAddress remote;
    try {
      remote = new InetSocketAddress(args[0], PORT);
    } catch (RuntimeException ex) {
      System.err.println("Usage: java UDPEchoClientWithChannels host");
      return;
    }

    try (DatagramChannel channel = DatagramChannel.open()) {
      channel.configureBlocking(false);
      channel.connect(remote);

      Selector selector = Selector.open();
      channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);

      ByteBuffer buffer = ByteBuffer.allocate(4);
      int n = 0;
      int numbersRead = 0;
      while (true) {
        if (numbersRead == LIMIT) break;
        // wait one minute for a connection
        selector.select(60000);
        Set<SelectionKey> readyKeys = selector.selectedKeys();
        if (readyKeys.isEmpty() && n == LIMIT) {
          // All packets have been written and it doesn't look like any
          // more are will arrive from the network
          break;
        }
        else {
          Iterator<SelectionKey> iterator = readyKeys.iterator();
          while (iterator.hasNext()) {
            SelectionKey key = (SelectionKey) iterator.next();
            iterator.remove();
            if (key.isReadable()) {
              buffer.clear();
              channel.read(buffer);
              buffer.flip();
              int echo = buffer.getInt();
              System.out.println("Read: " + echo);
              numbersRead++;
            }
            if (key.isWritable()) {
              buffer.clear();
              buffer.putInt(n);
              buffer.flip();
              channel.write(buffer);
              System.out.println("Wrote: " + n);
              n++;
              if (n == LIMIT) {
                // All packets have been written; switch to read-only mode
                key.interestOps(SelectionKey.OP_READ);
              }
            }
          }
        }
      }
      System.out.println("Echoed " + numbersRead + " out of " + LIMIT +
                                                               " sent");
      System.out.println("Success rate: " + 100.0 * numbersRead / LIMIT +
                                                               "%");
    } catch (IOException ex) {
      System.err.println(ex);
    }
  }
}

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

A typical run produced output like this:

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

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

Closing

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

public void close() throws IOException

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

public boolean isOpen()

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

Like all channels, in Java 7 DatagramChannel implements AutoCloseable so you can use it in try-with-resources statements. Prior to Java 7, close it in a finally block if you can. By now the pattern should be quite familiar. In Java 6 and earlier:

DatagramChannel channel = null;
try {
  channel = DatagramChannel.open();
  // Use the channel...
} catch (IOException ex) {
  // handle exceptions...
} finally {
  if (channel != null) {
    try {
      channel.close();
    } catch (IOException ex) {
      // ignore
    }
  }
}

and in Java 7 and later:

try (DatagramChannel channel = DatagramChannel.open()) {
  // Use the channel...
} catch (IOException ex) {
  // handle exceptions...
}

Socket Options // Java 7

In Java 7 and later, DatagramChannel supports eight socket options listed in Table 1.

Option Type Constant Purpose
SO_SNDBUF StandardSocketOptions. Integer Size of the buffer used for sending datagram packets
SO_RCVBUF StandardSocketOptions.SO_RCVBUF Integer Size of the buffer used for receiving datagram packets
SO_REUSEADDR StandardSocketOptions.SO_REUSEADDR Boolean Enable/disable address reuse
SO_BROADCAST StandardSocketOptions.SO_BROADCAST Boolean Enable/disable broadcast messages
IP_TOS StandardSocketOptions.IP_TOS Integer Traffic class
IP_MULTICAST_IF StandardSocketOptions.IP_MULTICAST_IF NetworkInterface Local network interface to use for multicast
IP_MULTICAST_TTL StandardSocketOptions.IP_MULTICAST_TTL Integer Time-to-live value for multicast datagrams
IP_MULTICAST_LOOP StandardSocketOptions.IP_MULTICAST_LOOP Boolean Enable/disable loopback of multicast datagrams

The first five options have the same meanings as they do for datagram sockets as described in Socket Options.
.
These are inspected and configured by just three methods:

public <T> DatagramChannel setOption(SocketOption<T> name, T value)
    throws IOException
public <T> T getOption(SocketOption<T> name) throws IOException
public Set<SocketOption<?>> supportedOptions()

The supportedOptions() method lists the available socket options. The getOption() method tells you the current value of any of these. And setOption() lets you change the value. For example, suppose you want to send a broadcast message. SO_BROADCAST is usually turned off by default, but you can switch it on like so:

try (DatagramChannel channel = DatagramChannel.open()) {
  channel.setOption(StandardSocketOptions.SO_BROADCAST, true);
  // Send the broadcast message...
} catch (IOException ex) {
  // handle exceptions...
}

Example 18 opens a channel just to check the default values of these options.

import java.io.IOException;
import java.net.SocketOption;
import java.nio.channels.DatagramChannel;

public class DefaultSocketOptionValues {

  public static void main(String[] args) {
    try (DatagramChannel channel = DatagramChannel.open()) {
      for (SocketOption<?> option : channel.supportedOptions()) {
        System.out.println(option.name() + ": " + channel.getOption(option));
      }
    } catch (IOException ex) {
      ex.printStackTrace();
    }
  }
}

Here’s the output I got on my Mac:

IP_MULTICAST_TTL: 1
SO_BROADCAST: false
SO_REUSEADDR: false
SO_RCVBUF: 196724
IP_MULTICAST_LOOP: true
SO_SNDBUF: 9216
IP_MULTICAST_IF: null
IP_TOS: 0

It’s a bit surprising that my send buffer is so much larger than my receive buffer.

Reference