Multithreaded TCP File Transfer in Python

Today, we are going to implement a simple file transfer TCP client-server program in the python programming language. Here, the server is able to handle multiple clients simultaneously by using multiple threads. The server assigns each client a thread to handle communication with that client.


Outline

  1. Architecture
  2. Functions of the Server
  3. Program Structure
  4. Server – server.py
  5. Client – client.py
  6. Summary

Architecture

For the file transfer program, we are going to use the TCP client-server architecture. Here, we are going to implement a concurrent server with the help of threading.

Next, the questions can be what is this TCP and concurrent server?

TCP – Transmission Control Protocol

TCP is a transport layer protocol that is used for the transmission of data packets from the source to the destination. It is a connection-oriented protocol, which means before the transmission of data packets, it first establishes the connection and then begins with the transmission.

Concurrent Server

A concurrent server has the ability to handle multiple clients at the same time. Here, in our program, the server accepts the client connection and then assigns it to a new thread and becomes available to connect to the next client.

Functions of the Server

The server provides some functionality to the client. The client needs to type the following commands to use the given functionality.

  1. LIST – List all the files present on the server.
  2. UPLOAD – Upload a file to the server.
  3. DELETE – Delete a file from the server.
  4. LOGOUT – Disconnect from the server.
  5. HELP – Show a list of all the commands.

Related Tutorials:

  1. File Transfer using TCP Socket in C
  2. File Transfer using UDP Socket in C

Program Structure

The followings are the files and folders present in the program.

.
├── client_data
│   ├── data.txt
│   └── yt.txt
├── client.py
├── server_data
└── server.py

Here,

  • client_data – This is the folder where client text files are saved, just for the sake of easiness.
  • server_data – This is the folder where the server stores all the files.
  • client.py – This is the client program file.
  • server.py – This is the server program file.

For now, the client_data folder has two files, you can add more files to this folder.

Server – server.py

We begin by importing all the required libraries.

import os
import socket
import threading

Now, we define some variables and set their values.

IP = socket.gethostbyname(socket.gethostname())
PORT = 4456
ADDR = (IP, PORT)
SIZE = 1024
FORMAT = "utf-8"
SERVER_DATA_PATH = "server_data"

Next, we define our main() function, which is the main point of execution of the server program.

def main():
    print("[STARTING] Server is starting")
    server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    server.bind(ADDR)
    server.listen()
    print(f"[LISTENING] Server is listening on {IP}:{PORT}.")

    while True:
        conn, addr = server.accept()
        thread = threading.Thread(target=handle_client, args=(conn, addr))
        thread.start()
        print(f"[ACTIVE CONNECTIONS] {threading.activeCount() - 1}")

Here,

  1. We first, create a TCP server socket.
  2. Bind it with the given ADDR, which is a tuple of IP and PORT.
  3. We wait for the client to connect to the server.

After that, we have an infinite while loop, indicating that the server would run forever without stopping. Inside the while loop, we perform the following:

  1. Accept the client connection.
  2. Create a thread and assign that client to it.
  3. We start the thread and then print the number of active connections.

Each thread uses the handle_client function to communicate with the client.

The server accepted the connection from the client and print the number of active connections.
The server accepted the connection from the client and print the number of active connections.
The client is connected to the server.

Now, we will begin by implementing the handle_client function. Here, is the complete code for the handle_client function.

def handle_client(conn, addr):
    print(f"[NEW CONNECTION] {addr} connected.")
    conn.send("OK@Welcome to the File Server.".encode(FORMAT))

    while True:
        data = conn.recv(SIZE).decode(FORMAT)
        data = data.split("@")
        cmd = data[0]

        if cmd == "LIST":
            files = os.listdir(SERVER_DATA_PATH)
            send_data = "OK@"

            if len(files) == 0:
                send_data += "The server directory is empty"
            else:
                send_data += "\n".join(f for f in files)
            conn.send(send_data.encode(FORMAT))

        elif cmd == "UPLOAD":
            name, text = data[1], data[2]
            filepath = os.path.join(SERVER_DATA_PATH, name)
            with open(filepath, "w") as f:
                f.write(text)

            send_data = "OK@File uploaded successfully."
            conn.send(send_data.encode(FORMAT))

        elif cmd == "DELETE":
            files = os.listdir(SERVER_DATA_PATH)
            send_data = "OK@"
            filename = data[1]

            if len(files) == 0:
                send_data += "The server directory is empty"
            else:
                if filename in files:
                    os.system(f"rm {SERVER_DATA_PATH}/{filename}")
                    send_data += "File deleted successfully."
                else:
                    send_data += "File not found."

            conn.send(send_data.encode(FORMAT))

        elif cmd == "LOGOUT":
            break
        elif cmd == "HELP":
            data = "OK@"
            data += "LIST: List all the files from the server.\n"
            data += "UPLOAD <path>: Upload a file to the server.\n"
            data += "DELETE <filename>: Delete a file from the server.\n"
            data += "LOGOUT: Disconnect from the server.\n"
            data += "HELP: List all the commands."

            conn.send(data.encode(FORMAT))

    print(f"[DISCONNECTED] {addr} disconnected")
    conn.close()

Now, we will break the handle_client function and understand how the communication between the client and the server happens.

def handle_client(conn, addr):
    print(f"[NEW CONNECTION] {addr} connected.")
    conn.send("OK@Welcome to the File Server.".encode(FORMAT))

The handle_client function takes two arguments:

  1. conn – client socket.
  2. addr – tuple of client IP address and port number.

Next, we print a message on the server terminal and then send a welcome message to the client.

    while True:
        data = conn.recv(SIZE).decode(FORMAT)
        data = data.split("@")
        cmd = data[0]

Next, we have an infinite while loop. Inside the loop, all the communication happens until the client sends the LOGOUT command.

Inside, the loop, we first receive the data from the client. The data received from the client has a specific format, i.e., COMMAND@Msg1@Msg2.

So, we split the data string on the basis of @ character. This breaks the data string and creates a list. Next, we extract the command from the beginning of the list. After that, we have some if-else statements for each command.

LIST Command

The LIST command sends the list of all files present in the server folder. If no file is present then send a message “The server directory is empty“.

        if cmd == "LIST":
            files = os.listdir(SERVER_DATA_PATH)
            send_data = "OK@"

            if len(files) == 0:
                send_data += "The server directory is empty"
            else:
                send_data += "\n".join(f for f in files)
            conn.send(send_data.encode(FORMAT))
The client sends the LIST command and received the appropriate output.
The client sends the LIST command and received the appropriate output.

UPLOAD Command

The UPLOAD command extracts the file name and the file data from the data list. Next, it creates the text file in the server folder and saves the data in it. When the data is saved, the server sends the response back.

        elif cmd == "UPLOAD":
            name, text = data[1], data[2]
            filepath = os.path.join(SERVER_DATA_PATH, name)
            with open(filepath, "w") as f:
                f.write(text)

            send_data = "OK@File uploaded successfully."
            conn.send(send_data.encode(FORMAT))
The client uses the UPLOAD command with the text file path and server response back.
The client uses the UPLOAD command with the text file path and server response back.

The above screenshot shows that the client uploaded the file and received the response back. After that, we use the LIST command to check if that file exists or not. We can clearly see that the file has been uploaded successfully.

DELETE Command

The DELETE command deletes a specific file from the server folder. The server first extracts the filename that needs to be deleted from the data list. After that, it checks if the server has the specific files and if it does then it deletes that specific file. After that, the server sends a response back to the client.

        elif cmd == "DELETE":
            files = os.listdir(SERVER_DATA_PATH)
            send_data = "OK@"
            filename = data[1]

            if len(files) == 0:
                send_data += "The server directory is empty"
            else:
                if filename in files:
                    os.system(f"rm {SERVER_DATA_PATH}/{filename}")
                    send_data += "File deleted successfully."
                else:
                    send_data += "File not found."

            conn.send(send_data.encode(FORMAT))
The client uses the DELETE command along with the filename.
The client uses the DELETE command along with the filename.

The above screenshot shows that the client uses the DELETE command with the filename and the file has been deleted successfully.

LOGOUT Command

The LOGOUT command simply breaks the loop and the client gets disconnected.

        elif cmd == "LOGOUT":
            break

HELP Command

The HELP command helps shows all the command that are available along with their short description.

        elif cmd == "HELP":
            data = "OK@"
            data += "LIST: List all the files from the server.\n"
            data += "UPLOAD <path>: Upload a file to the server.\n"
            data += "DELETE <filename>: Delete a file from the server.\n"
            data += "LOGOUT: Disconnect from the server.\n"
            data += "HELP: List all the commands."

            conn.send(data.encode(FORMAT))

Now, when the client uses the LOGOUT command, it breaks the while loop and outside we disconnect the client.

    print(f"[DISCONNECTED] {addr} disconnected")
    conn.close()

This completes the handle_client function.

Client – client.py

We begin by importing all the required libraries and specifying the values of some variables.

import socket

IP = socket.gethostbyname(socket.gethostname())
PORT = 4456
ADDR = (IP, PORT)
FORMAT = "utf-8"
SIZE = 1024

Next, we define our main() function, where we create a TCP socket for the client and connect it to the server.

def main():
    client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    client.connect(ADDR)

We have an infinite while loop, inside which the communication between the client and the server happens.

    while True:
        data = client.recv(SIZE).decode(FORMAT)
        cmd, msg = data.split("@")

        if cmd == "DISCONNECTED":
            print(f"[SERVER]: {msg}")
            break
        elif cmd == "OK":
            print(f"{msg}")

        data = input("> ")
        data = data.split(" ")
        cmd = data[0]

        if cmd == "HELP":
            client.send(cmd.encode(FORMAT))
        elif cmd == "LOGOUT":
            client.send(cmd.encode(FORMAT))
            break
        elif cmd == "LIST":
            client.send(cmd.encode(FORMAT))
        elif cmd == "DELETE":
            client.send(f"{cmd}@{data[1]}".encode(FORMAT))
        elif cmd == "UPLOAD":
            path = data[1]

            with open(f"{path}", "r") as f:
                text = f.read()

            filename = path.split("/")[-1]
            send_data = f"{cmd}@{filename}@{text}"
            client.send(send_data.encode(FORMAT))

    print("Disconnected from the server.")
    client.close()

Summary

In this post, we have learned how to build a multithreaded file transfer program using a TCP socket in Python programming language. Watch the YouTube video for a more detailed understanding: Multithreaded File Transfer using TCP Socket in Python

I hope you have learned something new from this blog, if YES.

Follow me:

Nikhil Tomar

I am an independent researcher in the field of Artificial Intelligence. I love to write about the technology I am working on.

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *