Глава 6. CsShopper: FTP-клиентДжон Пенман Отправляйтесь в Internet за бесплатным барахлом! В этом вам поможет компонент, выполняющий функции FTP-клиента, и полноцен ное приложение для пересылки файлов, построенное на его основе. Популярность Internet в немалой степени обусловлена возможностью обмена информацией между компьютерами. Такой обмен становится возможным благодаря протоколу пересылки файлов FTP (File Transfer Protocol) ≈ одному из самых старых протоколов, используемых в Internet. Формальная спецификация используемого в настоящее время протокола FTP содержится в документе RFC959. Протокол FTP, как и другие Internet-протоколы, берет свое начало в классической модели клиент/сервер. FTP-сервер иногда представляется мне в виде старомодного продавца, который снимает товар с полки и передает его покупателю (FTP-клиенту). В этой главе мы реализуем компонент Delphi с весьма подходящим именем CsShopper, выполняющий функции FTP-клиента. Компонент CsShopper построен на основе CsSocket ≈ простейшего компонента-оболочки для функций Winsock API, созданного в главе 5. CsSocket обеспечивает базовые возможности, необходимые для работы протокола FTP в сети TCP/IP. Таким образом, о мелочах есть кому позаботиться, и мы можем сразу же прейти к более пристальному рассмотрению процесса FTP глазами клиента. Вас обслуживают?По умолчанию FTP-сервер всегда ожидает, что клиент инициирует соедине ние через TCP-порт с номером 21. Это соединение (оно называется управляющим соединением, control connection) остается открытым до тех пор, пока либо клиент, либо сервер не закроет его со своей стороны. Через установлен ное соединение клиент и сервер обмениваются командами FTP и кодами ответов соответственно. В командах Internet-протоколов обычно используется обычный англоязычный текст (чаще всего в верхнем регистре). Это остается справедливым даже при взаимодействиях между программами. Причина заключается в том, что Internet первоначально работал только с 7-разрядной ASCII-кодировкой, которая была (и остается) ╚наименьшим общим знамена телем╩ для общения двух систем ≈ компьютерных или любых других. Это обстоятельство не лучшим образом сказывается на скорости работы, но зато человеку становится значительно легче уследить за взаимодействием двух Internet-программ. На каждую команду, полученную от клиента, сервер обычно посылает код ответа. Код состоит из трех цифр, за которыми следует дефис или пробел, а затем ≈ некоторый текст. Типичные сообщения могут выглядеть следующим образом: 200 PORT command successful. Дефис или пробел, следующий за числовым кодом, содержит важную для клиента информацию. Дефис сообщает клиенту о том, что данное сообщение является комментарием и его можно спокойно игнорировать. Пробел указывает клиенту на необходимость перехода к следующей фазе текущей операции. Текст, который идет дальше, обычно содержит информацию о статусе или инструкцию для пользователя. Диаграмма, изображенная на рис. 6.1, описывает взаимодействие клиента с сервером во время регистрации. FTP-сеанс начинается с посылки клиентом команды USER, за которой следует имя пользователя, и получения со стороны сервера кода ответа, состоящего из трех цифр. Если имя пользовате ля признается допустимым, сервер отвечает кодом 331 или 230. При недопустимом имени пользователя генерируется код 4xx или 5xx, где xx описывает код конкретной ошибки. Ответ 230 означает, что имя пользователя признано допустимым и для доступа к системе не требуется никакой дополнительной информации. Сервер обычно выдает этот код в ответ при знаменитой ╚анонимной╩ регистрации пользователей. Ответ 331 означает, что имя пользователя также признано допустимым, но для доступа к системе необходим пароль. В этом случае клиент посылает команду PASS, за которой следует пароль. Неверный пароль вызывает ответ 4xx или 5xx, свидетельствующий об ошибке. Если пароль принят, сервер может послать код 230, чтобы сообщить о завершении регистрации. Если для регистрации необходимы сведения об используемых ресурсах (account), сервер снова отвечает кодом 331, чтобы клиент послал команду ACCT и требуемые сведения.
Рис. 6.1. Регистрация FTP-клиента на FTP-сервере После того как соединение будет успешно установлено, клиент может продолжить посылку команд. Однако при возникновении проблемы (например, посылке команды с неверным синтаксисом) или слишком большом количестве пользователей, работающих в системе, сервер посылает код 4xx или 5xx и закрывает соединение. Компонент CsShopperCsShopper происходит от VCL-компонента CsSocket из главы 5. В нем класс TCsSocket используется для выполнения повседневных задач ≈ загрузки Winsock DLL, заполнения структур данных для установки соединения с хостом, пересылки данных, разрыва соединения с сервером и последующего закрытия Winsock. Свойство Service базового VCL-компонента CsSocket имеет значение NoService. Компонент CsShopper всегда выполняет функции FTP-клиента, поэтому в конструкторе TCsShopper.Create свойство Service получает значение FTP. В остальном протокол FTP использует стандартные настройки CsSocket ≈ все-таки отличная штука эти компоненты! Как показано на рис. 6.2, помимо Service CsShopper содержит 10 других свойств: Access, AddrType, Asynchronous, Debug, HomeServer, LogOn, Password, Protocol, SockType и UserName.
Рис. 6.2. Свойства CsShopper в инспекторе Свойство Asynchronous определяет режим работы CsShopper ≈ блокирующий или асинхронный . Хотя данное свойство не относится к протоколу FTP, выбор режима может повлиять на скорость пересылки данных, надежность приложения и его гибкость. Например, когда CsShopper работает в асинхронном режиме (то есть свойство Asynchronous равно TRUE), пользователь может прервать чересчур затянувшуюся пересылку файла. В блокирующем режиме такая возможность отсутствует (впрочем, если ChShopper написан как многопоточное приложение, то пересылку файла можно прервать и в блокирующем режиме, но это совсем другая история). Асинхронный режим устроен несколько сложнее, поэтому сначала мы посмотрим, как CsShopper работает в блокирующем режиме. Асинхронный режим будет описан позднее в этой главе. Самые полезные FTP-команды (в том числе USER, PASSWORD, RETR и PUT) реализованы в CsShopper в виде свойств. Эти свойства находятся в public-секции TCsShopper и потому доступны для пользователей компонента. В блокирующем режиме соответствующие методы используют процедуру FTPCommand, которая является ╚сердцем╩ компонента CsShopper. FTPCommand представляет собой простейший анализатор, реализованный в виде большого оператора case. Недостаток изящества подобной конструкции возмещается ее простотой. В асинхронном режиме CsShopper использует другой подход. Полный исходный текст компонента,
находящийся в файле CSSHOPPER.PAS, занимает около
3000 строк, и я не стал включать его в эту главу.
Будут приведены лишь отдельные фрагменты,
поясняющие некоторые аспекты его работы. Для
более подробного знакомства вы можете
распечатать полный файл Организация выводаХотя CsShopper и относится к невизуальным компонентам, время от времени ему приходится взаимодействовать с приложением пользователя и отображать сообщения, которыми сервер обменивается с клиентом. Такую возможность предоставляет published-свойство OnInfo класса TCsShopper (унаследован ное от класса TCsSocket) и private-процедура InfoEvent. Процедура InfoEvent выглядит следующим образом: procedure TCsSocket.InfoEvent(Msg : String); Когда через управляющее соединение отправляется или принимается сообщение, локальная переменная TempStr в процедуре FTPCommand задает значение свойства Info, после чего FTPCommand вызывает процедуру InfoEvent. Внутри InfoEvent проверка Assignеd возвращает значение True, а процедура CsShopper1Info из приложения отображает Info. Чтобы такое взаимодействие между CsShopper и клиентским приложением стало возможным, я создал процедуру CsShopper1Info с помощью вкладки Events инспектора объектов. Содержимое окна memLog, в котором отображаются все эти сообщения, обновляется с каждым событием FOnInfoEvent. CsShopper1Info содержит следующий фрагмент кода: procedure TfrmMain.CsShopper1Info(Sender: TObject; Msg: String); SHOPPER32 за работойSHOPPER32 ≈ базовое FTP-приложение, созданное с помощью компонента CsShopper, оно изображено на рис. 6.3. Создайте новый проект с именем SHOPPER32, вызовите главную форму frmMain и сохраните в модуле MAIN.PAS содержимое листинга 6.1. Листинг 6.1. Модуль MAIN.PAS (* Модуль Main Написан для книги High Performance Delphi Programming - Джон К.Пенман 1997 За дополнительной информацией и помощью обращайтесь по адресу info@craiglockhart.com *) unit main; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Buttons, FileCtrl, ComCtrls, CsSocket, CsShopper, MkDirFrm, CsFtpMsg, ToolWin, Registry, ExtCtrls;
Рис. 6.3. Приложение SHOPPER32 type TfrmMain = class(TForm) CsShopper1: TCsShopper; pcShopper: TPageControl; tsConnect: TTabSheet; tsOptions: TTabSheet; tsAbout: TTabSheet; gbLocal: TGroupBox; gbRemote: TGroupBox; gbActions: TGroupBox; dcbLocal: TDriveComboBox; dlbLocal: TDirectoryListBox; flbLocal: TFileListBox; sbStatus: TStatusBar; pbDataTransfer: TProgressBar; lbRemoteFiles: TListBox; bbtnExit: TBitBtn; bbtnConnect: TBitBtn; bbtnAbort: TBitBtn; gbUserName: TGroupBox; gbPassword: TGroupBox; gbDefLocalDir: TGroupBox; gbDefTextEditor: TGroupBox; edDefUserName: TEdit; edDefPassword: TEdit; edDefLocalDir: TEdit; edDefTextEditor: TEdit; bbtnFtpCmds: TBitBtn; bbtnLocateTxtEditor: TBitBtn; bbtnLocateDefLocalDir: TBitBtn; gbMoreActions: TGroupBox; bbtnRefresh: TBitBtn; bbtnFTPHelp: TBitBtn; bbtnSite: TBitBtn; bbtnNewDir: TBitBtn; bbtnDelDir: TBitBtn; bbtnViewFile: TBitBtn; memLog: TMemo; rgFileType: TRadioGroup; bbtnRestart: TBitBtn; bbtnQuit: TBitBtn; tsProfiles: TTabSheet; gbSetProfile: TGroupBox; gbPrName: TGroupBox; gbPrHostName: TGroupBox; gbPrUserName: TGroupBox; gbPrPassWord: TGroupBox; gbPrRemDir: TGroupBox; gbPrLocDir: TGroupBox; edPrName: TEdit; edPrHostName: TEdit; edPrUserName: TEdit; edPrPassword: TEdit; edPrRemDir: TEdit; edPrLocDir: TEdit; gbPrList: TGroupBox; lbPrList: TListBox; bbtnPrNew: TBitBtn; bbtnPrSave: TBitBtn; bbtnPrDelete: TBitBtn; rgFTPMode: TRadioGroup; sbbtnRetr: TSpeedButton; sbbtnStor: TSpeedButton; Panel1: TPanel; Label1: TLabel; Label2: TLabel; Label3: TLabel; Label4: TLabel; Label5: TLabel; bbtnStat: TBitBtn; gbHints: TGroupBox; cbHints: TCheckBox; gbFTPOptions: TGroupBox; BitBtn2: TBitBtn; rgFileStructure: TRadioGroup; rgTransfer: TRadioGroup; bbtnAddNew: TBitBtn; procedure bbtnConnectClick(Sender: TObject); procedure FormCreate(Sender: TObject); procedure bbtnFtpCmdsClick(Sender: TObject); procedure CsShopper1Info(Sender: TObject; Msg: String); procedure CsShopper1UpDateList(Sender: TObject; List: TStringList); procedure lbRemoteFilesDblClick(Sender: TObject); procedure CsShopper1List(Sender: TObject; List: TStringList); procedure bbtnSiteClick(Sender: TObject); procedure bbtnFTPHelpClick(Sender: TObject); procedure CsShopper1Busy(Sender: TObject; BusyFlag: Boolean); procedure CsShopper1Progress(Sender: TObject; Position: Integer); procedure rgFileTypeClick(Sender: TObject); procedure CsShopper1FileType(Sender: TObject; FileType: TFileTypes); procedure CsShopper1Error(Sender: TObject; Status: TConditions; Msg: String); procedure bbtnNewDirClick(Sender: TObject); procedure bbtnDelDirClick(Sender: TObject); procedure CsShopper1Connect(Sender: TObject; sSocket: Integer); procedure bbtnQuitClick(Sender: TObject); procedure rgFTPModeClick(Sender: TObject); procedure bbtnRefreshClick(Sender: TObject); procedure sbbtnRetrClick(Sender: TObject); procedure sbbtnStorClick(Sender: TObject); procedure CsShopper1DataDone(Sender: TObject; Done: Boolean); procedure bbtnStatClick(Sender: TObject); procedure bbtnRestartClick(Sender: TObject); procedure flbLocalDblClick(Sender: TObject); procedure lbRemoteFilesClick(Sender: TObject); procedure flbLocalClick(Sender: TObject); procedure lbPrListDblClick(Sender: TObject); procedure bbtnConnectMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); procedure bbtnViewFileClick(Sender: TObject); procedure bbtnAbortClick(Sender: TObject); procedure bbtnPrSaveClick(Sender: TObject); procedure bbtnExitClick(Sender: TObject); procedure lbPrListClick(Sender: TObject); procedure bbtnPrNewClick(Sender: TObject); procedure bbtnAddNewClick(Sender: TObject); procedure edPrNameExit(Sender: TObject); procedure edPrHostNameExit(Sender: TObject); procedure edPrUserNameExit(Sender: TObject); procedure edPrPasswordExit(Sender: TObject); procedure edPrRemDirExit(Sender: TObject); procedure edPrLocDirExit(Sender: TObject); procedure bbtnPrDeleteClick(Sender: TObject); procedure bbtnLocateDefLocalDirClick(Sender : TObject); procedure bbtnLocateTxtEditorClick(Sender: TObject); procedure BitBtn2Click(Sender: TObject); private { Private declarations } public { Public declarations } HelpCmd : String; UsedProfile, UsedQFTP, NewProfile : Boolean; OldTransferMode, OldFileStruct : String; OldProfiles, HostNameList, UsernameList, PasswordList, RemoteDirList, LocalDirList, CurrentProfiles, ProfileNameList : TStringList; NoOfUsers, LastProfileUsed, NoProfiles : Integer; procedure LoadSettings; procedure SaveOptions; procedure SaveProfiles; end; var frmMain: TfrmMain; implementation uses RMDirFrm, HelpFrm, QuickFTPfrm, LocateDirFrm, LocateEdFrm; {$R *.DFM} const FtpClientKey = 'Software\High Performance Delphi\Shopper32'; procedure TfrmMain.LoadSettings; var Reg : TRegistry; Count : Integer; ProfileName : String; begin Reg := TRegistry.Create; // Считываем имя пользователя по умолчанию try Reg.OpenKey(FtpClientKey, TRUE); if Reg.ValueExists('UserName') then edDefUserName.Text := Reg.ReadString('UserName') else edDefUserName.Text := 'anonymous'; finally Reg.CloseKey; end; // Считываем пароль по умолчанию try Reg.OpenKey(FtpClientKey, TRUE); if Reg.ValueExists('Password') then edDefPassword.Text := Reg.ReadString('Password') else edDefPassword.Text := 'guest'; finally Reg.CloseKey; end; // Считываем локальный каталог по умолчанию try Reg.OpenKey(FtpClientKey, TRUE); if Reg.ValueExists('DefLocalDir') then edDefLocalDir.Text := Reg.ReadString('DefLocalDir') else edDefLocalDir.Text := 'C:\'; finally Reg.CloseKey; end; // Считываем редактор, используемый по умолчанию try Reg.OpenKey(FtpClientKey, TRUE); if Reg.ValueExists('Editor') then edDefTextEditor.Text := Reg.ReadString('Editor') else edDefTextEditor.Text := 'NOTEPAD '; finally Reg.CloseKey; end; // Задаем свойства try Reg.OpenKey(FtpClientKey, TRUE); if Reg.ValueExists('Asynchronous') then begin with CsShopper1 do begin Asynchronous := Reg.ReadBool('Asynchronous'); if Asynchronous then rgFTPMode.ItemIndex := 0 else rgFTPMode.ItemIndex := 1; end; end else begin CsShopper1.Asynchronous := FALSE; rgFTPMode.ItemIndex := 0; end; finally Reg.CloseKey; end; try Reg.OpenKey(FtpClientKey, TRUE); if Reg.ValueExists('Hints') then cbHints.Checked := Reg.ReadBool('Hints') else cbHints.Checked := FALSE; finally Reg.CloseKey; end; try Reg.OpenKey(FtpClientKey, TRUE); if Reg.ValueExists('DTransferMode') then begin OldTransferMode := Reg.ReadString('DTransferMode'); if UpperCase(OldTransferMode) = UpperCase(FtpTransferStr[STREAM]) then begin CsShopper1.Transfer := STREAM; rgTransfer.ItemIndex := 0; end; if UpperCase(OldTransferMode) = UpperCase(FtpTransferStr[BLOCK]) then begin CsShopper1.Transfer := BLOCK; rgTransfer.ItemIndex := 1; end; if UpperCase(OldTransferMode) = UpperCase(FtpTransferStr[COMPRESSED]) then begin CsShopper1.Transfer := COMPRESSED; rgTransfer.ItemIndex := 2; end; end else begin OldTransferMode := UpperCase(FtpTransferStr[STREAM]); CsShopper1.Transfer := STREAM; rgTransfer.ItemIndex := 0; end; finally Reg.CloseKey; end; // Свойство файловой структуры try Reg.OpenKey(FtpClientKey, TRUE); if Reg.ValueExists('DFileStructure') then begin OldFileStruct := Reg.ReadString('DFileStructure'); if UpperCase(OldFileStruct) = UpperCase(FtpFileStructStr[NOREC]) then begin CsShopper1.FileStruct := NOREC; rgFileStructure.ItemIndex := 0; end; if UpperCase(OldFileStruct) = UpperCase(FtpFileStructStr[REC]) then begin CsShopper1.FileStruct := REC; rgFileStructure.ItemIndex := 1; end; if UpperCase(OldFileStruct) = UpperCase(FtpFileStructStr[PAGE]) then begin CsShopper1.FileStruct := PAGE; rgFileStructure.ItemIndex := 2; end; end else begin OldFileStruct := UpperCase(FtpFileStructStr[NOREC]); CsShopper1.FileStruct := NOREC; rgFileStructure.ItemIndex := 0; end; finally Reg.CloseKey; end; try Reg.OpenKey(FtpClientKey, TRUE); if Reg.ValueExists('LastProfileUsed') then LastProfileUsed := Reg.ReadInteger('LastProfileUsed') else LastProfileUsed := 0; finally Reg.CloseKey; end; try Reg.OpenKey(FtpClientKey, TRUE); if Reg.ValueExists('NoProfiles') then NoProfiles := Reg.ReadInteger('NoProfiles') else NoProfiles := 1; finally Reg.CloseKey; end; // Список профилей for Count := 0 to NoProfiles - 1 do begin ProfileName := Concat('ProfileName', IntToStr(Count)); try Reg.OpenKey(FtpClientKey + '\Profiles' + '\ ' + ProfileName, TRUE); if Reg.ValueExists('ProfileName') then ProfileNameList.Add(Reg.ReadString ('ProfileName')) else ProfileNameList.Add('PROFILE'); OldProfiles.Add(Reg.ReadString('ProfileName')); if Reg.ValueExists('Host') then HostNameList.Add(Reg.ReadString('Host')) else HostNameList.Add('HOST'); if Reg.ValueExists('User') then UserNameList.Add(Reg.ReadString('User')) else UserNameList.Add('ANONYMOUS'); if Reg.ValueExists('Password') then PasswordList.Add(Reg.ReadString('Password')) else PasswordList.Add('GUEST'); if Reg.ValueExists('RemoteDir') then RemoteDirList.Add(Reg.ReadString('RemoteDir')) else RemoteDirList.Add('\'); if Reg.ValueExists('LocalDir') then LocalDirList.Add('LocalDir') else LocalDirList.Add('\'); finally Reg.CloseKey; end; end; // цикл for Reg.Free; lbPrList.Items := ProfileNameList; lbPrList.ItemIndex := LastProfileUsed; edPrName.Text := ProfileNameList.Strings[lbPrList.ItemIndex]; edPrHostName.Text := HostNameList.Strings[lbPrList.ItemIndex]; edPrUserName.Text := UserNameList.Strings[lbPrList.ItemIndex]; edPrPassword.Text := PasswordList.Strings[lbPrList.ItemIndex]; edPrRemDir.Text := RemoteDirList.Strings[lbPrList.ItemIndex]; edPrLocDir.Text := LocalDirList.Strings[lbPrList.ItemIndex]; CsShopper1.UserName := edPrUserName.Text; CsShopper1.Password := edPrPassword.Text; lbPrList.Refresh; end; procedure TfrmMain.SaveProfiles; var Reg : TRegistry; Count : Integer; ProfileName : String; begin Reg := TRegistry.Create; try Reg.OpenKey(FtpClientKey, TRUE); Reg.WriteInteger('LastProfileUsed', LastProfileUsed); finally Reg.CloseKey; end; NoProfiles := lbPrList.Items.Count; try Reg.OpenKey(FtpClientKey, TRUE); Reg.WriteInteger('NoProfiles',NoProfiles); finally Reg.CloseKey; end; for Count := 0 to NoProfiles - 1 do begin ProfileName := Concat('ProfileName', IntToStr(Count)); try Reg.OpenKey(FtpClientKey + '\Profiles' + '\ ' + ProfileName, TRUE); Reg.WriteString('ProfileName', lbPrList.Items.Strings[Count]); Reg.WriteString('ProfileName', ProfileNameList.Strings[Count]); Reg.WriteString('Host', HostNameList.Strings[Count]); Reg.WriteString('User', UserNameList.Strings[Count]); Reg.WriteString('Password', PasswordList.Strings[Count]); Reg.WriteString('RemoteDir', RemoteDirList.Strings[Count]); Reg.WriteString('LocalDir', LocalDirList.Strings[Count]); finally Reg.CloseKey; end; end; Reg.Free; end; procedure TfrmMain.SaveOptions; var Reg : TRegistry; begin Reg := TRegistry.Create; // Сохраняем имя пользователя по умолчанию try Reg.OpenKey(FtpClientKey, TRUE); Reg.WriteString('UserName', edDefUserName.Text); finally Reg.CloseKey; end; // Сохраняем пароль по умолчанию try Reg.OpenKey(FtpClientKey, TRUE); Reg.WriteString('Password', edDefPassword.Text); finally Reg.CloseKey; end; // Сохраняем локальный каталог по умолчанию try Reg.OpenKey(FtpClientKey, TRUE); Reg.WriteString('DefLocalDir', edDefLocalDir.Text); finally Reg.CloseKey; end; // Сохраняем редактор, используемый по умолчанию try Reg.OpenKey(FtpClientKey, TRUE); Reg.WriteString('Editor', edDefTextEditor.Text); finally Reg.CloseKey; end; try Reg.OpenKey(FtpClientKey,TRUE); case rgFTPMode.ItemIndex of 0 : Reg.WriteBool('Asynchronous',TRUE); 1 : Reg.WriteBool('Asynchronous',FALSE); end; finally Reg.CloseKey; end; try Reg.OpenKey(FtpClientKey, TRUE); if cbHints.Checked then Reg.WriteBool('Hints',TRUE) else Reg.WriteBool('Hints',FALSE); finally Reg.CloseKey; end; try Reg.OpenKey(FtpClientKey,TRUE); case rgTransfer.ItemIndex of 0 :Reg.WriteString('DTransferMode', FtpTransferStr[STREAM]); 1 :Reg.WriteString('DTransferMode', FtpTransferStr[BLOCK]); 2 :Reg.WriteString('DTransferMode', FtpTransferStr[COMPRESSED]); end; finally Reg.CloseKey; end; try Reg.OpenKey(FtpClientKey,TRUE); case rgFileStructure.ItemIndex of 0 :Reg.WriteString('DFileStructure', FtpFileStructStr[NOREC]); 1 :Reg.WriteString('DFileStructure', FtpFileStructStr[REC]); 2 :Reg.WriteString('DFileStructure', FtpFileStructStr[PAGE]); end; finally Reg.CloseKey; end; Reg.Free; end; procedure TfrmMain.bbtnConnectClick(Sender: TObject); begin if (not UsedQFtp) and (not UsedProfile) then begin with CsShopper1 do begin HostName := HomeServer; if Status = Success then Start; end; end else if UsedQFtp then CsShopper1.Start else if UsedProfile then begin with CsShopper1 do begin UserName := edPrUserName.Text; Password := edPrPassword.Text; RemoteDir:= edPrRemDir.Text; LocalDir := edPrLocDir.Text; EditName := edDefTextEditor.Text; HostName := edPrHostName.Text; if Status = Success then Start; end; end; end; procedure TfrmMain.FormCreate(Sender: TObject); begin bbtnQuit.Enabled := FALSE; bbtnRefresh.Enabled := FALSE; bbtnViewFile.Enabled := FALSE; bbtnFtpCmds.Enabled := FALSE; bbtnAbort.Enabled := FALSE; rgFileType.Enabled := FALSE; gbMoreActions.Visible := FALSE; pbDataTransfer.Visible := FALSE; sbbtnRetr.Enabled := FALSE; sbbtnStor.Enabled := FALSE; OldProfiles := TStringList.Create; ProfileNameList := TStringList.Create; HostNameList := TStringList.Create; UserNameList := TStringList.Create; PasswordList := TStringList.Create; RemoteDirList := TStringList.Create; LocalDirList := TStringList.Create; LoadSettings; if CsShopper1.Asynchronous then begin sbStatus.Panels[2].Text := Concat('Mode : ', 'Asynchronous'); rgFTPMode.ItemIndex := 0; end else begin sbStatus.Panels[2].Text := Concat('Mode : ', 'Non-Asynchronous'); rgFTPMode.ItemIndex := 1; end; sbStatus.Panels[0].Text := Concat('Local Host : ', CsShopper1.LocalName); sbStatus.Panels[3].Text := Concat('Status : ', 'Idle'); pcShopper.ActivePage := tsProfiles; UpDate; end; procedure TfrmMain.bbtnFtpCmdsClick(Sender: TObject); begin gbMoreActions.Visible := not gbMoreActions.Visible; if gbMoreActions.Visible then begin bbtnFtpCmds.Hint := 'Click here to close the panel of FTP commands'; bbtnFtpCmds.Caption := 'Close'; end else begin bbtnFtpCmds.Hint := 'Click here to get more FTP commands'; bbtnFtpCmds.Caption := 'FTP Cmds'; end; end; procedure TfrmMain.CsShopper1Info(Sender: TObject; Msg: String); begin memLog.Lines.Add(Msg); end; procedure TfrmMain.CsShopper1UpDateList(Sender: TObject; List: TStringList); begin LbRemoteFiles.Items := List; lbRemoteFiles.UpDate; gbRemote.Caption := Concat('Files on ', CsShopper1.HostName); sbStatus.Panels[1].Text := Concat('Remote Host : ',CsShopper1.HostName); end; procedure TfrmMain.lbRemoteFilesDblClick (Sender: TObject); begin pbDataTransfer.Visible := TRUE; if lbRemoteFiles.ItemIndex <> -1 then CsShopper1.Get := lbRemoteFiles.Items.Strings [lbRemoteFiles.ItemIndex] else pbDataTransfer.Visible := FALSE; end; procedure TfrmMain.CsShopper1List (Sender: TObject; List: TStringList); begin lbRemoteFiles.Clear; lbRemoteFiles.Items := List; lbRemoteFiles.UpDate; gbRemote.Caption := CsShopper1.RemoteDir; end; procedure TfrmMain.bbtnSiteClick(Sender: TObject); begin CsShopper1.SiteFtp; end; procedure TfrmMain.bbtnFTPHelpClick(Sender: TObject); var Counter : Integer; begin frmHelp := TfrmHelp.Create(Application); for Counter := SFtpUser to SFtpNoop do frmHelp.lbHelpFtpCmds.Items.Add (LoadStr(Counter)); frmHelp.ShowModal; CsShopper1.FtpHelp := HelpCmd; HelpFtpCmdList.Free; frmHelp.Free; end; procedure TfrmMain.CsShopper1Busy (Sender: TObject; BusyFlag: Boolean); begin if BusyFlag then begin lbRemoteFiles.Enabled := FALSE; sbStatus.Panels[3].Text := Concat('Status : ','Busy'); end else begin lbRemoteFiles.Enabled := TRUE; sbStatus.Panels[3].Text := Concat('Status : ','Idle'); end; Update; end; procedure TfrmMain.CsShopper1Progress (Sender: TObject; Position: Integer); begin pbDataTransfer.Position := Position; pbDataTransfer.UpDate; end; procedure TfrmMain.rgFileTypeClick (Sender: TObject); begin with CsShopper1 do case rgFileType.ItemIndex of 0 : FileType := ASCII; 1 : FileType := IMAGE; 2 : FileType := AUTO; end; end; procedure TfrmMain.CsShopper1FileType (Sender: TObject; FileType: TFileTypes); begin case FileType of ASCII : rgFileType.ItemIndex := 0; IMAGE : rgFileType.ItemIndex := 1; AUTO : rgFileType.ItemIndex := 2; end; end; procedure TfrmMain.CsShopper1Error (Sender: TObject; Status: TConditions; Msg: String); begin memLog.Lines.Add(Msg); end; procedure TfrmMain.bbtnNewDirClick (Sender: TObject); begin frmMkNewDir := TfrmMkNewDir.Create(Application); frmMkNewDir.ShowModal; if Length(NewDirName) > 0 then CsShopper1.MkDirName := NewDirName; frmMkNewDir.Free; end; procedure TfrmMain.bbtnDelDirClick(Sender: TObject); begin if lbRemoteFiles.ItemIndex <> -1 then CsShopper1.RmDirName := emoteFiles.Items.Strings[lbRemoteFiles.ItemIndex]; CsShopper1.FilesList; end; procedure TfrmMain.CsShopper1Connect(Sender: TObject; sSocket: Integer); begin bbtnQuit.Enabled := TRUE; bbtnRefresh.Enabled := TRUE; bbtnViewFile.Enabled := TRUE; bbtnFtpCmds.Enabled := TRUE; rgFileType.Enabled := TRUE; if rgFTPMode.ItemIndex = 1 then begin sbbtnRetr.Enabled := TRUE; sbbtnStor.Enabled := TRUE; end else begin sbbtnRetr.Enabled := FALSE; sbbtnStor.Enabled := FALSE; end; bbtnConnect.Enabled := FALSE; bbtnExit.Enabled := FALSE; rgFTPMode.Enabled := FALSE; gbRemote.Caption := 'Remote : ' + CsShopper1.RemoteDir; sbStatus.Panels[1].Text := 'Remote Host : ' + CsShopper1.HostName; sbStatus.Panels[3].Text := 'Status : Connected'; Update; end; procedure TfrmMain.bbtnQuitClick(Sender: TObject); begin bbtnQuit.Enabled := FALSE; bbtnRefresh.Enabled := FALSE; bbtnViewFile.Enabled := FALSE; bbtnFtpCmds.Enabled := FALSE; bbtnAbort.Enabled := FALSE; rgFileType.Enabled := FALSE; sbbtnRetr.Enabled := FALSE; sbbtnStor.Enabled := FALSE; gbMoreActions.Visible := FALSE; pbDataTransfer.Visible := FALSE; bbtnConnect.Enabled := TRUE; bbtnExit.Enabled := TRUE; rgFTPMode.Enabled := TRUE; with sbStatus do begin Panels[1].Text := 'Remote Host : '; Panels[3].Text := 'Status : Idle'; end; lbRemoteFiles.Clear; Update; CsShopper1.Finish; end; (* procedure TfrmMain.Exit1Click(Sender: TObject); begin Close; end; *) procedure TfrmMain.rgFTPModeClick(Sender: TObject); begin if rgFTPMode.ItemIndex = 0 then begin CsShopper1.Asynchronous := TRUE; sbStatus.Panels[2].Text := 'Mode : ' + 'Asynchronous'; sbbtnRetr.Enabled := FALSE; sbbtnStor.Enabled := FALSE; end else begin CsShopper1.Asynchronous := FALSE; sbStatus.Panels[2].Text := 'Mode : ' + 'Non-Asynchronous'; sbbtnRetr.Enabled := TRUE; sbbtnStor.Enabled := TRUE; end; sbStatus.Update; end; procedure TfrmMain.bbtnRefreshClick(Sender: TObject); begin CsShopper1.FilesList end; procedure TfrmMain.sbbtnRetrClick(Sender: TObject); begin pbDataTransfer.Visible := TRUE; bbtnAbort.Enabled := TRUE; CsShopper1.MGet; end; procedure TfrmMain.sbbtnStorClick(Sender: TObject); begin pbDataTransfer.Visible := TRUE; bbtnAbort.Enabled := TRUE; CsShopper1.MPut; end; procedure TfrmMain.CsShopper1DataDone(Sender: TObject; Done: Boolean); begin if Done then begin pbDataTransfer.Visible := FALSE; bbtnAbort.Enabled := FALSE end else begin pbDataTransfer.Visible := TRUE; bbtnAbort.Enabled := TRUE end; pbDataTransfer.Update; end; procedure TfrmMain.bbtnStatClick(Sender: TObject); begin CsShopper1.Stat; end; procedure TfrmMain.bbtnRestartClick(Sender: TObject); begin ShowMessage('Not implemented in this version'); end; procedure TfrmMain.flbLocalDblClick(Sender: TObject); begin pbDataTransfer.Visible := TRUE; if flbLocal.ItemIndex <> -1 then CsShopper1.Put := flbLocal.Items.Strings[flbLocal.ItemIndex] else pbDataTransfer.Visible := FALSE; end; procedure TfrmMain.lbRemoteFilesClick(Sender: TObject); begin CsShopper1.RemoteFiles.Add (lbRemoteFiles.Items.Strings [lbRemoteFiles.ItemIndex]); end; procedure TfrmMain.flbLocalClick(Sender: TObject); begin CsShopper1.LocalFiles.Add (flbLocal.Items.Strings[flbLocal.ItemIndex]); end; procedure TfrmMain.lbPrListDblClick(Sender: TObject); begin UsedProfile := TRUE; pcShopper.ActivePage := tsConnect; ActiveControl := bbtnConnect; bbtnConnect.Click; end; procedure TfrmMain.bbtnConnectMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); begin if Button = mbRight then // Выполняем упрощенный ftp begin UsedQFtp := TRUE; UsedProfile := FALSE; frmQuickFtp := TfrmQuickFTP.Create(Application); frmQuickFtp.ShowModal; with CsShopper1 do begin UserName := frmQuickFtp.edUserName.Text; Password := frmQuickFtp.edPassword.Text; HostName := frmQuickFtp.edHostName.Text; end; frmQuickFtp.Free; ActiveControl := bbtnConnect; bbtnConnect.Click; end else UsedQFtp := FALSE; end; procedure TfrmMain.bbtnViewFileClick(Sender: TObject); begin if lbRemoteFiles.ItemIndex <> -1 then CsShopper1.View := lbRemoteFiles.Items.Strings [lbRemoteFiles.ItemIndex]; end; procedure TfrmMain.bbtnAbortClick(Sender: TObject); begin CsShopper1.Abort; bbtnAbort.Enabled := FALSE; end; procedure TfrmMain.bbtnPrSaveClick(Sender: TObject); begin SaveProfiles; end; procedure TfrmMain.bbtnExitClick(Sender: TObject); begin OldProfiles.Free; ProfileNameList.Free; HostNameList.Free; UserNameList.Free; PasswordList.Free; RemoteDirList.Free; LocalDirList.Free; end; procedure TfrmMain.lbPrListClick(Sender: TObject); begin if lbPrList.ItemIndex <> -1 then begin LastProfileUsed := lbPrList.ItemIndex; edPrName.Text := ProfileNameList.Strings[LastProfileUsed]; edPrHostName.Text := HostNameList.Strings[LastProfileUsed]; edPrUserName.Text := UserNameList.Strings[LastProfileUsed]; edPrPassword.Text := PasswordList.Strings[LastProfileUsed]; edPrRemDir.Text := RemoteDirList.Strings[LastProfileUsed]; edPrLocDir.Text := LocalDirList.Strings[LastProfileUsed]; Update; end; end; procedure TfrmMain.bbtnPrNewClick(Sender: TObject); begin NewProfile := TRUE; edPrName.Text := ''; edPrHostName.Text := ''; edPrUserName.Text := edDefUserName.Text; edPrPassword.Text := edDefPassword.Text; edPrLocDir.Text := edDefLocalDir.Text; edPrRemDir.Text := '\'; lbPrList.Visible := FALSE; end; procedure TfrmMain.bbtnAddNewClick(Sender: TObject); begin ProfileNameList.Add(edPrName.Text); HostNameList.Add(edPrHostName.Text); UserNameList.Add(edPrUserName.Text); PasswordList.Add(edPrPassword.Text); RemoteDirList.Add(edPrRemDir.Text); LocalDirList.Add(edPrLocDir.Text); lbPrList.Items.Add(edPrName.Text); lbPrList.Visible := TRUE; lbPrList.refresh; NewProfile := FALSE; end; procedure TfrmMain.edPrNameExit(Sender: TObject); begin if (edPrName.Modified) and (not NewProfile) then begin lbPrList.Items.Strings[lbPrList.ItemIndex] := edPrName.Text; lbPrList.Refresh; ProfileNameList.Strings[lbPrList.ItemIndex] := edPrName.Text; end; end; procedure TfrmMain.edPrHostNameExit(Sender: TObject); begin if (edPrHostName.Modified) and (not NewProfile) then HostNameList.Strings[lbPrList.ItemIndex] := edPrHostName.Text; end; procedure TfrmMain.edPrUserNameExit(Sender: TObject); begin if (edPrUserName.Modified) and (not NewProfile) then UserNameList.Strings[lbPrList.ItemIndex] := edPrUserName.Text; end; procedure TfrmMain.edPrPasswordExit(Sender: TObject); begin if (edPrPassword.Modified) and (not NewProfile) then PasswordList.Strings[lbPrList.ItemIndex] := edPrPassword.Text; end; procedure TfrmMain.edPrRemDirExit(Sender: TObject); begin if (edPrRemDir.Modified) and (not NewProfile) then RemoteDirList.Strings[lbPrList.ItemIndex] := edPrRemDir.Text; end; procedure TfrmMain.edPrLocDirExit(Sender: TObject); begin if (edPrLocDir.Modified) and (not NewProfile) then LocalDirList.Strings[lbPrList.ItemIndex] := edPrLocDir.Text; end; procedure TfrmMain.bbtnPrDeleteClick(Sender: TObject); var Reg : TRegistry; Profile : String; begin Reg := TRegistry.Create; Profile := Concat('ProfileName',IntToStr (lbPrList.ItemIndex)); if Reg.DeleteKey(FtpClientKey + '\Profiles\' + Profile) then begin ProfileNameList.Delete(lbPrList.ItemIndex); HostNameList.Delete(lbPrList.ItemIndex); UserNameList.Delete(lbPrList.ItemIndex); PasswordList.Delete(lbPrList.ItemIndex); RemoteDirList.Delete(lbPrList.ItemIndex); LocalDirList.Delete(lbPrList.ItemIndex); lbPrList.Items.Delete(lbPrList.ItemIndex); edPrName.Clear; edPrHostName.Clear; edPrUserName.Clear; edPrRemDir.Clear; edPrLocDir.Clear; NoProfiles := lbPrList.Items.Count; lbPrList.Refresh; end; Reg.Free; end; procedure TfrmMain.bbtnLocateDefLocalDirClick (Sender: TObject); begin frmLocateDir := TfrmLocateDir.Create(Application); frmLocateDir.ShowModal; edDefLocalDir.Text := frmLocateDir.LocateDir; frmLocateDir.Free; end; procedure TfrmMain.bbtnLocateTxtEditorClick(Sender: TObject); begin frmLocateEditor := TfrmLocateEditor.Create (Application); frmLocateEditor.ShowModal; edDefTextEditor.Text := frmLocateEditor.EditorPath; frmLocateEditor.Free; end; procedure TfrmMain.BitBtn2Click(Sender: TObject); begin SaveOptions; end; end. Не забудьте предварительно включить CsSocket и CsShopper в палитру компонентов. Поместите компонент CsShopper на главную форму. Создайте на форме кнопку для каждой команды FTP. Например, кнопка Connect вызывает процедуру CsShopper1.Start: procedure TfrmMain.bbtnConnectClick(Sender: TObject); begin Перед тем как подключаться к FTP-серверу с помощью программы SHOPPER32, вы должны создать на вкладке Profiles некий ╚профиль╩, включающий имя FTP-сервера, а также пользовательское имя и пароль для регистрации (см. рис. 6.4). Профили сохраняются в системном реестре Windows и извлекаются из него перед регистрацией, чтобы вам не пришлось всякий раз вводить информацию для доступа к FTP-серверу. Чтобы добавить новый профиль, нажмите кнопку New; при этом стирается содержимое всех текстовых полей на вкладке Profiles. Затем введите имя профиля, имя FTP-сервера, имя пользователя и пароль в текстовых полях edPrName, edPrHostName, edPrUserName и edPrPassword соответственно. Для анонимной регистрации следует ввести в поле edPrUserName строку anonymous, а в поле edPrPassword ≈ ваш адрес электронной почты.
Рис. 6.4. Типичный вид профиля на вкладке Profiles Нажмите кнопку Add, чтобы внести профиль в список, и затем сохраните новые данные в реестре кнопкой Save. Если потребуется удалить профиль из реестра, выделите его имя в списке Profiles и нажмите кнопку Delete. Чтобы подключиться к FTP-серверу, щелкните на имени профиля в списке Profiles, перейдите на вкладку Connect и нажмите кнопку Connect. Существует и другой, более удобный способ ≈ дважды щелкнуть на имени профиля в списке. При этом автоматически активизируется вкладка Connect, и на ней нажимается кнопка Connect, как показано в следующем фрагменте обработчика события OnDblClick для списка lbPrList: procedure TfrmMain.lbPrListDblClick(Sender: TObject); Чтобы процесс регистрации стал еще проще, мы сохраняем информацию о локальном и удаленном каталогах в текстовых полях edPrLocDir и edPrRemDir соответственно. CsShopper пользуется этой информацией для автоматиче ского, не требующего вмешательства пользователя, перехода к нужному каталогу. Чтобы обратиться к редко используемому FTP-серверу, для которого нет смысла заводить специальный профиль, активизируйте кнопку Connect (на вкладке Connect) и щелкните на ней правой кнопкой мыши ≈ на экране появится диалоговое окно Quick FTP. В нем следует ввести имя пользователя и пароль. Значения по умолчанию берутся с вкладки Options. Если они окажутся подходящими, вы сразу же начинаете сеанс работы кнопкой OK. Замечание Для получения доступа к некоторым FTP-серверам и выполнения некоторых FTP-команд (например, удаления каталога командой RMD) необходимо ввести информацию об используемом ресурсе (она посылается серверу командой ACCT). Если вы хотите работать с таким сервером, придется добавить на вкладку Profiles дополнительное текстовое поле и изменить компонент CsShopper для посылки команды ACCT с соответствующей информацией. ПодключениеПользуясь введенной информацией, метод CsShopper.Start вызывает GetHost, чтобы открыть соединение с удаленным хостом. Если вызов функции завершится неудачно, WSAErrorMsg отображает возможную причину неудачи и присваивает Status значение Failure. В противном случае Status присваивается значение Success. При успешной установке соединения CsShopper вызывает процедуру события ConnEvent (унаследованную от CsSocket), чтобы сообщить SHOPPER32 о необходимости изменения состояния кнопок. Например, кнопка Quit блокируется до момента установления соединения, а затем становится доступной. Start вызывает FTPCommand для посылки команд USER, PASS, SYST и PWD (именно в таком порядке) с соответствующими аргументами. Затем Start устанавливает соединение данных (data connection) для пересылки списка каталогов и файлов удаленного хоста, при этом порт данных для соединения задается функцией GetPort. Чтобы получить список каталогов, Start посылает команду LIST с помощью FTPCommand. Результат сохраняется, а последующий вызов Decode анализирует полученные данные и ищет в них информацию о каталогах и файлах. Замечание Механизм анализа несложен, однако описание каталогов и файлов на разных системах может выглядеть по-разному. Анализатор CsShopper работает с серверами, использующими Unix и Unix-подобные системы. Для других операционных систем он иногда выдает неверную информацию о каталогах. Decode сравнивает первый символ каждой строки файла FTPFILE.TMP с ╚d╩ (для каталогов) или два начальных символа ≈ с ╚-r╩ (для файлов). Если будет найден символ ╚d╩, Decode удаляет его, проверяет оставшуюся часть строки и преобразует ее в знакомый формат \ddd. Обратная косая черта сообщает SHOPPER32 о том, что строка содержит имя каталога. Аналогично в случае файлов Decode удаляет символы ╚-r╩ и ищет в строке имя, время, дату и размер файла, выделяя их в подстроки. Затем эти составные части переставляются так, чтобы получившаяся строка подходила для просмотра в окне списка SHOPPER32 (см. рис. 6.5). Метод FRemFiles.Add, используемый внутри Decode, читает каждую сформатированную строку и заносит ее в FRemFiles. Свойство FRemFiles представляет собой список строк, производный от класса TStringList и созданный в конструкторе TCsShopper.Create. После того как процедура Decode завершит построение списка, CsShopper передает FRemFiles процедуре TCsShopper.ChangeList, вызывающей обработчик OnList: procedure TCsShopper.ChangeList(List : TStringList);
Рис. 6.5. Отображение файлов и каталогов в SHOPPER32 Обработчик события OnList в программе SHOPPER32 обновляет содержимое списка lbRemoteFiles: procedure TfrmMain.CsShopper1List(Sender: TObject; List:TStringList); Закрываем соединениеДля завершения работы с FTP-сервером необходимо лишь разорвать соединение командой QUIT. Нажатие кнопки Quit приводит к вызову CsShopper1.Finish и завершению сеанса: procedure TfrmMain.bbtnQuitClick(Sender: TObject); begin bbtnQuit.Enabled := FALSE; bbtnRefresh.Enabled := FALSE; bbtnViewFile.Enabled := FALSE; bbtnFtpCmds.Enabled := FALSE; bbtnAbort.Enabled := FALSE; rgFileType.Enabled := FALSE; sbbtnRetr.Enabled := FALSE; sbbtnStor.Enabled := FALSE; gbMoreActions.Visible := FALSE; pbDataTransfer.Visible := FALSE; bbtnConnect.Enabled := TRUE; bbtnExit.Enabled := TRUE; with sbStatus do begin Panels[1].Text := 'Remote Host : '; Panels[3].Text := 'Status : Idle'; end; lbRemoteFiles.Clear; CsShopper1.Finish; Update; end; Прием и передача файловПрием и передача могут осуществляться как по отдельности, так и пакетами, состоящими из нескольких файлов, Сначала мы рассмотрим пересылку отдельных файлов. Она начинается двойным щелчком на имени принимаемого или передаваемого файла в списке. Ключевым моментом при этом является создание нового события. После того как вы поместите список lbRemoteFiles на вкладку Connect, создайте обработчик для его события ObDblClick на вкладке Events инспектора объектов. Это событие обрабатывается процедурой TfrmMain.lbRemoteFilesDblClick. Как показано в следующем фрагменте, в результате имя файла присваивается свойству CsShopper.Get: procedure TfrmMain.lbRemoteFilesDblClick(Sender: TObject); begin Внутри компонента CsShopper свойство Get передает имя файла в виде параметра Name процедуре Retrieve. Чтобы обеспечить правильную пересылку и сохранение файла, SetUpFileTransfer проверяет расширение файла. Для двоичных файлов (например, EXE, DLL и ZIP) SetUpFileTransfer приказывает FTP Command выдать команду TYPE IMAGE, в результате чего сервер будет пересылать файл в виде непрерывного потока байтов. Для недвоичных файлов SetUp FileTransfer выдает команду TYPE A. После того как FTP-сервер подтвердит получение команды TYPE, SetUpFileTransfer через FTPCommand посылает команду RETR имя_файла. Изменение каталогов для пересылки файловЕсли двойной щелчок был сделан на имени каталога (например, \DELPHI), то вместо пересылки SetUpFileTransfer вызывает ChangeDir, чтобы обработать переход к другому каталогу. ChangeDir в свою очередь вызывает процедуру FTP Command, которая посылает FTP-серверу команду CWD имя_каталога (скажем, CWD \DELPHI). Если сервер принимает команду, он возвращает код ответа 250. Затем ChangeDir посылает команду LIST (тоже через FTPCommand), чтобы обновить содержимое списка файлов хоста. Наконец, Decode заполняет список содержимым нового каталога. Передача файловС точки зрения внутренней логики процесс передачи файлов похож на их прием. Свойство CsShopper.Put выполняет передачу с помощью метода PutFile. Чтобы упростить передачу файла от клиента к серверу, я создал на главной форме несколько списков, производных от компонентов с вкладки Windows 3.1 палитры: dcbLocal ≈ от TDriveComboBox, dlbLocal ≈ от TDirectoryListBox и flbLocal ≈ от TFileListBox. Все эти списки синхронизированы друг с другом. При выборе в dcbLocal другого дискового устройства немедленно изменяется содержимое dlbLocal и flb Local. Как и в случае списка lbRemoteFiles, я воспользовался вкладкой Events инспектора объектов и создал новый обработчик события OnDblClick ≈ Tfrm Main.flbLocalDblClick ≈ для двойного щелчка на имени файла в списке flbLocal. Таким образом, двойной щелчок на имени передаваемого файла вызывает TfrmMain.flbLocalDblClick, в результате чего имя файла назначается свойству CsShopper1.Put. Пересылка нескольких файловВторой способ позволяет переслать сразу несколько файлов (пакет). Перед тем как начинать прием, мы выделяем файлы в списке lbRemoteFiles, щелкая на их именах. При этом в обработчике TfrmMain.lbRemoteFilesClick имена файлов заносятся в строковый список RemoteFiles. Это демонстрирует следующий фрагмент кода: procedure TfrmMain.lbRemoteFilesClick (Sender: TObject); begin CsShopper1.RemoteFiles.Add (lbRemoteFiles.Items.Strings [lbRemoteFiles.ItemIndex]); end; На рис. 6.6 видно несколько файлов, выделенных в каталоге удаленного хоста и готовых к приему. После того как будут выделены все принимаемые файлы, начинайте пересылку с помощью кнопки , расположенной вверху рядом со списком lbRemoteFiles. При этом будет вызван метод CsShopper.MGet. Соответствующий код выглядит так: procedure TfrmMain.sbbtnRetrClick(Sender: TObject); begin pbDataTransfer.Visible := TRUE; bbtnAbort.Enabled := TRUE; CsShopper1.MGet; end;
Рис. 6.6. Выделенные файлы готовы к пакетному приему Однако для того, чтобы описанная схема работала, нам придется изменить два свойства списка lbRemoteFiles в инспекторе объектов: во-первых, измените значение ExtendedSelect с FALSE на TRUE, а во-вторых, измените значение MultiSelect также с FALSE на TRUE. Если теперь щелкнуть на имени файла в списке lbRemoteFiles, оно заносится в строковый список CsShopper1.RemoteFiles (относящийся к типу TStringList). Аналогично в случае пакетной передачи вам придется изменить те же два свойства для списка flbLocal. Замечание Учтите, что возможность пакетной пересылки отсутствует в асинхронном режиме ≈ это обусловлено трудностями с синхронизацией файловых операций. Асинхронная пересылка файловПознакомившись с протоколом FTP в блокирующем (синхронном) режиме, кратко рассмотрим работу CsShopper в асинхронном режиме. Поскольку процесс регистрации на FTP-сервере подробно описан выше, наше основное внимание будет сосредоточено на пересылке, и особенно ≈ на асинхронном приеме файла с FTP-сервера. Перед тем как подключаться к FTP-серверу в асинхронном режиме, следует установить переключатель Asynchronous в групповом поле FTP Mode вкладки Options. Этот переключатель управляет режимом всего соединения; после того как SHOPPER32 подключится к FTP-серверу, групповое поле FTP Mode блокируется до окончания сеанса. Процесс выбора принимаемого файла в асинхронном режиме происходит так же, как и в блокирующем режиме; другими словами, перед вызовом Retrieve мы присваиваем имя файла свойству Get. Отличия начинаются внутри Retrieve. Определив тип файла, мы присваиваем флагу состояния FFtpCmd значение FTP_TYPEI и тем самым приказываем серверу переслать файл как непрерывный поток байтов. Команда TYPE передается через процедуру SendFtpCmd. Когда Winsock получает событие сокета FD_READ, которое происходит в результате ответа FTP-сервера на команду TYPE, он посылает процедуре FtpEvent сообщение с описанием события. В FtpEvent сообщение анализируется на предмет поиска событий FD_READ, FD_WRITE и FD_CLOSE. Для распознавания события сокета используется оператор case. При получении события FD_READ процедура InfoEvent отправляет все содержимое буфера FRcvBuffer для вывода в приложении SHOPPER32. В буфере FRcv Buffer, содержащем код ответа от сервера, ищется символ 4 или 5, свидетель ствующий об ошибке FTP. Если поиск окажется успешным, FFtpCmd присваивается значение FTP_FAIL, которое сигнализирует приложению о возникнове нии ошибки. В противном случае процедура ProcessRecvData обрабатывает FRcvBuffer и флаг состояния FFtpCmd с использованием оператора case. Так как FFtpCmd имеет значение FTP_TYPEI, ProcessRecvData вызывает процедуру ProcessTypeI, в которой выполняется подробный анализ содержимого FRcvBuffer. Следующий фрагмент кода показывает, как это делается: procedure TCsShopper.ProcessTypeI; begin case GetReplyCode(FRcvBuffer) of 200 : begin if Pos('200-',String(FRcvBuffer)) = 0 then // Сервер ждет, пока мы создадим // соединение данных и пошлем команду USER begin ProcessPort; end; { остаток кода пропущен } end; // case FillChar(FRcvBuffer, SizeOf(FRcvBuffer),#0); end; Если код ответа равен 200, вызывается процедура ProcessPort, из которой в свою очередь вызывается InitDataConn, выполняющая четыре задачи:
Если в результате вызова InitDataConn будет создан допустимый сокет данных, ProcessPort создает для соединения данных уникальный номер порта, который затем передается процедурой SendFtpCmd. Наконец, флагу состояния FFtpCmd присваивается значение FTP_RETR, которое сигнализирует CsShopper о том, что следующее событие сокета FD_READ должно анализироваться в контекс те приема файла. Когда на управляющем соединении происходит следующее событие FD_READ (при условии отсутствия ошибок сокета или отрицательных кодов ответа), вызывается процедура ProcessRecvData, которая в свою очередь инициирует ProcessGet. В ProcessGet при получении кода ответа 200 (признак успеха) создается локальный файл, имя которого совпадает с именем файла на сервере. В дальнейшем код ответа 150 сигнализирует FTP-клиенту о том, что сервер приступил к пересылке информации через соединение данных. Сразу же после того, как FTP-сервер свяжется с клиентом через соединение данных, Winsock уведомляет об этом процедуру FtpDataEvent с помощью события FD_ACCEPT. В ветви FD_ACCEPT оператора case вызывается функция WSAAsyncSelect, которая инициализирует сокет данных для приема только следующих событий: FD_READ, FD_WRITE и FD_CLOSE. Следующий фрагмент процедуры FtpDataEvent показывает, как это делается: FD_ACCEPT : begin FStartTime := GetTickCount; FIntTime := FStartTime; if FListenSocket <> INVALID_SOCKET then begin nLen := SizeOf(TSockAddr); FDataSocket := accept(FListenSocket, @FRemoteHost, @nLen); if FDataSocket = SOCKET_ERROR then begin InfoEvent(Concat('Error : ',WSAErrorMsg)); FFtpCmd := FTP_FAIL; Exit; end; nStat := WSAAsyncSelect(FDataSocket, FDataWnd, DATA_EVENT, FD_READ or FD_WRITE or FD_CLOSE); if nStat = SOCKET_ERROR then begin InfoEvent(Concat('Error : ',WSAErrorMsg)); FFtpCmd := FTP_FAIL; Exit; end; { остаток кода пропущен } end; end; При приеме первого и последнего пакета данных через соединение данных Winsock уведомляет FtpDataEvent с помощью события FD_READ, что приводит к вызову RecvData для получения и сохранения поступающих данных в локальном файле. После завершения пересылки FTP-сервер закрывает соединение данных со своей стороны, заставляя Winsock послать сообщение FD_CLOSE. На этом пересылку файла логично было бы завершить, но иногда в сокете данных FTP-клиента все еще остаются непрочитанные данные. Чтобы избежать потерь информации, мы присваиваем флагу FTransferDone значение TRUE. Все сказанное демонстрируется следующим фрагментом кода из процедуры FtpDataEvent: FD_CLOSE : begin FTransferDone := TRUE; case FFTPCmd of FTP_RETR, FTP_LIST, FTP_VIEW : RecvData; FTP_STOR : SendData; end; end; Флаг FTransferDone сообщает о необходимости продолжить чтение оставшихся данных сокета в цикле while, как показано в следующем фрагменте кода процедуры RecvData: FTP_RETR : begin { часть кода пропущена } if FTransferDone then // Работа с //FTP-сервером закончена, // однако необходимо прочитать // и сохранить данные, оставшиеся // в сокете данных begin Done := FALSE; while not Done do begin BlockWrite(FRetrFile, FDataBuffer, Response); { часть кода пропущена } Response := recv(FDataSocket, FDataBuffer, SizeOf(FDataBuffer), 0); if Response = SOCKET_ERROR then begin Done := TRUE; WSAAsyncSelect(FDataSocket, // Прекратить посылку FDataWnd, 0, 0); // уведомлений CloseSocket(FDataSocket); System.CloseFile(FRetrFile); ChangeBusy(FALSE); ChangeDataDone(TRUE); InfoEvent(Concat('ERROR : ',WSAErrorMsg)); end; if Response = 0 then // Данных не осталось begin { часть кода пропущена } Done := TRUE; WSAAsyncSelect(FDataSocket, FDataWnd, 0, 0); CloseSocket(FDataSocket); System.CloseFile(FRetrFile); ChangeBusy(FALSE); ChangeDataDone(TRUE); GetList; end; end; end else if Response > 0 then // FTP-сервер продолжает // посылать данные, // их необходимо обработать begin BlockWrite(FRetrFile, FDataBuffer, Response); { часть кода пропущена } end; end; Передача файла FTP-серверу в асинхронном режиме выполняется по тому же принципу, что и прием. Положи на место!В асинхронном режиме в отличие от блокирующего можно легко прервать затянувшуюся пересылку файла ≈ достаточно нажать кнопку Abort на вкладке Connect (обратите внимание на то, что в блокирующем режиме эта кнопка недоступна). При нажатии кнопки Abort вызывается метод CsShopper.Abort, который посылает серверу через управляющее соединение команду ABOR. Рассмотрим следующий фрагмент кода: procedure TCsShopper.Abort; begin ChangeBusy(TRUE); SendFtpCmd(LoadStr(SFtpAbor)); FFtpCmd := FTP_ABORT; ChangeBusy(FALSE); end; При получении кода ответа 226, означающего успешную отмену пересылки, CsShopper.ProcessAbort закрывает соединение данных, а в случае приема файла ≈ стирает локальный файл. ЗаключениеFTP-клиент CsShopper ≈ невизуальный компонент. Он не умеет сохранять и загружать имена хостов, имена пользователей, пароли и сведения о ресурсах. Все это остается на совести программистов, которые должны спроектировать эти визуальные средства в соответствии с потребностями конкретного приложения. Тем не менее приложение SHOPPER32 наглядно показывает, как легко можно при необходимости организовать сохранение и загрузку профилей. |
|