/*******************************************************************************
* 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.AnonNode;
import staticContent.framework.controller.Layer4TransportMixController;
import staticContent.framework.userDatabase.User;
public class BasicOutputStreamMix extends OutputStream implements Callable<BasicOutputStreamMix> {
private boolean isClosed = false;
private StreamAnonSocketMixImpl socket;
private User user;
private boolean sendImmediately;
private int timeToWaitForFurtherData; // in microseconds
public static ScheduledThreadPoolExecutor scheduler = new ScheduledThreadPoolExecutor(1); // TODO: shut down on exit in AnonNode
private ScheduledFuture<BasicOutputStreamMix> currentTimer;
private ByteBuffer payloadForNextMessage;
private Object synchronizer = new Object();
private ByteBuffer timerByteBufferReference; // used to check if ByteBuffer was already sent
private Layer4TransportMixController layer4controller;
public BasicOutputStreamMix(
AnonNode owner,
StreamAnonSocketMixImpl socket,
User user
) {
this.layer4controller = owner.getTransportLayerControllerMix();
this.user = user;
this.socket = socket;
this.timeToWaitForFurtherData = owner.TIME_TO_WAIT_FOR_FURTHER_DATA;
if (timeToWaitForFurtherData == 0)
sendImmediately = true;
}
@Override
public void close() throws IOException {
this.isClosed = true;
this.socket = 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 = getMTU() > toWrite.remaining() ? toWrite.remaining() : getMTU();
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(getMTU());
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) {
layer4controller.write(user, payload);
}
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);
}
}
/**
* returns the maximum number of bytes that can be transmitted in a single
* mix message
* @return
*/
public int getMTU() {
return layer4controller.getMaxSizeOfNextWrite();
}
@Override
public BasicOutputStreamMix call() {
synchronized (synchronizer) {
if (payloadForNextMessage != null && timerByteBufferReference == payloadForNextMessage) {
sendNow();
}
return this;
}
}
}