/******************************************************************************* * gMix open source project - https://svs.informatik.uni-hamburg.de/gmix/ * Copyright (C) 2014 SVS * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. *******************************************************************************/ package staticContent.framework.util; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; /** * Data structure that can be used to send/receive messages via network without * having to deal with message lengths. The addChunk(byte[]) method can be used * on the receiver side to add message chunks received via network. The * received parts will be recombined to the same message as intended by the * sender. * Will create an Overhead of 4 bytes (length header). */ public class FragmentedMessage { private ByteBuffer headerCache = ByteBuffer.allocate(4); // length header private ByteBuffer messageCache = null; private final static String BAD_FRAGMENT_MESSAGE = "bad fragment. some possible reasons: " + "\nthe method toSendableMessage(message) was not called on the" + "sender side" + "\nmessage bytes were changed during transfer (faulty " + "implementation of underying communication channel or none " + "reliable in-order channel)" + "\nthe bypassed byte array does not belong to this message"; /** * Use on receiver side only. On sender side, use the static method * toSendableMessage(message). */ public FragmentedMessage() { } /** * Use this method on the sender side to prepare am message for sending via * network. * @return */ public static byte[] toSendableMessage(byte[] message) { assert message != null; assert message.length != 0; // add length header: return Util.concatArrays(Util.intToByteArray(message.length), message); } /** * Use this method on receiver side. reads a (possibly) fragmented message * from the given InputStream. blocks until the message is fully received * or an IOException occurs. * if you don't want a blocking method, create a FragmentedMessage * instance and add fragments (addFragment(fragment)) until the message is * fully received (check with method isFullyReceived()). * @param inputStream * @return the message read or null if EOF */ public static byte[] forceReadMessage(InputStream inputStream) throws IOException { int length = Util.forceReadInt(inputStream); if (length < 0) throw new RuntimeException(BAD_FRAGMENT_MESSAGE); return Util.forceRead(inputStream, length); } /** * Use this method on the sender side to send a fragmentable message via * the bypassed OutputStream. Blocks until the message is fully transmitted * to the OutputStream. * Do NOT use toSendableMessage(message) on the bypassed byte array before * sending. * Does NOT call flush() after writing. * * @param inputStream * @return */ public static void writeMessage(OutputStream outputStream, byte[] message) throws IOException { if (message == null) throw new RuntimeException("bypassed message == null"); if (outputStream == null) throw new RuntimeException("bypassed outputStream == null"); if (message.length == 0) throw new RuntimeException("bypassed message is of length 0. can't send it via network"); outputStream.write(Util.intToByteArray(message.length)); outputStream.write(message); } /** * Use this method on receiver side to add fragments until the message is * received completely (see isFullyReceived()). * @param fragment the message fragment received via network * @return bytes of the bypassed array that were not needed to create the * FragmentedMessage (may belong to the next fragment), or null if all * bytes were added to this FragmentedMessage. */ public byte[] addFragment(byte[] fragment) { if (headerCache != null) { // header not yet read (completely) if (headerCache.hasRemaining()) { if (fragment.length < headerCache.remaining()) { // not enough data to read header completely headerCache.put(fragment); return null; } else if (fragment.length == headerCache.remaining()) { // exactly enough data to read header completely headerCache.put(fragment); int length = Util.byteArrayToInt(headerCache.array()); headerCache = null; if (length < 0) throw new RuntimeException(BAD_FRAGMENT_MESSAGE); messageCache = ByteBuffer.allocate(length); return null; } else if (fragment.length > headerCache.remaining()) { // more data available than needed to read header completely byte[][] splitted = Util.split(headerCache.remaining(), fragment); headerCache.put(splitted[0]); fragment = splitted[1]; // will be used by the code below this if statement int length = Util.byteArrayToInt(headerCache.array()); headerCache = null; if (length < 0) throw new RuntimeException(BAD_FRAGMENT_MESSAGE); messageCache = ByteBuffer.allocate(length); } } } // Note: at this point we know that the header is read completely and that "fragment" contains data (see code above) // try to read rest of message: if (fragment.length <= messageCache.remaining()) { // the whole fragment belongs to this message (not enough or exactly enough data to read message completely) messageCache.put(fragment); return null; } else { // only a fraction of the fragment belongs to this message (more data available than needed to read message completely) byte[][] splitted = Util.split(messageCache.remaining(), fragment); messageCache.put(splitted[0]); return splitted[1]; // return not needed bytes (may belong to next fragmented message) } } public boolean isFullyReceived() { if (messageCache != null && messageCache.remaining() == 0) return true; else return false; } public byte[] getRawMessage() { if (!isFullyReceived()) throw new RuntimeException("the message is not yet fully received! check the result of isFullyReceived() before calling getRawMessage()"); return messageCache.array(); } }