/* * Sun Public License * * The contents of this file are subject to the Sun Public License Version * 1.0 (the "License"). You may not use this file except in compliance with * the License. A copy of the License is available at http://www.sun.com/ * * The Original Code is the SLAMD Distributed Load Generation Engine. * The Initial Developer of the Original Code is Neil A. Wilson. * Portions created by Neil A. Wilson are Copyright (C) 2004-2010. * Some preexisting portions Copyright (C) 2002-2006 Sun Microsystems, Inc. * All Rights Reserved. * * Contributor(s): Neil A. Wilson */ package com.slamd.server; import java.io.IOException; import java.io.InterruptedIOException; import java.net.Socket; import java.util.ArrayList; import com.slamd.admin.AccessManager; import com.slamd.admin.AdminAccess; import com.slamd.asn1.ASN1Element; import com.slamd.asn1.ASN1Exception; import com.slamd.asn1.ASN1Reader; import com.slamd.asn1.ASN1Writer; import com.slamd.common.Constants; import com.slamd.common.SLAMDException; import com.slamd.job.JobClass; import com.slamd.message.ClientHelloMessage; import com.slamd.message.HelloResponseMessage; import com.slamd.message.KeepAliveMessage; import com.slamd.message.Message; import com.slamd.message.RegisterStatisticMessage; import com.slamd.message.ReportStatisticMessage; import com.slamd.message.ServerShutdownMessage; /** * This class defines a thread that is spawned by the server to handle each * stat client connection. It takes care of reading messages in from the client * and provides methods for sending messages to the client. * * * @author Neil A. Wilson */ public class StatClientConnection extends Thread implements Comparable { // The queue that will be used to hold messages received from the client. private ArrayList<Message> messageQueue; // The ASN.1 reader used to read data from the client. private ASN1Reader asn1Reader; // The ASN.1 writer used to write data to the client. private ASN1Writer asn1Writer; // Indicates whether this thread should continue listening for communication // from the client. private boolean keepListening; // Indicates whether this client supports time synchronization with the // server. private boolean supportsTimeSync; // The listener that accepted this client connection. private StatListener listener; // The next message ID that should be used for sending a request to the // client. private int nextMessageID; // A mutex used to provide threadsafe access to the message queue. private final Object messageQueueMutex; // The real-time stat handler to which data will be reported. private RealTimeStatHandler statHandler; // The SLAMD server with which this client connection is associated. private SLAMDServer slamdServer; // The socket that provides communication with the client. protected Socket clientSocket; // The client ID of the client associated with this connection. private String clientID; // The IP address for this client connection. private String clientIPAddress; // The version of the software on the client associated with this connection. private String clientVersion; /** * Creates a new stat client connection based on the provided information. * * @param slamdServer The SLAMD server with which this client is * associated. * @param listener The listener that accepted this connection. * @param clientSocket The socket used to communicate with the client. * * @throws SLAMDException If a problem occurs while creating the connection. */ public StatClientConnection(SLAMDServer slamdServer, StatListener listener, Socket clientSocket) throws SLAMDException { this.slamdServer = slamdServer; this.listener = listener; this.clientSocket = clientSocket; this.supportsTimeSync = false; statHandler = slamdServer.getStatHandler(); messageQueue = new ArrayList<Message>(); messageQueueMutex = new Object(); nextMessageID = 2; // Get the IP address of the client and create the ASN.1 reader and writer. try { clientIPAddress = clientSocket.getInetAddress().getHostAddress(); asn1Reader = new ASN1Reader(clientSocket); asn1Writer = new ASN1Writer(clientSocket); } catch (IOException ioe) { slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG, JobClass.stackTraceToString(ioe)); throw new SLAMDException("Unable to establish the reader and/or writer " + "to the stat client.", ioe); } // Read the hello request from the client. ClientHelloMessage helloMessage = null; try { ASN1Element element = asn1Reader.readElement(Constants.MAX_BLOCKING_READ_TIME); helloMessage = (ClientHelloMessage) Message.decode(element); } catch (Exception e) { try { clientSocket.close(); } catch (Exception e2) {} slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG, JobClass.stackTraceToString(e)); throw new SLAMDException("Unable to read or parse the hello message " + "from the stat client: " + e, e); } // Determine how to handle client authentication. clientID = helloMessage.getClientID(); clientVersion = helloMessage.getClientVersion(); supportsTimeSync = helloMessage.supportsTimeSync(); String authID = helloMessage.getAuthID(); String authCredentials = helloMessage.getAuthCredentials(); int authType = helloMessage.getAuthType(); if ((authID == null) || (authID.length() == 0) || (authCredentials == null) || (authCredentials.length() == 0)) { if (listener.requireAuthentication()) { String msg = "Authentication required but client did not " + "provide sufficient authentication data."; HelloResponseMessage helloResp = new HelloResponseMessage(0, Constants.MESSAGE_RESPONSE_SERVER_ERROR, msg, -1); try { asn1Writer.writeElement(helloResp.encode()); } catch (IOException ioe) {} slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT, "Rejected new stat client connection " + clientID + " -- " + msg); try { clientSocket.close(); } catch (Exception e) {} throw new SLAMDException(msg); } } else { if (authType != Constants.AUTH_TYPE_SIMPLE) { throw new SLAMDException("Invalid authentication type " + authType); } AccessManager accessManager = AdminAccess.getAccessManager(); if (accessManager == null) { String msg = "The SLAMD server is not properly configured " + "to perform authentication."; HelloResponseMessage helloResp = new HelloResponseMessage(0, Constants.MESSAGE_RESPONSE_SERVER_ERROR, msg, -1); try { asn1Writer.writeElement(helloResp.encode()); } catch (IOException ioe) {} slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT, "Rejected new stat client connection " + clientID + " -- " + msg); try { clientSocket.close(); } catch (Exception e) {} throw new SLAMDException(msg); } StringBuilder msgBuffer = new StringBuilder(); int resultCode = accessManager.authenticateClient(authID, authCredentials, msgBuffer); if (resultCode != Constants.MESSAGE_RESPONSE_SUCCESS) { String msg = msgBuffer.toString(); HelloResponseMessage helloResp = new HelloResponseMessage(0, resultCode, msg, -1); try { asn1Writer.writeElement(helloResp.encode()); } catch (IOException ioe) {} slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT, "Rejected new client connection " + clientID + " -- " + msg); try { clientSocket.close(); } catch (Exception e) {} throw new SLAMDException(msg); } } // If we should use keepalive messages, then add a socket timeout for this // connection. int keepAliveInterval = listener.getKeepAliveInterval(); if (keepAliveInterval > 0) { try { clientSocket.setSoTimeout(keepAliveInterval*1000); } catch (IOException ioe) { slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG, "Unable to set socket timeout for connection " + "to stat client " + clientID + " -- keepalive messages will not be used."); } } // See if the client ID for this client conflicts with the client ID of // a client that has already been connected. StatClientConnection[] conns = listener.getConnectionList(); for (int i=0; i < conns.length; i++) { if (conns[i].getClientID().equalsIgnoreCase(clientID)) { try { HelloResponseMessage helloResponse = new HelloResponseMessage(helloMessage.getMessageID(), Constants.MESSAGE_RESPONSE_CLIENT_REJECTED, "A stat client connection has already been " + "established with client ID \"" + clientID + "\".", -1); asn1Writer.writeElement(helloResponse.encode()); clientSocket.close(); } catch (IOException ioe) { try { clientSocket.close(); } catch (Exception e) {} slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG, JobClass.stackTraceToString(ioe)); throw new SLAMDException("Unable to send the hello response to the " + "stat client: " + ioe, ioe); } throw new SLAMDException("Rejected stat client connection due to " + "duplicate client ID -- " + clientID + '.'); } } // Send the hello response to the client. try { long serverTime = (supportsTimeSync ? System.currentTimeMillis() : -1); HelloResponseMessage helloResponse = new HelloResponseMessage(helloMessage.getMessageID(), Constants.MESSAGE_RESPONSE_SUCCESS, serverTime); asn1Writer.writeElement(helloResponse.encode()); } catch (IOException ioe) { try { clientSocket.close(); } catch (Exception e) {} slamdServer.logMessage(Constants.LOG_LEVEL_EXCEPTION_DEBUG, JobClass.stackTraceToString(ioe)); throw new SLAMDException("Unable to send the hello response to the " + "stat client: " + ioe, ioe); } setName("Stat Client Connection " + clientID); } /** * Retrieves the client ID associated with this stat client connection. * * @return The client ID associated with this stat client connection. */ public String getClientID() { return clientID; } /** * Retrieves the IP address of the client system associated with this * connection. * * @return The IP address of the client system associated with this * connection. */ public String getClientIPAddress() { return clientIPAddress; } /** * Retrieves the version of the client software that the client system is * running. * * @return The version of the client software that the client system is * running. */ public String getClientVersion() { return clientVersion; } /** * Retrieves the message ID that should be used for the next request sent to * the client. * * @return The message ID that should be used for the next request sent to * the client. */ private int nextMessageID() { int messageID = nextMessageID; nextMessageID += 2; return messageID; } /** * Retrieves the message with the specified message ID from the receive queue. * * @param messageID The message ID of the message to retrieve. * * @return The requested message, or <CODE>null</CODE> if an appropriate * response does not arrive within an appropriate timeout period. */ public Message getMessage(int messageID) { synchronized (messageQueueMutex) { for (int i=0; i < messageQueue.size(); i++) { Message message = messageQueue.get(i); if (message.getMessageID() == messageID) { messageQueue.remove(i); return message; } } // If we have gotten here, then the requested message wasn't in the // queue. Wait for it to arrive. try { messageQueueMutex.wait(1000 * listener.getMaxResponseWaitTime()); } catch (InterruptedException ie) {} for (int i=0; i < messageQueue.size(); i++) { Message message = messageQueue.get(i); if (message.getMessageID() == messageID) { messageQueue.remove(i); return message; } } } // If we have gotten here, then the message didn't arrive. Return null. return null; } /** * Sends a message to the client that indicates the server is shutting down, * and then optionally closes the connection to the client. * * @param closeSocket Indicates whether the connection to the client should * be closed. */ public void sendServerShutdownMessage(boolean closeSocket) { slamdServer.logMessage(Constants.LOG_LEVEL_TRACE, "In " + "StatClientConnection.sendServerShutdownMessage() " + "for " + clientID); slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG, "In " + "ClientConnection.sendServerShutdownMessage() " + "for " + clientID); ServerShutdownMessage shutdownMessage = new ServerShutdownMessage(nextMessageID()); try { asn1Writer.writeElement(shutdownMessage.encode()); slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG, "Sent shutdown message to " + clientID); } catch (IOException ioe) { slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG, "Could not send shutdown message to " + clientID + ": " + ioe); ioe.printStackTrace(); listener.connectionLost(this); } if (closeSocket) { keepListening = false; try { slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG, "Closing socket for " + clientID); clientSocket.close(); } catch (IOException ioe) {} } } /** * Listens for messages from the client and either handles them or hands them * off to be handled elsewhere. */ @Override() public void run() { slamdServer.logMessage(Constants.LOG_LEVEL_TRACE, "In StatClientConnection.run() for " + clientID); slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG, "In StatClientConnection.run() for " + clientID); keepListening = true; // First, set a timeout on the socket. This will allow us to interrupt the // reads periodically to send keepalive messages. int keepAliveTime = listener.getKeepAliveInterval(); if (keepAliveTime > 0) { try { clientSocket.setSoTimeout(keepAliveTime*1000); slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG, "Set socket timeout of " + keepAliveTime + " seconds for " + clientID); } catch (IOException ioe) { slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG, "Could not set timeout for client connection " + clientID); } } // This flag will be used to detect two consecutive failures (indicates // that the connection has been closed without the client notifying us) boolean consecutiveFailures = false; // Loop infinitely (or at least until the client shuts down) and read // messages from the client while (keepListening) { try { ASN1Element element = asn1Reader.readElement(); if (element == null) { // This should only happen if the client has closed the connection. slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT, "Detected connection closure from stat " + "client " + clientID); try { clientSocket.close(); } catch (IOException ioe2) {} listener.connectionLost(this); return; } slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG, "Read a message from " + clientID); Message message = Message.decode(element); slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG, "Decoded message from " + clientID); if (message instanceof RegisterStatisticMessage) { RegisterStatisticMessage msg = (RegisterStatisticMessage) message; statHandler.handleRegisterStatMessage(msg); } else if (message instanceof ReportStatisticMessage) { ReportStatisticMessage msg = (ReportStatisticMessage) message; statHandler.handleReportStatMessage(msg); } } catch (InterruptedIOException iioe) { // If this exception was thrown, it was because the socket timeout was // reached. We need to send a keepalive message. try { KeepAliveMessage kaMsg = new KeepAliveMessage(nextMessageID()); asn1Writer.writeElement(kaMsg.encode()); slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG, "Sent keepalive to " + clientID); } catch (IOException ioe) { slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG, "Unable to send keepalive to " + clientID + ": " + ioe); } } catch (IOException ioe) { // Some other I/O related exception was thrown slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG, "I/O exception from " + clientID + ": " + ioe); // Some problem occurred. See if this is a second consecutive failure // and if so, end the connection and this thread. Otherwise set a // flag that will be used to detect a second consecutive failure if (consecutiveFailures) { slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG, "Consecutive failures on connection " + clientID + " -- closing"); try { clientSocket.close(); } catch (IOException ioe2) {} listener.connectionLost(this); return; } else { consecutiveFailures = true; } } catch (SLAMDException se) { // The specified class could not be found slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG, "Exception handling message from " + clientID + ": " + se); se.printStackTrace(); } catch (ASN1Exception ae) { slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG, "Exception decoding message from " + clientID + ": " + ae); ae.printStackTrace(); } } slamdServer.logMessage(Constants.LOG_LEVEL_TRACE, "Leaving StatClientConnection.run() for " + clientID); slamdServer.logMessage(Constants.LOG_LEVEL_CLIENT_DEBUG, "Leaving StatClientConnection.run() for " + clientID); } /** * Compares this stat client connection with the provided object. The given * object must be a stat client connection object, and the comparison will be * made based on the lexicographic ordering of the associated client IDs. * * @param o The stat client connection object to compare against this stat * client connection. * * @return A negative value if this stat client connection should come before * the provided stat client connection in a sorted list, a positive * value if this stat client connection should come after the * provided stat client connection in a sorted list, or zero if there * is no difference in their ordering as far as this method is * concerned. * * @throws ClassCastException If the provided object is not a stat client * connection object. */ public int compareTo(Object o) throws ClassCastException { StatClientConnection c = (StatClientConnection) o; return clientID.compareTo(c.clientID); } }