/** * This file is part of SecureNIO. Copyright (C) 2014 K. Dermitzakis * <dermitza@gmail.com> * * SecureNIO is free software: you can redistribute it and/or modify it under * the terms of the GNU Affero General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) any * later version. * * SecureNIO is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR * A PARTICULAR PURPOSE. See the GNU Affero General Public License for more * details. * * You should have received a copy of the GNU Affero General Public License * along with SecureNIO. If not, see <http://www.gnu.org/licenses/>. */ package ch.dermitza.securenio; import ch.dermitza.securenio.packet.PacketListener; import ch.dermitza.securenio.packet.worker.AbstractPacketWorker; import ch.dermitza.securenio.socket.SocketContainer; import ch.dermitza.securenio.socket.SocketIF; import ch.dermitza.securenio.socket.secure.HandshakeListener; import ch.dermitza.securenio.socket.secure.TaskListener; import ch.dermitza.securenio.socket.secure.TaskWorker; import ch.dermitza.securenio.socket.timeout.TimeoutListener; import ch.dermitza.securenio.socket.timeout.worker.TimeoutWorker; import ch.dermitza.securenio.util.PropertiesReader; import ch.dermitza.securenio.util.logging.LoggerHandler; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; import java.net.InetAddress; import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.nio.channels.spi.SelectorProvider; import java.security.KeyManagementException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLEngine; import javax.net.ssl.TrustManagerFactory; /** * An abstract selector implementation that can be the basis (can be extended) * of an NIO TCP server or client, supporting both plain and SSL/TLS encrypted * sockets. * * @author K. Dermitzakis * @version 0.19 * @since 0.18 */ public abstract class AbstractSelector implements Runnable, TaskListener, HandshakeListener, TimeoutListener { /** * */ protected static final Logger logger = LoggerHandler.getLogger(AbstractSelector.class.getName()); /** * The address to listen at or connect */ protected InetAddress address; /** * The port to listen on or connect to */ protected int port; // The buffer into which we'll read data when it's available private ByteBuffer readBuffer = ByteBuffer.allocate(8192); private final ArrayDeque<ChangeRequest> pendingChanges = new ArrayDeque<>(); /** * Maps a SocketChannel to a list of ByteBuffer instances */ protected final HashMap<SocketChannel, List<ByteBuffer>> pendingData = new HashMap<>(); /** * The selector we'll be monitoring */ protected Selector selector; private boolean running = false; private boolean processAll = true; /** * Whether we are using SSL/TLS */ protected final boolean usingSSL; /** * The associated SSLContext */ protected SSLContext context = null; /** * A SocketContainer to hold active socket instances */ protected final SocketContainer container = new SocketContainer(); private AbstractPacketWorker packetWorker; /** * The underlying TaskWorker */ protected final TaskWorker taskWorker; /** * The underlying TimeoutWorker */ protected final TimeoutWorker toWorker; /** * */ protected String[] protocols; /** * */ protected String[] cipherSuits; /** * Whether this AbstractSelector is a client */ protected final boolean isClient; /** * Whether this AbstractSelector needs clientAuth */ protected final boolean needClientAuth; /** * Whether the SSLEngine tasks required run in the AbstractSelector thread */ protected final boolean singleThreaded; /** * Create a new AbstractSelector instance. * * @param address The address this selector will use * @param port The port this selector will use * @param packetWorker The instance of packet worker to use * @param usingSSL Whether we are using SSL/TLS * {@link ch.dermitza.securenio.socket.secure.TaskWorker} thread. * @param isClient If the current Selector implementation is a client * implementation (false indicates it is a server implementation). * @param needClientAuth If the current implementation is a server * implementation, whether the client should also verify its authenticity * (i.e. sets up SSLEngine.setNeedClientAuth(true)). */ public AbstractSelector(InetAddress address, int port, AbstractPacketWorker packetWorker, boolean usingSSL, boolean isClient, boolean needClientAuth) { this.address = address; this.port = port; this.singleThreaded = PropertiesReader.getSelectorSingleThreaded(); this.processAll = PropertiesReader.getSelectorProcessAll(); this.usingSSL = usingSSL; this.isClient = isClient; this.needClientAuth = needClientAuth; this.packetWorker = packetWorker; this.taskWorker = (singleThreaded)?null:new TaskWorker(this); //this.taskWorker = new TaskWorker(this); this.toWorker = new TimeoutWorker(); logger.log(Level.FINE, "Using ssl: {0}", usingSSL); } /** * If the server/client has been initialized to use SSL/TLS, this method is * used to setup and initialize the SSL/TLS required parameters. * * @param trustStoreLoc The location of the trustStore on the disk. The * trustore is *ALWAYS* required for a client implementation. For a server * implementation it is only required if needClientAuth is true, i.e. IFF * the client should also verify its authenticity; it can be null otherwise. * @param keyStoreLoc The location of the keyStore on the disk. The keystore * is *ALWAYS* required for a server implementation. For a client * implementation it is only required if needClientAuth is true, i.e. IFF * the client should also verify its authenticity; it can be null otherwise. * @param tsPassPhrase The passphrase to use with the trustStore or null if * the truststore is not required. * @param ksPassPhrase The passphrase to use with the keyStore or null if * the keystore is not required. * @param protocolsLoc The location of the file containing the SSL/TLS * protocols to be used by the client/server. WARNING: If null is passed, or * the file is not found, the default SSL/TLS protocols will be used * instead, as per SSLEngine.getEnabledProtocols(). * @param cipherSuitesLoc The location of the file containing the SSL/TLS * cipher suites to be used by the client/server. WARNING: If null is * passed, or the file is not found, the default SSL/TLS cipher suites will * be used instead, as per SSLEngine.getEnabledCipherSuites(). */ public void setupSSL(String trustStoreLoc, String keyStoreLoc, char[] tsPassPhrase, char[] ksPassPhrase, String protocolsLoc, String cipherSuitesLoc) { if (!usingSSL) { logger.log(Level.WARNING, "Trying to set SSL parameters with a " + "non-SSL/TLS {0}" + ". SSL/TLS was NOT set or initialized.", (isClient ? "client" : "server")); return; } protocols = PropertiesReader.getProtocols(); cipherSuits = PropertiesReader.getCipherSuites(); TrustManagerFactory tmf = null; KeyManagerFactory kmf = null; KeyStore ks = null; FileInputStream fis = null; if (isClient || (!isClient && needClientAuth)) { // Need to initialize truststores try { tmf = TrustManagerFactory.getInstance("SunX509"); ks = KeyStore.getInstance("JKS"); fis = new FileInputStream(trustStoreLoc); ks.load(fis, tsPassPhrase); tmf.init(ks); } catch (NoSuchAlgorithmException nsae) { nsae.printStackTrace(); // tmf } catch (KeyStoreException kse) { kse.printStackTrace(); // ks, tmf.init() } catch (FileNotFoundException fnfe) { fnfe.printStackTrace(); // fis } catch (IOException ioe) { ioe.printStackTrace(); // ks.load() } catch (CertificateException ce) { ce.printStackTrace(); // ks.load() } finally { try { fis.close(); } catch (IOException ioe) { ioe.printStackTrace(); // fis.close(); } } } if (!isClient || (isClient && needClientAuth)) { // Need to initialize keystores try { kmf = KeyManagerFactory.getInstance("SunX509"); ks = KeyStore.getInstance("JKS"); fis = new FileInputStream(keyStoreLoc); ks.load(fis, ksPassPhrase); kmf.init(ks, ksPassPhrase); } catch (NoSuchAlgorithmException nsae) { nsae.printStackTrace(); // kmf } catch (KeyStoreException kse) { kse.printStackTrace(); // kmf.init() } catch (UnrecoverableKeyException uke) { uke.printStackTrace(); // kmf.init() } catch (FileNotFoundException fnfe) { fnfe.printStackTrace(); // fis, fis.close() } catch (IOException ioe) { ioe.printStackTrace(); // ks.load() } catch (CertificateException ce) { ce.printStackTrace(); // ks.load() } finally { try { fis.close(); } catch (IOException ioe) { ioe.printStackTrace(); // fis.close(); } } } // Finally, initialize the context try { context = SSLContext.getInstance("TLS"); if (kmf == null) { context.init(null, tmf.getTrustManagers(), null); } else if (tmf == null) { context.init(kmf.getKeyManagers(), null, null); } else { context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null); } } catch (NoSuchAlgorithmException nsae) { nsae.printStackTrace(); // context } catch (KeyManagementException kme) { kme.printStackTrace(); // context.init() } } /** * Sets up the underlying {@link SSLEngine} to be used with a * {@link ch.dermitza.securenio.socket.secure.SecureSocket} implementation. * The SSLEngine is initialized based on whether this instance is a server * or a client, whether we need clientAuth or not, and based on the provided * protocols and cipher suites provided in {@link * #setupSSL(String trustStoreLoc, String keyStoreLoc, char[] tsPassPhrase, * char[] ksPassPhrase, String protocolsLoc, String cipherSuitesLoc)}. The * peerHost and peerPort parameters are passed as hints to the * {@link SSLEngine} for engine re-usage purposes but can also be null. * * @param peerHost The peer host of the socket * @param peerPort The peer port of the socket * @return An initialized and configured SSLEngine ready to be used */ protected SSLEngine setupEngine(String peerHost, int peerPort) { SSLEngine engine = context.createSSLEngine(peerHost, peerPort); engine.setUseClientMode(isClient); engine.setNeedClientAuth(needClientAuth); // Setup protocols and suites try { engine.setEnabledProtocols(protocols); } catch (IllegalArgumentException iae) { logger.log(Level.WARNING, "Provided protocols invalid, using default", iae); } try { engine.setEnabledCipherSuites(cipherSuits); } catch (IllegalArgumentException iae) { logger.log(Level.WARNING, "Provided cipher suites invalid, using default", iae); } return engine; } /** * Initializes the selector, the worker threads, and initiates a select * procedure that can be either server or client based. The select operation * can be interrupted when there are pending tasks to be completed, which * happens via the {@link #processChanges()} method. Upon shutdown, a * best-effort attempt is made to shutdown cleanly via the * {@link #shutdown()} method. * * @see #processChanges() * @see #shutdown() */ @Override public void run() { try { // Initialize the selector selector = SelectorProvider.provider().openSelector(); // Now init the connection // this is implementation specific (server or client wise) initConnection(); new Thread(packetWorker, "PacketWorkerThread").start(); new Thread(taskWorker, "TaskWorkerThread").start(); new Thread(toWorker, "TimeoutWorkerThread").start(); running = true; } catch (IOException ioe) { running = false; logger.log(Level.SEVERE, "Could not initialize selector, shutting down", ioe); } int keyNo; while (running) { try { // Process any pending changes processChanges(); // Wait for an event on one of the registered channels keyNo = (processAll) ? selector.select() : selector.select(PropertiesReader.getSelectorTimeoutMS()); if (keyNo > 0) { // Iterate over the set of keys for which events are available Iterator<SelectionKey> selectedKeys = this.selector.selectedKeys().iterator(); while (selectedKeys.hasNext()) { SelectionKey key = selectedKeys.next(); selectedKeys.remove(); // Key could have been invalidated, if so // just ignore it and go back to selecting if (!key.isValid()) { continue; } // Check what event is available and deal with it // IMPORTANT: key can be A COMBINATION OF all states // e.g. acceptable and connectable and readable etc. // Elseifs would ignore the multistatus, use separate // ifs instead if (key.isAcceptable()) { // Accept, we are inside a server accept(key); } if (key.isConnectable()) { // Connect, we are inside a client connect(key); } if (key.isValid() && key.isReadable()) { // Ready to read stuff read(key); } if (key.isValid() && key.isWritable()) { // Ready to write stuff // however, application data will be consumed (destroyed) // if we are still handshaking. In this case, we need // to unregister the key for being writable until the // handshake is complete //if(container.getSocket(key.channel()).handshakePending()){ // need to flush // try{ // container.getSocket(key.channel()).flush(); // } catch(IOException ioe){ // System.out.println("IOE while flushing"); // closeSocket(container.getSocket(key.channel())); // } // }else{ write(key); // } } } } } catch (IOException ioe) { // Selector should NOT throw an IOException during select(), // if it does, something is really wrong, shutdown running = false; logger.log(Level.SEVERE, "IOException in select(), shutting down", ioe); } } shutdown(); } /** * Invalidate the {@link javax.net.ssl.SSLSession} associated with the * provided {@link SocketIF}. The invalidation is queued as a pending change * where it is executed in a FIFO fashion along with any other pending * changes. This method could be periodically used via a * {@link ch.dermitza.securenio.socket.timeout.worker.Timeout} to perform * SSL/TLS session rotation if needed. * * @param socket The socket whose underlying * {@link javax.net.ssl.SSLSession} should be invalidates * * @see #processChanges() */ protected void invalidateSession(SocketIF socket) { synchronized (this.pendingChanges) { socket.invalidateSession(); pendingChanges.add(new ChangeRequest(socket, ChangeRequest.TYPE_SESSION, 0)); this.selector.wakeup(); } } /** * Send an {@link ByteBuffer} over the specified {@link SocketIF}. This * method does not directly send the packets, but rather queues them for * sending as soon as possible. <p> Data are subsequently written by setting * the associated {@link SelectionKey} to SelectionKey.OP_WRITE. In case of * an SSL/TLS implementation and where the handshaking is not completed, the * SelectionKey is not changed until the handshake has finished. * * @param socket The SocketIF to send the packet through * @param data The ByteBuffer to send through the associated SocketIF * * @see #processChanges() */ protected void send(SocketIF socket, ByteBuffer data) { synchronized (this.pendingChanges) { // If the handshake has been completed, indicate that we want the // interest ops changed to OP_WRITE. If the handshake is still // pending, a PendingChange will be issued when the handshake has // been completed. if (!socket.handshakePending()) { pendingChanges.add(new ChangeRequest(socket, ChangeRequest.TYPE_OPS, SelectionKey.OP_WRITE)); } // Queue the data we want written to the remote end. If a queue does // not yet exist, a new one will be created and the data will be // added to it. synchronized (this.pendingData) { List<ByteBuffer> queue = this.pendingData.get(socket.getSocket()); if (queue == null) { queue = new ArrayList<>(); this.pendingData.put(socket.getSocket(), queue); } queue.add(data); } } // If the handshake has been previously completed, wake up our selecting // thread so it can make the required changes. If the handshake is still // pending, there are no changes to be made, so we can leave the selector // in the select() state until an actual change happens. if (!socket.handshakePending()) { this.selector.wakeup(); } } /** * Any pending {@link ChangeRequest}s are processed via this method, in the * {@link AbstractSelector} thread. There is an option to process everything * at once, once this method is called, or to process a part of changes * queued. In the first case, care should be taken that the queued changes * are not so many that the main thread cannot return to the select() fast * enough to process a large number of connections or data reads. In the * second case, care should be taken that sufficiently many changes are * processed in time, such that the change queue does not grow too large * and/or remotely connected systems timeout (and potentially fail) due to * our inactivity to process changes fast enough. * * @param processAll Whether or not to process all changes in a single pass * * @see ChangeRequest */ private void processChanges() { int changeCount = 0; synchronized (this.pendingChanges) { for (ChangeRequest change : this.pendingChanges) { SelectionKey key; switch (change.getType()) { // The request concerns switching the interestOps of a key // associated with a particular socket case ChangeRequest.TYPE_OPS: key = change.getChannel().getSocket().keyFor(selector); // At this point we might get a CancelledKeyException if // we are trying to set the interestOps on a key that has // been previously been cancelled. Check if it is valid // before changing the interestOps; // It can also be the case that the the socket has already been // unregistered with the selector, thus having a null key. In this // case, we do not need to process anything regarding that socket if (key != null && key.isValid()) { key.interestOps(change.getOps()); } break; // The request concerns an SSLEngineTask that has just // finished running on the TaskWorker thread case ChangeRequest.TYPE_TASK: // At this point, we need to resume processing the // SSL/TLS handshake on the associated socket. try { // First update the result of the finished task change.getChannel().updateResult(); // Then continue processing the handshake // TODO, processHandshake can be merged with // inithandshake change.getChannel().processHandshake(); } catch (IOException ioe) { // At this point, the handshake is NOT completed. // Drop the socket. logger.log(Level.INFO, "IOE after task", ioe); closeSocket(change.getChannel()); } break; case ChangeRequest.TYPE_TIMEOUT: // The timeout has expired on the given socket. // As such, the socket needs to be closed logger.config("Timeout expired"); closeSocket(change.getChannel()); break; case ChangeRequest.TYPE_SESSION: // The SSL/TLS session has been invalidated, we need to // re-initiate handshaking try { change.getChannel().initHandshake(); } catch (IOException ioe) { // At this point, the handshake is NOT completed. // Drop the socket. logger.log(Level.INFO, "IOE while initializing handshake", ioe); closeSocket(change.getChannel()); } } // Remove the change we just processed pendingChanges.removeFirst(); changeCount++; if (!processAll && changeCount >= PropertiesReader.getMaxChanges()) { // processed the changes we were asked to. Break from the // loop only clearing the changes we just processed. The // rest of changes will be processed in a subsequent iteration. return; } } // All pending changes have been processed at this point // we are free to clear the list. NOTE: if the pending changes to // be processed are too many, this can cause the selecting thread // to start refusing connections. It could in this case be better // to not process everything at once, but rather process them one at // a time this.pendingChanges.clear(); } } /** * Initialize a connection. This method is up to the server or client * implementations to implement, but at the minimum it should set * implemented channels to non-blocking, and set-up any SSL/TLS * configuration if necessary. * * @throws IOException Propagates all underlying IOExceptions as thrown, to * be handled by the application layer. * * @see AbstractSelector#run() */ protected abstract void initConnection() throws IOException; /** * Should accept incoming connections and bind new non-blocking * {@link SocketIF} instances to them. If the server implementation is using * SSL/TLS, it should also set up the {@link SSLEngine}, to be used. * * @param key The selection key with the underlying {@link SocketChannel} to * be accepted * * @see AbstractSelector#run() */ protected abstract void accept(SelectionKey key); /** * Should finish the connection to the server. This method should also * instantiate an SSLEngine handshake if the underlying {@link SocketIF} is * a secure socket. Finally, after the connection has been established, the * socket should be registered to the underlying {@link Selector}, with a * {@link SelectionKey} of OP_READ, signalling it is ready to read data. * * @param key The selection key with the underlying {@link SocketChannel} * that needs a connection finalization. */ protected abstract void connect(SelectionKey key); /** * Writes data to the socket associated with the given {@link SelectionKey}. * This method is ONLY called once we have set the {@link SelectionKey} * associated with the socket to OP_WRITE. It tries to write as much data as * possible before returning. Once there is no more data to be written on * this socket, it sets the {@link SelectionKey} to OP_READ, disallowing any * further calls to this method until more data is available. * * @param key The SelectionKey whose associated socket we should write on * * @see #processChanges() * @see #send(ch.dermitza.securenio.socket.SocketIF, java.nio.ByteBuffer) */ protected void write(SelectionKey key) { SocketIF socketChannel = container.getSocket(key.channel()); synchronized (this.pendingData) { List<ByteBuffer> queue = this.pendingData.get(socketChannel.getSocket()); // Write until there's not more data ... while (!queue.isEmpty()) { ByteBuffer buf = queue.get(0); try { int written = socketChannel.write(buf); logger.log(Level.FINEST, "Written {0} bytes", written); } catch (IOException ioe) { // If a IOE happens when writing, something really bad // happened (generally). Close the socket where the write // was happening logger.log(Level.INFO, "IOE while writing", ioe); closeSocket(socketChannel); return; } if (buf.remaining() > 0) { // ... or the socket's buffer fills up break; } queue.remove(0); } if (queue.isEmpty()) { // We wrote away all data, so we're no longer interested // in writing on this socket. Switch back to waiting for // data. key.interestOps(SelectionKey.OP_READ); } } } /** * Reads data from the socket associated with the given * {@link SelectionKey}. This method is called as soon as data is ready to * be read. It tries to read as much data as possible, handing bytes read to * the underlying {@link AbstractPacketWorker} for reconstruction and * further processing. It also handles potential socket disconnections * and/or errors, upon which, it makes a best effort to close the socket * cleanly. * * @param key The SelectionKey whose associated socket we should read from * * @see AbstractPacketWorker#addData(SocketIF, ByteBuffer, int) * @see #closeSocket(ch.dermitza.securenio.socket.SocketIF) */ protected void read(SelectionKey key) { SocketIF socketChannel = container.getSocket(key.channel()); // Clear out our read buffer so it's ready for new data this.readBuffer.clear(); // Attempt to read off the channel int numRead; try { numRead = socketChannel.read(readBuffer); //numRead = socketChannel.read(this.readBuffer); logger.log(Level.FINEST, "Read {0} bytes", numRead); } catch (IOException ioe) { // The remote forcibly closed the connection, cancel // the selection key and close the channel. // Closing the channel automatically cancels the key // TODO, recover the IP here logger.log(Level.INFO, "Remote forcibly disconnected", ioe); closeSocket(socketChannel); return; } catch (BufferOverflowException boe) { // Can be thrown during read from a secure socket // We would need to handle this, TODO logger.log(Level.INFO, "BufferOverflowException while reading", boe); closeSocket(socketChannel); return; } if (numRead == -1) { // Remote entity shut the socket down cleanly. Do the // same from our end and cancel the channel. // Closing the channel automatically cancels the key // TODO, recover the IP here logger.config("Remote disconnected"); closeSocket(socketChannel); return; } if (numRead > 0) { // Here we have a bytebuffer with some data on a SocketChannel // We need to construct a packet and then fire the listener methods // this happens in the worker thread packetWorker.addData(socketChannel, readBuffer, numRead); } } /** * Closes the given {@link SocketIF}. In doing that, it also removes any * potentially queued {@link ChangeRequest}s not yet processed on that * socket, and also removes the socket from the underlying * {@link SocketContainer}. * * TODO: Only pending data is being removed. A correct implementation would * also remove all other pendingChanges instances. Alternatively, we should * check for closed sockets when processing changes * ({@link #processChanges()}) and not perform any changes if the * socket is closed. * * @param socket The socket to close * * @see #processChanges() * */ protected void closeSocket(SocketIF socket) { try { logger.log(Level.INFO, "Disconnecting remote: {0}", socket.getSocket().getRemoteAddress().toString()); } catch (IOException ioe) { logger.log(Level.INFO, "IOE while obtaining remote IP", ioe); } if (socket != null) { try { socket.close(); } catch (IOException ioe) { logger.log(Level.INFO, "Closing socket failed", ioe); // We need to at least try to cancel the key here SelectionKey key = socket.getSocket().keyFor(selector); if (key != null) { logger.finest("Cancelling registered key"); key.cancel(); } } finally { // remove all pending bytebuffers registered to be sent through this // socket in pendingData synchronized (this.pendingData) { List<ByteBuffer> queue = this.pendingData.get(socket.getSocket()); // clear the queue if it is not empty if (queue != null && !queue.isEmpty()) { queue.clear(); } // remove the socket reference from pendingData pendingData.remove(socket.getSocket()); } // remove the reference from the container container.removeSocket(socket.getSocket()); //logger.log(Level.INFO, "Disconnecting remote: " + reason); } } } //----------------------- RUNNABLE METHODS -------------------------------// /** * Check whether the {@link AbstractSelector} is running. * * @return true if it is running, false otherwise */ public boolean isRunning() { return this.running; } /** * Set the running status of the {@link AbstractSelector}. If the running * status of the selector is set to false, the selector is interrupted in * order to cleanly shutdown by invoking {@link Selector#wakeup()}. * * @param running Whether the AbstractSelector should run or not */ public void setRunning(boolean running) { this.running = running; // If a selector is already blocked in select() // and someone asked us to shutdown, // we should interrupt it so that it shuts down // after processing all possible pending requests if (!running) { selector.wakeup(); } } /** * Shutdown procedure. This method is called if the {@link AbstractSelector} * was asked to shutdown; it cleanly process the shutdown procedure, cleanly * shutting down all associated worker threads, closing all open sockets, * and clearing any pending {@link ChangeRequest}s. */ private void shutdown() { logger.config("Shutting down.."); // Close the packetworker if(packetWorker.isRunning()){ packetWorker.setRunning(false); } // Close the taskWorker if(!singleThreaded){ if(taskWorker.isRunning()){ taskWorker.setRunning(false); } } // Cancel all pending timeouts if(toWorker.isRunning()){ toWorker.setRunning(false); } // Close all channels registered with the selector // This automatically invalidates the keys, so we dont // need to invalidate them ourselves Set<SelectionKey> keys = selector.keys(); for (SelectionKey key : keys) { if (key.channel().isOpen()) { closeSocket(container.getSocket(key.channel())); } } // After all sockets are closed, clear pending changes. // Pending data associated with the sockets has already been invalidated // by the closeSocket() method synchronized (this.pendingChanges) { pendingChanges.clear(); } // Close the selector too try { selector.close(); } catch (IOException ioe) { // Ignore } } //----------------------- CHANGES METHODS -------------------------------// /** * Unused future clean-up implementation TODO * * @param changeRequest */ private void queueChangeRequest(ChangeRequest changeRequest) { // Queue the ChangeRequest synchronized (this.pendingChanges) { pendingChanges.add(changeRequest); } // And wake up the selecting thread so it can make the required changes this.selector.wakeup(); } /** * Unused future clean-up implementation TODO * * @param changeRequest */ private boolean dataExists(SocketIF socket) { // Check if data exists for the socket supplied to us synchronized (this.pendingData) { List<ByteBuffer> queue = this.pendingData.get(socket.getSocket()); if (queue == null || queue.isEmpty()) { // There is no data to be written, we do not need to register // for writing, we can just return. return false; } else { return true; } } } //----------------------- LISTENER METHODS -------------------------------// /** * The timeout on this socket has expired. We need to close the socket. As * this is called from a different thread, we need to queue a request for * closing the socket * * @param socket The socket of which its timeout has expired */ @Override public void timeoutExpired(SocketIF socket) { synchronized (this.pendingChanges) { pendingChanges.add(new ChangeRequest(socket, ChangeRequest.TYPE_TIMEOUT, 0)); } // Finally, wake up our selecting thread so it can make the required changes this.selector.wakeup(); } /** * A SSLEngine Task for a particular socket was completed by the TaskWorker. * As it is completed, the handshake on this particular socket is ready to * continue immediately. Since this method is called from the TaskWorker * thread, we need to queue a request for continuing to process the * handshake * * @param socket The socket of which an SSLEngine task was completed */ @Override public void taskComplete(SocketIF socket) { // A SSLEngine task for a particular socket was completed by the // TaskWorker. As it is completed, the handshake on this particular // socket is ready to continue immediately. Since this method is called // from the TaskWorker thread, we need to queue a request for continuing // to process the handshake synchronized (this.pendingChanges) { pendingChanges.add(new ChangeRequest(socket, ChangeRequest.TYPE_TASK, 0)); } // Finally, wake up our selecting thread so it can make the required changes this.selector.wakeup(); } // @Override // public void crap(SocketIF socket){ // synchronized (this.pendingChanges){ // pendingChanges.add(new ChangeRequest(socket, ChangeRequest.TYPE_OPS, SelectionKey.OP_WRITE)); // } // this.selector.wakeup(); // } /** * A handshake is completed on this socket, as such the socket is ready to * be used (reading and writing). We need to see if there is data already * queued for the particular socket and if there is, to set the associated * key as writable. * * @param socket the socket of which its handshake is completed */ @Override public void handshakeComplete(SocketIF socket) { synchronized (this.pendingData) { List<ByteBuffer> queue = this.pendingData.get(socket.getSocket()); if (queue == null || queue.isEmpty()) { // There is no data to be written, we do not need to register // for writing, we can just return. return; } else { // Data exists, we need to register for writing synchronized (this.pendingChanges) { pendingChanges.add(new ChangeRequest(socket, ChangeRequest.TYPE_OPS, SelectionKey.OP_WRITE)); } } } // Finally, wake up our selecting thread so it can make the required changes this.selector.wakeup(); } /** * Pass-through method to allow registration of multiple * {@link PacketListener}s to the underlying {@link AbstractPacketWorker}. * * @param listener The listener to register to the underlying PacketWorker */ public void addListener(PacketListener listener) { packetWorker.addListener(listener); } /** * Pass-through method to allow de-registration of multiple * {@link PacketListener}s from the underlying {@link AbstractPacketWorker}. * * @param listener The listener to unregister from the underlying * PacketWorker */ public void removeListener(PacketListener listener) { packetWorker.removeListener(listener); } }