服务端:Selector, DatagramChannel, ByteBuffer
客户端: DatagramChannel, ByteBuffer
区别:
a. 服务端没有与TCP的ServerSocketChannel相对应的Channel,服务端和客户端之间更趋于平等,不过服务端的端口号还是固定的。
b. Selector在处理完读取操作后,会触发写操作:发送数据到客户端。
1. 服务端
public class UDPEchoServerSelector { private static final int TIMEOUT = 4000; // 超时 (毫秒) private static final int CAPACITY = 255; public static void main(String[] args) throws IOException { args = new String[1]; args[0] = "4451"; int servPort = Integer.parseInt(args[0]); Selector sel = Selector.open(); // 创建选择器,可以处理多路通道。 DatagramChannel channel = DatagramChannel.open(); channel.configureBlocking(false); channel.socket().bind(new InetSocketAddress(servPort)); // 通道关联的socket绑定地址 channel.register(sel, SelectionKey.OP_READ, new ClientData()); while (true) { // 持续运行,接收和返回数据 if (sel.select(TIMEOUT) == 0) { System.out.println("No I/O needs to be processed"); continue; } Iteratoriter = sel.selectedKeys().iterator(); // 获取可操作的选择键集合 while (iter.hasNext()) { SelectionKey key = iter.next(); // 键为位掩码 if (key.isReadable()) { // 客户端有数据发送过来 handleRead(key); } if (key.isValid() && key.isWritable()) { // 通道正常,且客户端需要响应 handleWrite(key); } iter.remove(); // 从集合中移除选择键 } } } private static void handleRead(SelectionKey key) throws IOException { DatagramChannel channel = (DatagramChannel) key.channel(); ClientData clntDat = (ClientData) key.attachment(); clntDat.buffer.clear(); clntDat.clientAddress = channel.receive(clntDat.buffer); // 获取客户端的地址,用以发送响应 if (clntDat.clientAddress != null) { // 接收到数据 key.interestOps(SelectionKey.OP_WRITE); // 关注客户端读取响应 } } private static void handleWrite(SelectionKey key) throws IOException { DatagramChannel channel = (DatagramChannel) key.channel(); ClientData clntDat = (ClientData) key.attachment(); clntDat.buffer.flip(); // 从起始位置开始发送 int bytesSent = channel.send(clntDat.buffer, clntDat.clientAddress); if (bytesSent != 0) { key.interestOps(SelectionKey.OP_READ); // 关注客户端发送数据 } } public static class ClientData { public SocketAddress clientAddress; public ByteBuffer buffer = ByteBuffer.allocate(CAPACITY); } }
2. 客户端
public class UDPEchoClient { private static final int CAPACITY = 255; private static final String UTF8 = "UTF-8"; public static void main(String[] args) throws IOException { args = new String[2]; args[0] = "localhost"; args[1] = "4451"; String servName = args[0]; int servPort = Integer.parseInt(args[1]); DatagramChannel clntChan = DatagramChannel.open(); clntChan.configureBlocking(false); clntChan.connect(new InetSocketAddress(servName, servPort)); ByteBuffer sentBuffer = ByteBuffer.wrap("Hello UDP".getBytes(UTF8)); int bytesSent = clntChan.write(sentBuffer); // 向服务器发送数据 System.out.println("UDP client sent " + bytesSent + " bytes"); ByteBuffer recvBuffer = ByteBuffer.allocate(CAPACITY); clntChan.receive(recvBuffer); // 读取响应 recvBuffer.flip(); System.out.println("UDP client received: " + new String(recvBuffer.array(), UTF8)); } }
客户端连接服务器后,发送数据可以用write或send方法:
... clntChan.connect(new InetSocketAddress(servName, servPort)); ... int bytesSent = clntChan.write(sentBuffer);
... clntChan.connect(new InetSocketAddress(servName, servPort)); ... int bytesSent = clntChan.send(sentBuffer, new InetSocketAddress(servName, servPort)); // 发送地址必须和前面的连接地址相同,否则报java.lang.IllegalArgumentException: Connected address not equal to target address
客户端没有连接服务器,需要用send方法指定目标地址:
... int bytesSent = clntChan.send(sentBuffer, new InetSocketAddress(servName, servPort)); // 使用write方法会报java.nio.channels.NotYetConnectedException