Как передать файл по 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 в блокирующем режиме работы сокетов.
После запуска сервер требует ввода адреса и порта для привязки и приема входящих подключений. Затем сервер бесконечно принимает новое подключение и обслуживает запросы клиента до его отсоединения (то есть обслуживается один клиент за раз).
- Создайте новый проект 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 в противном случае — при ошибках или отключении клиента.
- Здесь и далее в указаниях обработка ошибок опущена — необходимо добавлять ее ко всему коду лабораторной работы.
Сетевые приложения должны работать корректно при любых прибывающих данных. В данном случае известно, что длина запроса клиента не превышает 300 байтов (самый длинный запрос содержит имя файла, которое протокол же ограничивает 255 байтами). Целесообразно вынести это значение в константу за пределами функций:
- Реализуйте функцию process_unexpected_message() — точно такую же, как process_unexpected_response() из ЛР № 4. В ее реализации потреюуется и hex_dump() из ЛР № 2.
Передача файлов
Ключевая функция send_file() обслуживает запрос на загрузку файла. Она зеркальна функции download_file() из ЛР № 4.
- Напишите функцию 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 уровень - Сетевых интерфейсов
Физическая передача данных
Общий ход передачи информации выглядит следующим образом:
Данные от приложения отправляются протоколу транспортного уровня.
Получив данные от приложения, протокол разделяет всю информацию на небольшие блоки (пакеты). К каждому пакету добавляется адрес назначения, а затем пакет передается на следующий уровень - уровень протоколов Интернет (сетевой уровень).
На сетевом уровне пакет помещается в дейтаграмму протокола Интернет (IP), к которой добавляется заголовок и концевик. Протокол сетевого уровня определяет адрес следующего пункта назначения IP-дейтаграммы и отправляет его на уровень сетевого интерфейса.
Уровень сетевого интерфейса принимает IP-дейтаграмму и передает их в виде кадров с помощью аппаратного обеспечения (например, сетевой карты).
Пакеты доставляются на компьютер получателя, после чего проходят все уровни протоколов в обратном порядке. На каждом уровне удаляются соответствующие этому уровню заголовки, после чего данные передаются на уровень приложения.
Практика¶
1. Напишите функции для передачи данных с помощью модели TCP/IP от клиента сети. Каждая функция должна имитировать работу одного из уровней передачи данных. Используйте заготовку кода ниже:
2. Напишите функции для получения данных из сети клиентом с помощью модели TCP/IP. Каждая функция должна имитировать работу одного из уровней передачи данных. Используйте заготовку кода ниже, учтите потери данных:
Читайте также: