package games.strategy.net.nio;
import java.io.ByteArrayInputStream;
import java.io.Externalizable;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.net.Socket;
import java.nio.channels.SocketChannel;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import games.strategy.engine.message.HubInvocationResults;
import games.strategy.engine.message.HubInvoke;
import games.strategy.engine.message.SpokeInvocationResults;
import games.strategy.engine.message.SpokeInvoke;
import games.strategy.net.CouldNotLogInException;
import games.strategy.net.INode;
import games.strategy.net.IObjectStreamFactory;
import games.strategy.net.MessageHeader;
import games.strategy.net.Node;
import games.strategy.net.nio.QuarantineConversation.ACTION;
/**
* A thread to Decode messages from a reader.
*/
public class Decoder {
private static final Logger logger = Logger.getLogger(Decoder.class.getName());
private final NIOReader reader;
private volatile boolean running = true;
private final IErrorReporter errorReporter;
private final IObjectStreamFactory objectStreamFactory;
private final NIOSocket nioSocket;
/**
* These sockets are quarantined. They have not logged in, and messages
* read from them are not passed outside of the quarantine conversation.
*/
private final ConcurrentHashMap<SocketChannel, QuarantineConversation> quarantine =
new ConcurrentHashMap<>();
private final Thread thread;
public Decoder(final NIOSocket nioSocket, final NIOReader reader, final IErrorReporter reporter,
final IObjectStreamFactory objectStreamFactory, final String threadSuffix) {
this.reader = reader;
errorReporter = reporter;
this.objectStreamFactory = objectStreamFactory;
this.nioSocket = nioSocket;
thread = new Thread(() -> loop(), "Decoder -" + threadSuffix);
thread.start();
}
public void shutDown() {
running = false;
thread.interrupt();
}
private void loop() {
while (running) {
try {
SocketReadData data;
try {
data = reader.take();
} catch (final InterruptedException e) {
continue;
}
if (data == null || !running) {
continue;
}
if (logger.isLoggable(Level.FINEST)) {
logger.finest("Decoding packet:" + data);
}
final ByteArrayInputStream stream = new ByteArrayInputStream(data.getData());
try {
final MessageHeader header = readMessageHeader(data.getChannel(), objectStreamFactory.create(stream));
if (logger.isLoggable(Level.FINEST)) {
logger.log(Level.FINEST, "header decoded:" + header);
}
// make sure we are still open
final Socket s = data.getChannel().socket();
if (!running || s == null || s.isInputShutdown()) {
continue;
}
final QuarantineConversation converstation = quarantine.get(data.getChannel());
if (converstation != null) {
sendQuarantine(data.getChannel(), converstation, header);
} else {
if (nioSocket.getLocalNode() == null) {
throw new IllegalStateException("we are writing messages, but no local node");
}
if (header.getFrom() == null) {
throw new IllegalArgumentException("Null from:" + header);
}
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "decoded msg:" + header.getMessage() + " size:" + data.size());
}
nioSocket.messageReceived(header, data.getChannel());
}
} catch (final Exception ioe) {
// we are reading from memory here
// there should be no network errors, something
// is odd
logger.log(Level.SEVERE, "error reading object", ioe);
errorReporter.error(data.getChannel(), ioe);
}
} catch (final Exception e) {
// catch unhandles exceptions to that the decoder
// thread doesnt die
logger.log(Level.WARNING, "error in decoder", e);
}
}
}
private void sendQuarantine(final SocketChannel channel, final QuarantineConversation conversation,
final MessageHeader header) {
final ACTION a = conversation.message(header.getMessage());
if (a == ACTION.TERMINATE) {
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "Terminating quarantined connection to:" + channel.socket().getRemoteSocketAddress());
}
conversation.close();
// we need to indicate the channel was closed
errorReporter.error(channel, new CouldNotLogInException());
} else if (a == ACTION.UNQUARANTINE) {
if (logger.isLoggable(Level.FINER)) {
logger.log(Level.FINER, "Accepting quarantined connection to:" + channel.socket().getRemoteSocketAddress());
}
nioSocket.unquarantine(channel, conversation);
quarantine.remove(channel);
}
}
private MessageHeader readMessageHeader(final SocketChannel channel, final ObjectInputStream objectInput)
throws IOException, ClassNotFoundException {
INode to;
if (objectInput.read() == 1) {
to = null;
} else {
if (objectInput.read() == 1) {
// this may be null if we
// have not yet fully joined the network
to = nioSocket.getLocalNode();
} else {
to = new Node();
((Node) to).readExternal(objectInput);
}
}
INode from;
final int readMark = objectInput.read();
if (readMark == 1) {
from = nioSocket.getRemoteNode(channel);
} else if (readMark == 2) {
from = null;
} else {
from = new Node();
((Node) from).readExternal(objectInput);
}
Serializable message;
final byte type = (byte) objectInput.read();
if (type != Byte.MAX_VALUE) {
final Externalizable template = getTemplate(type);
template.readExternal(objectInput);
message = template;
} else {
message = (Serializable) objectInput.readObject();
}
return new MessageHeader(to, from, message);
}
private static Externalizable getTemplate(final byte type) {
switch (type) {
case 1:
return new HubInvoke();
case 2:
return new SpokeInvoke();
case 3:
return new HubInvocationResults();
case 4:
return new SpokeInvocationResults();
default:
throw new IllegalStateException("not recognized, " + type);
}
}
/**
* Most messages we pass will be one of the types below
* since each of these is externalizable, we can
* reduce network traffic considerably by skipping the
* writing of the full identifiers, and simply write a single
* byte to show the type.
*/
public static byte getType(final Object msg) {
if (msg instanceof HubInvoke) {
return 1;
} else if (msg instanceof SpokeInvoke) {
return 2;
} else if (msg instanceof HubInvocationResults) {
return 3;
} else if (msg instanceof SpokeInvocationResults) {
return 4;
}
return Byte.MAX_VALUE;
}
public void add(final SocketChannel channel, final QuarantineConversation conversation) {
quarantine.put(channel, conversation);
}
public void closed(final SocketChannel channel) {
// remove if it exists
final QuarantineConversation conversation = quarantine.remove(channel);
if (conversation != null) {
conversation.close();
}
}
}