FTP-протокол + WinSocks на примере простого FTP-клиента (зеркала) — Архив WASM.RU

Все статьи

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 – произошло или не произошло событие, и события касающиеся сокетов, которые приходят на процедуру главного окна).

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

call    VirtualAlloc,ebx,1024000,MEM_COMMIT+MEM_RESERVE,PAGE_READWRITE
mov     ReciveDataBufferOffset,eax
call    VirtualAlloc,ebx,10240,MEM_COMMIT+MEM_RESERVE,PAGE_READWRITE
mov     ReciveCommandBufferOffset,eax

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

call    CreateEventA,ebx,ebx,ebx,ebx
mov   HDataReciveEvent,eax
……

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

call    CreateThread,ebx,ebx,offset ReciveThread,offset ReciveDataThreadStruc, \
        NORMAL_PRIORITY_CLASS,offset ThreadID_data
call    CreateThread,ebx,ebx,offset ReciveThread,offset ReciveCommandThreadStruc,\
        NORMAL_PRIORITY_CLASS,offset ThreadID_command

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

call    gethostname, offset HostName,64
call    gethostbyname,offset HostName
…..
mov     PortInPort,esi
ret     0

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

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

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

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

- создаем сокет;
call    socket, AF_INET, SOCK_STREAM, IPPROTO_TCP
mov     ReciveCommandSock,eax
- выбираем неблокирующий режим для сокета, указываем что хотим получать 
  сообщения о получении новых данных на сокет, а так же о успешном его 
  приконекчивании.
call    WSAAsyncSelect, ReciveCommandSock, newhwnd, WM_COMMANDSOCK,FD_READ+FD_CONNECT
- получаем информацию об удаленном хосте и конектимся к нему
…..
call    connect,ReciveCommandSock,offset sockaddr_in,16
- ждем получения события FD_CONNECT, когда оно приходит в процедуру главного окна 
  обработчик с помощью  call    SetEvent,HWaitConnectEvent устанавливает событие, 
  чего мы и ожидаем в следующей строке, если событие не будет установлено в течении 
  5 секунд, выводим сообщение об ошибке и заканчиваем сеанс связи.
call    WaitForSingleObject,HWaitConnectEvent,5000
call    ResetEvent,HWaitConnectEvent
- так как после соединения сервер должен послать нам приветствие, ждем его еще 5 
  секунд, если оно не поступило - выходим. Процедура WaitAnswerRecive описана ниже.
call    WaitAnswerRecive,5000
or      eax,eax
jnz     errorwithregisration

- входным параметром к функции является интервал, в течении которого, функция будет 
  ждать ответа сервера, если за указанный интервал ответ не будет получен, функция 
  выводит сообщение об ошибке и завершается с ненулевым значением регистра eax.
WaitAnswerRecive proc TimeToWait:dword
        call    WaitForSingleObject,HWaitCommandEvent,TimeToWait
- ожидаем возникновение события HWaitCommandEvent, которое устанавливается в потоке 
  получения данных по управляющему соединению, в случае успешного получения данных.
        or      eax,eax
        jz      NoTimeOutGet
        call    MessageBoxA,newhwnd,offset ErrTimeOutCommand,offset ErrorCap,40h
        call    ResetEvent,HWaitCommandEvent
- сбросили событие HWaitCommandEvent т.к. истек таймаут, и событие осталось в 
  сигнальном состоянии.
NoTimeOutGet:
        ret
WaitAnswerRecive endp

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

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

- в качестве параметра поток получает адрес структуры ReciveDataThreadStruc 
  или ReciveCommandThreadStruc в зависимости от предназначения трэда. 
Структура для ReciveCommandThreadStruc такая:
- хэндл события по которому трэд активизируется;
HCommandReciveEvent           dd ?   
- хэндл события, которое устанавливает трэд если все данные успешно получены;
HWaitCommandEvent             dd ? 
- адрес буфера получения данных;
ReciveCommandBufferOffset     dd ?
- здесь содержится общее количество полученных данных;
BytesCommandRecived           dd 0
- и наконец, с какого сокета надо получить данные;
ReciveCommandSock             dd ?

ReciveThread     proc parametr:dword
        mov      edi,parametr
InfinityLoop:
- ждем возникновения события, что данные можно принимать;
        call     WaitForSingleObject,dword ptr [edi],-1
- настраиваем esi на место, куда данные будут считаны - адрес буфера+количество 
  полученных ранее;
        mov      esi,[edi+8]
        add	 esi,[edi+12]
- получаем не более 4096 байт;
        call     recv,dword ptr [edi+16],esi,4096,0
- прибавляем к уже полученным ранее, полученные в данный момент;
        add      [edi+12],eax
- в ebx заносим хэндл события, которое надо установить, если получены все нужные данные;
        mov      ebx,[edi+4]
- если мы получаем данные по соединению данных то идем к проверке конца получения 
  данных по соединению данных, иначе - проверяем получили ли мы все данные по 
  управляющему соединению;
        cmp      edi,offset ReciveDataThreadStruc
        je	 comparefordata
- по управляющему соединению получены все данные в случае если последние байты ответа 
  0dh, 0ah, что мы и проверяем;
        mov      eax,[edi+12]
        mov      esi,[edi+8]
        cmp      byte ptr [esi+eax-1],10
        je       short CallEvent
        jmp      InfinityLoop
comparefordata:
- по соединению данных получено все, если количество полученных байт = длине файла;
        mov      eax,[edi+12]
        cmp      FileLenght,eax
        jne      InfinityLoop
CallEvent:
- в случае если все данные получены выставляем соответствующее событие;
        call     SetEvent,ebx
        jmp      InfinityLoop
ReciveThread     endp

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

- принимает аргументами сокет, в который нужно передать команду, и смещение на буфер, 
  в котором содержится команда;
SendCommandInSocket proc uses ebx ecx esi edi, hSocket:dword, OutBufOffset:dword
- сначала определяем длину команды;
        mov     edi,OutBufOffset
        push    edi
        mov     eax,0ah
        mov     ecx,100
        repne   scasb
        sub     edi,OutBufOffset
        mov     ecx,edi
        pop     esi
        push    edi
- переносим команду в буфер для приема ответов для сервера, сделано это для того, 
  что бы потом его можно было сохранить в удобочитаемом виде лога;
        mov     edi,ReciveCommandBufferOffset
        add     edi,BytesCommandRecived
        rep     movsb
        pop     edi
        add     BytesCommandRecived,edi
- посылаем команду в сокет;
        call    send,hSocket,OutBufOffset,edi,ebx
- ждем ответа сервера, с помощью уже описанной выше функции WaitAnswerRecive;
        mov     eax,5001
Wait2Answer:
        dec     eax
        push    eax
        call    WaitAnswerRecive
        or      eax,eax
        jnz     ErrorProcessed
- ответ получен, ищем первый байт ответа, мы его ИЩЕМ, а не просто используем 
  смещение на конец предпоследнего полученого сообщения, потому, что нами может 
  быть получено в одном сеансе получения данных обновременно два ответа от сервера. 
  Уясните себе этот момент.
        mov     edi,ReciveCommandBufferOffset
        mov     ecx,BytesCommandRecived
        dec     ecx
        dec     ecx
        add     edi,ecx
        mov     al,0ah
        std
        repne   scasb
        cld
        xor     eax,eax
- проверяем первый символ ответа;
        mov     cl,[edi+2]
        cmp     cl,'1'
- если это "1" то ждем еще одного сообщения от сервера
        jz      Wait2Answer
        cmp     cl,'3'
- если ответ больше "3" - произошла ошибка;
        jna     NoErrorProcessed
        call    MessageBoxA,newhwnd,edi,offset ErrorCap,40h
ErrorProcessed:
        xor     eax,eax
        inc     eax
NoErrorProcessed:
        ret
SendCommandInSocket endp 

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

CreateListenSock proc 
        pushad
- создаем сокет;
        call     socket, AF_INET, SOCK_STREAM, IPPROTO_TCP
        mov      datasock,eax
- переводим его в не-блокирующий режим, указывая, что хотим получать в процедуре 
  окна сообщения о подтверждении приконекчивания к этому сокету, о поступлении 
  новых данных, и о закрытии соединения;
        call     WSAAsyncSelect, datasock, newhwnd, WM_DATASOCK, FD_ACCEPT+FD_READ+FD_CLOSE
- ввязываем сокет с локальным адресом;
        mov      sin_port,0 ; указываем ноль, в этом случае система даст нам 
		                    ; первый свободный порт
        mov      sin_family,AF_INET
        mov      sin_addr,INADDR_ANY
        call     bind, datasock, offset sockaddr_in, 16
- получаем инфу о сокете;
        call     getsockname,datasock,offset sockaddr_in,offset szSockaddr_in
- преобразуем номер порта к нормальному виду;
        xor      eax,eax
        mov      ax,sin_port
        call     ntohs,eax
        push     eax
        shr      eax,8
- дальше преобразуем номер порта в символы ASCII;
        call     DECtoASCII,eax,PortInPort
- и записываем их в шаблон команды PORT
        mov      al,','
        stosb
        pop      eax
        and      eax,0ffh
        call     DECtoASCII,eax,edi
        mov      ax,0a0dh
        stosw
        mov      esi,PortInPort
- слушаем сокет;
        call     listen, datasock, 1
        popad
        ret 
CreateListenSock endp

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

- получает в качестве аргумента в течении которого мы буде ждать ОКОНЧАНИЯ получения 
  данных, после того, как соединение будет закрыто со стороны сервера.
WaitTransferComplete proc uses ecx edi, TimeToWaitEndTransfer:dword
WaitProgress:
- ждем установления события закрытия соединения со стороны сервера, оно устанавливается 
  в главной процедуре окна;
        call    WaitForSingleObject,HWaitCloseEvent,-1
- дальше ждем, события успешного получения данных нашим потоком, которое в нем и 
  устанавливается;
        call    WaitForSingleObject,HWaitDataEvent,TimeToWaitEndTransfer
        or      eax,eax
        jz      CloseDataSocks
- был таймаут, и если мы получаем директорию, то выходим без ошибки, т.к. при получении 
  директории у нас всегда таймаут, потому, что мы заранее не знаем количество получаемых 
  байт и поток получения не может установить событие успешного получения;
        cmp     TimeToWaitEndTransfer,1000 ;если ждем каталог
        jz      CloseDataSocks
        call    MessageBoxA,newhwnd,offset ErrTimeOutCommand,offset ErrorCap,40h

CloseDataSocks:
- сбрасываем событие успешного получения;
        call    ResetEvent,HWaitDataEvent
- закрываем соединение со своей стороны;
        call    closesocket,ReciveDataSock
        call    closesocket,datasock
        ret
WaitTransferComplete endp

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

Заключение.

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

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

2002-2013 (c) wasm.ru