Objective
Build a file transfer server and client that can reliably transfer files over TCP, handling large files and showing progress.
Requirements
- Server: Receives files from clients
- Client: Sends files to server
- Protocol: Binary data transfer with metadata
- Progress: Display transfer progress
- Reliability: Verify file integrity
- Error Handling: Handle network errors gracefully
Protocol Design
Message Format
Header (12 bytes):
- Filename length: 2 bytes (big-endian)
- File size: 8 bytes (big-endian)
- Checksum: 2 bytes (reserved for future)
Body:
- Filename: variable length (UTF-8)
- File data: variable length (binary)
Protocol Flow
CLIENT → SERVER: [Header][Filename][File Data]
SERVER → CLIENT: "OK" or "ERROR:message"
Basic Implementation
Server Structure
import socket
import os
class FileTransferServer:
def __init__(self, host='localhost', port=8080, save_dir='./received'):
self.host = host
self.port = port
self.save_dir = save_dir
os.makedirs(save_dir, exist_ok=True)
def start(self):
# TODO: Create server, accept connections, receive files
pass
def receive_file(self, client):
# TODO: Receive header, filename, and file data
# TODO: Save file to disk
# TODO: Send confirmation
pass
if __name__ == '__main__':
server = FileTransferServer()
server.start()
Client Structure
import socket
import os
class FileTransferClient:
def __init__(self, host='localhost', port=8080):
self.host = host
self.port = port
def send_file(self, filepath):
# TODO: Read file
# TODO: Send header
# TODO: Send filename
# TODO: Send file data in chunks
# TODO: Receive confirmation
pass
if __name__ == '__main__':
client = FileTransferClient()
client.send_file('example.txt')
Implementation Steps
Step 1: Basic File Transfer
- Send filename and file size
- Send file data
- Receive and save file
Step 2: Add Progress Display
def show_progress(sent, total):
percent = (sent / total) * 100
bar_length = 40
filled = int(bar_length * sent / total)
bar = '=' * filled + '-' * (bar_length - filled)
print(f'\r[{bar}] {percent:.1f}% ({sent}/{total} bytes)', end='')
Step 3: Handle Large Files
Transfer in chunks to avoid memory issues:
CHUNK_SIZE = 4096 # 4KB chunks
with open(filepath, 'rb') as f:
while True:
chunk = f.read(CHUNK_SIZE)
if not chunk:
break
client.sendall(chunk)
Step 4: Add Error Handling
- Verify file exists before sending
- Check disk space before receiving
- Handle network errors
- Verify file integrity
Advanced Features
Feature 1: Resume Transfer
CLIENT → SERVER: "RESUME:filename:offset"
SERVER → CLIENT: "OK" or "NOT_FOUND"
CLIENT → SERVER: [Data from offset]
Feature 2: Multiple Files
CLIENT → SERVER: "MULTI:count"
SERVER → CLIENT: "READY"
CLIENT → SERVER: [File 1][File 2]...[File N]
Feature 3: Directory Transfer
- Transfer entire directories
- Preserve directory structure
- Handle nested directories
Feature 4: Compression
- Compress files before sending
- Decompress on server
- Use gzip or zlib
Expected Behavior
Server Output
File transfer server started on localhost:8080
Waiting for connections...
Client connected from ('127.0.0.1', 54321)
Receiving file: example.txt (1024 bytes)
[========================================] 100.0% (1024/1024 bytes)
File saved to: ./received/example.txt
Transfer complete
Client Output
Connecting to localhost:8080...
Connected
Sending file: example.txt (1024 bytes)
[========================================] 100.0% (1024/1024 bytes)
Transfer complete: OK
Testing
Test 1: Small File
# Create test file
echo "Hello, World!" > test.txt
# Terminal 1: Start server
python file_server.py
# Terminal 2: Send file
python file_client.py test.txt
Test 2: Large File
# Create large file (10MB)
dd if=/dev/urandom of=large.bin bs=1M count=10
# Send large file
python file_client.py large.bin
# Verify progress display works
Test 3: Error Handling
# Try sending non-existent file
python file_client.py nonexistent.txt
# Should show error
# Stop server during transfer
# Client should handle error gracefully
Solution Hints
Receiving file with header
import struct
def receive_file(self, client):
# Receive header (2 bytes filename length, 8 bytes file size)
header = client.recv(10)
if len(header) != 10:
raise ValueError("Incomplete header")
filename_len = struct.unpack('!H', header[0:2])[0]
file_size = struct.unpack('!Q', header[2:10])[0]
# Receive filename
filename = client.recv(filename_len).decode('utf-8')
# Receive file data
filepath = os.path.join(self.save_dir, filename)
with open(filepath, 'wb') as f:
remaining = file_size
while remaining > 0:
chunk = client.recv(min(4096, remaining))
if not chunk:
raise ValueError("Connection closed during transfer")
f.write(chunk)
remaining -= len(chunk)
client.sendall(b"OK")
Success Criteria
- ✅ Small files transfer correctly
- ✅ Large files transfer without memory issues
- ✅ Progress display works
- ✅ Error handling for network failures
- ✅ Files saved correctly on server
- ✅ Supports various file types (text, binary, images)
Bonus Challenges
- Add file integrity check (MD5/SHA256)
- Implement transfer encryption
- Add bandwidth throttling
- Support multiple concurrent transfers
- Create a GUI client
Next: Exercise 4: Simple HTTP Server to understand web protocols.