Прошло уже больше месяца с момента последней публикации в блоге. Так получилось, что все это время я к Delphi практически не обращался — занимался вещами далекими и от Delphi и от программирования, в принципе. По сему пропустил и выход AppMethod, но, думаю, что этот пробел я со временем восполню, а пока решил вернуться к работе над Synapse и предоставить на всеобщий обзор следующую главу, касающуюся работы с FTP в Synapse. По этому протоколу в блоге информации мало, поэтому большая часть главы писалась с нуля. PDF-версия главы — в конце сообщения. Первая глава по сокетам в Synapse доступна здесь, вторая (по HTTP) — здесь.
Общие сведения о протоколе
FTP (англ. File Transfer Protocol — протокол передачи файлов) — стандартный протокол, предназначенный для передачи файлов по TCP-сетям (например, Интернет).
Протокол построен на архитектуре «клиент-сервер» и использует разные сетевые соединения для передачи команд и данных между клиентом и сервером. Пользователи FTP могут пройти аутентификацию, передавая логин и пароль открытым текстом, или же, если это разрешено на сервере, могут подключиться анонимно.
Можно использовать протокол SSH для безопасной передачи, скрывающей (шифрующей) логин и пароль, а также шифрующей содержимое.
FTP является одним из старейших прикладных протоколов, появившимся задолго до HTTP, и даже до TCP/IP, в 1971 году.
Особенность протокола FTP состоит в том, что он использует множественное подключение. При этом один канал является управляющим, через который поступают команды серверу и возвращаются его ответы (обычно через TCP-порт 21), а через остальные происходит собственно передача данных, по одному каналу на каждую передачу.
Поэтому в рамках одной сессии по протоколу FTP можно передавать одновременно несколько файлов, причём в обоих направлениях. Для каждого канала данных открывается свой TCP порт, номер которого выбирается либо сервером, либо клиентом, в зависимости от режима передачи.
Передача данных может осуществляться в любом из трёх режимов:
- Поточный режим — данные посылаются в виде непрерывного потока, освобождая FTP от выполнения какой бы то ни было обработки. Вместо этого, вся обработка выполняется TCP.
- Блочный режим — FTP разбивает данные на несколько блоков (блок заголовка, количество байт, поле данных) и затем передаёт их TCP.
- Режим сжатия — данные сжимаются единым алгоритмом.
При передаче данных по сети могут быть использованы четыре представления данных:
- ASCII — используется для текста. Данные, если необходимо, до передачи конвертируются из символьного представления на хосте-отправителе в «восьмибитный ASCII», и (опять же, если необходимо) в символьное представление принимающего хоста. Как следствие, этот режим не подходит для файлов, содержащих не только обычный текст.
- Режим изображения (бинарный) — устройство-отправитель посылает каждый файл байт за байтом, а получатель сохраняет поток байтов при получении. Поддержка данного режима была рекомендована для всех реализаций FTP.
- EBCDIC — используется для передачи обычного текста между хостами в кодировке EBCDIC. В остальном, этот режим аналогичен ASCII-режиму.
- Локальный режим — позволяет двум компьютерам с идентичными установками посылать данные в собственном формате без конвертации в ASCII.
FTP может работать в активном или пассивном режиме, от выбора которого зависит способ установки соединения.
В активном режиме клиент создаёт управляющее TCP-соединение с сервером и отправляет серверу свой IP-адрес и произвольный номер клиентского порта, после чего ждёт, пока сервер не запустит TCP-соединение с этим адресом и номером порта. В случае, если клиент находится за брандмауэром и не может принять входящее TCP-соединение, может быть использован пассивный режим.
В пассивном режиме клиент использует поток управления, чтобы послать серверу команду PASV, и затем получает от сервера его IP-адрес и номер порта, которые затем используются клиентом для открытия потока данных с произвольного клиентского порта к полученному адресу и порту.
Сервер отвечает по потоку управления трехзначными ASCII-кодами состояния с необязательным текстовым сообщением. Например, «200» (или «200 ОК») означает, что последняя команда была успешно выполнена. Цифры представляют код ответа, а текст — разъяснение или запрос. Текущая передача по потоку данных может быть прервана с помощью прерывающего сообщения, посылаемого по потоку управления.
Команды FTP
Список основных команд FTP представлен в таблице ниже
Команда |
Описание |
ABOR | Прервать передачу файла |
CDUP | Сменить директорию на вышестоящую |
CWD | Сменить директорию |
DELE | Удалить файл (DELE filename). |
EPSV | Войти в расширенный пассивный режим. Применяется вместо PASV. |
HELP | Выводит список команд, принимаемых сервером |
LIST | Возвращает список файлов директории. Список передаётся через соединение данных. |
MDTM | Возвращает время модификации файла |
MKD | Создать директорию |
NLST | Возвращает список файлов директории в более кратком формате, чем LIST. Список передаётся через соединение данных |
NOOP | Пустая операция |
PASV | Войти в пассивный режим. Сервер вернёт адрес и порт, к которому нужно подключиться, чтобы забрать данные. Передача начнётся при введении следующих команд: RETR, LIST и т.д. |
PORT | Войти в активный режим. Например PORT 12,34,45,56,78,89. В отличие от пассивного режима для передачи данных сервер сам подключается к клиенту. |
PWD | Возвращает текущую директорию |
QUIT | Отключиться |
REIN | Реинициализировать подключение |
RETR | Скачать файл. Перед RETR должна быть команда PASV или PORT |
RMD | Удалить директорию |
RNFR и RNTO | Переименовать файл. RNFR — что переименовывать, RNTO — во что. |
SIZE | Возвращает размер файла |
STOR | Закачать файл. Перед STOR должна быть команда PASV или PORT. |
SYST | Возвращает тип системы (UNIX, WIN, …) |
TYPE | Установить тип передачи файла (бинарный, текстовый) |
USER | Имя пользователя для входа на сервер |
Коды ответов FTP
Ответ FTP сервера на любую команду FTP сервера состоит из трех цифр.
Первая цифра отвечает за один из трёх исходов: успех, отказ или указание на ошибку либо неполный ответ.
Вторая цифра определяет тип ошибки.
Третья цифра окончательно специфицирует ошибку.
Рассмотрим значение каждой цифры в каждой позиции.
Первая позиция |
Вторая позиция |
Третья позиция |
|
0 |
Не определено | Синтаксическая ошибка |
Окончательно специфицирует ошибку |
1 |
Команда принята к выполнению но ещё не завершена | Информационное сообщение | |
2 |
Выполнение команды успешно завершено | Ошибка соединения. Сообщение относится либо к управляющему соединению, либо к соединению данных | |
3 |
Команда принята и ожидается какая-либо дополнительная команда | Сообщение об аутентификации пользователя и его правах | |
4 |
В данный момент команда не может быть выполнена | Не определено | |
5 |
Принципиальная невозможность выполнения команды | Сообщение о состоянии файловой системы |
В Synapse работа с протоколом FTP реализована в модуле FTPSend.pas. Рассмотрим этот модуль подробно и реализуем несколько примеров по работе с FTP в Synapse.
Модуль FTPSend.pas
Основным классом в модуле FTPSend.pas является TFPSend – класс, реализующий клиент для работы с FTP протоколом. Однако для упрощения работы с данными, поступающими от сервера, этот модуль содержит также и другие вспомогательные классы, которые мы и рассмотрим вначале.
Класс TFTPListRec
Объекты этого класса хранят информацию о файлах и поддиректориях, полученных с FTP-сервера.
У класса TFTPListRec определены следующие свойства:
property FileName: string |
Имя файла или подкаталог |
property Directory: Boolean |
True, если объект является подкаталогом |
property Readable: Boolean |
True, если есть доступ на чтение объекта |
property FileSize: int64 |
Размер файла в байтах |
property FileTime: TDateTime |
Дата/время файла. Используется часовой пояс сервера. |
property OriginalLine: string |
«Сырые» данные об объекте, полученные с сервера |
property Mask: string |
Маска, которая будет использоваться использована для разбора ответа сервера |
property Permission: string |
Разрешения, установленные для объекта. |
Класс TFTPList
Объект этого класса содержит информацию обо всех объектах, находящихся в определенной директории на FTP-сервере.
У класса определены следующие свойства:
property List: TList |
Список объектов |
property Items[Index: Integer]: TFTPListRec |
Запись об отдельном объекте в списке |
property Lines: TStringList |
«Сырые» данные, полученные от сервера |
property Masks: TStringList |
Перечень масок для анализа данных сервера. Значения в этом списке устанавливаются по умолчанию, однако вы можете добавлять свои собственные маски для работы анализатора |
property UnparsedLines: TStringList |
Список строк, анализ которых не удалось провести с использованием текущего списка масок. |
Также класс содержит методы:
procedure Clear; virtual; |
Удаление всех данных об объектах. Очищаются свойства List, Lines и UnparsedLines. Свойство Masks остается без изменений. |
function Count: integer; virtual; |
Возвращает количество элементов в списке List |
procedure Assign(Value: TFTPList); virtual; |
Копирует все данные из Value в текущий объект |
procedure ParseLines; virtual; |
Производит анализ данных, содержащихся в списке Lines, формируя при этом список объектов List. |
Класс TFTPSend
Как было сказано выше, класс TFTPSend реализует работу с протоколом FTP. Рассмотрим какие свойства, методы и события содержит этот класс.
Свойства класса:
property ResultCode: Integer |
Содержит код последнего ответа сервера |
property ResultString: string |
Содержит строку описания последнего ответа сервера |
property FullResult: TStringList |
После выполнения очередной команды содержит список всех строк ответа сервера |
property Account: string |
Содержит информацию об аккаунте, которая может потребоваться для входа на сервер. В большинстве случаев использовать это свойство не требуется |
property FWHost: string |
Адрес брэндмауэра. Если это свойство не заполнено, то считается, что брэндмауэр не используется. |
property FWPort: string |
Порт брэндмауэра. |
property FWUsername: string |
Имя пользователя брэндмауэра. |
property FWPassword: string |
Пароль пользователя брэндмауэра. |
property FWMode: integer |
Тип брэндмауэра. В зависимости от значения этого свойства определяется последовательность выбора параметров при входе на сервер. |
property Sock: TTCPBlockSocket |
Объект сокета управляющего канала. |
property DSock: TTCPBlockSocket |
Объект сокета канала для передачи данных |
property DataStream: TMemoryStream |
Поток, используемый для передачи данных в случае, если свойство DirectFile равно False |
property DataIP: string |
После создания соединения для канала данных содержит IP адрес этого соединения |
property DataPort: string |
После создания соединения для канала данных содержит порт этого соединения |
property DirectFile: Boolean |
Определяет режим передачи данных. Если это свойство равно False, то передача данных осуществляется с использованием потока DataStream, иначе – данные для передачи получаются непосредственно из файла, имя которого указано в свойстве DirectFileName. Значение по умолчанию False |
property DirectFileName: string |
Имя файла, который используется для передачи данных |
property CanResume: Boolean |
После подключения к серверу указывает поддерживает ли сервер докачку файлов |
property PassiveMode: Boolean |
Если это свойство равно True, то работа ведется в пассивном режиме. Значение по умолчанию True. |
property ForceDefaultPort: Boolean |
Если свойство равно True, то для соединения канала данных устанавливается стандартный порт (20). По умолчанию это свойство равно False, т.к. сервер может назначить свой собственный порт при установке соединения. Если работа ведется в пассивном режиме, то это свойство не используется |
property ForceOldPort: Boolean |
Если это свойство равно True, то отсутствует возможность использовать такие команды как EPSV и EPRT. Однако без этих команд вы не сможете использовать IPv6. Использовать это свойство рекомендуется только в случае возникновения проблем при работе с брэндмауэром. |
property FtpList: TFTPList |
Содержит список объектов, полученных после выполнения метода List |
property BinaryMode: Boolean |
Если значение равно True, то работа ведется в бинарном режиме, иначе – в режиме ASCII. |
property AutoTLS: Boolean |
Если значение равно True, то вначале проверяется поддержка сервером SSL/TLS, а затем производится подключение в зависимости от результатов проверки. |
property FullSSL: Boolean |
Если сервер использует SSL/TLS, то это свойство необходимо установить в значение True |
property IsTLS: Boolean |
Указывает использует ли канал управления SSL/TLS |
property IsDataTLS: Boolean |
Указывает использует ли канал данных SSL/TLS |
property TLSonData: Boolean |
Если значение равно True (по умолчанию), то при передаче данных осуществляется попытка использования SSL/TLS |
Методы класса
function ReadResult: Integer; virtual; |
Ожидает ответ сервера и в результате возвращает его код. Этот метод следует вызывать только в особых случаях работы с FTP |
procedure ParseRemote(Value: string); virtual; |
Разбирает ответ сервера Value, полученный в результате выполнения команды PASV. Этот метод следует вызывать только в особых случаях работы с FTP |
procedure ParseRemoteEPSV(Value: string); virtual; |
Разбирает ответ сервера Value, полученный в результате выполнения команды EPSV. Этот метод следует вызывать только в особых случаях работы с FTP |
function FTPCommand(const Value: string): integer; virtual; |
Отправляет команду Value на сервер и возвращает код ответа. Этот метод удобно использовать для выполнения команд не предусмотренных другими методами класса. |
function Login: Boolean; virtual; |
Осуществляет вход на сервер и, если вход успешно выполнен, то возвращает в результате True |
function Logout: Boolean; virtual; |
Закрывает текущую сессию работы с FTP сервером |
procedure Abort; virtual; |
Прерывает текущую передачу данных |
procedure TelnetAbort; virtual; |
Метод аналогичен Abort, но перед прерыванием операции на сервер отправляется команда ABOR, которую требуют некоторые серверы |
function List(Directory: string; NameList: Boolean): Boolean; virtual; |
Получает с сервера информацию об объектах в директории Directory. Если Directory содержит пустую строку, то получается информация из текущей директории. Если параметр NameList равен True, то запрашиваются только имена объектов в директории, иначе – получается вся информация, которая затем размещается в списке FtpList. |
function RetrieveFile(const FileName: string; Restore: Boolean): Boolean; virtual; |
Получает данные файла FileName сервера. Если сервер поддерживает докачку и параметр Restore равен True, то получение данных возобновляется с места последнего прерывания. |
function StoreFile(const FileName: string; Restore: Boolean): Boolean; virtual; |
Загружает данные из файла FileName на сервер. Если сервер поддерживает докачку и параметр Restore равен True, то получение данных возобновляется с места последнего прерывания. |
function StoreUniqueFile: Boolean; virtual; |
Загружает данные на сервер и присваивает новому файлу уникальное имя. |
function AppendFile(const FileName: string): Boolean; virtual; |
Дописывает данные в файл с именем FileName на сервере |
function RenameFile(const OldName, NewName: string): Boolean; virtual; |
Изменяет имя файла на сервере с OldName на NewName |
function DeleteFile(const FileName: string): Boolean; virtual; |
Удаляет файл с именем FileName с сервера |
function FileSize(const FileName: string): int64; virtual; |
Возвращает размер файла FileName на сервере |
function NoOp: Boolean; virtual; |
Посылает пустую команду NOOP на сервер для поддержки соединения |
function ChangeWorkingDir(const Directory: string): Boolean; virtual; |
Изменяет текущую директорию на директорию Directory |
function ChangeToParentDir: Boolean; virtual; |
Изменяет текущую директорию на родительскую |
function ChangeToRootDir: Boolean; virtual; |
Изменяет текущую директорию на корневую |
function DeleteDir(const Directory: string): Boolean; virtual; |
Удаляет директорию Directory с сервера |
function CreateDir(const Directory: string): Boolean; virtual; |
Создает директорию Directory на сервере |
function GetCurrentDir: String; virtual; |
Возвращает текущую рабочую директорию |
function DataRead(const DestStream: TStream): Boolean; virtual; |
Устанавливает канал данных и считывает данные с сервера. Эта функция может потребоваться при передаче на сервер своих команд методом FTPCommand |
function DataWrite(const SourceStream: TStream): Boolean; virtual; |
Устанавливает канал данных и отправляет данные на сервер. Эта функция может потребоваться при передаче на сервер своих команд методом FTPCommand |
События класса
type
TFTPStatus = procedure(Sender: TObject; Response: Boolean; const Value: string) of object; property OnStatus: TFTPStatus |
Это событие возникает при передаче команд на сервер или получении ответа с сервера. Sender – указывает на объект TFTPSend Value – команда или ответ сервера на команду Response – содержит True, если строка Value является ответом сервера |
Вспомогательные методы
function FtpGetFile(const IP, Port, FileName, LocalFile, User, Pass: string): Boolean;
Функция для получения файла с именем FileName с сервера и сохранения его на диске с именем LocalName.
function FtpPutFile(const IP, Port, FileName, LocalFile, User, Pass: string): Boolean;
Функция для передачи файла LocalName с локального диска на сервер. Файл сохраняется на сервере с именем FileName.
function FtpInterServerTransfer(const FromIP, FromPort, FromFile, FromUser, FromPass: string; const ToIP, ToPort, ToFile, ToUser, ToPass: string): Boolean;
Функция для передачи файла с одного FTP сервера на другой.
Примеры работы с FTP в Synapse
Первый пример: подключение к серверу и работа с директориями
В качестве первого примера рассмотрим как правильно подключаться к FTP серверу и осуществлять навигацию по директориям.
Создаем новый проект (назовем его «syna_ftp») и размещаем на главной форме компоненты как показано на рисунке:
На форме расположены следующие компоненты:
- 3 TEdit для ввода адреса сервера, имени пользователя и пароля
- TButton для подключения к серверу или разрыва соединения
- TListView для вывода информации об объектах в текущей директории
- TMemo для вывода информации полученной/отправленной по каналу управления
Наша программа будет работать следующим образом:
- Подключаться к серверу и считывать данные об объектах в текущей директории
- Двойной клик по записи в ListView будет осуществлять переход в выбранную директорию или на один уровень вверх (для этого мы предусмотрим в ListView отдельный элемент.
Теперь подключаем в uses главного модуля приложения модуль FtpSend и пишем обработчики событий OnCreate и OnDestroy главной формы и OnStatus для объекта FTPSend:
unit uMain; interface uses ..., ftpsend, ....; type TForm3 = class(TForm) ... procedure FormCreate(Sender: TObject); procedure FormDestroy(Sender: TObject); private procedure Status(Sender: TObject; Response: Boolean; const Value: string); public FTPSend: TFTPSend; end; var fmain: Tfmain; implementation {$R *.dfm} procedure TForm3.FormCreate(Sender: TObject); begin FTPSend:=TFTPSend.Create; FTPSend.OnStatus:=Status; end; procedure TForm3.FormDestroy(Sender: TObject); begin FTPSend.Free; end; procedure Tfmain.Status(Sender: TObject; Response: Boolean; const Value: string); begin if Response then memLog.Lines.Add('--> '+Value) else memLog.Lines.Add('<-- '+Value) end;
Теперь напишем вспомогательную процедуру, которая будет формировать содержимое ListView по данным из свойства FtpList объекта FTPSend:
procedure Tfmain.UpdateFileList; var LI: TlistItem; I: Integer; begin lbCurrDir.Caption:=FTPSend.GetCurrentDir; ListView1.Items.BeginUpdate; try ListView1.Items.Clear; {создаем элемент для перехода на один уровень вверх} LI:=ListView1.Items.Add; LI.Caption:='[...]'; {заполняем список информацией об объектах в текущей директории} for I := 0 to FTPSend.FtpList.Count-1 do begin LI:=ListView1.Items.Add; LI.Caption:=FTPSend.FtpList[i].FileName; if FTPSend.FtpList[i].Directory then Li.SubItems.Add('Папка') else Li.SubItems.Add('Файл'); LI.SubItems.Add(IntToStr(FTPSend.FtpList[i].FileSize)); LI.SubItems.Add((DateToStr(FTPSend.FtpList[i].FileTime))); Li.SubItems.Add(FTPSend.FtpList[i].Permission); end; finally ListView1.Items.EndUpdate; end; end;
Теперь напишем обработчик события OnClick кнопки btnLogin:
procedure Tfmain.btnLoginClick(Sender: TObject); const cLogin = 'Login'; cLogout = 'Logout'; var i:integer; begin if btnLogin.Caption=cLogin then begin //задаем параметры соединения FTPSend.TargetHost:=edServer.Text; FTPSend.UserName:=edUserName.Text; FTPSend.Password:=edPassword.Text; //используем пассивный режим FTPSend.PassiveMode:=True; //вход произведен успешно if FTPSend.Login then begin //выводим информацию о текущей директории lbCurrDir.Caption:=FTPSend.GetCurrentDir; //получаем информацию об объектах в директории FTPSend.List(lbCurrDir.Caption, false); //обновляем список UpdateFileList; //меняем надпись кнопки на "Logout" btnLogin.Caption:=cLogout end else memLog.Lines.Add('Error connect') end else begin //завершаем сессию FTPSend.Logout; //меняем надпись кнопки на "Login" btnLogin.Caption:=cLogin end; end;
Остается написать обработчик двойного клика в ListView и проверить работу программы. Пишем обработчик:
procedure Tfmain.ListView1DblClick(Sender: TObject); var b: boolean; begin if not Assigned(ListView1.Selected) then Exit; //если пробуем подняться на уровень вверх if ListView1.ItemIndex=0 then b:=FTPSend.ChangeToParentDir else //пробуем сменить директорию на выбранную b:=FTPSend.ChangeWorkingDir(ListView1.Selected.Caption); if b then begin //смена текущей директории прошла успешно //получаем данные об объектах в текущей директории FTPSend.List(EmptyStr,False); //обновляем список UpdateFileList; end else memLog.Lines.Add('Change current directory filed') end;
Теперь можно запустить приложение и проверить его работу:
По логу работы программы вы можете проследить всю цепочку «общения» клиента и сервера, а используя ListView перемещаться по директориям на сервере.
Второй пример: передача данных с заполнением ProgressBar’а
При изучении работы класса THTTPSend мы рассматривали пример скачивания больших файлов с заполнением ProgressBar’a. Рассмотрим и в этой главе подобный пример – научимся загружать/скачивать данные с заполнением ProgressBar.
В начале рассмотрим в чем будут заключаться особенности этого примера от рассмотренного ранее. Во-первых, если при скачивании файла по HTTP сервер может не вернуть нам размер скачиваемого файла, то при работе с FTP мы можем точно знать каков размер файла (при выполнении команды LIST мы получим всю информацию об объекте, включая и его размер). Во-вторых, следует учитывать особенность самого протокола FTP – многоканальную работу. Так, если обработчик OnStatus объекта FTPSend возвращает нам информацию исключительно из канала управления, то для отслеживания передачи информации по каналу данных нам необходимо написать обработчик OnStatus для сокета DSock (сокет канала данных).
Теперь приступим к работе над примером. В качестве основы для работы воспользуемся предыдущим примером. Копируем код предыдущего примера в другую директорию и добавляем на форму компонент TProgressBar как показано на рисунке ниже, а также компоненты:
- TPopupMenu для выбора операции с объектом в текущей директории.
- TOpenFileDialog – для выбора файла на локальном диске.
У компонента ListView в свойстве PopupMenu указываем компонент меню, а в самом меню создаем следующие элементы:
- Скачать файл
- Загрузить файл на сервер
- Удалить
Теперь приступим к написанию кода программы. Чтобы использовать события сокета нам необходимо подключить в uses модуль blcksock. Теперь напишем обработчик события OnStatus для сокета:
procedure Tfmain.DSockStatus(Sender: TObject; Reason: THookSocketReason; const Value: String); begin if Reason in [HR_ReadCount, HR_WriteCount] then ProgressBar1.Position:=ProgressBar1.Position+StrToInt(Value); end;
Здесь мы проверяем какая операция в данный момент осуществляется и, если это операция чтения или записи, то увеличиваем свойство Position у ProgressBar.
Теперь напишем обработчик OnClick элемента меню «Скачать файл». Обработчик может выглядеть следующим образом:
procedure Tfmain.N1Click(Sender: TObject); begin if not Assigned(ListView1.Selected) then Exit; if ListView1.Selected.Index=0 then Exit; //если выбрана папка, то выводим в лог сообщение if ListView1.Selected.SubItems[0]='Папка' then memLog.Lines.Add('Can''''t downoload directory') else //скачиваем файл begin //устанавливаем свойства ProgressBar ProgressBar1.Max:=StrToInt(ListView1.Selected.SubItems[1]); ProgressBar1.Position:=0; //определяем обработчик событие FTPSend.DSock.OnStatus:=DSockStatus; //пробуем скачать файл if FTPSend.RetrieveFile(ListView1.Selected.Caption,False) then begin //скачивание прошло успешно - сохраняем файл FTPSend.DataStream.SaveToFile(ListView1.Selected.Caption); //выводим сообщение об успешной закачке ShowMessage('Download complete'); end else //загрузка не удалась - выводим сообщение в лог memLog.Lines.Add('Can''''t downoload file'); ProgressBar1.Position:=0; FTPSend.DSock.OnStatus:=nil; end; end;
Рассмотрим этот обработчик подробно.
Чтобы скачать файл мы вначале проверяем выбран ли какой-либо объект в списке ListView и, если ничего не выбрано, то выходим из обработчика.
if not Assigned(ListView1.Selected) then Exit;
Если в ListView выделен какой-либо элемент, то определяем является ли этот элемент самым первым в списке.
if ListView1.Selected.Index=0 then Exit;
Если это так, то значит пользователь выбрал служебный элемент […] для перехода на уровень вверх и, следовательно, т.к. скачать мы ничего в этом случае не можем, то работа прерывается.
Следующая проверка – проверка на тип объекта.
if ListView1.Selected.SubItems[0]='Папка' then memLog.Lines.Add('Can''''t downoload directory')
Если выбрана папка, то выводим сообщение в лог о том, что мы не можем скачать папку и выходим из обработчика.
Если в ListView выбран файл, то мы точно знаем его размер – он записан во втором столбце ListView, поэтому мы можем настроить наш ProgressBar, установив ему верное значение свойство Max, что мы и делаем.
//устанавливаем свойства ProgressBar ProgressBar1.Max:=StrToInt(ListView1.Selected.SubItems[1]);
Далее мы назначаем обработчик события OnStatus сокета канала данных:
FTPSend.DSock.OnStatus:=DSockStatus;
И пробуем скачать файл и сохранить его на диске
if FTPSend.RetrieveFile(ListView1.Selected.Caption,False) then begin //скачивание прошло успешно - сохраняем файл FTPSend.DataStream.SaveToFile(ListView1.Selected.Caption); <i>//выводим сообщение об успешной закачке ShowMessage('Download complete'); end else //загрузка не удалась - выводим сообщение в лог memLog.Lines.Add('Can''''t downoload file');</i>
В любом случае, после выполнения операции скачивания файла убираем с сокета обработчик и обнуляем прогресс в ProgressBar:
ProgressBar1.Position:=0; FTPSend.DSock.OnStatus:=nil;
Если не снять обработчик OnStatus с сокета, то при последующей работе с FTPSend ProgressBar будет пробовать заполняться, т.к. через канал данных проходят все данные, в т.ч. и данные о содержимом директории.
Обработчик события OnClick элемента меню «Загрузить файл на сервер» будет немногим отличаться от приведенного выше, поэтому просто приведем его код с комментариями:
procedure Tfmain.N4Click(Sender: TObject); var b:boolean; begin if OpenDialog1.Execute then begin //загружаем данные файла в поток FTPSend.DataStream.LoadFromFile(OpenDialog1.FileName); //устанавливаем значения свойств ProgressBar'a ProgressBar1.Position:=0; ProgressBar1.Max:=FTPSend.DataStream.Size; //определяем обработчик события FTPSend.DSock.OnStatus:=DSockStatus; //пробуем загрузить файл в директорию b:=FTPSend.StoreFile(ExtractFileName(OpenDialog1.FileName), False); //убираем обработчик события FTPSend.DSock.OnStatus:=nil; if b then begin //загрузка прошла успешно - обновляем список объектов FTPSend.List(EmptyStr,False); UpdateFileList; end; //обнуляем прогресс ProgressBar1.Position:=0; end; end;
И третий обработчик – обработчик события OnClick элемента меню «Удалить»:
procedure Tfmain.N2Click(Sender: TObject); var b:boolean; begin if not Assigned(ListView1.Selected) then Exit; if ListView1.Selected.Index=0 then Exit; if ListView1.Selected.SubItems[0]='Папка' then b:=FTPSend.DeleteDir(ListView1.Selected.Caption) else b:=FTPSend.DeleteFile(ListView1.Selected.Caption); if b then begin memLog.Lines.Add('Delete object complete'); //получаем данные об объектах в текущей директории FTPSend.List(EmptyStr,False); //обновляем список UpdateFileList; end else memLog.Lines.Add('Can''''t delete object'); end
Теперь можно запустить приложение, подключиться к серверу и проверить работу с файлами:
Третий пример: докачка файлов
Ещё одной немаловажной проблемой при работе с FTP является докачка файлов. На FTP сервере могут содержаться файлы самого разного размера и наличие возможности загрузить файл, начиная с определенной позиции после восстановления соединения с сервером, является скорее необходимостью для современных FTP-клиентов, а не «ещё одной дополнительной возможностью». И в этом примере мы разберемся с тем, как можно получать/отправлять данные с места определенной позиции в файле или, иначе говоря, производить докачку файлов.
Как докачать файл по FTP?
Чтобы начать загрузку файла с определенной позиции FTP-клиент должен выполнить как минимум две команды:
- REST – для указания серверу позиции с которой необходимо получать данные из файла.
- RETR – для скачивания файла.
В Synapse обе эти команды реализованы в методах TFTPSend, которые мы уже использовали ранее – это RetrieveFile и StoreFile. Оба эти метода содержат такой входной параметр как Restore. Этот параметр указывает будет ли производится докачка файла с последней позиции в файле (параметр содержит True) или передача данных начнется с начала (параметр содержит False).
Так как не каждый FTP-сервер поддерживает докачку файлов, то для того, чтобы узнать поддерживается ли эта возможность на сервере, у TFTPSend имеется специальное свойство — CanResume, которое будет содержать значение True, если сервер поддерживает докачку.
Таким образом, чтобы реализовать докачку файлов по FTP нам не необходимо внести в предыдущий пример совсем незначительные изменения. Теперь приступим к реализации практического примера.
Реализуем FTP-клиент с возможностью докачки файлов
Копируем исходный код предыдущего примера в отдельную папку и переименовываем проект, например, в syna_ftp_resume.
Теперь нам необходимо немного изменить обработчики OnClick элементов меню «Скачать файл» и «Загрузить файл на сервер». Изменения в этих методах будут минимальными, а точнее – по одной строке в каждом методе:
для обработчика OnClick у элемента «Скачать файл» строку:
if FTPSend.RetrieveFile(ListView1.Selected.Caption,False) then
необходимо поменять следующим образом:
if FTPSend.RetrieveFile(ListView1.Selected.Caption, FTPSend.CanResume) then
и, соответственно, в обработчике OnClick элемента «Загрузить файл на сервер» строка:
b:=FTPSend.StoreFile(ExtractFileName(OpenDialog1.FileName), False);
меняется на:
b:=FTPSend.StoreFile(ExtractFileName(OpenDialog1.FileName), FTPSend.CanResume);
Теперь, если FTP-сервер поддерживает докачку файлов, то, в случае разрыва связи с сервером и последующего его восстановления, файл будет скачиваться/закачиваться с той позиции на которой произошел разрыв. Но на этом наш пример не заканчивается. Не стоит забывать про такой элемент интерфейса нашей программы как ProgressBar. Если Вы прямо сейчас попробуете реализовать, например, докачку файла, начиная с середины, то заметите, что после восстановления процесса загрузки ProgressBar заполниться ровно наполовину. Происходит это потому, что мы не указываем в свойстве ProgressBar.Position начальную позицию в файле с который мы начинаем загрузку. Чтобы ProgressBar заполнялся корректно, необходимо отследить момент, когда на сервер отправляется команда REST и выделить из строки команды значение позиции в файле.
Строка на выполнение команды REST может выглядеть, например, так:
REST 12567899
Соответственно, нам необходимо выделить из этой строки число 12567899 и указать его в свойстве ProgressBar.Position. Делать это удобно в обработчике события OnStatus для свойства FTPSend.Sock. Этот обработчик у нас уже есть в программе и нам его, опять же, достаточно дописать следующим образом:
procedure Tfmain.Status(Sender: TObject; Response: Boolean; const Value: string); begin if Response then memLog.Lines.Add('--> '+Value) else memLog.Lines.Add('<-- '+Value); //на сервер отправляется команда REST if pos('REST', Value)>0 then ProgressBar1.Position:=StrToInt64Def(copy(value,Pos('REST',Value)+5,Length(Value)-Pos('REST',Value)-4),0) end;
Теперь с реализацией докачки файлов все в порядке – если сервер поддерживает докачку, то после разрыва связи файл именно докачивает, а ProgressBar корректно реагирует на восстановление процесса скачивания/загрузки файлов.
Четвертый пример: выполнение произвольных команд
Бывают случаи, когда имеющихся методов и свойств класса Synapse оказывается недостаточно для выполнения реализации каких-либо функций в программе. Например, у класса TFTPSend отсутствует метод с помощью которого можно изменять атрибуты файлов на сервере. Для реализации такой возможности нам необходимо использовать метод FTPCommand и самостоятельно разбирать ответ сервера. Ниже рассмотрены два примера использования этой полезной функции в Synapse.
Копируем исходный код предыдущего примера в отдельную папку и переименовываем проект, например, в syna_ftp_commands.
Теперь на главную форму приложения добавляем компоненты, как показано на рисунке ниже:
- CommandBox: TComboBox – выпадающий список в который мы будем записывать все поддерживаемые команды
- edCmdParams: TEdit – текстовое поле в которое мы впоследствии будем записывать параметры команды
- btnCmd: TButton – кнопка «Выполнить» клик по которой будет выполнять произвольную команду
Теперь нам необходимо научиться получать список команд, поддерживаемых FTP-сервером. Для получения списка поддерживаемых команд достаточно отправить на сервер команду HELP без каких-либо дополнительных параметров. В результате FTP-сервер вернет в ответе следующий ответ (список команд в ответе может отличаться в зависимости от настроек сервера):214-The following commands are recognized.
ABOR ACCT ALLO APPE CDUP CWD DELE EPRT EPSV FEAT HELP LIST MDTM MKD
MODE NLST NOOP OPTS PASS PASV PORT PWD QUIT REIN REST RETR RMD RNFR
RNTO SITE SIZE SMNT STAT STOR STOU STRU SYST TYPE USER XCUP XCWD XMKD
XPWD XRMD
214 Help OK.
Напишем отдельный метод, который будет отправлять на сервер команду HELP и заполнять список CommandBox. Назовем этот метод FTPHelp. Код методы будет следующий:
procedure Tfmain.FTPHelp; begin //если команда HELP успешно выполнена if FTPSend.FTPCommand('HELP')=214 then begin //удаляем из полного текста ответа сервера первую и последнюю строку FTPSend.FullResult.Delete(0); FTPSend.FullResult.Delete(FTPSend.FullResult.Count-1); //очищаем ComboBox и записываем в него список доступных команд CommandBox.Clear; CommandBox.Items.DelimitedText:=FTPSend.FullResult.Text; end; end;
Здесь мы, в случае положительного ответа сервера, удаляем из FullResult (полного ответа сервера) первую и последнюю строки, т.к. в них не содержаться названия команд, а затем оставшуюся строку передаем в CommandBox.
Вызывать этот метод мы будем сразу после того, как происходит авторизация на сервере, т.е. необходимо в обработчике OnClick кнопки btnLogin добавить следующую строку:
procedure Tfmain.btnLoginClick(Sender: TObject); ... begin if btnLogin.Caption=cLogin then begin .... if FTPSend.Login then begin ... //считываем список доступных команд FTPHelp; end ... end;
Проверим работу программы. Запускаем приложение, авторизуемся на сервере и получаем следующий результат:
Теперь переходим к следующей части примера – попробуем выполнить какую-либо произвольную команду. Как было сказано выше, у TFTPSend нет метода с помощью которого можно было бы изменять атрибуты файла. Разберемся как можно выполнить это действие на примере ниже.
За выполнение специфичных для сервера команд, как раз таких как смена атрибутов файла или директории, отвечает команда SITE. Так, для того, чтобы изменить атрибуты файла с именем readme.html на значение 777 нам необходимо отправить на сервер команду SITE со следующими аргументами:SITE CHMOD 777 readme.html
и, в случае, если команды будет выполнена успешно, сервер вернет следующую строку ответа:200 SITE CHMOD command ok.
Напишем обработчик OnClick кнопки «Выполнить» следующим образом:
procedure Tfmain.btnCmdClick(Sender: TObject); begin //если не выбрана команда - выходим if CommandBox.ItemIndex0 then FTPSend.FTPCommand(CommandBox.Text+' '+edCmdParams.Text) else FTPSend.FTPCommand(CommandBox.Text); //выводим ответ сервера пользователю ShowMessage(FTPSend.FullResult.Text); end;
Теперь можно запустить приложение и попробовать выполнить, например, команду SITE. Результат представлен на рисунке:
Аналогичным образом можно выполнять и другие FTP-команды. При этом следует отметить, что для получения результата выполнения команды, наравне со свойством TFTPSend FullResult можно также использовать свойства ResultCode и ResultString.
Книжная полка
Описание: Рассмотрены практические вопросы по разработке клиент-серверных приложений в среде Delphi 7 и Delphi 2005 с использованием СУБД MS SQL Server 2000, InterBase и Firebird. Приведена информация о теории построения реляционных баз данных и языке SQL. Освещены вопросы эксплуатации и администрирования СУБД.
|
||
Название: О чем не пишут в книгах по Delphi
Описание: Рассмотрены малоосвещенные вопросы программирования в Delphi. Описаны методы интеграции VCL и API. Показаны внутренние механизмы VCL и приведены примеры вмешательства в эти механизмы. Рассмотрено использование сокетов в Delphi: различные режимы их работы, особенности для протоколов TCP и UDP и др.
|
Спасибо! Выложили бы еще и первую главу в PDF, цены бы вам небыло!!!
Ой, блин..про неё-то я и забыл))) Спасибо за напоминание. Сейчас выложу.
Выложил в конце главы по HTTP. Скачивайте
Спасибо!
Привет, Влад. Мы как-то потерялись. Может Я ненароком в спам-листе оказался?
Привет! Да нет вроде…ты же мне на gmail обычно отвечаешь? От тебя писем нет ни где…в спаме тоже. Последнее письмо я отправлял тебе 24 марта
[…] глава книги «Глава 2 «Работа с FTP в Synapse»« Предыдущая глава книги «Глава 0 […]
Влад, нужна ваша помощь! Можете с базой данных помочь? несколько частных уроков… Пожаааалуйста. Моя почта nhramcova@yandex.ru
Ребята, в Synapse с httpsend какой-то косяк! httpsend:=THTTPSend.Create; HTTPSend.Protocol:=’1.1′; HTTPSend.TargetPort:=’80’; HTTPSend.TargetHost:=’143.123.45.12′; //левый IP, который нам никогда не ответит httpsend.UserAgent:=’Opera/12.0(Windows NT 5.1;U;ru)Presto/22.9.168 Version/12.00′; httpsend.Headers.Add(‘Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8’); httpsend.Headers.Add(‘Accept-Language: ru,en-us;q=0.7,en;q=0.3’); httpsend.Headers.Add(‘Accept-Encoding: gzip,deflate’); httpsend.Headers.Add(‘Accept-Charset: windows-1251,utf-8;q=0.7,*;q=0.7’); HTTPSend.KeepAliveTimeout:=300; httpsend.KeepAlive:=true; //Любое из этого httpsend.Timeout:=10000; httpsend.Sock.SocksTimeout:=10000; httpsend.Sock.SetTimeout(10000); httpsend.Sock.SetSendTimeout(10000); httpsend.Sock.SetRecvTimeout(10000); Memo1.Lines.Add(‘START: [‘+FormatDateTime(‘c’,Now)+’]’); stream:=TStringStream.Create(»); if HTTPSend.HTTPMethod(‘GET’,’143.123.45.12′) then begin end; Memo1.Lines.Add(‘STOP : [‘+FormatDateTime(‘c’,Now)+’]’); stream.Free; httpsend.Free; exit; Имеем всегда магические 21 секунды ожидания. Не 10, не 20, не 1000, а именно 21! И не важно что мы пишем в таймауте. Слабое место synapse — таймауты. Как же изменить таймаут ожидания? Вообще сперва мне нужно было его уменьшить, а сейчас уже нужно увеличить, но… Подробнее »
Ребята, кто поможет по проблеме, которую я выше описал, без вопросов 1000 вмр переведу
Proger, не стОит торопиться :) Я уже изучаю этот вопрос. Если найду решение, то отвечу или комментарием или отдельным постом
Всё равно, от своих слов не отказываюсь.
И если раньше найду решение, тоже отпишусь сюда.
Ну вот, собственно, и ответ:
HTTPSend.Sock.ConnectionTimeout:=10000;
Код выше устанавливает таймаут на соединение в 10 секунд. Как я понимаю, это именно то, что хотелось видеть в первоначальном варианте кода.
Timeout := 10000;
Sock.SocksTimeout := 10000;
Sock.SetTimeout(10000);
Sock.SetSendTimeout(10000);
Sock.SetRecvTimeout(10000);
Устанавливают таймауты на чтение/запись данных. Вот такой Synapse :)
З.Ы. Кстати, то ли я пропустил это свойство, когда писал главу про сокеты, то ли свойство появилось недавно…
HTTPSend.Sock.ConnectionTimeout:=10000;
вызывает у меня ошибку: Undeclared identifier: ‘ConnectionTimeout’
У вас какая версия синапс?
Project : Ararat Synapse | 003.012.006
На данный момент используется самая последняя версия из репозитория. А вообще свойство ConnectionTimeout гарантированно было в версии 009.009.001 (модуль blcksock.pas), как с версией 003.012.006 — не знаю.
Очень странно. Сделал поиск по всем «.pas» в составе Синапс версия 40 скаченного с офф сайта http://www.ararat.cz/synapse/doku.php/download по слову «ConnectionTimeout» — ничего не найдено.
Если версия та же, что и у меня, то открываете модуль blcksock.pas, класс TBlockSocket, предпоследнее поле — FConnectionTimeout: integer (это строка 317). Само свойство определено в этом же классе в разделе published строка 837:
{:Timeout for @link(Connect) call. Default value 0 means default system timeout.
Non-zero value means timeout in millisecond.}
property ConnectionTimeout: Integer read FConnectionTimeout write FConnectionTimeout;
Зашел blcksock.pas, который шел в 40 релизе, там версия стоит: 009.008.005
У вас 009.009.001. В репозитарии тоже стоит версия 009.009.001. Во дела!
Сейчас скачаю оттуда и проверю.
архив с исходниками Synapse не всегда содержит самую актуальную версию модулей — лучше пользоваться репозиторием
Обновил все классы в составе синапс. 22 осталось без изменений (40-ая версия, последняя), а вот другие поменялись. И действительно! Таймаут заработал :) а я через «жэ» уже сделал в свое время проверку работосопособности прокси (а то 21 секунду ждать не вариант было)
Можешь на мыло прислать кошель вмр, скину обещенное
Повесил в сайдбаре номер: R200985316566. На мыло тож скинул :)
Скинул так.
‘»Скажи «Спасибо»!’ — ее не заметно вообще. При большом объеме статьи к кол-ве комментариев она пропадает где-то в середине
Получил. Спасибо. Кнопку «Спасибо» оставил так, т.к., честно — лень снова лезть в шаблон и выискивать код этой кнопки :) Повесил номер WMR рядом с копилкой — пусть висит
httpsend.Timeout:=4000;
HTTPSend.Sock.ConnectionTimeout:=3000;
httpsend.Sock.SocksTimeout:=6000;
httpsend.Sock.SetTimeout(6000);
httpsend.Sock.SetSendTimeout(6000);
httpsend.Sock.SetRecvTimeout(6000);
Решил использовать полученный бонус. Беру крупный файл, скачиваю его. 6 секунд на скачивание, иначе сброс. Но скачивание идет до конца, процесс после 6 сек не обрывается. Вроде верно всё выставил. Какие еще таймауты можно выставить?
httpsend.Sock.SetTimeout(6000); httpsend.Sock.SetSendTimeout(6000); httpsend.Sock.SetRecvTimeout(6000); Так писать излишне, т.к. SetTimeout уже устанавливает таймауты и на получение и на отправку, т.е. достаточно указать просто: httpsend.Sock.SetTimeout(6000); или же так: httpsend.Sock.SetSendTimeout(6000); httpsend.Sock.SetRecvTimeout(6000); здесь вы указываете интервал 6 сек. между двумя операциями чтения/записи. Файл у нас качается не одним куском, а пакетами по N байт. Так вот в вашем случае получается так: если очередной пакет не поступил через 6 сек, то надо разорвать соединение. И, естественно, что пакет успевает «добежать» за 6 сек :) По поводу того какие тайм-ауты есть ещё прямо сейчас сказать не могу, но статью по поводу таймаутов уже пишу — думаю сделать… Подробнее »
Аха, уже нашел в исходнике
procedure TBlockSocket.SetTimeout(Timeout: Integer);
begin
SetSendTimeout(Timeout);
SetRecvTimeout(Timeout);
end;
httpsend.Sock.InterPacketTimeout:=false; не повлиял на картину.
ок. Если найду решение — скажу
По моему последнюю задачку просто так не решить, используя только значения каких-либо свойств в классах Synapse. Как вариант, можно попробовать использовать событие OnHeartbeat и в его обработчике считать сколько времени прошло с момента запуска операции чтения и, если времени прошло больше, чем нам надо, то вызывать Abort у THTTPSend и вызывать свое исключение. Но тут следует помнить, что событие OnHeartbeat может вызываться как чаще, так и реже времени, указанного в свойстве HeartbeatRate.
Не получается реализовать докачку с ftp я так понимаю это вообще не работает
Выложите реализованный исходник с работающей докачкой