Rust Programming
Network Programming and File Handling
Project Networked File Transfer Application
In this article, we explain how to build a simple networked file transfer tool—a basic client-server application using Rust. The client reads a file in chunks and sends it to the server over a TCP connection. The server reassembles these chunks, writes them to disk, and acknowledges the successful file transfer.
Application Features
- TCP Communication: Uses TCP sockets to establish reliable client-server communication.
- File Transfer: Transfers a single file from the client to the server.
- File Path Input: The client prompts the user to input the absolute file path of the file to send.
- Error Handling: Implements basic error handling during connection setup and file transfer.
Core Components
Server
The server component consists of a TCP listener that binds to a specified port, accepts client connections, reads file chunks, and writes them to disk. Once the entire file is received, the server sends an acknowledgment back to the client.
Client
The client takes user input for the file path, connects to the server via TCP, reads the file in manageable chunks, and sends these chunks to the server. It includes error handling and waits for the server's acknowledgment before closing the connection.
Client Execution Flow
- Prompt the user for the absolute file path.
- Establish a TCP connection to the server.
- Open the file and read it in fixed-size chunks (e.g., 1024 bytes).
- Send each file chunk to the server.
- Wait for the server's acknowledgment after transmitting all chunks.
- Close the TCP connection.
Server Execution Flow
- The server listens on a predefined TCP port.
- It accepts incoming connections from clients.
- It reads the file chunks sent by the client and writes them to disk.
- After receiving the complete file, the server sends an acknowledgment back to the client.
- The connection is then closed.
File Transfer Details
The file transfer is optimized for large files by reading and sending data in chunks. If a file with the same name already exists on the server, it will be overwritten.
Libraries and Tools
This project leverages several Rust standard libraries and tools:
- std::net::TcpListener and TcpStream: For establishing and managing TCP connections.
- std::fs::File: For file operations such as reading and writing.
- std::io::{Read, Write}: For handling data input/output.
- std::io::stdin: For managing standard input.
Code Walkthrough
Below is a step-by-step walkthrough of the server and client code for our file transfer application.
Project Setup
Create a new Rust project by executing the following command in your project directory:
cargo new tcp_file_transfer
Inside the
src
directory, create a new folder namedbin
and add two files:client.rs
andserver.rs
. This separation keeps the server and client logic distinct.
Server Code
The following code demonstrates the server functionality, including error handling and proper chunked file writing:
use std::fs::File;
use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;
fn handle_client(mut stream: TcpStream) -> std::io::Result<()> {
// Create a new file to store the incoming data
let mut file = File::create("received_file.txt")?;
let mut buffer = [0u8; 1024]; // 1 KB buffer
loop {
// Read data from the stream into the buffer
match stream.read(&mut buffer) {
Ok(0) => {
// End-of-file reached; client closed the connection
println!("End of file reached, client closed the connection.");
break;
}
Ok(bytes_read) => {
println!("Received {} bytes", bytes_read);
file.write_all(&buffer[..bytes_read])?;
}
Err(e) => {
eprintln!("Error reading from stream: {}", e);
break;
}
}
}
// Send an acknowledgment back to the client
println!("File received successfully.");
match stream.write_all(b"Transfer complete") {
Ok(_) => println!("Sent acknowledgment to the client."),
Err(e) => eprintln!("Error sending acknowledgment: {}", e),
}
Ok(())
}
fn start_server() -> std::io::Result<()> {
// Bind the server to localhost on port 8080
let listener = TcpListener::bind("127.0.0.1:8080")?;
println!("Server listening on port 8080...");
// Accept and handle incoming client connections
for stream in listener.incoming() {
match stream {
Ok(stream) => {
thread::spawn(move || {
if let Err(e) = handle_client(stream) {
eprintln!("Error handling client: {}", e);
}
});
}
Err(e) => {
eprintln!("Error accepting connection: {}", e);
}
}
}
Ok(())
}
fn main() -> std::io::Result<()> {
start_server()
}
Client Code
The client code prompts the user for the file path, reads the file in chunks, sends each chunk to the server, and waits for an acknowledgment upon completion.
use std::fs::File;
use std::io::{self, Read, Write};
use std::net::{TcpStream, Shutdown};
use std::process;
fn get_file_path_from_user() -> String {
println!("Please enter the absolute path of the file to send:");
let mut file_path = String::new();
io::stdin()
.read_line(&mut file_path)
.expect("Failed to read line");
file_path.trim().to_string()
}
fn send_file(file_path: &str) -> io::Result<()> {
// Open the specified file
let mut file = File::open(file_path)?;
// Connect to the server at localhost:8080
let mut stream = TcpStream::connect("127.0.0.1:8080")?;
println!("Connected to server at 127.0.0.1:8080");
let mut buffer = [0u8; 1024]; // 1 KB buffer for file data
loop {
let bytes_read = match file.read(&mut buffer) {
Ok(0) => break, // End-of-file reached
Ok(n) => n,
Err(e) => {
eprintln!("Error reading file: {}", e);
return Err(e);
}
};
stream.write_all(&buffer[..bytes_read])?;
}
// Signal to the server that no more data will be sent
stream.shutdown(Shutdown::Write)?;
// Read and display the server's acknowledgment
let mut response = [0u8; 1024];
match stream.read(&mut response) {
Ok(bytes_read) => {
println!("{}", String::from_utf8_lossy(&response[..bytes_read]));
}
Err(e) => {
eprintln!("Error reading acknowledgment: {}", e);
}
}
Ok(())
}
fn main() {
let file_path = get_file_path_from_user();
if let Err(e) = send_file(&file_path) {
eprintln!("File transfer failed: {}", e);
process::exit(1);
}
}
Running the Application
Build the Project
Run the following command to build the project:
cargo build
Run the Server
Open a terminal and run the server with:
cargo run --quiet --bin server
You should see output like:
Server listening on port 8080...
Run the Client
Open another terminal and run the client with:
cargo run --quiet --bin client
When prompted, enter the absolute file path of the file to send. An example interaction is shown below:
Please enter the absolute path of the file to send: /Users/yourname/projects/tcp_file_transfer/temp.txt Connected to server at 127.0.0.1:8080 Transfer complete
On the server terminal, you will see logs detailing each received chunk, the end-of-file notification, and the acknowledgment status.
cargo run --quiet --bin server
Server listening on port 8080...
Received 1024 bytes
Received 1024 bytes
Received 1024 bytes
Received 960 bytes
End of file reached, client closed the connection.
File received successfully.
Sent acknowledgment to the client.
Summary
In this article, we built a networked file transfer application using Rust and TCP sockets. The client reads a file in chunks and sends it to the server, which writes the chunks to disk and sends back an acknowledgment upon successful completion.
Extend the Project
This basic application can be extended with features such as file compression, encryption, or support for transferring multiple files concurrently.
We hope you enjoyed building this project and learning about networked file transfers with Rust!
Watch Video
Watch video content