Simple FTP File Transfer using UDP

We are all aware of how we send and receive files over email. But do we know the process behind the simple send and receiving of a file? Well, this post is kind of touching upon on the same topic. This is my learning and experiment in implementing UDP for transferring files i.e. a simple FTP file transfer. For those who doesn’t know how UDP works, please check out this Wikipedia page before moving with the post. The similar concept can be implemented using TCP but utilizing UDP will result in better understanding of the encapsulation, sending and checksum of the packets. In this post, I will try to use Go-Back-N protocol for sending the packets from client side to the server side.

Introduction and Mechanism of the project:

For this post, we will be developing Client and Server side applications to do simple FTP transfer using UDP. First, Client would be establishing the connection with the server. On connection, the client will try to send a file to the server using GoBackN protocol. Here, we are experimenting the code on LocalHost, so there would not be any packet loss while sending the data. To duplicate the working of GoBackN, we are trying to imitate packet loss using random probability. The mechanism for this system is shown below in the image:

simple ftp file transfer

As per the above mechanism, the Client side code of the application which is as follows:

import sys
import time
import datetime
import socket
import pickle
import select
import signal
import os
import timeit
import threading

from Packet import *

if len(sys.argv) != 6:
    print('python client.py <server-host-name> <server-port#> <file-name> <N> <MSS>')
    exit(0)

class Client:

    def __init__(self, ipAddress, portNumber, fileName, windowSize, mss):
        self.portNumber = portNumber
        self.fileName = fileName
        # Creating the socket for the Client
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.ipAddress = socket.gethostbyname(ipAddress)
        self.sock.connect((self.ipAddress, self.portNumber))
        self.sock.setblocking(0)
        self.mss = mss
        self.packetList = list() # Packet List as buffer to save data as the packet to be sent
        self.window_end = windowSize
        self.unacked = 0
        self.total_unacked = 0
        self.timeout = 0.1 # Timeout value for every packet to be sent
        self.sequenceNumber = 0 # default sequence Number to divide file


    # Carry bit used in one's combliment
    def carry_around_add(self, num_1, num_2):
        c = num_1 + num_2
        return (c & 0xffff) + (c >> 16)


    # Calculate the checksum of the data only. Return True or False
    def checksum(self,msg):
        """Compute and return a checksum of the given data"""
        msg = msg.decode('utf-8')
        if len(msg) % 2:
            msg += "0"
        
        s = 0
        for i in range(0, len(msg), 2):
            w = ord(msg[i]) + (ord(msg[i + 1]) << 8)
            s = self.carry_around_add(s, w)
        return ~s & 0xffff

    # RDT Send method to send the file to the server
    def rdt_send(self):
        sendingData = self.divideFile(self.mss, self.fileName, self.sequenceNumber)
        self.unacked = 0
        self.total_unacked = 0
        while self.unacked < len(sendingData):
            if self.total_unacked < self.window_end and (self.total_unacked + self.unacked) < len(sendingData):
                
                for i in sendingData:
                    sq = int(i.sequenceNumber, 2)
                    if sq == self.total_unacked + self.unacked:
                        sendingPkt = i
                self.sock.send(pickle.dumps(sendingPkt))
                self.total_unacked += 1
                continue
            else:
                ready = select.select([self.sock], [], [], self.timeout)
                if ready[0]:
                    ackData, address = self.sock.recvfrom(4096)
                    ackData = pickle.loads(ackData)
                    if ackData.ackField != 0b1010101010101010:
                        continue
                    
                    if int(ackData.sequenceNumber,2) == self.unacked:
                        self.unacked += 1
                        self.total_unacked -= 1
                    else:
                        self.total_unacked = 0
                        continue
                else:
                    print('Timeout, sequence number = ', self.unacked)
                    self.total_unacked = 0
                    continue

        print('Files are transmitted successfully.')
        self.sock.close()

    # Function to divide the file into many chunks of packets based on the MSS value and save into list buffer
    def divideFile(self, mss, filename, sequenceNumber):
        #k = list()
        sequenceNumber = format(sequenceNumber, '032b')
        with open(filename, "rb") as binary_file:
            # Read the whole file at once
            data = binary_file.read()
            # Seek position and read N bytes
            i = 0
            length = sys.getsizeof(data)
            while i <= length:
                binary_file.seek(i)  # Go to beginning
                couple_bytes = binary_file.read(mss)
                checksum = self.checksum(couple_bytes)
                if i + mss > length:
                    self.packetList.append(Packet(sequenceNumber, couple_bytes, checksum, 1)) # adding eof value = 1 for the lastpacket
                else:
                    self.packetList.append(Packet(sequenceNumber, couple_bytes, checksum, 0))
                i += mss
                temp = int(sequenceNumber, 2) + 1
                sequenceNumber = format(temp, '032b')
        return self.packetList



# Main function which will run the main thread
def main():
    # Arguments are passed in order of acceptance in the command line arguments
    socClient = Client(sys.argv[1], int(sys.argv[2]), sys.argv[3], int(sys.argv[4]), int(sys.argv[5]))
    #try:
    print("Run Time:",timeit.timeit(socClient.rdt_send, number = 1))
    #except:
    #   print("Error occured while sending")

if __name__ == "__main__":
    main()

Code Explanation:
In the code above, divideFile method in class Client is to divide the file into smaller chunks of packets whose size is not more than the MSS specified in the command line argument. In this method, we are calling Checksum method to calculate the checksum for the packet and adding that to the encapsulated packet. This checksum method is similar for both Client and Server. The client always sends the complete window and wait for the acknowledgment. As soon as an acknowledgment is received for the first packet sent, the window is slid to right. This similar fashion would be adopted by the Client for all the batch of packets sent to the server.

The Server code is based on receiving the file from the Client where it is waiting for the packet till the last packet arrives. Server tends to reject the packet on following three points:

  • If the Sequence number which arrives was expected by the Server, if not then it is rejected by Server.
  • If the Checksum of the packet received is correct, if not then it is rejected by Server.
  • If randomly found number is greater than the assigned probability loss of packet, if not then it is rejected by Server,

The server code is as follows:

import sys
import time
import datetime
import socket
import random
import operator
import pickle
from Packet import *

if len(sys.argv) != 4:
    print("python server.py <serverPort> <fileName> <p>")
    exit(1)

data = None
class Server:

    def __init__(self, port, filename, p):
        self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self.host = '127.0.0.1'
        self.sock.bind((self.host, port))
        self.sock.setblocking(0)
        print('Server Started on: ', self.host, port)
        self.filename = filename
        self.p = p     
        self.expected_seq = 0
        self.ack_seq = 0
        self.timeout = 0.1
        self.packet_loss = False

    # Carry bit used in one's combliment
    def carry_around_add(self, num_1, num_2):
        c = num_1 + num_2
        return (c & 0xffff) + (c >> 16)


    # Calculate the checksum of the data only. Return True or False
    def checksum(self,msg):
        """Compute and return a checksum of the given data"""
        msg = msg.decode('utf-8')
        if len(msg) % 2:
            msg += "0"
        
        s = 0
        for i in range(0, len(msg), 2):
            w = ord(msg[i]) + (ord(msg[i + 1]) << 8)
            s = self.carry_around_add(s, w)
        return ~s & 0xffff

    def startListener(self):
        global data
        file = open('data.txt', 'wb')
        try:
            while True:
                try:
                    data, address = self.sock.recvfrom(4096)
                    data = pickle.loads(data)
                    prob = random.random()
                    if self.expected_seq != int(data.sequenceNumber, 2):
                            continue
                    elif data.checksum != self.checksum(data.packet):  
                            continue
                        
                    elif prob <= self.p:
                            print('Packet Loss, Sequence Number = ', int(data.sequenceNumber, 2))
                            continue

                except socket.error:
                    continue
                except KeyboardInterrupt:
                    self.sock.close()
                    sys.exit(0)

                file.write(data.packet)
                sendACK = Acknowledgment(data.sequenceNumber)
                self.sock.sendto(pickle.dumps(sendACK), address)
                if data.eof == 1:
                    print('File Received.')
                    sys.exit(0)
                self.expected_seq += 1
            file.close()
        except:
            pass

def main():
    server = Server(int(sys.argv[1]), sys.argv[2], float(sys.argv[3]))
    server.startListener()


if __name__ == "__main__":
    main()

 

Code explanation:

Server class constructor initializes with the basic details such as hostname, port number on which server is listening, filename in which data will be stored, p: probability of losing the packet. startListener is the method which listens to any incoming connection and checks packet for rejecting based on the three points discussed above.

 

Additional Code:

As both the codes are using a particular packet format which had packet field, checksum field, packet data, and end of file field. We encapsulated all this information into a class called Packet. Similarly, acknowledgments are sent from the server which is also encapsulated in the Acknowledgement class. The code for both the class is as follows:

"""
This code is to create class for the Packet to be transferred
"""


class Packet:
    def __init__(self, sequenceNumber, packet, checksum, eof = False):
        self.sequenceNumber = sequenceNumber
        self.dataId = 0b0101010101010101
        self.packet = packet
        self.checksum = checksum
        self.eof = eof
    def printPacket(self):
        print('Sequence Number: ', self.sequenceNumber)
        print('ID: ', self.dataId)
        print('Checksum: ', self.checksum)
        print('End of File: ', self.eof)

class Acknowledgment:
    def __init__(self, sequenceNumber):
        self.sequenceNumber = sequenceNumber
        self.zeros = 0
        self.ackField = 0b1010101010101010
    
    def printAcknowledgement(self):
        print('Sequence Number: ', self.sequenceNumber)
        print('Zeros: ', self.zeros)
        print('ACK Field: ', self.ackField)

You can get the code from this GitHub link and test it by yourself: https://github.com/intellectape/Simple-ftp
Let me know in case you need help with any query related to socket programming. I would be more than happy to help.
Till then, MAY THE FORCE BE WITH YOU!