FTP-протокол + WinSocks на примере простого FTP-клиента (зеркала)

Дата публикации 13 июн 2004

FTP-протокол + WinSocks на примере простого FTP-клиента (зеркала) — Архив WASM.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) которые могут быть посланы серверу, но это совсем не значит что сервер все их будет поддерживать. Приведу пример наиболее часто используемых команд.

USER имя пользователя

Указывает имя пользователя

PASS пароль

Указывает пароль пользователя

LIST список файлов

Запрос списка файлов

PORT n1,n2,n3,n4,n5,n6

Указание IP и порта для соединения данных

RETR имя файла

Получить файл с сервера

STOR имя файла

Положить файл на сервер

TYPE тип

Тип передаваемых данных

QUIT

Отключение от сервера

ABOR

Отмена предыдущее команды. Прекращение передачи данных.

При получении запроса сервер, по тому же управляющему соединению отправляет ответ на него. Ответ сервера состоит из трех символов (цифр) в формате ASCII, за которыми следует не обязательный текст, обычно поясняющий цифирный код ответа, за этим пояснением следуют неизменные CR, LF. Ответ например может быть таким: 226 File send OK. – в этом примере сервер сообщает нам о том, что файл отправлен с его стороны (что совсем не означает, что он уже получен со стороны клиента). Первая цифра отклика сервера наиболее значимая, и дает однозначное представление о том как выполнилась (или не выполнилась) команда. Значения могут быть такими:

1хх

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

2хх

Команда выполнена. Сервер находится в ожидании следующей.

3хх

Команда выполнена, но для продолжения необходима еще одна команда

4хх

Команда не была выполнена, необходимо подождать и повторить команду

5хх

Команда не была выполнена и не будет выполнена при повторе.

По второй цифре отклика можно судить о том, какая ситуация привела к возникновению отклика:

x0x

Ошибка синтаксиса.

x1x

Информация.

x2x

Отклик относится к состоянию управляющего или соединению данных.

x3x

Отклик относится к аутентификации пользователя или состоянию бюджета.

x4x

Не определенно.

x5x

Отклик относится к состоянию файловой системы.

Ну и наконец третья цифра отклика несет в себе дополнительную информацию.

Следует обратить особое внимание на то, что хотя на большую часть команд сервер отвечает одним откликом, есть и широко используются команды, в ответ на которые сервер генерирует несколько откликов. При этом первая цифра первого отклика будет «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» - ссылка и т.д.);
xxxxxxxxx – атрибуты защиты файла;
user – пользователь, владелец файла;
group – группа владельца;
size -  размер элемента;
mm – месяц создания элемента в текстовом виде, например «jul»;
dd – день месяца создания элемента;
yytt – здесь может быть год или время создания элемента;
name – имя элемента (файла, каталога, ссылки);
[  ] – один или более пробелов.

Да, между этими элементами может быть различное количество пробелов, надо сказать спасибо, что в различных реализациях серверов оставили одно количество значимых столбцов, поэтому при анализе таблицы файлов следует это учитывать. Стоит еще учесть такую вещь, что не всегда первая строка из таблицы есть значимая строка, несущая информацию о первом элементе каталога. В некоторых реализациях 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 – произошло или не произошло событие, и события касающиеся сокетов, которые приходят на процедуру главного окна).

Итак, начнем. При создании основного окна приложения мы проводим основную инициализацию программы, поясню основные моменты:

Код (Text):
  1.  
  2. call    VirtualAlloc,ebx,1024000,MEM_COMMIT+MEM_RESERVE,PAGE_READWRITE
  3. mov     ReciveDataBufferOffset,eax
  4. call    VirtualAlloc,ebx,10240,MEM_COMMIT+MEM_RESERVE,PAGE_READWRITE
  5. mov     ReciveCommandBufferOffset,eax

Здесь выделяется память под буфер приема файла (1 Мб) и под буфер команд (10 Кб).

Код (Text):
  1.  
  2. call    CreateEventA,ebx,ebx,ebx,ebx
  3. mov   HDataReciveEvent,eax
  4. ……

Создаются объекты event (события) более подробно о назначении событий позже.

Код (Text):
  1.  
  2. call    CreateThread,ebx,ebx,offset ReciveThread,offset ReciveDataThreadStruc, \
  3.         NORMAL_PRIORITY_CLASS,offset ThreadID_data
  4. call    CreateThread,ebx,ebx,offset ReciveThread,offset ReciveCommandThreadStruc,\
  5.         NORMAL_PRIORITY_CLASS,offset ThreadID_command

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

Код (Text):
  1.  
  2. call    gethostname, offset HostName,64
  3. call    gethostbyname,offset HostName
  4. …..
  5. mov     PortInPort,esi
  6. ret     0

Смысл строк выше в получении IP-адреса нашего хоста, небольшом преобразовании и записи его в отдельное место, адрес хоста нам потребуется для выполнения команды PORT.

На этом процесс начальной инициализации заканчивается, и программа находится в состоянии ожидания команды пользователя. Давайте посмотрим что происходит при нажатии пользователем кнопки «соединиться».

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

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

Код (Text):
  1.  
  2. - создаем сокет;
  3. call    socket, AF_INET, SOCK_STREAM, IPPROTO_TCP
  4. mov     ReciveCommandSock,eax
  5. - выбираем неблокирующий режим для сокета, указываем что хотим получать
  6.   сообщения о получении новых данных на сокет, а так же о успешном его
  7.   приконекчивании.
  8. call    WSAAsyncSelect, ReciveCommandSock, newhwnd, WM_COMMANDSOCK,FD_READ+FD_CONNECT
  9. - получаем информацию об удаленном хосте и конектимся к нему
  10. …..
  11. call    connect,ReciveCommandSock,offset sockaddr_in,16
  12. - ждем получения события FD_CONNECT, когда оно приходит в процедуру главного окна
  13.   обработчик с помощью  call    SetEvent,HWaitConnectEvent устанавливает событие,
  14.   чего мы и ожидаем в следующей строке, если событие не будет установлено в течении
  15.   5 секунд, выводим сообщение об ошибке и заканчиваем сеанс связи.
  16. call    WaitForSingleObject,HWaitConnectEvent,5000
  17. call    ResetEvent,HWaitConnectEvent
  18. - так как после соединения сервер должен послать нам приветствие, ждем его еще 5
  19.   секунд, если оно не поступило - выходим. Процедура WaitAnswerRecive описана ниже.
  20. call    WaitAnswerRecive,5000
  21. or      eax,eax
  22. jnz     errorwithregisration
  23.  
  24. - входным параметром к функции является интервал, в течении которого, функция будет
  25.   ждать ответа сервера, если за указанный интервал ответ не будет получен, функция
  26.   выводит сообщение об ошибке и завершается с ненулевым значением регистра eax.
  27. WaitAnswerRecive proc TimeToWait:dword
  28.         call    WaitForSingleObject,HWaitCommandEvent,TimeToWait
  29. - ожидаем возникновение события HWaitCommandEvent, которое устанавливается в потоке
  30.   получения данных по управляющему соединению, в случае успешного получения данных.
  31.         or      eax,eax
  32.         jz      NoTimeOutGet
  33.         call    MessageBoxA,newhwnd,offset ErrTimeOutCommand,offset ErrorCap,40h
  34.         call    ResetEvent,HWaitCommandEvent
  35. - сбросили событие HWaitCommandEvent т.к. истек таймаут, и событие осталось в
  36.   сигнальном состоянии.
  37. NoTimeOutGet:
  38.         ret
  39. WaitAnswerRecive endp

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

Универсальный трэд для получения данных по управляющему и соединению данных приведен ниже.

Код (Text):
  1.  
  2. - в качестве параметра поток получает адрес структуры ReciveDataThreadStruc
  3.   или ReciveCommandThreadStruc в зависимости от предназначения трэда.
  4. Структура для ReciveCommandThreadStruc такая:
  5. - хэндл события по которому трэд активизируется;
  6. HCommandReciveEvent           dd ?  
  7. - хэндл события, которое устанавливает трэд если все данные успешно получены;
  8. HWaitCommandEvent             dd ?
  9. - адрес буфера получения данных;
  10. ReciveCommandBufferOffset     dd ?
  11. - здесь содержится общее количество полученных данных;
  12. BytesCommandRecived           dd 0
  13. - и наконец, с какого сокета надо получить данные;
  14. ReciveCommandSock             dd ?
  15.  
  16. ReciveThread     proc parametr:dword
  17.         mov      edi,parametr
  18. InfinityLoop:
  19. - ждем возникновения события, что данные можно принимать;
  20.         call     WaitForSingleObject,dword ptr [edi],-1
  21. - настраиваем esi на место, куда данные будут считаны - адрес буфера+количество
  22.   полученных ранее;
  23.         mov      esi,[edi+8]
  24.         add  esi,[edi+12]
  25. - получаем не более 4096 байт;
  26.         call     recv,dword ptr [edi+16],esi,4096,0
  27. - прибавляем к уже полученным ранее, полученные в данный момент;
  28.         add      [edi+12],eax
  29. - в ebx заносим хэндл события, которое надо установить, если получены все нужные данные;
  30.         mov      ebx,[edi+4]
  31. - если мы получаем данные по соединению данных то идем к проверке конца получения
  32.   данных по соединению данных, иначе - проверяем получили ли мы все данные по
  33.   управляющему соединению;
  34.         cmp      edi,offset ReciveDataThreadStruc
  35.         je   comparefordata
  36. - по управляющему соединению получены все данные в случае если последние байты ответа
  37.   0dh, 0ah, что мы и проверяем;
  38.         mov      eax,[edi+12]
  39.         mov      esi,[edi+8]
  40.         cmp      byte ptr [esi+eax-1],10
  41.         je       short CallEvent
  42.         jmp      InfinityLoop
  43. comparefordata:
  44. - по соединению данных получено все, если количество полученных байт = длине файла;
  45.         mov      eax,[edi+12]
  46.         cmp      FileLenght,eax
  47.         jne      InfinityLoop
  48. CallEvent:
  49. - в случае если все данные получены выставляем соответствующее событие;
  50.         call     SetEvent,ebx
  51.         jmp      InfinityLoop
  52. ReciveThread     endp

Вернемся теперь к основному потоку, мы успешно получили ответ от сервера, в том что он готов к приему команд, теперь мы можем передавать ему команды, в данной реализации за отправку команд серверу отвечает функция SendCommandInSocket, в основном потоке далее мы вызываем эту функцию для отправки серверу последовательно команд: USER, PASS, TYPE, CWD, PORT и LIST. Сама функция выглядит так:

Код (Text):
  1.  
  2. - принимает аргументами сокет, в который нужно передать команду, и смещение на буфер,
  3.   в котором содержится команда;
  4. SendCommandInSocket proc uses ebx ecx esi edi, hSocket:dword, OutBufOffset:dword
  5. - сначала определяем длину команды;
  6.         mov     edi,OutBufOffset
  7.         push    edi
  8.         mov     eax,0ah
  9.         mov     ecx,100
  10.         repne   scasb
  11.         sub     edi,OutBufOffset
  12.         mov     ecx,edi
  13.         pop     esi
  14.         push    edi
  15. - переносим команду в буфер для приема ответов для сервера, сделано это для того,
  16.   что бы потом его можно было сохранить в удобочитаемом виде лога;
  17.         mov     edi,ReciveCommandBufferOffset
  18.         add     edi,BytesCommandRecived
  19.         rep     movsb
  20.         pop     edi
  21.         add     BytesCommandRecived,edi
  22. - посылаем команду в сокет;
  23.         call    send,hSocket,OutBufOffset,edi,ebx
  24. - ждем ответа сервера, с помощью уже описанной выше функции WaitAnswerRecive;
  25.         mov     eax,5001
  26. Wait2Answer:
  27.         dec     eax
  28.         push    eax
  29.         call    WaitAnswerRecive
  30.         or      eax,eax
  31.         jnz     ErrorProcessed
  32. - ответ получен, ищем первый байт ответа, мы его ИЩЕМ, а не просто используем
  33.   смещение на конец предпоследнего полученого сообщения, потому, что нами может
  34.   быть получено в одном сеансе получения данных обновременно два ответа от сервера.
  35.   Уясните себе этот момент.
  36.         mov     edi,ReciveCommandBufferOffset
  37.         mov     ecx,BytesCommandRecived
  38.         dec     ecx
  39.         dec     ecx
  40.         add     edi,ecx
  41.         mov     al,0ah
  42.         std
  43.         repne   scasb
  44.         cld
  45.         xor     eax,eax
  46. - проверяем первый символ ответа;
  47.         mov     cl,[edi+2]
  48.         cmp     cl,'1'
  49. - если это "1" то ждем еще одного сообщения от сервера
  50.         jz      Wait2Answer
  51.         cmp     cl,'3'
  52. - если ответ больше "3" - произошла ошибка;
  53.         jna     NoErrorProcessed
  54.         call    MessageBoxA,newhwnd,edi,offset ErrorCap,40h
  55. ErrorProcessed:
  56.         xor     eax,eax
  57.         inc     eax
  58. NoErrorProcessed:
  59.         ret
  60. SendCommandInSocket endp

Необходимо учесть еще одну вещь – перед отправкой команды PORT, нам надо создать слушающий сокет, это мы делаем с помощью вызова процедуры CreateListenSock.

Код (Text):
  1.  
  2. CreateListenSock proc
  3.         pushad
  4. - создаем сокет;
  5.         call     socket, AF_INET, SOCK_STREAM, IPPROTO_TCP
  6.         mov      datasock,eax
  7. - переводим его в не-блокирующий режим, указывая, что хотим получать в процедуре
  8.   окна сообщения о подтверждении приконекчивания к этому сокету, о поступлении
  9.   новых данных, и о закрытии соединения;
  10.         call     WSAAsyncSelect, datasock, newhwnd, WM_DATASOCK, FD_ACCEPT+FD_READ+FD_CLOSE
  11. - ввязываем сокет с локальным адресом;
  12.         mov      sin_port,0 ; указываем ноль, в этом случае система даст нам
  13.                             ; первый свободный порт
  14.         mov      sin_family,AF_INET
  15.         mov      sin_addr,INADDR_ANY
  16.         call     bind, datasock, offset sockaddr_in, 16
  17. - получаем инфу о сокете;
  18.         call     getsockname,datasock,offset sockaddr_in,offset szSockaddr_in
  19. - преобразуем номер порта к нормальному виду;
  20.         xor      eax,eax
  21.         mov      ax,sin_port
  22.         call     ntohs,eax
  23.         push     eax
  24.         shr      eax,8
  25. - дальше преобразуем номер порта в символы ASCII;
  26.         call     DECtoASCII,eax,PortInPort
  27. - и записываем их в шаблон команды PORT
  28.         mov      al,','
  29.         stosb
  30.         pop      eax
  31.         and      eax,0ffh
  32.         call     DECtoASCII,eax,edi
  33.         mov      ax,0a0dh
  34.         stosw
  35.         mov      esi,PortInPort
  36. - слушаем сокет;
  37.         call     listen, datasock, 1
  38.         popad
  39.         ret
  40. CreateListenSock endp

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

Код (Text):
  1.  
  2. - получает в качестве аргумента в течении которого мы буде ждать ОКОНЧАНИЯ получения
  3.   данных, после того, как соединение будет закрыто со стороны сервера.
  4. WaitTransferComplete proc uses ecx edi, TimeToWaitEndTransfer:dword
  5. WaitProgress:
  6. - ждем установления события закрытия соединения со стороны сервера, оно устанавливается
  7.   в главной процедуре окна;
  8.         call    WaitForSingleObject,HWaitCloseEvent,-1
  9. - дальше ждем, события успешного получения данных нашим потоком, которое в нем и
  10.   устанавливается;
  11.         call    WaitForSingleObject,HWaitDataEvent,TimeToWaitEndTransfer
  12.         or      eax,eax
  13.         jz      CloseDataSocks
  14. - был таймаут, и если мы получаем директорию, то выходим без ошибки, т.к. при получении
  15.   директории у нас всегда таймаут, потому, что мы заранее не знаем количество получаемых
  16.   байт и поток получения не может установить событие успешного получения;
  17.         cmp     TimeToWaitEndTransfer,1000 ;если ждем каталог
  18.         jz      CloseDataSocks
  19.         call    MessageBoxA,newhwnd,offset ErrTimeOutCommand,offset ErrorCap,40h
  20.  
  21. CloseDataSocks:
  22. - сбрасываем событие успешного получения;
  23.         call    ResetEvent,HWaitDataEvent
  24. - закрываем соединение со своей стороны;
  25.         call    closesocket,ReciveDataSock
  26.         call    closesocket,datasock
  27.         ret
  28. WaitTransferComplete endp

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

Заключение.

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

Исходник здесь. © Mad_C


0 930
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532