allasm.ru |
|
Введение. В этой статье я не ставлю себе целью пересказать все RFC касающиеся протокола FTP, коих не мало, в них вы сможете найти информацию гораздо полнее, попытаюсь лишь в общих чертах познакомить Вас с протоколом FTP и основными приемами работы с ним со стороны клиента. Общие сведения о протоколе FTP. Итак, FTP (File Transfer Protocol) – протокол передачи файлов в сетях стандарта TCP/IP. Этот протокол был специально создан для облегчения и стандартизации программирования алгоритмов передачи файлов между клиентом и сервером. Как и все протоколы высокого уровня, он не занимается непосредственной передачей данных (этим занимается протокол более низкого уровня – TCP, а так же протоколы ниже), а лишь описывает способ «общения» клиент-сервер. Перейдем непосредственно к описанию протокола. Отличительной его особенностью является использование двух соединений между сервером и клиентом. Одно соединение (командное или управляющее) используется для передачи команд серверу, а так же приема ответов на эти команды. Второе соединение (соединение данных) используется непосредственно для приема или передачи данных. Управляющее соединение всегда происходит со стороны клиента на порт сервера 21 и остается на протяжении всего сеанса работы открытым. Соединение данных открывается и закрывается по мере необходимости в приеме или получении данных. После того как установлено управляющее соединение клиент может отправлять по нему серверу различные команды. Каждая команда представляет из себя 3 или 4 заглавных символа ASCII, за которыми после одного или более пробелов следуют, в некоторых командах не обязательные аргументы. Любая команда заканчивается парой CR, LF – это, несомненно, известные всем 0dh, 0ah – если речь идет о DOS/Windows. В общих чертах схема команды такая: Команда [аргумент(ы)] CR, LF. Всего существует чуть более 30 команд (в RFC959 – 33) которые могут быть посланы серверу, но это совсем не значит что сервер все их будет поддерживать. Приведу пример наиболее часто используемых команд.
При получении запроса сервер, по тому же управляющему соединению отправляет ответ на него. Ответ сервера состоит из трех символов (цифр) в формате ASCII, за которыми следует не обязательный текст, обычно поясняющий цифирный код ответа, за этим пояснением следуют неизменные CR, LF. Ответ например может быть таким: 226 File send OK. – в этом примере сервер сообщает нам о том, что файл отправлен с его стороны (что совсем не означает, что он уже получен со стороны клиента). Первая цифра отклика сервера наиболее значимая, и дает однозначное представление о том как выполнилась (или не выполнилась) команда. Значения могут быть такими:
По второй цифре отклика можно судить о том, какая ситуация привела к возникновению отклика:
Ну и наконец третья цифра отклика несет в себе дополнительную информацию. Следует обратить особое внимание на то, что хотя на большую часть команд сервер отвечает одним откликом, есть и широко используются команды, в ответ на которые сервер генерирует несколько откликов. При этом первая цифра первого отклика будет «1» - т.е. если взглянуть на таблицы выше, сервер сообщает нам о том, что необходимо подождать еще одного сообщения от него, перед тем, как посылать следующую команду. Примером такой команды может служить команда RETR, когда сервер принимает ее и начинает пересылку данных он отвечает нам что-то вроде: «150 Opening BINARY mode data connection for HIDE.ASM (958 bytes).» - смысл сообщения сводится к «начата передача данных». Затем, когда данные им уже будут отправлены (но опять хочу заострить внимание – не факт, что получены клиентом) он отправит по управляющему соединению еще один отклик – «226 File send OK.» - т.е. «файл отправлен». Вот в этом случае только после получения второго сообщения сервер готов к выполнению следующей команды. Вместо последнего сообщения мы вполне можем получить сообщение с ошибкой начинающееся с «4» - в том случае, если возникнут какие-либо проблемы с передачей файла. В общих чертах это все, что касается управляющего соединения. Теперь поговорим о соединении данных. Как уже говорилось выше, соединение данных организуется по мере необходимости, и закрывается каждый раз после передачи или приема данных. Так происходит потому, что режим передачи данных между клиентом и сервером – потоковый, а в таком режиме окончание передачи данных – закрытие соединения. Из вышесказанного мы должны сделать один немаловажный вывод – судить об окончании передачи данных со стороны сервера мы можем по закрытию соединения. Обычно соединение данных открывается следующим образом: - клиент выбирает свободный порт на своем хосте и осуществляет пассивное открытие на него; - клиент сообщает серверу по управляющему соединению свой IP-адрес и номер порта, на который сделал пассивное открытие; - сервер, получив порт и IP-адрес осуществляет его активное открытие; - передаются или принимаются данные; - в зависимости от того кто передает, а кто принимает данные осуществляется закрытие порта. Небольшое отступление: если вы внимательно прочтете второй пункт, может возникнуть вопрос – «А что будет если мы передадим серверу фиктивный адрес и порт?». Ответ неоднозначен, сервер может проверять IP-адрес, но это происходит не всегда, поэтому существуют некоторые интересные «заморочки» с использованием фиктивных адресов. Что касается порта, выбираемого для соединения данных клиентом. Обычно используется динамически назначаемый ОС порт, - т.е. делается запрос к системе, она дает первый свободный. Если клиент не указывает серверу порт для соединения, оно происходит на порт с которого было проведено управляющее соединение (поступать так не рекомендуется). Сервер всегда осуществляет соединение данных с 20-го порта. Это все основное, что я хотел рассказать о соединении данных. Теперь, когда мы знаем для чего и как работают оба соединения, хочу отметить еще один момент (при первом прочтении можно пропустить). Команда LIST возвращает список файлов текущей директории, и возвращает его по соединению данных. Список представляет из себя набор строк ASCII оканчивающихся символами CR, LF. Каждая строка несет в себе информацию об одном из элементов запрашиваемого каталога. Общий шаблон этой строки такой: Txxxxxxxxx[ ]uk[ ]user[ ]group[ ]size[ ]mm[ ]dd[ ]yytt[ ]name CR, LF где, T – тип элемента («d» - каталог, «-» - файл, «l» - ссылка и т.д.); Да, между этими элементами может быть различное количество пробелов, надо сказать спасибо, что в различных реализациях серверов оставили одно количество значимых столбцов, поэтому при анализе таблицы файлов следует это учитывать. Стоит еще учесть такую вещь, что не всегда первая строка из таблицы есть значимая строка, несущая информацию о первом элементе каталога. В некоторых реализациях FTP-серверов (например ftpd на FreeBSD), первой строкой списка является строка «total NN». Как это должно работать? Давайте немного отвлечемся и посмотрим, как же должен выглядеть FTP сеанс получения файла «изнутри». Итак, мы запускаем клиента. Сервер в это время уже пассивно открыл и слушает 21-ый порт. В первую очередь нам необходимо создать управляющее соединение – конектимся на сервер на порт 21. Что дальше? Сразу, как только мы удачно законектились с сервером по созданному управляющему соединению нам приходит приветствие от сервера, это будет что-то вроде «220 VSFTP deamon base on Alt Linux 2.2, Shpakovsky». Следующим шагом должна быть регистрация – допустим мы соединяемся с анонимным сервером - по управляющему соединению клиент посылает серверу команду USER anonymous, на что, если сервер поддерживает анонимного пользователя получаем ответ: «331 Please specify the password.» - «пожалуйста сообщите пароль», заметим цифру «3» в ответе сервера, что означает, что для продолжения требуется еще команда, что собственно и делает клиент – посылаем команду PASS 1@1 – в качестве пароля указав фиктивный e-mail. На что получаем ответ сервера «230 Login successful. Have fun.» - «Регистрация прошла успешно». Все, теперь наши действия зависят от того что мы хотим, а как говорилось выше, хотим мы получить с сервера файл, пусть к примеру это будет файл «HIDE.EXE», расположенный в корневом каталоге сервера. Перед тем, как осуществлять прием или передачу данных серверу необходимо указать какой тип данных будет передаваться, делается это командой TYPE N, где N = «A», если тип ASCII и N = «I», если файл бинарный. Клиент посылает серверу команду TYPE I, на что получает ответ – «200 Switching to Binary mode.». Итак, осталось только получить файл. Для этого клиенту необходимо открыть соединение данных. Клиентом выбирается свободный порт, осуществляется пассивное открытие, т.е. клиент его «слушает». Дальше клиенту нужно сообщить серверу свой IP-адрес и номер порта, который только что пассивно открыл (допусти IP-адрес хоста клиента будет 10.21.23.10, а номер порта 2000). Клиент посылает серверу по управляющему соединению команду PORT 10,21,23,10,7,208 – «что за 7,208?» - спросите вы. Это и есть номер порта строится он так – 7*256+208 = 2000. Сервер после получения этой команды попытается сделать активное открытие указанного порта и в случае удачи вернет что-то вроде «200 PORT command successful. Consider using PASV.». Все, соединение данных установлено остается дать команду передачи данных серверу, что и делает клиент - RETR HIDE.EXE, на что в случае если все нормально (файл существует и может быть передан) сервер отвечает «150 Opening BINARY mode data connection for HIDE.EXE (4096 bytes).» и начинает сливать файл по соединению данных. Опять обращаю ваше внимание на первую цифру ответа. Когда файл будет полностью отправлен сервер пошлет сообщение «226 File send OK.» и произведет закрытие соединения данных. Клиент ждет окончания получения данных со своей стороны (о чем свидетельствует получение сообщения от сервера + закрытие соединения данных, тут есть ньюансы, но о них позже) после чего закрывает порт соединения данных со своей стороны. Итак файл получен клиентом, остается разорвать управляющее соединение, клиент посылает команду QUIT, сервер отвечает «221 Goodbye.» и разрывает соединение. Вот собственно самое важные теоретические сведения о протоколе. Перед тем как переходить к практике очень советую побаловаться управляющим соединением с FTP-сервером, используя telnet, соединение данных создать не получится, но команды и ответы на них будут на виду. Так же рекомендую поработать с каким-либо консольным клиентом FTP и понаблюдать во время всего этого за созданием и закрытием соединений с помощью какой-нибудь утилиты для этого, коих в Интернете как грязи. Реализация. Теперь о самой реализации. В этой реализации клиента я использую non-blocking (не блокирующие) сокеты, поэтому модель клиента – событийная, т.е. выполнять те или иные действия, касающиеся используемых клиентом сокетов клиент будет только при возникновении соответствующего события (например закрытие соединения, уведомление о получении данных и т.д.). В качестве событий используются сообщения, приходящие в процедуру главного окна. Кроме того, модель программы поточная, используется поток для чтения соединения данных и поток для чтения управляющего соединения, а так же основной поток клиента, запускающийся при нажатии на кнопку «соединение». Так как программа многопоточная для синхронизации работы этих трех потоков (а так же процедуры сообщений главного окна) используются «event’s» («события», не путать эти события, используемые программой как датчик 1 или 0 – произошло или не произошло событие, и события касающиеся сокетов, которые приходят на процедуру главного окна). Итак, начнем. При создании основного окна приложения мы проводим основную инициализацию программы, поясню основные моменты:
Здесь выделяется память под буфер приема файла (1 Мб) и под буфер команд (10 Кб).
Создаются объекты event (события) более подробно о назначении событий позже.
Создаются 2 потока – один для чтения данных, другой для чтения управляющего потока. Оба этих потока при старте находятся в приостановленном состоянии, и начинают работать только при установлении соответствующего события.
Смысл строк выше в получении IP-адреса нашего хоста, небольшом преобразовании и записи его в отдельное место, адрес хоста нам потребуется для выполнения команды PORT. На этом процесс начальной инициализации заканчивается, и программа находится в состоянии ожидания команды пользователя. Давайте посмотрим что происходит при нажатии пользователем кнопки «соединиться». В основной процедуре окна создается главный поток приложения, рассмотрим его ключевые моменты. Сразу при старте мы инициализируем переменные относящиеся к приему данных и получаем из окна диалога введенные пользователям параметры соединения (сервер, пароль и.д.). После этого нам необходимо создать управляющее соединение с сервером, что мы и делаем:
Теперь пришло время рассмотреть потоки получения данных, как говорилось выше эти потоки создаются в процессе инициализации главного окна, и находятся постоянно в процессе ожидания новых данных, потоки активизируются в процедуре главного окна при получении ей сообщении о том, что есть новые данные, сообщение для управляющего соединения мы определили в самом начале главного потока функцией WSAAsyncSelect, сообщение для соединения данных определяется при создании этого соединения, как мы увидим позже. Универсальный трэд для получения данных по управляющему и соединению данных приведен ниже.
Вернемся теперь к основному потоку, мы успешно получили ответ от сервера, в том что он готов к приему команд, теперь мы можем передавать ему команды, в данной реализации за отправку команд серверу отвечает функция SendCommandInSocket, в основном потоке далее мы вызываем эту функцию для отправки серверу последовательно команд: USER, PASS, TYPE, CWD, PORT и LIST. Сама функция выглядит так:
Необходимо учесть еще одну вещь – перед отправкой команды PORT, нам надо создать слушающий сокет, это мы делаем с помощью вызова процедуры CreateListenSock.
Итак последней отправленной командой была команда LIST, после нее на соединение данных должен прийти список файлов текущей директории, соответственно после отправки сообщения нам необходимо подождать пока этот список будет нами получен, т.к. даже если сервер отправил нам сообщение о том, что он успешно завершил отправку всех данных это совсем не значит, что наш поток уже все отработал и получил, поэтому мы ожидаем окончания получения функцией WaitTransferComplete.
В случае успешного завершения процедуры выше в буфере приема данных будет лежать таблица каталога. Ниже по программе мы обрабатываем полученную таблицу и по очереди получаем все найденные в ней файлы, получение файла ничем не отличается от получения директории, поэтому здесь этого я описывать не буду. После того, как все файлы были получены и сохранены мы закрываем управляющее соединение и завершаем поток. Заключение. Мы разобрали основные принципы работы с протоколом FTP со стороны клиента, конечно же были затронуты далеко не все аспекты этой задачи. Например не была рассмотрена отправка файлов на сервер, но думаю, внимательно изучив материал выше, а так же прилагающийся исходный код, можно без проблем сделать и это, пусть дальнейшее изучение протокола FTP со стороны сервера будет вашим «домашним заданием». Исходник здесь. |