Eigenständiger FTP-Client zum Herunterladen von Dateien

Ich möchte meine Erfahrungen mit der Entwicklung eines eigenständigen FTP-Clients teilen.

Es gibt einen FTP-Server, auf dem Daten regelmäßig in Form von Grafiken und Textdateien angezeigt werden. Ihre Größe variiert zwischen zehn Kilobyte und ein paar Gigabyte. Der Zugang zum Internet kann über ein Kabel oder über eine GSM-Pfeife oder allgemein über einen Satelliten erfolgen, dh stabil bzw. instabil. Im zweiten Fall steigt die Wahrscheinlichkeit eines Verbindungsverlusts aufgrund der Wetterbedingungen, der geografischen Lage usw. stark an.

Die Kundenanforderungen lauten also wie folgt:

  1. Frage den FTP-Server nach neuen Dateien und deren anschließendem Download ab.
  2. Im Falle eines plötzlichen Stopps des Downloads (unabhängig davon, ob es sich um eine Unterbrechung handelt oder ob das System, auf dem mein FTP-Client installiert ist, abstürzt), sollte der Download so bald wie möglich fortgesetzt werden.
  3. Beschränken Sie die Download-Geschwindigkeit (dies ist auf die Kosten des GSM-Verkehrs zurückzuführen).

Wenn meine Art, das Problem zu lösen, interessant ist, bitte ich um einen Schnitt!

Der Einfachheit halber können Sie den gesamten Artikel mit Codebeispielen und einer detaillierteren Beschreibung der Feinheiten der Arbeit in wichtige Phasen der Arbeit des Kunden unterteilen.

Erklärung des Problems


Nachdem ich nachgedacht hatte, entschied ich mich, einen Client zu schreiben, der nach folgendem Schema arbeitet:

  1. Wir klopfen auf den Server, wir erhalten die Liste der Dateien.
  2. Wir sehen uns unser Download-Protokoll an. Befindet sich die Datei nicht im Protokoll, fügen wir sie der Download-Warteschlange hinzu.
  3. Wenn die Datei aus irgendeinem Grund nicht heruntergeladen werden konnte, wird sie an das Ende der Download-Warteschlange gesendet.
  4. Eine erfolgreich hochgeladene Datei wird dem Verlauf hinzugefügt.

Und einige Features:

  • Der Verlauf wird zur Laufzeit gespeichert und in einer XML-Datei dupliziert. Von dort aus kann der Verlauf wiederhergestellt werden
  • Der Client unterstützt das gleichzeitige Herunterladen mehrerer Dateien in verschiedenen Streams

Regelmäßiges Abrufen des Servers und Abrufen einer Dateiliste


Die Entscheidung, den Server regelmäßig abzufragen, fällt fast sofort ein - einen Timer zu starten, der die Methode zum Abrufen einer Liste von Dateien enthält. Der Server hat jedoch eine etwas eigenartige Verzeichnisstruktur. Kurz gesagt, es gibt zwei Ordner auf dem Server - Benachrichtigungen und Dateien. Der Ordner files enthält die Daten selbst, die heruntergeladen werden müssen, und alle haben eindeutige Namen vom Typ FILE_ID_xxx , wobei x eine beliebige Ziffer ist. Der Benachrichtigungsordner enthält XML-Dateien mit der Beschreibung der Dateien aus dem Ordner files , einschließlich des tatsächlichen Namens, des Datums der Platzierung auf dem Server und der Größe.

Nachdem ich die gesamte XML- Datei aus dem Benachrichtigungsordner gelesen habe, erstelle ich aus einem einfachen FileItem eine Sammlung :

public class FileItem
    {
        [XmlAttribute(AttributeName = "RemoteUri")]
        public string RemoteUri;
        [XmlAttribute(AttributeName = "SavePath")]
        public string SavePath;
        [XmlAttribute(AttributeName = "Date")]
        public string Date;
        [XmlAttribute(AttributeName = "RefId")]
        public string RefId;
        [XmlAttribute(AttributeName = "Name")]
        public string Name;
        [XmlAttribute(AttributeName = "Extention")]
        public string Extention;
        [XmlAttribute(AttributeName = "Size")]
        public long Size;
    }

Anschließend prüfen wir anhand der Sammlung, ob die Datei im Download-Verlauf vorhanden ist und ob sie gerade geladen wird

foreach (var df in dataFiles)
{
    if (!FileHistory.FileExists(df) && !client.AlreadyInProgress(df))
    {
        client.DownloadFile(df);
    }
}

Das ist alles. Die Serverabfrage und Suche nach neuen Dateien ist abgeschlossen. Über wen solche FileHistory und Client - werde ich weiter schreiben.

Laden Sie Dateien in mehrere Streams hoch


Der " Client " im obigen Code ist eine Instanz der FTPClient- Klasse , die nur das Herunterladen von Dateien vom Server behandelt. Und tatsächlich ist FTPClient mein FtpWebRequest-Wrapper.

FTPClient hat eine thread-sichere Warteschlange, die als "Download-Warteschlange" bezeichnet wird:

private ConcurrentQueue downloadQueue;

Was passiert also, wenn Sie die DownloadFile- Methode aufrufen :

public void DownloadFile(FileItem file)
{
    downloadQueue.Enqueue(file);
    StartDownloadTask();
}

Es ist ganz einfach: Die Datei wird der Download-Warteschlange hinzugefügt, und danach wird die Methode aufgerufen, die das Herunterladen der Datei mit TPL veranlasst. So sieht es aus:

private void StartDownloadTask()
        {
            if (currentActiveDownloads <= Settings.MaximumDownloadThreads)
            {
                FileItem file;
                if (!downloadQueue.IsEmpty && downloadQueue.TryDequeue(out file))
                {
                    Task t;
                    if (File.Exists(file.SavePath))
                    {
                        FileInfo info = new FileInfo(file.SavePath);
                        var currentSize = info.Length;
                        t = new Task(() => DownloadTask(file, currentSize));
                    }
                    else
                    {
                        t = new Task(() => DownloadTask(file, 0));
                    }
                    t.ContinueWith(OnTaskComplete);
                    t.Start();
                    Interlocked.Increment(ref currentActiveDownloads);
                    lock (inProgressLock)
                    {
                        inProgress.Add(file);
                    }
                }
            }

Wenn wir Russisch sprechen, überprüfen wir zunächst, wie viele Aufgaben beim Laden von Dateien bereits ausgeführt werden und ob die Möglichkeit besteht, eine weitere zu verschieben. Anschließend versuchen wir, das FileItem aus der Download-Warteschlange abzurufen, wenn die Warteschlange nicht leer ist. Dann stellen wir fest, ob die Datei bereits lokal vorhanden ist oder nicht. Eine Datei ist möglicherweise lokal vorhanden, wenn der Download unerwartet unterbrochen wurde. Alles, was wir herunterladen konnten, bleibt auf der Festplatte. In diesem Fall starten wir den Download einfach an der Stelle, an der wir aufgehört haben. OnTaskComplete-

Methode , die nach Abschluss von DownloadTask aufgerufen wird :

private void OnTaskComplete(Task t)
        {
            Interlocked.Decrement(ref currentActiveDownloads);
            StartDownloadTask();
        }

Das heißt, wir reduzieren den Zähler für aktive Downloads und versuchen, eine neue Download-Aufgabe zu starten. Das heißt, es stellt sich heraus, dass eine neue Download-Aufgabe erstellt wird, wenn eine neue Datei zur Download-Warteschlange hinzugefügt wird und wenn die aktuelle Download-Aufgabe abgeschlossen ist.

Nun die Methode, die die Datei direkt vom Server herunterlädt:

private void DownloadTask(FileItem file, long offset)
        {
            // Перед началом загрузки поставим поток на паузу. В случае, если файл не доступен по какой-то причине, то мы не будем спамить на сервер в попытках достучаться до него
            Thread.Sleep(10 * 1000);
            Log.Info(string.Format("Загружается файл {0}", file.Name));
            try
            {
                if (offset == file.Size)
                {
                    Log.Info(string.Format("Файл {0} уже полностью скачан.", file.Name));
                    FileHistory.AddToDownloadHistory(file);
                    return;
                }
                using (var readStream = GetResponseStreamFromServer(file.RemoteUri, WebRequestMethods.Ftp.DownloadFile, offset))
                {
                    using (var writeStream = new FileStream(file.SavePath, FileMode.Append, FileAccess.Write))
                    {
                        var bufferSize = 1024;
                        var buffer = new byte[bufferSize];
                        int second = 1000;
                        int timePassed = 0;
                        var stopWatch = new Stopwatch();
                        var readCount = readStream.Read(buffer, 0, bufferSize);
                        int downloadedBytes = readCount;
                        while(readCount > 0)
                        {
                            // Считаем данные потока и засечём сколько на это ушло времени
                            stopWatch.Start();
                            writeStream.Write(buffer, 0, readCount);
                            readCount = readStream.Read(buffer, 0, bufferSize);
                            stopWatch.Stop();
                            // Если скорость ограничена (0 считается за отсутствие ограничения)
                            if (Settings.MaximumDownloadSpeed > 0)
                            {
                                var downloadLimit = (Settings.MaximumDownloadSpeed * 1024 / 8) / currentActiveDownloads;
                                downloadedBytes += readCount;
                                timePassed += (int)stopWatch.ElapsedMilliseconds;
                                if (downloadedBytes >= downloadLimit)
                                {
                                    var pause = second - timePassed;
                                    if (pause > 0)
                                        Thread.Sleep(pause);
                                    timePassed = 0;
                                    downloadedBytes = 0;
                                    stopWatch.Reset();
                                }
                                if (timePassed > second)
                                {
                                    stopWatch.Reset();
                                    timePassed = 0;
                                    downloadedBytes = 0;
                                }
                            }
                        }
                    }
                }
                lock (inProgressLock)
                {
                    inProgress.Remove(file);
                }
                FileHistory.AddToDownloadHistory(file);
                Log.Info(string.Format("Файл загружен - {0}", file.Name));
                Interlocked.Add(ref currentLoadedSize, -file.Size);
            }
            catch (WebException e)
            {
                Log.Error(e);
                downloadQueue.Enqueue(file);
            }
            catch (Exception e)
            {
                Log.Error(e);
            }
        }

Und die Methode, die die Anforderung an den Server erstellt und eine Antwort zurückgibt:

private Stream GetResponseStreamFromServer(string uri, string method, long offset)
        {
            var request = (FtpWebRequest)WebRequest.Create(uri);
            request.UseBinary = true;
            request.Credentials = new NetworkCredential(Settings.Login, Settings.Password);
            request.Method = method;
            request.Proxy = null;
            request.KeepAlive = false;
            request.ContentOffset = offset;
            var response = request.GetResponse();
            return response.GetResponseStream();
        }

Das heißt, um den Stream nicht von Anfang an zu lesen, wird beim Generieren der Anforderung eine Zeile verwendet:

request.ContentOffset = offset;

Und das Tempolimit funktioniert folgendermaßen: Zunächst berechnen wir downloadLimit , wie viele Bytes der aktuelle Stream laden kann. Das allgemeine Tempolimit und die Anzahl der aktiven Download-Threads werden berücksichtigt. Dann lesen wir den Stream von 1024 Bytes. Wir haben festgestellt, wie lange es gedauert hat ( timePassed ). Die Gesamtzahl der gelesenen Bytes wird in downloadedBytes geschrieben .

Wenn das Limit überschritten wird, wird der Stream für die verbleibende Zeit bis zum Ende der Sekunde angehalten:

var pause = second - timePassed;
if (pause > 0)
    Thread.Sleep(pause);

Nach einer Sekunde werden die Zähler auf Null zurückgesetzt.

Bei WebExeption wird die Datei erneut zur Download-Warteschlange hinzugefügt. Und die Datei wird erst nach erfolgreichem Abschluss in die Geschichte eingehen.

Verlauf herunterladen


Das Speichern des Download-Verlaufs in einer Datei ist hilfreich, wenn die Anwendung plötzlich neu gestartet wird und der zur Laufzeit gespeicherte Verlauf verloren geht.

In der FileHistory- Klasse befindet sich eine Auflistung, in der das FileItem gespeichert ist , das wir bereits erfolgreich heruntergeladen haben:

private static List downloadHistory; 

Das Hinzufügen einer Datei ist sehr einfach - wir fügen die Datei der Sammlung hinzu und schreiben die Änderungen sofort in xml:

public static void AddToDownloadHistory(FileItem file)
        {
            lock (historyLock)
            {
                XmlSerializer serializer = new XmlSerializer(typeof(List));
                using (var writer = GetXml())
                {
                    downloadHistory.Add(file);
                    serializer.Serialize(writer, downloadHistory);
                }
            }
        }

Und das passiert, wenn wir nach einer Datei im Verlauf suchen wollen:

public static bool FileExists(FileItem file)
        {
            lock (historyLock)
            {
                if (downloadHistory.Count == 0)
                {
                    if (!TryRestoreHistoryFromXml())
                    {
                        return false;
                    }
                }
                return downloadHistory.Any(f => f.RefId == file.RefId);
            }
        }

Lassen Sie mich erklären - die Verifizierungsmethode heißt. Und die Einträge in unserer Sammlung sind Null. Anscheinend stürzte die Anwendung ab und die Geschichte ging verloren. In diesem Fall werden wir versuchen, den Verlauf aus xml wiederherzustellen. Wenn dies fehlschlägt (die Datei fehlt oder ist beschädigt) - wir glauben, dass wir diese Datei noch nicht heruntergeladen haben.

Fertigstellung


Ich hoffe, dass dieser Artikel auch denen hilft, die ihren FTP-Client zum ersten Mal schreiben müssen, wie ich. Ich behaupte nicht, dass die Lösung perfekt ist. Und dies ist meine erste Erfahrung beim Schreiben von Artikeln über Habr, daher bin ich offen für Kritik und Kommentare.

Jetzt auch beliebt: