package electrosphere.net.server; import electrosphere.entity.types.creature.CreatureTemplate; import electrosphere.engine.Globals; import electrosphere.logger.LoggerInterface; import electrosphere.net.parser.net.message.AuthMessage; import electrosphere.net.parser.net.message.NetworkMessage; import electrosphere.net.parser.net.message.ServerMessage; import electrosphere.net.parser.net.raw.NetworkParser; import electrosphere.net.server.player.Player; import electrosphere.util.CodeUtils; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.net.Socket; import java.net.SocketException; import java.util.LinkedList; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.Semaphore; import java.util.concurrent.TimeUnit; /** * A connection to the server */ public class ServerConnectionHandler implements Runnable { //the player id associated with this connection static int playerIdIncrementer = 0; //local carrier variables boolean local = false; //socket carrier variables Socket socket; //the streams for the connection // CryptoInputStream inputStream; // CryptoOutputStream outputStream; /** * The input stream for packets */ InputStream inputStream; /** * The output stream for packets */ OutputStream outputStream; //the network parser for the streams NetworkParser networkParser; //initialized status boolean initialized; //authentication status boolean isAuthenticated = false; //the player id int playerID; //the player's entity id int playerEntityID; /** * Tracks whether this connection is still communicating with the client */ boolean isConnected = true; //the creature template associated with this player CreatureTemplate currentCreatureTemplate; //the server protocol object associated with this player MessageProtocol messageProtocol; //thresholds for determining when to send pings and when a client has disconnected static final long SEND_PING_THRESHOLD = 3000; static final long PING_DISCONNECT_THRESHOLD = 20000; //used to keep track of ping/pong messages with client long lastPingTime = 0; long lastPongTime = 0; //flag to disconnect due to pipe break boolean socketException = false; //debug netmonitor stuff String netMonitorHandle; //Used to copy messages from network parser to NetMonitor List netMonitorCache = new LinkedList(); //the lock used for synchronizing the synchronous message queue Semaphore synchronousMessageLock = new Semaphore(1); //the queue of synchonous network messages List synchronousMessageQueue = new CopyOnWriteArrayList(); /** * Constructs a connection from a socket * @param socket the socket */ public ServerConnectionHandler(Socket socket) { this.socket = socket; playerID = getNewPlayerID(); LoggerInterface.loggerNetworking.INFO("Player ID: " + playerID); this.messageProtocol = new MessageProtocol(this); } /** * Constructs a connection from an arbitrary input and output stream * @param serverInputStream the input stream * @param serverOutputStream the output stream */ public ServerConnectionHandler(InputStream serverInputStream, OutputStream serverOutputStream){ this.local = true; playerID = getNewPlayerID(); LoggerInterface.loggerNetworking.INFO("Player ID: " + playerID); inputStream = serverInputStream; outputStream = serverOutputStream; this.messageProtocol = new MessageProtocol(this); } @Override public void run() { LoggerInterface.loggerNetworking.INFO("ServerConnectionHandler start"); initialized = false; /// /// /// SETUP /// /// //startup is different whether need to set socket streams or received in construction of object (ie if creating local "connection") if(this.local){ //run if serverconnectionHandler is created by passing in input/output streams networkParser = new NetworkParser(inputStream,outputStream); messageProtocol = new MessageProtocol(this); } else { //run if ServerConnectionHandler is created by passing in a socket try { socket.setSoTimeout(100); } catch (SocketException ex) { ex.printStackTrace(); } //TODO: use this commented block of code as a reference for implementing encryption on top of the game connection // final SecretKeySpec key = new SecretKeySpec(("1234567890123456").getBytes(),"AES"); // final Properties properties = new Properties(); // final RSAKeyGenParameterSpec spec = new RSAKeyGenParameterSpec(4096, BigInteger.probablePrime(4000, new Random())); // try { // inputStream = new CryptoInputStream("AES/ECB/PKCS5Padding",properties,socket.getInputStream(),key,spec); // } catch (IOException ex) { // ex.printStackTrace(); // System.exit(1); // } // try { // outputStream = new CryptoOutputStream("AES/ECB/PKCS5Padding",properties,socket.getOutputStream(),key,spec); // } catch (IOException ex) { // ex.printStackTrace(); // System.exit(1); // } try { inputStream = socket.getInputStream(); outputStream = socket.getOutputStream(); networkParser = new NetworkParser(inputStream,outputStream); messageProtocol = new MessageProtocol(this); } catch (IOException ex) { ex.printStackTrace(); LoggerInterface.loggerNetworking.ERROR("", ex); } } NetworkMessage pingMessage = ServerMessage.constructPingMessage(); NetworkMessage authRequestMessage = AuthMessage.constructAuthRequestMessage(); //net monitor registration if(Globals.netMonitor != null){ this.netMonitorHandle = Globals.netMonitor.registerConnection(); Globals.netMonitor.logMessage(netMonitorHandle, pingMessage, false); Globals.netMonitor.logMessage(netMonitorHandle, authRequestMessage, false); } networkParser.addOutgoingMessage(pingMessage); networkParser.addOutgoingMessage(authRequestMessage); /// /// /// MAIN LOOP /// /// initialized = true; while(Globals.threadManager.shouldKeepRunning() && this.isConnected == true){ // // Main Loop // //parse messages both incoming and outgoing try { parseMessages(); } catch (SocketException e) { //if we get a SocketException broken pipe (basically the client dc'd without telling us) //set flag to disconnect client //TODO: fix, this doesn't actually catch the socket exception which is exceedingly obnoxious socketException = true; LoggerInterface.loggerNetworking.DEBUG(e.getLocalizedMessage()); this.disconnect(); break; } catch (IOException e){ //if we get a SocketException broken pipe (basically the client dc'd without telling us) //set flag to disconnect client //TODO: fix, this doesn't actually catch the socket exception which is exceedingly obnoxious socketException = true; LoggerInterface.loggerNetworking.DEBUG(e.getLocalizedMessage()); this.disconnect(); break; } // // Pings // //ping logic long currentTime = System.currentTimeMillis(); //basically if we haven't sent a ping in a while, send one if(currentTime - lastPingTime > SEND_PING_THRESHOLD){ addMessagetoOutgoingQueue(ServerMessage.constructPingMessage()); lastPingTime = currentTime; if(lastPongTime == 0){ lastPongTime = lastPingTime; } } // // Disconnections // //check if we meet disconnection criteria //has it been too long since the last ping? //have we had a socket exception? if(lastPingTime - lastPongTime > PING_DISCONNECT_THRESHOLD || this.socketException == true){ //disconnected from the server LoggerInterface.loggerNetworking.WARNING("Client disconnected"); //run disconnect routine disconnect(); break; } } LoggerInterface.loggerNetworking.WARNING("Server connection thread ended"); } /** * Had to wrap the message parsing block in a function to throw a SocketException * without my linter freaking out * @throws SocketException */ void parseMessages() throws SocketException, IOException { // //Read in messages // //attempt poll incoming messages networkParser.readMessagesIn(); // //Net monitor (debug) // //net monitor if(Globals.netMonitor != null){ //incoming netMonitorCache.clear(); networkParser.copyIncomingMessages(netMonitorCache); for(NetworkMessage message : netMonitorCache){ Globals.netMonitor.logMessage(netMonitorHandle, message, true); } } // //Parse messages // //ponder incoming messages while(networkParser.hasIncomingMessaage()){ NetworkMessage message = networkParser.popIncomingMessage(); this.messageProtocol.handleAsyncMessage(message); } // //Net monitor (debug) // if(Globals.netMonitor != null){ //outgoing netMonitorCache.clear(); networkParser.copyOutgoingMessages(netMonitorCache); for(NetworkMessage message : netMonitorCache){ Globals.netMonitor.logMessage(netMonitorHandle, message, false); } } // //Send out messages // //push outgoing message networkParser.pushMessagesOut(); try { //sleep TimeUnit.MILLISECONDS.sleep(1); } catch (InterruptedException ex) { CodeUtils.todo(ex, "Handle sleep interrupt on server connection"); } } /** * Handles synchronous packets in the queue */ public void handleSynchronousPacketQueue(){ this.messageProtocol.handleSyncMessages(); } public void setPlayerId(int id){ playerID = id; } public int getPlayerId(){ return playerID; } static int getNewPlayerID(){ playerIdIncrementer++; return playerIdIncrementer; } public void setPlayerEntityId(int id){ LoggerInterface.loggerNetworking.DEBUG("Set player(" + this.playerID + ")'s entity ID to be " + id); playerEntityID = id; } public int getPlayerEntityId(){ return playerEntityID; } /** * Gets the player associated with this connection * @return The player object */ public Player getPlayer(){ return Globals.playerManager.getPlayerFromId(playerID); } /** * Gets the ip address of the connection * @return The ip address */ public String getIPAddress(){ if(local){ return "127.0.0.1"; } else { return socket.getRemoteSocketAddress().toString(); } } /** * Gets the socket of the connection, if it is a socket based connection. Otherwise returns null. * @return The socket if it exists, null otherwise */ public Socket getSocket(){ return this.socket; } public void addMessagetoOutgoingQueue(NetworkMessage message){ networkParser.addOutgoingMessage(message); } boolean isConnectionPlayerEntity(int id){ return id == this.playerEntityID; } /** * Returns true if running both client and server in same instance of the app and this is the player for that instance of the app * @return */ boolean isServerClient(){ return Globals.RUN_SERVER && Globals.RUN_CLIENT && Globals.clientPlayer != null && this.playerID == Globals.clientPlayer.getId(); } public void setCreatureTemplate(CreatureTemplate currentCreatureTemplate){ this.currentCreatureTemplate = currentCreatureTemplate; } public CreatureTemplate getCurrentCreatureTemplate(){ return this.currentCreatureTemplate; } public void markReceivedPongMessage(){ lastPongTime = System.currentTimeMillis(); } /** * Routine to run when the client disconnects */ private void disconnect(){ //close socket if(socket != null && socket.isConnected()){ try { socket.close(); } catch (IOException e) { LoggerInterface.loggerNetworking.ERROR("Error closing socket", e); } } this.isConnected = false; //add connection to server list of connections to cleanup Globals.server.addClientToCleanup(this); } }