Introduction
In today's digital landscape, seamless file management across remote systems is essential. Whether it's cloud storage, remote backups, or collaborative file sharing, robust and efficient data transfer is a necessity. In this blog, we’ll explore a custom TCP-based client-server architecture designed to handle file transfers, renaming, summary generation, and more—leveraging low-level networking with Python.
Understanding the Project Scope
The project consists of two key components:
- A server that listens for client requests and processes file operations.
- A client that connects to the server, sends requests, and retrieves responses.
The system is built using Python’s socket
module, implementing a custom binary protocol for communication between the client and server. The primary functionalities include:
- Uploading files (
PUT
)
- Downloading files (
GET
)
- Renaming files (
CHANGE
)
- Retrieving a file summary (
SUMMARY
)
- Requesting help information (
HELP
)
Now, let’s break down the technical details of each component.
Server Architecture: Handling Requests Efficiently
The server is the backbone of this system, running a TCP socket listener to accept incoming connections from clients. Upon receiving a request, the server deciphers the operation code (opcode) and processes the corresponding file operation.
Handling Client Requests
Each client request begins with a single-byte opcode that determines the operation type. The server extracts:
- The 3-bit opcode (operation type)
- The 5-bit filename length (length of the filename in bytes)
Based on the extracted opcode, the server executes one of the following handlers:
Handling File Uploads (PUT)
When a client sends a PUT request, the server:
- Receives the filename.
- Extracts the file size (sent as a 4-byte integer).
- Reads the file data in 1 KB chunks until the complete file is received.
- Saves the file and responds with a success message.
def handle_put(client_socket, filename):
file_size_bytes = client_socket.recv(4)
file_size = int.from_bytes(file_size_bytes, 'big')
with open(filename, 'wb') as file:
bytes_read = 0
while bytes_read < file_size:
chunk = client_socket.recv(min(1024, file_size - bytes_read))
file.write(chunk)
bytes_read += len(chunk)
send_success_response(client_socket)
Handling File Downloads (GET)
For a GET request, the server:
- Checks if the file exists.
- Sends a response with a success opcode, filename, and file size.
- Transfers the file in 1 KB chunks until complete.
- If the file is not found, the server returns a "File Not Found" error.
def handle_get(client_socket, filename):
if not os.path.isfile(filename):
send_error_response(client_socket, error_code=0b011) # File Not Found
return
file_size = os.path.getsize(filename)
response_header = (0b001 << 5 | len(filename)).to_bytes(1, 'big') + filename.encode() + file_size.to_bytes(4, 'big')
client_socket.send(response_header)
with open(filename, 'rb') as file:
client_socket.sendfile(file)
Handling File Renaming (CHANGE)
For renaming files, the server:
- Receives the old and new filenames.
- Checks if the original file exists.
- Performs the rename operation.
- Responds with a success or failure code.
def handle_change(client_socket, old_filename, new_filename):
if os.path.exists(old_filename):
os.rename(old_filename, new_filename)
send_success_response(client_socket)
else:
send_error_response(client_socket, error_code=0b011)
Handling File Summaries (SUMMARY)
For text-based numerical files, the server can generate statistical summaries, including:
- Maximum value
- Minimum value
- Average value
def handle_summary(client_socket, filename):
try:
with open(filename, 'r') as file:
numbers = [float(line.strip()) for line in file if line.strip()]
summary_content = f"Max: {max(numbers)}\nMin: {min(numbers)}\nAvg: {sum(numbers)/len(numbers)}\n"
client_socket.send(summary_content.encode())
except FileNotFoundError:
send_error_response(client_socket, error_code=0b011)
Client Architecture: Sending Requests
The client program connects to the server and sends user-typed commands.
Sending a File (PUT)
def send_file(sock, filename):
with open(filename, 'rb') as file:
sock.sendall(file.read())
Receiving a File (GET)
def receive_file(sock, expected_filename):
file_size = int.from_bytes(sock.recv(4), 'big')
with open(expected_filename, 'wb') as file:
while file_size > 0:
chunk = sock.recv(min(1024, file_size))
file.write(chunk)
file_size -= len(chunk)
Requesting Help (HELP)
def send_help_request(sock):
help_request_byte = 0b10000000
sock.send(help_request_byte.to_bytes(1, 'big'))
Protocol Design: A Custom Binary Protocol
To optimize efficiency and reduce overhead, the system uses a custom binary protocol rather than plain text. Each message follows this structure:
Byte |
Description |
1st Byte |
Opcode (3 bits) + Filename Length (5 bits) |
Filename |
Variable Length (Max: 31 bytes) |
File Size |
4 Bytes (only for GET and PUT requests) |
File Data |
Variable Length |
This compact format ensures fast processing and reduced network bandwidth usage.
Download Project Files
Click the link below to download the complete TCP-based file management system source code.
Download TCP Project (tcp.zip)