/* * 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.mobilyzer.measurements; import android.content.Context; import android.os.Parcel; import android.os.Parcelable; 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; import com.mobilyzer.Config; import com.mobilyzer.MeasurementDesc; import com.mobilyzer.MeasurementResult; import com.mobilyzer.MeasurementTask; import com.mobilyzer.MeasurementResult.TaskProgress; import com.mobilyzer.exceptions.MeasurementError; import com.mobilyzer.util.Logger; import com.mobilyzer.util.MLabNS; import com.mobilyzer.util.PhoneUtils; /** * * 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; private long duration; private TaskProgress taskProgress; private volatile boolean stopFlag; // 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, int contextIntervalSec, Map<String, String> params) throws InvalidParameterException { super(UDPBurstTask.TYPE, key, startTime, endTime, intervalSec, count, priority, contextIntervalSec, 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; } if ((target = params.get("target")) == null) { this.target = MLabNS.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; } protected UDPBurstDesc(Parcel in) { super(in); packetSizeByte = in.readInt(); udpBurstCount = in.readInt(); dstPort = in.readInt(); target = in.readString(); dirUp = in.readByte() != 0; udpInterval = in.readInt(); } public static final Parcelable.Creator<UDPBurstDesc> CREATOR = new Parcelable.Creator<UDPBurstDesc>() { public UDPBurstDesc createFromParcel(Parcel in) { return new UDPBurstDesc(in); } public UDPBurstDesc[] newArray(int size) { return new UDPBurstDesc[size]; } }; @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeInt(packetSizeByte); dest.writeInt(udpBurstCount); dest.writeInt(dstPort); dest.writeString(target); dest.writeByte((byte) (dirUp ? 1 : 0)); dest.writeInt(udpInterval); } } @SuppressWarnings("rawtypes") public static Class getDescClass() throws InvalidClassException { return UDPBurstDesc.class; } public UDPBurstTask(MeasurementDesc desc) { super(new UDPBurstDesc(desc.key, desc.startTime, desc.endTime, desc.intervalSec, desc.count, desc.priority, desc.contextIntervalSec, desc.parameters)); this.taskProgress = TaskProgress.FAILED; this.stopFlag = false; this.duration = Config.DEFAULT_UDPBURST_DURATION; this.dataConsumed = 0; } protected UDPBurstTask(Parcel in) { super(in); taskProgress = (TaskProgress) in.readSerializable(); stopFlag = in.readByte() != 0; duration = in.readLong(); dataConsumed = in.readLong(); } public static final Parcelable.Creator<UDPBurstTask> CREATOR = new Parcelable.Creator<UDPBurstTask>() { public UDPBurstTask createFromParcel(Parcel in) { return new UDPBurstTask(in); } public UDPBurstTask[] newArray(int size) { return new UDPBurstTask[size]; } }; @Override public void writeToParcel(Parcel dest, int flags) { super.writeToParcel(dest, flags); dest.writeSerializable(taskProgress); dest.writeByte((byte) (stopFlag ? 1 : 0)); dest.writeLong(duration); dest.writeLong(dataConsumed); } /** * 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.contextIntervalSec, desc.parameters); return new UDPBurstTask(newDesc); } /** * 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); dataConsumed+=DnsLookupTask.AVG_DATA_USAGE_BYTE; 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++) { if (stopFlag) { throw new MeasurementError("Cancelled"); } 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); // Sleep udpInterval millisecond try { Thread.sleep(desc.udpInterval); Logger.i("UDP Burst sleep " + desc.udpInterval + "ms"); } catch (InterruptedException e) { Logger.e("UDPBurst -> sendUpBurst got interrupted"); return null; } } // 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); if (stopFlag) { throw new MeasurementError("Cancelled"); } 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()); dataConsumed+=recvpacket.getLength(); 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); 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; if (stopFlag) { throw new MeasurementError("Cancelled"); } // 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 { dataConsumed += packet.getLength(); sock.send(packet); } catch (IOException e) { sock.close(); throw new MeasurementError("Error closing Output Stream to:" + desc.target); } 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; // Receive response Logger.i("Waiting for UDP burst from " + desc.target); // 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++) { if (stopFlag) { throw new MeasurementError("Cancelled"); } try { sock.setSoTimeout(RCV_DOWN_TIMEOUT); sock.receive(recvpacket); } catch (IOException e) { Logger.e("Timeout at round " + i); break; } UDPPacket dataPacket = new UDPPacket(recvpacket.getData()); dataConsumed+=recvpacket.getLength(); if (dataPacket.type == UDPBurstTask.PKT_DATA) { // Received seq number must be same with client seq if (dataPacket.seq != seq) { String err = "Server send data packets with different seq, old " + seq + " => new " + dataPacket.seq; Logger.e(err); throw new MeasurementError(err); } Logger.i("Recv UDP response from " + desc.target + " type:" + dataPacket.type + " burst:" + dataPacket.burstCount + " pktnum:" + dataPacket.packetNum + " timestamp:" + dataPacket.timestamp); pktRecv++; metricCalculator.addPacket(dataPacket.packetNum, dataPacket.timestamp); } else { throw new MeasurementError("Error closing input stream from " + desc.target); } } // 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; this.taskProgress = TaskProgress.FAILED; 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); if (stopFlag) { throw new MeasurementError("Cancelled"); } pktrecv = udpResult.packetCount; response = pktrecv / (float) desc.udpBurstCount; this.taskProgress = TaskProgress.COMPLETED; } else { socket = sendDownRequest(); udpResult = recvDownResponse(socket); if (stopFlag) { throw new MeasurementError("Cancelled"); } pktrecv = udpResult.packetCount; response = pktrecv / (float) desc.udpBurstCount; this.taskProgress = TaskProgress.COMPLETED; } } 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++; if (socket != null) { socket.close(); } } MeasurementResult result = new MeasurementResult(phoneUtils.getDeviceInfo().deviceId, phoneUtils.getDeviceProperty(this.getKey()), UDPBurstTask.TYPE, System.currentTimeMillis() * 1000, this.taskProgress, 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); MeasurementResult[] mrArray = new MeasurementResult[1]; mrArray[0] = result; return mrArray; } @Override public String getType() { return UDPBurstTask.TYPE; } /** * Returns a brief human-readable descriptor of the task. */ @Override public String getDescriptor() { return UDPBurstTask.DESCRIPTOR; } /** * 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; } @Override public long getDuration() { return this.duration; } @Override public void setDuration(long newDuration) { if (newDuration < 0) { this.duration = 0; } else { this.duration = newDuration; } } /** * 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 boolean stop() { stopFlag = true; return true; } /** * Based on a direct accounting of UDP packet sizes. */ @Override public long getDataConsumed() { return dataConsumed; } }