package games.strategy.net.nio; import java.io.IOException; import java.net.Socket; import java.net.SocketAddress; import java.net.SocketException; import java.nio.channels.ClosedChannelException; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.LinkedBlockingQueue; import java.util.logging.Level; import java.util.logging.Logger; /** * A thread that reads socket data using NIO from a collection of sockets.<br> * Data is read in packets, and placed in the output queye.<br> * Packets are placed in the output queue in order they are read from the socket. */ public class NIOReader { private static final Logger logger = Logger.getLogger(NIOReader.class.getName()); private final LinkedBlockingQueue<SocketReadData> outputQueue = new LinkedBlockingQueue<>(); private volatile boolean running = true; private final Map<SocketChannel, SocketReadData> reading = new ConcurrentHashMap<>(); private final IErrorReporter errorReporter; private final Selector selector; private final Object socketsToAddMutex = new Object(); private final List<SocketChannel> socketsToAdd = new ArrayList<>(); private long totalBytes; public NIOReader(final IErrorReporter reporter, final String threadSuffix) { errorReporter = reporter; try { selector = Selector.open(); } catch (final IOException e) { logger.log(Level.SEVERE, "Could not create Selector", e); throw new IllegalStateException(e); } final Thread t = new Thread(() -> loop(), "NIO Reader - " + threadSuffix); t.start(); } public void shutDown() { running = false; try { selector.close(); } catch (final Exception e) { logger.log(Level.WARNING, "error closing selector", e); } } public void add(final SocketChannel channel) { synchronized (socketsToAddMutex) { socketsToAdd.add(channel); selector.wakeup(); } } private void selectNewChannels() { List<SocketChannel> toAdd = null; synchronized (socketsToAddMutex) { if (socketsToAdd.isEmpty()) { return; } toAdd = new ArrayList<>(socketsToAdd); socketsToAdd.clear(); } for (final SocketChannel channel : toAdd) { try { channel.register(selector, SelectionKey.OP_READ); } catch (final ClosedChannelException e) { // this is ok, the channel is closed, so dont bother reading it return; } } } private void loop() { while (running) { try { if (logger.isLoggable(Level.FINEST)) { logger.finest("selecting..."); } try { // exceptions can be thrown here, nothing we can do // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4729342 selector.select(); } catch (final Exception e) { logger.log(Level.INFO, "error reading selection", e); } if (!running) { continue; } selectNewChannels(); final Set<SelectionKey> selected = selector.selectedKeys(); if (logger.isLoggable(Level.FINEST)) { logger.finest("selected:" + selected.size()); } final Iterator<SelectionKey> iter = selected.iterator(); while (iter.hasNext()) { final SelectionKey key = iter.next(); iter.remove(); if (key.isValid() && key.isReadable()) { final SocketChannel channel = (SocketChannel) key.channel(); final SocketReadData packet = getReadData(channel); if (logger.isLoggable(Level.FINEST)) { logger.finest("reading packet:" + packet + " from:" + channel.socket().getRemoteSocketAddress()); } try { final boolean done = packet.read(channel); if (done) { totalBytes += packet.size(); if (logger.isLoggable(Level.FINE)) { String remote = "null"; final Socket s = channel.socket(); SocketAddress sa = null; if (s != null) { sa = s.getRemoteSocketAddress(); } if (sa != null) { remote = sa.toString(); } logger.log(Level.FINE, " done reading from:" + remote + " size:" + packet.size() + " readCalls;" + packet.getReadCalls() + " total:" + totalBytes); } enque(packet); } } catch (final Exception e) { logger.log(Level.FINER, "exception reading", e); key.cancel(); errorReporter.error(channel, e); } } else if (!key.isValid()) { logger.fine("Remotely closed"); final SocketChannel channel = (SocketChannel) key.channel(); key.cancel(); errorReporter.error(channel, new SocketException("triplea:key cancelled")); } } } catch (final Exception e) { // catch unhandles exceptions to that the reader // thread doesnt die logger.log(Level.WARNING, "error in reader", e); } } } private void enque(final SocketReadData packet) { reading.remove(packet.getChannel()); outputQueue.offer(packet); } private SocketReadData getReadData(final SocketChannel channel) { if (reading.containsKey(channel)) { return reading.get(channel); } final SocketReadData packet = new SocketReadData(channel); reading.put(channel, packet); return packet; } public SocketReadData take() throws InterruptedException { return outputQueue.take(); } public void closed(final SocketChannel channel) { reading.remove(channel); } }