5
\$\begingroup\$

My task was similar to my last assignment but this time I had to do it with UDP instead of TCP. This basically means I had to emulate TCP over UDP.

Multithreading was an interesting problem as I had to simulate TCP's 3-way handshake.

I decided to use encapsulation and break up the file I'm transferring into 512 byte size blocks, except for the last block which is more likely going to be smaller. I then create my message object with the correct segmentID and a bytesToWrite. The bytesToWrite is needed to handle the last block, there is a better way to do this. I just got lazy here. I'll fix that when I have time.

Every segment received is acknowledged, after a 2x second timeout if there is no ACK then the segment is resent. In the event an ACK fails to get through the receiver will get a segment ID that is exactly -1 to the expected ID. In this way I know an ACK has failed and I resend it.

This has been tested locally using the built in fail simulator (2% chance a packet fails) and tested in the wild by asking a friend set the server up in London, England and I downloaded a 14.4mb file to my machine in Galway, Ireland. It took a long time and had 35 failed packets, 19 of which were failed ACKs.

Both the Server and Client use the UDPFileReceiver and UDPFileSender classes.

Server

/*****
 * CT326 - Assignment 12 - [email protected] - 09101916
 */
package pkg12;

import java.io.*;
import java.net.*;

public class Server
{
    private static int clientID = 1;
    private static DatagramSocket serverSocket;

    public static void main(String[] args) throws IOException
    {
        System.out.println("Server started.");
        byte[] buffer = new byte[512];

        /**
         * ASSIGNMENT INSTRUCTION The server should be multi-threaded, and
         * have one thread per connection.
         */
        serverSocket = new DatagramSocket(8550);
        while (true)
        {
            try
            {
                DatagramPacket packet =  new DatagramPacket(buffer, buffer.length );
                serverSocket.receive(packet);
                System.out.println("SERVER: Accepted connection.");
                System.out.println("SERVER: received"+new String(packet.getData(), 0, packet.getLength()));

                //new socket created with random port for thread
                DatagramSocket threadSocket = new DatagramSocket();

                Thread t = new Thread(new CLIENTConnection(threadSocket, packet, clientID++));

                t.start();

            } catch (Exception e)
            {
                System.err.println("Error in connection attempt.");
            }
        }
    }
}

CLIENTConnection thread class

/*****
 * CT326 - Assignment 12 - [email protected] - 09101916
 */
package pkg12;

import java.io.File;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 *
 * @author [email protected] - 09101916
 */
public class CLIENTConnection implements Runnable
{

    private int clientID;
    private byte[] buffer;
    private int bytesToReceive;
    private DatagramSocket clientSocket;
    private DatagramPacket packet, initPacket;
    private String userInput, filename, initString;

    public CLIENTConnection(DatagramSocket clientSocket, DatagramPacket packet, int clientID) throws IOException
    {
        this.clientSocket = clientSocket;
        this.packet = packet;
        this.clientID = clientID;
    }

    @Override
    public void run()
    {
        try
        {
            buffer = new byte[618];

            System.out.println("THREAD: " + new String(packet.getData(), 0, packet.getLength()));

            initString = new String(packet.getData(), 0, packet.getLength());

            StringTokenizer t = new StringTokenizer(initString);

            userInput = t.nextToken();
            filename = t.nextToken();
            if (t.hasMoreTokens())
            {
                bytesToReceive = new Integer(t.nextToken()).intValue();
            }

            switch (messageType.valueOf(userInput))
            {
                case put:
                    //sends a message gets the new port information to the client
                    send(packet.getAddress(), packet.getPort(), ("OK").getBytes());

                    //create Object to handle incoming file
                    new UDPFileReceiver(clientSocket);

                    break;
                case get:
                    File theFile = new File(filename);

                    send(packet.getAddress(), packet.getPort(), ("OK").getBytes());

                    //create object to handle out going file
                    UDPFileSender fileHandler = new UDPFileSender(clientSocket, packet);
                    fileHandler.sendFile(theFile);
                    break;
                default:
                    System.out.println("Incorrect command received.");
                    break;
            }
        } catch (IOException ex)
        {
            Logger.getLogger(CLIENTConnection.class.getName()).log(Level.SEVERE, null, ex);
        }
        System.out.println("*** Transfer for client " + clientID + " complete. ***");
    }

    private void send(InetAddress recv, int port, byte[] message) throws IOException
    {
        DatagramPacket packet = new DatagramPacket(message, message.length, recv, port);
        clientSocket.send(packet);
    }

    private void send(byte[] message) throws IOException
    {
        DatagramPacket packet = new DatagramPacket(message, message.length);
        clientSocket.send(packet);
    }

    public enum messageType
    {
        get, put;
    }
}

Message class

/*****
 * CT326 - Assignment 12 - [email protected] - 09101916
 */
package pkg12;

import java.io.Serializable;

public class Message implements Serializable {

    private int segmentID;
    private byte[] packet;
    private int bytesToWrite;

    public Message(){}

    public Message(int segmentID, byte[] packet, int bytesToWrite) {
        this.segmentID = segmentID;
        this.packet = packet;
        this.bytesToWrite = bytesToWrite;
    }

    public int getBytesToWrite() {
        return bytesToWrite;
    }

    public int getSegmentID() {
        return segmentID;
    }

    public byte[] getPacket() {
        return packet;
    }
}

Client

/*****
 * CT326 - Assignment 12 - [email protected] - 09101916
 */
package pkg12;

import java.io.BufferedReader;
import java.io.File;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.util.StringTokenizer;

/**
 *
 * @author [email protected] - 09101916
 */
public class Client
{

    private static byte[] buffer;
    private static int port = 8550;
    private static DatagramSocket socket;
    private static BufferedReader stdin;
    private static StringTokenizer userInput;
    private static DatagramPacket initPacket, packet;

    public static void main(String[] args) throws IOException
    {
        socket = new DatagramSocket();
        InetAddress address = InetAddress.getByName("localhost");

        buffer = new byte[618];

        stdin = new BufferedReader(new InputStreamReader(System.in));

        String selectedAction = selectAction();
        userInput = new StringTokenizer(selectedAction);

        try
        {
            switch (messageType.valueOf(userInput.nextToken()))
            {
                case put:
                    packet = new DatagramPacket((selectedAction).getBytes(), (selectedAction).getBytes().length, address, port);
                    socket.send(packet);

                    File theFile = new File(userInput.nextToken());

                    initPacket = receivePacket();

                    //create object to handle out going file
                    UDPFileSender fileHandler = new UDPFileSender(socket, initPacket);
                    fileHandler.sendFile(theFile);
                    break;


                case get:
                    packet = new DatagramPacket((selectedAction).getBytes(), (selectedAction).getBytes().length, address, 8550);
                    socket.send(packet);

                    initPacket = receivePacket();

                    socket.send(initPacket);

                    //create Object to handle incoming file
                    new UDPFileReceiver(socket);
                    break;
            }
        } catch (Exception e)
        {
            System.err.println("not valid input");
        }

        socket.close();
    }

    private static DatagramPacket receivePacket() throws IOException
    {
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
        socket.receive(packet);

        return packet;
    }

    public static String selectAction() throws IOException
    {
        System.out.println("COMMANDS: get *filename*");
        System.out.println("\t  put *filename*");
        System.out.println("\t  example: put data.txt");
        System.out.print("ftp> ");

        return stdin.readLine();
    }

    public enum messageType
    {
        get, put;
    }
}

UDPFileSender

/*****
 * CT326 - Assignment 12 - [email protected] - 09101916
 */
package pkg12;

import java.io.*;
import java.net.*;

public class UDPFileSender
{
    private int segmentID;
    private int reSendCount;
    private byte[] msg, buffer;
    private FileInputStream fileReader;
    private DatagramSocket datagramSocket;
    private int fileLength, currentPos, bytesRead;
    private final int packetOverhead = 106; // packet overhead

    public UDPFileSender(DatagramSocket socket, DatagramPacket initPacket) throws IOException
    {
        msg = new byte[512];
        buffer = new byte[512];
        datagramSocket = socket;

        //setup DatagramSocket with correct Inetaddress and port of receiver
        datagramSocket.connect(initPacket.getAddress(), initPacket.getPort());
        segmentID = 0;
    }

    public void sendFile(File theFile) throws IOException
    {
        fileReader = new FileInputStream(theFile);
        fileLength = fileReader.available();

        System.out.println("*** Filename: " + theFile.getName() + " ***");
        System.out.println("*** Bytes to send: " + fileLength + " ***");

        send((theFile.getName() + "::" + fileLength).getBytes());

        DatagramPacket reply = new DatagramPacket(buffer, buffer.length);
        datagramSocket.receive(reply);

        //waits for receiver to indicate OK to send
        if (new String(reply.getData(), 0, reply.getLength()).equals("OK"))
        {
            System.out.println("*** Got OK from receiver - sending the file ***");

            //outer while to control when send operation comlete
            //inner while to control ACK messages from receiver
            while (currentPos < fileLength)
            {
                bytesRead = fileReader.read(msg);

                Message message = new Message(segmentID, msg, bytesRead);
                System.out.println("Sending segment " + message.getSegmentID() + " with " + bytesRead + " byte payload.");
                byte[] test = serialize(message);

                send(test, bytesRead + packetOverhead);

                currentPos = currentPos + bytesRead;

                //handle ACK of sent message object, timeout of 2 seconds. If segementID ACK is not received
                //resend segment.
                datagramSocket.setSoTimeout(2000);
                boolean receiveACK = false;
                while (!receiveACK)
                {
                    try
                    {
                        datagramSocket.receive(reply);
                    } catch (SocketTimeoutException e)
                    {
                        send(test, bytesRead + packetOverhead);
                        System.out.println("*** Sending segment " + message.getSegmentID() + " with " + bytesRead + " payload again. ***");
                        reSendCount++;
                    }
                    if (new String(reply.getData(), 0, reply.getLength()).equals(Integer.toString(message.getSegmentID())))
                    {
                        System.out.println("Received ACK to segment" + new String(reply.getData(), 0, reply.getLength()));
                        segmentID++;
                        receiveACK = true;
                    }
                }
            }
            System.out.println("*** File transfer complete...");
            System.out.println(reSendCount + " segments had to be resent. ***");
        } else
        {
            System.out.println("Recieved something other than OK... exiting");
        }
    }

    private void send(byte[] message, int length) throws IOException
    {
        DatagramPacket packet = new DatagramPacket(message, length);
        datagramSocket.send(packet);
    }

    private void send(byte[] message) throws IOException
    {
        DatagramPacket packet = new DatagramPacket(message, message.length);
        datagramSocket.send(packet);
    }

    public byte[] serialize(Object obj) throws IOException
    {
        ByteArrayOutputStream byteStream = new ByteArrayOutputStream();
        ObjectOutputStream objectStream = new ObjectOutputStream(byteStream);
        objectStream.writeObject(obj);
        objectStream.flush();
        return byteStream.toByteArray();
    }
}

UDPFileReceiver

/*****
 * CT326 - Assignment 12 - [email protected] - 09101916
 */
package pkg12;

import java.io.*;
import java.net.*;
import java.util.*;

public class UDPFileReceiver
{
    private byte[] buffer;
    private Message receiveMSG;
    private DatagramSocket socket;
    private String filename, initString;
    private FileOutputStream fileWriter;
    private DatagramPacket initPacket, receivedPacket;
    private int bytesReceived, bytesToReceive, simulateBadConnection, expectedSegmentID;
    private final boolean simulateMessageFail = false;//true if you want to simulate a bad connection

    public UDPFileReceiver(DatagramSocket socket) throws IOException
    {
        this.socket = socket;
        buffer = new byte[618];  //618 is the size of my message object. 512 payload, 106 overhead

        System.out.println("*** Ready to receive file on port: " + socket.getLocalPort() + " ***");

        initPacket = receivePacket();
        initString = "Recieved-" + new String(initPacket.getData(), 0, initPacket.getLength());

        //get the file name and byte size of file name
        StringTokenizer t = new StringTokenizer(initString, "::");
        filename = t.nextToken();
        bytesToReceive = new Integer(t.nextToken()).intValue();

        System.out.println("*** The file will be saved as: " + filename + " ***");
        System.out.println("*** Expecting to receive: " + bytesToReceive + " bytes ***");

        //tell the sender OK to send data
        send(initPacket.getAddress(), initPacket.getPort(), ("OK").getBytes());

        fileWriter = new FileOutputStream(filename);

        //two while loops. First checks that there is still more data to receive
        //and inner do/while is to error check on received packets and catch missing ACK
        //sent to sender
        while (bytesReceived < bytesToReceive)
        {
            receiveMSG = new Message();
            do
            {
                receivedPacket = receivePacket();
                try
                {
                    receiveMSG = (Message) deserialize(receivedPacket.getData());
                } catch (ClassNotFoundException ex)
                {
                    System.out.println("*** Message packet failed. ***");
                }

                //logically if the last ACK sent fails to be received the UDPSender will resend the last segment.
                //A simple check on the segemntID will catch this as it will be equal to expectedID - 1. Resending the ACK
                //for this previous segment will eventually get through to the server to send the next expected segment.
                if ((expectedSegmentID - 1) == receiveMSG.getSegmentID())
                {
                    String ACK = Integer.toString(receiveMSG.getSegmentID());
                    send(initPacket.getAddress(), initPacket.getPort(), (ACK).getBytes());
                    System.out.println("*** Resending ACK for segment " + ACK + " ***");
                }

                if (simulateMessageFail)
                {
                    simulateBadConnection = (Math.random() < 0.98) ? 0 : 1; //simulate a 2% chance a message object is lost
                }

                //by adding 1 to segmentIDExpected we can make the receiver determine a message object is lost
                //as the server has a 2 second timeout before it will resend we can check the error control in this way.
            } while (receiveMSG.getSegmentID() != (expectedSegmentID + simulateBadConnection));

            expectedSegmentID++;

            //handles the last byte segmentID size .getBytesToWrite()
            fileWriter.write(receiveMSG.getPacket(), 0, receiveMSG.getBytesToWrite());

            System.out.println("Received segmentID " + receiveMSG.getSegmentID());

            //adding payload size for outer while condition
            bytesReceived = bytesReceived + 512;

            //simulate a 2% chance an ACK is lost
            if (simulateMessageFail)
            {
                if ((Math.random() < 0.98))
                {
                    String ACK = Integer.toString(receiveMSG.getSegmentID());
                    send(initPacket.getAddress(), initPacket.getPort(), (ACK).getBytes());
                } else
                {
                    System.out.println("*** failed to send ACK ***");
                }
            } else
            {
                String ACK = Integer.toString(receiveMSG.getSegmentID());
                send(initPacket.getAddress(), initPacket.getPort(), (ACK).getBytes());
            }
        }
        System.out.println("*** File transfer complete. ***");
        fileWriter.close();
    }

    private DatagramPacket receivePacket() throws IOException
    {
        DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
        socket.receive(packet);

        return packet;
    }

    private void send(InetAddress recv, int port, byte[] message) throws IOException
    {
        DatagramPacket packet = new DatagramPacket(message, message.length, recv, port);
        socket.send(packet);
    }

    private Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException
    {
        ByteArrayInputStream byteStream = new ByteArrayInputStream(bytes);
        ObjectInputStream objectStream = new ObjectInputStream(byteStream);
        return (Message) objectStream.readObject();
    }
}
\$\endgroup\$
2
  • \$\begingroup\$ I tried compiling your code. It works fine for file size less than 512 bytes. For everything else I am getting this exception. I am not sure what's wrong. \$\endgroup\$
    – Quixotic
    Commented Sep 30, 2013 at 11:42
  • \$\begingroup\$ Use constants for PACKET_OVERHEAD and PREFERRED_TU (transmission unit, in memoriam MTU). Put them in the "size expression" instead of commenting it. @VelvetThunder bytesRead + packetOverhead a bit much for buffer, possibly. \$\endgroup\$
    – greybeard
    Commented Jan 11, 2016 at 7:23

1 Answer 1

3
\$\begingroup\$
  • You shouldn't do work in the constructor let alone ALL the work. You can move everything except this.socket = socket; from UDPFileReceiver constructor to a method receive(), like the sendFile method of UDPFileSender.

  • There are unused fields, e.g. buffer from CLIENTConnection. You should heed compiler warnings.

\$\endgroup\$
5
  • \$\begingroup\$ Cheers. Point taken on the constructor. When one stares at the same code for so long basics slip past. I'll check on why I missed the missed buffer compiler warning too. \$\endgroup\$ Commented Feb 25, 2013 at 16:00
  • \$\begingroup\$ Thanks @abuzittin, I've changed my code as per your recommendations. I appreciate you took the time to check my code and point out the oversights. \$\endgroup\$ Commented Feb 26, 2013 at 11:59
  • 1
    \$\begingroup\$ I turned this answer into community wiki as it was not meant to be a comprehensive review at the time it was accepted. I want to see some review on the network protocol, as may people who find this question through search engines. \$\endgroup\$ Commented Feb 26, 2013 at 15:45
  • \$\begingroup\$ I wasn't sure about accepting actually, I just wanted to give credit where it was due. I can remove the accepted answer, let me know. \$\endgroup\$ Commented Feb 26, 2013 at 19:59
  • \$\begingroup\$ You can unaccept it.I was trying to get things rolling and keep the question active. \$\endgroup\$ Commented Feb 27, 2013 at 7:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.