В сегодняшнем ландшафте цифровой взаимосвязанности эффективная и безопасная передача файлов по локальным сетям является краеугольным камнем современного управления данными. Задавались ли вы когда-нибудь вопросом, глядя на экран: «Как бы плавно переместить файлы между устройствами в локальной сети?». Если да, то это руководство именно для вас.

В нем понятным для новичков языком рассказывается о тонкостях использования Rust для беспрепятственного обмена файлами между такими платформами, как Android, Windows, Mac, Linux и другими. Те, кто хотят сразу перейти к практическому применению, могут в конце руководства найти готовые к использованию бинарные файлы, адаптированные к конкретным системам.

Этот курс научит вас ориентироваться в технических аспектах и прольет свет на прагматичные решения Rust для локальной передачи файлов. Вы получите знания и практические навыки для эффективной передачи файлов по локальным сетям с помощью Rust. Итак, приступим к исследованию сетевых возможностей Rust.

Методы передачи файлов по локальной сети в Rust

Создавая инструмент для совместного редактирования, разрабатывая приложение для одноранговой синхронизации файлов или решая задачу эффективного перемещения больших массивов данных, вы должны иметь представление о доступных методах передачи файлов по локальной сети в Rust. Вот некоторые из распространенных методов.

1. Сокеты и потоки: базовые элементы сетевого взаимодействия

Являясь основой сетевого взаимодействия, сокеты обеспечивают низкоуровневый контроль над обменом данными. Мощный сетевой API Rust позволяет работать непосредственно с сокетами TCP и UDP, создавая собственные протоколы и адаптируя взаимодействие к конкретным потребностям. Этот подход, обеспечивая беспрецедентную гибкость, требует более глубокого понимания сетевых протоколов и значительного количества шаблонного кода для решения таких задач, как сериализация и обработка ошибок.

2. Сторонние библиотеки и фреймворки

Богатая экосистема Rust выходит за пределы стандартной библиотеки: существует множество сторонних библиотек и фреймворков, предназначенных для упрощения сетевых задач. Применение таких библиотек, как mio и reqwest, рассчитанных на создание высокоуровневых абстракций, упрощает разработку и позволяет сосредоточиться на основной логике.

В этом руководстве мы воспользуемся более низкоуровневым подходом и изучим стандартные библиотеки Sockets и Streams. Будем использовать их для передачи файлов по локальной сети, например по сети WIFI. Начнем с создания проекта на языке Rust, осваивая термины и технические особенности по ходу дела.

Создание проекта Rust

Прежде чем приступить к написанию кода, убедимся в готовности необходимых предварительных условий.

Необходимые условия

Rust и Cargo: динамичный дуэт! Убедитесь, что в вашей системе установлены Rust и Cargo. Большинство операционных систем предлагают удобные установочные пакеты или скрипты. Подробные инструкции и варианты загрузки можно найти на официальном сайте Rust.

После установки перейдите в папку, в которую хотите поместить проект. В этом каталоге выполните следующую команду, чтобы создать новый бинарный файл cargo под названием file-transfer:

cargo new file-transfer

Получите сообщение о том, что проект создан.

Роль TCP в сетевой передаче файлов в Rust

TCP можно сравнить с хорошо организованной почтовой службой для цифровых данных. TCP использует структурированный подход, гарантируя, что каждый байт достигнет места назначения в правильном порядке и без ошибок. Эта сложная «хореография» включает несколько ключевых шагов.

  • Передача данных. Перед передачей данных TCP устанавливает двусторонний канал связи между отправителем и получателем. Это обеспечивает выделенный путь для файла, не позволяя ему затеряться в цифровых дебрях сети.
  • Сегментация пакетов. Большой файл (пакет данных) тщательно разбивается на более мелкие, пронумерованные пакеты, что облегчает их передачу и управление ими. Считайте, что вы отправляете данные в индивидуально маркированных коробках, а не в одном громоздком ящике.
  • Упорядочивание и подтверждение. Каждый пакет получает уникальный порядковый номер, действующий как цифровой отпечаток пальца. Получатель подтверждает получение каждого пакета, что позволяет отправителю выявить недостающие пакеты и инициировать повторную передачу. Представьте себе почтовую службу, скрупулезно проверяющую каждый доставленный ящик и запрашивающую замену для тех, которые пропали.
  • Контроль потока. Чтобы не перегружать получателя, TCP динамически регулирует скорость отправки в зависимости от перегруженности сети. Как почтальон не стал бы заваливать ваш порог множеством посылок, так и TCP обеспечивает постепенную и эффективную доставку.
  • Обнаружение и исправление ошибок. Встроенные контрольные суммы проверяют целостность каждого пакета. Если возникают ошибки, поврежденные данные идентифицируются и передаются повторно, гарантируя доставку файла в целости и невредимости. Представьте почтовую службу, которая тщательно проверяет каждую коробку на наличие повреждений и заменяет все коробки с оторванными уголками или выцветшими этикетками.

Преобразование этого потока в код можно отобразить следующим образом.

Соединение отправителя (server) с получателем (client) -> отправка данных (upload) -> загрузка данных (download) -> конкатенация полученных данных в нечто осмысленное.

Попробуем реализовать это в коде на Rust.

Программирование сервера и клиента

Прежде чем погрузиться в код на языке Rust, посмотрим, как это делается.

Вот очень высокоуровневая иллюстрация, призванная упростить ситуацию. В локальной сети каждому устройству маршрутизатор присваивает IP address. IP-адрес обычно выглядит следующим образом: 192.168.100.46. IP-адрес служит для определения местоположения устройства в сети. Узнав его местоположение, вы можете открыть порт, через который будет осуществляться связь. 

Представьте оживленное здание с различными офисами. Каждый офис служит определенной цели: один — для приема почты, другой — для телефонных звонков и так далее. Если перейти с компьютерной терминологии на обычный язык, то можно уподобить порты разным офисам. 

Наиболее распространенными портами являются: 80 для HTTP, 443 для HTTPS и реже 9100 для принтеров, подключенных по сети. Номера портов варьируются от 0 до 65535. Порты с 0 по 1023 — это системные порты, для использования которых обычно требуется root-доступ. 

Порты с 1024 по 49151 известны как зарегистрированные порты. Хотя нам не нужен root-доступ для этих портов, есть вероятность, что их использует другая программа. Например, если к сети подключен принтер, может использоваться порт 9100 для IP-адреса принтера. Для нашего приложения выберем случайный номер порта от 49152 до 65535. Тем самым гарантированно обеспечим бесперебойную работу.

Теперь, немного разобравшись с IP и портами, спланируем, как должна работать программа. Во-первых, нужно установить соединение между сервером и клиентом. Сервер будет прослушивать TCP-соединения через пользовательский порт. Клиент, с другой стороны, будет просто подключаться к адресу сокета сервера, то есть ip + номер порта. Вот как это выглядит в коде:

use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;

fn main() -> thread::Result<()> {
let server = thread::spawn(|| {
server()
});
let _client = thread::spawn(|| {
client()
});
server.join()
}

fn server() {
// Прослушивание входящих соединений
let listener = TcpListener::bind("0.0.0.0:55515").unwrap();
for stream in listener.incoming() {
match stream {
Ok(mut stream) => {
let mut data = String::new();
stream.read_to_string(&mut data).unwrap();
println!("connection established. Data: {}", data);
}
Err(error) => {
println!("error during connection {:?}", error);
}
}
}
}

fn client() {
// Подключитесь к серверу и отправьте данные
// Замените 192.168.100.47 на IP вашего устройства
let mut stream = TcpStream::connect("192.168.100.47:55515").unwrap();
stream.write_all(b"Hello world").unwrap()
}

Используя TcpListener, можно прослушивать TCP-соединения по адресу 0.0.0.0:55515. Часть адреса перед двоеточием — это IP-адрес, используемый TCP-сервером (он одинаков для каждого компьютера). Цифра после двоеточия означает номер порта. Обратите внимание, что в коде клиента указываем IP-адрес сервера и собственный порт. Замените IP-адрес на IP-адрес вашего компьютера и запустите его. На выходе должно появиться сообщение connection established. Data: Hello world (Соединение установлено. Данные: Hello world).

TCP — двустороннее соединение, поэтому и клиент, и сервер могут отправлять и получать данные.

Сериализация данных

Теперь, ознакомившись с основами сокетных соединений, усовершенствуем нашу программу. Поскольку это простая программа передачи файлов, реализуем собственный протокол. Этот протокол прост: сначала отправляем клиенту информацию о файле. Если клиент отвечает Yes (Да) — отправляем файл.

use std::io::{Read, Write};
use std::net::{TcpListener, TcpStream};
use std::{fs, io, thread};
use std::path::PathBuf;

fn main() -> thread::Result<io::Result<()>> {
let server = thread::spawn(|| {
server()
});
let _client = thread::spawn(|| {
client()
});
server.join()
}

#[derive(serde::Serialize, serde::Deserialize, Debug)]
struct FileInformation {
path: PathBuf,
name: String,
size_in_bytes: u64,
}

impl FileInformation {
fn new(file_path: &str) -> io::Result<FileInformation> {
let file = fs::File::open(file_path)?;
let metadata = file.metadata()?;
let path = PathBuf::from(file_path);
let name = format!("{}", path.file_name().unwrap().to_string_lossy());
Ok(FileInformation {
path,
name,
size_in_bytes: metadata.len(),
})
}
}

fn server() -> io::Result<()> {
let listener = TcpListener::bind("0.0.0.0:55515").unwrap();
for stream in listener.incoming() {
match stream {
Ok(mut stream) => {
let info = FileInformation::new("test-image.png")?;
let bytes = serde_json::to_vec(&info).unwrap();
stream.write_all(&bytes)?;
let mut response = [0; 10];
let read = stream.read(&mut response)?;
let response = String::from_utf8_lossy(&response[..read]);
let should_send = response == "YES";
println!("response {} should send {}", response, should_send)
}
Err(error) => {
println!("error during connection {:?}", error);
}
}
}
Ok(())
}

fn client() -> io::Result<()> {
// Замените на IP вашего компьютера
let mut stream = TcpStream::connect("192.168.100.47:55515").unwrap();
let mut buf = [0; 1024];
let size = stream.read(&mut buf)?;
let file_info: FileInformation = serde_json::from_slice(&buf[..size]).unwrap();
println!("File information {:?}", file_info); // Проверка, которая проводится для того, чтобы убедиться, что это правильный файл
stream.write_all(b"YES")?; // Ответ для продолжения передачи
Ok(())
}

В этом примере мы пытаемся отправить файл test-image.png. Можете указать путь к файлу, который хотите отправить. Сериализуем информацию о файле в JSON с помощью serde и serde-json. Это удобные rust-крейты для работы с JSON-данными. После отправки информации о файле ждем ответа от клиента — Yes (Да) или No (Нет). Таким образом, клиент узнает, какой файл отправляется и какой у него размер, прежде чем принять его. Теперь изменим код клиента и сервера, чтобы они также отправляли данные о файле.

// -- Фрагмент -- 
impl FileInformation {

// -- Фрагмент --

fn file_data(&self) -> io::Result<Vec<u8>> {
let mut file = fs::File::open(&self.path)?;
let mut data = Vec::with_capacity(self.size_in_bytes as usize);
file.read_to_end(&mut data)?;
Ok(data)
}
}
fn server() -> io::Result<()> {
let listener = TcpListener::bind("0.0.0.0:55515").unwrap();
for stream in listener.incoming() {
match stream {
Ok(mut stream) => {
let info = FileInformation::new("test-image.png")?;
let bytes = serde_json::to_vec(&info).unwrap();
stream.write_all(&bytes)?;
let mut response = [0; 10];
let amount_read = stream.read(&mut response)?;
let response = String::from_utf8_lossy(&response[..amount_read]);
let should_send = response == "YES";
if should_send {
stream.write_all(&info.file_data()?)?;
}
}
Err(error) => {
println!("error during connection {:?}", error);
}
}
}
Ok(())
}

fn client() -> io::Result<()> {
// Замените на IP вашего компьютера
let mut stream = TcpStream::connect("192.168.100.47:55515").unwrap();
let mut buf = [0; 1024];
let size = stream.read(&mut buf)?;
let file_info: FileInformation = serde_json::from_slice(&buf[..size]).unwrap();
println!("File information {:?}", file_info); // Проверка, которая проводится для того, чтобы убедиться, что это правильный файл
stream.write_all(b"YES")?; // Ответ для продолжения передачи
println!("Downloading: {}", file_info.name);
let mut file_data = Vec::new();
loop {
let mut buf = [0; 32768]; // Увеличение буфера
let size = stream.read(&mut buf)?;
if size > 0 {
file_data.extend_from_slice(&buf[..size]);
let file_size = file_info.size_in_bytes;
println!("Received {} of {} bytes {:.3}%", file_data.len(), file_size, file_data.len() as f64 * 100.0 / file_size as f64);
} else {
// Поток был закрыт, возможно, из-за завершенной передачи
break;
}
}
// Сохраните файл
// Создайте папку `test_folder` или укажите собственную папку
let path = Path::new("test_folder").join(file_info.name);
let mut new = fs::File::create(path).unwrap();
new.write_all(&file_data)?;
println!("done");
Ok(())
}

Теперь, когда все это готово, можете заменить путь к файлу на тот, который вам больше нравится, и запустить программу. Файл может быть изображением, видео, pdf и т. д. Результат должен быть примерно таким:

Downloading: test-image.png
Received 32664 of 283043 bytes 11.540%
Received 65432 of 283043 bytes 23.117%
Received 98200 of 283043 bytes 34.694%
Received 130968 of 283043 bytes 46.271%
--- ---- --- --
Received 283043 of 283043 bytes 100.000%
done

Теперь разделим код, чтобы пользователь мог указать, кем ему быть — клиентом или отправителем.

Указание типа устройства

Убедившись, что код сервера и клиента работает исправно, изменим программу так, чтобы пользователь мог передать аргумент, указывающий, кем он является — получателем или отправителем. Для этого последуем аналогичному руководству из учебника по Rust.

use std::env; 

fn main() -> thread::Result<io::Result<()>> {
    let args: Vec<String> = env::args().collect();
    let device_type = args.get(1).expect("Please specify either client or server").as_str().to_lowercase();
    match device_type.as_str() {
        "client" => {
            thread::spawn(|| {
                client()
            }).join()
        }
        "server" => {
            thread::spawn(|| {
                server()
            }).join()
        }
        _ => panic!("Please specify either client or server")
    }
}

// -- Фрагмент --

Чтобы подтвердить настройку, откройте один терминал и запустите сервер следующим образом: cargo run -- server. После этого откройте другой терминал и запустите команду cargo run -- client. В клиентском терминале увидите вывод, аналогичный тому, что был в предыдущем разделе.

Запуск программы на нескольких устройствах

Настало время опробовать созданную программу на нескольких устройствах. Для начала нужно скомпилировать программу для целевого устройства. Предположим, вы хотите опробовать ее на Android и macOS, тогда понадобится программа, скомпилированная для соответствующих ОС. Чтобы скомпилировать программу для конкретной ОС, укажите архитектуру ОС с помощью флага — target в Cargo.

Например:

cargo build --target x86_64-apple-darwin # для macOS

В качестве альтернативы можете получить предварительно скомпилированные бинарные файлы из моего репозитория (ссылка на него приводится ниже) вместе с обновленным исходным кодом, готовым к производству.

Заключение

Подводя итог, можно сказать, что подход Rust к локальной передаче файлов — это не просто чудо техники; это надежный помощник в освоении искусства написания кода. Благодаря защищенным сетям и молниеносной производительности, Rust обеспечивает бесперебойную передачу файлов, оставляя больше времени на то, чтобы наслаждаться кофе и размышлять о тайнах программирования. Независимо от того, пересылаете ли вы файлы по локальной сети или создаете большое приложение, Rust прикроет вашу спину — и ваши байты!

Репозиторий

Этот репозиторий содержит гораздо более структурированный и документированный исходный код с дополнительными возможностями, такими как установка уникального имени для каждого устройства в сети и отправка текстовых сообщений. Он поставляется в виде бинарного файла и библиотеки, так что можете использовать его по своему усмотрению.

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Kofi Otuo: From Sender to Receiver: Rust’s Approach to Local File Transfers

Предыдущая статья10 типичных ошибок в коде и способы их предупреждения
Следующая статьяМодульное тестирование с помощью JUnit в Android