Как стать автором
Обновить

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

Ненормальное программирование
Это еще одна статья «давно гуляющая» по интернету, и опять, как автор, сделаю перепост. Думаю пригодиться и тут.

Введение


В этой статье я не ставлю себе целью пересказать все 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 со стороны сервера будет вашим «домашним заданием».
Теги:Ассемблерwebwinapiсистемное администрирование
Хабы: Ненормальное программирование
Всего голосов 10: ↑7 и ↓3 +4
Просмотры11.1K

Похожие публикации

Лучшие публикации за сутки