package games.strategy.net; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.Serializable; import java.net.InetSocketAddress; import java.net.Socket; import java.nio.channels.SocketChannel; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import games.strategy.debug.ClientLogger; import games.strategy.engine.framework.startup.mc.ServerModel; import games.strategy.engine.framework.ui.SaveGameFileChooser; import games.strategy.engine.message.HubInvoke; import games.strategy.engine.message.RemoteMethodCall; import games.strategy.engine.message.RemoteName; import games.strategy.net.nio.ClientQuarantineConversation; import games.strategy.net.nio.NIOSocket; import games.strategy.net.nio.NIOSocketListener; import games.strategy.net.nio.QuarantineConversation; import games.strategy.util.ListenerList; import games.strategy.util.ThreadUtil; public class ClientMessenger implements IClientMessenger, NIOSocketListener { private INode m_node; private final ListenerList<IMessageListener> m_listeners = new ListenerList<>(); private final ListenerList<IMessengerErrorListener> m_errorListeners = new ListenerList<>(); private final CountDownLatch m_initLatch = new CountDownLatch(1); private Exception m_connectionRefusedError; private final NIOSocket m_socket; private final SocketChannel m_socketChannel; private INode m_serverNode; private volatile boolean m_shutDown = false; /** * Note, the name paramater passed in here may not match the name of the * ClientMessenger after it has been constructed. */ public ClientMessenger(final String host, final int port, final String name, final String mac, final IConnectionLogin login) throws IOException { this(host, port, name, mac, new DefaultObjectStreamFactory(), login); } /** * Note, the name paramater passed in here may not match the name of the * ClientMessenger after it has been constructed. */ public ClientMessenger(final String host, final int port, final String name, final String mac) throws IOException { this(host, port, name, mac, new DefaultObjectStreamFactory()); } /** * Note, the name paramater passed in here may not match the name of the * ClientMessenger after it has been constructed. */ public ClientMessenger(final String host, final int port, final String name, final String mac, final IObjectStreamFactory streamFact) throws IOException { this(host, port, name, mac, streamFact, null); } /** * Note, the name paramater passed in here may not match the name of the * ClientMessenger after it has been constructed. */ public ClientMessenger(final String host, final int port, final String name, final String mac, final IObjectStreamFactory streamFact, final IConnectionLogin login) throws IOException { m_socketChannel = SocketChannel.open(); m_socketChannel.configureBlocking(false); final InetSocketAddress remote = new InetSocketAddress(host, port); if (!m_socketChannel.connect(remote)) { // give up after 10 seconds int waitTimeMilliseconds = 0; while (true) { if (waitTimeMilliseconds > 10000) { m_socketChannel.close(); throw new IOException("Connection refused"); } if (m_socketChannel.finishConnect()) { break; } if (!ThreadUtil.sleep(50)) { shutDown(); m_socket = null; return; } waitTimeMilliseconds += 50; } } final Socket socket = m_socketChannel.socket(); socket.setKeepAlive(true); m_socket = new NIOSocket(streamFact, this, name); final ClientQuarantineConversation conversation = new ClientQuarantineConversation(login, m_socketChannel, m_socket, name, mac); m_socket.add(m_socketChannel, conversation); // allow the credentials to be shown in this thread conversation.showCredentials(); // wait for the quarantine to end try { m_initLatch.await(); } catch (final InterruptedException e) { m_connectionRefusedError = e; try { m_socketChannel.close(); } catch (final IOException e2) { // ignore } } if (conversation.getErrorMessage() != null || m_connectionRefusedError != null) { // our socket channel should already be closed m_socket.shutDown(); if (conversation.getErrorMessage() != null) { String msg = conversation.getErrorMessage(); if (m_connectionRefusedError != null) { msg += ", " + m_connectionRefusedError; } login.notifyFailedLogin(msg); throw new CouldNotLogInException(); } else if (m_connectionRefusedError instanceof CouldNotLogInException) { throw (CouldNotLogInException) m_connectionRefusedError; } else if (m_connectionRefusedError != null) { throw new IOException(m_connectionRefusedError.getMessage()); } } } @Override public synchronized void send(final Serializable msg, final INode to) { // use our nodes address, this is our network visible address final MessageHeader header = new MessageHeader(to, m_node, msg); m_socket.send(m_socketChannel, header); } @Override public synchronized void broadcast(final Serializable msg) { final MessageHeader header = new MessageHeader(m_node, msg); m_socket.send(m_socketChannel, header); } @Override public void addMessageListener(final IMessageListener listener) { m_listeners.add(listener); } @Override public void removeMessageListener(final IMessageListener listener) { m_listeners.remove(listener); } @Override public void addErrorListener(final IMessengerErrorListener listener) { m_errorListeners.add(listener); } @Override public void removeErrorListener(final IMessengerErrorListener listener) { m_errorListeners.remove(listener); } @Override public boolean isConnected() { return m_socketChannel.isConnected(); } @Override public void shutDown() { m_shutDown = true; if (m_socket != null) { m_socket.shutDown(); } try { m_socketChannel.close(); } catch (final IOException e) { // ignore } } public boolean isShutDown() { return m_shutDown; } @Override public void messageReceived(final MessageHeader msg, final SocketChannel channel) { if (msg.getFor() != null && !msg.getFor().equals(m_node)) { throw new IllegalStateException("msg not for me:" + msg); } for (final IMessageListener listener : m_listeners) { listener.messageReceived(msg.getMessage(), msg.getFrom()); } } @Override public INode getLocalNode() { return m_node; } @Override public INode getServerNode() { return m_serverNode; } @Override public boolean isServer() { return false; } @Override public void socketUnqaurantined(final SocketChannel channel, final QuarantineConversation converstaion2) { final ClientQuarantineConversation conversation = (ClientQuarantineConversation) converstaion2; // all ids are based on the socket adress of nodes in the network // but the adress of a node changes depending on who is looking at it // ie, sometimes it is the loopback adress if connecting locally, // sometimes the client or server will be behind a NAT // so all node ids are defined as what the server sees the adress as // we are still in the decode thread at this point, set our nodes now // before the socket is unquarantined m_node = new Node(conversation.getLocalName(), conversation.getNetworkVisibleSocketAdress()); m_serverNode = new Node(conversation.getServerName(), conversation.getServerLocalAddress()); m_initLatch.countDown(); } @Override public void socketError(final SocketChannel channel, final Exception error) { if (m_shutDown) { return; } // if an error occurs during set up // we need to return in the constructor // otherwise this is harmless m_connectionRefusedError = error; for (final IMessengerErrorListener errorListener : m_errorListeners) { errorListener.messengerInvalid(ClientMessenger.this, error); } shutDown(); m_initLatch.countDown(); } @Override public INode getRemoteNode(final SocketChannel channel) { // we only have one channel return m_serverNode; } @Override public InetSocketAddress getRemoteServerSocketAddress() { return (InetSocketAddress) m_socketChannel.socket().getRemoteSocketAddress(); } private void bareBonesSendMessageToServer(final String methodName, final Object... messages) { final List<Object> args = new ArrayList<>(); final Class<? extends Object>[] argTypes = new Class<?>[messages.length]; for (int i = 0; i < messages.length; i++) { final Object message = messages[i]; args.add(message); argTypes[i] = args.get(i).getClass(); } final RemoteName rn = ServerModel.SERVER_REMOTE_NAME; final RemoteMethodCall call = new RemoteMethodCall(rn.getName(), methodName, args.toArray(), argTypes, rn.getClazz()); final HubInvoke hubInvoke = new HubInvoke(null, false, call); send(hubInvoke, getServerNode()); } @Override public void changeServerGameTo(final String gameName) { bareBonesSendMessageToServer("changeServerGameTo", gameName); } @Override public void changeToLatestAutosave(final SaveGameFileChooser.AUTOSAVE_TYPE typeOfAutosave) { bareBonesSendMessageToServer("changeToLatestAutosave", typeOfAutosave); } @Override public void changeToGameSave(final byte[] bytes, final String fileName) { bareBonesSendMessageToServer("changeToGameSave", bytes, fileName); } @Override public void changeToGameSave(final File saveGame, final String fileName) { final byte[] bytes = getBytesFromFile(saveGame); if (bytes == null || bytes.length == 0) { return; } changeToGameSave(bytes, fileName); } private static byte[] getBytesFromFile(final File file) { if (file == null || !file.exists()) { return null; } // Get the size of the file final long length = file.length(); if (length > Integer.MAX_VALUE) { return null; } // Create the byte array to hold the data final byte[] bytes = new byte[(int) length]; try (InputStream is = new FileInputStream(file)) { is.read(bytes); } catch (final IOException e) { ClientLogger.logQuietly("Failed to read file: " + file); ClientLogger.logQuietly(e); } return bytes; } @Override public String toString() { return "ClientMessenger LocalNode:" + m_node + " ServerNodes:" + m_serverNode; } }