Пример FTP-клиента на Delphi
Рубрика: Delphi -> Программирование -> Журнал Хакер -> Статьи
Метки: delphi | ftp | rfc | sockets | программирование
Просмотров: 15903
FTP-клиент - одна из самых часто используемых утилит в повседневной жизни продвинутого пользователя. Закачать html-странички, слить warez с сервака, качнуть фильмов в локалке – работа FTP-клиента. Стоимость таких программ на рынке колеблется от 10 до 100 баксов. Скажи, тебе не хочется срубить столько же, да еще и не особо напрягаясь? Если ты решительно ответил «Да», то усаживайся поудобнее и читай статью, познавая секреты программирования FTP-клиентов. Никаких компонентов, никаких чужих библиотек – все свое, родное!
Теория FTP-протокола
File transfer protocol (протокол передачи файлов) берет свое начало в 70-х. Именно в то время возникла необходимость в создании протокола, который смог бы решить проблему передачи файлов с одного компьютера на другой. На протяжении 30 лет протокол неоднократно менялся и совершенствовался. Последняя спецификация приведена в RFC 959 (http://athena.vvsu.ru/docs/tcpip/rfc/rfc959.txt). Я очень рекомендую тебе скачать этот документ и хорошенько с ним ознакомиться, поскольку только в нем ты найдешь ответы на вопросы, которые могут возникнуть у тебя при написании полноценного FTP-клиента. Как и большинство сетевых протоколов (HTTP, POP3, SMTP и др.), FTP работает поверх TCP. В отличие от всех перечисленных протоколов, FTP обладает одной интересной особенностью. Для полноценной работы ему нужно не одно, а целых два соединения:
Управляющее – используется на протяжении всего сеанса связи. По этому соединению отправляются все команды для FTP-сервера и возвращаются результаты их выполнения.
Соединение для передачи данных – создается в момент, когда нужно получить/отправить данные. После передачи данных соединение должно завершиться.
Установка соединения
Давай подробно рассмотрим процесс установки связи с FTP-сервером. Чтобы выполнить какую-либо команду, клиенту нужно установить управляющее соединение. Сделать это можно, подключившись на 21-й порт (порт по умолчанию у большинства FTP-серверов) удаленного компьютера. Как только соединение будет установлено, FTP-сервер отправит приветствие. Обычно в нем содержится название используемого сервера и другие данные. Для продолжения работы клиенту необходимо пройти авторизацию – отправить серверу свой логин и пароль. Логин передается командой USER [имя пользователя], а пароль - командой PASS [твой пароль]. Если введенные данные окажутся верными, то сервер радостно отправит сообщение с кодом «230 OK». Текст этого сообщения означает, что авторизация успешно пройдена и можно отправлять команды. Наглядный пример установки управляющего соединения ты можешь увидеть на рисунке, где изображено, как с помощью telnet я подсоединился к ftp-серверу.
После установки управляющего соединения можно посылать команды для получения списка файлов или же для копирования самих файлов, но перед этим необходимо установить второе соединение – для передачи данных. Я уже много раз говорил о соединении для передачи данных, но до сих пор ничего не сказал о принципах его создания. Первым делом программе-клиенту нужно открыть любой свободный порт. Затем удаленному серверу необходимо послать данные в специальном формате, которые будут включать IP-адрес (твой реальный IP) и порт (тот, который ты и открывал для подключения). Все эти данные отправляются с помощью команды PORT.
Если ты читал внимательно, то, наверное, обратил внимание на упоминание о специальном формате. Чтобы долго не объяснять, покажу на простом примере, а недостающую теорию ты всегда сможешь почерпнуть из RFC-959. Допустим, что у клиента в качестве IP-адреса у нас 192.168.0.1, а открытым портом является 31337-й. Тогда команда PORT будет выглядеть следующим образом: «PORT 192,168,0,1,122,105». Думаю, разглядеть IP-адрес в этой строке тебе удалось, а вот при поиске порта, вероятнее всего, возникли небольшие проблемы. По спецификации RFC, номер порта нужно передавать двумя числами. В нашем примере это 122 и 105. Теперь ясно, для чего эти цифры нужны, но не ясно, какое отношение они имеют к 31337-му порту. Ответ, как обычно, находится в RFC. Согласно этому документу, синтаксис команды PORT выглядит следующим образом: «PORT n1, n2, n3, n4, n5, n6», где n1-n4 – разделенный запятыми IP-адрес, а n5*256+n6 – номер порта. Теперь ясно? Нет? Ок! Бери в руки старый калькулятор и приготовься выполнить простейшие математические операции. Число 122 умножь на 256, в результате получишь 31232. Теперь к результату прибавь 105. Если ты правильно нажимал на кнопки калькулятора, то у тебя должно получиться число 31337 – порт, который мы и задавали.
После отправки команды PORT сервер проверит принятые данные. И если все тип-топ, то будет создан канал для передачи данных (удаленный сервер установит соединение с твоей программой), а значит, ты сможешь отправить команду LIST, и тебе придет список файлов заданной директории. После передачи данных сервер сам завершит второе соединение.
Первый шаг на пути к FTP-клиенту
Я уже познакомил тебя с теорией FTP-протокола, теперь ты представляешь себе принцип работы программ клиентов, но знаний для реализации своего клиента все же мало. Что ж, заполним этот пробел – перейдем к рассмотрению сетевых функций.
Страшный WinSock API
В Delphi есть готовые компоненты, которые существенно облегчают процесс программирования FTP-клиента, но, к сожалению, при их использовании зачастую сталкиваешься со всякого рода ограничениями. Лучше написать свой FTP-клиент полностью самому, для этого мы воспользуемся сетевыми функциями Windows. Научившись ими пользоваться, ты сможешь написать не только FTP-клиент, но и любое другое сетевое приложение. Перед вызовом сетевых функций нужно инициализировать сетевую библиотеку. Для этой цели необходимо воспользоваться функцией WSAStartup, которая описана в модуле WinSock.pas следующим образом:
WSAStartup (wVersionRequested: word; var WSAData:TWSAData):Integeder; stdcall;
Функции мы передаем два параметра:
Получить код ошибки можно вызовом функции WSAGetLastError, о которой я расскажу чуть позже, а сейчас мы посмотрим, как можно освободить инициализированную библиотеку WinSock.
function WSACleanup:Integer; stdcall;
Функции ничего не нужно передавать в качестве параметров, поскольку все, что она делает, – это освобождает инициализированную библиотеку. По идее, ее можно не вызывать, так как теоретически после завершения приложения ОС должна это сделать сама, но, не надеясь на программистов из MS, лучше освободить библиотеку самостоятельно.
function socket (af:integer; type:integer; protocol:integer):TSocket; stdcall;
Функция socket после своего успешного выполнения возвращает объект типа TSocket, с помощью которого мы и будем работать с сетью. В качестве параметров этой функции необходимо передать:
function bind (S:TSocket; var addr:TSockAddr; namelen:Integer):integer; stdcall;
Для связывания локального сетевого адреса с сокетом мы должны воспользоваться функцией bind. Это функция пригодится нам для организации канала передачи данных. Давай рассмотрим ее параметры: 1) s – созданный с помощью функции socket сокет; 2) addr – указатель на структуру TSockAddr; 3) размер структуры TSockAddr. При успешном выполнении функция вернет 0, в случае ошибки - SOCKET_ERROR. Определить код ошибки можно с помощью функции WSAGetLastError(). В качестве второго параметра нам необходимо передавать указатель на структуру TSockAddr. При программировании сетевых приложений это структура используется повсеместно, поэтому ты должен ее знать так же хорошо, как и дырки в своих зубах. Описывается структура следующим образом:
Структура SockAddr_IN
TSockAddrIN = sockaddr_in;
SockAddr_In = record
sin_family: u_short;
sin_port: u_short;
sin_addr:TInAddr;
sin_zero: array[0..7] of Char;
end;
В структуре присутствуют следующие параметры:
function listen (s:TSocket; backlog:Integer):Integer; stdcall;
После успешного выполнения функции bind можно приступать к прослушке порта в ожидании гостей. Для этого, собственно, и создана функция listen. Функции нужно передать всего два параметра: 1) s – сокет, который связали с локальным адресом; 2) backlog – максимальное количество запросов на ожидание подключения. Если функция выполнится успешно, то она вернет 0, в противном случае - ошибки, коды которых можно узнать из модуля WinSock.pas.
function Accept (S:TSocket; addr: PSockAddr; AddrLen:PInteger):TSocket; stdcall;
Как только клиент сделал попытку подключиться (если мы выступаем сервером), необходимо принять соединение. Этим нехитрым делом и занимается функция Accept. В качестве параметров функции нужно передать: 1) s – сокет; 2) addr - указатель на структуру SockAddr; 3) AddrLen - размер структуры SockAddr. В случае успешного выполнения функция Accept возвращает указатель на сокет, через который мы можем работать с подключившимся клиентом.
function connect (s:TSocket; var name:TSockAddr; namelen:integer):integer; stdcall;
Думаю, рассказывать про предназначение функции connect не нужно, поскольку все и так понятно из ее названия. Из параметров функции передаются: 1) сокет; 2) структура sockAddr, в которой содержатся данные для подключения к серверу (протокол, адрес, порт); 3) размер структуры sockAddr. Как и большинство сетевых функций, в случае успешного выполнения эта функция вернет нам 0.
function send (s:TSocket; var Buf; len:Integer; flags:integer):Integer; stdcall;
Для отправки данных в наборе Winsock есть несколько функций, среди которых имеется функция с именем send. Рассмотрим ее параметры: 1) s - сокет, который будет использоваться для отправки данных; 2) buf – буфер, в котором содержатся данные для отправки; 3) len – размер буфера; 4) flags – флаги, влияющие на метод отправки. Мы можем просто указать 0. Если функция выполнится успешно, то есть сможет отправить данные, то в качестве результата она вернет количество фактически отправленных данных, в противном случае - ошибку.
function recv (s:TSocket; var buf; len:integer; flags:integer):Integer; stdcall;
Функция recv исполняет противоположную функции send роль – она принимает данные. Ее параметры полностью идентичны параметрам функции send, поэтому не будем тратить время и рассматривать их еще раз.
function CloseSocket (s:TSocket):integer; stdcall;
Функция CloseSocket необходима для закрытия сокета. По правилам хорошего тона ее принято вызвать после функции shutdown, но программисты пренебрегают этими правилами и вызывают ее сразу, забывая про shutdown.
function WSAGetLastError:integer; stdcall;
С помощью этой полезной функции можно получить код возникшей ошибки при вызове какой-либо другой функции. При вызове WSAGetLastError() сразу же вернет код последней возникшей в системе ошибки. В последствии его можно проанализировать и довести до пользователя.
FTP-клиент – делаем это!
Ну вот и настал тот момент, когда мы готовы опробовать наши теоретические знания на практике. Запускай Delphi и создавай новый проект типа Application. На форму брось один компонент TRichEdit и один TMainMenu. Поле TRichEdit у меня растянуто по всей форме, а в TMainMenu созданы элементы меню:
1. Соединение
Подключиться
Отключиться
Настройки
2. Команды
LIST
CWD
Мою готовую форму ты можешь увидеть на рисунке. По нажатию на кнопку «Настройки» будет вызываться дополнительная форма, в которой нужно будет ввести имя пользователя, пароль, сервер и порт. Ее вид также представлен на рисунке.
Кодинг
Простенький дизайн нашего FTP-клиента готов, поэтому приступаем к приготовлению горячей начинки – написанию кода. Первым делом не забудь подключить к нашему проекту модуль Winsock.pas, иначе вызов сетевых функций будет невозможен. Теперь надо объявить все необходимые переменные в разделе private:
_wsaData:TWSADATA;
_clientSocket:TSocket;
_serverSocket:TSocket;
_clientAddr:sockaddr_in;
_serverAddr:sockaddr_in;
_mode:integer;
_tempSocket:TSocket;
Об их предназначении ты узнаешь по ходу рассмотрения примера. При описании сетевых функций я говорил, что перед их использованием нужно инициализировать сетевую библиотеку. Инициализацию сетевой библиотеки я делаю во время создания формы, а ее освобождение - во время закрытия окна. Если при этом у тебя возникли проблемы, то открывай исходник на нашем DVD и сравнивай. После инициализации сетевой библиотеки можно попытаться соединиться с удаленным сервером. По нажатию кнопки «Соединиться» у меня вызывается самописная процедура _connect(), которой нужно передать все необходимые для подключения данные (адрес сервера, логин, пароль, порт). Код этой процедуры ты можешь увидеть во врезке «Соединяемся с сервером».
Давай-ка подробно рассмотрим содержимое кода установки соединения с удаленным сервером, отображенного в этой врезке. В самом начале я создаю новый сокет. Ты уже знаешь, для того чтобы создать новый сокет, нужно воспользоваться функцией SOCKET, которая после выполнения возвратит указатель на созданный сокет. После вызова функции SOCKET, я проверяю, а не возникла ли ошибка. Если да, то я запускаю самописную процедуру GetError, передав ей название вызываемой функции. Процедура GetError попытается получить код ошибки и в конце концов проинформирует пользователя, напечатав в TRichEdit соответствующий текст. Код процедуры GetError ты можешь посмотреть, открыв исходник примера, любезно дожидающийся тебя на нашем диске. Если ошибки не возникло, то можно начинать готовиться к соединению с удаленным сервером. Как ты должен помнить, чтобы установить соединение с сервером, нужно вызвать функцию connect, которой необходимо передать структуру типа sockaddr_in. Ну а чтобы ее передать, ее нужно заполнить, что я и делаю. После заполнения структуры, я вызываю функцию WSAAsyncSelect(). Эта функция устанавливает асинхронный режим для выбранного сокета и заставляет Windows генерировать сообщения для сетевых событий. Таким образом, нам достаточно указать сообщение, которое должно приходить окну нашего приложения при возникновении события на определенном сокете.
На первый взгляд может показаться, что этот метод сложен в реализации. На самом деле это не так, и через несколько минут ты в этом убедишься, но сначала давай взглянем на параметры, которые нужно передать функции:
Перед тем как привести код процедуры, я хотел бы объяснить тебе, что собой представляет WM_MYSOCKMESS. В нашем примере это константа, которая объявлена мной и равна WM_USER+1. Что такое WM_USER? Это число. Все числа, меньше этого, могут уже использоваться системой, поэтому, чтобы не было конфликтов, нужно просто использовать это число +1.
Немного отвлечемся от рассмотрения функции WSAAsyncSelect() и вернемся к разбору кода установки соединения с удаленным сервером. После WSAAsyncSelect() вызывается функция Connect, которая начинает устанавливать соединение с удаленным сервером. По окончанию выполнения функции Connect я приступаю к отправке данных для прохождения авторизации (вспоминай теорию). Отправка данных реализована в самописной функции _send(). В качестве параметров ей нужно передать сокет, через который будут отправлены данные, и сами данные.
Теперь вернемся к интересной функции WSAAsyncSelect(). Я уже говорил, что для перехвата нужного события необходимо объявить специальную процедуру. В примере я объявил ее в разделе private следующим образом:
procedure NetMSG (var M:TMessage); message WM_MYSOCKMESS;
Код тела процедуры ты можешь увидеть в соответствующей врезке. Как видно из описания, в процедуру передается структура типа TMessage. В этой структуре имеется несколько параметров, но нас будут интересовать только два: WParam и LParam. В первом хранится дескриптор сокета, на котором произошло событие, а во втором - его тип. Для проверки типа я использую управляющую структуру CASE, в которой и проверяю интересующие меня события.
При возникновении события FD_READ вызывается процедура _recv, которая и выполняет чтение данных с определенного сокета. Код процедур _recv и _send (для отправки данных) ты можешь увидеть в исходнике примера.
Тестирование
Наш пример наполовину готов, теперь самое время протестировать его работоспособность. Попробуем скомпилить наше приложение и подцепиться к какому-нибудь ftp-серверу. Результаты моего тестирования ты можешь увидеть на рисунке. Для теста я подцепился к серверу своего хостера и успешно прошел авторизацию.
Disconnect
Рассмотреть весь код FTP-клиента в рамках одной статьи просто невозможно. Поэтому разбираться с установлением второго соединения (для передачи данных) тебе придется самостоятельно. Сильно по этому поводу не переживай. Ты всегда можешь заглянуть на наш диск и посмотреть исходник FTP-клиента, в котором я уже реализовал получения списка файлов определенной директории. Весь исходник я постарался максимально прокомментировать, поэтому с пониманием возникнуть проблем не должно. Но, если что, ты всегда можешь задать мне вопрос на мыло a@iantonov.me
Код тела процедуры
procedure TForm1.NetMSG(var M: TMessage);
begin
case m.LParam of
FD_ACCEPT:
begin
_tempSocket:=accept(m.WParam, nil, nil);
WSAAsyncSelect(_tempSocket, handle, WM_MYSOCKMESS, FD_READ+FD_CLOSE);
end;
FD_READ: _recv(m.WParam);
FD_CLOSE: CloseSocket(M.WParam);
end;
end;
Соединяемся с сервером
procedure TForm1._Connect(ftp_server, ftp_port, user_name,
user_pass: string);
begin
_clientsocket:=SOCKET(AF_INET, SOCK_STREAM, IPPROTO_IP);
if _clientSocket=INVALID_SOCKET then
begin
GetError('Socket');
Exit;
end;
_clientAddr.sin_family:=AF_INET;
_clientAddr.sin_addr.S_addr:=htonl(INADDR_ANY);
_clientAddr.sin_addr.S_addr:=inet_addr(pchar(ftp_server));
_clientAddr.sin_port:=htons(StrToInt(ftp_port));
WSAAsyncSelect(_clientSocket, handle, WM_MYSOCKMESS, FD_READ);
Connect(_clientsocket, _clientaddr, sizeof(_clientaddr));
Sleep(100);
_send(_clientsocket, 'USER '+user_name);
_send(_clientsocket, 'PASS '+user_pass);
_send(_clientSocket, 'FEAT');
Статья опубликована журнале "Хакер" (http://xakep.ru). Июнь 2007 г. Издательство GameLand.
Ссылка на опубликованную статью сайта издания: http://www.xakep.ru/magazine/xa/102/110/1.asp
Ссылка на журнал: http://www.xakep.ru/magazine/xa/102/