/** * 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.socket.SocketIF; import java.nio.ByteBuffer; /** * An abstract, extensible implementation of a variable length packet worker. * This worker extends {@link AbstractPacketWorker} to support variable length * packets. <p> For the default implementation, packets should extend * {@link ch.dermitza.securenio.packet.PacketIF} and have a 3-byte header: 1 * byte for packet designation and 2 bytes for message length. This results in * 255 packet types, with maximum packet length of 32,767 bytes if using java * shorts or 65,535 bytes if using an unsigned length implementation. NOTE: * packet length excludes the 3-byte header, i.e. is payload length. If more * packet types or a larger packet (payload) size is needed, use the argument * constructor. Note that the largest header size the constructor can take is 2 * bytes, and the length variable can either be 2 or 4 bytes, representing java * shorts and ints respectively. <p> * * Application code should extend the singular method * {@link #assemblePacket(ch.dermitza.securenio.socket.SocketIF, short, byte[])} * to assemble the reconstructed packet and subsequently * {@link #fireListeners(SocketIF, ch.dermitza.securenio.packet.PacketIF)} with * the reconstructed packet. * * @author K. Dermitzakis * @version 0.18 */ public abstract class VariableLengthPacketWorker extends AbstractPacketWorker { /** * The default header length */ public static final int HEADER_LENGTH = 1; /** * The default packet size length */ public static final int SIZE_LENGTH = 2; private final int headerLength; private final int sizeLength; /** * Initializes this packet worker to work with packets having a custom * header and payload length. * * @param singleByte true if the header is one byte, false if it is 2 bytes * long * @param shortSize true if the payload length is 2 bytes, false if it is 4 * bytes long */ public VariableLengthPacketWorker(boolean singleByte, boolean shortSize) { this.headerLength = singleByte ? HEADER_LENGTH : 2; this.sizeLength = shortSize ? SIZE_LENGTH : 4; } /** * Initializes this packet worker to work with packets having a 1 byte * header and 2 bytes payload length. */ public VariableLengthPacketWorker() { this(true, true); } /** * Process data received on pending sockets, in a FIFO fashion. If data * received is not enough to reconstruct a packet, the socket's pending * status is removed, as we cannot do further processing on it. The socket's * pending status will be re-instantiated once more data on that particular * socket has been received. Sockets that have been completely drained from * data are also removed here. */ @Override protected void processData() { synchronized (this.pendingSockets) { while (!pendingSockets.isEmpty()) { // Peek at the first element SocketIF socket = pendingSockets.getFirst(); // See if this socket has data ByteBuffer data = pendingData.get(socket); if (data == null) { //System.out.print("Socket has no data, removing"); // This socket is not pending, remove it from the queue pendingSockets.removeFirst(); // Go for the next socket break; } parseBuffer(socket, data); } } } /** * Parse a buffer with data on a socket. If there is enough data to * reconstruct a packet here, then * {@link #assemblePacket(SocketIF, short, byte[])} is called. If there is * not enough data to reconstruct a packet, * {@link #prepareForWait(ByteBuffer)} is called, and the socket's pending * status is removed. * * @param socket The socket we have some data to parse on * @param data The data that needs parsing * * @see #assemblePacket(SocketIF, short, byte[]) * @see #prepareForWait(ByteBuffer) */ private void parseBuffer(SocketIF socket, ByteBuffer data) { synchronized (this.pendingSockets) { //System.out.println("Preparing the buffer"); // prepare the buffer. This should happen ONLY once //System.out.println("limit " + data.limit() + " position " + data.position()); data.flip(); //System.out.println("After flip: limit " + data.limit() + " position " + data.position()); while (data.position() < data.limit()) { if (data.limit() - data.position() > (headerLength + sizeLength)) { //System.out.println("Have at least header+size bytes, reading"); // get the first byte // get the header short head; if (headerLength == 1) { head = data.get(); } else { head = data.getShort(); } // get the message length int len; if (sizeLength == 2) { len = data.getShort(); } else { len = data.getInt(); } //System.out.println("Length: " + (len + 3)); if (data.limit() - data.position() >= len) { // we have at least one complete packet, make a copy and // send it for reassembly byte[] bytes = new byte[len]; data.get(bytes); assemblePacket(socket, head, bytes); //System.out.println("Removing read bytes"); // Fix the buffer //System.out.println("Limit: " + data.limit() + " position: " + data.position()); data.compact(); //System.out.println("After compact Limit: " + data.limit() + " position: " + data.position()); data.flip(); //System.out.println("After flip Limit: " + data.limit() + " position: " + data.position()); if (data.position() == data.limit()) { //System.out.println("No more data, removing socket and buffer"); // no more data on this socket, remove it pendingData.remove(socket); pendingSockets.removeFirst(); } } else { //System.out.println("Not enough data to reconstruct packet"); prepareForWait(data); break; } } else { //System.out.println("Not enough data to read yet"); prepareForWait(data); break; } } //System.out.println("loop was broken"); } } /** * This method is called if we have read some data from the buffer on a * particular socket during {@link #processData()}, but the buffer does not * have enough data received to reconstruct a complete packet. In this case, * we need to reset the buffer in a configuration where it can receive * additional data. Furthermore, as the socket we read data on does still * not have enough data to reconstruct a complete packet, we remove it from * being pending, as it is not pending for further processing at this point. * It will become pending again in subsequent data reception. * * @param data The ByteBuffer to reset in a writable configuration */ private void prepareForWait(ByteBuffer data) { //System.out.println("Limit: " + data.limit() + " position: " + data.position()); // set the position to the limit data.position(data.limit()); //System.out.println("Setting position = limit: " + data.limit() + " position: " + data.position()); data.flip(); //System.out.println("After flip: Limit: " + data.limit() + " position: " + data.position()); data.compact(); //System.out.println("After compact: Limit: " + data.limit() + " position: " + data.position()); pendingSockets.removeFirst(); //System.out.println("PendingSockets isempty(): " + pendingSockets.isEmpty()); } /** * This method is called from the packet worker when a complete packet has * been received. It should be extended to reconstruct the packet into what * the application code needs, and subsequently fire the packet listeners to * act on the packet received. * * @param socket The socket on which a complete packet was received * @param head The head of the packet * @param data The packet's payload * * @see #fireListeners(SocketIF, ch.dermitza.securenio.packet.PacketIF) */ protected abstract void assemblePacket(SocketIF socket, short head, byte[] data); }