Как передать файл по tcp

Обновлено: 04.07.2024

Обычные примерчики из интернета типа, socket.send, socket.recv мне ну разумеется не подходят. Почему? Да потому что все они как один не учитывают Command Channel, а тупо шлют куски файла пока буффер не заполнится и не передадут весь файл. А мне нужно помимо скачивания/закачивания файлов еще иметь возможность в том же самом TCP соединении, параллельно - слать рандомные клиентские пакетики. Почему не замутить 2 разных соединения? да потому что я планирую сделать не просто скачивание обновлений, а целую систему синхронизации файлов, или даже собственный контроль версий. И пока синхронизируются файлы, пользователь может получать от сервера и другие пакеты, например об отвалившихся или подсоединившихся пользователях, да и мало ли чего еще.

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

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

Размер Optional буффера можно скорее всего просто задать вручную, либо вычислить программно в зависимости от параметров системы, главное чтобы его хватало, чтобы реализовать потенциал пропускной способности сервера. Формально Optional буффер равноценен сокет буфферу, по своему смыслу. В то время как WriterBuffer это чисто моя фича, это не просто сокет буффер, но и сразу очередь пакетов, его переполнение равносильно переполнению очереди - разрыв соединения, и срочный патч с увеличенным буффером. За количеством пакетов следит уже разработчик сервера и логика сервера, ограничение Writer буффера это лишь крайняя мера, но вместо исключения просто разрыв соединения.

С буфферами разобрались, теперь че по протоколу? Я разделил передачу файлов на 2 задачи: передача манифеста, и передача файла(файлов). Всего 8 пакетов:

Отправка манифеста и файлов, это 2 отдельные задачи, но взаимосвязанные. Для приема/отправки файлов нам нужно установить манифест приема/отправки, не важно где мы его взяли, получили по сети или загрузили локально с файла. Сетевая библиотека автоматически записывает получаемые файлы строго в соответствии с манифестом, файловые пакеты не содержат никаких имён или размеров, всё находится в ManifestRecords.

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

Даже если нам нужно передать всего 1 файл, значит нужно указать манифест с 1м файлом. Который можно создать динамически или загрузить из файла.

Одновременно будет выполняться FileTransfer только в одну сторону, либо Send либо Recv, этого мне пока достаточно.

Итак вкрацце: вводим Optional буффер для файловых пакетов, которые в зависимости от перегруженности буффера либо записываются в буффер в этом кадре, либо в следующем, либо когда освободится место. В то время как обычные пакеты "всегда" записываются в Required буффер моментально, без всяких проверок есть место или нет.

Как это использовать? 2 варианта:
1) Система обновлений (Download/Upload).
- Клиент соединяется к серверу
- Если версия клиента не последняя, сервер посылает клиенту манифест с последней версией абсолютно всех файлов.
- Клиент проверяет каждый файл на хешсумму и размер, генерирует свой Diff манифест с отличающимися файлами/папками, и посылает серверу.
- Сервер начинает передачу всех файлов по клиентскому манифесту, если они были в серверном манифесте само собой.

2) Самодельная простая система контроля версий или синхронизации (без чекинов).
Скачивание/синхронизация последней версии:
- Аналогично системе обновлений, за исключением того, что клиент получает только те файлы, к которым у него есть доступ на чтение или запись.

Отправка коммита:
- Клиент посылает пакет с инфой коммита.
- Клиент посылает манифест с файлами коммита, которые разрешено изменять.
- Клиент посылает файлы указанные в манифесте.

Научиться создавать серверы TCP в блокирующем режиме работы сокетов.

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

  1. Создайте новый проект lab05-tcp-server , подключите необходимые библиотеки для работы с API сокетов. Для Windows инициализируйте API.

Прием подключений

Сервер TCP работает по более сложной схеме, чем клиент (см. ту же презентацию, что в ЛР № 4):

В смысле ресурсов сокет-слушатель соответствует одному порту, через который клиенты могут подключаться к приложению, а сокет-передатчик — одному соединению. В простейшей реализации этой ЛР используется только один сокет-передатчик за раз, но их может быть много и они могут работать параллельно (ЛР № 6—7).

При помощи функции ask_endpoint() из предыдущих ЛР запросите адрес и порт для привязки и привяжите к ним сокет функцией bind() .

Переведите сокет в режим слушателя:

Второй параметр listen() — размер очереди входящих соединений. Он важен, если в то время, пока сервер обслуживает одного клиента (то есть пока не вызвана accept() ) попытаются присоединиться новые. До трех первых из них станут в очередь на подключение (ОС проведет само подключение, но не позволит обмениваться данными), прочие сразу получат ошибку подключения. Максимально длинная очередь обозначается константой SOMAXCONN .

Вызов accept() для принятия нового подключения — блокирующий, то есть выполнение программы останавливается на нем, пока извне не попытается подключиться клиент. Помимо сокета-слушателя accept() принимает указатель на адрес и на размер адреса подключившегося клиента, полностью аналогично функции recvfrom() с ее адресом отправителя. При ошибке accept() возвращает INVALID_SOCKET (Windows) или -1 (*nix).

В бесконечном цикле ведите прием подключений:

Добавьте обработку ошибок accept() — прерывайте цикл при ошибке.

Добавьте получение адреса подключившегося клиента и его печать перед вызовом serve_requests() , как в ЛР № 3 для recvfrom() .

Добавьте закрытие сокета после окончания цикла:

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

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

    Уберите цикл, но не его тело, то есть принимайте одно подключение за один сеанс работы программы.

Временно замените вызов serve_requests() на прием единственного байта:

При помощи netcat присоединитесь к ней:

Отправьте единственный байт (нажми Enter в netcat), чтобы завершить соединение (сервер считате один байт, завершит recv() и вызовет closesocket() ).

Обслуживание запросов и обработка ошибок

Обслуживание запросов — еще один цикл:

Используются receive_some() и send_some() из ЛР № 4.

Функция server_request() должна возвращать true , если запрос успешно обслужен, и false в противном случае — при ошибках или отключении клиента.

  1. Здесь и далее в указаниях обработка ошибок опущена — необходимо добавлять ее ко всему коду лабораторной работы.

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

  1. Реализуйте функцию process_unexpected_message() — точно такую же, как process_unexpected_response() из ЛР № 4. В ее реализации потреюуется и hex_dump() из ЛР № 2.

Передача файлов

Ключевая функция send_file() обслуживает запрос на загрузку файла. Она зеркальна функции download_file() из ЛР № 4.

  1. Напишите функцию send_file() с обработкой возможных ошибок (которая не делается в приведенном ниже коде).

Имя файла для загрузки не передается, а принимается. Буфер для приема в виде вектора заполняется нулями и на один байт больше, чем нужно. Дополнительный байт не заполняется и остается '\0' , таким образом указатель на начало вектора является указателем на завершающуюся нулем строку, т. н. строку C.

Полученная строка C используется для открытия файла. В случае любых ошибок при открытии клиенту сообщается о невозможности доступа к файлу.

При работе с файлом есть текущая позиция чтения из него: при открытии она 0, если прочитать 10 символов, она станет 10, а если еще 10 — станет 20 и т. д. Можно узнать позицию методом tellg() и изменить ее методом seekg() . Чтобы определить размер файла, можно сместиться к его концу (на нулевое смещение от конца), узнать эту позицию и вернуться в начало:

Размер ответа — сумма размера типа (1 байт) и размера файла ( size байтов); передается в сетевом порядке байт:

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

Результат чтения из файла стоит проверять на ошибки:

Метод readsome() не пытается считать данные, если их больше не доступно, поэтому флаг input.eof() никогда не будет взведен, зато можно проверить достижение конца файла по результату readsome() и выйти из цикла:

Отправка списка файлов

Получение списка файлов в каталоге делается по-разному в зависимости от ОС. Готовая функция list_files() дана в listing.h (изменен 31.03) , она работает в Windows и Linux и возвращает вектор строк-имен файлов:

Файл предлагается сохранить в каталог своего проекта и подключить к программе:

Функция list_files() выдает список только обычных файлов (не скрытых, не директорий) в текущем каталоге. Гарантируется, что ни одно имя не будет длиннее 255 символов (байтов).

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

Переменная file содержит строку-имя очередного файла. К динамическому массиву body необходимо добавить один байт-длину file (типа uint8_t ) и все байты строки file . Для этого необходимо увеличить длину body : новая длина равна сумме старой длины, одного байта и длины строки file .

После изменения размера body состоит из двух участков:

  • от &body[0] до &body[old_body_length - 1] содержит данные, которые уже были в body до изменения размера;
  • от &body[old_body_size] и до конца предназначен для записи новых данных.

Значение old_body_size необходимо было сохранить до изменения размера, после этого его уже нельзя было бы вычислить — деление массива существует только с точки зрения логики программы, а не самого массива.

Записывать данные во вторую область последовательно удобно с помощью указателя на первый из еще не использованных байтов, названный place :

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

Указатель place увеличивается на количество записанных данных, т. е. на один.

Следующим шагом все символы file копируются в ту (свободную) область памяти, на которую указывает place :

Если бы после этого требовалось бы записывать еще какие-либо данные через place , следовало бы увеличить place на количество записанных данных, т. е. на file.length() .

Во всех заданиях нужно расширить описание протокола, поддержать изменения в клиенте и сервере и подготовить демонстрацию работы программы. Опущенные детали (тексты ошибок, типы данных и т. п.) выберите сами.

Добавьте к списку файлов (команда /list ) их размеры в байтах, изменив формат пакета типа 0x00 . Для этого можно воспользоваться усовершенствованным listing.h .

Измените сервер, чтобы при запросе файла INFO , есть он или нет, выдавалось не содержимое файла, а IP-адрес и порт клиента в виде текста. Получить адрес можно getpeername() , формировать строку — stringstream .

Добавьте команду /delete и новый запрос, позволяющий удалить файл по имени. Это можно сделать DeleteFile() (Windows) или unlink() (*nix).

Добавьте команду /view , аналогичную /get , но с параметром-количеством первых байтов файла, которые пользователь желает скачать. Сервер должен выдавать не более этоно числа байтов в теле ответа.

Добавьте команду /stat и новый тип запроса (код 0x18 ), по которому сервер отдает в двоичном виде статистику: количество подключений, количество запросов на файлы и суммарный размер выгруженных данных.

Козлюк Д. А. для кафедры Управления и информатики НИУ «МЭИ», 2018 г.

Передавать сырые данные TCP и UDP умеют программы Ncat, Netcat, nc.

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

Как передавать и получать сырые данные по протоколу UDP

Чтобы не просто выполнить подключение, когда данные вводятся вручную (вводить шестнадцатеричные данные вручную затруднительно для нас), а чтобы подключиться и сразу передать данные, можно использовать команду вида:

Опция -u означает использовать UDP протокол (по умолчанию используется TCP).

Пример команды, которая отправляет данные из файла hello-camera.bin на удалённый IP 255.255.255.255 на UDP порт 34569:

UDP протокол не дожидается ответа, он разрывает соединение. Для отправки ответа удалённых хост запускает новое UDP соединение, но дело в том, что для его подключения мы должны прослушивать порт. Ответ придёт на UDP порт 34569. Прослушивать порт можно также командой ncat. Для этого используется команда вида:

В этой команде опция -u означает использовать UDP протокол (по умолчанию используется TCP). Опция -l означает прослушивать входящие соединения. IP-АДРЕС - это IP сетевого интерфейса на локальной машине, где запущена утилита ncat. ПОРТ - это порт для прослушивания.

IP адрес компьютера, где будет запущена ncat, 192.168.0.88, нужно прослушивать на 34569 порту, тогда команда следующая:

Кажется, что ничего не происходит, но программа и не завершает работу - она просто ожидает входящее соединение.

Не закрывая это окно терминала, откроем другую консоль и вновь повторяем первую команду:

После этого в первой консоли будет показан полученный ответ:


Чтобы сохранить присланный ответ (вместо того, чтобы выводить его на экран), можно использовать следующую команду для прослушивания входящих подключений:

В результате присланный ответ будет сохранён в файле response.bin.

Как передавать и получать сырые данные по протоколу TCP

Как можно увидеть, мы получили ответ.


Для своих целей я записал всех бинарные строки, которые отправляла программа CMS на камеру в отдельные файлы с именами hex1, hex2 и так далее до hex12. Для воспроизведения полного диалога с камерой можно использовать команду вида:

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

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

Уровни модели TCP/IP:

За что отвечает

4 уровень - Прикладной

Формат данных, их шифрование

3 уровень - Транспортный

Способ передачи данных

2 уровень - Сетевой

Маршрутизация в сети

1 уровень - Сетевых интерфейсов

Физическая передача данных

Общий ход передачи информации выглядит следующим образом:

  1. Данные от приложения отправляются протоколу транспортного уровня.

  2. Получив данные от приложения, протокол разделяет всю информацию на небольшие блоки (пакеты). К каждому пакету добавляется адрес назначения, а затем пакет передается на следующий уровень - уровень протоколов Интернет (сетевой уровень).

  3. На сетевом уровне пакет помещается в дейтаграмму протокола Интернет (IP), к которой добавляется заголовок и концевик. Протокол сетевого уровня определяет адрес следующего пункта назначения IP-дейтаграммы и отправляет его на уровень сетевого интерфейса.

  4. Уровень сетевого интерфейса принимает IP-дейтаграмму и передает их в виде кадров с помощью аппаратного обеспечения (например, сетевой карты).

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

Практика¶

1. Напишите функции для передачи данных с помощью модели TCP/IP от клиента сети. Каждая функция должна имитировать работу одного из уровней передачи данных. Используйте заготовку кода ниже:

2. Напишите функции для получения данных из сети клиентом с помощью модели TCP/IP. Каждая функция должна имитировать работу одного из уровней передачи данных. Используйте заготовку кода ниже, учтите потери данных:

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