/** * 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.packet.worker; import ch.dermitza.securenio.packet.PacketIF; import ch.dermitza.securenio.packet.PacketListener; import ch.dermitza.securenio.socket.SocketIF; import ch.dermitza.securenio.util.PropertiesReader; import ch.dermitza.securenio.util.logging.LoggerHandler; import java.nio.ByteBuffer; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.HashMap; import java.util.logging.Level; import java.util.logging.Logger; /** * An abstract, extensible implementation of a Packet Worker. This thread waits * for raw data arriving from remote peers through {@link SocketIF}s, adds them * to respective queues and then reassembles packets based on the data in these * queues. Each socket maintains a queue of data, for there is a possibility * that the packets received are fragmented and cannot be reassembled during * processing. In this case, processing should be delegated until additional * data on that socket arrives. * * @author K. Dermitzakis * @version 0.19 * @since 0.18 */ public abstract class AbstractPacketWorker implements Runnable { private final Logger logger = LoggerHandler.getLogger(AbstractPacketWorker.class.getName()); private final ArrayList<PacketListener> listeners = new ArrayList<>(); /** * Maps a SocketChannel to a list of ByteBuffer instances */ protected final HashMap<SocketIF, ByteBuffer> pendingData = new HashMap<>(); /** * Sockets that need to be operated upon (i.e. have pending operations) */ protected final ArrayDeque<SocketIF> pendingSockets = new ArrayDeque<>(); private boolean running = false; private byte[] tempArray; /** * Queue data received from a {@link SocketIF} for processing and * reconstruction. A deep copy of the passed {@link ByteBuffer} is made * locally. Following, a data queue for that {@link SocketIF} is created if * it does not exist, and the current piece of data received is added to * that queue for later processing and reconstruction. * * @param socket The SocketIF data was received from * @param data The ByteBuffer containing the data (bytes) received * @param count The number of bytes received * * @see #processData() */ public void addData(SocketIF socket, ByteBuffer data, int count) { tempArray = new byte[count]; System.arraycopy(data.array(), 0, tempArray, 0, count); synchronized (this.pendingSockets) { if (!pendingSockets.contains(socket)) { // Check that we do not add a socket twice. Once is enough // to trigger processing pendingSockets.add(socket); } synchronized (this.pendingData) { ByteBuffer buffer = this.pendingData.get(socket); if (buffer == null) { // allocate a large enough buffer to hold the data we // just received int size = (count > PropertiesReader.getPacketBufSize()) ? count : PropertiesReader.getPacketBufSize(); buffer = ByteBuffer.allocate(size); this.pendingData.put(socket, buffer); } if (count > buffer.remaining()) { int diff = count - buffer.remaining(); logger.log(Level.CONFIG, "Buffer needs resizing, remaining{0}" + "needed {1} difference {2}", new Object[]{buffer.remaining(), count, diff}); logger.log(Level.FINEST, "old size: {0}", buffer.capacity()); // Allocate a new buffer. To minimize new buffer allocation, // we resize the buffer appropriately, in case this is a // *really* busy channel. Notes: If it is an extremely busy // channel, the buffer will keep growing, potentially making // the worker run out of memory trying to continuously // allocate larger and larger buffers. Also, a continuously // growing buffer can also indicate that the underlying // data is never or wrongly processed, that can indicate a // problem with the end application. int extSize = (diff > PropertiesReader.getPacketBufSize()) ? diff : PropertiesReader.getPacketBufSize(); ByteBuffer temp = ByteBuffer.allocate(buffer.capacity() + extSize); logger.log(Level.FINEST, "new size: {0}", temp.capacity()); // Flip existing buffer to prepare for putting in the replacement buffer.flip(); logger.log(Level.FINEST, "pos {0} lim {1} cap {2}", new Object[]{buffer.position(), buffer.limit(), buffer.capacity()}); // put existing buffer into the temporary replacement temp.put(buffer); // Remove the old reference this.pendingData.remove(socket); // Replace reference buffer = temp; // associate the new buffer with the socket this.pendingData.put(socket, buffer); // Buffer is now resized appropriately, let the data be // added naturally } // Make a copy of the data buffer.put(tempArray); logger.log(Level.FINEST, "pos {0} lim {1} cap {2}", new Object[]{buffer.position(), buffer.limit(), buffer.capacity()}); } pendingSockets.notify(); } } /** * This is the main entry point of received data processing and reassembly. * This is left for the application layer to decide how to process raw * incoming data */ protected abstract void processData(); /** * The run() method of the {@link AbstractPacketWorker}. Here, sequential * processing of data that need to be reconstructed and processed should be * done in a FIFO fashion. The {@link AbstractPacketWorker} is otherwise * waiting for incoming tasks via the * {@link #addData(SocketIF, ByteBuffer, int)} method. */ @Override public void run() { running = true; logger.config("Initializing..."); runLoop: while (running) { // Wait for data to become available synchronized (pendingSockets) { while (pendingSockets.isEmpty()) { // Check whether someone asked us to shutdown // If its the case, and as the queue is empty // we are free to break from the main loop and // call shutdown(); if (!running) { break runLoop; } try { pendingSockets.wait(); } catch (InterruptedException e) { } } // We have some data on a socket here } // Do something with the data here processData(); } shutdown(); } /** * Check whether the {@link AbstractPacketWorker} is running. * * @return true if it is running, false otherwise */ public boolean isRunning() { return this.running; } /** * Set the running status of the {@link AbstractPacketWorker}. If the * running status of the worker is set to false, the AbstractPacketWorker is * interrupted (if waiting for a task) in order to cleanly shutdown. * * @param running Whether the PacketWorker should run or not */ public void setRunning(boolean running) { this.running = running; // If the worker is already blocked in queue.wait() // and someone asked us to shutdown, // we should interrupt it so that it shuts down // after processing all possible pending requests if (!running) { synchronized (pendingSockets) { pendingSockets.notify(); } } } /** * Shutdown procedure. This method is called if the * {@link AbstractPacketWorker} was asked to shutdown; it cleanly process * the shutdown procedure, clearing any queued data remaining and removing * all listener references. */ private void shutdown() { logger.config("Shutting down..."); // Clear the queue pendingData.clear(); pendingSockets.clear(); // Remove all listener references listeners.clear(); } //----------------------- LISTENER METHODS -------------------------------// /** * Allows registration of multiple {@link PacketListener}s to this * {@link AbstractPacketWorker}. * * @param listener The listener to register to this PacketWorker */ public void addListener(PacketListener listener) { synchronized (listeners) { listeners.add(listener); } } /** * Allows de-registration of multiple {@link PacketListener}s from this * {@link AbstractPacketWorker}. * * @param listener The listener to unregister from this PacketWorker */ public void removeListener(PacketListener listener) { synchronized (listeners) { listeners.remove(listener); } } /** * Once a {@link PacketIF} has been completely reconstructed, registered * listeners are notified via this method. This method creates a local copy * of the already registered listeners when firing events, to avoid * potential concurrent modification exceptions. * * @param socket The SocketIF a complete PacketIF is reconstructed * @param packet The completely reconstructed AbstractPacket * * @see PacketListener#paketArrived(SocketIF, PacketIF) */ protected void fireListeners(SocketIF socket, PacketIF packet) { PacketListener[] temp; if (!listeners.isEmpty()) { temp = (PacketListener[]) listeners.toArray(new PacketListener[listeners.size()]); for (PacketListener listener : temp) { listener.paketArrived(socket, packet); } } } }