package games.strategy.net.nio;
import java.io.IOException;
import java.net.Socket;
import java.net.SocketAddress;
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.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A thread that writes socket data using NIO .<br>
* Data is written in packets that are enqued on our buffer.
* Packets are sent to the sockets in the order that they are received.
*/
public class NIOWriter {
private static final Logger s_logger = Logger.getLogger(NIOWriter.class.getName());
private final Selector m_selector;
private final IErrorReporter m_errorReporter;
// this is the data we are writing
private final Map<SocketChannel, List<SocketWriteData>> m_writing =
new HashMap<>();
// these are the sockets we arent selecting on, but should now
private List<SocketChannel> m_socketsToWake = new ArrayList<>();
// the writing thread and threads adding data to write synchronize on this lock
private final Object m_mutex = new Object();
private long m_totalBytes = 0;
private volatile boolean m_running = true;
public NIOWriter(final IErrorReporter reporter, final String threadSuffix) {
m_errorReporter = reporter;
try {
m_selector = Selector.open();
} catch (final IOException e) {
s_logger.log(Level.SEVERE, "Could not create Selector", e);
throw new IllegalStateException(e);
}
final Thread t = new Thread(() -> loop(), "NIO Writer - " + threadSuffix);
t.start();
}
public void shutDown() {
m_running = false;
try {
m_selector.close();
} catch (final IOException e) {
s_logger.log(Level.WARNING, "error closing selector", e);
}
}
private void addNewSocketsToSelector() {
List<SocketChannel> socketsToWriteCopy;
synchronized (m_mutex) {
if (m_socketsToWake.isEmpty()) {
return;
}
socketsToWriteCopy = m_socketsToWake;
m_socketsToWake = new ArrayList<>();
}
for (final SocketChannel channel : socketsToWriteCopy) {
try {
channel.register(m_selector, SelectionKey.OP_WRITE);
} catch (final ClosedChannelException e) {
s_logger.log(Level.FINEST, "socket already closed", e);
}
}
}
private void loop() {
while (m_running) {
try {
if (s_logger.isLoggable(Level.FINEST)) {
s_logger.finest("selecting...");
}
try {
// exceptions can be thrown here, nothing we can do
// http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4729342
m_selector.select();
} catch (final Exception e) {
s_logger.log(Level.INFO, "error reading selection", e);
}
if (!m_running) {
continue;
}
// select any new sockets that can be written to
addNewSocketsToSelector();
final Set<SelectionKey> selected = m_selector.selectedKeys();
if (s_logger.isLoggable(Level.FINEST)) {
s_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.isWritable()) {
final SocketChannel channel = (SocketChannel) key.channel();
final SocketWriteData packet = getData(channel);
if (packet != null) {
try {
if (s_logger.isLoggable(Level.FINEST)) {
s_logger.finest("writing packet:" + packet + " to:" + channel.socket().getRemoteSocketAddress());
}
final boolean done = packet.write(channel);
if (done) {
m_totalBytes += packet.size();
if (s_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();
}
s_logger.log(Level.FINE, " done writing to:" + remote + " size:" + packet.size() + " writeCalls;"
+ packet.getWriteCalls() + " total:" + m_totalBytes);
}
removeLast(channel);
}
} catch (final Exception e) {
s_logger.log(Level.FINER, "exception writing", e);
m_errorReporter.error(channel, e);
key.cancel();
}
} else {
// nothing to write
// cancel the key, otherwise we will
// spin forever as the socket will always be writable
key.cancel();
}
}
}
} catch (final Exception e) {
// catch unhandles exceptions to that the writer
// thread doesnt die
s_logger.log(Level.WARNING, "error in writer", e);
}
}
}
/**
* Remove the data for this channel.
*/
public void closed(final SocketChannel channel) {
removeAll(channel);
}
private void removeAll(final SocketChannel to) {
synchronized (m_mutex) {
m_writing.remove(to);
}
}
private void removeLast(final SocketChannel to) {
synchronized (m_mutex) {
final List<SocketWriteData> values = m_writing.get(to);
if (values == null) {
s_logger.log(Level.SEVERE, "NO socket data to:" + to + " all:" + values);
return;
}
values.remove(0);
// remove empty lists, so we can detect that we need to wake up the socket
if (values.isEmpty()) {
m_writing.remove(to);
}
}
}
private SocketWriteData getData(final SocketChannel to) {
synchronized (m_mutex) {
if (!m_writing.containsKey(to)) {
return null;
}
final List<SocketWriteData> values = m_writing.get(to);
if (values.isEmpty()) {
return null;
}
return values.get(0);
}
}
public void enque(final SocketWriteData data, final SocketChannel channel) {
synchronized (m_mutex) {
if (!m_running) {
return;
}
if (m_writing.containsKey(channel)) {
m_writing.get(channel).add(data);
} else {
final List<SocketWriteData> values = new ArrayList<>();
values.add(data);
m_writing.put(channel, values);
m_socketsToWake.add(channel);
m_selector.wakeup();
}
}
}
}