/* Copyright 2013 RobustNet Lab, University of Michigan. All Rights Reserved.
*
* 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 com.mobiperf.measurements;
import com.mobiperf.Config;
import com.mobiperf.Logger;
import com.mobiperf.MeasurementDesc;
import com.mobiperf.MeasurementError;
import com.mobiperf.MeasurementResult;
import com.mobiperf.MeasurementTask;
import com.mobiperf.util.MLabNS;
import com.mobiperf.util.PhoneUtils;
import android.content.Context;
import java.io.*;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.security.InvalidParameterException;
import java.util.ArrayList;
import java.util.Date;
import java.util.Map;
/**
*
* UDPBurstTask provides two types of measurements, Burst Up and Burst Down,
* described next.
*
* 1. UDPBurst Up: the device sends sends a burst of UDPBurstCount UDP packets
* and waits for a response from the server that includes the number of packets
* that the server received
*
* 2. UDPBurst Down: the device sends a request to a remote server on a UDP port
* and the server responds by sending a burst of UDPBurstCount packets. The size
* of each packet is packetSizeByte
*/
public class UDPBurstTask extends MeasurementTask {
public static final String TYPE = "udp_burst";
public static final String DESCRIPTOR = "UDP Burst";
private static final int DEFAULT_PORT = 31341;
/**
* Min packet size = (int type) + (int burstCount) + (int packetNum) +
* (int intervalNum) + (long timestamp) +
* (int packetSize) + (int seq) + (int udpInterval)
* = 36
*/
private static final int MIN_PACKETSIZE = 36;
// Leave enough margin for min MTU in the link and IP options
private static final int MAX_PACKETSIZE = 500;
private static final int DEFAULT_UDP_PACKET_SIZE = 100;
/**
* Default number of packets to be sent
*/
private static final int DEFAULT_UDP_BURST = 16;
private static final int MAX_BURSTCOUNT = 100;
/**
* TODO(Hongyi): Interval between packets in millisecond level seems too long
* for regular UDP transmission. Microsecond level may be better.
* Need Discussion
*/
private static final int DEFAULT_UDP_INTERVAL = 1;
private static final int MAX_INTERVAL = 1;
// TODO(Hongyi): choose a proper timeout period
private static final int RCV_UP_TIMEOUT = 2000; // round-trip delay, in msec.
private static final int RCV_DOWN_TIMEOUT = 1000; // one-way delay, in msec
private static final int PKT_ERROR = 1;
private static final int PKT_RESPONSE = 2;
private static final int PKT_DATA = 3;
private static final int PKT_REQUEST = 4;
private String targetIp = null;
private Context context = null;
private static int seq = 1;
// Track data consumption for this task to avoid exceeding user's limit
private long dataConsumed;
/**
* Encode UDP specific parameters, along with common parameters inherited
* from MeasurementDesc
*
*/
public static class UDPBurstDesc extends MeasurementDesc {
// Declare static parameters specific to SampleMeasurement here
public int packetSizeByte = UDPBurstTask.DEFAULT_UDP_PACKET_SIZE;
public int udpBurstCount = UDPBurstTask.DEFAULT_UDP_BURST;
public int dstPort = UDPBurstTask.DEFAULT_PORT;
public String target = null;
public boolean dirUp = false;
public int udpInterval = UDPBurstTask.DEFAULT_UDP_INTERVAL;
public UDPBurstDesc(String key, Date startTime, Date endTime,
double intervalSec, long count, long priority,
Map<String, String> params)
throws InvalidParameterException {
super(UDPBurstTask.TYPE, key, startTime, endTime, intervalSec,
count, priority, params);
initializeParams(params);
if (this.target == null || this.target.length() == 0) {
throw new InvalidParameterException("UDPBurstTask null target");
}
}
/**
* There are three UDP specific parameters:
*
* 1. "direction": "up" if this is an uplink measurement. or "down"
* otherwise 2. "packet_burst": how many packets should a up/down burst
* have 3. "packet_size_byte": the size of each packet in bytes
*/
@Override
protected void initializeParams(Map<String, String> params) {
if (params == null) {
return;
}
this.target = params.get("target");
try {
String val = null;
if ((val = params.get("dst_port")) != null && val.length() > 0
&& Integer.parseInt(val) > 0) {
this.dstPort = Integer.parseInt(val);
}
if ((val = params.get("packet_size_byte")) != null
&& val.length() > 0 && Integer.parseInt(val) > 0) {
this.packetSizeByte = Integer.parseInt(val);
if (this.packetSizeByte < MIN_PACKETSIZE) {
this.packetSizeByte = MIN_PACKETSIZE;
}
if (this.packetSizeByte > MAX_PACKETSIZE) {
this.packetSizeByte = MAX_PACKETSIZE;
}
}
if ((val = params.get("packet_burst")) != null
&& val.length() > 0 && Integer.parseInt(val) > 0) {
this.udpBurstCount = Integer.parseInt(val);
if ( this.udpBurstCount > MAX_BURSTCOUNT ) {
this.udpBurstCount = MAX_BURSTCOUNT;
}
}
if ((val = params.get("udp_interval")) != null
&& val.length() > 0 && Integer.parseInt(val) >= 0) {
this.udpInterval = Integer.parseInt(val);
if ( this.udpInterval > MAX_INTERVAL ) {
this.udpInterval = MAX_INTERVAL;
}
}
} catch (NumberFormatException e) {
throw new InvalidParameterException("UDPTask invalid params");
}
String dir = null;
if ((dir = params.get("direction")) != null && dir.length() > 0) {
if (dir.compareToIgnoreCase("Up") == 0) {
this.dirUp = true;
}
}
}
@Override
public String getType() {
return UDPBurstTask.TYPE;
}
}
@SuppressWarnings("rawtypes")
public static Class getDescClass() throws InvalidClassException {
return UDPBurstDesc.class;
}
public UDPBurstTask(MeasurementDesc desc, Context context) {
super(new UDPBurstDesc(desc.key, desc.startTime, desc.endTime,
desc.intervalSec, desc.count, desc.priority, desc.parameters), context);
this.context = context;
dataConsumed = 0;
}
/**
* Make a deep cloning of the task
*/
@Override
public MeasurementTask clone() {
MeasurementDesc desc = this.measurementDesc;
UDPBurstDesc newDesc = new UDPBurstDesc(desc.key, desc.startTime,
desc.endTime, desc.intervalSec, desc.count, desc.priority,
desc.parameters);
return new UDPBurstTask(newDesc, context);
}
/**
* Opens a datagram (UDP) socket
*
* @return a datagram socket used for sending/receiving
* @throws MeasurementError
* if an error occurs
*/
private DatagramSocket openSocket() throws MeasurementError {
DatagramSocket sock = null;
// Open datagram socket
try {
sock = new DatagramSocket();
} catch (SocketException e) {
throw new MeasurementError("Socket creation failed");
}
return sock;
}
/**
* @author Hongyi Yao (hyyao@umich.edu)
* This class encapsulates the results of UDP burst measurement
*/
private class UDPResult {
public int packetCount;
public double outOfOrderRatio;
public long jitter;
public UDPResult () {
packetCount = 0;
outOfOrderRatio = 0.0;
jitter = 0L;
}
}
/**
* @author Hongyi Yao (hyyao@umich.edu)
* This class calculates the out-of-order ratio and delay jitter
* in the array of received UDP packets
*/
private class MetricCalculator {
private int maxPacketNum;
private ArrayList<Long> offsetedDelayList;
private int packetCount;
private int outOfOrderCount;
public MetricCalculator(int burstSize) {
maxPacketNum = -1;
offsetedDelayList = new ArrayList<Long>();
packetCount = 0;
outOfOrderCount = 0;
}
/**
* Out-of-order packets is defined as arriving packets with sequence numbers
* smaller than their predecessors.
* @param packetNum: packet number in burst sequence
* @param timestamp: estimated one-way delay(contains clock offset)
*/
public void addPacket(int packetNum, long timestamp) {
if ( packetNum > maxPacketNum ) {
maxPacketNum = packetNum;
}
else {
outOfOrderCount++;
}
offsetedDelayList.add(System.currentTimeMillis() - timestamp);
packetCount++;
}
/**
* Out-of-order ratio is defined as the ratio between the number of
* out-of-order packets and the total number of packets.
* @return the inversion number of the current UDP burst
*/
public double calculateOutOfOrderRatio() {
if (packetCount != 0) {
return (double) outOfOrderCount / packetCount;
} else {
return 0.0;
}
}
/**
* Calculate jitter as the standard deviation of one-way delays[RFC3393]
* We can assume the clock offset between server and client is constant in
* a short period(several milliseconds) since typical oscillators have no
* more than 100ppm of frequency error , then it will be cancelled out
* during the calculation process
* @return the jitter of UDP burst
*/
public long calculateJitter() {
if ( packetCount > 1 ) {
double offsetedDelay_mean = 0;
for ( long offsetedDelay : offsetedDelayList ) {
offsetedDelay_mean += (double)offsetedDelay / packetCount;
}
double jitter = 0;
for ( long offsetedDelay : offsetedDelayList ) {
jitter += ((double)offsetedDelay - offsetedDelay_mean)
* ((double)offsetedDelay - offsetedDelay_mean) / (packetCount - 1);
}
jitter = Math.sqrt(jitter);
return (long)jitter;
}
else {
return 0;
}
}
}
/**
* @author Hongyi Yao (hyyao@umich.edu)
* A helper structure for packing and unpacking network message
*/
private class UDPPacket {
public int type;
public int burstCount;
public int packetNum;
public int outOfOrderNum;
// Data packet: local timestamp
// Response packet: jitter
public long timestamp;
public int packetSize;
public int seq;
public int udpInterval;
/**
* Create an empty structure
* @param cliId corresponding client identifier
*/
public UDPPacket() {}
/**
* Unpack received message and fill the structure
* @param cliId corresponding client identifier
* @param rawdata network message
* @throws MeasurementError stream reader failed
*/
public UDPPacket(byte[] rawdata)
throws MeasurementError{
ByteArrayInputStream byteIn = new ByteArrayInputStream(rawdata);
DataInputStream dataIn = new DataInputStream(byteIn);
try {
type = dataIn.readInt();
burstCount = dataIn.readInt();
packetNum = dataIn.readInt();
outOfOrderNum = dataIn.readInt();
timestamp = dataIn.readLong();
packetSize = dataIn.readInt();
seq = dataIn.readInt();
udpInterval = dataIn.readInt();
} catch (IOException e) {
throw new MeasurementError("Fetch payload failed! " + e.getMessage());
}
try {
byteIn.close();
} catch (IOException e) {
throw new MeasurementError("Error closing inputstream!");
}
}
/**
* Pack the structure to the network message
* @return the network message in byte[]
* @throws MeasurementError stream writer failed
*/
public byte[] getByteArray() throws MeasurementError {
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
DataOutputStream dataOut = new DataOutputStream(byteOut);
try {
dataOut.writeInt(type);
dataOut.writeInt(burstCount);
dataOut.writeInt(packetNum);
dataOut.writeInt(outOfOrderNum);
dataOut.writeLong(timestamp);
dataOut.writeInt(packetSize);
dataOut.writeInt(seq);
dataOut.writeInt(udpInterval);
} catch (IOException e) {
throw new MeasurementError("Create rawpacket failed! " + e.getMessage());
}
byte[] rawPacket = byteOut.toByteArray();
try {
byteOut.close();
} catch (IOException e) {
throw new MeasurementError("Error closing outputstream!");
}
return rawPacket;
}
}
/**
* Opens a Datagram socket to the server included in the UDPDesc and sends a
* burst of UDPBurstCount packets, each of size packetSizeByte.
*
* @return a Datagram socket that can be used to receive the server's
* response
*
* @throws MeasurementError
* if an error occurred.
*/
private DatagramSocket sendUpBurst() throws MeasurementError {
UDPBurstDesc desc = (UDPBurstDesc) measurementDesc;
InetAddress addr = null;
// Resolve the server's name
try {
addr = InetAddress.getByName(desc.target);
targetIp = addr.getHostAddress();
} catch (UnknownHostException e) {
throw new MeasurementError("Unknown host " + desc.target);
}
DatagramSocket sock = null;
sock = openSocket();
UDPPacket dataPacket = new UDPPacket();
// Send burst
for (int i = 0; i < desc.udpBurstCount; i++) {
dataPacket.type = UDPBurstTask.PKT_DATA;
dataPacket.burstCount = desc.udpBurstCount;
dataPacket.packetNum = i;
dataPacket.timestamp = System.currentTimeMillis();
dataPacket.packetSize = desc.packetSizeByte;
dataPacket.seq = seq;
// Flatten UDP packet
byte[] data = dataPacket.getByteArray();
DatagramPacket packet = new DatagramPacket(data, data.length, addr,
desc.dstPort);
try {
sock.send(packet);
dataConsumed += packet.getLength();
} catch (IOException e) {
sock.close();
throw new MeasurementError("Error sending " + desc.target);
}
Logger.i("Sent packet pnum:" + i + " to " + desc.target + ": "
+ targetIp);
// Update progress bar, leave the last grid for receiving response
this.progress = 100 * i / (desc.udpBurstCount + 1);
this.progress = Math.min(Config.MAX_PROGRESS_BAR_VALUE, progress);
broadcastProgressForUser(this.progress);
// Sleep udpInterval millisecond
try {
Thread.sleep(desc.udpInterval);
Logger.i("UDP Burst sleep " + desc.udpInterval + "ms");
} catch (InterruptedException e) {
Logger.e("Error: sleep interrupted!");
}
} // for()
return sock;
}
/**
* Receive a response from the server after the burst of uplink packets was
* sent, parse it, and return the number of packets the server received.
*
* @param sock
* the socket used to receive the server's response
* @return the number of packets the server received
*
* @throws MeasurementError
* if an error or a timeout occurs
*/
private UDPResult recvUpResponse(DatagramSocket sock)
throws MeasurementError {
UDPBurstDesc desc = (UDPBurstDesc) measurementDesc;
UDPResult udpResult = new UDPResult();
// Receive response
Logger.i("Waiting for UDP response from " + desc.target + ": "
+ targetIp);
byte buffer[] = new byte[UDPBurstTask.MIN_PACKETSIZE];
DatagramPacket recvPacket = new DatagramPacket(buffer, buffer.length);
try {
sock.setSoTimeout(RCV_UP_TIMEOUT);
sock.receive(recvPacket);
} catch (SocketException e1) {
sock.close();
throw new MeasurementError("Timed out reading from " + desc.target);
} catch (IOException e) {
sock.close();
throw new MeasurementError("Error reading from " + desc.target);
}
// Reconstruct UDP packet from flattened network data
UDPPacket responsePacket = new UDPPacket(recvPacket.getData());
if ( responsePacket.type == PKT_RESPONSE ) {
// Received seq number must be same with client seq
if ( responsePacket.seq != seq ) {
Logger.e("Error: Server send response packet with different seq, old "
+ seq + " => new " + responsePacket.seq);
}
Logger.i("Recv UDP resp from " + desc.target + " type:"
+ responsePacket.type + " burst:" + responsePacket.burstCount
+ " pktnum:" + responsePacket.packetNum + " out_of_order_num: "
+ responsePacket.outOfOrderNum + " jitter: " + responsePacket.timestamp);
// Update the last grid in progress bar
this.progress = Config.MAX_PROGRESS_BAR_VALUE;
broadcastProgressForUser(this.progress);
udpResult.packetCount = responsePacket.packetNum;
udpResult.outOfOrderRatio =
(double)responsePacket.outOfOrderNum / responsePacket.packetNum;
udpResult.jitter = responsePacket.timestamp;
return udpResult;
}
else {
throw new MeasurementError("Error: not a response packet! seq: " + seq);
}
}
/**
* Opens a datagram socket to the server in the UDPDesc and requests the
* server to send a burst of UDPBurstCount packets, each of packetSizeByte
* bytes.
*
* @return the datagram socket used to receive the server's burst
* @throws MeasurementError
* if an error occurs
*/
private DatagramSocket sendDownRequest() throws MeasurementError {
UDPBurstDesc desc = (UDPBurstDesc) measurementDesc;
DatagramPacket packet;
InetAddress addr = null;
// Resolve the server's name
try {
addr = InetAddress.getByName(desc.target);
targetIp = addr.getHostAddress();
} catch (UnknownHostException e) {
throw new MeasurementError("Unknown host " + desc.target);
}
DatagramSocket sock = null;
sock = openSocket();
Logger.i("Requesting UDP burst:" + desc.udpBurstCount + " pktsize: "
+ desc.packetSizeByte + " to " + desc.target + ": " + targetIp);
UDPPacket requestPacket = new UDPPacket();
requestPacket.type = PKT_REQUEST;
requestPacket.burstCount = desc.udpBurstCount;
requestPacket.packetSize = desc.packetSizeByte;
requestPacket.seq = seq;
requestPacket.udpInterval = desc.udpInterval;
// Flatten UDP packet
byte[] data = requestPacket.getByteArray();
packet = new DatagramPacket(data, data.length, addr, desc.dstPort);
try {
sock.send(packet);
dataConsumed += packet.getLength();
} catch (IOException e) {
sock.close();
throw new MeasurementError("Error sending " + desc.target);
}
// Update the first grid of progress bar for sending request
this.progress = 100 * 1 / (desc.udpBurstCount + 1);
this.progress = Math.min(Config.MAX_PROGRESS_BAR_VALUE, progress);
broadcastProgressForUser(this.progress);
return sock;
}
/**
* Receives a burst from the remote server and counts the number of packets
* that were received.
*
* @param sock
* the datagram socket that can be used to receive the server's
* burst
*
* @return the number of packets received from the server
* @throws MeasurementError
* if an error occurs
*/
private UDPResult recvDownResponse(DatagramSocket sock)
throws MeasurementError {
int pktRecv = 0;
UDPBurstDesc desc = (UDPBurstDesc) measurementDesc;
// Reconstruct UDP packet from flattened network data
byte buffer[] = new byte[desc.packetSizeByte];
DatagramPacket recvPacket = new DatagramPacket(buffer, buffer.length);
MetricCalculator metricCalculator = new MetricCalculator(
desc.udpBurstCount);
for (int i = 0; i < desc.udpBurstCount; i++) {
try {
sock.setSoTimeout(RCV_DOWN_TIMEOUT);
sock.receive(recvPacket);
} catch (IOException e) {
break;
}
dataConsumed += recvPacket.getLength();
UDPPacket dataPacket = new UDPPacket(recvPacket.getData());
if (dataPacket.type == UDPBurstTask.PKT_DATA) {
// Received seq number must be same with client seq
if ( dataPacket.seq != seq ) {
Logger.e("Error: Server send data packets with different seq, old "
+ seq + " => new " + dataPacket.seq);
break;
}
Logger.i("Recv UDP response from " + desc.target + " type:"
+ dataPacket.type + " burst:" + dataPacket.burstCount + " pktnum:"
+ dataPacket.packetNum + " timestamp:" + dataPacket.timestamp);
// Update progress bar, the first grid is taken by client request
this.progress = 100 * (i + 1) / (desc.udpBurstCount + 1);
this.progress = Math.min(Config.MAX_PROGRESS_BAR_VALUE, progress);
broadcastProgressForUser(this.progress);
pktRecv++;
metricCalculator.addPacket(dataPacket.packetNum, dataPacket.timestamp);
}
else {
throw new MeasurementError("Error: not a data packet! seq: " + seq);
}
} // for()
UDPResult udpResult = new UDPResult();
udpResult.packetCount = pktRecv;
udpResult.outOfOrderRatio = metricCalculator.calculateOutOfOrderRatio();
udpResult.jitter = metricCalculator.calculateJitter();
return udpResult;
}
/**
* Depending on the type of measurement, indicated by desc.Up, perform an
* uplink/downlink measurement
*
* @return the measurement's results
* @throws MeasurementError
* if an error occurs
*/
@Override
public MeasurementResult call() throws MeasurementError {
DatagramSocket socket = null;
float response = 0.0F;
UDPResult udpResult;
int pktRecv = 0;
boolean isMeasurementSuccessful = false;
UDPBurstDesc desc = (UDPBurstDesc) measurementDesc;
if (!desc.target.equals(MLabNS.TARGET)) {
throw new InvalidParameterException("Unknown target " + desc.target +
" for UDPBurstTask");
}
ArrayList<String> mlabNSResult = MLabNS.Lookup(context,
"mobiperf");
if (mlabNSResult.size() == 1) {
desc.target = mlabNSResult.get(0);
} else {
throw new InvalidParameterException("Invalid MLabNS query result"
+
" for UDPBurstTask");
}
Logger.i("Setting target to: " + desc.target);
PhoneUtils phoneUtils = PhoneUtils.getPhoneUtils();
Logger.i("Running UDPBurstTask on " + desc.target);
try {
if (desc.dirUp == true) {
socket = sendUpBurst();
udpResult = recvUpResponse(socket);
pktRecv = udpResult.packetCount;
response = pktRecv / (float) desc.udpBurstCount;
isMeasurementSuccessful = true;
} else {
socket = sendDownRequest();
udpResult = recvDownResponse(socket);
pktRecv = udpResult.packetCount;
response = pktRecv / (float) desc.udpBurstCount;
isMeasurementSuccessful = true;
}
} catch (MeasurementError e) {
throw e;
} finally {
// Update the sequence number to be used by the next burst.
// Hongyi: we should update seq number no matter the measurement is
// succeeded or not. It ensures previous last UDP burst's packets
// will not affect the current one.
seq++;
socket.close();
}
MeasurementResult result = new MeasurementResult(
phoneUtils.getDeviceInfo().deviceId,
phoneUtils.getDeviceProperty(), UDPBurstTask.TYPE,
System.currentTimeMillis() * 1000, isMeasurementSuccessful,
this.measurementDesc);
result.addResult("target_ip", targetIp);
result.addResult("loss_ratio", 1.0 - response);
result.addResult("out_of_order_ratio", udpResult.outOfOrderRatio);
result.addResult("jitter", udpResult.jitter);
return result;
}
@Override
public String getType() {
return UDPBurstTask.TYPE;
}
/**
* Returns a brief human-readable descriptor of the task.
*/
@Override
public String getDescriptor() {
return UDPBurstTask.DESCRIPTOR;
}
private void cleanUp() {
// Do nothing
}
/**
* Stop the measurement, even when it is running. Should release all
* acquired resource in this function. There should not be side effect if
* the measurement has not started or is already finished.
*/
@Override
public void stop() {
cleanUp();
}
/**
* This will be printed to the device log console. Make sure it's well
* structured and human readable
*/
@Override
public String toString() {
UDPBurstDesc desc = (UDPBurstDesc) measurementDesc;
String resp;
if (desc.dirUp) {
resp = "[UDPUp]\n";
} else {
resp = "[UDPDown]\n";
}
resp += " Target: " + desc.target + "\n Interval (sec): "
+ desc.intervalSec + "\n Next run: " + desc.startTime;
return resp;
}
/**
* Based on a direct accounting of UDP packet sizes.
*/
@Override
public long getDataConsumed() {
return dataConsumed;
}
}