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-серверу.

Подключение к 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 для вывода
  // какого-то сообщения пользователю или освобождения ресурсов
}

Отключение от FTP-сервера
Рис. 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,
  // чтобы показать пользователю какое-то сообщение
}

Получение текущего каталога от FTP-сервера
Рис. 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,
  // чтобы вывести пользователю какое-то сообщение
}

Смена текущего каталога на FTP-сервере
Рис. 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())
  {
    // Обрабатываем имена всех каталогов в текущем каталоге
  }
}

Отправка FTP-серверу команды LIST
Рис. 9. Отправка FTP-серверу команды LIST

Отображение объектов файловой системы в ответ на команду 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-сервер.

Загрузка файла на 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-сервера.

Скачивание файла с 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.