Windows Phone 8
Добавление поддержки FTP в Windows Phone 8
Продукты и технологии:
Windows Phone 8, Windows 8, Visual Studio
В статье рассматриваются:
- протокол FTP;
- создание FTP-библиотеки и клиентского FTP-приложения;
- подключение и аутентификация;
- просмотр каталогов;
- поддержка пассивных команд.
Исходный код можно скачать по ссылке.
FTP — один из самых популярных протоколов для обмена файлами. Он используется для передачи файлов клиентам, распространения установочных файлов потребителям, обеспечения доступа к иначе недоступным частям файловой системы по соображениям безопасности в корпоративных средах и в целом ряде других сценариев. С нынешним распространением облачных технологий может показаться, что дальнейшее применение FTP нецелесообразно. Но если вам требуется прямое управление вашими файлами и в то же время необходимо обеспечить простой доступ к ним, то FTP по-прежнему остается лучшим из возможных вариантов.
Благодаря добавлению поддержки сокетов в Windows Phone для взаимодействия с множеством разнообразных сервисов пользователи почувствуют себя «более подключенными». Однако в Windows Phone (равно как и в Windows 8) разработчику, возможно, придется самому предоставить реализацию сервиса. Одна из функций, которых по-прежнему не хватает в Windows Phone, — поддержка FTP. В этой ОС нет никаких API, позволяющих напрямую использовать FTP-сервисы в приложении. С точки зрения предприятия, это сильно затрудняет их сотрудникам доступ к файлам с телефонов.
Эта статья посвящена добавлению поддержки FTP в Windows Phone 8 за счет создания FTP-библиотеки и простого FTP-клиента, которые будут выполняться на устройстве с Windows Phone. FTP — хорошо документированный протокол (см. RFC 959 на bit.ly/fB5ezA). RFC 959 описывает спецификацию, функциональность и архитектуру этого протокола, а также команды с их ответами. Я не собираюсь рассказывать обо всей функциональности и всех командах FTP, но надеюсь дать вам хорошую отправную точку для создания собственной реализации FTP. Заметьте, что функциональность, обсуждаемая в этой статье, не является реализацией от Microsoft и не факт, что она будет одобрена.
FTP-каналы
FTP состоит из двух каналов: для команд и данных. Канал команд создается, когда клиент подключается к FTP-серверу, и используется для передачи всех команд и ответов между клиентом и FTP-сервером. Этот канал остается открытым, пока клиент не отключится, не истечет время простоя соединения или не возникнет какая-то ошибка на FTP-сервере или в канале команд.
Канал данных применяется для передачи данных между клиентом и FTP-сервером. Этот канал является временным по своей природе: как только передача данных завершается, канал отключается. Для каждой передачи данных устанавливается новый канал данных..
FTP-соединение может быть либо активным, либо пассивным. В активном режиме FTP-клиент посылает информацию о канале данных (номер порта данных), по которому будет отправлен файл. В пассивном режиме клиент запрашивает сервер создать канал данных на его стороне и предоставить адрес сокета и информацию о порте, чтобы клиент мог подключиться к этому каналу данных и начать операцию передачи файла.
Пассивный режим полезен, когда FTP-клиент не хочет управлять портами данных или каналами данных на своей стороне. Разрабатывая FTP-клиент для Windows Phone, я задействую пассивный режим. То есть перед началом операции передачи файла я буду просить FTP-сервер создать и открыть канал данных, а затем отправить мне информацию о конечной точке его сокета; после этого я смогу подключиться к серверу, и начнется передача данных. Как использовать пассивный режим, я расскажу далее в этой статье.
Приступаем
В этой статье не описываются все команды, упоминаемые в RFC 959. Я сосредоточусь на основных командах, необходимых для создания минимального FTP-клиента, в том числе используемых в процедуре подключения-отключения, при аутентификации, просмотре файловой системы и скачивании/закачивании файлов.
Для начала я создам решение Visual Studio, которое будет содержать два проекта: Windows Phone Class Library для FTP-библиотеки и Windows Phone App с кодом UI и для работы с FTP-библиотекой..
Чтобы создать решение, откройте Visual Studio, создайте проект Windows Phone Class Library и присвойте ему имя WinPhoneFtp.FtpService. Теперь добавьте проект Windows Phone App и назовите его WinPhoneFtp.UserExperience. В нем будет содержаться UI для тестового приложения.
WinPhoneFtp.FtpService — это клиентская FTP-библиотека, на которую будет ссылаться проект WinPhoneFtp.UserExperience, чтобы использовать FTP-сервисы. FTP-библиотека основана на шаблоне async/await и применении событий. Каждая операция будет вызываться асинхронно в фоновой задаче, и по ее завершению будет генерироваться событие, уведомляющее UI. Клиентская FTP-библиотека содержит различные асинхронные методы для каждой FTP-операции (поддерживаемых команд), и каждый асинхронный метод сопоставлен с определенным событием, генерируемым по окончании выполнения асинхронного метода. В табл. 1 показаны сопоставления асинхронных методов и событий.
Табл. 1. Асинхронные методы и связанные с ними события
Метод (с параметрами) | Связанные события |
Constructor(System.String IPAddress, System.Windows.Threading.Dispatcher UIDispatcher) | Нет события |
ConnectAsync | FtpConnected |
DisconnectAsync | FtpDisconnected |
AuthenticateAsync AuthenticateAsync(System.String Username, System.String Password) |
Успех — FtpAuthenticationSucceeded Неудача — FtpAuthenticationFailed |
GetPresentWorkingDirectoryAsync | FtpDirectoryListed |
ChangeWorkingDirectoryAsync | Успех — FtpDirectoryChangedSucceeded Неудача — FtpDirectoryChangedFailed |
GetDirectoryListingAsync | FtpDirectoryListed |
UploadFileAsync(System.IO.Stream LocalFileStream, System.String RemoteFilename) | Успех — FtpFileUploadSucceeded Неудача — FtpFileUploadFailed В процессе выполнения — FtpFileTransferProgressed |
DownloadFileAsync(System.IO.Stream LocalFileStream, System.String RemoteFilename) | Успех — FtpFileDownloadSucceeded Неудача — FtpFileDownloadFailed В процессе выполнения — FtpFileTransferProgressed |
UI приложения состоит из двух текстовых полей и двух кнопок. Одна пара из текстового поля и кнопки будет использоваться для IP-адреса FTP-сервера и подключения к нему. Другая пара — будет принимать FTP-команды от пользователя и посылать их на FTP-сервер.
Оболочка TCP-сокета
Прежде чем заниматься FTP-клиентом, я создал оболочку TcpClientSocket для класса Windows.Networking.Sockets.StreamSocket (bit.ly/15fmqhK). Эта оболочка предоставляет определенные уведомляющие события на основе различных операций сокета. Меня интересуют уведомления SocketConnected, DataReceived, ErrorOccured и SocketClosed. Когда объект StreamSocket подключается к удаленной конечной точке, генерируется событие SocketConnected. Аналогично при приеме данных через сокет генерируется событие DataReceived, в аргументах которого указан буфер, содержащий данные. Если при операции сокета происходит ошибка, возникает событие ErrorOccured. Наконец, когда сокет закрывается (удаленным или локальным хостом), генерируется событие SocketClosed, в аргументе которого указывается причина закрытия. Я не стану вдаваться в детали того, как реализована эта оболочка, но вы можете скачать сопутствующий исходный код для этой статьи и самостоятельно посмотреть реализацию (archive.msdn.microsoft.com/mag201309WPFTP).
Подключение и отключение по FTP
Когда FTP-клиент впервые подключается к FTP-серверу, он устанавливает с сервером канал команд, по которому клиент и сервер будут обмениваться командами и ответами на них. Как правило, FTP использует порт 21, но по соображениям безопасности или по другим причинам можно сконфигурировать порт с другим номером. Прежде чем пользователь сможет начать обмен файлами, ему нужно аутентифицироваться на сервере — обычно по имени и паролю. Если аутентификация прошла успешно, сервер отвечает (в моем случае) кодом: 220 Microsoft FTP Service.
Перед подключением к FTP-серверу я создам объект ftpClient на основе класса из моей клиентской FTP-библиотеки, а также обработчики для всех ранее описанных событий:
FtpClient ftpClient = null;
async private void btnLogin_Tap(object sender,
System.Windows.Input.GestureEventArgs e)
{
ftpClient = new FtpClient(txtIp.Text, this.Dispatcher);
// Добавляем обработчики для событий объекта ftpClient
await ftpClient.ConnectAsync();
}
Создав объект и обработчики различных событий FTP-клиента, я вызываю метод ConnectAsync объекта FtpClient. Вот как работает ConnectAsync:
public async Task ConnectAsync()
{
if (!IsConnected)
{
logger.AddLog("FTP Command Channel Initailized");
await FtpCommandSocket.PrepareSocket();
}
}
ConnectAsync подключается к FTP-серверу как операция подключения сокета. Когда FTP-клиент успешно соединяется с FTP-сервером, генерируется событие FtpConnected, чтобы уведомить клиент о том, что он подключен к FTP-серверу:
async void ftpClient_FtpConnected(object sender, EventArgs e)
{
// Обрабатываем событие FtpConnected для вывода приглашения
// пользователю или вызова AuthenticateAsync
// для его автоматической аутентификации после соединения
}
На рис. 1 показано, как выглядит приложение, когда пользователь успешно подключился к FTP-серверу.
Рис. 1. Подключение к FTP-серверу и аутентификация
Заметьте: когда вы подключаетесь к другим FTP-серверам, вы можете увидеть совершенно другой текст.
Есть три способа отключения от FTP-сервера:
- пользователь посылает серверу команду QUIT для явного закрытия соединения;
- после простоя в течение определенного времени FTP-сервер закрывает соединение;
- на одной или обеих сторонах произошла ошибка сокета или какая-то внутренняя ошибка.
Для явного отключения командой QUIT я ввожу ее в текстовое поле команды и касаюсь кнопки Send Command, после чего обработчик событий этой кнопки вызывает метод DisconnectAsync:
async private void btnFtp_Tap(object sender,
System.Windows.Input.GestureEventArgs e)
{
...
if (txtCmd.Text.Equals("QUIT"))
{
logger.Logs.Clear();
await ftpClient.DisconnectAsync();
return;
}
...
}
А так DisconnectAsync посылает команду FTP-серверу:
ppublic async Task DisconnectAsync()
{
ftpCommand = FtpCommand.Logout;
await FtpCommandSocket.SendData("QUIT\r\n");
}
После закрытия соединения генерируется событие FtpDisconnected, поэтому клиент может обработать это событие и корректно освободить ресурсы (результат приведен на рис. 2):
void ftpClient_FtpDisconnected(object sender,
FtpDisconnectedEventArgs e)
{
// Обрабатываем событие FtpDisconnected для вывода
// какого-то сообщения пользователю или освобождения ресурсов
}
Рис. 2. Отключение от FTP-сервера
В любом из упомянутых сценариев FTP-клиент отключается от FTP-сервера, и любые незавершенные операции передачи файлов по FTP отменяются.
Аутентификация по FTP
Перед началом любой файловой операции по FTP пользователь должен аутентифицироваться на FTP-сервере. Пользователи могут быть двух типов: анонимные и не анонимные (с удостоверениями защиты). Когда FTP-сервер общедоступный, пользователи аутентифицируются как анонимные. Для них имени пользователя считается слово «anonymous», а паролем может быть любой текст в формате адреса электронной почты. Если FTP-сервер не является анонимным, пользователи должны предоставлять правильные удостоверения. Прежде чем реализовать какую-либо схему аутентификации, нужно узнать, какой тип аутентификации включен на сервере.
Аутентификация осуществляется посредством двух FTP-команд: USER и PASS. Команда USER позволяет передать идентификацию пользователя, т. е. имя пользователя, а команда PASS — пароль. Хотя считается хорошей практикой предоставлять адрес электронной почты пользователя, принимается любой текст в формате такого адреса:
[FTP Client]: USER anonymous
[FTP Server:] 331 Anonymous access allowed, send identity
(e-mail name) as password
[FTP Client]: PASS m@m.com
[FTP Server]: 230 User logged in
У метода AuthenticateAsync есть две перегруженные версии. Первая версия аутентифицирует пользователя с удостоверениями по умолчанию, которые обычно применяются для анонимной аутентификации, а вторая — принимает имя пользователя и пароль для аутентификации на FTP-сервере.
Чтобы автоматически аутентифицировать пользователя как анонимного, я вызываю AuthenticateAsync, когда принимается событие FtpConnected:
async void ftpClient_FtpConnected(object sender, EventArgs e)
{
await (sender as FtpClient).AuthenticateAsync();
}
На внутреннем уровне AuthenticateAsync вызывает другую, перегруженную версию с удостоверениями по умолчанию:
public async Task AuthenticateAsync()
{
await AuthenticateAsync("anonymous", m@m.com);
}
Эта перегруженная версия AuthenticateAsync выдает команду USER с именем пользователя, полученным из параметра:
public async Task AuthenticateAsync(
String Username, String Password)
{
ftpCommand = FtpCommand.Username;
this.Username = Username;
this.Password = Password;
logger.AddLog(String.Format(
"FTPClient -> USER {0}\r\n", Username));
await FtpCommandSocket.SendData(String.Format(
"USER {0}\r\n", Username));
}
После ответа на команду USER посылается команда PASS с паролем, полученным из параметра (рис. 3).
Рис. 3. Отправка пароль через PASS после команды USER
async void FtpClientSocket_DataReceived(object sender,
DataReceivedEventArgs e)
{
String Response = System.Text.Encoding.UTF8.GetString(
e.GetData(), 0, e.GetData().Length);
logger.AddLog(String.Format("FTPServer -> {0}", Response));
switch (ftpPassiveOperation)
{
...
case FtpPassiveOperation.None:
switch (ftpCommand)
{
case FtpCommand.Username:
if (Response.StartsWith("501"))
{
IsBusy = false;
RaiseFtpAuthenticationFailedEvent();
break;
}
this.ftpCommand = FtpCommand.Password;
logger.AddLog(String.Format(
"FTPClient -> PASS {0}\r\n", this.Password));
await FtpCommandSocket.SendData(
String.Format("PASS {0}\r\n", this.Password));
break;
...
}
}
...
}
Если аутентификация успешна, генерируется событие FtpAuthenticationSucceeded; в ином случае возникает событие FtpAuthenticationFailed:
void ftpClient_FtpAuthenticationFailed(
object sender, EventArgs e)
{
logger.AddLog("Authentication failed");
}
void ftpClient_FtpAuthenticationSucceeded(
object sender, EventArgs e)
{
logger.AddLog("Authentication succeeded");
}
Просмотр каталогов
FTP поддерживает просмотр каталогов, но многое зависит от того, как FTP-клиент отображает информацию о каталогах. Если у FTP-клиента есть GUI, информация может показываться в виде дерева каталогов. Консольный клиент, напротив, может выводить лишь список каталогов на экране.
Поскольку нет никакого механизма, который позволил бы узнать, в каком каталоге вы находитесь, FTP предоставляет команду для отображения текущего каталога. Отправив команду PWD на FTP-сервер по каналу команд, вы получаете полный путь от корня до текущего каталога. Когда пользователь аутентифицируется, он оказывается либо в корневом каталоге, либо в каталоге, заданном для него в конфигурации FTP-сервера.
Команду PWD можно выдать вызовом GetPresentWorkingDirectoryAsync. Чтобы получить текущий каталог, я посылаю PWD из поля команд и вызываю GetPresentWorkingDirectoryAsync в обработчике событий кнопки Send Command:
async private void btnFtp_Tap(object sender,
System.Windows.Input.GestureEventArgs e)
{
...
if (txtCmd.Text.Equals("PWD"))
{
logger.Logs.Clear();
await ftpClient.GetPresentWorkingDirectoryAsync();
return;
}
...
}
На внутреннем уровне GetPresentWorkingDirectoryAsync отправляет PWD на FTP-сервер через сокет:
public async Task GetPresentWorkingDirectoryAsync()
{
if (!IsBusy)
{
ftpCommand = FtpCommand.PresentWorkingDirectory;
logger.AddLog("FTPClient -> PWD\r\n");
await FtpCommandSocket.SendData("PWD\r\n");
}
}
Когда эта процедура заканчивается успешно и FTP-сервер посылает ответ с путем к текущему каталогу, FTP-клиент уведомляется об этом через событие FtpPresentWorkingDirectoryReceived. Используя аргументы события, клиент может получить информацию о пути к текущему каталогу (как показано на рис. 4):
void ftpClient_FtpPresentWorkingDirectoryReceived(
object sender, FtpPresentWorkingDirectoryEventArgs e)
{
// Обрабатываем событие PresentWorkingDirectoryReceived,
// чтобы показать пользователю какое-то сообщение
}
Рис. 4. Получение текущего каталога от FTP-сервера
Для перехода в другой каталог FTP-клиент может использовать команду CWD (Change Working Directory). Заметьте, что CWD позволяет перейти лишь на одну ступень — либо в подкаталог текущего каталога, либо в его родительский каталог. Если пользователь находится в корневом каталоге, он не может переходить назад, и при попытке сделать это CWD сообщит в ответе об ошибке.
Чтобы сменить текущий каталог, я ввожу команду CWD в текстовое поле команд, а затем в обработчике касания кнопки Send Command вызываю метод ChangeWorkingDirectoryAsync:
async private void btnFtp_Tap(object sender,
System.Windows.Input.GestureEventArgs e)
{
...
if (txtCmd.Text.StartsWith("CWD"))
{
logger.Logs.Clear();
await ftpClient.ChangeWorkingDirectoryAsync(
txtCmd.Text.Split(new char[] { ' ' },
StringSplitOptions.RemoveEmptyEntries)[1]);
return;
}
...
}
На внутреннем уровне ChangeWorkingDirectoryAsync отправляет команду CWD на FTP-сервер:
public async Task ChangeWorkingDirectoryAsync(String RemoteDirectory)
{
if (!IsBusy)
{
this.RemoteDirectory = RemoteDirectory;
ftpCommand = FtpCommand.ChangeWorkingDirectory;
logger.AddLog(String.Format("FTPClient -> CWD {0}\r\n",
RemoteDirectory));
await FtpCommandSocket.SendData(String.Format("CWD {0}\r\n",
RemoteDirectory));
}
}
Если пользователю нужно вернуться на ступень назад в иерархии каталогов, он может послать две точки «..» в качестве параметра для этой процедуры. Когда смена каталога выполняется успешно, клиент уведомляется об этом и генерируется событие FtpDirectoryChangedSucceeded (рис. 5). Если команда CWD заканчивается неудачей и в ответе посылается ошибка, клиент уведомляется о неудаче и генерируется событие FtpDirectoryChangedFailed:
void ftpClient_FtpDirectoryChangedSucceded(object sender,
FtpDirectoryChangedEventArgs e)
{
// Обрабатываем событие DirectoryChangedSucceeded,
// чтобы вывести пользователю какое-то сообщение
}
void ftpClient_FtpDirectoryChangedFailed(object sender,
FtpDirectoryChangedEventArgs e)
{
// Обрабатываем событие DirectoryChangedFailed,
// чтобы вывести пользователю какое-то сообщение
}
Рис. 5. Смена текущего каталога на FTP-сервере
Добавление поддержки пассивных команд
Теперь пора заняться добавлением поддержки пассивных команд, поскольку я хочу, чтобы FTP-сервер управлял всеми соединениями для передачи данных между собой и клиентом. В пассивном режиме любая команда, которой нужно передать данные, должна использовать соединение для обмена данными, инициированное FTP-сервером. Сервер создает такое соединение и посылает информацию о сокете для этого соединения, чтобы клиент мог подключиться к нему и выполнить какие-то операции передачи данных.
Пассивный режим является временным по своей природе. Перед отправкой команды FTP-серверу, который в ответ передаст или получит данные, клиент должен сообщить серверу переключиться в пассивный режим — это нужно делать для каждой отправляемой команды. Если в двух словах, то при каждой передаче данных всегда посылается две команды: переключения в пассивный режим и сама команда передачи данных.
Чтобы указать FTP-серверу подготовиться к пассивному режиму, используется команда PASV. В ответ сервер посылает IP-адрес и номер порта соединения для обмена данными в кодированном формате. Ответ декодируется, а затем клиент подготавливает соединение для обмена данными с FTP-сервером, используя декодированные IP-адрес и номер порта. По окончании операции передачи данных это соединение закрывается и уничтожается, поэтому им нельзя воспользоваться повторно. Это происходит всякий раз, когда FTP-серверу посылается команда PASV.
Декодирование ответа на команду PASV Ответ на команду PASV выглядит примерно так:
227 Entering Passive Mode (192,168,33,238,255,167)
В ответе содержится информация о канале данных. На основе этого ответа я должен определить адрес сокета — IP-адрес и порт данных, используемый FTP-сервером для операции передачи данных. Вот как вычисляются IP-адрес и порт, показанные на рис. 6:
- первые четыре группы целых чисел образуют IPV4-адрес FTP-сервера, т. е. 192.168.33.238;
- остальные целые числа выражают номер порта данных. С этой целью нужно сдвинуть влево пятую группу целых чисел на 8, а затем выполнить побитовую операцию OR с шестой группой целых чисел. Конечное значение даст вам номер порта, по которому будет доступен канал данных.
Рис. 6. Запрос и ответ при использовании пассивной команды
На рис. 7 показан код, который разбирает ответ и извлекает IP-адрес и номер порта конечной точки канала данных. Метод PrepareDataChannelAsync принимает исходный FTP-ответ от команды PASV. Иногда строка ответа может изменяться FTP-сервером для включения некоторых других параметров, но чаще всего в ответе посылается только IP-адрес и номер порта.
Рис. 7. Разбор ответа для получения конечной точки канала данных
private async Task PrepareDataChannelAsync(String ChannelInfo)
{
ChannelInfo = ChannelInfo.Remove(0, "227 Entering Passive Mode".Length);
// Получаем IP-адрес
String[] Splits = ChannelInfo.Substring(ChannelInfo.IndexOf("(") + 1,
ChannelInfo.Length - ChannelInfo.IndexOf("(") - 5).Split(
new char[] { ',', ' ', },
StringSplitOptions.RemoveEmptyEntries);
String Ipaddr = String.Join(".", Splits, 0, 4);
// Вычисляем порт данных
Int32 port = Convert.ToInt32(Splits[4]);
port = ((port << 8) | Convert.ToInt32(Splits[5]));
logger.AddLog(String.Format(
"FTP Data Channel IPAddress: {0}, Port: {1}", Ipaddr, port));
// Здесь создаем канал данных с извлеченными IP-адресом
// и номером порта
logger.AddLog("FTP Data Channel connected");
}
Дополнительные команды: LIST, TYPE, STOR и RETR
Перечисление содержимого каталога Для перечисления содержимого текущего каталога я посылаю FTP-серверу команду LIST по каналу команд. Однако FTP-сервер ответит на эту команду по каналу данных, поэтому я должен сначала послать команду PASV, чтобы создать соединение для передачи данных, по которому будет отправлен ответ от команды LIST. Формат листинга каталога зависит от ОС, в которой установлен FTP-сервер. То есть, если операционная система на FTP-сервере — Windows, листинг каталога будет получен в формате перечисления содержимого каталога Windows, а если ОС основана на Unix, то — в формате Unix.
Чтобы перечислить содержимое текущего каталога, я ввожу команду LIST в текстовое поле команд и в обработчике касания кнопки Send Command вызываю метод GetDirectoryListingAsync:
async private void btnFtp_Tap(object sender,
System.Windows.Input.GestureEventArgs e)
{
...
if (txtCmd.Text.Equals("LIST"))
{
logger.Logs.Clear();
await ftpClient.GetDirectoryListingAsync();
return;
}
...
}
На внутреннем уровне GetDirectoryListingAsync посылает FTP-серверу команду PASV через сокет:
public async Task GetDirectoryListingAsync()
{
if (!IsBusy)
{
fileListingData = null;
IsBusy = true;
ftpCommand = FtpCommand.Passive;
logger.AddLog("FTPClient -> PASV\r\n");
await FtpCommandSocket.SendData("PASV\r\n");
}
}
После приема информации о конечной точке для команды PASV создается соединение для обмена данными и на FTP-сервер посылается команда LIST, как показано на рис. 8.
Рис. 8. Обработка команды List в обработчике события DataReceived сокета
async void FtpClientSocket_DataReceived(object sender, D
ataReceivedEventArgs e)
{
String Response = System.Text.Encoding.UTF8.GetString(
e.GetData(), 0, e.GetData().Length);
...
IsBusy = true;
DataReader dataReader = new DataReader(
FtpDataChannel.InputStream);
dataReader.InputStreamOptions = InputStreamOptions.Partial;
fileListingData = new List<byte>();
while (!(await dataReader.LoadAsync(1024)).Equals(0))
{
fileListingData.AddRange(dataReader.DetachBuffer().ToArray());
}
dataReader.Dispose();
dataReader = null;
FtpDataChannel.Dispose();
FtpDataChannel = null;
String listingData = System.Text.Encoding.UTF8.GetString(
fileListingData.ToArray(), 0, fileListingData.ToArray().Length);
String[] listings = listingData.Split(
new char[] { '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
List<String> Filenames = new List<String>();
List<String> Directories = new List<String>();
foreach (String listing in listings)
{
if (listing.StartsWith("drwx") || listing.Contains("<DIR>"))
{
Directories.Add(listing.Split(new char[] { ' ' }).Last());
}
else
{
Filenames.Add(listing.Split(new char[] { ' ' }).Last());
}
}
RaiseFtpDirectoryListedEvent(Directories.ToArray(),
Filenames.ToArray());
...
fileListingData = new List<byte>();
ftpPassiveOperation = FtpPassiveOperation.ListDirectory;
logger.AddLog("FTPClient -> LIST\r\n");
await FtpCommandSocket.SendData("LIST\r\n");
...
}
Когда листинг каталога принимается по каналу данных, генерируется событие FtpDirectoryListed, содержащее в своих аргументах список файлов и подкаталогов (на рис. 9 показана отправка команд PASV и LIST, на рис. 10 — вывод листинга каталога):
void ftpClient_FtpDirectoryListed(object sender,
FtpDirectoryListedEventArgs e)
{
foreach (String filename in e.GetFilenames())
{
// Обрабатываем имена всех файлов в текущем каталоге
}
foreach (String directory in e.GetDirectories())
{
// Обрабатываем имена всех каталогов в текущем каталоге
}
}
Рис. 9. Отправка FTP-серверу команды LIST
Рис. 10. Отображение объектов файловой системы в ответ на команду LIST
Описание формата данных Команда TYPE позволяет описать формат данных, в котором будут приниматься или отправляться файловые данные. Она не требует переключения в режим PASV. Я использую с командой TYPE формат «I», обозначающий передачу данных типа Image. TYPE обычно используется при сохранении или извлечении файлов по соединению, предназначенному для обмена данными. Это сообщает FTP-серверу, что передача будет осуществляться в двоичном режиме, а не в текстовом или в каком-то режиме с некоей структуризацией данных. Об остальных режимах, которые можно использовать с командой TYPE, см. в RFC 959.
Сохранение файла на FTP-сервере Чтобы сохранить содержимое файла (расположенного на устройстве) на FTP-сервере, я посылаю команду STOR вместе с именем файла (и расширением); эти аргументы FTP-сервер использует для создания и сохранения файла. Передача содержимого файла выполняется по соединению для обмена данными, поэтому перед отправкой STOR я запрашиваю детальные сведения о конечной точке от FTP-сервера с помощью команды PASV. Получив конечную точку, я посылаю STOR, а после ответа на нее — содержимое файла в двоичной форме по соединению для обмена данными.
Команду STOR можно посылать вызовом метода UploadFileAsync, как показано на рис. 11. Этот метод принимает два параметра: объект Stream локального файла и имя файла на FTP-сервере как String.
Рис. 11. Метод UploadFileAsync
async private void btnFtp_Tap(object sender,
System.Windows.Input.GestureEventArgs e)
{
...
if (txtCmd.Text.StartsWith("STOR"))
{
logger.Logs.Clear();
String Filename = txtCmd.Text.Split(new char[] { ' ', '/' },
StringSplitOptions.RemoveEmptyEntries).Last();
StorageFile file =
await Windows.Storage.ApplicationData.Current.LocalFolder.GetFileAsync(
txtCmd.Text.Split(new char[] { ' ' },
StringSplitOptions.RemoveEmptyEntries)[1]);
await ftpClient.UploadFileAsync(await file.OpenStreamForReadAsync(),
"video2.mp4");
return;
}
...
}
На внутреннем уровне метод UploadFileAsync выдает FTP-серверу команду PASV для получения информации о конечной точке канала данных:
public async Task UploadFileAsync(
System.IO.Stream LocalFileStream, String RemoteFilename)
{
if (!IsBusy)
{
ftpFileInfo = null;
IsBusy = true;
ftpFileInfo = new FtpFileOperationInfo(
LocalFileStream, RemoteFilename, true);
ftpCommand = FtpCommand.Type;
logger.AddLog("FTPClient -> TYPE I\r\n");
await FtpCommandSocket.SendData("TYPE I\r\n");
}
}
Как показано на рис. 12, получив ответ на команду PASV, обработчик события DataReceived выдает команду STOR после создания и открытия канала данных; после этого начинается передача данных от клиента серверу.
Рис. 12. Обработка команды STOR в обработчике события DataReceived сокета
async void FtpClientSocket_DataReceived(
object sender, DataReceivedEventArgs e)
{
String Response = System.Text.Encoding.UTF8.GetString(
e.GetData(), 0, e.GetData().Length);
...
IsBusy = true;
DataWriter dataWriter = new DataWriter(
FtpDataChannel.OutputStream);
byte[] data = new byte[32768];
while (!(await ftpFileInfo.LocalFileStream.ReadAsync(
data, 0, data.Length)).Equals(0))
{
dataWriter.WriteBytes(data);
await dataWriter.StoreAsync();
RaiseFtpFileTransferProgressedEvent(
Convert.ToUInt32(data.Length), true);
}
await dataWriter.FlushAsync();
dataWriter.Dispose();
dataWriter = null;
FtpDataChannel.Dispose();
FtpDataChannel = null;
...
await PrepareDataChannelAsync(Response);
ftpPassiveOperation = FtpPassiveOperation.FileUpload;
logger.AddLog(String.Format("FTPClient -> STOR {0}\r\n",
ftpFileInfo.RemoteFile));
await FtpCommandSocket.SendData(String.Format("STOR {0}\r\n",
ftpFileInfo.RemoteFile));
...
}
Пока файл закачивается, клиент уведомляется о прогрессе загрузки через FtpFileTransferProgressed:
void ftpClient_FtpFileTransferProgressed(
object sender, FtpFileTransferProgressedEventArgs e)
{
// Обновляем UI информацией о прогрессе
// или используем данные для обновления полоски прогресса
}
Если операция закачки файла завершается успешно, генерируется событие FtpFileUploadSucceeded. В ином случае возникает событие FtpFileUploadFailed с аргументом, в котором сообщается о причине неудачи:
void ftpClient_FtpFileUploadSucceeded(
object sender, FtpFileTransferEventArgs e)
{
// Обрабатываем событие UploadSucceeded
// для вывода какого-то сообщения пользователю
}
void ftpClient_FtpFileUploadFailed (
object sender, FtpFileTransferEventArgs e)
{
// Обрабатываем событие UploadFailed
// для вывода какого-то сообщения пользователю
}
На рис. 13 и 14 показан процесс загрузки файла на FTP-сервер.
Рис. 13. Загрузка файла на FTP-сервер
Рис. 14. Успешное завершение загрузки файла
Получение файла с FTP-сервера Чтобы получить содержимое файла с FTP-сервера, я посылаю команду RETR с именем и расширением файла (предполагая, что файл находится на сервере в текущем каталоге). При передаче содержимого файла используется соединение для обмена данными, поэтому перед отправкой RETR я запрашиваю от FTP-сервера информацию о конечной точке с помощью команды PASV. Получив конечную точку, я посылаю команду RETR, а после приема ответа на нее я извлекаю с FTP-сервера содержимое файла в двоичной форме по соединению для обмена данными.
Как показано на рис. 15, команду RETR можно отправить вызовом метода DownloadFileAsync. Этот метод принимает два параметра: объект Stream локального файла для сохранения содержимого и имя файла на FTP-сервере как String.
Рис. 15. Метод DownloadFileAsync
async private void btnFtp_Tap(
object sender, System.Windows.Input.GestureEventArgs e)
{
...
if (txtCmd.Text.StartsWith("RETR"))
{
logger.Logs.Clear();
String Filename = txtCmd.Text.Split(new char[] { ' ', '/' },
StringSplitOptions.RemoveEmptyEntries).Last();
StorageFile file =
await Windows.Storage.ApplicationData.Current.LocalFolder.
CreateFileAsync(
txtCmd.Text.Split(new char[] { ' ' }, StringSplitOptions.
RemoveEmptyEntries)[1],
CreationCollisionOption.ReplaceExisting);
await ftpClient.DownloadFileAsync(
await file.OpenStreamForWriteAsync(), Filename);
return;
}
...
}
На внутреннем уровне метод DownloadFileAsync выдает FTP-серверу команду PASV для получения информации о конечной точке канала данных:
public async Task DownloadFileAsync(
System.IO.Stream LocalFileStream, String RemoteFilename)
{
if (!IsBusy)
{
ftpFileInfo = null;
IsBusy = true;
ftpFileInfo = new FtpFileOperationInfo(
LocalFileStream, RemoteFilename, false);
ftpCommand = FtpCommand.Type;
logger.AddLog("FTPClient -> TYPE I\r\n");
await FtpCommandSocket.SendData("TYPE I\r\n");
}
}
Как видно на рис. 16, получив ответ на команду PASV, обработчик события DataReceived выдает команду RETR после создания и открытия канала данных; после этого начинается передача данных с сервера клиенту.
Рис. 16. Обработка команды RETR в обработчике события DataReceived сокета
async void FtpClientSocket_DataReceived(
object sender, DataReceivedEventArgs e)
{
String Response = System.Text.Encoding.UTF8.GetString(
e.GetData(), 0, e.GetData().Length);
...
IsBusy = true;
DataReader dataReader = new DataReader(FtpDataChannel.InputStream);
dataReader.InputStreamOptions = InputStreamOptions.Partial;
while (!(await dataReader.LoadAsync(32768)).Equals(0))
{
IBuffer databuffer = dataReader.DetachBuffer();
RaiseFtpFileTransferProgressedEvent(databuffer.Length, false);
await ftpFileInfo.LocalFileStream.WriteAsync(
databuffer.ToArray(), 0, Convert.ToInt32 (databuffer.Length));
}
await ftpFileInfo.LocalFileStream.FlushAsync();
dataReader.Dispose();
dataReader = null;
FtpDataChannel.Dispose();
FtpDataChannel = null;
...
await PrepareDataChannelAsync(Response);
ftpPassiveOperation = FtpPassiveOperation.FileDownload;
logger.AddLog(String.Format("FTPClient -> RETR {0}\r\n",
ftpFileInfo.RemoteFile));
await FtpCommandSocket.SendData(String.Format("RETR {0}\r\n",
ftpFileInfo.RemoteFile));
...
}
}
Пока файл скачивается, клиент уведомляется о прогрессе загрузки через FtpFileTransferProgressed:
void ftpClient_FtpFileTransferProgressed(object sender,
FtpFileTransferProgressedEventArgs e)
{
// Обновляем UI информацией о прогрессе
// или используем данные для обновления полоски прогресса
}
Если операция скачивания файла завершается успешно, генерируется событие FtpFileDownloadSucceeded. В ином случае возникает событие FtpFileDownloadFailed с аргументом, который содержит причину неудачи:
void ftpClient_FtpFileDownloadSucceeded(object sender,
FtpFileTransferFailedEventArgs e)
{
// Обрабатываем событие DownloadSucceeded
// для вывода какого-то сообщения пользователю
}
void ftpClient_FtpFileDownloadFailed(object sender,
FtpFileTransferFailedEventArgs e)
{
// Обрабатываем событие UploadFailed
// для вывода какого-то сообщения пользователю
}
На рис. 17 и 18 показан процесс скачивания файла с FTP-сервера.
Рис. 17. Скачивание файла с FTP-сервера
Рис. 18. Успешное завершение скачивания файла
Заключение
Заметьте, что вы можете предоставить сопоставление URI с приложением, реализующим FTP, чтобы другие приложения тоже могли обращаться к FTP-сервису и запрашивать данные от FTP-сервера. Более подробную информацию на эту тему вы найдете на странице Windows Phone Dev Center «Auto-launching apps using file and URI associations for Windows Phone 8» по ссылке bit.ly/XeAaZ8, а пример кода — по ссылке bit.ly/15x4O0y.
Написанный мной код FTP-библиотеки и клиентского приложения для Windows Phone полностью поддерживается в Windows 8.x, так как я не использовал никаких API, которые были бы несовместимы с Windows 8.x. Вы можете либо заново скомпилировать код для Windows 8.x, либо поместить его в Portable Class Library (PCL), которая ориентирована на обе платформы.
Удейя Гупта (Uday Gupta) — старший инженер по разработке продуктов в Symphony Teleca Corp. (India) Pvt Ltd. Имеет опыт использования многих .NET-технологий, особенно Windows Presentation Foundation (WPF), Silverlight, Windows Phone и Windows 8. Большую часть времени посвящает кодированию, играм, изучению новых вещей и помощи другим.
Выражаю благодарность за рецензирование статьи экспертам Тони Чэмпиону (Tony Champion) из Champion DS и Энди Уигли (Andy Wigley) из Microsoft.