/******************************************************************************* * 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.socket.stream; import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.util.concurrent.Callable; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import staticContent.framework.interfaces.Layer4TransportClient; import staticContent.framework.socket.socketInterfaces.StreamAnonSocket; import staticContent.framework.util.Util; public class BasicOutputStreamClient extends OutputStream implements Callable<BasicOutputStreamClient> { private StreamAnonSocket socket; private Layer4TransportClient layer4; private boolean isClosed = false; private boolean sendImmediately; // TODO: wait for further data should actually be a layer 3 task (some plug-ins use their own buffering and mght be less efficient with this additional buffer) private int timeToWaitForFurtherData; // in microseconds public static ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1); // TODO: shut down on exit in AnonNode private ScheduledFuture<BasicOutputStreamClient> currentTimer; private ByteBuffer payloadForNextMessage; private Object synchronizer = new Object(); private ByteBuffer timerByteBufferReference; // used to check if ByteBuffer was already sent public BasicOutputStreamClient( StreamAnonSocket socket, Layer4TransportClient layer4 ) { this.socket = socket; this.layer4 = layer4; this.timeToWaitForFurtherData = socket.getOwner().TIME_TO_WAIT_FOR_FURTHER_DATA; if (timeToWaitForFurtherData == 0) sendImmediately = true; } @Override public void close() throws IOException { this.isClosed = true; this.socket = null; this.layer4 = null; } @Override public void write(byte[] b) throws IOException { ByteBuffer toWrite = ByteBuffer.wrap(b); write(toWrite); } @Override public void write(byte[] b, int off, int len) throws IOException { ByteBuffer toWrite = ByteBuffer.wrap(b, off, len); write(toWrite); } @Override public void write(int b) throws IOException { if (isClosed) throw new IOException("OutputStream closed!"); if (!socket.isConnected()) throw new IOException("not connected"); write(new byte[] {(byte)b}); } private void write(ByteBuffer toWrite) throws IOException { if (isClosed) throw new IOException("OutputStream closed"); if (!socket.isConnected()) throw new IOException("not connected"); if (sendImmediately) { while (toWrite.hasRemaining()) { int length = getMaxSizeForNextMessageSend() > toWrite.remaining() ? toWrite.remaining() : getMaxSizeForNextMessageSend(); byte[] payload = new byte[length]; toWrite.get(payload); sendMessage(payload); } } else { synchronized (synchronizer) { while (toWrite.hasRemaining()) { // send as many full packets as possible; store additional data (that doesn't fill a complete packet) for later sending if (payloadForNextMessage == null) payloadForNextMessage = ByteBuffer.allocate(getMaxSizeForNextMessageSend()); if (toWrite.remaining() >= payloadForNextMessage.remaining()) { // send new message now if (payloadForNextMessage.position() == 0) { // empty -> send directly (without "payloadForNextMessage"-ByteBuffer) byte[] payload = new byte[payloadForNextMessage.capacity()]; toWrite.get(payload); sendMessage(payload); } else { // send together with data already stored if (currentTimer != null && !currentTimer.isDone()) { // early timer cancel (not really needed, but prevents unnecessary TimerTask executions) currentTimer.cancel(false); } byte[] newData = new byte[payloadForNextMessage.remaining()]; toWrite.get(newData); payloadForNextMessage.put(newData); sendMessage(payloadForNextMessage.array()); payloadForNextMessage = null; } } else { // store data for later sending payloadForNextMessage.put(toWrite); } } // set timeout if needed if (payloadForNextMessage != null && payloadForNextMessage.position() != 0 && (currentTimer == null || currentTimer.isDone())) { timerByteBufferReference = payloadForNextMessage; currentTimer = scheduler.schedule(this, timeToWaitForFurtherData, TimeUnit.MICROSECONDS); } } } } @Override public void flush() throws IOException { if (sendImmediately) { // don't do anything; data is always sent directly } else { sendNow(); } } private void sendMessage(byte[] payload) { payload = Util.concatArrays(Util.shortToByteArray(socket.getDestinationPort()), payload); // add destination port (= which layer 5 service/ServerSocket shall be addressed) if (!socket.getOwner().LAYER_1_LINKS_MESSAGES) // add a pseudonym for the (final) receiver, so it can link the messages of this sender/socket payload = Util.concatArrays(Util.intToByteArray(socket.getEndToEndPseudonym()), payload); //Request request = MixMessage.getInstanceRequest(payload); //request.destinationPseudonym = socket.getDestinationPseudonym(); layer4.write(payload, socket.getDestinationPseudonym()); } private void sendNow() { synchronized (synchronizer) { if (payloadForNextMessage == null || payloadForNextMessage.position() == 0) return; if (currentTimer != null && !currentTimer.isDone()) currentTimer.cancel(false); byte[] payload = new byte[payloadForNextMessage.position()]; payloadForNextMessage.get(payload); payloadForNextMessage = null; sendMessage(payload); } } public int getMaxSizeForNextMessageSend() { int maxSize = layer4.getMaxSizeOfNextWrite() - 2; // -2 for port; see sendMessage() if (!socket.getOwner().LAYER_1_LINKS_MESSAGES) // -4 for pseudonym; see sendMessage() maxSize -= 4; return maxSize; } @Override public BasicOutputStreamClient call() { synchronized (synchronizer) { if (payloadForNextMessage != null && timerByteBufferReference == payloadForNextMessage) { sendNow(); } return this; } } }