package com.aberdyne.droidnavi.client; import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.Socket; import java.net.UnknownHostException; import java.util.Date; import org.codehaus.jackson.JsonFactory; import org.codehaus.jackson.JsonNode; import org.codehaus.jackson.JsonParser; import org.codehaus.jackson.map.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import pctelelog.EventSerializer; import pctelelog.Packet; import pctelelog.events.AbstractEvent; import pctelelog.events.ClientConnectEvent; import pctelelog.events.HeartBeatEvent; import pctelelog.events.HelloEvent; import pctelelog.events.ShutdownEvent; public class ServerConnection implements Comparable<String> { private static final Logger logger = LoggerFactory.getLogger(ServerConnection.class); private static final int SERVER_PORT = 43212; private static final int READ_TIMEOUT = 10 * 1000; // 10 second read timeout private static final int CONNECT_TIMEOUT = 3 * 1000; // 3 second connect timeout private static final int HEARTBEAT_DELAY = 5 * 1000; // 5 second delay between heartbeat checks private Socket m_server = null; private String m_ipStr = null; private boolean m_isActiveSocket = false; // Is the socket connect()-able? private boolean m_isStandby = true; private Date m_lastEvent = null; // Timestamp for last event sent, used for heartbeat() /** * Protected constructor that allows specifying whether the socket * should be active(connect()-able) or not. * * @param ip A valid IP with no port specified * @param isActiveSocket True if the socket should be connected. * False if the socket should remain unconnected. * @throws ServerConnectionException */ protected ServerConnection(String ip, boolean isActiveSocket) throws ServerConnectionException { assert ip != null; m_isActiveSocket = isActiveSocket; m_ipStr = ip; } /** * Main constructor for a server connection. * * This will not connect to the server automatically so connect() * should be called before attempting to send events. * * @param ip An ip representing the remote computer. * @throws ServerConnectionException */ public ServerConnection(String ip) throws ServerConnectionException { this(ip, true); } /** * Helper function to validate a host/ip is valid. * * @param host A string containing a hostname or IP to check. * @return True if the host string is valid. False if it is not. */ static public boolean validateHost(String host) { try { InetAddress.getByName(host); } catch(Exception e) { return false; } return true; } /** * This will attempt to connect to the remote computer and * perform all the verification. * @return True if the connection was successful and the remote computer passed * the handshake. * False if the problems occurred or if the computer failed handshake. */ public boolean connect() { return openSocket(SERVER_PORT); } public int compareTo(String another) { return this.toString().compareTo(another); } /** * This is not a deep equals of the object. * The method will compare the *STRING* Ips of the two objects ONLY. */ @Override public boolean equals(Object o) { if(o == null) { return false; } try { ServerConnection server = ServerConnection.class.cast(o); if(server.toString() != null && this.toString() != null) { return server.toString().equals(this.toString()); } } catch(ClassCastException e) { return false; } return false; } @Override public String toString() { return m_ipStr; } /** * Shutdown the server connection and put socket on standby. */ public void shutdown(boolean sendShutdownEvent) { if(sendShutdownEvent){ try { ShutdownEvent shutdown_event = new ShutdownEvent(new Date()); sendEvent(shutdown_event); } catch(Exception e) { /* Continue closing socket */} } try { m_server.close(); } catch (IOException e) { try { // Try shutting down the Output m_server.shutdownOutput(); m_server.shutdownInput(); } catch (IOException e1) {} } catch(NullPointerException e) {} setStandbyStatus(true); m_server = null; } public boolean getStandbyStatus() { return m_isStandby; } public boolean isConnected() { if(m_server == null) { return false; } return m_server.isConnected(); } public boolean isClosed() { if(m_server == null) { return true; } return m_server.isClosed(); } /** * Try sending a heartbeat to the server to check socket state. * * This is a simple check with no expected reply. If the socket is dead, * then an exception should be thrown and the ServerConnection state * moved to standby. * * @return Return true if heartbeat was ok or not needed. False if it failed * and the socket is shutdown. */ public boolean heartbeat() { Date now = new Date(); if(m_lastEvent == null) { return sendEvent(new HeartBeatEvent()); } else { long diff = now.getTime() - m_lastEvent.getTime(); if(diff > HEARTBEAT_DELAY) { return sendEvent(new HeartBeatEvent()); } } return true; } /** * Helper function for sending unserialized events * @param event An event * @return True if no problems occurred while sending, False otherwise. */ public boolean sendEvent(AbstractEvent event) { if(m_isActiveSocket) { return sendEvent(m_server, event); } else { if(m_isStandby) { // If on standby try to connect if(!connect()) { return false; } } return sendEvent(m_server, event); } } /** * Send json event to specified server * * @param server An open and connected socketed * @param json The serialized event to send * @return True if successful, False otherwise. */ private synchronized boolean sendEvent(Socket server, AbstractEvent event) { logger.trace("ENTRY ServerConnection.sendEvent"); assert server != null; if(server.isConnected() && !server.isOutputShutdown()) { try { logger.info("Writing bytes"); Packet[] packets = Packet.createPackets(event); for(Packet packet: packets) { server.getOutputStream().write(packet.serialize()); } m_lastEvent = new Date(); // Update last event } catch (IOException e) { setStandbyStatus(true); shutdown(false); logger.debug("Catching: {}", e); logger.trace("EXIT ServerConnection.sendEvent:{}", Boolean.FALSE); return false; } } else { logger.info("Server not connected. Handshake Fail."); logger.trace("EXIT ServerConnection.sendEvent: {}", Boolean.FALSE); return false; } logger.trace("EXIT ServerConnection.sendEvent: {}", Boolean.TRUE); return true; } private void setStandbyStatus(boolean status) { m_isStandby = status; } /** * Open a new socket and handshake with server. * * This will clear the old socket, so make sure to handle any shutdown routines * before calling. * This method will use the IP string stored at creation to connect. * * On failure, the socket will call shutdown and go on standby. * * @param port Port to connect to. * @return True if connection was successful. False otherwise. */ private boolean openSocket(int port) { logger.trace("ENTRY ServerConnection.openSocket({})", port); if(m_server != null) { shutdown(true); } m_server = createSocket(m_ipStr, port); if(m_server == null) { setStandbyStatus(true); System.err.println("Failed to connect to server: " + m_ipStr + ":" + Integer.toString(SERVER_PORT)); logger.trace("Exiting ServerConnection.openSocket: {}", Boolean.FALSE); return false; } if(!handshake(m_server)) { setStandbyStatus(true); shutdown(false); System.err.println("The server (" + m_ipStr + ") failed the handshake."); logger.trace("EXIT ServerConnection.openSocket: {}", Boolean.FALSE); return false; } setStandbyStatus(false); sendEvent(new ClientConnectEvent(new Date())); logger.trace("EXIT ServerConnection.openSocket: {}", Boolean.TRUE); return true; } /** * Creates a socket and sets appropriate settings * @param ip A computer to connect to * @param port A port to connect to * @return Returns an open socket if there are no problem. * Returns null if problems occurred and print exceptions. */ private Socket createSocket(String ip, int port) { logger.trace("ENTRY ServerConnection.createSocket"); logger.debug("IP: {} PORT: {}", ip, port); Socket socket = null; try { socket = new Socket(); socket.connect(new InetSocketAddress(ip, port), CONNECT_TIMEOUT); socket.setSoTimeout(READ_TIMEOUT); } catch (UnknownHostException e) { logger.error("Catching UnknownHostException: {}", e.getMessage()); socket = null; } catch (IOException e) { logger.error("Catching IOException: {}", e.getMessage()); socket = null; } logger.trace("EXIT ServerConnection.createSocket: {}", socket); return socket; } /** * Handshakes with the specified socket. * * State of the ServerConnection and supplied socket remains unchanged. * * @return True if successful, False if it wasn't */ private boolean handshake(Socket server) { logger.trace("ENTRY ServerConnection.handshake({})", server); if(!server.isConnected() || server.isInputShutdown() || server.isOutputShutdown()) { logger.debug("Socket not connected."); logger.trace("EXIT ServerConnection.handshake: {}", Boolean.FALSE); return false; } // Get Hello ObjectMapper mapper = new ObjectMapper(); JsonFactory factory = mapper.getJsonFactory(); AbstractEvent event = null; try { byte[] data = new byte[3000]; boolean isFinish = false; while(!isFinish) { if(server.getInputStream().available() > 0) { int read = server.getInputStream().read(data); logger.info("Read: " + Integer.toString(read)); } else { Thread.sleep(3000); logger.info("Available(2): " + Integer.toString(server.getInputStream().available())); isFinish = server.getInputStream().available() > 0 ? false : true; } } JsonParser parser = factory.createJsonParser(data); logger.info("Reading Hello"); JsonNode node = parser.readValueAsTree(); event = EventSerializer.deserialize(node); logger.info("Received: " + event.toString()); if(event instanceof HelloEvent) { // Send Hello sendEvent(server, new HelloEvent(new Date())); logger.info("Handshake PASS"); logger.trace("EXIT ServerConnection.handshake: {}", Boolean.TRUE); return true; } else { logger.info("Handshake FAIL"); logger.trace("EXIT ServerConnection.handshake: {}", Boolean.FALSE); return false; } } catch(Exception e) { e.printStackTrace(); logger.error("Catching: {}", e); } logger.trace("EXIT ServerConnection.handshake: {}", Boolean.TRUE); return false; } @SuppressWarnings("serial") public class ServerConnectionException extends RuntimeException { public ServerConnectionException(String msg) { super(msg); } } }