Cliente FTP asíncrono

by Valeriano Tortola 7. octubre 2007 11:33

Este es un (largo) ejemplo de como crear un cliente FTP asíncrono que gestione múltiples descargas de forma paralela. En este modelo, generalmente la lógica de la aplicación suele estar compuesta por métodos estáticos, pero se añade un objeto de estado (objectState) que realiza el seguimiento de la tarea durante las fases de la aplicación, el estado de la tarea puede notificarse mediante eventos por cambio, temporizados ó devolver el mismo objeto de estado para su seguimiento externo.

En este caso, he optado por devolver el propio objeto de estado pero protegido con una interfaz para que solo determinados miembros puedan ser accesibles y solo para lectura, el final de la tarea se indica con un evento que trae el mismo tipo de objeto. Guardar el objeto de estado y monitorizarlo ó simplemente esperar al evento de fin... queda a la libre elección.

La interfaz y el delegado que define al evento:

public interface IFtpDownloadInfo
{
  Int32 ID{get;}
  Boolean Ended{get;}
  Int64 Donwloaded{get;}
  Exception Error{get;}
  Int64 ElapsedMilliseconds{get;}
  Int64 Length{get;}
  String LocalDirectory{get;}
  String RemoteFile{get;}
  String FileName { get;}
}
 
public delegate void FtpEventDlg_(IFtpDownloadInfo e);

La clase que define el objeto de estado cumple esta interfaz, de forma que cuando enviemos el evento, relamente se pasa dicho objeto, pero solo serán accesibles los miembros de dicha interfaz. Esta clase contiene toda la información y campos necesarios para realizar el tracking de la descarga, además implementa el patrón desechable para poder liberar los recursos correctamente una vez haya terminado. El método .ToString ha sido redefinido para poder obtener una información rápida del objeto:

internal class FtpDownloadState:IFtpDownloadInfo,IDisposable
{
  private readonly Int32 id;
  private Int64 length = 0;
  private Int64 downloadedBytes = 0;
  private String localDirectory;
  private String remoteFile;
  private String fileName;
  private Exception error;
 
  public Int32 ID{
    get { return id; }}
  
  public Boolean Ended { 
    get { return (downloadedBytes == length)&&(length!=0);}}
  
  public Int64 Donwloaded { 
    get { return Interlocked.Read(ref downloadedBytes); }
    set { Interlocked.Exchange(ref downloadedBytes,value); }}
  
  public Exception Error{
    get { return error; }
    set { error = value;}}
  
  public Int64 ElapsedMilliseconds{
    get { lock(DownloadStopWatch) 
            return DownloadStopWatch.ElapsedMilliseconds;}}
  
  public Int64 Length{
    get { return length; }
    set { length = value; }}
  
  public String LocalDirectory{
    get { return localDirectory; }}
  
  public String RemoteFile { 
    get { return remoteFile; } }
  
  public String FileName { 
    get { return fileName; } }
 
  private Boolean disposing = false;
  internal Stopwatch DownloadStopWatch = new Stopwatch();
  internal Int32 BufferLength;
  internal NetworkCredential Credential;
  internal FtpWebRequest Ftpcon;
  internal Stream Target;
  internal Stream Source;
  internal Byte[] Buffer;
 
  internal FtpDownloadState(Int32 ID, String LocalDirectory, String RemoteFile, 
                            Int32 BufferLength, String Login, String Password)
  {
    this.localDirectory = LocalDirectory;
    this.remoteFile = RemoteFile;
    this.BufferLength = BufferLength;
    if ((Login!=null)&&(Login.Length>0))
      this.Credential = new NetworkCredential(Login, Password);
    this.fileName = Path.GetFileName(this.RemoteFile);
    this.Buffer = new byte[this.BufferLength];
    this.id = ID;
  }
 
  public override string ToString()
  {
    if (this.error == null)
    {
      return string.Format("{0}: {1}kB/{2}kB en {3}s.", this.fileName,
        this.Donwloaded / 1024, this.Length / 1024,
        this.ElapsedMilliseconds / 1000);
    }
    else
    {
      return string.Format("{0}: {1}", this.fileName, this.error.Message);
    }
  }
 
  private void Dispose(Boolean d)
  {
    if (!disposing)
    {
      disposing = true; 
      this.Target.Dispose();
      this.Source.Dispose();
      GC.SuppressFinalize(this);
    }
  }
 
  public void Dispose()
  {
    Dispose(true);
  }
 
  ~FtpDownloadState()
  {
    Dispose(true);
  }
}

Como se puede ver, esta clase no tiene funcionalidad FTP en si misma, lo que permite poder substituir la entidad ó la lógica de forma separada, incluso trabajar con otros tipos de Streams.

Sobre la sincronización, se protege ElapsedMillisecons y Downloaded porque son dos miembros de 64bits que pueden ser modificados al mismo tiempo que se leen, por lo que hay que asegurar la atomicidad.

Ahora la lógica que realiza todo el proceso de la descarga de FTP. El principio de funcionamiento es el mismo que el ejemplo de la descarga FTP, solo que ahora uso .BeginRead en lugar de .Read.  Lamentablemente, para obtener cual es la longitud total del archivo hay que conectar en modo GetFileSize antes, lo cual complica un poco la lógica, que aún así es bastante simple:

static public class FtpAsyncDownload
{
  // Evento de fin de descarga
  static public event FtpEventDlg_ FtpDownloadEvent;
 
  // Orden de descarga
  // Prepara el objectState y obtiene pide el tamaño.
  static public IFtpDownloadInfo Download(Int32 ID, String LocalDirectory, 
                                          String RemoteFile,Int32 BufferLength, 
                                          String Login, String Password)
  {
    FtpDownloadState ftpdwn = 
      new FtpDownloadState(ID, LocalDirectory, RemoteFile, BufferLength, 
                           Login, Password);
    
    ftpdwn.Ftpcon = (FtpWebRequest)FtpWebRequest.Create(ftpdwn.RemoteFile);
    ftpdwn.Ftpcon.Credentials = ftpdwn.Credential;
    ftpdwn.Ftpcon.KeepAlive = false;
    ftpdwn.Ftpcon.UseBinary = true;
    ftpdwn.Ftpcon.Proxy = null;
    ftpdwn.Ftpcon.EnableSsl = false;
    ftpdwn.Ftpcon.Method = WebRequestMethods.Ftp.GetFileSize;
    ftpdwn.Source = ftpdwn.Ftpcon.GetResponse().GetResponseStream();
    ftpdwn.Source.BeginRead(ftpdwn.Buffer, 0,ftpdwn.BufferLength, 
                            startDownload, ftpdwn);
    return ftpdwn;
  }
 
  // Comienza la descarga
  // Obtiene el tamaño y pide el inicio de 
  // la descarga del archivo.
  static private void startDownload(IAsyncResult ia)
  {
    FtpDownloadState ftpdwn = ia.AsyncState as FtpDownloadState;
    try
    {
      ftpdwn.Source.EndRead(ia);
 
      ftpdwn.Length = ftpdwn.Ftpcon.GetResponse().ContentLength;
      if (ftpdwn.Length <= 0) throw new ArgumentException("FileSize <=0");
 
      ftpdwn.Ftpcon = (FtpWebRequest)FtpWebRequest.Create(ftpdwn.RemoteFile);
      ftpdwn.Ftpcon.Credentials = ftpdwn.Credential;
      ftpdwn.Ftpcon.KeepAlive = false;
      ftpdwn.Ftpcon.UseBinary = true;
      ftpdwn.Ftpcon.Proxy = null;
      ftpdwn.Ftpcon.EnableSsl = false;
      ftpdwn.Ftpcon.Method = WebRequestMethods.Ftp.DownloadFile;
      ftpdwn.Target = new FileStream(Path.Combine(ftpdwn.LocalDirectory, ftpdwn.FileName), 
                                     FileMode.Create, FileAccess.Write, FileShare.None);
      ftpdwn.Source = ftpdwn.Ftpcon.GetResponse().GetResponseStream();
      ftpdwn.DownloadStopWatch.Start();
      ftpdwn.Source.BeginRead(ftpdwn.Buffer, 0, ftpdwn.BufferLength, 
                                    downloadCallback, ftpdwn);
    }
    catch (Exception ex)
    {
      ftpdwn.Error = ex;
      if (FtpDownloadEvent != null)
        FtpDownloadEvent.Invoke(ftpdwn);
    }
  }
 
  // Descarga
  // Función que se ejecuta cada vez que se obtiene
  // un buffer completo para escribir y volver a ejecutarse 
  // hasta que acabe.
  static private void downloadCallback(IAsyncResult ia)
  {
    FtpDownloadState ftpdwn = ia.AsyncState as FtpDownloadState;
    try
    {
      int readed = ftpdwn.Source.EndRead(ia);
      ftpdwn.Target.Write(ftpdwn.Buffer, 0, readed);
      ftpdwn.Donwloaded += readed;
 
      if (!ftpdwn.Ended)
      {
        ftpdwn.Source.BeginRead(ftpdwn.Buffer, 0, ftpdwn.BufferLength,
                                      downloadCallback, ftpdwn);
      }
      else
      {
        try
        {
          if (FtpDownloadEvent != null)
            FtpDownloadEvent.Invoke(ftpdwn);
        }
        catch { }
 
        ftpdwn.DownloadStopWatch.Stop();
        ftpdwn.Dispose();
      }
    }
    catch (Exception ex)
    {
      ftpdwn.Error = ex;
      if (FtpDownloadEvent != null)
        FtpDownloadEvent.Invoke(ftpdwn);
    }
  }
}

Básicamente el funcionamiento consiste en lo siguiente:

  • Se llama al método Download.
  • Este crea un nuevo objeto de estado e invoca asíncronamente la obtención del tamaño del archivo. Indica que al finalizar vaya al método startDownload.
  • Se ejecuta startDownload, obtiene el tamaño de archivo, configura la conexión para realizar la descarga e invoca asíncronamente la descarga del archivo. Indica que al finalizar vaya al método downloadCallback.
  • Cuando haya descargado un buffer completo, se llama a downloadCallback, escribe el resultado en el archivo y si no se ha descargado todo aún se vuelve a invocar asíncronamente la descarga hasta que complete.
  • Cuando acaba, dispara el evento y llama a .Dispose para que libere los Streams del objeto de estado.

Para realizar una descarga, simplemente hay que subscribirse al evento e invocar el método estático Download con los parámetros adecuados:

      FtpAsyncDownload.FtpDownloadEvent += 
          new FtpEventDlg_(FtpAsyncDownload_FtpDownloadEvent);
      
      FtpAsyncDownload.Download(0, Environment.CurrentDirectory, @"ftp://miftp.com/miarchivo.rar", 
                                      8192, "MiLogin", "M1P455W0RD");

Como decía anteriormente, el método Download devuelve un objeto de tipo IFtpDownloadInfo con el que podemos monitorizar el estado de la descarga. Una vez acabe, se disparará el evento si estamos subscritos a él.

Tags: , , , , ,

.NET 2.0 | C# 2.0

Comentarios

07/10/2007 12:39:14 #

pingback

Pingback from elbruno.com

Cliente FTP asincrono - vtortola

elbruno.com |

11/12/2007 19:57:25 #

espinete

Fantástica serie, señor.

Al final de la serie de artículos, pondrá el código completo del ftp ??

Saludos.

espinete |

12/12/2007 16:57:38 #

vtortola

Humm... es que no estoy haciendo ningún cliente FTP, esto es un simple pasatiempos ... como el que hace sudokus... yo hago cosas de este tipo ;)

vtortola España |

Comentarios no permitidos