/*******************************************************************************
* Copyright 2015 alladin-IT GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*******************************************************************************/
package at.alladin.rmbt.util.net.udp;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.DatagramChannel;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* udp stream sender used by the udp and voip qos test
* @author lb
*
*/
public class NioUdpStreamSender implements StreamSender<DatagramChannel> {
UdpStreamSenderSettings<DatagramChannel> settings;
UdpStreamCallback callback;
final AtomicBoolean isRunning = new AtomicBoolean(false);
public NioUdpStreamSender(UdpStreamSenderSettings<DatagramChannel> settings, UdpStreamCallback udpStreamCallback) {
this.settings = settings;
this.callback = udpStreamCallback;
}
public void stop() {
isRunning.set(false);
}
/**
* send a stream of udp packets
* @return the {@link DatagramChannel} used for this stream or null if an exception occurred
* @throws InterruptedException
* @throws IOException
* @throws TimeoutException
*/
public DatagramChannel send() throws InterruptedException, TimeoutException {
isRunning.set(true);
int packetsSent = 0;
int packetsRcv = 0;
final ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
final DataOutputStream dataOut = new DataOutputStream(byteOut);
final ByteBuffer buffer = ByteBuffer.allocate(1024);
final SocketAddress targetAddress = new InetSocketAddress(settings.targetHost, settings.getTargetPort());
final long delayMs = TimeUnit.MILLISECONDS.convert(settings.delay, settings.timeUnit);
long lastSendTimestamp = 0;
final long startTimeMs = System.currentTimeMillis();
final long timeoutMs = TimeUnit.MILLISECONDS.convert(settings.timeout, settings.timeUnit);
final long stopTimeMs = timeoutMs > 0 ? timeoutMs + startTimeMs : 0;
DatagramChannel channel = null;
Selector selector = null;
try {
if (settings.socket == null) {
channel = DatagramChannel.open();
channel.configureBlocking(false);
if (settings.incomingPort != null) {
channel.socket().bind(new InetSocketAddress(settings.incomingPort));
if (callback != null) {
callback.onBind(channel.socket().getLocalPort());
}
}
else {
channel.socket().bind(null);
}
if (callback != null) {
callback.onBind(channel.socket().getLocalPort());
}
}
else {
channel = settings.socket;
}
selector = Selector.open();
if (settings.writeOnly) {
channel.register(selector, SelectionKey.OP_WRITE);
}
else {
channel.register(selector, SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
while(isRunning.get()) {
if (Thread.interrupted()) {
isRunning.set(false);
throw new InterruptedException();
}
if (stopTimeMs > 0 && stopTimeMs < System.currentTimeMillis()) {
isRunning.set(false);
throw new TimeoutException("Exceeded timeout of " + timeoutMs + "ms");
}
//calculate correct packet delay
long currentDelay = System.currentTimeMillis() - lastSendTimestamp;
currentDelay = currentDelay > delayMs ? 0 : delayMs - currentDelay;
if (currentDelay > 0) {
Thread.sleep(currentDelay);
}
selector.select(1000);
Set<SelectionKey> readyKeys = selector.selectedKeys();
if (!readyKeys.isEmpty()) {
Iterator<SelectionKey> iterator = readyKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = (SelectionKey) iterator.next();
iterator.remove();
if (key.isReadable() && (packetsRcv < settings.packets) && key.isValid()) {
buffer.clear();
channel.receive(buffer);
buffer.flip();
final DatagramPacket dp = new DatagramPacket(buffer.array(), buffer.array().length);
if (callback != null) {
callback.onReceive(dp);
}
packetsRcv++;
}
if (key.isWritable() && (packetsSent < settings.packets) && key.isValid()) {
byteOut.reset();
buffer.clear();
try {
if (callback != null) {
if (callback.onSend(dataOut, packetsSent)) {
final byte[] data = byteOut.toByteArray();
buffer.put(data);
buffer.flip();
channel.send(buffer, targetAddress);
packetsSent++;
lastSendTimestamp = System.currentTimeMillis();
}
}
} catch (IOException e) {
e.printStackTrace();
return null;
}
}
}
}
if (!settings.writeOnly) {
if ((packetsSent >= settings.packets) && (packetsRcv >= settings.packets)) {
isRunning.set(false);
}
}
else {
if ((packetsSent >= settings.packets)) {
isRunning.set(false);
}
}
}
return channel;
}
catch (IOException e) {
e.printStackTrace();
}
finally {
if (selector != null && selector.isOpen()) {
try {
selector.close();
} catch (Exception e) {
e.printStackTrace();
}
}
if (channel != null && channel.socket() != null && !channel.socket().isClosed() && settings.closeOnFinish) {
try {
channel.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
return null;
}
}