Разработка программных средств В проекте разрабатывается два приложения, причем, по условиям они не должны быть зависимы друг от друга и полностью полагаться на протокол связи между ними. Протоколом выбран FTP, описание которого есть в RFC 959. Следует грамотно описать каждый функциональный элемент программы. Такими элементами являются классы и функции, опирающиеся в основном на команды протокола. Рассмотрим минимально необходимый набор команд для данной системы: CDUP — Сменить директорию на вышестоящую. CWD — Сменить директорию (CWD directoryname). DELE — Удалить файл (DELE filename). LIST — Возвращает список файлов директории. Список передается через соединение данных. MKD — Создать директорию (MKD directory name). PASV — Войти в пассивный режим. Сервер вернет адрес и порт к которому нужно подключиться чтобы забрать данные. Передача начнется при введении следующих команд RETR, LIST и тд. PWD — Возвращает текущую директорию. QUIT — Отключиться RETR — Скачать файл. Перед RETR должна быть команда PASV или PORT (RETR filename). RMD — Удалить директорию (RMD directory). STOR — Закачать файл. Перед STOR должна быть команда PASV или PORT (STOR filename). TYPE — Установить тип передачи файла: бинарный или текстовый (TYPE A/I/E/L N/T/C) USER — Имя пользователя для входа на сервер (USER username). PASS — Пароль для входа на сервер (PASS password). Как можно заметить, отсутствую команды переименования RNFR и RNTO и еще несколько команд. В текущем проекте они не пригодятся из-за особенностей выбранной платформы, плюс к этому, сервер и клиент, хоть и должны соответствовать стандартам, разрабатываются для конкретной задачи – приема/передачи файлов. Теперь можно перейти непосредственно к описанию приложений. Сервер. Сервер должен работать не останавливаясь после запуска. Ожидать инициации соединения, принимать данные (в общем случае - команды) и выполнять необходимые функции. Сначала необходимо определить пространство имен, в котором будут создаваться классы – SharpFtpServer. Определим в нём стандартный класс Program (Приложение А, файл Program.cs, строка 10) и метод Program.Main(Приложение А, файл Program.cs, строка 14) – основная функция программы, принимающая массив аргументов. В методе Program.Main необходимо вызывать функцию инициации соединения, ожидания команд и пр. Гораздо проще создать отдельный класс FtpServer, где можно реализовать функцию соединения, а экземпляр класса создать в методе Program.Main(Приложение А, файл Program.cs, строка 16) и использовать методы Start() и Stop(), описанные в классе FtpServer. Также в Program.Main можно вывести сообщение на консоль, говорящее о возможности закрыть приложение любой кнопкой, что и будет реализовано при помощи функции чтения нажатых клавиш Console.Readkey(). Для реализации класса FtpServer желательно создать новый файл server.cs. Единственное внутреннее поле класса - экземпляр класса System.Net.Sockets.TcpListener _listener(Приложение А, файл server.cs, строка 19). Экземпляры этого класса ожидают подключения от TCP-клиентов сети. В методе FtpServer.Start()(Приложение А, файл server.cs, строка 29) в поле _listener инициализируется новый экземпляр класса TcpListener для ожидания входящего подключения на порт 21 от любого IP-адреса. Затем «слушатель» запускается и начинает асинхронную операцию, чтобы принять попытку входящего подключения. Важность асинхронной операции в том, что при этом не блокируется выполнение программы, но при попытке соединения управления передается этой операции и выполняется она. Метод HandleAcceptTcpClient (Приложение А, файл server.cs, строка 50), являющийся этой асинхронной операцией также определен в классе FtpServer. Он обрабатывает это TCP соединение с клиентом. В частности, создает новый экземпляр класса TcpListener client, который служит для связи по установленному соединению с удаленным узлом. Теперь можно получить объект System.Net.Sockets.NetworkStream, используемый для отправки и получения данных из поток. Стоит сказать, что экземпляр класса TcpListener client связан с управляющим соединением, таким образом, получив объект NetworkStream из client, можно получить приходящие или отправить исходящие команды по этому соединению. Зная, что команды и ответы в данном соединении всего лишь набор символов ASCII, то можно использовать экземпляры классов StreamWriter и StreamReader. Но тогда метод HandleAcceptTcpClient будет загружен кодом. Решением проблемы является создание нового класса ClientConnection, который будет обрабатывать приходящие команды и отвечать на них. Экземпляр этого класса создается в методе HandleAcceptTcpClient с использованием экземпляра TcpListener(Приложение А, файл server.cs, строка 55) и вызывается метод обработчика клиента(HandleClient). Однако правильней будет поместить выполнения метода в очередь(Приложение А, файл server.cs, строка 57), тогда метод будет находиться в фоновом потоке, оставляя свободный основной. Когда этот поток будет доступен из пула потоков метод будет выполнен. ClientConnection будет реализован в файле connect.cs. В нем реализованы основные функции, выполняемые при приеме команды от пользователя. Следует рассмотреть методы этого класса, также будут затронуты и поля по мере ознакомления и надобности. Как было сказано выше, реализация для текущего проекта упрощена – правильно реализованы только основные функции при приеме команд, однако отсутствует, например, получение атрибутов файла и нет реализации при выборе типа передачи EBCDIC, что не всегда допустимо при создании полного не учебного продукта. Конструктор класса ClientConnection(Приложение А, файл connect.cs, строка 101) перегружен и принимает client, объявленный в методе FtpServer. HandleAcceptTcpClient (Приложение А, файл server.cs, строка 55), как параметр. Этим параметром заполняется внутреннее поле _controlClient и в данном классе работа происходит с этим полем. Теперь, использовав это поле можно получить объект NetworkStream в поле _controlStream. И на основе _controlStream получить экземпляры классов StreamWriter и StreamReader(Приложение А, файл connect.cs, строка 105) в полях _controlReader и _controlWriter соответственно. Так завершится построение экземпляра класса. Обработчик клиента ClientConnection.HandleClient реализован следующим образом. Т.к. при вызове этого метода соединение с сервером установлено, то имеет смысл отправить клиенту ответ. Согласно RFC ответы начинаются с последовательности цифр, обозначающих категорию ошибок, и текста уведомления. При подключении к данному серверу будет отправлен ответ вида «220 Service Ready» (Приложение А, файл connect.cs, строка 113), указывающий на установленное соединение. Т.к. перед выполнением всех команд обязательно пройти аутентификацию по логину и паролю, то нужны эталонные логин и пароль, которые хранятся в текстовом файле settings.txt в каталоге с приложением, естественно он заполняется тем, кто и устанавливает приложение на виртуальную машину. При этом, если файлы пользователя будут размещены на другом логическом диске, то получить файл настроек, да и сам файл приложения сервера не удастся. В данном проекте для облегчения реализации создан всего один пользователь, соответственно в файле settings.txt написана лишь одна строка следующего содержания: «admin admin F:\». Где первый параметр – имя пользователя, второй – пароль, а третий – корневая директория пользователя, где будут расположены все его файлы. Если settings.txt имеется(Приложение А, файл connect.cs, строка 118), то прочитанная строка из него делится на строки, разделенные пробелом, которые затем присваиваются внутренним полям класса _username, _password и _root. Причем первая строка присваивается первому полю – имя пользователя, вторая – второму полю – пароль и третья – третьему полю – корневая директория. Если же файла не окажется, то будут использованы предустановленные значения в полях(Приложение А, файл connect.cs, строка 66). Затем организуется цикл, условием которого является непустая строка line, полученная от потока ввода(_controlReader) (Приложение А, файл connect.cs, строка143). Строка line делится пробелом на две подстроки, которые заполняют массив строк command. Первый элемент массива – сама команда – записывается в строковую переменную cmd, второй элемент, если таковой есть – является аргументом и записывается в argument, иначе – значением argument является null. Инициализируется строка ответа response, в неё будет записываться ответ, который в последствии передается клиенту. Следующим действием является проверка условия – равно ли поле идентификатора пользователя _userid нулю(Приложение А, файл connect.cs, строка 160). Т.к. заполняется оно только лишь в методе Password, вызываемом командой PASS, то при первом прохождении цикла _userid и вправду равно 0. При этом выполняется распознавание полученной от клиента команды – USER или PASS. В обоих случаях вызывается метод, принимающий в качестве параметра аргумент команды. Так реализуется авторизация – пока не изменится _userid, любые другие команды будут не распознаны и ответом на них будет сообщение, что вход не произведен. Однако, если авторизация была успешно пройдена и _userid не равен 0, то будет происходить распознавание всех команд(Приложение А, файл connect.cs, строка 177), реализованных на сервере. Причем при каждом распознавании будет запускаться свой метод с аргументом или без него. Стоит отметить, что не все команды имеют аргумент в виде безпробельной последовательности символов. Такая команда как TYPE может иметь и не иметь второго аргумента, это было учтено при распознавании аргументов команды(Приложение А, файл connect.cs, строка 199). В случае принятия команды, для которой нет реализации сервер выдает клиенту ответ, говорящий об этом – «502 Command not implemented» (Приложение А, файл connect.cs, строка 227). После выполнения метода, если теряется соединение(Приложение А, файл connect.cs, строка 234), то выполнение цикла приостанавливается, иначе полученный ответ(response) отправляется клиенту(Приложение А, файл connect.cs, строка 242). Однако, если ответ имеет код 221, то поле отправки ответа цикл приостанавливается и, как и в первом случае, сбрасывается соединение и останавливаются все «слушатели», кроме того, что ожидает попытки соединения с сервером. Так реализован механизм закрытия сессии. Сейчас имеет смысл рассмотреть реализацию методов, вызываемых при принятии определенных команд. Каждой команде соответствует один метод: • USER – User(username) • PASS – Password(password) • CDUP – ChangeWorkingDirectory(pathname) • CWD – ChangeWorkingDirectory(pathname) • DELE – Delete(pathname) • LIST – List(pathname) • MKD – CreateDir(pathname) • PASV – Passive() • PWD – PrintWorkingDirectory() • QUIT – Quit() • RETR – Retrive(pathname) • RMD – RemoveDir(pathname) • STOR – Store(pathname) • TYPE – Type(typeCode, formatControl) Видно, что для команд CWD и CDUP используется один и тот же метод. Так сделано для упрощения кода – обе команды требуют сервер сменить текущую директорию, а значит нет нужды дублировать метод, достаточно изменить параметр. И если для команды CWD параметром будет аргумент команды, то для CDUP параметром является строка «..». Такой параметр при применении дает переход на каталог выше. User(username) (Приложение А, файл connect.cs, строка 468) Метод сравнивает параметр с полем _username и, если они совпадают, тогда необходимо обозначить, что имя пользователя введено верно. Для этого в поле класса _usertrue записывается истинное значение(true). Если же имя пользователя разное в поле и в параметре, то _usertrue устанавливается в false. В обоих случаях передается ответ клиенту. Password(password) (Приложение А, файл connect.cs, строка 487) Метод сравнивает параметр с полем _password, если они равны, а также поле _usertrue установлено в true, тогда текущей директорией становится директория пользователя(Приложение А, файл connect.cs, строка 491), присваивается идентификатор пользователя отличный от нуля и возвращается ответ о входе в систему. В противном случае идентификатор пользователя обнуляется. ChangeWorkingDirectory(pathname) (Приложение А, файл connect.cs, строка 505) Смысл метода в том, чтобы изменить текущую директорию, представленную полем _currentDirectory, на поддиректорию с именем, заданным в параметре. Если параметр представляет собой строку(Приложение А, файл connect.cs, строка 507) «/», то, т.к. по стандартам так обозначается корневая директория, следует в поле текущей директории записать адрес корневой директории – из поля _root. В другом случае следует воссоздать путь, который должен являться текущим. Это необходимо по причине того, что аргумент CWD представляет из себя лишь название директории, а не полный путь. Поэтому при помощи метода System.IO.Path.Combine()(Приложение А, файл connect.cs, строка 523) с параметрами в виде текущей директории и аргументом команды можно получить полный путь. Использование системного метода позволяет не заботиться о составлении пути для разных платформ. После получения пути необходимо проверить существование этой директории при помощи метода System.IO.Directory.Exist()(Приложение А, файл connect.cs, строка 526), параметром которого является найденный ранее путь. Если директория существует, тогда в поле текущей директории записывается полный путь, найденный стандартными средствами. Затем следует проверка правильности нахождения этого пути с использованием метода ClientConnection.IsPathValid(), если строка найдена не верно, то в поле текущей директории записывается путь до корневой директории пользователя, аналогичное действие производится, если директории не существует(Приложение А, файл connect.cs, строка 237). IsPathValid(path) (Приложение А, файл connect.cs, строка 754) Метод проверяет корректность пути, сравнивая его начало с корневой директорией. При совпадении возвращается значение true, иначе false. Delete(pathname) (Приложение А, файл connect.cs, строка 670) Принятый параметр содержит в себе имя файла, который требуется удалить. Метод NormalizeFilename()(Приложение А, файл connect.cs, строка 672) получает полный путь до необходимого файла, если этот путь получился со значением null, тогда в ответе сервер сообщит об этом клиенту(Приложение А, файл connect.cs, строка 688). В другом случае, при помощи методов класса System.IO.File проверяется наличие файла(Приложение А, файл connect.cs, строка 676) и его удаление. Об этом сервер также пишет в ответе клиенту. NormalizeFilename(path) (Приложение А, файл connect.cs, строка 762) Метод возвращает либо полный путь к фалу, либо значение null. Возврат зависит от значения IsPathValid(path) (Приложение А, файл connect.cs, строка 782) с параметром в виде пути. Но этот путь необходимо еще собрать в полный – если весь путь представляет собой строку «/», то возвращается корневая директория. Иначе, если путь лишь начинается с такого символа(Приложение А, файл connect.cs, строка 773), с использованием поля FullName вновь созданного экземпляра класса System.IO.FileInfo в переменную путь записывается полный путь до указанного файла или каталога. Если путь не начинается с символа «/», тогда полный путь получается аналогичным методом за исключением параметра при создании экземпляра класса System.IO.FileInfo: в первом случае это путь, представляемый корневой директорией и строкой параметра path без первого символа(Приложение А, файл connect.cs, строка 775); в другом случае путь представлен комбинацией строки в поле _currentDirectory и строки параметра path. List(pathname) (Приложение А, файл connect.cs, строка 372) Команда указывает серверу передать клиенту список файлов и каталогов в директории, заданной аргументом. При помощи нового экземпляра класса System.IO.DirectoryInfo как и с классом System.IO.FileInfo определяется полный путь до необходимой директории(Приложение А, файл connect.cs, строка 379). Далее, если путь верен необходимо связаться с клиентом для передачи данных. Присутствует реализация для активного режима(Приложение А, файл connect.cs, строка 383), однако этот режим в данном проекте не используется. При пассивном режиме поле _passiveListener, экземпляр класса TcpListener, используется для вызова метода BeginAcceptTcpClient()(Приложение А, файл connect.cs, строка 390) для вызова асинхронной операции DoList. Затем возвращает клиенту ответ-уведомление о том, что соединение пассивным методом удалось. DoList(result) (Приложение А, файл connect.cs, строка 402) Выполняет операцию передачи данных – списка файлов и каталогов. Для начала создается объект TcpClient и записывается в поле _dataClient(Приложение А, файл connect.cs, строка 410). Затем параметр result в формате строки записывается в переменную pathname(Приложение А, файл connect.cs, строка 414), тем самым логически обозначая путь к директории. Создается поток - экземпляр класса NetworkStream – полученный из соединения, а также потоки чтения и записи в переменных _dataReader и _dataWriter(Приложение А, файл connect.cs, строка 420), являющихся экземплярами StreamReader и StreamWriter соответственно. Создается перечислитель directories при помощи метода System.IO.Directory.EnumerateDirectories()(Приложение А, файл connect.cs, строка 423) с параметром в виде пути до директории. Затем организуется перечисление для каждой директории в списке и в строку записывается информация о каждой директории в отдельности. После она передается клиенту(Приложение А, файл connect.cs, строка 437) через соединение данных. Стоит отметить, что текущая реализация позволяет получить только время последней модификации и имя директории. Аналогичные действия проводятся для файлов в директории(Приложение А, файл connect.cs, строка 442), помимо имени и времени модификации можно получить еще и размер файла. Необходимо пояснить формат строки, служащей ответом на команду LIST. Сама строка представляет следующий набор символов «drwxrwxrwx 4 2003 2003 51200 02 9 21:28 home». Формат строки дает более ясное объяснение значениям: «режим связи владелец группа размер дата_время имя». Каждое поле разделено пробелом и обозначает свойство файла или директории: Режим Поле содержит максимум 10 символов. Каждый символ имеет свое значение. В табл.№3 указаны возможные первые символы поля и их значение для объекта в директории. Символ Значение d каталог b специальный блоковый файл c специальный символьный файл символьная ссылка. Либо был задан флаг -N, либо l символьная ссылка не указывала на существующий файл p специальный файл FIFO. s локальный сокет. обычный файл. Следующие девять символов разделены на три группы по три символа. При этом каждый из трех символов в группе обозначает, соответственно, права на чтение, запись и выполнение. Первые три символа задают права доступа для владельца. Следующие три символа задают права доступа для других пользователей группы. Последние три символа задают права доступа для всех остальных пользователей. В табл. №4 указаны возможные символы и права, которые он обозначает Символ Права r чтение w запись (изменение) x выполнение (поиск) права не предоставлены Связи Число связей с объектом. Длина поля - от 3 до 5 символов. Владелец Владелец объекта. Длина поля - от 8 до 10 символов. Это поле содержит имя пользователя или ID владельца. Группа Аналогично полю владельца. Длина поля - от 8 до 10 символов. Содержит имя группы пользователя или ID группы. Размер Размер объекта. Длина поля - от 7 до 10 символов. Если нельзя определить размер объекта, то указывается нулевое значение. Дата_время Строка длиной 12 символов, задающая время изменения. Если изменение объекта произошло за последние 180 дней, то время записывается в формате: MM dd HH:mm(Месяц в цифровом формате, день, часы:минуты), если ранее 180 дней, то MM dd yyyy(Месяц в цифровом формате, день, год) Имя Имя объекта переменной длины, которое заканчивается символом возврата каретки и переноса строки. Причем имя может содержать пробелы. После отправки всех данных о директории клиенту объект TcpClient, открытый в начале метода закрывается и клиенту отправляется уведомление о завершении передачи уже по каналу управления(Приложение А, файл connect.cs, строка 460). CreateDir(pathname) (Приложение А, файл connect.cs, строка 718) Аналогично функции удаления файла, вначале нормализуется имя директории, и, если её не существует, то такая директория будет создана. В другом случае сервер ответит клиенту о невозможности создать такую директорию. Passive()(Приложение А, файл connect.cs, строка 331) Метод позволяет создать соединение в пассивном режиме. Для этого в экземпляр класса System.Net.IPAddress localAddress записывается локальный IP-адрес, полученный из управляющего соединения с клиентом(_controlClient) (Приложение А, файл connect.cs, строка 334). Затем возникает необходимость получить внешний IP-адрес сервера, но возникает трудность – виртуальная машина находится в такой структуре, что не может получить внешний адрес своими средствами. Однако есть всевозможные сервисы в Сети, позволяющие узнать внешний IP-адрес. Один из таких сервисов - http://icanhazip.com, заглавная страница которого содержит лишь адрес, с которого был отправлен запрос к этому сервису. Таким образом, используя экземпляр класса System.Net.WebClient и его метод DownloadString() с параметром в виде адреса нужного сервиса, можно получить необходимый IP-адрес(Приложение А, файл connect.cs, строка 336). Для того, чтобы полученный IP-адрес из строки перевести в нужный массив байт имеет смысл воспользоваться внутренними методами для массива строк – - Split(),Take() и Select()(Приложение А, файл connect.cs, строка 338). Первый возвращает массив, полученный при разделении исходной строки символами (в данном случае ‘.’). Второй возвращает массив из первых нескольких значений массива (в данном случае первых четырех), а третий метод каждый элемент массива конвертирует, в данном случае, в байтовое представление. С помощью полученного массива байт можно получить экземпляр класса IPAddress remoteAddress, содержащий внешний IP машины. Предостерегая незакрытые соединения, с помощью поля _passiveListener вызываются методы класса TcpListener для закрытия соединения в пассивном режиме(Приложение А, файл connect.cs, строка 344). Затем открывается новый «слушатель» в _ passiveListener, использующий локальный IP-адрес и выбранный порт, куда будут приходить данные(Приложение А, файл connect.cs, строка 352). Порт выбирается при помощи метода PortNum(). После запуска «слушателя» необходимо передать клиенту ответ, содержащий адрес и порт в определенном формате: «1ый октет адреса, 2ой октет, 3ий октет, 4ый октет, старший байт порта, младший байт порта». И если октеты адреса известны, то старший и младший байты порта – нет. Для их получения необходимо получить сетевую конечную точку, содержащую IP-адрес и порт(Приложение А, файл connect.cs, строка 356), затем считать из этого объект порт и получить байтовый массив при помощи преобразования полученного порта в 16-битовое(2хбайтное) представление(Приложение А, файл connect.cs, строка 360). И уже элементы этого массива будут являться старшим и младшим байтом порта. В данном проекте реализован метод Port()(Приложение А, файл connect.cs, строка 313) для создания активного соединения, однако на практике он не применяется. В случае же использования сервера на иной платформе с иной сетевой инфраструктурой метод Port() может быть использован. PortNum()(Приложение А, файл connect.cs, строка 849) Метод позволяет выбрать случайно один из пяти портов, которые были предварительно открыты на виртуальном маршрутизаторе: 1024-1028. PrintWorkingDirectory()(Приложение А, файл connect.cs, строка 546) Простой в реализации метод, возвращающий полный путь до текущей директории, естественно в унифицированном формате – где корневая директория заменена на символ «/». Quit()(Приложение А, файл connect.cs, строка 741) Закрывает соединения для передачи данных, если они открыты и обнуляет идентификатор пользователя в поле _userid. Таким образом, следующие за QUIT команды не будут распознаваться сервером, пока пользователь не аутентифицируется. Retrive(pathname) (Приложение А, файл connect.cs, строка 561) Метод действует также как и метод List() и Store() – нормализует имя файла, проверяет его существование и запускает асинхронное выполнение метода DoRetrive(), во время чего сообщает клиенту об успешном подключении в пассивном режиме для передачи данных. DoRetrive(result) (Приложение А, файл connect.cs, строка 588) Отличием от метода DoList() является то, что после получения объекта NetworkStream открывается файловый поток FileStream, используя путь до файла, режим чтения FileMode.Open и режим доступа FileAccess.Read(Приложение А, файл connect.cs, строка 603). Затем выполняется метод CopyStream с параметрами в виде объекта FileStream и NetworkStream. Он копирует поток данных из потока в первом параметре в поток, заданный вторым параметром(Приложение А, файл connect.cs, строка 605). Затем соединение для передачи данных закрывается и сервер сообщает клиенту об успешном завершении. CopyStream(input, output) (Приложение А, файл connect.cs, строка 835) Позволяет, используя тип передачи данных из поля _transferType, выбрать способ копирования потоков – если выбран байтовый тип, тогда используется метод CopyStream() с указанием в виде параметра помимо входа и выхода размер байтового массива для буфера передачи. В другом случае используется метода CopyStreamAscii() с аналогичными параметрами. CopyStream(input, output, bufferSize) (Приложение А, файл connect.cs, строка 790) Предварительно создается новый байтовый массив с размером bufferSize. Затем организуется цикл(Приложение А, файл connect.cs, строка 796), выход из которого состоится тогда, когда из входного потока не будет прочитано ни одного байта (в этом случае считается, что входящий поток данных закончился). Считывание входящего потока идет блоками длиной, равной длине массива и также записывается в исходящий поток(Приложение А, файл connect.cs, строка 798). CopyStreamAscii(input, output, bufferSize) (Приложение А, файл connect.cs, строка 810) Метод отличается от предыдущего созданием потоков ввода/вывода на основе переданных параметрами. Причем при создании потока вывода используется декодирование в ASCII(Приложение А, файл connect.cs, строка 818). RemoveDir(pathname) (Приложение А, файл connect.cs, строка 694) Аналогично методу Delete(), метод нормализует путь и, если директория существует удаляет её, в противном случае возвращает клиенту уведомление о невозможности выполнения операции. Store(pathname) (Приложение А, файл connect.cs, строка 618) Метод действует также как и метод List() и Retrive() – нормализует имя файла, проверяет его существование и запускает асинхронное выполнение метода DoStore(), во время чего сообщает клиенту об успешном подключении в пассивном режиме для передачи данных. DoStore(result) (Приложение А, файл connect.cs, строка 641) Полностью аналогичен методу DoRetrive() за исключением открытия файлового потока: в данном методе файл либо открывается либо создается, а методом доступа становиться FileAccess.Write. Причем при открытии указывается невозможность совместного использования, размер буфера для записи и последовательность доступа к файлу – от начала к концу(Приложение А, файл connect.cs, строка 655). Также отличием является и смена параметров в вызываемом методе CopyStream() – входящим потоком является объект NetworkStream, а исходящим – файловый поток. Type(typeCode, formatControl) (Приложение А, файл connect.cs, строка 271) Метод заполняет поле _transferType, обозначающее тип передачи данных, в данном проекте реализована бинарная передача(I) и текстовая(A). Клиент. Приложение-клиент должно работать на платформе Android и иметь удобный интерфейс и, естественно, соединяться с сервером по запросу пользователя. Разработка под мобильные платформы имеет свою специфику, рассмотрение которой не предполагалось в проекте. Однако стоит отметить сильную зависимость кода от интерфейса, т.к. Java-код приложения представляет собой набор классов и методов, вызываемых действиями пользователя. Таким образом, целесообразно рассмотреть программную часть клиента, учитывая интерфейс приложения. Необходимо пояснить, главный элемент приложения – класс Activity. Он, по сути, является классом, представляющим экран приложения, визуальный интерфейс. Отдельные Activity, которые непосредственно используются в приложении, являются наследниками этого класса. Приложение может иметь одну activity, а может и несколько. Каждая отдельная activity задает отдельное окно для отображения. Код, главного Activity-класса находится в файле MainActivity.java. При запуске приложения создается основной экран, в нем создается три вкладки, именуемые Tab(Приложение Б, файл MainActivity.java, строка 105) с разными заголовками: REMOTE FILES, LOCAL FILES, SETTINGS. Для каждой вкладки формируется т.н. Fragment – фрагменты. Фрагменты могут содержать разную информацию, но находятся в одном Activity. В первом фрагменте, значит на первой вкладке, предполагается разместить список файлов и директорий на удаленной машине, которые будут получены командой List. На втором фрагменте – локальные файлы. А третий фрагмент содержит в себе поля для заполнения и различные кнопки. Помимо фрагментов есть меню опций(Приложение Б, файл MainActivity.java, строка 113) в правом верхнем углу экрана, одна из которых должна выводить на экран журнал подключений. Журнал подключения. Журнал для отображения вызывается опцией Log из меню, при этом создается новый класс Activity(Приложение Б, файл ViewLogActivity.java, строка 28), описанный в файле ViewLogActivity.java. На экране журнал отображается в виде списка, который включает в себя следующее: действия клиента, команды отправленные серверу и ответы от сервера. Весь журнал разделен на две части и обе части хранятся в разных файлах – в одном из них – журнал текущей сессии, который можно очистить из приложения, в другом – журнал всех действий, включающий также время и дату выполнения действия. Фрагмент настроек(Приложение Б, файл MainActivity.java, строка 563). Класс фрагмента настроек имеет название ThirdFragment и имеет три кнопки – Connect, Clear log и Read set. и 4 поля ввода – Server, Port, Login, Password. Стоит сказать, что в главном классе Activity объявлены 4 поля: _server, _port, _user, _password, в эти поля записываются значения из полей ввода соответственно. Кнопка Connect определяет пустые поля на экране и, если таковые имеются, под кнопкой отображается текст, говорящий что поля пусты(Приложение Б, файл MainActivity.java, строка 624). При этом обработка нажатия кнопки заканчивается. В другом же случае, когда все поля заполнены, этими данными заполняются внутренние поля класса, инициализируется поле _currentRemote(Приложение Б, файл MainActivity.java, строка 633), содержащее значение текущего удаленного каталога, и поле _client, являющееся экземпляром класса FtpClass(). Далее вызываются последовательно методы класса FtpClass и сопутствующие им всплывающие уведомления на экране: myDisconnect()(Приложение Б, файл MainActivity.java, строка 639) и myConnect()(Приложение Б, файл MainActivity.java, строка 657). Причем, в зависимости от успеха выполненной операции, всплывающие уведомления содержат различный текст. Помимо этого при успешном выполнении подключения вызывается метод AddSettings()(Приложение Б, файл MainActivity.java, строка 662), сохраняющий введенные данные в текстовый файл. AddSettings()(Приложение Б, файл MainActivity.java, строка 695) Метод определяет директорию примонтированного внешнего хранилища (SD-карты, например), открывает или создает в хранилище папку /FTPFiles/set, причем имя директории FTPFiles хранится в поле _Dir главного класса Activity. В директории создается заново файл с именем LastSettings и в него построчно записываются данные введенные в поля ввода. Кнопка Clear log(Приложение Б, файл MainActivity.java, строка 674) вызывает статический метод класса FtpClass ClearLog(). Он позволяет очистить файл с текущим журналом. Кнопка Read set. (Приложение Б, файл MainActivity.java, строка 679) считывает записанные настройки из файла LastSettings при помощи метода ReadSettings()(Приложение Б, файл MainActivity.java, строка 687) и вписывает их в поля ввода. После этого кнопка Connect может быть использована по назначению. ReadSettings()(Приложение Б, файл MainActivity.java, строка 738) Метод имеет возвращаемым типом массив строк String[], однако для работы используется списковый тип ArrayList<String>. В каждый элемент этого списка записывается считанная строка из файла настроек(Приложение Б, файл MainActivity.java, строка 767). Фрагмент списка локальных файлов. (Приложение Б, файл MainActivity.java, строка 433) SecondFragment содержит лишь список файлов и каталогов на устройстве клиента, однако каждый элемент списка при нажатии на него вызывает диалоговое меню, описание которого есть в файле DialogScreen.java(Приложение Б, файл MainActivity.java, строка 478 и Приложение Б, файл DialogScreen.java, строка 20). Список элементов составляется в методе getFilesLocalList(). Диалоговое окно содержит два пункта – Передать и Удалить – минимальный набор опций для работы с локальными файлами. При выборе одного из пунктов выполняется один из методов класса SecondFragment, но из-за особой архитектуры методы нельзя было сделать статическими, поэтому приходится создавать новый экземпляр класса SecondFragment. Это значит, что методы Upload() и Delete() должны не зависеть от текущего экземпляра класса. getFilesLocalList(cur_dir) (Приложение Б, файл MainActivity.java, строка 486) Метод возвращает список файлов и каталогов в директории указанной в параметре. Файлы располагаются в папке /FTPFiles/download, путь к ней находится также как и при чтении/записи настроек. Список файлов получается стандартными средствами и имена вместе с датой последней модификации сохраняются как элемент возвращаемого списка. Если же ни один файл не будет найден, то в список запишется один элемент в виде строки «Not files» Upload(namefile) (Приложение Б, файл MainActivity.java, строка 528) Вызывает метод из класса FtpClass STOR(), загружающий необходимый файл на сервер. Delete(namefile) (Приложение Б, файл MainActivity.java, строка 538) Находит необходимый файл в текущей локальной папке и удаляет его средствами системы. Фрагмент списка файлов на сервере. (Приложение Б, файл MainActivity.java, строка 230) FirstFragment, как и SecondFragment, содержит список имен файлов и директорий, но уже на удаленной машине. Получение списка происходит с помощью метода getFilesList() при каждом обновлении фрагмента. Помимо этого каждый элемент списка по нажатию на него вызывает диалоговое окно(Приложение Б, файл MainActivity.java, строка 263 и Приложение Б, файл DialogScreen.java, строка 41). В нем содержится три пункта – Получить, Удалить и Создать папку – они реализуют минимальный набор необходимых функций для управления файлами на сервере. Однако, если элемент списка представлен папкой, то можно перейти в неё используя метод ChangeDir()(Приложение Б, файл MainActivity.java, строка 298). Является ли элемент списка директорией проверяется по атрибутам, также получаемым методом getFilesList(), указание на то, файл это или директория, отображается как символ «d» на первом месте строки атрибутов. Подробнее стоит рассмотреть пункты диалогового окна – как и в диалоговом окне фрагмента списка локальных файлов каждый из пунктов вызывает нестатический метод класса FirstFragment и эти методы также не должны зависеть от конкретного экземпляра класса. Важно сказать, что при пунктах Получить и Удалить вызываются методы Download() и Delete() соответственно, но при выборе пункта Создать папку вызывается дополнительное диалоговое окно (Приложение Б, файл DialogScreen.java, строка 58), запрашивающее имя папки и только после получения имени вызывается метод MakeDir(). getFilesList(cur_dir, command) (Приложение Б, файл MainActivity.java, строка 318) Метод имеет такой же возвращаемый тип, что и у аналогичного метода в SecondFragment, но в качестве параметров принимает не только имя нужной директории, но и команду, которую требуется послать. Таким образом при переходе в новую директорию клиент автоматически получает список файлов. Т.к. класс FirstFragment имеет поля типа ArrayList<String> с именами Names, Dates, Sizes, Rights, UIDs, GIDs и Links, то можно утверждать, что они будут заполняться в этом методе при выполнении команды LIST. Предварительно очищенные поля принимают части строки-элемента списка List. Эти части разделены пробелом, а список List заполняется при выполнении нестатического метода LIST класса FtpClass(Приложение Б, файл MainActivity.java, строка 333). При этом, если получен лишь один элемент, то это означает, что получена ссылка на каталог верхнего уровня – такова реализация метода LIST. В соответствии со стандартом заполняются указанные выше поля, а сам вывод строки представляет собой комбинацию из имени и времени модификации объекта. Download(namefile) (Приложение Б, файл MainActivity.java, строка 378) Вызывает метод из класса FtpClass RETR(), загружающий необходимый файл с сервера на устройство клиента. Delete(namefile) (Приложение Б, файл MainActivity.java, строка 390) Вызывает метод из класса FtpClass DELE(), удаляющий файл на сервере. MakeDir(name) (Приложение Б, файл MainActivity.java, строка 401) Вызывает метод из класса FtpClass MKD(), создающий на сервере директорию с указанным именем. ChangeDir(namedir) (Приложение Б, файл MainActivity.java, строка 410) Метод вызывает getFileList() с необходимой командой – если имя директории в параметре совпадает со строкой «/», значит требуется перейти на уровень вверх и команда при вызове списка файлов – «CDUP». В любом другом случае вызов метода идет с командой «CWD». После полю главного класса Activity _currentRemote присваивается имя текущей папки, полученной вызовом метода PWD класса FtpClass(Приложение Б, файл MainActivity.java, строка 424). FtpClass(Приложение Б, файл FtpClass.java, строка 41) FtpClass наследуется от класса главного Activity и описан в файле FtpClass.java. Класс описывает взаимодействие с сервером, а также журналирование событий. Конструктор класса(Приложение Б, файл FtpClass.java, строка 55) заполняет поля _serverFTP, _portFTP, _userFTP и _passwordFTP соответствующими полями из класса-родителя. Помимо этих полей, есть поля содержащие имена файлов журналов и использующиеся для записи ответа от сервера. Методы, несвязанные с соединением. WriteLog(s) (Приложение Б, файл FtpClass.java, строка 65) Метод позволяет записывать строку-параметр в журнал, при этом строка дублируется в два журнала: в полный с именем AllLog и в текущей с именем CurrentLog. Оба журнала хранятся в папке FTPFiles/log и создаются, если обнаружено их отсутствие. Метод записи аналогичен методу записи при сохранении настроек входа. Отличием является то, что в полный журнал идет запись строки, содержащий помимо параметра текущее время системы, имя сервера, к которому выполнено подключение и имя пользователя под которым клиент зашел на сервер(Приложение Б, файл FtpClass.java, строка 74). ReadLog()(Приложение Б, файл FtpClass.java, строка 129) Метод возвращает массив строк, прочитанных из файла текущего журнала. Как и в методе чтения настроек, здесь чтение ведется по строчкам, которые записываются в предварительно созданный список и весь список выводится в качестве массива строк. ClearLog()(Приложение Б, файл FtpClass.java, строка 165) Метод просто удаляет файл текущего журнала, но файл полного журнала приложение удалить не в состоянии, поэтому следует иногда вручную удалять его. Методы, реализующие запросы к серверу. Стоит сказать, что платформа Android не позволяет запускать функции соединения с удаленной машиной в основном потоке, поэтому необходимо вызывать асинхронные методы для таких случаев. Однако при определенной последовательности действий пользователя при выполнении асинхронных операций система может неправильно себя повести – произвести вылет, или неверно интерпретировать команды. При этом большая нагрузка может пойти и на сервер, выполняющий подряд идущие несколько несвязанных команд в рамках одной сессии. Поэтому было принято решение запускать асинхронную операцию и ожидать её окончания, запрещая при этом любые действия в приложении. Пример такого создания следующий: в методе создается экземпляр класса асинхронной задачи, запускается на выполнение методом execute(), а выходные данные задачи получаются методом get(), который не позволяет дальше работать с приложением, пока не будет выполнен. LogReply()(Приложение Б, файл FtpClass.java, строка 176) Записывает в журнал полученный от сервера ответ. myConnect() и класс FtpConnect. (Приложение Б, файл FtpClass.java, строка 181 и 396) Класс реализует соединение с сервером, а метод определяет успешно или нет прошло соединение. В классе реализуется создание соединения, посылка команды «USER» с именем пользователя в виде аргумента и посылка команды «PASS» с паролем пользователя. Каждое действие записывается в журнал, также определяется и ответ сервера после посылки команды «PASS». Если он не будет начинаться с последовательности «230», то считается, что соединение не выполнено. Результатом выполнения операции являются числа 0 и 1, причем 0 возвращается, если соединение прошло успешно. myDisconnect() и класс FtpDisconnect(Приложение Б, файл FtpClass.java, строка 203 и 448) Аналогично методу myConnect() вызывается выполнение кода из класса. Класс отправляет команду «QUIT», если соединение обнаружено. В случае, если соединения нет, то оно устанавливается заново, посылается команды «QUIT», чтобы выйти из сессии на сервера и только после этого соединение закрывается. LIST(directory, command) и класс FtpList(Приложение Б, файл FtpClass.java, строка 225 и 521) Для выполнения асинхронной операции при вызове метода execute() из класса как параметр передается имя директории и команда. В классе же определяется принятая команда – если она соответствует «CDUP», то она передается серверу без аргумента, в противном случае аргументом выступает имя директории. Если соединения нет, то в список строк заносится информация об этом и возвращается. Если же команды были приняты сервером и был получен ответ на них, то посылается команда «PWD», затем выбирается тип передачи командой «TYPE» с аргументом «I». В данном клиенте реализована именно бинарная передача, однако сервер воспринимает и текстовую. После ответа на команду «TYPE» посылается команда включения пассивного режима передачи данных «PASV». Ответ сервера содержит октеты IP-адреса и верхний и нижний байт номера порта, разделенные запятыми. Начиная с 27ой позиции в строке она разбивается на 6 подстрок и они записываются в массив reply_passiv(Приложение Б, файл FtpClass.java, строка 576). При этом определяется порт по формуле: Верхний_байт * 256 + Нижний_байт. Затем составляется IP-адрес и, используя его и полученный номер порта, организуется соединение для передачи данных. Серверу отправляется команда «LIST» и список строк начинает заполняться построчно из входного потока. Кроме того, первым элементом в списке является строка «/», определяющая каталог уровнем выше. CWD(directory) и класс FtpList(Приложение Б, файл FtpClass.java, строка 246) Метод вызывает выполнение асинхронной операции из класса FtpList, предварительно передав команду «CWD» и имя папки назначения. При этом, если пришедший список пуст, то добавляется строка, содержащая один символ «/» - директория уровня выше. CDUP(directory) и класс FtpList(Приложение Б, файл FtpClass.java, строка 267) Аналогично методу CWD() вызывает выполнение операции из FtpList, передавая в качестве команды CDUP. PWD()и класс FtpPwd(Приложение Б, файл FtpClass.java, строка 288 и 1015) Метод возвращает строку, содержащую имя текущей директории. Для этого в классе серверу отправляется команда «PWD», в ответе на которую содержится имя текущей директории. RETR(directoryremote, directorylocal, name) и класс FtpRetr(Приложение Б, файл FtpClass.java, строка 309 и 679) Метод использует класс для получения файла с сервера. Для выполнения операции передается текущие локальная и удаленная директория, а также имя файла. Операция в классе FtpRetr возвращает целочисленное значение – 0, если принятие файла прошло успешно и 1 в другом случае. До начала передачи отправляется команда «CWD» с аргументом в виде удаленной папки, затем «PWD» для проверки. Выбирается бинарный тип передачи отправкой «TYPE» с аргументом «I» и запрашивается включение пассивного режима командой «PASV». Как и в методе класса FtpList налаживаеся соединение для передачи данных и отправляется команда «RETR» с аргументом в виде имени файла. Затем создается новый файл на локальном устройстве и открывается поток для записи, связанный с ним. Чтение входящего потока происходит блоками по 4096 байт, которые поочередно записываются в файл(Приложение Б, файл FtpClass.java, строка 788). После записи потоки закрываются и ответы сервера записываются в журнал. DELE(directory, name) и класс FtpDele(Приложение Б, файл FtpClass.java, строка 331 и 1072) Метод вызывает исполнение операции из класса для удаления файла на сервере. В классе реализована посылка команды «DELE» и аргумента. По коду принятого ответа определяется успешность операции. MKD(directory, name) и класс FtpMkd(Приложение Б, файл FtpClass.java, строка 352 и 1041) Аналогично методу DELE() и классу FtpDele посылается команда серверу «MKD» с аргументом в виде имени создаваемой папки. Ответ, а точнее его код, позволяет определить успешность операции. STOR(directoryremote, directorylocal, name) и класс FtpStor(Приложение Б, файл FtpClass.java, строка 373 и 849) Метод и класс полностью аналогичны методу RETR() и классу FtpRetr, за исключением того, что данные пишут в поток, отправляемый на сервер, а читаются из файла на локальном устройстве.