package org.threadly.litesockets; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.DatagramChannel; import java.nio.channels.SelectableChannel; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import org.threadly.concurrent.future.ListenableFuture; import org.threadly.concurrent.future.SettableListenableFuture; import org.threadly.util.Pair; /** * A Simple UDP Server. * * This is a UDP socket implementation for litesockets. This UDPServer is treated like a * TCPServer. It will notify the ClientAcceptor any time a new unique ip:port send a packet to this * UDP socket. The UDPServer does not technically "Accept" new connections it just reads data from the socket * and that data also has the host/port of where it came from. * * You can also just create a {@link UDPClient} from a server to initiate a connection to another UDP server, if * that server sends data back from that same port/ip pair it will show up as a read in the created client. */ public class UDPServer extends Server { public static final int DEFAULT_FRAME_SIZE = 1500; /** * UDPFilter enum. * * @author lwahlmeier * */ public static enum UDPFilterMode {WhiteList, BlackList}; private final ConcurrentHashMap<InetSocketAddress, UDPClient> clients = new ConcurrentHashMap<InetSocketAddress, UDPClient>(); private final ConcurrentLinkedQueue<Pair<InetSocketAddress, ByteBuffer>> writeQueue = new ConcurrentLinkedQueue<Pair<InetSocketAddress, ByteBuffer>>(); private final ConcurrentLinkedQueue<SettableListenableFuture<Long>> writeFutures = new ConcurrentLinkedQueue<SettableListenableFuture<Long>>(); private final ConcurrentHashMap<InetAddress, Integer> filter = new ConcurrentHashMap<InetAddress, Integer>(); private final DatagramChannel channel; private volatile UDPFilterMode filterMode = UDPFilterMode.BlackList; private volatile UDPReader setUDPReader = null; private volatile int frameSize = DEFAULT_FRAME_SIZE; private volatile ClientAcceptor clientAcceptor; protected UDPServer(final SocketExecuter sei, final String host, final int port) throws IOException { super(sei); channel = DatagramChannel.open(); channel.socket().bind(new InetSocketAddress(host, port)); channel.configureBlocking(false); } @Override public void start() { getSocketExecuter().setUDPServerOperations(this, true); } @Override public void stop() { getSocketExecuter().setUDPServerOperations(this, false); } /** * Sets the UDPfilterMode for the server. This allows us to white or black list IP/ports from being accepted. * * NOTE: calling set on this also resets any hosts currently already in the filter! * * @param fm the UDPFilterMode to use. */ public void setFilterMode(UDPFilterMode fm) { filterMode = fm; filter.clear(); } /** * Adds a host to the filter. How this filter will apply depends on what the UDPFilterMode is set to in the UDPServer. * A port number of 0 means we block/accept all ports for that host. * * @param isa the InetSocketAddress to use for the filter. */ public void filterHost(InetSocketAddress isa) { filter.put(isa.getAddress(), isa.getPort()); } /** * Sets the frame size for this UDPServer. This will also set it on all clients that are spawned from this server. * * @param size the frame size in bytes. */ public void setFrameSize(final int size) { frameSize = size; } /** * Gets the frame size for this UDPServer. * * @return the max allowed UDP frame size. */ public int getFrameSize() { return frameSize; } @Override public void acceptChannel(final SelectableChannel c) { if(c.equals(channel)) { final ByteBuffer bb = ByteBuffer.allocate(frameSize); try { final InetSocketAddress isa = (InetSocketAddress)channel.receive(bb); if(filterMode == UDPFilterMode.BlackList && filter.size() > 0) { Integer port = filter.get(isa.getAddress()); if(port != null && (port == 0 || port == isa.getPort())) { return; } } else if (filterMode == UDPFilterMode.WhiteList) { Integer port = filter.get(isa.getAddress()); if(port == null || (port != 0 && port != isa.getPort())) { return; } } bb.flip(); getSocketExecuter().getExecutorFor(isa).execute(new NewDataRunnable(this, isa, bb)); } catch (IOException e) { } } } @Override public WireProtocol getServerType() { return WireProtocol.UDP; } @Override public DatagramChannel getSelectableChannel() { return channel; } @Override public ClientAcceptor getClientAcceptor() { return clientAcceptor; } @Override public void setClientAcceptor(final ClientAcceptor clientAcceptor) { this.clientAcceptor = clientAcceptor; } @Override public void close() { if(setClosed()) { try { getSocketExecuter().stopListening(this); channel.close(); } catch (IOException e) { //Dont Care } finally { this.callClosers(); } } } protected int doWrite() { Pair<InetSocketAddress, ByteBuffer> wdp = writeQueue.poll(); SettableListenableFuture<Long> slf = writeFutures.poll(); if(wdp != null) { try { return channel.send(wdp.getRight(), wdp.getLeft()); } catch (IOException e) { return 0; } finally { if(slf != null) { slf.setResult(0L); } } } return 0; } protected boolean needsWrite() { return !writeQueue.isEmpty(); } /** * Allows you to write to the UDPServer directly without a UDPClient. * * * @param bb The {@link ByteBuffer} to write. * @param remoteAddress the remote host/port to write too. * @return a {@link ListenableFuture} that will be completed once the ByteBuffer for this write is put on the socket. */ public ListenableFuture<?> write(final ByteBuffer bb, final InetSocketAddress remoteAddress) { SettableListenableFuture<Long> slf = new SettableListenableFuture<Long>(); this.writeFutures.add(slf); writeQueue.add(new Pair<InetSocketAddress, ByteBuffer>(remoteAddress, bb)); getSocketExecuter().setUDPServerOperations(this, true); return slf; } /** * Sets a {@link UDPReader} for this server. This can be used to intercept reads before they create/call on a UDPClient. * * Set to null to remove it. * * @param udpReader the {@link UDPReader} to use for this UDPServer. */ public void setUDPReader(final UDPReader udpReader) { this.setUDPReader = udpReader; } /** * Creates a new client from this UDPServer. If a client is already created for that * source address that client will be returned. * * @param host the remote host to send data to. * @param port the port on that host to send data to. * @return a {@link UDPClient} pointing to that remote address. */ public UDPClient createUDPClient(final String host, final int port) { final InetSocketAddress sa = new InetSocketAddress(host,port); if(! clients.containsKey(sa)) { final UDPClient c = new UDPClient(new InetSocketAddress(host, port), this); clients.putIfAbsent(sa, c); } return clients.get(sa); } /** * Internal class used to deal with udpData, either creating a client for it or * adding to an existing client. * @author lwahlmeier * */ private static class NewDataRunnable implements Runnable { private final InetSocketAddress isa; private final ByteBuffer bb; private final UDPServer us; public NewDataRunnable(final UDPServer us, final InetSocketAddress isa, final ByteBuffer bb) { this.us = us; this.isa = isa; this.bb = bb; } @SuppressWarnings("resource") @Override public void run() { UDPReader reader = us.setUDPReader; if(reader == null || reader.onUDPRead(bb.duplicate(), isa)) { if(! us.clients.containsKey(isa)) { UDPClient udpc = new UDPClient(isa, us); udpc = us.clients.putIfAbsent(isa, udpc); if(udpc == null) { udpc = us.clients.get(isa); us.clientAcceptor.accept(udpc); } } final UDPClient udpc = us.clients.get(isa); if(udpc.canRead()) { udpc.addReadBuffer(bb); } } } } /** * The {@link UDPServer} UDPReader. If a UDPReader is set on a UDPServer every read from the socket * will call onUDPRead before being passed to a UDPClient. If false is returned the UDPPacket will not * be sent to the UDPClient, if true, then it will be. * * @author lwahlmeier * */ public interface UDPReader { /** * This is called whenever the UDPServer reads data from the socket. * * @param bb the ByteBuffer containing the data from the read. * @param isa the SocketAddress of who sent the data. * @return true if the data should be passed onto the UDPClient, false if it should not be. */ public boolean onUDPRead(ByteBuffer bb, InetSocketAddress isa); } }