FTP клиент на C#
192C# и .NET --- Сетевое программирование --- FTP клиент на C#
Представленный в данной статье пример FTP клиента можно загрузить по ссылке - FtpClient.rar.
Обзор FTP
File Transfer Protocol (FTP) — это протокол прикладного уровня, построенный поверх протокола транспортного уровня, обычно поверх TCP. Он используется для копирования файлов с удаленного сервера и на удаленный сервер.
Принцип работы FTP заключается в следующем: сначала открывается соединение TCP с сервером, отправляются текстовые команды для выполнения таких действий, как извлечение файла с сервера, и сервер возвращает трехразрядный код (вместе с сообщением, удобным для восприятия человеком), чтобы показать состояние запрошенного действия.
В FTP используются два разных соединения — управляющее соединение, на котором посылаются команды и получаются ответы сервера, и соединение для данных, используемое для самой передачи файлов с сервера или на сервер. По умолчанию сервер слушает команды от клиента на порту 21, а когда нужно отправлять данные, открывает второе соединение с портом 20 клиента.
Активный и пассивный режимы
Соединение для данных открывается, если только отправлена команда копирования файла. В активном режиме (который установлен по умолчанию) клиент должен слушать соединения. Когда потребуется отправить данные, FTP-сервер откроет соединение с этим сокетом и передаст данные к клиенту.
При таком подходе проблема заключается в том, что большинство конфигураций брандмауэров не позволит извне установить соединения с машинами за брандмауэром, а разрешит только те соединения, которые были инициализированы из-за брандмауэра. В FTP эта проблема решается вводом пассивного режима. В этом случае клиент отправляет команду, указывающую, что используется пассивный режим, и на нее сервер отвечает номером порта, на котором он слушает. Когда нужно отправить данные, вместо того чтобы слушать запрос от сервера, клиент может открыть соединение с указанным портом. В нашей реализации будет использоваться пассивный режим.
FTP-команды
Спецификация FTP определяет несколько команд для аутентификации, копирования файлов на сервер и с сервера и изменения каталога на сервере. В нашем коде не будем пользоваться всеми командами, поэтому рассмотрим только те, которые нам понадобятся:
Команда | Описание |
---|---|
USER <имя пользователя> | Имя пользователя, которое нужно удостоверить на сервере |
PASS <пароль> | Пароль, связанный с именем пользователя |
RETR <имя файла> | Скопировать с сервера указанный файл |
ST0R <имя файла> | Скопировать файл на сервер и сохранить его в указанном месте |
TYPE <индикатор типа> | Формат данных. Может принимать одно значение из следующего перечня: A — ASCII, Е — EBCDIC, I — изображение (двоичные данные), L <размер байта> — локальный размер байта |
PASV | Использовать пассивный режим |
STAT | Принуждает сервер отправить сообщение состояния клиенту. Команда может использоваться в ходе передачи данных, чтобы указать состояние операции |
QUIT | Закрывает соединение с сервером |
Коды состояния FTP
Трехразрядные коды FTP упорядочены в соответствии со степенью детализации, предоставленной разрядом кода: в первом разряде дается общее указание о состоянии команды; второй разряд указывает общий тип возникшей ошибки; в третьем разряде содержится более конкретная информация. Первый разряд может принимать следующие значения:
1 - Положительный предварительный ответ — запрошенное действие инициировано, и, прежде чем клиент сможет послать новую команду, будет отправлен еще один ответ.
2 - Положительный окончательный ответ — запрошенное действие завершено.
3 - Положительный промежуточный ответ — команда принята, но сервер, прежде чем приступить к выполнению, должен получить дополнительную информацию.
4 - Временный отрицательный ответ — команда отвергнута, но ошибка имеет временный характер, и команду можно повторить.
5 - Постоянный отрицательный ответ — команда отвергнута.
Вот некоторые конкретные ответы, которые будут обрабатываться:
Код | Описание |
---|---|
125 | Открыто соединение для данных — начинается передача. |
150 | Готовность к открытию соединения для данных. |
200 | Команда принята |
220 | Обслуживание готово для нового пользователя. |
227 | Вход в пассивный режим. |
230 | Пользователь вошел в систему. |
331 | Имя пользователя принято — отправляйте пароль. |
Кодирование FTP-клиента
Ниже представлен код FTP-клиента, реализованного на WPF с использованием собственной реализации класса FtpWebRequest в виде класса Client:
// Реализация класса FtpWebRequest
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
namespace FtpClient
{
public class Client {
private string password;
private string userName;
private string uri;
private int bufferSize = 1024;
public bool Passive = true;
public bool Binary = true;
public bool EnableSsl = false;
public bool Hash = false;
public Client(string uri, string userName, string password) {
this.uri = uri;
this.userName = userName;
this.password = password;
}
public string ChangeWorkingDirectory(string path) {
uri = combine(uri, path);
return PrintWorkingDirectory();
}
public string DeleteFile(string fileName) {
var request = createRequest(combine(uri, fileName), WebRequestMethods.Ftp.DeleteFile);
return getStatusDescription(request);
}
public string DownloadFile(string source, string dest) {
var request = createRequest(combine(uri, source), WebRequestMethods.Ftp.DownloadFile);
byte[] buffer = new byte[bufferSize];
using (var response = (FtpWebResponse)request.GetResponse()) {
using (var stream = response.GetResponseStream()) {
using (var fs = new FileStream(dest, FileMode.OpenOrCreate)) {
int readCount = stream.Read(buffer, 0, bufferSize);
while (readCount > 0) {
if (Hash)
Console.Write("#");
fs.Write(buffer, 0, readCount);
readCount = stream.Read(buffer, 0, bufferSize);
}
}
}
return response.StatusDescription;
}
}
public DateTime GetDateTimestamp(string fileName) {
var request = createRequest(combine(uri, fileName), WebRequestMethods.Ftp.GetDateTimestamp);
using (var response = (FtpWebResponse)request.GetResponse()) {
return response.LastModified;
}
}
public long GetFileSize(string fileName) {
var request = createRequest(combine(uri, fileName), WebRequestMethods.Ftp.GetFileSize);
using (var response = (FtpWebResponse)request.GetResponse()) {
return response.ContentLength;
}
}
public string[] ListDirectory() {
var list = new List<string>();
var request = createRequest(WebRequestMethods.Ftp.ListDirectory);
using (var response = (FtpWebResponse)request.GetResponse()) {
using (var stream = response.GetResponseStream()) {
using (var reader = new StreamReader(stream, true)) {
while (!reader.EndOfStream) {
list.Add(reader.ReadLine());
}
}
}
}
return list.ToArray();
}
public string[] ListDirectoryDetails() {
var list = new List<string>();
var request = createRequest(WebRequestMethods.Ftp.ListDirectoryDetails);
using (var response = (FtpWebResponse)request.GetResponse()) {
using (var stream = response.GetResponseStream()) {
using (var reader = new StreamReader(stream, true)) {
while (!reader.EndOfStream) {
list.Add(reader.ReadLine());
}
}
}
}
return list.ToArray();
}
public string MakeDirectory(string directoryName) {
var request = createRequest(combine(uri, directoryName), WebRequestMethods.Ftp.MakeDirectory);
return getStatusDescription(request);
}
public string PrintWorkingDirectory() {
var request = createRequest(WebRequestMethods.Ftp.PrintWorkingDirectory);
return getStatusDescription(request);
}
public string RemoveDirectory(string directoryName) {
var request = createRequest(combine(uri, directoryName), WebRequestMethods.Ftp.RemoveDirectory);
return getStatusDescription(request);
}
public string Rename(string currentName, string newName) {
var request = createRequest(combine(uri, currentName), WebRequestMethods.Ftp.Rename);
request.RenameTo = newName;
return getStatusDescription(request);
}
public string UploadFile(string source, string destination) {
var request = createRequest(combine(uri, destination), WebRequestMethods.Ftp.UploadFile);
using (var stream = request.GetRequestStream()) {
using (var fileStream = System.IO.File.Open(source, FileMode.Open)) {
int num;
byte[] buffer = new byte[bufferSize];
while ((num = fileStream.Read(buffer, 0, buffer.Length)) > 0) {
if (Hash)
Console.Write("#");
stream.Write(buffer, 0, num);
}
}
}
return getStatusDescription(request);
}
public string UploadFileWithUniqueName(string source) {
var request = createRequest(WebRequestMethods.Ftp.UploadFileWithUniqueName);
using (var stream = request.GetRequestStream()) {
using (var fileStream = System.IO.File.Open(source, FileMode.Open)) {
int num;
byte[] buffer = new byte[bufferSize];
while ((num = fileStream.Read(buffer, 0, buffer.Length)) > 0) {
if (Hash)
Console.Write("#");
stream.Write(buffer, 0, num);
}
}
}
using (var response = (FtpWebResponse)request.GetResponse()) {
return Path.GetFileName(response.ResponseUri.ToString());
}
}
private FtpWebRequest createRequest(string method) {
return createRequest(uri, method);
}
private FtpWebRequest createRequest(string uri, string method) {
var r = (FtpWebRequest)WebRequest.Create(uri);
r.Credentials = new NetworkCredential(userName, password);
r.Method = method;
r.UseBinary = Binary;
r.EnableSsl = EnableSsl;
r.UsePassive = Passive;
return r;
}
private string getStatusDescription(FtpWebRequest request) {
using (var response = (FtpWebResponse)request.GetResponse()) {
return response.StatusDescription;
}
}
private string combine(string path1, string path2) {
return Path.Combine(path1, path2).Replace("\\", "/");
}
}
}
<!-- Разметка окна -->
<Window x:Class="FtpClient.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="FTP клиент" Style="{DynamicResource MainWindowStyle}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Border BorderThickness="0,0,1,0" BorderBrush="White" Margin="5">
<Grid Width="300">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="0.4*"/>
<ColumnDefinition Width="0.6*"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<TextBlock Text="Адрес сервера" />
<TextBox x:Name="txt_adres" Grid.Column="1" Text="ftp://77.222.61.135/"/>
<TextBlock Text="Логин" Grid.Row="1"/>
<TextBox x:Name="txt_login" Grid.Row="1" Grid.Column="1" Text="MyLogin"/>
<TextBlock Text="Пароль" Grid.Row="2"/>
<PasswordBox x:Name="txt_password" Grid.Row="2" Grid.Column="1" Password="123456"/>
<Button x:Name="btn_connect" Content="Соединиться по FTP" Padding="10" Margin="0,10"
Grid.Row="3" Grid.ColumnSpan="2" Width="180" HorizontalAlignment="Left" Click="btn_connect_Click_1" />
</Grid>
</Border>
<ListView Grid.Column="1" Margin="5" x:Name="lbx_files" ItemsSource="{Binding}">
<ListView.View>
<GridView>
<GridView.Columns>
<GridViewColumn Header="Файл/папка" Width="500">
<GridViewColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Height="36" MouseLeftButtonDown="folder_Click">
<Image Width="32" Height="32" Source="{Binding Type}" />
<TextBlock Foreground="#DDD" Text="{Binding Name}" Margin="12,0" />
</StackPanel>
</DataTemplate>
</GridViewColumn.CellTemplate>
</GridViewColumn>
<GridViewColumn Header="Размер" Width="200" DisplayMemberBinding="{Binding FileSize}"/>
<GridViewColumn Header="Дата создания" Width="200" DisplayMemberBinding="{Binding Date}"/>
</GridView.Columns>
</GridView>
</ListView.View>
</ListView>
</Grid>
</Window>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Net;
using System.IO;
using System.Text.RegularExpressions;
namespace FtpClient
{
public partial class MainWindow : Window
{
string prevAdress = "ftp://";
public MainWindow()
{
InitializeComponent();
}
private void btn_connect_Click_1(object sender, RoutedEventArgs e)
{
try
{
// Создаем объект подключения по FTP
Client client = new Client(txt_adres.Text, txt_login.Text, txt_password.Password);
// Регулярное выражение, которое ищет информацию о папках и файлах
// в строке ответа от сервера
Regex regex = new Regex(@"^([d-])([rwxt-]{3}){3}\s+\d{1,}\s+.*?(\d{1,})\s+(\w+\s+\d{1,2}\s+(?:\d{4})?)(\d{1,2}:\d{2})?\s+(.+?)\s?$",
RegexOptions.Compiled | RegexOptions.Multiline | RegexOptions.IgnoreCase | RegexOptions.IgnorePatternWhitespace);
// Получаем список корневых файлов и папок
// Используется LINQ to Objects и регулярные выражения
List<FileDirectoryInfo> list = client.ListDirectoryDetails()
.Select(s =>
{
Match match = regex.Match(s);
if (match.Length > 5)
{
// Устанавливаем тип, чтобы отличить файл от папки (используется также для установки рисунка)
string type = match.Groups[1].Value == "d" ? "DIR.png" : "FILE.png";
// Размер задаем только для файлов, т.к. для папок возвращается
// размер ярлыка 4кб, а не самой папки
string size = "";
if (type == "FILE.png")
size = (Int32.Parse(match.Groups[3].Value.Trim()) / 1024).ToString() + " кБ";
return new FileDirectoryInfo(size, type, match.Groups[6].Value, match.Groups[4].Value, txt_adres.Text);
}
else return new FileDirectoryInfo();
}).ToList();
// Добавить поле, которое будет возвращать пользователя на директорию выше
list.Add(new FileDirectoryInfo("","DEFAULT.png","...","",txt_adres.Text));
list.Reverse();
// Отобразить список в ListView
lbx_files.DataContext = list;
}
catch (Exception ex)
{
MessageBox.Show(ex.ToString() + ": \n" + ex.Message);
}
}
private void folder_Click(object sender, MouseButtonEventArgs e)
{
if (e.ClickCount >= 2)
{
FileDirectoryInfo fdi = (FileDirectoryInfo)(sender as StackPanel).DataContext;
if (fdi.Type == "DIR.png")
{
prevAdress = fdi.adress;
txt_adres.Text = fdi.adress + fdi.Name + "/";
btn_connect_Click_1(null, null);
}
}
}
}
}
// Вспомогательный класс
public class FileDirectoryInfo
{
string fileSize;
string type;
string name;
string date;
public string adress;
public string FileSize
{
get { return fileSize; }
set { fileSize = value; }
}
public string Type
{
get { return type; }
set { type = value; }
}
public string Name
{
get { return name; }
set { name = value; }
}
public string Date
{
get { return date; }
set { date = value; }
}
public FileDirectoryInfo() { }
public FileDirectoryInfo(string fileSize, string type, string name, string date, string adress)
{
FileSize = fileSize;
Type = type;
Name = name;
Date = date;
this.adress = adress;
}
}
Обратите внимание, что это простая реализация FTP-клиента, которая просто представляет возможность просматривать файлы и папки на сервере, используя графический интерфейс пользователя WPF. При желании можно расширить функционал данного клиента, используя методы класса Client для загрузки/скачивания файлов, изменения директорий и т.д. Используя возможности WPF (тему DarkBlue и шаблон окна), в конечном итоге можно получить программу следующего вида (здесь я использовал подключение к серверу, на котором хоститься данный сайт):